一、webpack安装与配置
npm i webpack webpack-cli
// 运行
npx webpack
基本配置
默认情况下,webpack会读取webpack.config.js
文件作为配置文件,也可以通过CLI参数--config
来指定某个配置文件
// webpack的基本配置
module.exports = {
// 编译模式 "development" | "production"
mode: "development",
// 配置源码地图 具体配置:https://www.webpackjs.com/configuration/devtool/
devtool: "source-map",
// 入口 webpack从入口开始依赖解析
entry:{
// 属性名:chunk的名称, 属性值:入口模块(启动模块)
main: "./src/index.js"
},
// 出口配置
output: {
// 公用的公共路径 /
publicPath: "/",
// 输出目录为 dist 默认为dist
path: path.resolve(__dirname, "dist"),
// js 输出到 dist/js/xxx
filename: "js/[name].[chunkhash:5].js"
},
// loader配置 运行时从从右到左,从下到上
module: {
// 模块的匹配规则
rules: [
// 规则1
{
// 正则表达式,匹配模块的路径
test: /index\.js$/,
// 匹配到了之后,使用哪些加载器
use: ["./loaders/loader1", "./loaders/loader2"]
},
// 规则2
{
// 正则表达式,匹配模块的路径
test: /\.js$/,
// 匹配到了之后,使用哪些加载器
use: ["./loaders/loader3", "./loaders/loader4"]
}
]
},
// 插件配置 运行顺序看内部生命周期钩子触发顺序,监听相同生命周期时从上到下(从左到右)
plugins: [
new MyPlugin({
// 插件内部配置
})
],
// 开发处理器配置 需要安装webpack-dev-server 只在开发环境有效
devServer: {
port: 8080,
// 自动打开网页
open: true,
// 开启热替换HMR 需要在代码里写入 module.hot.accept(); 接受热更新
hot:true,
// 代理规则
proxy: {
"/api": {
target: "http://blog.imtwa.com",
//更改请求头中的host和origin
changeOrigin: true
}
}
},
// 优化配置
optimization: {
splitChunks: {
// 分包配置
chunks: "all",
// 缓存组配置
cacheGroups:{
}
},
},
resolve: {
alias: {
// 别名 @ = src目录
"@": path.resolve(__dirname, "src"),
// 别名 _ = 工程根目录
_: __dirname,
},
},
stats: {
// 打包时使用不同的颜色区分信息
colors: true,
// 打包时不显示具体模块信息
modules: false,
// 打包时不显示入口模块信息
entrypoints: false,
// 打包时不显示子模块信息
children: false,
}
};
二、webpack打包流程
webpacke是实现模块化的重要构建工具,它是将开发时态的代码转成运行时态代码,所以webpack不会影响代码的开发和运行,只会在打包时影响打包过程。所以webpack是运行在node环境下,与浏览器环境无关。
初始化
此阶段,webpack会将CLI参数、配置文件、默认配置进行融合,形成一个最终的配置对象。
编译
1、创建chunk
chunk是webpack在内部构建过程中的一个概念,译为块
,它表示通过某个入口找到的所有依赖的统称。
根据入口模块(默认为./src/index.js
)创建一个chunk
每个chunk都有至少两个属性:
- name:默认为main
- id:唯一编号,开发环境和name相同,生产环境是一个数字,从0开始
2、构建所有依赖模块
webpack只能分析js代码,将js代码读取后进行AST抽象语法树分析,找到每个代码的依赖,并转换代码。
loader在做AST前生效,处理源代码。
AST在线测试工具:https://astexplorer.net/
3、产生chunk assets
在第二步完成后,chunk中会产生一个模块列表,列表中包含了模块id和模块转换后的代码
接下来,webpack会根据配置为chunk生成一个资源列表,即chunk assets
,资源列表可以理解为是生成到最终文件的文件名和文件内容
4、合并chunk assets
将多个chunk的assets合并到一起,并产生一个总的hash
输出
webpack将利用node中的fs模块,根据编译产生的总的assets,生成相应的文件。
涉及术语
- module:模块,分割的代码单元,webpack中的模块可以是任何内容的文件,不仅限于JS
- chunk:webpack内部构建模块的块,一个chunk中包含多个模块,这些模块是从入口模块通过依赖分析得来的
- bundle:chunk构建好模块后会生成chunk的资源清单,清单中的每一项就是一个bundle,可以认为bundle就是最终生成的文件
- hash:最终的资源清单所有内容联合生成的hash值
- chunkhash:chunk生成的资源清单内容联合生成的hash值
- chunkname:chunk的名称,如果没有配置则使用main
- id:通常指chunk的唯一编号,如果在开发环境下构建,和chunkname相同;如果是生产环境下构建,则使用一个从0开始的数字进行编号
三、loader
loader处理在读取到文件内容之后,发生在AST抽象语法树之前。loader的功能定位是转换代码,因为webpack只能对js代码进行抽象语法树分析,所以需要在loader阶段引入对图片、css等处理成js代码。
也可以对源代码进行修改,如babel-loader等。
loader执行顺序是从下到上、从右到左,上一个loader处理后再交给下一个loader。
常用的loader
// 使用 postcss 处理 css https://postcss.org/
postcss-loader
// 解析 CSS 文件中的 import 和 url()
css-loader
// 将 CSS 插入到 DOM 中
style-loader
// css 样式打包成一个文件
// 该 loader 负责记录要生成的 css 文件的内容
// 同时导出开启 css-module 后的样式对象
mini-css-extract-plugin.loader
// 各种图片、字体文件,均交给 url-loader 处理
url-loader
// 转换js代码,用于适配各版本浏览器 https://www.babeljs.cn/
babel-loader
四、plugin
plugin的本质是一个带有apply方法的对象,嵌入到webpack的编译流程中的节点进行处理。
执行顺序看节点,同节点的按配置中从上到下、从左到右。
apply函数会在初始化阶段,创建好Compiler对象后运行。compiler对象是在初始化阶段构建的,整个webpack打包期间只有一个compiler对象,后续完成打包工作的是compiler对象内部创建的compilation。
apply方法会在创建好compiler对象后调用,并向方法传入一个compiler对象
compiler对象提供了大量的钩子函数(hooks,可以理解为事件),plugin的开发者可以注册这些钩子函数,参与webpack编译和生成。
class MyPlugin{
apply(compiler){
compiler.hooks.事件名称.事件类型(name, function(compilation){
//事件处理函数
})
}
}
事件名称
即要监听的事件名,即钩子名,所有的钩子:https://www.webpackjs.com/api/compiler-hooks
事件类型
这一部分使用的是 Tapable API,这个小型的库是一个专门用于钩子函数监听的库。
它提供了一些事件类型:
- tap:注册一个同步的钩子函数,函数运行完毕则表示事件处理结束
- tapAsync:注册一个基于回调的异步的钩子函数,函数通过调用一个回调表示事件处理结束
- tapPromise:注册一个基于Promise的异步的钩子函数,函数通过返回的Promise进入已决状态表示事件处理结束
处理函数
处理函数有一个事件参数compilation
。
常用的plugin
// 清除输出目录文件
clean-webpack-plugin
// 自动生成html文件 并引入打包后的js
html-webpack-plugin
// 复制静态资源到输出目录
copy-webpack-plugin
// 将css文件打包成一个文件
// 和mini-css-extract-plugin.loader合用
mini-css-extract-plugin
webpack内置插件
// 全局常量定义插件,使用该插件通常定义一些常量值
DefinePlugin
// 可以为每个chunk生成的文件头部添加一行注释
// 一般用于添加作者、公司、版权等信息
BannerPlugin
// 自动加载模块,不用手动import 或 require
// 写法
/*
new webpack.ProvidePlugin({
$: 'jquery',
_: 'lodash'
})
*/
ProvidePlugin
五、性能优化
性能优化可以从三个方向入手
- 构建性能
- 传输性能
- 运行性能
构建性能
这里所说的构建性能,是指在开发阶段的构建性能,降低从打包开始,到代码效果呈现所经过的时间。
1、减少模块解析
配置module.noParse,对第三方或打包好的模块不进行解析。
2、优化loader性能
有一些库不用经过特定的loader解析,如配置exclude: /lodash/
,在使用babel-loader时不解析lodash库。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /lodash/,
use: "babel-loader"
}
]
}
}
3、开启热替换
默认情况下,webpack-dev-server
不管是否开启了热更新,当重新打包后,都会调用location.reload
刷新页面。
但如果运行了module.hot.accept()
,将改变这一行为。module.hot.accept()
的作用是让webpack-dev-server
通过socket
管道,把服务器更新的内容发送到浏览器。
浏览器将不在刷新重新请求整个模块,而是只请求发送变化的模块。
devServer:{
hot:true // 开启HMR
}
// 并在js中加入下面代码
if(module.hot){ // 是否开启了热更新
module.hot.accept() // 接受热更新
}
传输性能
传输性能是指,打包后的JS代码传输到浏览器经过的时间。
在优化传输性能时要考虑到:
- 总传输量:所有需要传输的JS文件的内容加起来,就是总传输量,重复代码越少,总传输量越少。
- 文件数量:当访问页面时,需要传输的JS文件数量,文件数量越多,http请求越多,响应速度越慢。
- 浏览器缓存:JS文件会被浏览器缓存,被缓存的文件不会再进行传输。
1、手动分包
将不依赖其他模块的文件打包成公共模块,公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单。后续打包时webpack分析到引用的模块在资源清单中,就不会打包该模块。
打包公共模块是一个独立的打包过程,在打包时利用DllPlugin
生成资源清单。
module.exports = {
mode: "production",
entry: {
jquery: ["jquery"],
lodash: ["lodash"]
},
output: {
filename: "dll/[name].js",
library: "[name]" // 每个bundle暴露的全局变量名
},
plugins: [
new webpack.DllPlugin({
// 这里不参与打包后的代码运行,不用放到输出目录中
path: path.resolve(__dirname, "dll", "[name].manifest.json"),
name: "[name]"
})
]
};
打包完成后,需要在页面手动引入公共模块的js文件。
并在主代码打包时引入打包好的资源清单
new webpack.DllReferencePlugin({
manifest: require("./dll/jquery.manifest.json")
}),
new webpack.DllReferencePlugin({
manifest: require("./dll/lodash.manifest.json")
})
手动打包的过程:
1. 开启`output.library`暴露公共模块
2. 用`DllPlugin`创建资源清单
3. 用`DllReferencePlugin`使用资源清单
手动打包的注意事项:
1. 资源清单不参与运行,可以不放到打包目录中
2. 记得手动引入公共JS,以及避免被删除
3. 不要对小型的公共JS库使用
优点:
1. 极大提升自身模块的打包速度
2. 极大的缩小了自身文件体积
3. 有利于浏览器缓存第三方库的公共代码
缺点:
1. 使用非常繁琐
2. 如果第三方库中包含重复代码,则效果不太理想
2、自动分包
配置webpack分包策略,webpack可以根据分包策略自动进行分包。
webpack提供了optimization
配置项,用于配置一些优化信息,其中splitChunks
是分包策略的配置。
module.exports = {
optimization: {
splitChunks: {
// 分包策略
chunks: "all"
}
}
}
chunks:从chunk中分离chunk,实现分包。
- all: 对于所有的chunk都要应用分包策略
- async:【默认】仅针对异步chunk应用分包策略
- initial:仅针对普通chunk应用分包策略
maxSize:控制包的最大字节数,超过后尽可能分包(因为分包是按模块划分,单一模块超过字节后无法再分)
minChunks:一个模块被多少个chunk使用时,才会进行分包,默认值1
缓存组配置
之前配置的分包策略是全局的,而实际上,分包策略是基于缓存组的。每个缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包。
默认情况下,webpack提供了两个缓存组:
module.exports = {
optimization:{
splitChunks: {
//全局配置
cacheGroups: {
// 属性名是缓存组名称,会影响到分包的chunk名
// 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
vendors: {
// 当匹配到相应模块时,将这些模块进行单独打包
test: /[\\/]node_modules[\\/]/,
// 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
priority: -10
},
default: {
// 覆盖全局配置,将最小chunk引用数改为2
minChunks: 2,
// 优先级
priority: -20,
// 重用已经被分离出去的chunk
reuseExistingChunk: true
}
}
}
}
}
修改缓存组以实现css公共样式抽离
module.exports = {
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
styles: {
test: /\.css$/, // 匹配样式模块
minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
minChunks: 2 // 覆盖默认的最小chunk引用数
}
}
}
},
module: {
rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["index"]
}),
new MiniCssExtractPlugin({
filename: "[name].[hash:5].css",
// chunkFilename是配置来自于分割chunk的文件名
chunkFilename: "common.[hash:5].css"
})
]
}
这两种分包方法所能分的最小包还是一个模块的大小,下面使用代码压缩和tree shaking将会减少模块的体积。
3、代码压缩
webpack自动集成了Terser https://terser.org/,可以减少代码体积;破坏代码的可读性,提升破解成本。
用法:
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
optimization: {
// 是否要启用压缩,默认情况下,生产环境会自动开启
minimize: true,
minimizer: [ // 压缩时使用的插件,可以有多个
new TerserPlugin(),
new OptimizeCSSAssetsPlugin()
],
},
};
4、tree shaking
tree shaking可以通过分析模块间的依赖关系,自动去除没有用到的模块内容,从而大大减少了打包体积。
但是tree shaking的模块分析依赖es6的模块分析,es的import绑定的变量是不可变的,更加稳定,所以在使用第三方库时,需要选择es6版本,如lodash-es
。
同时,在书写导入语句时,尽量分块导出、按需导入。
- 使用
export xxx
导出,而不使用export default {xxx}
导出 - 使用
import {xxx} from "xxx"
导入,而不使用import xxx from "xxx"
导入
webpack2开始内置了tree shaking,在打包生产环境中会自动开启。
5、懒加载
使用import()动态引入一个模块,webpack会将模块打包成异步包,等到运行该语句时动态加载js。/* webpackChunkName:"lodash" */
是修改包的名字。
btn.onclick = async function() {
//动态加载
//import 是ES6的草案
//浏览器会使用JSOP的方式远程去读取一个js模块
//import()会返回一个promise (* as obj)
// const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
const result = chunk([3, 5, 6, 7, 87], 2);
console.log(result);
};
其它优化
1、ESlint
使用ESLint可以检查代码格式与质量,在开发阶段编写优雅的代码。
2、bundle analyzer
bundle-analyzer可以分析打包结果,生成图形化的界面。
评论