认识 DLL
在介绍 DllPlugin 前先给大家介绍下 DLL。 用过 Windows 系统的人应该会经常看到以 .dll
为后缀的文件,这些文件称为动态链接库(dynamic link library),在一个动态链接库中可以包含给其他模块调用的函数或数据。
要给 Web 项目构建接入动态链接库的思想,需要完成以下事情:
- 把网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中去。一个动态链接库中可以包含多个模块。
- 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次被打包,而是去动态链接库中获取。
- 页面依赖的所有动态链接库需要被加载。
为什么给 Web 项目构建接入动态链接库的思想后,会大大提升构建速度呢? 原因在于包含大量复用模块的动态链接库只需要编译一次,在之后的构建过程中被动态链接库包含的模块将不会在重新编译,而是直接使用动态链接库中的代码。 由于动态链接库中大多数包含的是常用的第三方模块,例如 react、react-dom,只要不升级这些模块的版本,动态链接库就不用重新编译。
webpack 打包速度问题
在使用 webpack 进行打包时候,对于依赖的第三方库,比如 vue,vuex ,react,react-dom ,react-router,antd,iview,
等等这些不会修改的依赖,我们可以让它和我们自己编写的代码分开打包,这样做的好处是每次更改我本地代码的文件的时候,webpack只需要打包我项目本身的文件代码,而不会再去编译第三方库,那么第三方库在第一次打包的时候只打包一次,以后只要我们不升级第三方包的时候,那么 webpack 就不会对这些库去打包,这样的可以快速的提高打包的速度。因此为了解决这个问题,DllPlugin 和 DllReferencePlugin 插件就产生了。
DllPlugin 和 DllReferencePlugin 插件的好处
- 开发时提高webpack的打包速度。
- 分割了最后的打包包,资源加载包不会过大。
Webpack 已经内置了对动态链接库的支持,需要通过2个内置的插件接入,它们分别是:
- DllPlugin 插件:用于打包出一个个单独的动态链接库文件和索引的任务列表。
- DllReferencePlugin 插件:用于在主要配置文件中根据索引的任务列表去找 DllPlugin 插件打包好的动态链接库文件。
测试 DllPlugin
通过一个例子来看看 DllPlugin 的作用,它到底是干了什么?
我们先建一个 test.js
文件,文件内容为:
module.exports = "Condor Hero";
动态链接库文件需要一份单独的 webpack 配置导出给主配置文件用。新建一个 Webpack 配置文件 dll.config.test.js
,文件内容如下:
const path = require("path");
module.exports = {
mode : "development",
entry:{
test:"./test.js"
}
output:{
filename:"[name].dll.js",
path : path.join(__dirname,"dist")
}
}
通过命令行来打包文件:
webpack --config dll.config.test.js
去看看打包后的 test.dll.js 文件:
很明显这个模块是个闭包,还是个 IIFE ,引入的文件作为参数传进去,函数本身返回了结果但是并没有变量进行接收。
所以如果我们在不改变代码的情况下,定义一个变量来接收函数返回的值:
直接复制到浏览器的控制台打印出 a ,a 的结果就是:Condor Hero。
这个变量就是 DllReferencePlugin 根据依赖文件要找的东西。但是我们不可能给每个文件都手动添加一个变量接收函数返回的值,这时候 library 的作用出来了,修改我们的配置文件
library
的命名规范和 filename
相同:
const path = require("path");
module.exports = {
mode : "development",
entry:{
test:"./test.js"
},
output:{
filename:"[name].dll.js",
path : path.join(__dirname,"dist"),
library:"[name]"
}
}
这时候打包输出的 test.dll.js 文件变动地方如下:
一般 library 这样写
// 之所以在前面加上 _dll_ 是为了防止全局变量冲突 library: '_dll_[name]',
打包时自动 var 了个变量进行接收函数返回的值。如果不想使用 var 导出可以使用 libraryTarget
来定义导出方式。例如我们这样设置:libraryTarget:"commonjs"
。打包结果:
其值还可以为:umd,umd2,commonjs2,commonjs,amd.this,var,windows,global,jsonp
等等。
接下来引入 DLLPlugin 插件了:不过首先的把 entry 变成数组的形式,不然会报错。test: ["./test.js"]
。修改配置文件:
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "development",
entry: {
test: ["./test.js"]
},
output: {
filename: "[name].dll.js",
path: path.join(__dirname, "dist"),
library: "_dll_[name]",//_dll_ 是为了防止全局变量冲突
},
plugins: [
// 接入 DllPlugin
new webpack.DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
name: '_dll_[name]',//library===这里的name
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist', '[name].manifest.json'),
}),
]
}
这时候再去 dist 文件夹里面发现里面多了个test.manifest.json
文件内容为(格式化后):
{
"name": "_dll_test",
"content": {
"./test.js": {
"id": "./test.js",
"buildMeta": {
"providedExports": true
}
}
}
}
上见 test.manifest.json 文件清楚地描述了与其对应的 test.dll.js 文件中包含了哪些模块,以及每个模块的路径和 ID。
现在再来理理思路:
使用 webpack 打包时, main.js 文件是主入口,然后进行递归打包,当遇到的模块(主文件配置通过 DllReferencePlugin寻找的)在任务列表清单里面(test.manifest.json),会直接问全局要这个模块,全局注入这个模块通过 library 完成的。所以在 index.html 文件中需要把依赖的 test.dll.js 文件给加载进去, index.html 内容应为:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script src = "test.dll.js"></script>
<script src = "bundle.js"></script>
</body>
</html>
下面我们就可以以 react 项目为例来看看 DllPlugin 的实战使用:
dll.config.dev.js 文件配置:
const path = require("path");
const webpack = require("webpack");
const vendors = [
'react',
'react-dom',
"classnames",
"jquery"
];
module.exports = {
mode: "development",
entry: {
lib: vendors
},
output: {
filename: "[name].dll.js",
path: path.join(__dirname, "dist"),
library: "_dll_[name]",//_dll_ 是为了防止全局变量冲突
},
plugins: [
// 接入 DllPlugin
new webpack.DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
name: '_dll_[name]',//library===这里的name
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist', '[name].manifest.json'),
}),
]
}
生成 dll 文件。
webpack --config dll.config.dev.js
dist 文件夹里面生成两个文件:lib.dll.js
,lib.mainfest.json
。
index.html 里面引入 lib.dll.js。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>冲突时,以我template为准</title>
</head>
<body>
<script src="https://cdn.bootcss.com/jquery/3.4.1/core.js"></script>
<!-- 注意jQuery的在 div.id 之前引入 -->
<script src = "./dist/lib.dll.js"></script>
<div id = "app"></div>
</body>
</html>
webpack.config.js 环境增加:
const webpack = require('webpack');
plugins: [
// 告诉 Webpack 使用了哪些动态链接库
new webpack.DllReferencePlugin({
// 描述 lib 动态链接库的文件内容
manifest: require('./dist/lib.manifest.json'),
})
]
这时候开发在通过 webpack-dev-server 启动项目:
打包的文件由原来的 1.44m,变成了 336k。
访问 http://127.0.0.1:8080/ 。
dll 的优劣
dll 的缓存机制带来的打包效果是非常显著的, 能大幅减少项目的打包时间, 根据上面的案例看 dll 是一种拿空间换时间的一种方式, 可以先将项目所依赖的库提前打包出来, 存储在本地磁盘中, 然后通过mainifest.json 的映射文件, 在下次打包的时候产生链接关系, 这样就可以做到, 你把这份提前打包的dll包放置在 cdn上? 因为他不会经常性的修改, 只需要在项目的第三方依赖需要修改的时候再去替换他即可, 然后每次打包都可以跳过打包第三方依赖,只处理业务代码,
但其实这样也有一个问题, 那就是整个 spa 应用就会很蠢, 本来是可以利用 code split 来达到按需加载 js 文件的功能, 但是这样一整吧, 不管用户访问哪个页面都会把整个作为 dll 的依赖包下载下来, 变相的增加了加载时间, 所以 dll 也只是推荐在开发环境使用, 生产环境还是使用 commonChunk 的方式提取公共依赖把, 虽然 commonChunk 无法为你优化打包时间。
如果是现在使用 webpack4 也不需要 commonChunk ,而是使用内置的 optimization 配置项来配置,为什们会舍弃 commonChunk?
最初,块(及其内部导入的模块)是通过内部Webpack图形中的父子关系连接的。将CommonsChunkPlugin被用来避免在这些重复的依赖,但进一步的优化是不可能的。
从webpack v4开始,CommonsChunkPlugin删除了,而改为optimization.splitChunks。
说白了就是 optimization.splitChunks
能在 CommonsChunkPlugin
的基础上按需加载。
还有个插件可以无脑提升构建速度,hard-source-webpack-plugin
,只需要在 webpack 的 plugins 中加入以下代码:
plugins: [
new HardSourceWebpackPlugin()
]
就这仅仅一行即可, 效果拔群
最后
React 中 main.js 文件内容为:
import React from "react";
import {render} from "react-dom";
render(
<h3>Dllpligin测试</h3>,app
);
发现问题没,我没通过 document.getElementById("app")
直接得到 DOM 元素。关于这个问题:
HTML 中元素的 id 属性,到底有什么作用,为什么可以直接使用
直接用id访问是旧版本js遗留下来的特性,浏览器会建立window实例的id同名属性,这是为了兼容旧的网页。
因此不要依赖这个特性,在含有特殊字符或者和window实例的其他属性有冲突时可能失效。还是用document.getElementById比较保险。
至于id的作用,其他答案也说了,就是作为元素的唯一标识,方便在js中调用,或者用CSS设置样式。
参考链接: