React 最佳实践
组件化开发
- 组件应尽可能 stateless (无状态化 )
- React 拥抱函数式编程思想,纯正的函数式讲究的是绝对的无状态化,React 为了降低学习成本还是允许组件保持 state。
- 能通过计算得来的 state 就不要用 state,每次用时计算一遍即可。
- 在 componentWillReceiveProps 中如果有对这个 state 做同步,那就直接使用 props 即可
- 使用 pure render mixin/decorator
- 使用 stateless function
- 少用生命周期函数
- 知道为什么生命周期方法名都那么长吗?为什么叫
componentDidMount
而不是didMount
或mounted
呢?类似的还有超长的dangerouslySetInnerHTML
有考虑过键盘的感受吗。其实这是一种古老的命名策略,给不鼓励使用的方法设置非常长的方法名,来尽量避免使用。生命周期方法都是给你应急或与外部组件对接用的,如果能避免就尽量不用。
- 知道为什么生命周期方法名都那么长吗?为什么叫
- 胖的 render
- 既然要避免用生命周期,那么相关的逻辑自然只能放 render 里了。如果你需要对 props 做计算,如根据 firstName 和 lastName 来计算 fullName,只需要在这里定义一个临时变量 fullName 即可。不必担心每次计算带来的性能损失,React 另一个设计原则是认为『JavaScript 速度比你预想的要快』。如果真遇到了性能问题,就想办法减少 render 调用次数。
- 组件应该细粒度,以提高复用性
- 设置完整的 propTypes
- propType 可以对传入 props 的数据类型做验证,能提前发现很多问题。同时完成的 propType 定义也有文档的作用,使用组件时只要看一下 propType 定义就能大概知道组件用法。在生产环境打包时添加 NODE_ENV="production" 变量,可以让 uglify 略过 propType 代码。
- 为 Server Rendering 做准备
- 事件绑定放到 componentDidMount 或者更后的生命周期函数中
- 不要直接操作 DOM
- 使用 CSS Modules
一个 UI 组件的完整模板
class Button extends React.Component {
static propTypes = {
type: PropTypes.oneOf(['success', 'normal']),
onClick: PropTypes.func,
};
static defaultProps = {
type: 'normal',
};
handleClick() {
}
render() {
let { className, type, children, ...other } = this.props;
const classes = classNames(className, 'prefix-button', 'prefix-button-' + type);
return <span className={classes} {...other} onClick={::this.handleClick}>
{children}
</span>;
}
}
应用层开发
长痛不如短痛,如果你预料到业务未来会比较复杂的话,还是早点使用 Redux 吧。但即使使用了 Redux 并不是说只有一种选择,基于它上面的生态非常丰富。Redux 是一个重思想轻实现的框架,理解思想非常关键。
下图是我画的 Redux 操作流程图
有几点明确一下:
- Action 描述发生了什么,是一个普通 JS 对象,是全局的,只以
type
来区分- 全局的,这意味着你需要考虑好命名问题。建议使用命名空间的方法,通俗点讲就是加前缀
- 普通 JS 对象,也就是说它无法处理异步
- ActionCreator 没有画出来,它是一个函数,调用后会返回 action 对象,这是它和 action 的区别。
- Reducer 描述了 action 发生后如何修改数据。是无副作用的函数
- 无副作用就是使用相同的参数无论调用多少次结果都是相同的
- 每个 reducer 对应于界面上的一类的数据,所有 reducer 组合到一起后就形成了状态树(state tree),也被叫做 Store
- Middleware 是像洋葱皮一样嵌套执行的。它提供了对 action 修饰的能力。执行时间界于 action 发出后,到达 reducer 前,这是最常见的扩展 Redux 的方法,大部分异步处理都是通过引入 middleware 实现
connect
方法把 Store 中数据按需绑定到 View 上,是最核心方法之一,有很多的细节,建议看下源码- 因为 Redux 把所有数据都放到了 Store 里,也就是说 View 组件应该尽可能追求无状态化。这样才能达到最大的灵活性,(复用性倒是其次)
Redux 开发常用的问题
使用 Redux 时,最可能遇到了是这些问题
- 数据如何组织:因为所有数据都放到了一个 Store 树中,这棵树如何管理
- 性能:每次调用 action-> reducer 都可能会引起 Store 树的变化,绑定不对可能造成无数不相关的 View 重复渲染,浪费资源,尤其对于无线应用
- 复用:组件被拆分成了 view, action, reducer 如何复用
- 异步处理:这其实是最复杂的一块,但却是 Redux 本身最少涉及的部分,让灵活性丢给了开发者自己选择
一、数据如何组织
好的数据组织方式评判方法很简单:一眼就知道这个数据是哪个页面、哪个模块、大致做什么的。
现在大多是单页面应用,而且每个页面(Page)包含多个模块(我喜欢叫卡片 Card),所以这个数据树至少会包含 page 和 card 两层。在我开发的一个应用中,是这样来规划的
左边是页面大致的结构,包含可能多页面复用的全局筛选器(Global Filter),当前页面的多个卡片。所以在设计 Store 结构的时候就分了 page 和 card 两层,card 下面才是业务数据。为了让全局筛选器统一管理,单独在顶层开辟了 filters
分支。
二、性能
只要你使用了 immutable 的数据结构后,做 Redux 性能优化非常简单。由于 connect
默认开启了 pure render 模式,所以让需要数据的组件来 connect 数据性能最好,也就是 ** connect at lower level**。下图演示了在不同位置 connect 导致 render 的差异。
第一棵树中红色结点数据变化后
- 如果只在顶层 View 中
connect
所有数据,然后 props 形式把数据往下传,渲染结果如第二棵树,从顶层直到数据改变的组件都会渲染 - 如果在改变数据的地方直接
connect
,其它地方就不需要关心这块数据,结果只有改变数据的组件被渲染,结果如第三棵树
另外你还可以对 Component 添加 pure-render-decorator 来提升组件渲染性能。对于速度慢的函数使用 Memoization 来提升性能,常见的有 lodash.memoize
三、复用
首先要清楚,不要用了复用性而牺牲了开发的便利性,而且复用在最初是比较高效的,但可能随意业务的扩展,本来相同的东西变得不同,这时候最初的复用反而给未来增加了成本。我不是不鼓励复用,只是不建议把它摆在太高的位置。
View 的复用比较简单,只要保证 view 的纯粹,在 connect
之前可以当作标准的 react 组件任意复用。如果想把 view, action, reduer 做为一个整体的业务模块来考虑复用,是比较难的。但这其实是最能提升效率的。如果你也遇到这样的场景,可以试下这个方法。
generateView
方法,接收页面名(page)和卡片名(card)来生成 view 和 actiongenerateReducer
方法,接收同样的页面名(page)和卡片名(card)来生成 reducer
因为两个方法的 page 和 card 是一致的,这样就能保证它们互相引用没问题且和现有的不冲突。
这样复用一个业务组件就是复用这两个方法。
示例代码如下:
// generateFooView.js
export default function generateFooView({ pageName, cardName = 'overview' }) {
const NAMESPACE = `${pageName}/${cardName}/`;
const LOAD = NAMESPACE + 'LOAD';
function load(url, params) {
return {
type: LOAD,
};
}
@connect((state) => {
return {
[cardName]: state[pageName][cardName],
};
}, {
load,
})
class Overview extends Component {
render() {}
}
}
// generateFooReducer.js
export default function generateFooReducer({ pageName, cardName = 'overview' }) {
const NAMESPACE = `${pageName}/${cardName}/`;
const LOAD = NAMESPACE + 'LOAD';
const initialState = {
isLoading: false,
data: [],
};
// 导出 reducer
return function OverviewReducer(state = initialState, action) {
switch (action.type) {
case LOAD:
return {
...state,
isLoading: true
};
default:
return state;
}
};
}
四、异步处理
- 简单的数据处理用 thunk-middleware 即可,缺点是流程复杂后可能会导致 callback hell,结合 Promise 后稍好一些,优点是学习成本低
- 如果需要复杂型的异步控制,如 cancel 一个请求,监听 action,建议使用 redux-saga,如果再复杂一些的数据请求和交互使用 redux-observable 也是不错的选择,具体请参考相关文档
以上四点业务层的经验是我一年多以来感受比较深的。还有目录组织、路由等一些细节问题,可参考的资料很多就不赘述了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论