[TOC]

认识DLL

在Windows系统中我们会经常看到以.dll为后缀的文件,这些文件叫作动态链接库,在一个动态链接库中可以包含为其他模块调用的函数和数据。

要给Web项目构建接入动态链接库的思想,需要完成以下事情:

  1. 把网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中。在一个动态链接库中可以包含多个模块;
  2. 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次打包,而是去动态链接库中获取;
  3. 页面依赖的所有动态链接库都需要被加载。

为什么给Web项目构建接入动态链接库的思想后,会大大提升构建速度呢?原因在于:包含大量复用模块的动态链接库只需要被编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库中的代码。由于动态链接库中大多数包含的是常用的第三方模块,例如react、react-dom,所以只要不升级这些模块的版本,动态链接库就不用重新编译。

项目中使用DllPlugin

项目目录如下:

.
├── build
│   ├── webpack.config.js
│   └── webpack.dll.config.js
├── dist
│   ├── _dll_react.js
│   ├── bundle.js
│   ├── index.html
│   └── react.manifest.json
├── lib
│   ├── main.js
│   └── require.js
├── package.json
├── src
│   ├── index.html
│   └── index.js
└── yarn.lock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

依赖列表如下:

"@babel/cli": "^7.6.2",
"@babel/core": "^7.6.2",
"@babel/preset-env": "^7.6.2",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^3.0.0",
"html-webpack-include-assets-plugin": "^2.0.0", // 用于将生成的动态链接库自动插入到html文件中
"html-webpack-plugin": "^3.2.0",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"webpack": "^4.41.0",
"webpack-cli": "^3.3.9"
1
2
3
4
5
6
7
8
9
10
11
12
yarn add react react-dom
1

DllPlugin和DllReferencePlugin

Webpack已经内置了对动态链接库的支持,需要通过两个内置的插件(即Webpack自带的插件)接入,作用是将不会变动的库(比如 vue、react)拆分出来打包,避免每次都去打包,从而提升构建时的性能。分别是:

  1. DllPlugin插件:用于打包出一个个单独的动态链接库文件;
  2. DllReferencePlugin插件:用于在主要的配置文件中去引入DllPlugin插件打包好的动态链接库文件。

DLLPlugin和DLLReferencePlugin用某种方法实现了拆分bundles,同时还大大提升了构建的速度。

const webpack = require("webpack");
const path = require("path");

module.exports = {
    entry: {
        // 第三方库
        vue: ["vue", "vue-router"]
    },
    output: {
        filename: "[name].dll.js",
        path: path.resolve(__dirname, "../dist/dll")
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.join(__dirname, "../dist/dll", "[name].manifest.json")
        })
    ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

添加构建命令:

"dll": "webpack --config build/webpack.dll.config.js"
1

执行构建后会得到一个vue.dll.js、vue.manifest.json 文件。

在 webpack.prod.config.js 中使用 DllReferencePlugin 插件来关联 DllPlugin 生成的文件。

new webpack.DllReferencePlugin({
      context: path.resolve(__dirname, "../"),
      manifest: require("../dist/dll/vue.manifest.json")
});
1
2
3
4

html-webpack-plugin无法自动引入DllPlugin生成的dll,需要自行引入。可以在 html-webpack-plugin 配置的 template 中写好。有人做了插件assets-webpack-plugin和add-asset-html-webpack-plugin来更好的解决这个问题。

以_dll_react.js文件为例,其文件内容大致如下:

可见,一个动态链接库文件中包含了大量模块的代码,这些模块存放在一个数组里,用数组的索引号作为ID。并且还通过_dll_react变量把自己暴露在了全局中,也就是可以通过window._dll_react可以访问到它里面包含的模块。

其中,manifest.json文件也是由DllPlugin生成的,用于描述动态链接库文件中包含哪些模块,其文件内容大致如下:

