Coder Social home page Coder Social logo

blog's Introduction

  • 🚀 I use daily: JavaScript TypeScript Git VS Code

  • 💻 I work using: React React Hook React Hook GitHub

blog's People

Contributors

dependabot[bot] avatar rthong avatar

Watchers

 avatar

blog's Issues

Redux原理

起步

首先需求是根据state来进行视图渲染:

const appState = {
	title: {
		text: '第一次title text',
		color: 'red'
	},
	content: {
		text: '第一次content text',
		color: 'blue'
	}
}
// ...
function renderApp(appState) {
	console.log('app')

	renderTitle(appState.title)
	renderContent(appState.content)
}

function renderTitle(title) {
	console.log('title')

	const titleDOM = document.getElementById('title')
	titleDOM.innerHTML = title.text
	titleDOM.style.color = title.color
}

function renderContent(content) {
	console.log('content')

	const contentDOM = document.getElementById('content')
	contentDOM.innerHTML = content.text
	contentDOM.style.color = content.color
}

如果需要在此基础上对视图修改,只需要对appState 进行修改再手动调用渲染就能更新视图上的状态。但是也因此会有很多缺陷,比如说多个其他函数都需要对数据进行操作就会对同一个全局变量进行修改,这样出现问题就会很难定位。

1. 解决外部对同一个state的操作,导致错误很难定位的问题

为了解决这个问题,现在统一规定对state的修改必须调用dispatch函数,它接收一个普通的js对象action,其中type表明想要进行的操作

const dispatch = action => {
	switch (action.type) {
		case 'UPDATE_TITLE_TEXT':
			appState.title.text = action.payload
			break
		case 'UPDATE_TITLE_COLOR':
			appState.title.color = action.payload
			break
		default:
			break
	}
}
// ...
dispatch({ type: 'UPDATE_TITLE_TEXT', payload: '第二次修改的title text' })

这样,当外部函数想要修改state,都需要调用dispatch传入action,出现问题只需要在switch中进行调试。

2. 现在需要将这种模式单独抽离出来给其他项目使用

通过构建createStore函数专门提供dispatch,并且接受一个stateChange专门对state的操作进行描述

const createStore = (state, stateChange) => {
	const dispatch = action => stateChange(state, action)
	return { dispatch }
}
// ...
const stateChange = (state, action) => {
	switch (action.type) {
		case 'UPDATE_TITLE_TEXT':
			state.title.text = action.payload
			break
		case 'UPDATE_TITLE_COLOR':
			state.title.color = action.payload
			break
		default:
			break
	}
}
const store = createStore(appState, stateChange)

这样其他项目只需要通过向createStore传入一个state和一个描述state相关操作的stateChange就也可以使用这样的模式。但有个问题,当dispatch时只是修改了state而并没有渲染数据,所以每次dispatch时都需要手动调用renderApp。我们希望每次dispatch时会自动渲染数据。

3. dispatch时自动渲染数据

我们需要一种方式去“监听”数据的变化,这就用到了观察者模式,通过subscribe将渲染数据的回调保存在listeners数组中,再在dispatch中将listeners数组中的回调依次执行。

const createStore = (state, stateChange) => {
	const listeners = [] // 存储渲染回调
	const getState = () => state // 外部需要获取最新的state传给renderApp
	const dispatch = action => {
		stateChange(state, action)
		listeners.forEach(fn => fn())
	}
	const subscribe = fn => {
		listeners.push(fn)
	}
	return { dispatch, subscribe, getState }
}

这样每次dispatch时都会触发渲染回调,只需要在首次的时候手动调用一次渲染。至此,我们完成了一套dispatch更新数据->渲染数据的自动流程,但是还有一些缺陷,比如只更新state部分字段,但是还是会导致全部数据进行渲染,这样就有很严重的性能损失。

4. 需要每次数据改变返回新的state,以对比前后两次state是否发生改变

为了解决上述问题,就需要对比两次的state的改变,但是之前的变动都是在同一个state上进行修改数值,不能够直接判断。所以我们需要一种方式来判断两次state,为此我们需要在stateChange中返回一份state的拷贝,不变的字段和原state一样而改变的字段就指向一个新的对象。

function renderApp(newState, oldState = {}) {
	if (newState === oldState) return
	console.log('app')

	renderTitle(newState.title, oldState.title)
	renderContent(newState.content, oldState.content)
}

function renderTitle(newTitle, oldTitle = {}) {
	if (newTitle === oldTitle) return
	console.log('title')

	const titleDOM = document.getElementById('title')
	titleDOM.innerHTML = newTitle.text
	titleDOM.style.color = newTitle.color
}

function renderContent(newContent, oldContent = {}) {
	if (newContent === oldContent) return
	console.log('content')

	const contentDOM = document.getElementById('content')
	contentDOM.innerHTML = newContent.text
	contentDOM.style.color = newContent.color
}
//...
const stateChange = (state, action) => {
	switch (action.type) {
		case 'UPDATE_TITLE_TEXT':
			return {
				...state,
				title: {
					...state.title,
					text: action.payload
				}
			}
		case 'UPDATE_TITLE_COLOR':
			return {
				...state,
				title: {
					...state.title,
					color: action.payload
				}
			}
		default:
			break
	}
}
//...
let oldState = getState()
subscribe(() => {
	renderApp(getState(), oldState)
	oldState = getState()
})

这样将两次的state传入渲染函数就能进行判断状态是否改变从而决定是否重新渲染数据

5. 合并state和stateChange

经过之前几步,现在已经有一个很通用的createStore,使用方式是初始化state和描述状态改变的stateChange,并通过createStore生成的store去调用subscribe订阅渲染数据的回调,然后通过dispatch去返回新的state去触发数据渲染。
进一步优化,其实将state和stateChange放在一起,让stateChange既能够初始化state又能够生成新的state。

const stateChange= (state, action) => {
	if (!state) {
		return {
			title: {
				text: '第一次title text',
				color: 'red'
			},
			content: {
				text: '第一次content text',
				color: 'blue'
			}
		}
	}
	switch (action.type) {
		case 'UPDATE_TITLE_TEXT':
			return {
				...state,
				title: {
					...state.title,
					text: action.payload
				}
			}
		case 'UPDATE_TITLE_COLOR':
			return {
				...state,
				title: {
					...state.title,
					color: action.payload
				}
			}
		default:
			break
	}
}

同时现在需要在createStore中将state进行初始化

const createStore = stateChange=> {
	let state = null
	const listeners = [] // 存储渲染回调
	const getState = () => state // 外部需要获取最新的state传给renderApp
	const dispatch = action => {
		state = reducer(state, action)
		listeners.forEach(fn => fn())
	}
	const subscribe = fn => {
		listeners.push(fn)
	}
	dispatch({}) //初始化state
	return { dispatch, subscribe, getState }
}

