1.Tree-Shaking
Tree-Shaking的作用是过滤掉没有用到的JS代码和CSS代码。如果不过滤的话会将没有用到的代码也会打包, 这样就会增加打包的体积, 降低网页的性能。
参考文档: tree-shaking
JS模块Tree-Shaking
- 开发环境
webpack.config.dev.js
module.exports = {
// 告诉webpack只打包用到的JS代码
optimization: {
usedExports: true
},
}
package.json
{
// 告诉webpack哪些文件不做Tree-Shaking, 不过滤掉
"sideEffects": ["*.css", "*.less", "*.scss"],
}
-
生产环境
无需进行任何配置, webpack默认已经实现了Tree-Shaking
CSS模块Tree-Shaking
不光JS模块可以进行Tree-Shaking, CSS模块也可以进行Tree-Shaking
因为项目上线的时候才需要过滤, 所以这里只介绍生产环境如何对CSS模块过滤
参考文档: PurifyCSS Plugin
- 安装相关插件
npm i -D purifycss-webpack purify-css glob-all
-
配置插件
webpack.config.prod.js
const PurifyCSS = require("purifycss-webpack");
const glob = require("glob-all");
const path = require('path');
module.exports = {
plugins: [
new PurifyCSSPlugin({
// 告诉PurifyCSSPlugin需要过滤哪些文件
paths: glob.sync([
// 要做CSS Tree Shaking的路径文件
path.join(__dirname, 'src/*.html'),
path.join(__dirname, 'src/js/*.js'),
]),
})
]
2. Code-Splitting
Code-Splitting就是将不经常修改的模块打包到单独的文件中, 避免每次修改用户都需要重新下载所有内容
如何开启Code-Splitting
- 手动分割(了解)
- 在单独文件中引入模块, 将模块中的内容添加到window上, 例如:
custom.js
import $ from 'jquery';
window.$ = $;
index.js
window.$('html').css({ width: '100%', height: '100%' });
- 修改配置文件同时打包多个文件
webpack.config.common.js
module.exports = {
entry: {
other: './src/js/custom.js', // 先打包会被先引入
main: './src/js/index.js'
},
output: {
filename: "js/[name].js",
path: path.resolve(__dirname, "bundle")
}
}
手动分割操作麻烦, 可以使用webpack自动分割
-
自动分割
webpack会自动判断是否需要分割, 如果需要会自动帮助我们分割
只需要在配置文件中, 新增一个配置项
参考文档: code-splitting
webpack.config.common.js
module.exports = {
// 告诉webpack需要对代码进行分割
optimization: {
splitChunks: {
chunks: 'all'
}
},
}
3. SplitChunksPlugin
webpack在代码分割的时候底层使用的其实是Split-Chunks-Plugin来实现代码分割的,所以这个插件的作用就是进行代码分割
参考文档: SplitChunksPlugin
Split-Chunks-Plugin相关配置
webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'async', // 对哪些代码进行分割 async(只分割异步加载模块)、all(所有导入模块), 默认为async
minSize: 30000, // 表示被分割的代码体积至少有多大才分割(单位是字节)
minChunks: 1, // 表示node_modules中的模块至少被引用多少次数才分割,默认为1
maxAsyncRequests: 5, // 异步加载并发最大请求数(保持默认即可)
maxInitialRequests: 3, // 最大的初始请求数(保持默认即可)
automaticNameDelimiter: '~', // 指定被分割出来的文件名称的连接符
name: true, // 拆分出来块的名字使用0/1/2...(false) 还是指定名称(true)
// cacheGroups: 缓存组, 将当前文件中导入的所有模块缓存起来统一处理
cacheGroups: {
// vendors: 专门用于处理从node_modules中导入的模块, 会将所有从node_modules中导入的模块写入到一个文件中去
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10 // 抽取公共代码的优先级,数字越大,优先级越高
},
// default: 专门用于处理从任意位置导入的模块, 会将所有从任意位置导入的模块写入到一个文件中去
default: {
minChunks: 2, // 表示node_modules之外中的模块至少被引用多少次数才分割,默认为1
priority: -20,
reuseExistingChunk: true // 是否复用分割的代码, 如果当前代码块包含的模块已经有了,就不在产生一个新的代码块
}
}
}
},
}
【注意】
- 如果我们导入的模块同时满足了vendors和default两个条件, 那么就会按照优先级来写入
例如: 我们导入了jQuery, jQuery存放在了node_modules目录中
所以满足vendors的条件, 也满足default条件, 但是vendors的条件的优先级高于default的优先级,就只会执行vendors规则, 只会写入到vendors对应的文件中去 - 默认情况下如果所有的模块都是从node_modules中导入的, 那么会将所有从node_modules中导入的模块打包到同一个文件中去。
- 默认情况下如果所有的模块都不是从node_modules中导入的, 那么会将所有不是从node_modules中导入的模块打包到同一个文件中去
- 如果当前文件中导入的模块有的是从node_modules中导入的, 有的不是从node_modules中导入的,那么就会将所有从node_modules中导入的打包到一个文件中,就会将所有不是从node_modules中导入的,中导入的打包到另一个文件中。
4. 异步加载模块(懒加载)
官网解释:懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
参考文档: 懒加载
示例
我们增加一个交互, 当用户点击按钮的时候在body中新增一个div。但是会等到第一次交互的时候再加载那个代码块
index.js
const oBtn = document.querySelector('button');
oBtn.onclick = function() {
getComponent().then(($div) => {
document.body.appendChild($div[0]);
});
};
// 第一种写法
function getComponent() {
// import()动态加载返回的是一个promise
return import('jquery').then(({ default: $ }) => {
const $div = $('<div>我是div</div>');
return $div;
});
}
// 第二种写法
async function getComponent() {
const { default: $ } = await import('jquery');
const $div = $('<div>我是div</div>');
return $div;
}
在上述代码中,直接使用import()去动态加载资源,是es6草案中语法,并不是正式语法,所以直接使用会报错,需要配置相关的语法动态导入的插件@babel/plugin-syntax-dynamic-import,并在webpack中做简单配置:
webpack.config.js
module.exports = {
...
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
'@babel/preset-env',
'@babel/preset-react'
],
plugins:[
'@babel/plugin-syntax-dynamic-import'
]
}
}
}
]
},
...
}
5. Prefetching
通过异步加载(懒加载)的方式确实可以优化我们的代码,但是也存在一定的弊端, 弊端就是用到的时候再加载, 那么用户需要等待加载完成后才能使用。
例如: 弹出登录框的时候有一些业务逻辑, 如果这些业务逻辑使用懒加载的话, 那么只有加载完用户才能操作登录框
解决方案:
加载完当前需要使用的所有模块之后, 在空闲的时间提前把异步加载的模块也加载进来
这样既不会影响到第一次的访问速度, 还可以提升异步加载的速度较少用户等待的时间
所以就有了Prefetching
Prefetching: 空闲的时候加载
也就是等当前被使用的模块都加载完空闲下来的时候就去加载, 不用等到用户用到时再加载
使用方式:
异步加载时写上魔法注释即可
/* webpackPrefetch: true */
例如:
function getComponent() {
return import(/* webpackPrefetch: true */'jquery').then(({ default: $ }) => {
const $div = $('<div>我是div</div>');
return $div;
});
}
还可以利用魔法注释修改分割代码的名称
异步加载时在加载模块前面写上魔法注释, 例如:
import(/* webpackChunkName: "jquery" */"jquery").then();
6. 长缓存优化
浏览器缓存问题
浏览器会自动缓存网页上的资源, 以便于提升下次访问的速度,但正式因为浏览器的缓存机制, 导致文件内容被修改之后只要文件名称没有发生变化,就不会重新去加载修改之后的资源, 所以刷新网页后显示的还是修改之前的内容。
为了解决这个问题, 我们就需要在打包文件的时候给"文件名称加上内容的hash值",一旦内容发生了变化, 内容的hash值就会发生变化, 文件的名称也会发生变化; 一旦文件的名称发生了变化, 浏览器就会自动去加载新打包的文件
hash / chunkhash / contenthash
webpack提供了三种不同的hash
-
hash:
根据每次编译打包的内容生成的哈希值, 每次打包都不一样, 不能很好利用缓存, 不推荐 -
chunkhash:
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。
在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。
【注意】: 只支持css和js, 不支持img等其它资源 -
contenthash(推荐):
根据某个文件内容生成的哈希值, 只要某个文件内容发生改变,该文件的contenthash就会发生变化
示例
webpack.config.js
{
loader: 'file-loader',
options: {
name: '[name].[contenthash:8].[ext]'
}
}
manifest
webpack在打包时,会把库和业务代码之间的关系做manifest,它既存在于业务代码(main.js),也存在于库中(vendor.js),在旧版webpack中(webpack4之前),mainfest在每次打包的时候的时候可能会变化,所以contenthash值也会跟着变化。配置runtimeChunk后,会把manifest提取到runtime中,这样打包就不会影响到其他js了。
参考文档: manifest
webpack.config.js
module.exports = {
optimization: {
runtimeChunk: "single",
}
}
7. ProvidePlugin
ProvidePlugin可以自动加载模块,而不必到处 import 或 require 。
这个插件是webpack内置的模块, 不需要安装, 只需要在用的时候导入webpack模块并进行配置即可
参考文档: ProvidePlugin
示例: 使用:jQuery
webpack.config.js
const Webpack = require('webpack');
module.exports = {
plugins: [
new Webpack.ProvidePlugin({
// 将$变量指向jQuery模块, 在全局使用$代表jQuery
$: 'jquery'
})
]
}
8.imports-loader
imports-loader和Provide-Plugin功能一样可以实现全局导入, 但是imports-loader的功能比Provide-Plugin更强大。imports-loader除了可以实现全局导入以外, 还可以修改全局this指向。
默认情况下模块中的this指向一个空对象, 我们可以通过imports-loader实现让this指向window
参考文档: imports-loader
如何使用imports-loader
- 安装imports-loader
npm install imports-loader
-
配置imports-loader
webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
// 将$变量指向jQuery模块, 在全局使用$代表jQuery
loader: "imports-loader?$=jquery"
}
]
}
};
-
修改全局this
默认全局this是一个空对象, 如何将this改为window
webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
use: "imports-loader?this=>window"
}
]
}
};
- 使用imports-loader遇到的问题
在修改了全局的this为window后, 又在js文件中使用了import导入模块, 会出现'import' and 'export' may only appear at the top level错误提示
index.js
import $ from 'jquery';
$('div').css({ width: '300px', height: '300px', background: 'blue' });
console.log(this);
原因:
如果通过imports-loader修改了模块中this的指向,那么imports-loader会自动将模块中的所有代码放到一个自调用函数中。
(function () {
// my module
}.call(window));
如果在模块中用到了import, 那么在ES Module的规范中,import语句必须写在最前面, 否则就会报错。
所以如果通过imports-loader修改了模块中this的指向,而在模块中又用到了import, 那么import就不在第一行了,所以就报错了。
解决方案:
不要去修改this指向, 直接在模块中使用window。
如果你非要修改this指向,那么在导入模块的时候必须将import语法改为require
所以: 在开发中如果需要实现全局导入, 更推荐使用ProvidePlugin来实现,因为ProvidePlugin是webpack内置的官方插件更靠谱。也不要去修改this指向,直接用window即可。
9. resolve
resolve用于配置导入模块的解析规则
参考文档: resolve
- 映射导入路径, 简化导入代码
resolve: {
// 创建 import 或 require 的别名,来确保模块引入变得更简单
alias: {
bootstrap: path.resolve(__dirname, "bootstrap/dist/css/bootstrap.css")
},
}
- 修改入口查找顺序, 简化导入代码
resolve: {
// 指定模块入口的查找顺序
mainFields: ["style", "main"],
}
-
修改查找顺序, 简化导入代码
在导入语句没带文件后缀时,webpack会自动带上后缀去尝试访问文件是否存在。resolve.extensions用于配置在尝试过程中用到的后缀列表
resolve: {
// 指定导入模块查找顺序
extensions: [".css", ".js"]
}
-
指定查找范围
通过import导入模块的时候会先在node_modules中查找, 找不到再逐级向上查找,这样在打包的时候非常消耗性能,。能不能在打包的时候让webpack只去指定的目录查找,那就是通过resolve的modules。
resolve: {
// 指定查找范围, 告诉webpack只在node_modules中查找
modules: ["node_modules"],
}
10. noParse
默认情况下无论我们导入的模块(库)是否依赖于其它模块(库), 都会去分析它的依赖关系。但是对于一些独立的模块(库)而言, 其根本不存在依赖关系, 但是webpack还是会去分析它的依赖关系这样就大大降低了我们打包的速度。所以对于一些独立的模块(库), 我们可以提前告诉webpack不要去分析它的依赖关系,这样就可以提升我们的打包速度。
参考文档: module
-
如何告诉webpack这是一个独立的模块(库)
webpack.config.js
module.exports = {
module: {
noParse: /jquery/,
}
}
11. IgnorePlugin
IgnorePlugin是webpack的一个内置插件,用于忽略第三方包指定目录,让指定目录不被打包进去。
参考文档: ignore-plugin
示例
webpack.config.js
module.exports = {
plugins: [
// 在打包moment这个库的时候, 将整个locale目录都忽略掉
new Webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
}
12. externals
externals的作用就是将不会发生变化的第三方模块(库)设置为外部扩展,避免将这些内容打包到我们的项目中, 从而提升打包速度。
参考文档: externals
externals使用
-
手动全局引入第三方模块
index.html
<head>
...
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>
</head>
-
在配置文件中告诉webpack这是一个外部扩展库, 不需要打包
webpack.config.js
module.exports = {
// 告诉webpack哪些第三方模块不需要打包
externals: {
// 告诉webpack我们在通过import导入jquery的时候, 不是导入node_modules中的jquery,而是导入我们全局引入的jquery
jquery: '$',
lodash: '_'
}
}
13. dll动态链接库
dll动态链接库和externals功能其实是一样的, 都是用于防止重复打包不会发生变化的第三方模块, 都是用于提升webpack打包效率的。只不过externals不太符合前端的模块化思想, 所以就有了dll动态链接库。
参考文档: add-asset-html-webpack-plugin、dllreferenceplugin
如何实现让第三方模块只打包一次
1. 单独配置一个config.js文件打包不会发生变化的第三方库
webpack.config.dll.js
const path = require('path');
module.exports = {
mode: 'production',
entry: {
vendors: 'jquery'
},
output: {
filename: '[name].[contenthash:8].js',
path: path.resolve(__dirname, 'dll'),
library: '[name]' // 表示打包的是一个库, 表示将打包的内容通过全局变量暴露出去
}
};
2. 通过插件将打包好的库引入到界面上
npm install --save-d add-asset-html-webpack-plugin
webpack.config.js
module.exports = {
...
plugins: [
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, 'dll/vendors.dll.js')
}),
]
}
【注意】该插件需要配合HtmlWebpackPlugin使用, 并且需要在HtmlWebpackPlugin后创建
3. 生成动态库的映射关系
因为我们有可能将几个库打包到一个文件中, 所以需要生成一个映射文件方便webpack能够从中找到对应的库
webpack.config.dll.js
module.exports = {
...
plugins: [
// DllPlugin作用:在打包第三方库的时候生成一个清单文件
new Webpack.DllPlugin({
// 这里的name必须和library一致
name: '[name]',
path: path.resolve(__dirname, 'dll/[name].manifest.json')
})
]
}
4. 告诉webpack去哪里查找动态库
在打包的时候如何webpack回到指定的映射文件中查找对应的动态库, 找打了那么就不会重新打包动态库中的内容了, 如果找不到才会重新打包
webpack.config.js
module.exports = {
...
plugins: [
new Webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll/vendors.manifest.json')
}),
]
}
5. 动态链接库的优势
不用手动将第三方库插入到HTML中
所有第三方库只会被打包一次
6. 当前动态链接库存在的问题
如果我们提前打包生成了多个文件和清单, 那么需要手动增加插入的文件和查询的清单
如何解决
通过NodeJS代码动态添加
webpack.config.js
const plugins = [
// 将插件代码提取出来
]
// 动态添加AddAssetHtmlPlugin和DllReferencePlugin插件
const dllPath = path.resolve(__dirname, 'dll');
const files = fs.readdirSync(dllPath);
files.forEach(function (file) {
if(file.endsWith(".js")){
plugins.push(new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, 'dll', file)
}));
}else if(file.endsWith(".json")){
plugins.push(new Webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll', file)
}));
}
});
module.exports = {
...
plugins: plugins
}
我们发现完整地配置dll动态链接库是非常麻烦的, 那么有没有什么办法简化代码呢?答案是有的, 请移步到https://juejin.im/post/5d8aac8fe51d4578477a6699
14. HappyPack
默认情况下webpack打包项目是单线程的, 为了提升打包速度, 充分发挥多核 CPU 电脑的威力,我们可以通过HappyPack让webpack实现多线程打包
参考文档: happypack
HappyPack使用
- 安装HappyPack
npm install --save-dev happypack
-
配置HappyPack
webpack.config.js
const HappyPack = require('happypack');
module.exports = {
...
module: {
rules: [{
test: /.js$/,
use: 'happypack/loader',
include: [ /* ... */ ],
exclude: [ /* ... */ ]
}]
},
plugins: [
new HappyPack({
id: 'js',
use: [/*处理文件规则*/]
]
}
15. 多页面打包
我们开发不可能只写一个页面,每次都要写很多页面,这时为了开发效率,我们使用前端自动化工具webpack,那么是如何打包多页面的呢?
参考文档: html-webpack-plugin
需求:利用webpack打包生成两个页面
一个页面叫做index, 一个页面叫做detail
1. 有多少个界面就指定多少个入口, 并给不同的入口指定不同的名称
module.exports = {
...
entry: {
index: './src/js/index.js',
detail: './src/js/detail.js'
},
}
2. 有多少个界面就创建多少个HtmlWebpackPlugin, 并给不同的界面配置不同的名称
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'detail.html',
}),
]
}
3. 在HtmlWebpackPlugin中通过chunks属性告知需要插入到当前界面的文件
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
chunks: ['index', 'vendors~index']
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'detail.html',
chunks: ['detail', 'vendors~detail']
}),
]
}
多页面打包优化
当前打包多页面应用存在的问题
有多少个界面就要编写多少个入口,有多少个界面就要创建多少个HtmlWebpackPlugin,并且每个HtmlWebpackPlugin中的配置都不一样。
如何解决以上问题
入口还是必须手动指定, 但是创建多少个HtmlWebpackPlugin和如何配置HtmlWebpackPlugin可以通过代码动态生成。也就是原理和动态创建AddAssetHtmlPlugin/DllReferencePlugin(dll动态链接库)一样。
webpack.config.js
// 将原来的配置抽取出来
const config = {
...
entry: {
index: './src/js/index.js',
detail: './src/js/detail.js',
},
}
config.plugins = makePlugins(config);
function makePlugins(config) {
const plugins = [/*原来的插件*/];
// 拿到入口文件名
Object.keys(config.entry).forEach(function (key) {
// 动态添加HtmlWebpackPlugin插件
plugins.push(new HtmlWebpackPlugin({
template: './src/index.html',
filename: key + '.html',
chunks: [key, 'vendors~'+ key]
}),);
});
return plugins;
}
module.exports = config;
16. webpack-bundle-analyzer
webpack-bundle-analyzer是一个可视化的打包优化插件,会将打包的结果以图形化界面的方式展示给我们。从webpack-bundle-analyzer生成的图形化界面中我们可以很清楚的知道模块之间的依赖关系、模块大小、模块有没有重复打包,、重复引用等。
从而针对性的对我们的代码进行优化
参考文档: webpack-bundle-analyzer
如何使用webpack-bundle-analyzer
1. 安装webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
2. 配置webpack-bundle-analyzer*
webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
最后执行打包命令,在打包结束以后,默认会直接在浏览器里把最终的动态treemap图片展示出来
在这个树形图片里,会有包含下面的内容:
-
每个打包以后的bundle文件里面,真正包含哪些内容,项目里的module、js、component、html、css、img最后都被放到哪个对应的bunlde文件里了。
-
每个bundle文件里,列出了每一个的module、componet、js具体size,同时会列出start size、parsed
start size:原始没有经过minify处理的文件大小
parse size:比如webpack plugin里用了uglify,就是minified以后的文件大小
gzip size:被压缩以后的文件大小
基于以上给出的信息, 你就能比较直观的在图片里看到,哪些公用library被重复打包到不同的bundle文件里,或者是说哪一个过大影响性等等;从而你就可以对你的webpack打包方式进行优化。
网友评论