{
  // 描述该动态链接库文件暴露在全局的变量名称
  "name": "_dll_react",
    "content": {
        "./node_modules/react-dom/index.js": {
            "id": "./node_modules/react-dom/index.js",
            "buildMeta": {
                "providedExports": true
            }
        },
        "./node_modules/react/cjs/react.development.js": {
            "id": "./node_modules/react/cjs/react.development.js",
            "buildMeta": {
                "providedExports": true
            }
        }
        ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

可见manifest.json文件清楚地描述了与其对应的dll.js文件中包含了哪些模块,以及每个模块的路径和IDmain.js文件是编译出来的执行入口文件,当遇到其依赖的模块在dll.js文件中时,会直接通过dll.js文件暴露出的全局变量去获取打包在dll.js文件的模块。所以在 index.html文件中需要把依赖的两个dll.js文件给加载进去,index.html内容如下:

<body>
    <div id="root"></div>
    <script type="text/javascript" src="_dll_react.js"></script>
    <script type="text/javascript" src="bundle.js"></script>
</body>
1
2
3
4
5

以上就是所有接入DllPlugin后最终编译出来的代码,接下来讲解如何实现。

构建出动态链接库文件

构建输出的以下这两个个文件:

├── _dll_react.js
└── react.manifest.json
1
2

和以下这一个文件:

├── bundle.js
1

是由两份不同的构建分别输出的。

动态链接库文件相关的文件需要由一份独立的构建输出,用于给主构建使用。新建一个Webpack配置文件webpack_dll.config.js专门用于构建它们,文件内容如下:

const path = require('path');
const webpack = require('webpack');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
    mode: 'development',
    // JS执行入口文件
    entry: {
        react: ['react', 'react-dom'] // 将React相关的模块放到一个单独的动态链接库
    },
    output: {
        // 输出的动态链接库的文件名称,[name]代表当前动态链接库的名称,
        filename: '_dll_[name].js',
        path: path.resolve(__dirname, '../dist'), // 输出的文件都放到 dist 目录下
        library: '_dll_[name]', // 给输出的结果加个名字,这里叫_dll_react
        libraryTarget: 'var' // 配置如何暴露library,默认为var
        // commonjs 结果放在export属性上,umd统一资源模块, 默认是var
    },
    plugins: [
       new CleanWebpackPlugin(),
       new webpack.DllPlugin({
           // 这里的name要和output中的library名称一致
           name: '_dll_[name]', // name === library
           // 描述动态链接库的 manifest.json 文件输出时的文件名称
           path: path.resolve(__dirname, '../dist', '[name].manifest.json') // manifest.json 定义了各个模块的路径
       })
    ]
}
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

使用动态链接库文件

构建出的动态链接库文件用于在其它地方使用,在这里也就是给执行入口使用。

用于输出main.js的主Webpack配置文件内容如下:

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackIncludeAssetsPlugin = require('html-webpack-include-assets-plugin');

module.exports = {
    mode: 'production',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, '../dist'),
        // libraryTarget: 'amd'
        // umdNamedDefine: true
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: 'babel-loader'
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
           filename: 'index.html',
           template: './src/index.html',
           title: '自动生成html',
           inject: true // 默认为true,插入到body标签底部
        }),
        new HtmlWebpackIncludeAssetsPlugin({
            assets: ['_dll_react.js'],
            append: false
        }),
        // 告诉 Webpack 使用了哪些动态链接库
        new webpack.DllReferencePlugin({
            // 描述 react 动态链接库的文件内容
            manifest: path.resolve(__dirname, '../dist', 'react.manifest.json'),
            // sourceType: 'amd'
        })
    ]
}
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
36
37
38
39
40
41

注意:在webpack_dll.config.js文件中,DllPlugin中的name参数必须和output.library中保持一致。原因在于:DllPlugin中的name参数会影响输出的 manifest.json文件中name字段的值,而在webpack.config.js文件中DllReferencePlugin会去manifest.json文件读取name字段的值,把值的内容作为在从全局变量中获取动态链接库中内容时的全局变量名。

执行构建

在修改好以上两个Webpack配置文件后,需要重新执行构建。重新执行构建时要注意的是:需要先将动态链接库相关的文件编译出来,因为主Webpack配置文件中定义的 DllReferencePlugin依赖这些文件。

"scripts": {
    "build": "webpack --config build/webpack.config.js",
    "dll": "webpack --config build/webpack.dll.config.js"
},
1
2
3
4

执行构建时流程如下:

  1. 如果动态链接库相关的文件还没有编译出来,就需要先把它们编译出来。方法是执行yarn run dll命令;
  2. 在确保动态链接库存在时,才能正常的编译出入口执行文件。执行yarn run build命令,这时会发现构建速度有了非常大的提升。

libraryTarget指定为amd

output: {
     filename: '_dll_[name].js', // 生成的文件名
     path: path.resolve(__dirname, '../dist'),
     library: '_dll_[name]', // 给输出的结果加个名字,这里叫_dll_react
     libraryTarget: 'amd' // 配置如何暴露library,默认为var
     // commonjs 结果放在export属性上,umd统一资源模块, 默认是var
 }
1
2
3
4
5
6
7
new webpack.DllReferencePlugin({
   // 描述 react 动态链接库的文件内容
   manifest: path.resolve(__dirname, '../dist', 'react.manifest.json'),
   sourceType: 'amd'
})
1
2
3
4
5
output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, '../dist'),
     libraryTarget: 'amd'
}
1
2
3
4
5

需要注意:libraryTarget指定为amd,打包后的入口文件bundle.js和_dll_react.js都是AMD语法格式。

main.js:

;(function() {
    require.config({
        baseUrl: './', // 基础路径,出发点在根目录下
        paths: {
        }
    });
    require(['bundle'], function(bundle) {
        console.log('加载了');
    });
})()
1
2
3
4
5
6
7
8
9
10

入口html文件,这里需要引入AMD模块加载require.js。因为libraryTarget指定为amd后,打包后的文件都是AMD语法的,浏览器不能直接识别define和require语法。

<body>
    <div id="root"></div>
    <script data-main="main" src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
</body>
1
2
3
4

结果如下: