Webpack 之按需/异步加载/Code Splitting
09 Aug 2018
Reading time ~2 minutes
先看一段几乎无处不在的百度统计代码,Google 统计类似。
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?<xxxxx>";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
这就是常规的按需加载的实现方案了,即:将相关的 JavaScript 逻辑代码放到一个文件中,在需要的时候,创建一个 script 对象,插入 document 对象,由浏览器加载并执行该文件。
在大型的应用中,都有模块化方案,那若想对其中的某些内容进行按需加载,该怎么办呢?当前,主流的模块管理工具,都为此提供了相关的 API 支持。
Webpack
提到 Webpack 的异步加载,不得不提及 bundle-loader,源码 很简单,最终会输出一段 JavaScript 代码,如下:
var cbs = [], data;
module.exports = function(cb) {
if(cbs) cbs.push(cb);
else cb(data);
}
require.ensure([], function(require) {
data = require("xxx");
var callbacks = cbs;
cbs = null;
for(var i = 0, l = callbacks.length; i < l; i++) {
callbacks[i](data);
}
});
其中的核心就是 require.ensure
,基于 CommonJS 语法风格,使用方式为:
require.ensure(
dependencies: String[], // 依赖模块列表
callback: function(require), // 模块加载完成后的回调,在其中可以使用 require 载入模块
errorCallback: function(error),
chunkName: String
)
require.ensure 在 Webpack 生态中有举足轻重的地位,作为实现 Code-Splitting 的基石,为 Webpack 的开疆扩土立下了汗马功劳。
新人换旧人
随着技术的发展,人们已经不满足于 require.ensure “侵入式” 的实现方案,Webpack 也尝试输出更为优雅的 code splitting 方案,ECMAScript proposal 及时出现了。
Dynamic Imports 文档中明确的指出,Webpack 支持两种动态代码拆分技术。
- 符合 ECMAScript proposal 的
import()
语法,推荐使用 - 传统的
require.ensure
import()
import(‘path/to/module’) -> Promise
import() 用于动态加载模块,其引用的模块及子模块会被分割打包成一个独立的 chunk。
Webpack 还允许以注释的方式传参,进而更好的生成 chunk。
// single target
import(
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
'module'
);
// multiple possible targets
import(
/* webpackInclude: /\.json$/ */
/* webpackExclude: /\.noimport\.json$/ */
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
`./locale/${language}`
);
理解和使用 ,有两点需要特别注意:
- import() 不同于
import
,该方法为了动态加载模块而引入的新语法 - import() 返回结果是
Promise
路由
回归到实际业务场景,页面基本上都是通过路由的方式呈现,如果按照路由的方式实现页面级的异步加载,岂不是方便很多。
以 react-router 2.x 版本为例,提供了 getComponent 方法,可以直接实现异步的代码分割。
<Route
path="courses/:courseId"
getComponent={(nextState, cb) => {
// do asynchronous stuff to find the components
cb(null, Course)
}}
/>
在 React 官方的 Code-Splitting 文档中,介绍了最新的实现异步加载的方法。利用 import() + React Loadable,能优雅的实现基于 react-router 4.x 版本的路由分割,参见 Route-based code splitting
import Loadable from 'react-loadable';
import Loading from './Loading';
const LoadableComponent = Loadable({
loader: () => import('./Dashboard'),
loading: Loading,
})
export default class LoadableDashboard extends React.Component {
render() {
return <LoadableComponent />;
}
}
React Loadable
react-loadable 源码 比较直观,理解后就啥都明白了,主要是由两部分组成。
component
function load(loader) {
let promise = loader(); // 再次强调: import() 返回结果是 `Promise`
let state = {
loading: true,
loaded: null,
error: null
};
state.promise = promise.then(loaded => {
state.loading = false;
state.loaded = loaded;
return loaded;
}).catch(err => {
state.loading = false;
state.error = err;
throw err;
});
return state;
}
这段代码就是用来解析 loader: () => import('xxx')
的,loaded
就是加载的组件。
render
render() {
if (this.state.loading || this.state.error) {
return React.createElement(opts.loading, {
isLoading: this.state.loading,
pastDelay: this.state.pastDelay,
timedOut: this.state.timedOut,
error: this.state.error,
retry: this.retry
});
} else if (this.state.loaded) {
return opts.render(this.state.loaded, this.props);
} else {
return null;
}
}
// opts.render 的实际指向
function resolve(obj) {
return obj && obj.__esModule ? obj.default : obj;
}
function render(loaded, props) {
return React.createElement(resolve(loaded), props);
}
这里就是渲染 loaded
的 render
方法。
结尾
希望看完这些,你能明白在 Webpack 构建生态下,如何 Code Splitting,能根据业务需求合理选型,或者实现自己的 React Loadable。
参考资料
alcat2008
Dreamer, Practitioner, Incomplete Front-ender