2017 年学习 React + Redux 的一些建议
本文转载于@郭永峰发布在Github博客中的《在 2017 年学习 React + Redux 的一些建议》的上篇、中篇和下篇。本文将其合并在一起。如需转载,请注明原文出处。特别声明,这几篇文章内容和@rwieruch的英文版本非常近似,在此特提供原文地址:https://www.robinwieruch.de/tips-to-learn-react-redux/。
使用 React + Redux 这个技术栈开发应用已经有很长一段时间了,我的一些使用经验也许会有些主观,但我觉得写出来也许对你开始学习或是进阶使用 React + Redux 会有些帮助。Redux 并不是只和 React 结合使用的,它也可以和其他的很多类库结合起来一起使用,即使你还未开始深入使用,你也可以阅读文中的部分内容。同时,如果你有一些建议或是疑惑,可以在 Github 给我提交 Issue,很乐意与你一起交流。
对学习 React 的一些建议
在深入 Redux 、Testing 或是其他更高级的使用之前,我们还是先开始 React 吧。
不要太过在意脚手架
你已经准备好开始学习 React 了吗,我建议你不要一开始就进入到选择和学习项目脚手架的困扰中,因为你一上来就得接触Webpack、Babel、Testing Tool等,实在是眼花缭乱。
我建议你可以首先采用 create-react-app 这个工具,他是Facebook For React 官方提供的零配置CLI 命令行工具。从技术上来说虽然它依然是一个脚手架,但是它屏蔽了所有的工具配置,你可以用它来快速生成一个React项目所需的基本工程。
sudo npm install create-react-app -g
create-react-app react-demo
cd react-demo && npm install
npm start
仅仅以上几步,你就可以在启动开发了,非常的快吧。
但是等你想深入项目的工程化配置而不再想使用 create-react-app 的时候,那么,是时候该去学习一个适合你的构建工具了。在使用 create-react-app 这个工具的时候,它让你快速开发的同时也让你错过了如何真正的使用工具并去配置工具。
当你从零开始搭建一个你自己的项目工程的时候,那么你将需要从底层来了解这些技术是如何工作的,这样的话,慢慢就能产出一个你自己的项目脚手架了,在团队里面去推广使用。
小结:
- 避免开始学习的时候就去使用脚手架
- 使用 create-react-app 去学习 react
- 当你熟悉 react之后
- 去探索工具本身如何使用
- 创建属于你自己的项目脚手架
在你学习 Y 的时候要先去学习 X
虽然学习 React 不难,因为它仅仅是个 view library,但是 React 的整个生态链非常丰富,并且会有很多关于如何学习的方法和资料。
而我的建议是,在你学习东西的时候需要学习这个技术的前置技术,否则学习起来会很吃力。所以你在开始学习这个技术栈的其他东西之前,下面所列举的内容应该会对你很有帮助:
- JSX 语法
ReactDOM.render
- 使用
setState
来改变组件的状态state
- 组件生命周期相关的方法
- 事件和表单
- 几种创建组件的方式
- 复合组件
composeable
- 高阶组件的使用和定义 HOC
一般建议你在开始下一部分内容之前,你可以先把这些 React 相关的内容都学习完成。
什么时候引入 Redux
在你遇到应用的状态管理问题之前,你可以通过扩展应用的方式来解决,或是你在你的应用中压根就没遇到过这样的问题,因为你可以使用 setState
来很好的管理你的应用。但其实 setState
也不是那么高效,毕竟它也不是万能的。也许你会发现你已经在忙于处理太多的组件内部状态,那么,该是时候引入类似 Redux 等状态管理库了,不过有几个事情需要注意:
- 在学习 Redux 前得先学好 React
- 可以阅读以下You might not need Redux
- 不是所有的状态都需要放在 Redux 的
Store
中,也不能完全取代setState
那关于 ES6 呢
大量的关于React的示例都采用ES6语法,也许你在其他的项目中已经学习并使用了 ES6 相关的语法和API,或者也许你还没接触呢。不过,ES6对于React也不是必须的,我的一些建议是:
- 如果你有使用类似 angular 等前端框架或是类库的经验:你可以在你熟悉的环境里面去学习 ES6
- 如果你是前端入门的新人:你可以在React中继续使用ES5的语法和API或者在你学习React之前,先学习一下ES6
- 如果你是个前端老司机:逐步在React应用中使用ES6吧
我比较建议的学习方式是,学一个东西之前一定要先掌握它相关的前置知识,另外,也一定要记住:不要一次性学习所有东西,不然反而啥都掌握不了。
声明式的React组件
一般地,会有三种不同的方式来声明一个组件:
React.createClass
使用这个API来定义组件React ES6 class components
使用 ES6 的class 来定义组件Functional stateless components
通过函数直接定义无状态组件
// React.createClass
var TodoItem = React.createClass({ ... })
// React ES6 class
class TodoItem extends React.Component { ... }
// functional stateless component
function TodoItem() { ... }
那什么时候该用什么方式来定义呢
无状态组件:适合用来声明没有组件生命周期方法并且没有内部状态的组件。这种写法很简单,就是一个纯函数,一个状态输入,输出就是elements
。
(state) => View
这是一种最轻便最高效的组件声明方式,这种组件没有任何的内部状态,使用的时候也无法访问到组件的属性,所以一般建议能用这种方式声明组件的时候就尽量采用这种方式。
如果你需要使用组件生命周期方法,并且需要去处理组件内部状态(this.state
),或者需要获取这个组件(this.ref
)。那么,这个时候建议你使用ES6的 class
来声明组件。
另外,建议还是别用React.createClass
这种方式声明组件了,Facebook 官方在React V0.13版本的时候就说过,以后的目标是使用ES6 classes
的方式来定义,会完全废弃React.createClass
另外,这有两篇博文也推荐给你看:
- Should I use React.createClass, ES6 Classes or stateless functional components?
- React.createClass versus extends React.Component (译文,可以点击这里)
轻量级的函数式无状态组件
项目中一定需要去声明很多组件,我们可以一起来完成一个 TodoList
组件:
function TodoList({ list }) {
return (
<div>
{map(list, (item) => <div>{item.name}</div>)}
</div>
);
}
我们还可以按函数的方式将其拆分
function TodoList({ list }) {
return (
<div>
{map(list, (item) => <TodoItem item={item} />)}
</div>
);
}
function TodoItem({ item }) {
return <div>{item.name}</div>;
}
这个例子有些简单,无法很直接的看到这种定义方式的好处,但是当我们将组件拆分后将更具有可读性、可复用性以及可维护性。这种方式很灵活,而且很轻松就可以声明多个组件,一口气不费劲,推荐使用。
简洁的函数式无状态组件
我们可以使用 ES6 的 arrow functions 让组件的定义更加的清新。假如我们有一个这样的组件:
function Button({ onClick, children }) {
return (
<button onClick={onClick} type="button">
{children}
</button>
);
}
我们可以将其用 ES6 的语法改写升级一下:
const Button = ({ onClick, children }) => {
return (
<button onClick={onClick} type="button">
{children}
</button>
);
}
还可以再简单吗,我们来试一试:
const Button = ({ onClick, children }) =>
<button onClick={onClick} type="button">
{children}
</button>
但这种方式就只允许我们的组件只有 props
作为输入,elements
作为输出。但如果我们希望在这中间做一些业务逻辑呢,我们可以将其再修改一下:
const Button = ({ onClick, children }) => {
// do something
return (
<button onClick={onClick} type="button">
{children}
</button>
);
}
回过头来看看,箭头函数真是在我们定义无状态组件的时候帮了大忙,整个世界的清爽了。
木偶组件Presenter Component和容器组件Container Component
好吧,这两个词听起来实在是别扭,听我慢慢道来。
React 中的组件其实也只是应用状态的一种表现方式,这让我们可以非常清晰的通过 (State) => View
这样的方式来理解。并且,组件内由处理程序来改变 state
,从而改变不同 view
的展示。
那么,木偶组件和容器组件有什么区别或是怎么理解呢。
- 木偶组件只接受传递进来的
props
和callback
,然后返回对应需要展示的view
,这种组件比较单纯,大部分都是无状态组件,给我啥我就展示啥,给我什么callback
我就执行callback
,相同的输入总能得到相同的输出,像个机器人或是像个木偶,很纯粹很简单很可控; - 而容器组件内更多的是关注逻辑,你需要在容器组件中准备好数据和一些
callbac
函数,在这里处理一些事件或管理内部的状态,给木偶组件传递所需的数据,大致的意思就是,容器组件对木偶组件说:调度或是逻辑处理的事就交给我吧,你需要啥我给你啥,你安心干你的活就行,保证给我的产出即可。
当项目中使用了Redux的时候,Container Component 有个更好理解的名字,就是Connected Component。这类组件和Redux的store
进行了连接,并且获取到store
的数据之后进行一些操作后传递给子组件。
Container components容器组件关注事情是怎么做的,Presenter components木偶组件关注怎么展现,各司其职,其乐融融。
另外,也再推荐给一篇文章《Presentational and Container Components》,可以再更深入的理解。
什么时候该用 Container Components 容器组件呢
前面聊完,我想你应该知道两者的区别和使用场景了,但也许你还不太确定什么时候该用什么类型的组件。你可以定义一个Container Components,然后把一些Presenter Components都作为他的子组件,这样父组件关注如何工作,子组件关注如何展现。
但慢慢的,你发现父组件需要给子组件传递的properties
和callbacks
越来越多了,咋办呢,现在是时候来介绍两者如何结合使用了。
一般地,有个很好的原则:坚持presenter components不变,只新增一系列的container components去适应业务逻辑的改变。
把container component放到什么地方呢
- 父组件container component只关注
state
如何处理,现在你可以评估一下你的preenter component下面的子组件了。也许你会注意到preenter component下面的子组件没有被其他组件使用,那么找到这个组件的父组件,给它添加一个用于管理状态的container component,这样,父级的container component就能够变得更加清晰轻量,因为它没有必要去处理这些所有的状态。 - 很多presenters component也许只是包含一些只是为他们自己使用的
props
和callbacks
,那么,也给他们加上一个container component去处理这些逻辑,把这些逻辑放到container component,这样将使父级的container component 再次变得轻量。
写出你的第一个高阶组件HOC吧
想象一下你需要展示一列内容,但是你不得不首先通过异步的方式获取这些内容项。现在你需要一个加载指示器来显示目前正在发送请求中,等请求完成后,你再展示获取到的内容项。
不过你可以更进一步来学习HOC了,一个高阶组件HOC将会返回一个增强的函数。
// 定义一个高阶组件
function withLoadingSpinner(Component) {
return function EnhancedComponent({ isLoading, ...props }) {
if (!isLoading) {
return <Component { ...props } />;
}
return <LoadingSpinner />;
};
}
// 使用
const ListItemsWithLoadingIndicator = withLoadingSpinner(ListItems);
<ListItemsWithLoadingIndicator
isLoading={props.isLoading}
list={props.list}
/>
高阶组件很强大,但是我们也得有目的的去使用。另外,Recompose提供了一系列非常有用的高阶组件,在开始写你自己的高阶组件的时候,不妨先去这里看看,学习一下,也许就能解决你的疑惑了。
带有业务逻辑的样式名管理
也许你在组件中遇到了这样带有条件逻辑的样式名定义:
var buttonClasses = ['button'];
if (isRemoveButton) {
buttonClasses.push('warning');
}
<button className={buttonClasses.join(' ')} />
这种情况我们可以使用 classnames
来处理,这会让我们可以非常方便的在elements
上定义有条件的style
:
import classNames from 'classnames'
var buttonClasses = classNames(
'button',
{
'warning': isRemoveButton
},
);
<button className={buttonClasses} />
React 中的动画 Animations
通过这个小例子来感受一下React animation,react-motion 给我们提供了一个在 React 中使用动画效果的工具包。但是,我发现这个学习曲线非常陡峭,在你开始使用 React Motion 之后就会让人觉得非常沮丧。不过一旦你写出一个流畅的可拖放动画之后你会感觉很有成就感。
其实,对于大多数前端小伙伴们来说,我们也只是花很少一部分时间在动画效果的实现上面,所以当你本身不会经常使用这个动画库的时候,等到下次需要用到的时候又得重新熟悉一遍,这也造成了学习曲线反复的问题。
另外,velocity-react 是基于Velocity DOM animation 实现的另外一个React动画库,你可以在 React Motion和它之间二选一。
对于学习 Redux 的一些建议
React 和 Redux 经常结合在一起使用,Redux 是 flux 架构模式的一种优秀实现,并且在 React 社区被广泛使用,但也不是完全和 React 耦合在一起的。
全局 state
并不是所有的全局state
都需要被存储起来,一些组件可以使用 setState
来管理组件的内部状态,这也是为什么在学习 Redux 前要掌握 React 中的 setState
,否则你将习惯式的把所有的global state
都存储在store
里面。所以思考一下,在大型开发团队里面开发的复杂应用,你更不能将应用的所有 state
都切换成全局状态。
项目目录如何组织
这篇文章 给出了三种建议方式来组织项目结构。
第一种方式是按功能划分
React + Redux 的一些教程经常给我们展示按功能划分的目录,这也是一种很好的 React + Redux 学习方式,不过,将应用的所有 reducers
和 actions
都放在专门的文件夹维护的方案,并不是所有人都能赞同。
src/
--actions/
--reducers/
--components/
经常听到的有建设性的想法是,目录划分应该以组件为核心,每个目录应该有组件本身以及它所对应的 reducers
、actions
,那么一个示例的目录结构应该是这样的:
message/
--components
--reducer.js
--actions.js
一个包含 container component 、presenter component以及测试相关的详细的组件目录会是这样的:
message/
--components/
----messageItem/
------presenter.js
------spec.js
----messageList/
------container.js
------presenter.js
------spec.js
--reducer/
----index.js
----spec.js
--actions/
----index.js
----spec.js
当然了,也并不是大家都会喜欢这种方式。(其实,我个人是很赞同这样的就近维护组件的原则的,因为将各个功能性的reducer
和action
都丢到对应的目录,这以后维护起来会更加困难,文件也不好找,这可不像是MVC那样的分层结构。)尤其是将reducer
隐藏在各个功能目录中,这也不利于全局性的来理解使用 Redux 的架构意图。所以建议是适当的在最初就抽取一些 reducers
来共享他们所包含的功能。
但在现实场景中,尤其是多个团队在同一个应用项目中协作的时候,在开发进度的压力之下,并没有那么多机会来正确的抽象出一些 reducers
。反而通常是一口气的封装所有的功能模块,只为了感觉把活给干完了,让需求按时上线。
第二种方式是对功能模块划分清晰的界限
给每个模块都设置一个 index.js
文件作为入口,这个文件只是用于导出一些API给其他的模块使用。在基于 React + Redux 的应用中,index.js
文件可以用于导出一个 container components ,或是一个presenter components、action creators、能用于其他地方的 reducer
(但不是最终的reducer
)。那么,基于这样的思考,我们的目录就可以变成这样了:
message/
--index.js
--components/
----messageItem/
------index.js
------presenter.js
------spec.js
----messageList/
------index.js
------container.js
------presenter.js
------spec.js
--reducer/
----index.js
----spec.js
--actions/
----index.js
----spec.js
那么,在当前功能模块下的 index.js
文件应该包含这些代码:
import MessageList from './messageList';
export default MessageList;
export MessageItem from './messageItem';
export reducer from './reducer';
export actions from './actions';
好了,这样外部的其他模块就可以这样在他的 index.js
文件中调用 message
模块了。
// bad
import { reducer } from ./message/reducer;
// good
import { reducer } from ./message;
收获:按功能模块以及清晰的界限可以帮助我们很好的组织代码和目录。
命名约定
在软件编程中命名可真是一件令人头疼的事情,这跟给孩子取名一样费劲,哈哈。合适的命名是实现可维护性、易于理解的代码的最好实践,React + Redux 的应用中提供了大量的约束来帮助我们组织代码,而且不会在命名上固执己见。无论你的函数封装在 reducer
还是 component
中,在action creator
或是 selector
中,你都应该有一个命名约束,并且在扩展应用之前就确定如何命名,否则经常会让我们陷入难以捉摸的回调和重构当中。
而我习惯为每个类型的函数都加上一个前缀。
在组件的callback
中,为每个函数都加上 on
作为前缀,比如 onCreateRplay
在改变 state
的 reducer
中加上 applay
作为前缀,比如 applyCreateReply
在 selector
中 加上 get
作为前缀,比如 getReply
在 action creator
中加上 do
作为前缀,比如 doCreateReply
也许你不一定习惯这种加上前缀的方式,不过我还是推荐给你,同时也建议找到自己喜欢的命名约束规则。
追踪状态的改变
在持续迭代中的应用免不了定义大量的 action
,而且还需要追溯 state
是如何改变的,redux-logger 可以帮助你看到所有的 state change
。每条日志都会显示出 previous state
、执行的 action
、next state
。
不过你得确保 actions
是可被设备的,因此我建议为不同类型的 action
都加上一个前缀,比如这样:
const MESSAGE_CREATE_REPLY = 'message/CREATE_REPLY';
这样的话,无论你在何时触发了信息回复这个动作,你都能看到 message/CREATE_REPLY
这一条日志,如果出现 state
异常,便能迅速查到是那条错误的 state
改变而导致的。
尽可能让 state tree 扁平化
在 Redux 中,扁平化的 state tree
可以让你的 reducers
更加的简单,这样你就不需要在整个 store
的状态树中深层的查找到某个 state
后再将其修改,而是可以很轻松的就能实现。不过,在 Redux 中却不能做这么做,因为 state
是不可变的。
如果你正在开发一个博客应用,需要维护一个类似这样的列表对象,列表中包含 author
和 comment
字段:
{
post: {
author: {},
comments: [],
}
}
不过实际情况是每个对象都需要有对应的 id
来进行维护:
{
post: {
id: '1',
author: {
id: 'a',
...
},
comments: [
{
id: 'z',
...
},
...
],
}
}
这个时候,我们将数据序列化之后将会变得更有意义,数据解构变得更加扁平化了。序列化之后的数据通过 id
关联其他字段,之后,你就可以通过实体对象来将其报酬,通过 id
来进行关联数据的查找。
{
posts: {
1: {
authorId: 'a',
commentIds: ['z', ...]
}
},
authors: {
a: {
...
}
},
comments: {
z: {
...
}
},
}
这样,数据结构看起来就不在那么深层嵌套了,当你需要改变数据的时候,就可以轻松的实现数据的不可变性了。
normalizr 是个强大的 library,可以帮助我们进行数据格式化,噢耶~!
单一数据源原则
格式化之后的数据可以帮助你按同步的方式来管理 state
,而假如请求后端接口后返回的是深层嵌套的 blog
的 posts
数据结构呢,是不是欲哭无泪啊?!post
字段依然包含 author
和 comments
字段,不过这次,comments
是一个数组,数组中的每个对象都有 author
字段:
{
post: {
author: { id: 'a' },
comments: [
{
author: { id: 'b' },
reply: {},
},
{
author: { id: 'a' },
reply: {},
},
],
}
}
我们可以看到数据结构中 author
字段在 post
和 comments
中都有维护,这就导致嵌套的数据结构中出现了两次,这就不是单一数据源,当你改变了author
字段的时候就会变得很困难了。
这个时候当你将数据格式化之后, author
这个字段就只有一个了。
{
authors: {
a: {},
b: {},
}
}
当你想 follow
一个 author
的时候,就可以轻松的更新一个字段了 --- 数据源是单一的:
{
authors: {
a: { isFollowed: true },
b: {},
}
}
应用中所有依赖了 author
这个字段的地方都能得到更新。
Selectors
你还没使用 selectors
吗?没关系,在 Redux 中依然可以通过 mapStateToProps
来计算 props
:
function mapStateToProps(state) {
return {
isShown: state.list.length > 0,
};
};
而如何你一旦使用了 selectors
之后的话,你就可以将这部分计算的工作放到 selectors
,从而让 mapStateToProps
更加的简洁:
function mapStateToProps(state) {
return {
isShown: getIsShown(state),
};
};
你可以使用 reselect
来帮助你完成这些事情,它可以帮助你从 state
中计算得到衍生的数据,并且让你的应用的性能得到提升:
Selectors
可以推导出衍生数据,并传递所需数据的最小集,不用一次把所有数据都给组件,解决性能问题Selectors
是可组合的,它可以作为其他Selectors
的输入Reselect
所提供的selector
是非常高效,除非它的参数改变了,否则selector
不会重新计算,这在复杂应用中对性能提升是非常有帮助的。
不断的重构
随着时间得推移,你会想要重构你的代码,无论是你在应用中使用了 React 、React + Redux 或者其他前端框架,你总会不断的掌握更加高效的代码组织方式,或者是一些很好的设计模式。
如果你的应用中的组件非常的多,你可以找到一个更好的方式来分离和组织木偶组件和容器组件,你会发现他们之间的关系并做一些公共的抽取;如果你还没有使用合适的命名约束,你也可以在重构的时候去做这些事情。
Generators, Sagas, Observables, Epics, ...
Redux 是一个非常优秀的 library,让我们可以体验不同的编程范式和技术。而大家又常常需要不构建不同的类库来实现 async action
,这里有几种不同的方式来处理这些 side effects:
- Redux Thunk - (Delayed) Functions
- Redux Promise - Promises
- Redux Saga - Generators
- Redux Observable - Observables
- Redux Loop - Elm Effects
新手的话建议使用 Redux Thunk 来处理一些异步操作;等你慢慢的熟悉整个生态及其相关的应用的时候,可以看看其他的相关类库。Redux Saga 是目前被广泛采用的一种实现方式。不过,Redux Observables 目前也被越来越多的人所接受,这可是需要掌握不少关于 RXJS 及其响应式编程的概念及其使用方式。
其实,整体看来,Redux 生态圈的本身就产生了非常多的前端类库,真是让人应接不暇啊。但也别烦恼,那些你不需要用到的东西,自然也不需要都去掌握,对吧。
多阅读一下 Redux 的实现源码
Redux 本身的源码并不多,总共也才五六个关键文件,不超千行代码。如果你想对 Redux 更加熟悉,那么强烈建议你要抽些时间多分析一下他的源码。
在开始学习的时候,也推荐部分学习视频给你:
- Redux 的作者 @Dan Abramov 自己录制的入门级视频 《Getting started with Redux》》 ,大家都说录制的很棒,不过说实话,这个对理解实现原理是很有帮助的。《Javascript Redux implementing store from scratch 》和 《Javascript Redux implementing combinereducers from scratch》 两个视频可以帮助你理解
store
和combineReducer
的实现原理。 - 第二个系列的视频是《Building react applications with idiomatic Redux》,你可以从中学习到如何实现你自己的
middleware
中间件,学完后就可以学习如何在store
中使用它们。然后,你就能掌握到如何使用applayMiddleware
将中间件应用到store
中
这些视频内容不仅可以教你快速掌握如何使用 Redux,还可以让你理解 Redux 的实现原理。最后,你就可以啃一啃 Redux 的源码了,可以学习到很多有意思的编程思想和函数式的运用。
关于测试的一些学习建议
我们可以组合使用一些测试工具来帮助测试 JS 代码,一般使用 Mocha/Chai 或是 Karma/Jasmine 。而如果当你想测试 Angular 的代码时,你会发现还有更多的测试工具。不过对于 React 应用的测试,比较推荐使用 Airbnb 团队出品的 anzyme 来进行组件的测试,以保住组件的稳定可靠,目前使用非常广泛;而另一种方式是使用 Facebook 的 jest 来进行测试。
可能很多同学都觉得应该选择以上的某一个测试库来进行测试工作,不过,你也可以将 anzyme 和 jest 结合起来一起使用。特别是在进行一些 snapshot 快照测试的时候,两种都是互补的,它们已经是 React 应用测试中大家公认的标准库了。
sinon 也是个非常优秀的测试辅助工具,可以帮助我们在 spy、stub、mock 等测试阶段提供相应的工具辅助测试。如果对这几个概念不太清晰,可以看看这里。
另外,在这里给你隆重的给你推荐一篇 A. Sharif 写的 Some Thoughts On Testing React/Redux Applications,满满的干货分享哦。
多一些组件的单元测试,少一些集成测试
Enzyme 可以帮助我们实现组件的单元测试和集成测试。这里我们可以通过三种方式来渲染组件:
shallow()
mount()
render()
shallow()
只能用来渲染不包含 children
的组件,mount()
则能够渲染所有的子组件。所以单元测试的时候可以使用 shallow()
,mount()
则一般用于集成测试。集成测试往往是很容易被割裂的,因为他需要测试由一组或是多个组件树组合的场景,所以集成测试一般维护成本是比较高的。所以我们可以多做一些小巧的单元测试,少做一些重要的集成测试。
第三种测试的方式是使用 render()
方法,具有类似 mount()
方法的功能,不过 mount()
能够访问到组件的生命周期方法,比如 componentDidUpdate
等。
正如这个 issue 中提出的 API differences between render and mount/shallow,可以总结出:
- 使用
shallow
开始测试用例 - 如果
componentDidMount
或componentDidUpdate
等方法也需要测试,那么使用mount
方法吧 - 如果要测试组件生命周期方法、子组件的行为等,使用
mount
方法吧 - 如果想更高性能的测试子组件,并且对组件的生命周期等方法不怎么关注,那么使用
render
方法吧
保证测试用例简单、最小颗粒度
否则的话你需要为此付出很高的维护成本。
确认每个组件是否都有执行过单元测试,确认每个 props
和 callbacks
都在集成测试的时候传递给了对应的子组件。
为了保证组件测试用例的小颗粒度和简单化,你需要熟悉一下 selectors
,Enzyme
提供了丰富的 selector
去深入组件树。
另外,建议使用 sinon 来测试 callbacks
回调函数,不要在组件中测试业务逻辑,这真不是个好注意。而是应该将业务逻辑从组件中解耦并对其进行测试。
最后,Facebook 出品的 Jest 也能在初期帮助我们更加轻量的执行测试,你可以非常简单就设置好 snapshot test,这样当组件的输出改变的话测试用例会自动的报出失败的结果,并且能够获取到错误信息。
拥抱 TDD(测试驱动开发)
所有的人都可能会对你说:你应该按测试驱动的模式来进行开发。但是,几乎没几个人会这么,项目需求如山的积压,上线的脚本火急火燎,测试驱动?玩呢?!可能大部分小伙伴都是这样的心声。
不过,如果你能够清晰的在 React + Redux 的应用中使用对应的测试方案对每个部分都进行测试,你就能够非常轻松的实现 TDD。尽管你会发现 reducer
的测试和组件的测试是很不一样的,但其实每种类型(reducer
、component
、.... )的测试模式其实都是一样的。
就拿 reducer
的测试为例吧,一般是期望 reducer(state, action) === newState
,其实这种方式和 (input) => output
的模式是一样的。如果你要测试 state
的不可变性的话,建议你可以使用 deep-freeze,可以看下以下示例代码:
import deepFreeze from 'deep-freeze'
const initialState = { ... };
const action = { type: ..., payload: ... };
const expectedState = { ... };
deepFreeze(initialState);
expect(reducer(initialState, action)).to.equal(expectedState);
如果你能够很清晰的知道如何测试应用中的每一个部分,那就最好采用 TDD。
关于资源加载的选择
React 虽然是个 library,但是它的生态圈非常的丰富,会有非常多的可扩展框架或类库可以加入使用,但是千万别太快的加入这些扩展方案。并且每次新加入一个模块的时候,要在团队里面确认每个人都是清楚了解的。特别是对于 Redux 本身的一些生态扩展,会有非常多的一些小模块,比如下面这些:
- 在大家还没开始写
action creators
和reducers
之前,就不要添加 redux-actions - 在大家还没写出第一个自己的
form
表单和表单验证的时候,就不要加入 redux-form - 在大家还没开始写自己的
selectors
之前,就不要加入 reselect - 在大家还么开始写第一个高阶组件 HOC 之前,就不要加入 recompose
- .....
同时,关注一些大牛的最佳实践,并且建立你自己的最佳实践。不过得确保团队中其他小伙伴也能理解。定义清晰的命名规则和目录结构,并且在项目做一些升级的时候得把这些约定提前讨论清楚。
保持持续的技术学习热情
- 关注技术社区的新动向,比如在应用中使用
ramda.js
,看看如何在React中优雅的写代码 - 学习如何使用 React Native 构建你的移动应用
- 学习使用 Electron 构建你的桌面应用
- 也许你可以关注如何使用 Mobx 来进行应用状态管理
- React 仅仅只是 UI 层的一个 library ,你可以使用PREACT 和 inferno 等类似 React 的库来替代,他们的体积能加轻量,渲染更加高效,也许是个不错的选择。
- Airbnb 的 React/JSX 规范 我也建议你抽时间看看,对于团队一致化开发非常有帮助。同时,也可以使用 ESLint 来进行代码规则检查。
结束语
全文完结,感谢你的阅读,希望整个系列的文章对你今后的学习有所帮助。nike air max 2019 footlocker