react按需加载

以前在博客园记录过react代码分块的实现,不过当时主要是关注“能实现”,没有深入研究。今天复习顺便深入一下。

/***引子开始***/

初学者在学习react或用react开发小项目时,一般不需要考虑到代码分块,往往在打包的项目中就已经包含全部的state和reducers。

至今很多团队开发项目都是用传统的文件组织形式:

component(或container)文件夹:存放视图组件或容器组件

action文件夹:存放各个组件的action

reducer文件夹:存放各个组件的reducer

最初接触react的时候我也是这样,毕竟redux文档的例子也是这么做的。后来读到程墨的《九浅一深react和redux》,他介绍了另一种组织形式,一个需要异步引入的react redux模块保存在以模块命名的文件夹中,自包含action、actionCreator、reducer和视图、容器组件,并以inex.js作为组件对外的export接口,在app中只需要引用'./ComponentName'就能获取组件的容器组件和reducer等。

这种react redux modules的组织形式是开发大型项目(或可预见项目以后会越滚越大)时既科学又自然的选择。

但我们提到react的代码分块的时候,其实有两种情况:

异步加载组件:首次加载app时就加载全部的reducer并生成完整的store state,这时候只需要对视图组件(或容器组件)进行分块,以后异步引用即可,操作上比较简单。

异步加载redux模块:将用户不一定需要的redux modules 进行分块,因此首次加载时只加载部分reducer并生成不完整的state,当用户路由到某个模块时,异步加载该模块,加载后更新reducers和state并显示视图组件。

/***引子结束***/

1.代码分块的基石:

现在用webpack打包项目是主流,webpack中有个特定的语法require.ensure()可以在打包时识别某些代码需要分块,这个功能独立于前端框架。react官方手脚架create-react-app一开始就支持webpack这个特性。现在,该手脚架也支持dynamic import proposal,让我们可以用es6的import动态引入模块,且这个异步操作的结果是一个promise。

2.代码分块

有了上面提到的基础后,我们就可以着手对项目进行分块。分块并不是单纯将代码分割,我们更多考虑的是项目骨架的组装和状态如何扩展更新。

------异步加载组件 开始------

    1.asyncComponent

  最简单的情况是,我们只需要异步引入视图(容器)组件,不必考虑reducers和state的更新。这时候我们可以构造一个asyncComponent 函数:

该函数接受一个异步函数()=>import(componentURL)作为参数,并返回一个AsyncComponent组件。

该组件在async componentDidMount里执行传入的异步函数,获取组件后将this.state.component由null更新为所获取的组件。

该组件的render返回null,直到this.state.componet更新,渲染异步获取的组件。

const AsyncHome = asyncComponent(() => import("./containers/Home"));
//...
<Route path="/" exact component={AsyncHome} />

    2.react-loadable

  当我们用asyncComponent的时候,会希望在加载时显示一些loading组件,有一个简单的插件react-loadable

const AsyncHome = Loadable({
  loader: () => import("./containers/Home"),
  loading: MyLoadingComponent
}); 
const MyLoadingComponent = ({isLoading, error}) => {
  // Handle the loading state
  if (isLoading) {
    return <div>Loading...</div>;
  }
  // Handle the error state
  else if (error) {
    return <div>Sorry, there was a problem loading the page.</div>;
  }
  else {
    return null;
  }
};

 组件未加载完成前会自动显示MyLoadingComponent,加载完后显示组件。

在react-router4中code spliting的example也是通过loadable组件实现。

------异步加载组件 结束------

------异步加载redux模块 开始------

下面我会讲解两个案例,一个是react-boilerplate模板的实现。另一个是Twitter Lite的实现。

    随着前端工程化的推进,我们渐渐不再从0开始一个项目,而是用到一些手脚架,除了create-react-app之外,还有像react-boilerplate之类集成度更高的工具。在react-boilerplate中,我们可以看到/app/containers中项目的组件是以redux modules的形式组织的。


如上,每个模块对外输出的是一个react-loadable组件,因此未加载完时只会渲染LoadingIndicator。

原本的模块接口index.js则是一个完整的组件。

那么问题来了,在加载了组件后,是怎么更新reducers、扩充store state的呢?我们来看看index.js的输出


redux提供了一个compose方法,实际上是一个函数式编程中常用的组合函数,它接受若干个函数,并依次从右向左执行,上一个函数的结果作为下一个函数的参数。因此,这里export出去之前,Homepage经过了三层加工。

当执行到withReducer的时候,withReducer是一个函数。该函数接收一个组件WrappedComponent作为参数(这个案例中,接收的组件是前一层HoC即withSaga输出的组件),然后在函数体中创建一个组件ReducerInjector,组件ReducerInjector 的render函数直接返回WrappedComponent,如下:


简化后如下:

const widthReducers=({key,reducer})=>(wrappedComponent)=>{
    class ReducerInjector extends React.Component {
      static contextTypes = {
        store: PropTypes.object.isRequired,  //从这里获取store
    };
      componentWillMount() {
        injectReducer(this.context.store)(key, reducer);  //在这里传入store、reducer函数的名称、reducer函数
      }  
      render(){
         return <WrappedComponent {...this.props} />;    //直接将输入的组件返回
      }
    }
  return reducerInjector
}

显然,关键的地方在于componentDidMount做了一件事情:调用一个injectReducer函数,该函数主要执行这两行代码: 

(store)=>(key,reducer)=>{
   store.asyncReducer[key] = reducer; 
   store.replaceReducer(createReducer(store.asyncReducers))
}

这是该案例中最核心的两行代码!!!当我们异步获取组件AsyncHomePage并挂载完成时,AsyncHomePage在componentDidMount中对store的reducers进行更新。

这里要留意的是,ReducerInjector组件是通过react提供的contextAPI来获取store的。因为Provider内部已经在getChildContext里返回了store,所以我们只需要在组件里定义contextTypes获取store即可。


下面我补充一下对核心两行代码的描述。

首次加载app并生成初始的store之后,我们给store添加了一个属性asyncReducers:

store.asyncReducers={}  //每加载一个redux module,其reducer名称作为asyncReducer的属性,其reducer函数作为属性值,如加载完AsyncHomePage之后,store.asyncReducer={'home':homeReducer}

并将combineReducers这一步做了封装:

const createReducer=(asyncReducers)=>{
  return combinereducers({
    ReducerNameA:reducerA,
    ReducerNameB:reducerB,
    ...asyncreducers    //异步获取的redux模块的reducer都保存在store.asyncreducer,通过扩展运算符都给了combineReducers
  })
}

以后的每次异步加载redux模块,就执行一次injectReducer函数,更新store的reducer函数。


//有空再讨论另一种实现方式

------异步加载redux模块 结束------

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页