React-Router-v4的代码分割

React-Router v4 的一些新特性

新版 React-Router 采用了 Monorepo 管理方式,里面有若干独立的包。

  1. react-router
  2. react-router-dom
  3. react-router-native
  4. react-router-config

v4 版相比之前的版本有一些需要特别注意的地方。

  1. 部分组件不再是从 ‘react-router’ 中引入,比如 Link 需要从 ‘react-router-dom’ 中引入。
  2. 嵌套路由不在支持,需要从父组件里面再添加子路由,这也贯彻了组件即路由的思想。不过官方还是提供了一个相对友好的工具组件 ‘react-router-config’。
  3. 移除了onEnteronUpdateonLeave 事件,很多习惯了在路由状态层面处理的情况都得在组件的生命周期函数中处理。
  4. 路由匹配规则改动也很大,新的匹配规则遵循 path-to-regexp 规则。

针对这 v4 之前的版本,React-Router 官方给出了一份相对比较详细的升级指南。
Migrating from v2/v3 to v4


官方给出的文档能帮助我们很好的进行升级。但是针对服务端渲染,以及单页面应用服务端渲染后的代码分割,文档并没有给出很好的方案。
同构工程的主要的技术点:

  1. Server 端和 Client 端单独渲染出来的代码是相同的;
  2. Dynamic Imports;
  3. Code Splitting;

在这里我们探讨两种不同的 Code Splitting 的方案。

1. react-asyncmodule

react-asyncmodule (大佬同事开发),是一个专门来做代码分割的解决方案。搭配 WebpackImport 可以很好的解决工程的动态加载和代码分割。

假如现有项目用到的 React-Router 版本是 v4 之前的版本,这绝对是最友好的 Code Splitting 方案。
通常在 v4 版本之前,路由都是这样集中设置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
path: 'login',
getComponent(nextState, callback) {
require.ensure([], require => {
callback(null, require('./view/login/index'))
}, 'login')
}
},
{
path: 'song(/:id)',
getComponent(nextState, callback) {
require.ensure([], require => {
callback(null, require('./view/song/song').default)
}, 'song')
}
},
{
path: 'tiktoksong(/:id)',
getComponent(nextState, callback) {
require.ensure([], require => {
callback(null, require('./view/tiktoksong/tiktoksong'))
}, 'tiktoksong')
}
}

虽然 v4 官方推荐是 Dynamic Routing , 但是对于依然使用 Static Routing 方式集中管理路由的旧项目,v4 提供了 react-router-config 包,让我们依然可以使用集中管理路由的方式。虽然在 router config 的配置上面会和 v3 版的有一些区别,但也还好。从上面的代码中可以看到异步加载解决方案是基于 Webpack 的 require.ensure 来做的。而在 webpack 3+ 以后我们用 import() 来做异步加载。
当使用 import 时, 代码分割的配置就比之前会更复杂。在在这个时候需要引入 react-asyncmodule 方案 ,类似的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import AsyncModule from 'react-asyncmodule';

const AsyncComponent = AsyncModule({
loading: () => (<Loading />),
error: () => (<div>error</div>)
});
const Home = AsyncComponent(import('./view/index'));

const routesConfig = [{
path: '/:m?',
exact: true,
component: Home,
componentName: 'home',
fetchData: ({ dispatch, queryParams }) => {
const { s } = queryParams;
if (s) {
return (
dispatch(changeTab(2)),
dispatch(setInputValue(s)),
dispatch(setSearchState(false))
);
}
return dispatch(homeRemdRequest(queryParams));
}
}]

对应 webpack 配置文件也比以前稍微复杂。webpack(server端) 的 externals 需要做如下更改:

1
2
3
4
5
6
7
8
9
function getExternals() {
return fs.readdirSync(path.resolve(__dirname, './node_modules'))
.filter(filename => !/\.bin|react-asyncmodule/.test(filename))
.reduce((externals, filename) => {
// eslint-disable-next-line no-param-reassign
externals[filename] = `commonjs ${filename}`;
return externals;
}, {});
}

并且需要将 asyncmodule-import 放到 babel-loader 的 plugins 配置中。这样配置就差不多了,其余的都是正常的同构代码。

2. react-loadable

社区中最火的代码分割的解决方案。如同 react-loadable 文档中引用的一样:

A higher order component for loading components with dynamic imports

react-loadable 的具体用法可以参照其官方文档。但是需要注意的是其是基于动态路由的,也就是在路由设置上不能像前面那种集中配置,当然这也是 v4 版本推荐的路由设置方式。如果是一个新项目,还是推荐的。
在使用 react-loadable 同构的工程还是有一些需要注意的点:

  1. 在 server 端 app 的入口处需要加入 Loadable.preloadAll
  2. react-loadable 自带 Babel 插件 react-loadable/babel 和 Webpack 插件 react-loadable/webpack 需要分别在 babel-loader 的 ‘plugins’ 和 webpack 的 ‘plugins’ 中设置。
  3. 因为 react-loadable/webpack 会生成一份所有包信息的 json 文件。而这份文件在 server 端生成 html 的时候会用到,所以在开发者模式的时候得先执行 webpack 编译,然后再启动 server 。 如果不想这么麻烦,可以使用另外一个 npm 包 npm-run-all

最后 react-asyncmodulereact-loadable 都是基于 Component 进行代码分割的。两者的区别是:

  1. react-asyncmodule 对 loading 、 error 参数更好的复用;
  2. react-asyncmodule 的总体设置更简单,比如在 node 端不需要 preloadAll

这两种代码分割的解决方案都可以很好的帮我们完成同构,最后祝大家’同构’愉快!