我们平常使用框架写出来的代码,如.vue, .jsx, .less等文件浏览器是不认识的,需要进行转换,webpack就是干这事的,同时在转换过程中可以进行优化。
webpack的构建流程
- 合并shell参数和文件配置的参数
- 注册配置的插件,插件会监听 Webpack 构建生命周期的事件节点
- 执行run方法开始执行编译
- 根据Entry,对每个Module调用对应的Loader,生成AST, 再找到该 Module 依赖的 Module,递归地进行编译处理
- 对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统
常用配置项
mode(环境配置)
- production 生产模式下,Webpack 会自动优化打包结果;(例如:代码的压缩混淆等)
- development 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
- none 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
entry和output
单页面
module.exports = {
// entry: './app.js', // 字符串
entry:['./a.js','./b.js'], // 数组
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js', // 固定
},
plugins: [new HtmlWebpackPlugin()],
}
多页面
module.exports = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
},
output: {
filename: '[name].js', // 输出名称和输入的文件名一致
},
plugins: [
new HtmlWebpackPlugin(
{
template: './src/pages/pageOne/index.html',
chunks: ['pageOne'],
}
),
new HtmlWebpackPlugin(
{
template: './src/pages/pageTwo/index.html',
chunks: ['pageTwo'],
}
),
]
};
多页面时entry一般是不固定的,我们可以读取src下的目录自动生成entry和HtmlWebpackPlugin
const path = require("path");
const glob = require("glob");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"));
entryFiles.forEach((entryFile) => {
const match = entryFile.match(/src\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false,
},
})
);
});
return {
entry,
htmlWebpackPlugins,
};
};
const { entry, htmlWebpackPlugins } = setMPA();
module.exports = {
entry,
// CleanWebpackPlugin 会自动清空output目录
plugins: [new CleanWebpackPlugin(), ...getHtmlTemplate()],
};
devServer
webpack-dev-server里 Watch 模式默认开启。
devServer: {
port: 3000, // 默认为8080
proxy: { // 设置代理
'/api': {
target: 'http://localhost:3000',
pathRewrite:{ // pathRewrite会以正则的方式去替换我们请求的路径
"^/api":""
},
changeOrigin: true, // 跨域
}
},
hot: true, // 是否开启热跟新
open: true, // 是否自动打开默认浏览器进行预览,
}
module
js
- ts-loader: 将ts转为js
- babel-loader: 将es6+语法转换为es5语法
- @bable/core 作用是把js代码分析成ast(抽象语法树),供其它插件使用
- @babel/preset-env 推荐使用的预置器
- .vue文件需要使用vue-loader解析,react不需要单独配置,babel-loader可以解析jsx
css
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== "production";
module.exports = {
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/i,
use: [
devMode ? "style-loader" : MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"sass-loader",
],
},
],
},
plugins: [].concat(devMode ? [] : [new MiniCssExtractPlugin()]),
};
- css预处理器: 扩展了CSS语言,增加了变量、Mixin、函数等特性, 将css扩展语言编译成CSS
- postcss-loader: 处理CSS文件,比如添加浏览器前缀,压缩CSS,转译最新的css特性。
- css-loader: 支持css文件中的@import和url语句,处理css-modules,将CSS转化成字符串并作为CommonJS模块导出
- style-loader: 将css-loader的结果以style标签的方式插入dom中
- mini-css-extract-plugin: 提取 JS 中引入的 CSS 打包到单独文件中,然后通过标签link添加到头部,支持css module, css按需引入
- optimize-css-assets-webpack-plugin: 压缩css代码,webpack在生产模式下会自动开启
推荐:
- 可以使用postcss替代css预处理器。
- 开发使用style-loader,因为它工作得更快。生产使用mini-css-extract-plugin,css分离,可以并行加载,
- 缓存问题: 输出带hash的文件名
Asset Modules
之前我们常用的是:
- raw-loader: 将文件作为字符串输出,就是返回JSON.stringify后的内容
- file-loader: 复制资源文件并替换访问地址,音视频等资源也可以使用它,如background: url()语法和js直接import一个图片
- url-loader: 在file-loader的基础上加了一个data URL的功能。传给url-loader一个限制值,如果处理的文件小于这个值,loader将会把文件转化为base64的data URL输出。大于限制的文件则交给引入的file-loader处理。
webpack5帮我们内置了上面的功能。
- asset/source 将资源导出为源码字符串. 之前的 raw-loader 功能.
- asset/resource 将资源分割为单独的文件,并导出url,之前的 file-loader的功能.
- asset/inline 将资源导出为dataURL(url(data:))的形式,之前的 url-loader的功能.
- asset 自动选择导出为单独文件或者 dataURL形式(默认为8KB)
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
type: `asset`,
parser: {
dataUrlCondition: {
maxSize: 12111
}
}
}
]
},
DLLPlugin
dll 动态链接库,就是缓存,拿空间换时间。把公共库打包成一个单独的库文件,每次只需打包业务代码就行了,这样就减少了公共打包的那部分时间,整体速度得到了提升。
// webpack --config webpack.dll.js
const webpack = require("webpack");
const path = require("path");
module.exports = {
entry: {
react: ["react", "react-dom"],
},
output: {
filename: "dll_[name].js",
library: "[name]_[hash]",
path: path.resolve(__dirname, "dist/site"),
},
plugins: [
new webpack.DllPlugin({
context: __dirname,
path: path.join(__dirname, 'manifest.json'),
name: "[name]_[hash]",
}),
],
};
使用
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./manifest.json'),
});
devtool
选项:
- false: 没有映射
- source-map: 生成单独的映射文件
- inline-: 将映射文件内联到原始文件中
- hidden: 会生成映射文件,但浏览器不会加载
- nosources-: 只有模块信息和行信息
- eval: 通过 eval 包裹每个模块打包后代码以及对应生成的SourceMap,因为 eval 中为字符串形式,所以当源码变动的时候进行字符串处理会提升 rebuild 的速度。但容易受到xss攻击
- cheap: 只定位到行,默认是定位到行和列
- module: 编译前的代码
module.exports = {
devtool: 'cheap-module-eval-source-map' // 开发
devtool: 'cheap-module-source-map'; // 生产
}
VLQ base64解码
optimization(自定义打包策略)
splitChunks 分包
除cacheGroups的配置项都是公共配置,test, priority,reuseExistingChunk这三个配置不能作为公共配置
默认配置
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000, // 体积不足20k,将不会被拆包
minRemainingSize: 0,
minChunks: 1, // 被多次引用,但引用次数小于某个值,将不会被拆包
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000, // 大小超过这个值会被强制拆分
// 上面都是公共配置,对cacheGroups下的每一项都会生效
cacheGroups: {
defaultVendors: {
test: /[\/]node_modules[\/]/, // 匹配规则
priority: -10, // 权重
reuseExistingChunk: true, // 默认会将匹配到的chunk名称进行相连,该项为true时,直接使用已存在的文件,不会修改名称
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
chunks
- async: 动态模块打包进该vendor,非动态模块不进行优化打包
- initial: 非动态模块打包进该vendor,动态模块优化打包,如果同时存在动态和非动态导入,不会被打包到一个vendor
- all: 自动提取所有公共模块到单独 bundle 注:import()可动态加载模块,返回一个Promise。
minimizer(压缩)
js
webpack5在生产模式下,会自动使用terser-webpack-plugin, 压缩js代码
optimization: {
minimize: true // 控制开发环境也会生效
},
css
optimization: {
minimizer: [
`...`, // 继承默认的配置项,如js压缩,自定义会覆盖默认配置
new CssMinimizerPlugin(),
],
},
Tree-shaking(剔除无用代码)
js
Tree Shaking 只支持 ESM 的引入方式,不支持 Common JS 的引入方式。在使用第三方库时注意。
在生产环境下,Webpack 默认会添加 Tree Shaking 的配置,因此只需写一行 mode: 'production' 即可。
const config = {
mode: 'development',
optimization: {
usedExports: true,
}
const config = {
mode: 'production',
};
- 生产环境下才需要开启该功能,开发时不需要
- usedExports: 对识别出的无用代码做标记
- TerserPlugin: 剔除有无用代码标记的代码
- package.json下的sideEffects可以控制tree-shaking的生效范围
sideEffects
sideEffects: true // 全部文件都不可使用tree-shaking
sideEffects: false // 可对全部文件使用tree-shaking, 包括全局的css文件,需要按下面代码进行处理
sideEffects: ['./src/1.js', '*.css'] // 除了数组中的文件,其它文件都可使用tree-shaking
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
sideEffects: true
}
]
},
};
css
剔除无用的css需要用到purgecss
plugins: [
new PurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
]
常见面试题
module,chunk 和 bundle的区别
我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。
为什么代理能跨域
浏览器才有跨域问题,服务器没有,proxy实际上运行了一个本地服务器,帮我们转发请求,主要是用http-proxy-middleware 这个http代理中间件
const express = require('express');
const proxy = require('http-proxy-middleware');
const app = express();
app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true}));
app.listen(3000);
实现一个loader
loader 其实是一个函数,它的参数是匹配文件的源码,返回结果是处理后的源码, 如将var关键词替换为const:
module.exports = function (source) {
return source.replace(/var/g, 'const')
}
实现一个plugin
const pluginName = "ConsoleLogOnBuildWebpackPlugin";
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
// 同步
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log("The webpack build process is starting!");
});
// 异步需要传入cb参数,并手动执行
compiler.hooks.emit.tapAsync(pluginName, (compilation, cb) => {
compilation.assets["copyright.txt"] = {
source: function () {
return "copyright by LEE YANG";
},
size: function () {
return 21;
},
};
cb();
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
Compiler和Compilation的区别
Compiler代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation只是代表了一次新的编译,只要文件有改动,compilation就会被重新创建。
compiler上暴露的一些常用的钩子:
comple 在一次新的compilation前执行 made 完成一次Compilation前执行 done 完成一次Compilation后执行 emit 产出文件到output之前执行
webpack chain
webpack chain提供了很多方法,通过链式调用可以方便的修改各个配置项
chainWebpack: (config) => {
config.entryPoints.clear(); // 清空入口
config.entry("main").add("./src/main.js"); // 新增入口
config.output
.path("dist")
.filename("[name].[chunkhash].js")
.chunkFilename("chunks/[name].[chunkhash].js")
.libraryTarget("umd")
.library();
};
// 修改loader
module.exports = {
chainWebpack: (config) => {
config.module
.rule("vue")
.use("vue-loader")
.loader("vue-loader")
.tap((options) => {
// 修改它的选项...
return options;
});
},
};
hrm原理
{
entry: [
require.resolve("webpack-dev-server/client") + "?/", // WebpackDevServer 客户端
require.resolve("webpack/hot/dev-server"), // 监听执行热更新的事件
// 入口
paths.appIndexJs,
];
}
- 启动本地服务器(Webpack-dev-server),通过修改entry注入客户端和热更新操作,通过sockjs建立websocket长连接
- 对文件进行监听(Webpack-dev-middleware)
- 监听到文件更改,将重新编译后的代码保存在内存中, 通知浏览器进行更新
- 客户端并不请求热更新代码,也不执行热更新模块操作,只是通过emit一个webpackHotUpdate消息,将工作转交给webpack/hot/dev-server
- webpack/hot/dev-server会向服务器请求检测是否有新的模块更新,有则返回更新列表,通过jsonp请求最新的模块代码,返回的模块代码内容是直接执行 webpackHotUpdateCallback方法进行模块热替换,热更新过程中如出现错误将回退到刷新浏览器。
参考文章: juejin.cn/post/699277…
Tree-shaking原理
tree-shaking的消除原理是依赖于ES6的模块特性。ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析。
如何处理第三方库?
只导入用到的方法
import { cloneDeep } from 'lodash' // 会打包整个lodash 文件。
import cloneDeep from 'lodash/cloneDeep' // 只会打包cloneDeep
使用esm的库
lodash-es,这个包支持Tree-shaking,
webpack4, webpack5区别
- webpack5自带压缩,缓存,web worker
- 不再为Node.js内置模块自动添加Polyfills
- 内置的静态资源构建能力asset/resource
- 支持 Top Level Await
webpack优化
分析工具
- progress-bar-webpack-plugin:查看编译进度
- speed-measure-webpack-plugin:查看编译速度
- webpack-bundle-analyzer:打包体积分析
优化方法
- 尽量使用新版本的开发工具,包括node, npm, webpack
- 加快构建时间: cache,可加快二次构建速度, 多线程打包
- 减小打包体积: 压缩代码、分离重复代码、Tree Shaking,按需引入第三方库
- 加载速度:按需加载、浏览器缓存、CDN
- 其它:Source Map, hrm
谈谈vite, rollup等其它打包工具
rollup
一般用来打包类库,默认只支持esm, 有很多第三方插件,可以支持项目开发,hrm等
常见插件:
- @rollup/plugin-node-resolve 查找外部模块
- @rollup/plugin-commonjs 将CommonJS转换成 ES2015 模块的
- @rollup/plugin-babel 转译你的 ES6/7 代码
webpack
功能强大,完善,常用于完整的项目中,同时带来的问题就是慢,配置复杂
vite
-
启动快 webpack需要打包合并后,发给服务器,vite利用浏览器支持esm的特性,不需要打包,当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。
-
HMR快 当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高
-
生产打包使用的还是rollup