最后只需要将stateChange这个名称替换成reducer就实现了一个简单的redux模式。

最后

我们遵循一个发现问题->解决问题这样一个流程实现了redux模式:

  1. 外部可以通过随意修改同一个全局变量state的值来达到重新渲染数据,这种模式在出现问题的时候难以处理
  2. 规定必须通过dispatch并传入一个对修改state的描述对象action来统一修改state,于是就将这种模式抽离成一个专门生成dispatch的createStore
  3. 发现每次dispatch仅仅是改变state并没有触发渲染,每次dispatch都需要手动render很麻烦,就使用观察者模式来对数据变动进行监听并在dispatch时候调用渲染回调
  4. 发现每次dispatch都会将所有state都进行重现渲染十分浪费资源,我们就在stateChange中将每次直接修改state转变为返回新的state的拷贝,这样外部能够通过比对两次state来决定是否渲染数据
  5. 进一步优化,让stateChange既能初始化state又能生成新的state,并在createStore中调用依次dispatch进行state初始化

React hooks使用经验

useState

useState接收的值只作为初始渲染时的状态,后续的重新渲染的值都是通过setState去设置

1. 函数式更新

除了常规的setState(value)的方式去更新状态以外,setState还可以接收一个函数来更新状态。这种更新状态的方式通常使用在新的 state 需要通过使用先前的 state 计算得出的场景。

在effect 的依赖频繁变化的场景下有时也可以通过函数式更新状态来解决问题,例如一个每秒中自增状态的场景:

但是依赖项设置后会导致每次改变发生时定时器都被重置,这并不是我们想要的,所以这时就能够通过函数式更新状态并且不引用当前state。

2. 惰性初始 state

一些需要复杂计算的初始状态如果直接将函数运行结果传入useState,会在每次重新渲染时执行所传入的函数,比如:

codesandbox

所以可以向useState中传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用codesandbox

useEffect

useEffect用来完成副作用。

1. 处理副作用与class组件的区别

在class组件中,通常做法是在生命周期中去检查props.A 和 state.B,如果符合某个条件就去触发XXX副作用。而在function组件中,思维模式是我需要处理XXX副作用,它的依赖数据是props.A 和 state.B。从之前的命令式转变到function组件的声明式,开发者不再需要将精力投入到思考不同生命周期判断各种条件再执行副作用这样的事中,而可以将精力聚焦在更高的抽象层次上。

2. 需要将effect中用到的所有组件内的值都要包含在依赖中

React在每次渲染都有这次对应的state、props、事件处理函数和effect,如果设置了错误的依赖就可能会导致副作用函数中所使用到的值并不是最新的。

举个例子,我们来写一个每秒递增的计数器。在Class组件中,我们的直觉是:“开启一次定时器,清除也是一次”。这里有一个例子说明怎么实现它。当我们理所当然地把它用useEffect的方式翻译,直觉上我们会设置依赖为[],因为“我只想运行一次effect”。

然而,这个例子只会递增一次。因为副作用函数只在第一次渲染时候执行,第一次渲染对应的count是0,所以定时器中永远是setCoun(1)。

依赖项是我们给react的提示,告诉react不必每次渲染都去执行effect,只需要在依赖项变动时才去执行对应的effect。错误的依赖项导致effect中拿到的状态的值可能跟上一次effect执行时一样而不是最新的状态。

类似于这样的问题是很难被想到的,所以需要启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则,此规则会在添加错误依赖时发出警告并给出修复建议。

3. 优化依赖项

尽量设置少的依赖项

在effect内部去声明它所需要的函数

比如在某些情况下,组件内函数和effect依赖同一个state

因为doSomething这个函数并没有被使用到多个地方,所以可以将它声明到effect内部去减少依赖项

函数使用useCallback去包裹

和上面一种情况类似,但是doSomething这个函数在多个地方使用

这种情况不方便把一个函数移动到 effect 内部,可以将函数使用useCallback去包裹这个函数

将函数声明到组件外部

如果函数没有使用到组件内的值,可以将函数声明到组件外部以减少依赖项

使用函数式更新状态来减少依赖项

比如在依赖当前状态来更新状态的情况下,可以使用函数式更新状态来减少依赖项,就像上面useState中所举的例子一样。

useRef

1. 使用useRef保存组件中所需要的的唯一实例对象

比如在function组件中去使用rxjs数据流时,需要在组件挂载和销毁时监听和取消监听,如果在组件外去定义subject,全局监听的都是同一个observable

这样在其中任一个组件中next值时,都会被所有观察者接收到。

所以这里需要保持每个组件有自己独立的observable,并且它又不需要作为状态去参与渲染,所以这块使用useRef去保存这个observable。

这样就达到了目的。

2. 保存一些不参与渲染的值

当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染,所以我们可以使用useRef去保存一些不参数渲染的值。

3. 如果需要直接修改useRef的结果,则在泛型参数的类型中包含 | null

在给.current直接赋值时,ts会给出错误提示:

image.png

那么怎么将 current 属性转为 动态可变 的呢,其实在 useRef 的类型定义中已经给出了答案。

image.png

如果需要直接修改useRef的结果,则在泛型参数的类型中包含| null就可以了。

useMemo useCallback

useMemo和useCallback一起作为组件渲染优化的选择而出现,但是它们不能作为性能优化的银弹而去在任何情况下去使用。

我们需要知道这两个api本身也有开销。它们 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo/useCallback 可能会影响程序的性能。

所以要想合理使用 useMemo/useCallback,我们需要搞清楚 它们 适用的场景:

  • 有些计算开销很大,我们就需要「记住」它的返回值,避免每次 render 都去重新计算。
  • 由于值的引用发生变化,导致下游组件重新渲染,我们也需要「记住」这个值。

useReducer

useState的替代模式,在状态之间逻辑复杂时使用useReducer可以将what和how分开,只需要在组件中声明式的dispatch对应的行为,所有行为的具体实现都在reducer中维护,让我们的代码可以像用户的行为一样,更加清晰。

这是一个登录demo, 通过useReducer将登录的相关状态抽离出组件内部,防止组件内部多处去维护这些状态,组件内部只需要通过dispatch行为来完成各种交互。

如果你的页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer

useContext

与useReducer结合使用,代替将回调函数作为参数向下传递的方式,改为共享context中的dispatch

改写之前的登录demo,将登录button改为子组件,通过useContext去拿到dispatch

如果你的页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext

useImperativeHandle

useImperativeHandle 一般与 forwardRef 一起使用。

主要作用与forwarRef基本相同,都是为了将子组件的一些数据暴露给父组件。区别在于只使用forwarRef时只能够对ref进行转发对外暴露dom元素实例,使用useImperativeHandle时能够自定义对外暴露的实例值。

