背景

公司某 SPA 项目的代码越来越重,功能模块也越来多;而且里面的一个关键依赖react router的版本也有’年头’没有更新了。处于对欠技术债的恐惧以及小目标的召唤,就领了这个活:也就是标题说的两件事,升级 react router 到最新版(v4)和使用 code splitting 拆分模块。

Breaking Changes

凡是这种大的重构工程首先要保证的是不影响重构前的所有功能,为了避免产生副作用,好好过一遍官方文档尤其是Change Log是非常必要的。具体来说就是 react-router-v2 到目标版本 v4 的所有大改动, 点这里查看详情,为了节省时间,我把主要的更新日志列在这了:

[v3.0.0-alpha.2]

Jul 19, 2016

  • Breaking: Remove all deprecated functionality as of v2.6.0 ([#3603], [#3646])
  • Breaking: Support history v3 instead of history v2 ([#3647])
  • Feature: Add router to props for route components ([#3486])

[v3.0.0-alpha.1]

May 19, 2016

  • Breaking: Remove all deprecated functionality as of v2.3.0 ([#3340], [#3435])
  • Breaking/Feature: Make <Link> and withRouter update inside static containers ([#3430], [#3443])
  • Feature: Add params, location, and routes to props injected by withRouter and to properties on context.router ([#3444], [#3446])

官网非常贴心,给出了详细的迁移指导,为了节省时间,这里总结一下:

  • 依赖变化, v2.8.1 => v4.1.2(这不是废话吗)
  • 不再支持中心化的全局路由配置
  • 抽象出 react router core 包,单独的浏览器应用可以安装 react router dom 这个依赖
  • 移除了和 history 有关的接口,可以使用新的组件实现旧有功能

如果要集成 redux 使用,有几点要注意,

安装对应版本的 react-router-redux

1
npm install --save react-router-redux@next

使用提供的路由 hoc 关联路由信息到 react component - withRouter

1
2
3
4
5
6
// before
export default connect(mapStateToProps)(Something)

// after
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something)

安装独立的history依赖, 并且配置 redux store

1
npm install --save history
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
26
27
28
29
30
31
32
33
34
35
import React from 'react';
import ReactDOM from 'react-dom';

import { createStore, combineReducers, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';

import createHistory from 'history/createBrowserHistory';
import { Route } from 'react-router';

import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux';

import reducers from './reducers';

const middleware = routerMiddleware(history);

const store = createStore(
combineReducers({
...reducers,
router: routerReducer,
}),
applyMiddleware(middleware)
);

ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</ConnectedRouter>
</Provider>,
document.getElementById('root')
);

要更深度整合的话,可以参考这个Deep Intergration?(嘴上说不推荐要深度整合,anyway 还是支持了)

代码分割(code splitting)

分割这事主要分两个方面,一个是怎么才能提取公共的依赖,另一个是使用啥方法可以申明某个组件是要被拆出来做独立 chunk 的;实践下来,发现这两方面的事情都和 webpack 的配置密不可分。使用 webpack2+的版本就可以同时解决这两个问题了。

主流的提取公共依赖的途径有 2 种,Dll 和 CommonsChunk 插件。
Dll+Dll Reference 插件需要配合使用,优点是独立于 webpack 运行时打包,可以减少整体打包所花费的时间。CommonsChunk 配置优化的方法也有许多,在实践过程中,可以灵活配置 minChunks 属性按引用次数和引用位置打包可以得到比较好的效果。

1
2
3
4
5
minChunks: number|Infinity|function(module, count) -> boolean,
// The minimum number of chunks which need to contain a module before it's moved into the commons chunk.
// The number must be greater than or equal 2 and lower than or equal to the number of chunks.
// Passing `Infinity` just creates the commons chunk, but moves no modules into it.
// By providing a `function` you can add custom logic. (Defaults to the number of chunks)

还有一种方法就是手动配置 verdor 的 entry,适用于项目不大,开发者对项目依赖关系又非常清楚的场景。总的来说,具体使用哪种方法还是要按照实际项目的情况作选择。

谈到代码分割的另一个方面,如何拆分独立 chunk 也有下面几个可选项。

  • dynamic import,使用直观,可以一定程度的做封装
  • bundle loader,这个是 react router 官方例子使用的方法,可配置能力强
  • proise loader, 和 bundle loader 类似但是是用 Promise 的方式调用

可能遇到的问题

  • 很多项目是使用 html plugin 使用模版 html 插入生成的 bundle 文件,使用 dll 插件的时候,html plugin 没法取得对应的 chunk name, 这种情况可以使用add-asset-html-webpack-plugin,估计以后会有新的插件或者接口支持吧
  • 查看打包和拆分情况, 推荐使用 webpack bundle analyzer, 用 treemap 的形式直观展示 chunk 的分布和占比情况,还可以设置 gzip 或者原始输入大小 (parse gzip stat)

参考文章