• 首页
  • 关于
    • 前端行知录 photo

      前端行知录

      前端路漫漫,行知方知行

    • Email
    • Github
  • 文章
    • 所有文章
    • 所有标签
  • 作品

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。

参考资料

  • proposal-dynamic-import
JavaScript Theory

alcat2008

Dreamer, Practitioner, Incomplete Front-ender

← “跨界”有益 requestAnimationFrame 与 requestIdleCallback →