// 子组件
const Bar = React.forwardRef<{}, any>((props, ref) => {
  const [value, setValue] = useState('');

  const inputRef = useRef<HTMLInputElement>(null);

  // 对外暴露出api
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current?.focus();
    },
    blur: () => {
      inputRef.current?.blur();
    },
    changeValue: (val: string) => {
      setValue(val);
    },
    instance: inputRef.current,
  }));

  return (
    <div>
      <Input ref={inputRef} value={value}></Input>
    </div>
  );
});

// 父组件
const Foo = () => {
  const ref = useRef<HTMLInputElement>(null);

  useEffect(() => {
    console.log('【ref】', ref.current);
  }, []);

  return <Bar ref={ref}></Bar>;
};

可以看到父组件中打印结果:

image.png

确实拿到了子组件中暴露出来的一些数据。

父组件可以通过子组件useImperativeHandle暴露出的api来修改子组件的状态

常规父组件控制子组件的行为都是将状态通过props传给子组件然后在父组件中去控制改状态的形式来实现,但是在很少的情况下这种方式满足不了我们的需求,例如在做一些复杂的通过方法调用而不是组建式调用的ui组件时。这时就可以用到父组件可以通过子组件useImperativeHandle暴露出的api来修改子组件的状态的方式来达到目的。

比如说上例中在父组件调用的changeValue方法就能够间接的修改子组件内部的value状态,从而达到控制子组件的效果。

ref.current.changeValue('这是改变后的值');

image.png

不过需要强调的是,这种父组件控制子组件的方式在常规需求中几乎使用不到,应当尽量避免对子组件ref的过度使用。

参考文章:

实现一个简单的react-redux

前言

redux主要目的就是为了解决多处对同一状态修改带来的问题,而反映到react上就是多个层级不同的组件对同一个状态的操作。首先,需要让子组件有方法去访问到统一个状态,在react中刚好context就是做着个事情的,但是如果要进行状态变更的话就需要修改到context里面的状态,这会提高组件间的耦合性。所以我们可以将context和redux结合起来,既能够通过context获取store又能够通过redux来集中处理state。

1. 使用context来让所有子组件能够访问state

首先需要搞清楚如何通过使用context来让子组件获取到其中的状态,

// context.js
export const Context = React.createContext()
// index.js
class Index extends Component {
	state = {
		themeColor: 'red'
	}
	render() {
		return (
                         // 在需要用到状态的组件外部包裹Provider
			<Context.Provider value={this.state}>
				<Header />
				<Content changeColor={e => this.setState({ themeColor: e })} />
			</Context.Provider>
		)
	}
}
class Header extends Component {
	static contextType = Context
	render() {
                // 通过this.context进行访问状态
		return <h1 style={{ color: this.context.themeColor }}>hong</h1>
	}
}

而如果需要直接通过context来修改数据,就需要通过修改顶层的value来重新渲染数据。
至此,我们就了解了context的简单使用模式。

2. 引入redux,结合context达到redux集中数据管理

通过在Connext.Provider中value传入store,使得包裹的子组件能够通过context获取到store,也因此可以调用getState获取最新的state,subscribe来监听dispatch对状态的修改从而通过setState来重新渲染页面。

// index.js
const createStore = reducer => {
	let state = null
	const listeners = [] // 存储渲染回调
	const getState = () => state // 外部需要获取最新的state传给renderApp
	const dispatch = action => {
		state = reducer(state, action)
		listeners.forEach(fn => fn())
	}
	const subscribe = fn => {
		listeners.push(fn)
	}
	dispatch({}) //初始化state
	return { dispatch, subscribe, getState }
}

const reducer = (state, action) => {
	if (!state) {
		return {
			themeColor: 'red'
		}
	}
	switch (action.type) {
		case 'CHANGE_COLOR':
			return {
				...state,
				themeColor: action.payload
			}

		default:
			return state
	}
}
const store = createStore(reducer)

class Index extends Component {
	state = {
		themeColor: 'red'
	}
	render() {
		return (
			<Context.Provider value={store}>
				<Header />
				<Content changeColor={e => this.setState({ themeColor: e })} />
			</Context.Provider>
		)
	}
}
// ...
// 子组件内
class ThemeSwitch extends Component {
	static contextType = Context

	state = {
		themeColor: ''
	}
	componentWillMount() {
		const store = this.context
		this.setState({
			themeColor: store.getState().themeColor
		})
		store.subscribe(() => {
			this.setState({
				themeColor: store.getState().themeColor
			})
		})
	}

	render() {
		return (
			<div>
				<button
					style={{ color: this.state.themeColor }}
					onClick={() => {
						this.context.dispatch({ type: 'CHANGE_COLOR', payload: 'red' })
					}}
				>
					Red
				</button>
				<button
					style={{ color: this.state.themeColor }}
					onClick={() => {
						this.context.dispatch({ type: 'CHANGE_COLOR', payload: 'blue' })
					}}
				>
					Blue
				</button>
			</div>
		)
	}
}

但是这样直接去结合context和redux会使得业务代码和redux相关代码耦合严重,非常不好使用,需要将redux相关和组件解耦。

3. 通过connect封装高阶组件将数据以props形式传给子组件的方式解耦

由于组件大量依赖于context和store,导致其复用性很差,所以需要将redux和context相关从组件中抽离,这就需要使用高阶组件来对原组件进行封装,新组件再和原组件通过props来进行数据传递,保持原组件的pure。
首先是通过connect来进行高阶组件的封装:

// react-redux.js
// 将之前的组件中redux和context相关放入connect,然后将所有state全部以props传给子组件
const Context = React.createContext()
const connect = WrapperComponent => {
	class Connect extends React.Component {
		static contextType = Context
		state = {
			allProps: {}
		}

		componentWillMount() {
			const store = this.context
			this.setState({
				allProps: store.getState()
			})
			store.subscribe(() => {
				this.setState({
					allProps: store.getState()
				})
			})
		}
		_change(e) {
			const { dispatch } = this.context

			dispatch({ type: 'CHANGE_COLOR', payload: e })
		}
		render() {
			return <WrapperComponent {...this.state.allProps} change={e => this._change(e)} />
		}
	}
	return Connect
}
// ...

同时在子组件中通过connect(Component)的形式导出这个高阶组件,而原先index.js中Context.Provider由于Context已经移到react-redux.js中,所以也需要对外部导出一个Provider去接收外部传给context的store

// 因为需要和connect共用一个Context所以封装到一起
const Provider = props => {
	return <Context.Provider value={props.store}>{props.children}</Context.Provider>
}

至此,这个react-redux已经能用,并且外部组件也只是单纯通过props接收state保持了很好的复用性,context和redux相关也已经和组件实现分离。但是现在的组件从高阶组件接收到的总是所有的state,需要通过一种方式来告诉connect接收哪些数据。

4. 通过mapStateToProps和mapDispatchToProps来告诉connect接收的state和如何触发dispatch

在之前的基础上,我们需要知道每个原组件需要获取哪些state。
我们传入一个名为mapStateToprops的函数,它接受最新的state作为参数,返回一个对象作为原组件接受的state的props;同样,我们需要在修改state的组件中,告诉connect我们接受一个怎样dispatch的函数,我们传入一个名为mapDispatchToprops的函数,它接受dispatch作为参数,返回一个对象作为原组件接受的修改state的函数的props:

const mapStateToProps = state => {
	return {
		themeColor: state.themeColor
	}
}

const mapDispatchToProps = dispatch => {
	return {
		change(color) {
			dispatch({ type: 'CHANGE_COLOR', payload: color })
		}
	}
}

// 暂时这样进行调用
export default connect(
	ThemeSwitch,
	mapStateToProps,
	mapDispatchToProps
)

现在,每个组件都能够只获取到本组件使用到的state和修改state的函数。

const connect = (WrapperComponent, mapStateToProps, mapDispatchToProps) => {
	class Connect extends React.Component {
		static contextType = Context
		state = {
			allProps: {}
		}

		componentWillMount() {
			const store = this.context
			this._change(store)

			store.subscribe(() => {
				this._change(store)
			})
		}
                // 需要在初始化和执行dispatch回调时都获取最新的state
		_change(store) {
			const stateProps = mapStateToProps ? mapStateToProps(store.getState()) : {}
			const dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch) : {}
			this.setState({
				allProps: {
					...stateProps,
					...dispatchProps				 
				}
			})
		}
		render() {
                        // 对外部直接传入的props也直接传给原组件
			return <WrapperComponent {...this.state.allProps}  {}...this.props}/>
		}
	}
	return Connect
}

5. 对react-redux进行渲染优化

现在我们的react-redux还存在一些问题,就是当state改变时,Provider包裹的所有子组件都会重新进行渲染,因为mapStateToPropsmaoDispatchToProps每次返回新的对象,再传给原组件时相当于props发生改变,就会引起重新渲染。现在我们要对它进行优化。
暂时我自己优化的方法是通过在Connect组件的shouldComponentUpdate方法中通过判断state的改变来达到优化渲染的目的。
通过在每次调用connect时使用一个变量保存当前使用到的state,在下一次渲染时候在shouldComponentUpdate中对比两次使用到的state是否发生变化来决定是否渲染,同时还需要对外部直接传递的props进行判断是否变化。

const connect = (mapStateToProps, mapDispatchToProps) => {
	let oldState  // 保存当前使用的state
	return WrapperComponent => {
		class Connect extends React.Component {
			static contextType = Context
			state = {
				allProps: {}
			}

			componentWillMount() {
				const store = this.context
				oldState = mapStateToProps(store.getState())
				this._change(store)
				store.subscribe(() => {
					this._change(store)
				})
			}

			shouldComponentUpdate(props) {
				// 判断直接传入的props是否更改
				if (Object.keys(props).some(key => props[key] !== this.props[key])) {
					return true
				}
				const newState = mapStateToProps(this.context.getState())
                                // 判断两次使用的state是否更改
				const flag = Object.keys(oldState).some(key => oldState[key] !== newState[key])
				oldState = newState
				return flag
			}

			_change(store) {
				const stateProps = mapStateToProps ? mapStateToProps(store.getState()) : {}
				const dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch) : {}
				this.setState({
					allProps: {
						...stateProps,
						...dispatchProps
					}
				})
			}
			render() {
				return <WrapperComponent {...this.state.allProps} {...this.props} />
			}
		}
		return Connect
	}
}

这里调用connect和之前不太一样,这里会返回一个函数而不是返回高阶组件,react-redux就是采用这样的方式。其实这里我也不太明白为什么需要这样做。

总结

  1. 通过context可以让Context.Provider子组件都能够访问到同一个数据,通过修改Context.Provider的value可以去重新渲染子组件的数据
  2. 通过将store放到context让所有子组件去通过redux的模式去修改渲染state
  3. 直接在组件中混入redux相关内容导致组件复用性很差,所以将redux相关逻辑放入connect封装的高阶组件中,然后通过props的形式传递state给原组件
  4. 所有组件都会接收全部state,需要通过mapStateToProps来描述接收哪些state;修改state也需要获取到dispatch,通过mapDispatchToProps来获取dispatch进行state修改。
  5. 修改任意state都会导致所有组件重新渲染,原因是mapStateToProps、mapDispatchToProps会返回一个新的对象导致props更新,需要通过在Connect中的shouldComponentUpdate来对两次使用到的state和外部直接传入的props进行对比再决定是否重新渲染

Flutter初尝:实现一个天气app

前言

最近刚接触Flutter,想通过实现一个小项目进行熟悉,于是将之前做过的小程序的天气app改成flutter版。

效果

Screenshot_1565666209
Screenshot_1565666318
Screenshot_1565760915

布局

主要显示布局是以Stack搭建层叠结构,分为最底层的背景层,上一层的天气效果层,最顶层的天气信息和图标。

接口请求和接口数据interface

请求使用的库是flutter中文官网上推荐的Dio,基础用法

Response response =
      await dio.get(baseURL, queryParameters: {'cityIds': '$areaid'});
  return response.data;

返回的是json。在使用接口数据时想要ide进行字段提示,并且在编译阶段能进行字段检测就需要对json进行序列化并申明interface。flutter中的interface较之ts来说定义比较复杂,如果对象结构复杂,需要去申明多个class。

import 'package:json_annotation/json_annotation.dart';

part 'weather.g.dart';

@JsonSerializable()
class WeatherRes {
  final String code;
  final String message;
  final String redirect;

  final List<WeatherInfo> value;

  WeatherRes({this.code, this.message, this.redirect, this.value});

  factory WeatherRes.fromJson(Map<String, dynamic> json) =>
      _$WeatherResFromJson(json);
  Map<String, dynamic> toJson() => _$WeatherResToJson(this);
}
var weatherJson = await getWeather(areaid: this.areaid);
// 获取的json通过WeatherRes.fromJson来与WeatherRes对应起来
WeatherRes weather = new WeatherRes.fromJson(weatherJson);

flutter动画

其中包含几个重要的概念: Animation、AnimationController、Tween

  1. Animation:通过Animation对象来生成一连串动画进行的值,通过addListener中调用setstate进行每次值变化的渲染,通过addStatusListener来对动画status进行监听,animation.value能获取到最新生成的值。
  2. AnimationController:通过AnimationController来对Animation进行控制,指定动画执行时间,开始执行动画等。
  3. Tween:配置动画以生成不同的范围或数据类型的值。
    更多动画详细内容
