Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 最佳实践 #6

Open
camsong opened this issue Jan 31, 2016 · 5 comments
Open

React 最佳实践 #6

camsong opened this issue Jan 31, 2016 · 5 comments

Comments

@camsong
Copy link
Owner

camsong commented Jan 31, 2016

组件化开发

  • 组件应尽可能 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 也是不错的选择,具体请参考相关文档

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

@shaozj
Copy link

shaozj commented Dec 19, 2016

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

@ClarenceC
Copy link

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

@camsong
Copy link
Owner Author

camsong commented Sep 18, 2017

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

@iugo
Copy link

iugo commented Jan 3, 2018

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

@andyyxw
Copy link

andyyxw commented Nov 19, 2019

Server Rendering 现在还推荐吗?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants