cheungseol / cheungseol.github.io

2 stars 0 forks source link

Webpack in Action (2) #10

Open cheungseol opened 7 years ago

cheungseol commented 7 years ago

Code Split

通过配置 webapck, 使用 Code Split 能够把代码分离到不同的 bundle 中,从而按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级。

三种常用的 Code Split 方法:

  1. 入口起点:使用 entry 配置手动地分离代码。
  2. 防止重复:使用 CommonsChunkPlugin 去重和分离 chunk。
  3. 动态导入:通过模块的内联函数调用来分离代码。

1. 入口起点(entry points)

这种方式是最简单、直观的分离代码方法。但也存在一些问题

示例, 从 main bundle 中分离另一个模块:

项目目录:

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
  |- another-module.js
|- /node_modules

another-module.js

import _ from 'lodash';

console.log(
  _.join(['Another', 'module', 'loaded!'], ' ')
);

webpack.config.js

const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

webpack bundle 结果

Hash: 309402710a14167f42a8
Version: webpack 2.6.1
Time: 570ms
            Asset    Size  Chunks                    Chunk Names
  index.bundle.js  544 kB       0  [emitted]  [big]  index
another.bundle.js  544 kB       1  [emitted]  [big]  another
   [0] ./~/lodash/lodash.js 540 kB {0} {1} [built]
   [1] (webpack)/buildin/global.js 509 bytes {0} {1} [built]
   [2] (webpack)/buildin/module.js 517 bytes {0} {1} [built]
   [3] ./src/another-module.js 87 bytes {1} [built]
   [4] ./src/index.js 216 bytes {0} [built]

这种方式存在的问题:

  1. 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。 (如果在 ./src/index.js 中也引入过 lodash,这样就在两个 bundle 中造成重复引用)

  2. 不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。

2. CommonsChunkPlugin 防止重复

CommonsChunkPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。

用这个插件,可以解决上一个示例中重复引入的 lodash 模块的问题:

  const path = require('path');
  const webpack = require('webpack');
  const HTMLWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    plugins: [
      new HTMLWebpackPlugin({
        title: 'Code Splitting'
     }),
+     new webpack.optimize.CommonsChunkPlugin({
+       name: 'common' // 指定公共 bundle 的名称。
+     })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

webpack bundle 结果:

Hash: 70a59f8d46ff12575481
Version: webpack 2.6.1
Time: 510ms
            Asset       Size  Chunks                    Chunk Names
  index.bundle.js  665 bytes       0  [emitted]         index
another.bundle.js  537 bytes       1  [emitted]         another
 common.bundle.js     547 kB       2  [emitted]  [big]  common
   [0] ./~/lodash/lodash.js 540 kB {2} [built]
   [1] (webpack)/buildin/global.js 509 bytes {2} [built]
   [2] (webpack)/buildin/module.js 517 bytes {2} [built]
   [3] ./src/another-module.js 87 bytes {1} [built]
   [4] ./src/index.js 216 bytes {0} [built]

index.bundle.js 中已经移除了重复的依赖模块。需要注意的是,CommonsChunkPlugin 插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。

3. 动态导入(dynamic imports)

webpack 提供了两个类似的技术。第一种,也是优先选择的方式是,使用符合 ECMAScript 提案 的 import() 语法。第二种,则是使用 webpack 特定的 require.ensure。

示例:

不使用 dynamic imports 的方式:

index.js

import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";   // react router v4
import { matchRoutes, renderRoutes } from 'react-router-config';
import configureStore from './store/configureStore';

import App from './containers/App';   // 引入组件
import Home from './containers/Home';   // 引入组件

const store = configureStore();

render(
  <div className="app">
    <Provider store={store}>
       <Router>
        <div>
            <Route exact path="/" component={App}/>
            <Route path="/home" component={Home}/>
          </div>
      </Router>
    </Provider>
  </div>
  , document.getElementById('root')
);

访问的时候,在控制台可以看到网站打开时,会加载所有页面(路由)的 bundle 文件。

我们期望的是访问不同的页面(路由),依次(懒)加载相关的 bundle 文件。

实现方式如下:

import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import { matchRoutes, renderRoutes } from 'react-router-config';
import configureStore from './store/configureStore';

function asyncComponent(getComponent) {
  return class AsyncComponent extends React.Component {
 static Component = null;
 state = { Component: AsyncComponent.Component };

      componentWillMount() {
        if (!this.state.Component) {
          getComponent().then(Component => {
            AsyncComponent.Component = Component
            this.setState({ Component })
          })
        }
      }

      render() {
        const { Component } = this.state
        if (Component) {
          return <Component {...this.props} />
        }
        return null
      }
    }
}

const RealTime = asyncComponent(() =>
  import('./containers/RealTime')   // 动态加载
)
const Retention = asyncComponent(() =>
  import('./containers/Retention')   // 动态加载
)

const store = configureStore();
render(
<div className="app">
    <Provider store={store}>
       <Router>
        <div>
            **<Route exact path="/" component={App}/>**
            **<Route path="/home" component={Home}/>**
          </div>
      </Router>
    </Provider>
  </div>
  , document.getElementById('root')
);

效果对比:

不采用动态加载,访问网站时,所有的 bundle 文件一起加载。

2017-09-18 5 20 59

采用动态加载,第一次访问网站时,只加载公共的 bundle 文件 和当前页面的 bundle文件(0.bundle.js), 访问不同的路由(页面),继续加载其他的 bundle (1.bundle.js)

2017-09-18 5 23 08

参考

代码分离

Quick and dirty code splitting with React Router v4

Code splitting and v4

cheungseol commented 7 years ago

3.2 借助 react-loadable 轻松实现动态导入(dynamic imports)

import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import configureStore from './configureStore';
import Loadable from 'react-loadable';

const A = Loadable({
    loader: () => import('./containers/A'),
    loading: () => null,
});
const B = Loadable({
    loader: () => import('./containers/B'),
    loading: () => null,
});

const store = configureStore();
render(
  <div>
    <Provider store={store}>
       <Router>
        <div className="app-router-wrap">
            <Route exact path="/" component={A}/>
            <Route exact path="/realtime" component={A}/>
            <Route path="/retention" component={B}/>
          </div>
      </Router>
    </Provider>
  </div>
  , document.getElementById('root')
);

关于 Loadable 组件,参数中的 “loading” 属性为必选项,如果确实不想使用改属性,loading: () => null 即可。

参考

CRA with React Router v4 and react-loadable. Free code splitting