// 项目中风车无限转动的动画
class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  static final _rotateTween = new Tween<double>(begin: 0.0, end: math.pi * 2);

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) => Center(
        child: AnimatedBuilder(
            animation: animation,
            builder: (context, child) => Transform.rotate(
                angle: _rotateTween.evaluate(animation), child: child),
            child: child),
      );
}

class AnimationWidget extends StatefulWidget {
  Widget animationChild;
  AnimationWidget({this.animationChild});
  _AppState createState() => _AppState();
}

class _AppState extends State<AnimationWidget>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 5), vsync: this);
    // animation = Tween<double>(begin: 0.0, end: math.pi * 2).animate(controller);
    // animation =
    animation = new CurvedAnimation(parent: controller, curve: Curves.linear);
    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        controller.repeat();
      }
    });
    controller.forward();
  }
  // #enddocregion print-state

  @override
  Widget build(BuildContext context) => GrowTransition(
        child: widget.animationChild,
        animation: animation,
      );

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  // #docregion print-state
}

下拉刷新

flutter自带的RefreshIndicator widget就能实现下拉刷新,但是文档中说的都是需要RefreshIndicator的child必须是可滚动的widget,我的需求仅仅是下滑手势就进行刷新,并不会有widget被滚动。
这就需要对RefreshIndicator 的key进行标记,再通过this._refreshIndicatorKey.currentState.show()来手动显示刷新视图并调用onRefresh回调。

