camsong / blog

✍️Front-end Development Thoughts

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

React 最佳实践

camsong opened this issue · comments

组件化开发

  • 组件应尽可能 stateless (无状态化 )
    • React 拥抱函数式编程**,纯正的函数式讲究的是绝对的无状态化,React 为了降低学习成本还是允许组件保持 state。
    • 能通过计算得来的 state 就不要用 state,每次用时计算一遍即可。
    • 在 componentWillReceiveProps 中如果有对这个 state 做同步,那就直接使用 props 即可
  • 使用 pure render mixin/decorator
  • 使用 stateless function
  • 少用生命周期函数
    • 知道为什么生命周期方法名都那么长吗?为什么叫 componentDidMount 而不是 didMountmounted 呢?类似的还有超长的 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 操作流程图

image

有几点明确一下:

  • 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 两层。在我开发的一个应用中,是这样来规划的

image

左边是页面大致的结构,包含可能多页面复用的全局筛选器(Global Filter),当前页面的多个卡片。所以在设计 Store 结构的时候就分了 page 和 card 两层,card 下面才是业务数据。为了让全局筛选器统一管理,单独在顶层开辟了 filters 分支。

二、性能

只要你使用了 immutable 的数据结构后,做 Redux 性能优化非常简单。由于 connect 默认开启了 pure render 模式,所以让需要数据的组件来 connect 数据性能最好,也就是** connect at lower level**。下图演示了在不同位置 connect 导致 render 的差异。

image

第一棵树中红色结点数据变化后

  • 如果只在顶层 View 中 connect 所有数据,然后 props 形式把数据往下传,渲染结果如第二棵树,从顶层直到数据改变的组件都会渲染
  • 如果在改变数据的地方直接 connect,其它地方就不需要关心这块数据,结果只有改变数据的组件被渲染,结果如第三棵树

另外你还可以对 Component 添加 pure-render-decorator 来提升组件渲染性能。对于速度慢的函数使用 Memoization 来提升性能,常见的有 lodash.memoize

三、复用

首先要清楚,不要用了复用性而牺牲了开发的便利性,而且复用在最初是比较高效的,但可能随意业务的扩展,本来相同的东西变得不同,这时候最初的复用反而给未来增加了成本。我不是不鼓励复用,只是不建议把它摆在太高的位置。

View 的复用比较简单,只要保证 view 的纯粹,在 connect 之前可以当作标准的 react 组件任意复用。如果想把 view, action, reduer 做为一个整体的业务模块来考虑复用,是比较难的。但这其实是最能提升效率的。如果你也遇到这样的场景,可以试下这个方法。

image

  • generateView 方法,接收页面名(page)和卡片名(card)来生成 view 和 action
  • generateReducer 方法,接收同样的页面名(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;
    }
  };
}

四、异步处理

  1. 简单的数据处理用 thunk-middleware 即可,缺点是流程复杂后可能会导致 callback hell,结合 Promise 后稍好一些,优点是学习成本低
  2. 如果需要复杂型的异步控制,如 cancel 一个请求,监听 action,建议使用 redux-saga,如果再复杂一些的数据请求和交互使用 redux-observable 也是不错的选择,具体请参考相关文档

以上四点业务层的经验是我一年多以来感受比较深的。还有目录组织、路由等一些细节问题,可参考的资料很多就不赘述了。

亲,应用层开发怎么不写了呢?

redux store的数据组织,我有个问题?如果page1 和 page2 这两个页面里面有一个card里面的数据是相同事,那这个card store是该设计在page 1 ?page2?里面 还是该独立出来的啊?

@ClarenceC 如果只是这两个页面复杂,放到任何一个,在另一个页面直接 connect 数据即可。如果是很多页面都会复用这一部分数据,建议独立出来直接在 store 下开一个分支。

commented

是不是这样:
组件应该细粗度 -> 组件应该细粒度

commented

Server Rendering 现在还推荐吗?