1.优化打包构建速度
1.修改loader处理
当处理一个文件的时候,会去匹配所有的loader,但是往往一个文件只需要一个loader处理,其他剩余的loader没必要花时间去匹配处理,此时就可以使用oneOf
选项
使用:需要处理的loader放在oneOf数组中:
module: {
rules: [
{
oneOf: [
{
// css处理
test: /\.css$/,
use: [...commonCssPlugin]
},
{
// js语法检查
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint-loader',
options: {
fix: true
}
},
{
// js兼容性处理
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {
version: 3
},
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
]
}
}
]
}
]
},
此语法的意思:当匹配到符合的文件类型,则使用该配置,后续的loader不再遍历
但是需要注意的是:不能有两个配置处理同一类型文件
以上述代码为例,有两个配置处理js
文件,这是不允许的,但是不得不需要两个配置处理js文件,做法:将需要先处理的配置提取到oneOf所处对象之外,和之前写loader配置一样即可:
module: {
rules: [
{
// js语法检查
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint-loader',
options: {
fix: true
}
},
{
oneOf: [
{
// js兼容性处理
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {
version: 3
},
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
]
}
}
]
}
]
},
2.babel缓存
场景:babel是用来做js兼容性处理的,如果有很多js模块,只修改其中几个模块,当每次webpack打包时,都要重新编译处理,很影响打包构建速度,因此可以将babel缓存起来,也即将babel处理之后的js模块缓存起来,只对其中一个变化了的js模块重新编译,而没有变化的直接从缓存中获取。这一方式和HMR功能类似,但是为什么不使用呢?HMR基于devServer,而devServer是用于开发环境便于开发的,不用于生产环境。
作用:让第二次打包构建速度更快
使用:在babel配置中加上cacheDirectory: true
{
// js兼容性处理
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {
version: 3
},
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
],
## babel缓存
cacheDirectory: true
}
},
3.多进程打包
由于js引擎是单线程的,其同一时间只能干一件事,如果要完成的任务比较多的时候,其打包速度就会比较慢。此时可以采取多进程的方式进行打包
npm i -s thread-loader
使用:在需要开启多进程的处理中在最后加上thread-loader
即可,一般给babel-loader使用,原因:一般Js文件比较大,其次babel相对于eslint既要检查又要编译等操作。
{
test: /\.js$/,
exclude: /node_modules/,
use: [
/*
开启多进程打包。
进程启动大概600ms,进程通信也有开销。
只有工作消耗时间比较长,才需要多进程打包
*/
{
loader: 'thread-loader',
options: {
worker: 2 // 开启两个进程
}
},
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {
version: 3
},
targets: {
chrome: '60'
}
}
]
]
}
}
]
}
4.externals
当不需要将某个库文件进行打包的时候,需要拒绝其的打包。例如:通过cdn引入jquery,而不想对其打包的时候。
使用:在webpack.config.js中添加
externals: {
// 拒绝jquery的打包
jquery: 'jQuery'
}
但是需要注意的是:由于没有对其打包,要使用则需要自己引入
5.dll
不同于enternals
不对第三方库文件打包,dll
则对第三方库文件进行单独打包操作,由于代码打包分隔配置optimization
只是对第三方库文件打包成一个chunk,但是有时为了当每次打包时只要库文件不变,则不对其进行再次打包操作,则可以采取dll
将目标库文件单独打包成一个chunk
用法:
新建一个文件webpack.dll.js
# webpack.dll.js
const { resolve } = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
// 最终打包生成的[name]为jquery
jquery: ['jQuery'] // 由于是数组,如果有相关jquery文件都可以引入
},
output: {
filename: '[name].js',
path: resolve(__dirname,'dll'),
library: '[name]_[hash]'// 打包的库里面向外暴露出去的内容的名称
},
plugins: [
// 打包生成一个manifest.json 提供和jquery映射
new webpack.DllPlugin({
name: '[name]_[hash]',// 映射库的暴露的内容名称
path: resolve(__dirname,'dll/manifest.json')// 输出文件路径
})
],
mode: 'production'
}
该文件的作用:将jquery单独进行打包,然后生成的向外暴露的名称以及打包之后的文件,通过插件webpack.DllPlugin
生成manifest.json
文件进行映射。
因此此文件需要单独进行运行:webpack --config webpack.dll.js
运行之后生成的内容为:
dll
jquery.js // 包含向外暴露的名称以及内容
jquery.js.LICENSE.txt // 版本说明
manifest.json // 映射表
那如何将打包之后的jquery来进行使用呢?
在webpack.config.js文件中,使用两个插件
webpack.DllReferencePlugin
和add-asset-html-webpack-plugin
第一个插件是webpack内置的不用下载,第二个插件需要下载
npm i -s add-asset-html-webpack-plugin
使用:
// 告诉webpack哪些库不参与打包,同时使用时的名称也需要改
new webpack.DllReferencePlugin({
manifest: resolve(__dirname,'dll/manifset.json')
}),
// 将某个文件输出去,并在html中自动引入该资源
new addAssetHtmlWebpackPlugin({
filepath: resolve(__dirname,'dll/jquery.js')
})
该配置的作用就是:首先告诉webpack哪些文件是不需要打包的,但是仅仅只告诉其哪些文件不打包,并没有告诉其在哪使用如何去引入使用,所以一种方式是:手动引入打包之后的jquery.js文件,其次通过第二个插件add-asset-html-webpack-plugin
进行自动引入
这样,当每次webpack进行打包的时候,都不会重复去打包jquery,而是仅仅去引用即可。
总结:
- cdn链接引用则使用
externals
- 使用自己服务器单独对第三方打包则使用
dll
2.优化代码运行的性能
1.文件资源缓存
场景:一般状态下,对于js,css等文件资源,如果后台设置了一定时间的有效期,则当浏览器获取到这些资源的时候,发现当前在有效期时间内,则将文件资源存储于缓存中(Memory cache),当下次访问的时候,则直接从缓存中获取,这样用户访问速度变快。但是会涉及到一个问题:当后台源文件被修改了,但是文件资源还没有失效,则浏览器不会对文件资源再次获取,因此后台源文件的修改不会让浏览器显示部分同步变化,使得当有严重bug时不能及时修复。
解决办法:给每个文件资源后缀加上一个hash值,这样当文件名改了之后,浏览器检测到变化会再次发送请求获取新的资源
- hash:每次webpack构建时会生成一个唯一的hash值,而采用hash方式,则会采取webpack生成的hash值来构建文件资源后缀
// 在需要输出的文件资源后缀加上hash值
output: {
filename: 'js/index.[hash:10].js',
path: resolve(__dirname, 'build')
}
new miniCssExtractPlugin({
filename: 'css/index.[hash:10].css'
}),
问题:因为js和css使用的是同一个hash值,当只修改一个文件的时候,势必会导致所有缓存失效,这样和不使用hash的效果不大
-
chunkhash:根据chunk生成的hash值,如果打包来源于同一个chunk,那么hash值就一样。同一个chunk是:通过同一个关系结构树构造的chunk块
// 在需要输出的文件资源后缀加上chunkhash值 output: { filename: 'js/index.[chunkhash:10].js', path: resolve(__dirname, 'build') } new miniCssExtractPlugin({ filename: 'css/index.[chunkhash:10].css' }),
问题:由于css等资源一般都是被入口js文件引入,同属于一个chunk,当修改js文件时,其他资源缓存仍然失效
contenthash:根据文件内容生成hash值。不同文件hash值不一样,如果该文件没有变化则hash值也不会变,这样当修改一个文件的时候,不会影响其他文件资源缓存,让代码上线运行缓存更好使用
2.tree shaking
作用:去除无用代码,减少代码体积
前提:
- 当前文件必须使用了es6模块化
- 开启production环境
有以上的前提之后,会自动实现tree shaking功能
但是不同版本的webpack会对css处理方式不一样,可能会将css文件错误当作无效的代码去除。
比如:在package.json
中配置:"sideEffects": false
(表示所有代码都没有副作用,即都可以进行tree shaking),此时产生的问题是:将css/ @babel/polyfill文件都去除掉了
解决:指定不会被tree shaking的文件
"sideEffects": ["*.css"]
3.code split
将打包输出的一个bundle拆分成几个文件,比如:一个入口文件中引入了css,jquery文件,常规形成的bundle文件只有一个,此时将css,jquery等其他文件进行拆分,便于之后的按需加载。
-
方式一:
采取多入口文件
// 多文件入口 entry: { main: './src/index.js', test: './src/print.js' }, output: { filename: 'js/[name].[contenthash:10].js',// 取entry的命名作为生成的bundle名 path: resolve(__dirname, 'build') },
此方式,针对于多页面项目,仅能分离出js文件,由于每次增加入口文件都需要修改配置文件,因此一般不采用此方式。
-
方式二:
针对于将第三方文件单独打包成一个chunk。
- 对于单入口文件而言
module.exports = { entry:'./src/index.html', output: { filename: 'main.js', path: resolve(__dirname,'build') }, # 配置打包 optimization: { splitChunks: { chunks: 'all' } } }
此方式可以将第三方文件(
node_module
)中的代码单独打包成一个chunk输出-
多入口文件
module.exports = { entry:{ index: './src/index.js', test: './src/test.js' } output: { filename: '[name].js', path: resolve(__dirname,'build') }, # 配置打包 optimization: { splitChunks: { chunks: 'all' } } }
假如:index.js和test.js都引入了第三方包,此时打包之后的结果为:自动分析多文件入口chunk中,有没有公共的文件(必须有一定大小:30kb以上)。如果有会打包成一个chunk文件,也即
index.js
test.js
jquery.js
这三个chunk作为最终输出,而index.js于test.js对jquery可以进行单独引用,而不会每个入口文件打包时都分别引入第三方文件,造成代码冗余度很高代码体积很大等问题。
-
方式二:
针对单入口文件中需要打包除第三方文件之外的其他文件
采用:import动态导入语法,能将某个文件单独打包,通过js代码,让某个文件被单独打包成一个chunk
// 在入口文件中写入 // /* webpackChunkName: 'print' */表明给这个文件固定命名为print import (/* webpackChunkName: 'print' */'./print') .then((result) => { // 文件加载成功 console.log(result);// print模块导出的内容 }) .catch(() => { console.log('出错了'); })
-
总结:
一般情况下使用方式二与方式三的结合,通过方式二将第三方文件进行打包成单独的chunk,再通过方式三将入口文件除第三方文件之外的其他文件打包成单独的chunk
4.懒加载和预加载
此处讨论的只针对于js文件
-
懒加载:
多个文件同时引入,但是有时只是希望在某件事情触发之后才加载该文件,因此可以通过懒加载,在真正需要使用该文件的时候才加载
使用:采用import动态导入语法来解决懒加载
// index.js console.log('index被加载了'); document.querySelector('#btn').onclick = function () { import(/* webpackChunkName: 'test' */'./print') .then(({ print }) => { console.log(print(2,3)); }) .catch(() => { console.log('加载失败'); }) } // 只有当事件发生之后才加载print文件
-
预加载
在使用之前预先加载js文件
正常加载:并行加载,同一个时间加载多个文件,如果文件数目增多,则会导致阻塞
预加载:等其他资源加载完毕之后,浏览器空闲了,再加载资源,极大解决了阻塞问题
// index.js console.log('index被加载了'); // webpackPrefetch: true用于指定为预加载 document.querySelector('#btn').onclick = function () { import(/* webpackChunkName: 'test' ,webpackPrefetch: true*/'./print') .then(({ print }) => { console.log(print(2,3)); }) .catch(() => { console.log('加载失败'); }) }
注意:
webpackPrefetch: true
有兼容性问题,只适用于高版本的PC端,移动端的等不怎么适用,因此需要慎用
5.PWA
PWA(Progressive Web App):渐进式web应用,是一些技术的集合。作用:让网页像app一样可以离线访问。主要由service work
和cache
完成。使用pwa使得网页在离线的情况下也能访问。其的普及使用还待发展
使用:通过workbox,在webpack中则通过workbox-webpack-plugin
来完成
npm i -s workbox-webpack-plugin
使用:
-
引入
const workBoxWebpackPlugin = require('workbox-webpack-plugin');
-
webpack.config.js配置
plugins: [ new workBoxWebpackPlugin.GenerateSW({ /** * 1. 帮助serviceWorker快速启动 * 2. 删除旧的serviceWorker * 3. 通过入口文件中的配置生成一个service配置文件(通过该文件注册serviceWorker) */ clientsClaim: true, skipWaiting: true }) ],
-
入口文件"index.js"配置
/* 1. eslint不认识window、navigator全局变量 解决:需要修改package.json中的eslintConfig配置 "env": { "browser": true }, 2. sw代码必须运行在服务器上 */ // 注册serviceWorker // 处理兼容性问题,可以使用则使用,否则不使用 if ('serviceWorker' in navigator) { // 当页面资源加载完成之后注册servicWorker window.addEventListener('load', () => { // 该文件是由webpack.config.js中的workboxWebpackPLugin插件完成注册 navigator.serviceWorker .register('/service-worker.js') .then(() => { console.log('sw注册成功'); }) .catch(() => { console.log('sw注册失败'); }); }); }
注意:
.register('/service-worker.js')
中的路径是需要写成相对于项目根目录的路径,才可以被访问成功 -
如何知道PWA配置成功?
开启服务运行浏览器,右键打开开发者工具,
Application
->serviceWork
以及Cache Storage
如果有内容,则表明成功,此时把network改为offline
,如果依然正常访问,则配置成功。