new RefreshIndicator(
        key: _refreshIndicatorKey,
        onRefresh: this._refresh,
        displacement: 100,
        child: ConstrainedBox(
            constraints: BoxConstraints.expand(),
            child: GestureDetector(
                onVerticalDragStart: (DragStartDetails e) {
                  //打印手指按下的位置(相对于屏幕)
                  // print("用户手指按下:${e.globalPosition}");
                },
                //手指滑动时会触发此回调
                onVerticalDragUpdate: (DragUpdateDetails e) {
                  // print('正在滑动');
                },
                onVerticalDragEnd: (DragEndDetails e) {
                  // print(e.velocity);
                  //打印滑动结束时在x、y轴上的速度
                  //竖直速度大于0表示向下滑动就显示refreshIndicator,并获取数据
                  if (e.velocity.pixelsPerSecond.dy > 0) {
                    this._refreshIndicatorKey.currentState.show();
                  }
                },
// ...

超出屏幕的widget

flutter中普通widget超出屏幕就会直接报错,我暂时是通过OverflowBox指定maxWidth的值来让子widget超出屏幕

// 多云的云层滚动效果
OverflowBox(
                maxWidth: 1200,
                child: CloudAnimationWidget(
                    animationChild:
                        //  需要两张图片无限滚动的效果
                        Row(
                  children: <Widget>[
                    Container(
                      width: 600,
                      height: 200,
                      child: Image.asset(
                        'images/cloud.png',
                        // width: MediaQuery.of(context).size.width,
                        // fit: BoxFit.cover,
                      ),
                    ),
                    Container(
                      width: 600,
                      height: 200,
                      child: Image.asset(
                        'images/cloud.png',
                        // width: MediaQuery.of(context).size.width,
                        // fit: BoxFit.cover,
                      ),
                    ),
                  ],
                )));

高德定位

flutter中地位需要引入amap_location,同时还需要通过simple_permissions来获取手机定位权限。

void _checkPersmission() async {
    // 获取定位权限
    bool hasPermission =
        await SimplePermissions.checkPermission(Permission.WhenInUseLocation);
    if (!hasPermission) {
      PermissionStatus requestPermissionResult =
          await SimplePermissions.requestPermission(
              Permission.WhenInUseLocation);
      if (requestPermissionResult == PermissionStatus.denied) {
        Alert(context: context, title: "申请定位权限失败", desc: "Flutter is awesome.")
            .show();

        return;
      }
    }

    // 直接获取定位信息
    AMapLocation loc = await AMapLocationClient.getLocation(true);
    print(loc.district);
    City res = _getCity(loc.province, loc.city, loc.district);
    if (res is City) {
      widget.onCityChange(res.areaid);
      setState(() {
        this.cityName = res.countyname;
      });
    }
  }

下雨下雪效果

目标是生成20个雨滴动画,在每个雨滴动画结束时改变雨滴坐标和动画时间重新生成新的动画再销毁之前的动画。
先是尝试在监听动画回调中去将completed状态的动画重新设置参数赋值给animation和controller,试过之后发现有问题。
之后又继续尝试将一些动画参数作为状态,在completed的时候调用controller.repeat()并setstate来改变动画参数。但是发现,动画只会在第一次结束触发completed状态,之后repeat的动画并不会触发addStatusListener的回调。
于是想了一下让每个雨滴动画结束时调用外部传入的回调,然后在原来的雨滴动画List中替换掉已结束的动画widget,其中每个动画widget需要生成一个唯一的key让flutter来进行更新检测,这样20个雨滴动画就只会更新已结束的动画。

animation.addStatusListener((state) {
      if (state == AnimationStatus.completed) {
        // 执行外部结束动画回调
        widget.onComplete();
      }
    });
    controller.forward();
RainAni _createRainAni(int index) {
    return RainAni(
        width: widget.width,
        height: widget.height,
        // 随机生成key来标记每个动画
        key: ValueKey(math.Random().nextDouble()),
        isInit: this.isInit,
        aniMode: widget.aniMode,
        // 外部传入的结束动画回调
        onComplete: () {
          this.isInit = false;
          setState(() {
            // 替换动画数组中的已结束项
            this.rainList[index] = _createRainAni(index);
          });
        });
  }

结尾

整个完成下来感觉在体验方面确实十分流畅,当然也与项目简单有关,但是遇到不少坑:

  1. 容器widget的width和height经常无效并且会充满屏幕,需要在外面包裹一个布局类的widget
  2. 需要人为的将widget细分一下,不然widget嵌套会十分严重
  3. 定义interface比ts要复杂很多,Map对应的js的对象感觉用起来也很奇怪
  4. 调试样式也很费劲,不能像浏览器直接F12就能看到每个元素的样式

Antd Pro menu菜单使用自定义icon

在日常开发中,menu icon主要由三种方式:

  1. 使用antd自带的icon

  2. 使用iconfont中的icon

  3. 使用自定义的svg

在antd pro的文档中描述了前两中方式,但是使用自定义svg的方式并没有在文档中出现。

我们知道antd pro中的菜单是在pro-layout的基础上又封装了一层,将很多配置项注入到pro-layout中。

现在需要将自定义的icon传给pro-layout,但是由于umi的影响,又不能直接在route配置中直接写组件。所以需要在antd-pro的入口文件中,将route中的icon手动转换成pro-layout MenuDataItem中icon所接收的ReactNode。

pro-layout中menu的类型

接下来就是调用pro-layout中的api去修改菜单数据MenuData,具体来说有两个api都能够修改,分别是menuDataRenderpostMenuData

image.png

image.png

这两个api据官方文档描述,在不需要国际化的情况下,使用postMenuData修改数据能显著提升性能

Webpack配置环境变量

在实际项目开发中,需要基于多种环境去设置不同的环境变量以便于在构建阶段或是运行阶段去使用,例如常见的通过process.env.NODE_ENV在构建时去判断当前的构建环境是development还是production,例如需要在开发环境测试环境生产环境去访问不同的接口服务器。

为了模拟真实的项目,使用webpack搭建了一个最小化的项目结构:

├─package.json
├─src
|  └index.jsx
├─public
|   └index.html
├─config
|   └webpack.config.js

Node环境变量

Node环境变量就是指process.env这个属性

process.env 是什么?

它是 Nodejs 应用程序中,process.env 属性,返回包含用户环境的对象,所以它不可以在客户端侧代码中使用,也就不能在浏览器环境上使用。

// process.env(官方示例)
{
  TERM: 'xterm-256color',
  SHELL: '/usr/local/bin/bash',
  USER: 'nodejscn',
  PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
  PWD: '/Users/nodejscn',
  EDITOR: 'vim',
  SHLVL: '1',
  HOME: '/Users/nodejscn',
  LOGNAME: 'nodejscn',
  _: '/usr/local/bin/node'
}

修改Node环境变量

1. 修改 package.json 文件

使用 cross-env依赖包,支持跨平台配置环境变量。

// package.json
{
  ...,
  "scripts": {
    "start": "npm run dev",
    "dev": "cross-env NODE_ENV=development AAA=123 webpack serve --config ./config/webpack.config.js",
    "build:test": "cross-env NODE_ENV=test  webpack --config ./config/webpack.config.js",
    "build:pro": "cross-env NODE_ENV=production  webpack --config ./config/webpack.config.js"
  },
  ...
}

通过在package.json脚本中设置变量的方式来注入环境变量,同时cross-env还支持去设置多个环境变量,只需要通过空格区分,例如在dev脚本中设置的NODE_ENV=developmentAAA=123

这样在执行npm start就能够通过process.env获取到对应的环境变量。

// webpack.config.js
console.log("【process.env】", process.env.AAA);

能够在构建时的终端中打印出

但是在index.jsx中也就是浏览器环境下的文件中打印process.env就会报错

原因就是前文提到的peocess.env是Node环境的属性,浏览器环境不能够获取到。让浏览器环境获取到所需变量我们后文再说。

2. 通过.env文件注入

直接通过在script脚本中注入环境变量的方式不利于集中管理环境变量,而且在环境变量较多时这种方式也十分不友好,所以需要一种方式来集中管理这些环境变量。

使用dotenv依赖包可将环境变量从 .env 文件加载到 process.env

dotenv会默认加载根目录的.env文件去注入环境变量,通过require('dotenv').config()即可完成注入。

//webpack.config.js
dotenv.config();

//.env文件
AAA=123

同样也能够在终端中看到

dotenv多环境配置

在多环境配置时需要通过规定不同环境对应的.env文件,例如现在规定.env.test是测试环境对应的环境变量,.env.production是生产环境,.env是开发环境。然后通过dotenv.config({ path: })去加载对应文件的环境变量。

//webpack.config.js
const PROJECT_PATH = resolve(__dirname, "../");

const dotenvFile = resolve(PROJECT_PATH, `./.env.${process.env.NODE_ENV}`);

// 加载.env*文件  默认加载.env文件
dotenv.config({
  path: fs.existsSync(dotenvFile)
    ? dotenvFile
    : resolve(PROJECT_PATH, `./.env`),
});

console.log("【process.env】", process.env.ENV);

这里process.env.NODE_ENV是为了判断当前的运行环境来去加载对应的.env文件。

// package.json
"scripts": {
  "start": "npm run dev",
  "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.config.js",
  "build:test": "cross-env NODE_ENV=test  webpack --config ./config/webpack.config.js",
  "build:pro": "cross-env NODE_ENV=production  webpack --config ./config/webpack.config.js"
}

//.env.production
ENV=pro

//.env
ENV=dev

执行npm start

执行npm run build:pro

可以看到不同环境的变量确实已经注入成功。

浏览器环境变量

浏览器环境下也需要根据不同的环境变量来处理一些逻辑,但是它不能获取到process.env所以不能像注入Node环境变量的方式来实现。浏览器环境变量是基于webpack.DefinePlugin这个插件在项目构建时引入的,引入之后可以在前端代码中全局获取到对应的变量。

基础使用方式是将所需的变量按键值对的方式传入DefinePlugin中,需要注意的是变量值需要通过JSON.stringify进行包裹。

module.exports = {
  plugins: [
    new DefinePlugin({
        aaa: JSON.stringify("!!!!")
    })
  ]
}

执行脚本后可以在index.jsx中得到对应的结果

//index.jsx
console.log("【app】", aaa);
const App = () => {
  return <div>app</div>;
};

配置多环境的浏览器变量

浏览器环境中也需要根据项目环境来引入不同的变量,之前在Node环境中已经获取到不同的环境变量,我们可以建立一个基于此的映射表在项目构建时根据拿到的Node环境变量来引入对应的浏览器环境变量。

// webpack.config.js

// 浏览器环境注入的变量
const define = {
  dev: {
    baseURL: "/api/dev",
  },
  test: {
    baseURL: "/api/test",
  },
  pro: {
    baseURL: "/api/pro",
  },
};

module.exports = {
  new DefinePlugin({
    "process.env": Object.keys(define[process.env.ENV]).reduce((env, key) => {
      env[key] = JSON.stringify(define[process.env.ENV][key]);
      return env;
    }, {}),
  }),
}

执行npm start,可以在浏览器控制台看到结果

执行npm run build:pro,在dist目录开启一个服务器,可以在浏览器控制台看到结果

在实际项目中根据不同环境切换接口服务器地址的场景中,就能通过这样的方式来获取到不同环境中的接口地址。

在UMI中去配置多环境变量

在平时开发中较常使用umi作为项目框架,umi通过环境变量UMI_ENV区分不同环境来指定不同配置。

具体来说是通过脚本中注入的UMI_ENV=xxx去匹配对应的config.xxx.js配置文件,然后在define属性中去配置需要引入浏览器环境的变量。

具体相关内容可以看官方文档多环境配置
代码中可用的变量

// webpack.config.js
module.exports = {
  ...,
  "scripts": {
    "start": "cross-env UMI_ENV=dev umi dev",
    "build:test": "cross-env UMI_ENV=test umi build",
    "build:pre": "cross-env UMI_ENV=pre umi build",
    "build:pro": "cross-env UMI_ENV=pro umi build",
  }
  ...,
}

//config.dev.js
import { defineConfig } from 'umi';

export default defineConfig({
  define: {
    'process.env': {
      BASE_API: '/api/dev',
    },
  },
});

Function组件(Hooks) vs Class组件

useState

useState接收的值只作为初始渲染时的状态,后续的重新渲染的值都是通过setState去设置

1. 函数式更新

除了常规的setState(value)的方式去更新状态以外,setState还可以接收一个函数来更新状态。这种更新状态的方式通常使用在新的 state 需要通过使用先前的 state 计算得出的场景。

在effect 的依赖频繁变化的场景下有时也可以通过函数式更新状态来解决问题,例如一个每秒中自增状态的场景:

但是依赖项设置后会导致每次改变发生时定时器都被重置,这并不是我们想要的,所以这时就能够通过函数式更新状态并且不引用当前state。

2. 惰性初始 state

一些需要复杂计算的初始状态如果直接将函数运行结果传入useState,会在每次重新渲染时执行所传入的函数,比如:

codesandbox

所以可以向useState中传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用codesandbox

useEffect

useEffect用来完成副作用。

1. 处理副作用与class组件的区别

在class组件中,通常做法是在生命周期中去检查props.A 和 state.B,如果符合某个条件就去触发XXX副作用。而在function组件中,思维模式是我需要处理XXX副作用,它的依赖数据是props.A 和 state.B。从之前的命令式转变到function组件的声明式,开发者不再需要将精力投入到思考不同生命周期判断各种条件再执行副作用这样的事中,而可以将精力聚焦在更高的抽象层次上。

2. 需要将effect中用到的所有组件内的值都要包含在依赖中

React在每次渲染都有这次对应的state、props、事件处理函数和effect,如果设置了错误的依赖就可能会导致副作用函数中所使用到的值并不是最新的。

举个例子,我们来写一个每秒递增的计数器。在Class组件中,我们的直觉是:“开启一次定时器,清除也是一次”。这里有一个例子说明怎么实现它。当我们理所当然地把它用useEffect的方式翻译,直觉上我们会设置依赖为[],因为“我只想运行一次effect”。

然而,这个例子只会递增一次。因为副作用函数只在第一次渲染时候执行,第一次渲染对应的count是0,所以定时器中永远是setCoun(1)。

依赖项是我们给react的提示,告诉react不必每次渲染都去执行effect,只需要在依赖项变动时才去执行对应的effect。错误的依赖项导致effect中拿到的状态的值可能跟上一次effect执行时一样而不是最新的状态。

类似于这样的问题是很难被想到的,所以需要启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则,此规则会在添加错误依赖时发出警告并给出修复建议。

3. 优化依赖项

尽量设置少的依赖项

在effect内部去声明它所需要的函数

比如在某些情况下,组件内函数和effect依赖同一个state

因为doSomething这个函数并没有被使用到多个地方,所以可以将它声明到effect内部去减少依赖项

函数使用useCallback去包裹

和上面一种情况类似,但是doSomething这个函数在多个地方使用

这种情况不方便把一个函数移动到 effect 内部,可以将函数使用useCallback去包裹这个函数

将函数声明到组件外部

如果函数没有使用到组件内的值,可以将函数声明到组件外部以减少依赖项

使用函数式更新状态来减少依赖项

比如在依赖当前状态来更新状态的情况下,可以使用函数式更新状态来减少依赖项,就像上面useState中所举的例子一样。

useRef

1. 使用useRef保存组件中所需要的的唯一实例对象

比如在function组件中去使用rxjs数据流时,需要在组件挂载和销毁时监听和取消监听,如果在组件外去定义subject,全局监听的都是同一个observable

这样在其中任一个组件中next值时,都会被所有观察者接收到。

所以这里需要保持每个组件有自己独立的observable,并且它又不需要作为状态去参与渲染,所以这块使用useRef去保存这个observable。

这样就达到了目的。

2. 保存一些不参与渲染的值

当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染,所以我们可以使用useRef去保存一些不参数渲染的值。

useMemo useCallback

useMemo和useCallback一起作为组件渲染优化的选择而出现,但是它们不能作为性能优化的银弹而去在任何情况下去使用。

我们需要知道这两个api本身也有开销。它们 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo/useCallback 可能会影响程序的性能。

所以要想合理使用 useMemo/useCallback,我们需要搞清楚 它们 适用的场景:

  • 有些计算开销很大,我们就需要「记住」它的返回值,避免每次 render 都去重新计算。
  • 由于值的引用发生变化,导致下游组件重新渲染,我们也需要「记住」这个值。

useReducer

useState的替代模式,在状态之间逻辑复杂时使用useReducer可以将what和how分开,只需要在组件中声明式的dispatch对应的行为,所有行为的具体实现都在reducer中维护,让我们的代码可以像用户的行为一样,更加清晰。

这是一个登录demo, 通过useReducer将登录的相关状态抽离出组件内部,防止组件内部多处去维护这些状态,组件内部只需要通过dispatch行为来完成各种交互。

如果你的页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer

useContext

与useReducer结合使用,代替将回调函数作为参数向下传递的方式,改为共享context中的dispatch

改写之前的登录demo,将登录button改为子组件,通过useContext去拿到dispatch

如果你的页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext

参考文章:

Babel 7

平时在开发的过程中,我们可能并不太需要十分了解babel的内容,仅仅知道它能够将新特性的代码转换成能够在旧版本浏览器中运行的代码。但是这一次想要趁着自己搭建脚手架的机会去进一步的了解babel的知识,所以写了这篇文章。以下内容是babel 7.4之后的版本,也就是@babel/polyfill被废弃需要独立安装core-jsregenerator-runtime 模块的版本。

babel命令行工具 @babel/cli

@babel/cli是babel的命令行工具,主要提供babel命令。另外还需要安装@babel/core才能使用babel去编译。

npm install --save-dev @babel/core @babel/cli

将命令配置在 package.json 文件的 scripts 字段中:

// package.json
"scripts": {
    "compiler": "babel src --out-dir lib --watch"
}

这样就能够通过npm run compiler来执行编译,但是babel本身什么都不做,需要添加插件来帮助babel完成工作。

plugin

babel所有功能都建立在各种的plugin上,使用方式是安装相应的plugin再去配置文件中去使用。例如箭头函数转换插件,
安装@babel/plugin-transform-arrow-functions,然后在.babelrc配置文件中去指定对应的插件

//.babelrc
{
  plugins: ["@babel/plugin-transform-arrow-functions"],
};

然后执行npm run compiler,可以看到箭头函数已经被编译完成

image.png

image.png

但是如果我们每个功能都去一个个添加对应的plugin会很麻烦,多以我们就需要preset预设去直接添加一组插件。

preset

preset就是一组插件的集合,最常用的preset就是@babel/preset-env

@babel/preset-env

它的作用是根据目标环境去进行语法转换和导入对应的polyfill

需要注意的是,@babel/preset-env会根据你配置的目标环境,生成插件列表来编译。默认情况下,如果你没有在 Babel 配置文件中(如 .babelrc)设置 targets 或 ignoreBrowserslistConfig,@babel/preset-env 会使用 package.jsonbrowserslist 配置源。

我们可以模拟生产环境和开发环境的浏览器版本

const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];

通过设置不同浏览器环境使用@babel/preset-env去编译相同代码,可以看到最终的结果也会不同。

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        // targets: product,
        targets: development,
      },
    ],
  ],
};

babel 只负责对语法进行编译,比如当我们写箭头函数,babel 会帮你把它编译成普通函数。但是对一些新的扩展方法,新的类来说babel就不能转换了。这时就需要去引入polyfillpolyfill的中文意思是垫片,所谓垫片就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。

polyfill

babel v7.4版之后,需要直接安装core-jsregenerator-runtime去替代之前的@babel/polyfillcroe-js 提供了 ES5、ES6 规范中新定义的各种对象、方法的polyfill,regenerator-runtime 用来实现 ES6/ES7 中 generators、yield、async 及 await 等相关的 polyfill。

首先,我们需要安装他们到生产环境中,因为需要在生产环境中运行其中的polyfill

npm install --save core-js regenerator-runtime

@babel/preset-env的配置项中把useBuiltIns设置成usage,这样会根据目标浏览器环境去引入所需要的polyfill。需要注意点是,设置useBuiltIns还需要同时设置corejs

//.babelrc
const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: product,
        useBuiltIns: "usage",
        corejs: 3,
      },
    ],
  ],
};
//index.js
const isHas = [1, 2, 3].includes(2);

const getData = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(100);
    }, 1000);
  });

const main = async () => {
  const res = await getData();
  console.log(res);
};

main();

编译后的文件:

可以看到,编译后的文件中只引入了所用到的polyfill。

useBuiltIns还可以设置成其他值,比如entry,这需要在项目入口文件手动引入polyfills,例如@babel/polyfill或者core-js

//.babelrc
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: product,
        useBuiltIns: "entry",
        corejs: 3,
      },
    ],
  ],
};



//index.js
// 入口文件引入core-js
require("core-js");

但是这种方式会引入全量的polyfill。

useBuiltIns默认值为false,代表每个文件里不自动添加polyfill,或不将import "@babel/polyfill"转换为单独的polyfill。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime可以重复使用 Babel 注入的帮助程序

在使用@babel/preset-env配合useBuiltIns: usage时,文件中会引入一些辅助方法例如_classCallCheck,当多处文件都使用到class时同样也会在每个文件中去引入这些辅助方法,这样会增大打包体积并且完全没有必要多次去引入同样的辅助方法。

//index.js
class A {}


//.babelrc.js
const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: product,
        useBuiltIns: "usage",
        corejs: 3,
      },
    ],
  ],
};

编译结果:

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var A = function A() {
  _classCallCheck(this, A);
};

为了解决这个问题就需要使用@babel/plugin-transform-runtime,使用该插件,所有辅助方法都将引用模块 @babel/runtime,这样就可以避免编译后的代码中出现重复的辅助方法,有效减少包体积。

@babel/plugin-transform-runtime需要配合@babel/runtime来使用,@babel/plugin-transform-runtime在开发时使用,最终代码需要依赖@babel/runtime

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
//index.js
class A {}

//.babelrc.js
const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: product,
        useBuiltIns: "usage",
        corejs: 3,
      },
    ],
  ],
  //使用@babel/plugin-transform-runtime
  plugins: [["@babel/plugin-transform-runtime"]],
};

编译结果:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var A = function A() {
  (0, _classCallCheck2.default)(this, A);
};

可以看到这些辅助方法都是从@babel/runtime中引入。

@babel/plugin-transform-runtime可以创建一个沙盒环境来避免对全局环境的污染

之前在使用@babel/preset-env编译promise和includes时会引入core-js中的全局变量或者在对应的原型链中添加相应的方法,这样都造成了全局环境的污染。虽然这对于应用程序或命令行工具是可以的,但是如果你的代码是要发布供他人使用的库,或者无法完全控制代码运行的环境,则将成为一个问题。

首先,单独使用@babel/plugin-transform-runtime只能够处理辅助方法,如果想要去引入polyfill就需要配合@babel/runtime-corejs3使用。

同样还是在生产环境安装@babel/runtime-corejs3

npm install @babel/runtime-corejs3 --save

这里需要在.babelrc中去除@babel/preset-env配置中关于polyfill的部分以免与@babel/runtime-corejs3重复。

//index.js
const isHas = [1, 2, 3].includes(2);

const getData = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(100);
    }, 1000);
  });

getData();

//.babelrc.js
module.exports = {
  presets: [["@babel/preset-env"]],
  plugins: [
    [
      "@babel/plugin-transform-runtime",
      {
        corejs: 3,
      },
    ],
  ],
};

编译结果:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _setTimeout2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set-timeout"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

var _context;

var isHas = (0, _includes["default"])(_context = [1, 2, 3]).call(_context, 2);

var getData = function getData() {
  return new _promise["default"](function (resolve, reject) {
    (0, _setTimeout2["default"])(function () {
      resolve(100);
    }, 1000);
  });
};

getData();

可以看到,使用@babel/plugin-transform-runtime会用一个临时变量去保存polyfill中的一些值,并不是直接去修改原型链或者新增Promise方法。

在一般开发中使用@babel/preset-env配合useBuiltIns: usage,在开发第三方库时使用@babel/plugin-transform-runtime

在上面介绍@babel/plugin-transform-runtime的一些使用时可以看到,它不仅能够处理引入多次helper辅助方法的问题,而且在只引入所需polyfill时还不会污染全局环境,那还有必要使用@babel/preset-envuseBuiltIns吗?

其实@babel/plugin-transform-runtime配合@babel/runtime-corejs3引入polyfill有一个很大的不足就是不能够通过设置目标环境去引入所需要的polyfil。,我们在普通开发时只需要在package.json中的browserslist去设置开发环境和生产环境的浏览器版本,然后通过使用@babel/preset-envuseBuiltIns就能够根据不同的运行环境去引入适当的polyfill。

但是在开发第三方库时,不能确定代码的运行环境,所以就需要利用@babel/plugin-transform-runtime来保证引入的polyfill不去污染全局环境。

最后总结

一般开发: 通过useBuiltIns: usage去保证引入恰当的polyfill,通过@babel/plugin-transform-runtime保证辅助函数都是引用@babel/runtime

const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        //代替browserslist设置浏览器版本
        targets: product,
        useBuiltIns: "usage",
        corejs: 3,
      },
    ],
  ],
  plugins: [["@babel/plugin-transform-runtime"]],
};

参考文章

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.