常用的代码分离方法有三种
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle
中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的bundle
,以及控制资源加载优先级,如果使用合理,会极大缩减加载时间。
在webpack基础篇(三):管理资源(image、css、fonts、csv、json5)我们我们讲了使用 mini-css-extract-plugin
将 CSS 从主应用程序中分离,今天我们来看 JS 代码如何分离。
常用的代码分离方法有三种:
-
入口起点:使用
entry
配置手动地分离代码。 -
防止重复:使用
Entry dependencies
或者SplitChunksPlugin
去重和分离chunk
。 -
动态导入:通过模块的内联函数
import
调用来分离代码。
1. 入口起点
这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。先来看看如何从 main bundle 中分离 another module(另一个模块):
在 src
目录下创建 another-module.js
文件:
src/index.js
console.log('Hello world!');
src/another-module.js
import _ from 'lodash'
console.log(_.join(['another', 'module', 'chunk'], ' '));
这个模块依赖了 lodash ,需要安装一下:
npm install lodash
webpack.config.js
module.exports = {
mode: 'development',
entry: { // 配置多入口文件
index: './src/index.js',
another: './src/another_module.js'
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
},
}
执行webpack
命令,可以看到报错了 ̄□ ̄||
这个错误表明发生了冲突,多个入口文件打包后出现了相同的filename
,所以我们要对多个入口文件设置多个出口不同文件名文件
webpack.config.js
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another_module.js'
},
output: {
filename: '[name].bundle.js', // 对应多个出口文件名
path: path.resolve(__dirname, './dist'),
},
}
执行webpack
命令,可以看到不报错了,并且dist
输出了两个js文件
文件another.bundle.js
来源于entry.another
,即src/another.js
,文件大小为554kb
,因为被lodash
被打包进去了
文件index.bundle.js
来源于entry.index
,即src/index.js
,文件大小为1.21kb
查看dist/app.html
可以看到,两个js文件已经被写入script中
执行npx webpack-dev-server
可以看到,js文件加载也是正常的,控制台也能打印出another module chunk
但是,如果我们的其他入口也需要使用lodash
呢?
src/index.js
import _ from 'lodash'
console.log(_.join(['index', 'module', 'chunk'], ' '));
执行webpack
命令,可以看到打包成功
但是index.bundle.js
明显变大了很多,这是因为它也将lodash
打包进去了
执行npx webpack-dev-server
,可以看到控制台打印输出了another module chunk
index module chunk
问题
我们发现,lodash
在两个引用文件中都被打包了,我们期望lodash
应该是公用的,但是使用这种方式造成了重复打包问题
2. 防止重复
2.1 配置 entry 提取公用依赖
配置 dependOn option
选项,这样可以在多个 chunk 之间共享模块:
webpack.config.js
module.exports = {
mode: 'development',
entry: {
index: {
import: './src/index.js', // 启动时需加载的模块
dependOn: 'common_chunk', // 当前入口所依赖的入口
},
another: {
import: './src/another_module.js',
dependOn: 'common_chunk',
},
common_chunk: 'lodash' // 当上面两个模块有lodash这个模块时,就提取出来并命名为shared chunk
},
output: {
filename: '[name].bundle.js', // 对应多个出口文件名
path: path.resolve(__dirname, './dist'),
},
}
执行webpack
命令,可以看到打包结果
已经提取出来common_chunk.bundle.js
,即为提取打包了lodash
公用模块
index.bundle.js
another.bundle.js
体积也变小
查看dist/index.html
可以看到三个文件都被加载了
执行npx webpack-dev-server
可以看到页面加载正常,打开控制台可以看到打印结果也正常输出了
2.2 SplitChunksPlugin
SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件,将之前的示例中重复的 lodash 模块去除:
webpack.config.js
module.exports = {
entry: { // 多入口
index: './src/index.js',
another: './src/another_module.js',
},
output: {
filename: '[name].bundle.js', // 对应多个出口文件名
path: path.resolve(__dirname, './dist'),
},
optimization: {
splitChunks: { // 代码分割
// include all types of chunks
chunks: 'all'
}
},
}
执行webpack
可以看到打包结果,文件已经被分割为chunks
执行npx webpack-dev-server
可以看到代码也是正常加载的
关于splitChunks 你可以看这篇更细致的实验文章 splitChunks.chunks 中的 async、initial 和 all
3. 动态导入
3.1 import() 动态导入
当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure 。
这里让我们尝试使用第一种方式
首先,我们将之前的代码注释一部分
webpack.config.js
module.exports = {
entry: { // 多入口
index: './src/index.js',
// another: './src/another_module.js',
},
output: {
filename: '[name].bundle.js', // 对应多个出口文件名
path: path.resolve(__dirname, './dist'),
},
optimization: {
// splitChunks: {
// // include all types of chunks
// chunks: 'all'
// }
},
}
src.index.js
// import _ from 'lodash'
//
// console.log(_.join(['index', 'module', 'chunk'], ' '));
在 src 下创建 async-module.js
文件:
Warning
注意当调用 ES6 模块的 import() 方法(引入模块)时,必须指向模块的 .default 值,因为它才是 promise 被处理后返回的实际的 module 对象。
function getComponent() {
// import 返回 Promise
// 加载一个模块
return import('lodash').then(({ default: _ }) => {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element
}).
catch((error) =>'An error occurred while loading the component')
}
getComponent().then(component => {
document.body.appendChild(component)
})
src/index.js
import './async-module';
执行webpack
,可以看到公用模块也已经被抽离了
执行npx webpack-dev-server
,可以看到页面上加载了一个Hello webpack
这表明动态导入
能实现抽离模块
那么如果动态导入
与静态导入
一起使用会发生什么呢
src/index.js将之前的注释解开
import _ from 'lodash'
console.log(_.join(['index', 'module', 'chunk'], ' '));
执行webpack
命令成功,但是我们发现并没有实现代码分离
这说明一旦我们加入了静态资源时,我们需要开启optimization.splitChunks.chunks
module.exports = {
// ...
optimization: {
splitChunks: {
// include all types of chunks
chunks: 'all'
}
},
}
执行webpack
命令,可以看到打包成功,打包结果
可以看到已经抽离出了公用模块
执行npx webpack-dev-server
可以看到资源加载成功
再次开启多入口entry
webpack.config.js
module.exports = {
entry: { // 多入口
index: './src/index.js',
another: './src/another_module.js',
},
output: {
filename: '[name].bundle.js', // 对应多个出口文件名
path: path.resolve(__dirname, './dist'),
},
optimization: {
splitChunks: { // 代码分割
// include all types of chunks
chunks: 'all'
}
},
}
执行webpack
可以看到打包结果,文件已经被分割为chunks
执行npx webpack-dev-server
可以看到资源依旧加载成功
3.2 懒加载 / 按需加载
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
我们增加一个交互,当用户点击按钮的时候做一些事情。但是会等到第一次交互的时候再加载那个代码块(math.js)
我们之前创建过src/math.js
export function add (x, y) {
return x + y
}
export function reduce (x, y) {
return x - y
}
src/index.js
const button = document.createElement('button')
button.textContent = '点击执行加法运算'
button.addEventListener('click', () => {
// 魔法注释 webpackChunkName 修改懒加载打包文件名
// 即使不使用 webpackChunkName,webpack 5 也会自动在 development 模式下分配有意义的文件名。
import(/* webpackChunkName: 'math' */ './math.js').then(({ add }) => {
console.log(add(4, 5))
})
})
document.body.appendChild(button)
执行webpack
可以看到多出了一个新的js文件,打开可以看到这个文件里包含了我们写入的add、reduce函数。可见这个模块已经被单独的抽离了
执行npx webpack-dev-server
,可以看到页面上已经有了一个按钮
上图可以看到,点击按钮后才加载math.bundle.js
并执行了函数打印输出结果
3.3 prefetch 预获取
Webpack v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:
prefetch(预获取):将来某些导航下可能需要的资源(当页面所有内容都加载完毕后,在网络空闲的时候,加载资源)
src/index.js
const button = document.createElement('button')
button.textContent = '点击执行加法运算'
button.addEventListener('click', () => {
// webpackPrefetch: true 在动态引入时开始预获取
import(/* webpackChunkName: 'math', webpackPrefetch: true */ './math.js').then(({ add }) => {
console.log(add(4, 5))
})
})
document.body.appendChild(button)
执行npx webpack-dev-server
,可以看到math.bundle.js
已经预先获取了
3.4 preload 预加载
preload(预加载):当前导航下可能需要资源
与 prefetch 指令相比,preload 指令有许多不同之处:
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
- 浏览器支持程度不同。
源码地址:https://gitee.com/yanhuakang/webpack-test
如果有用,就点个赞吧(\*^▽^\*)