Webpack的特点
引入模块概念,统一了资源依赖。
它把之前不同类型的资源,比如 JS脚本, TS脚本,json数据, css样式, sass、less等增强版的CSS, png, jpg等图片,全部统称为模块,可以使用相同的方式(import)导入到脚本文件中。 这在以前,是不可以的。Webpack引入的模块依赖概念,把以前需要使用不同语法建立依赖的方式统一了。
擅长打包单页应用。
前端领域发展出了SPA单页应用的概念,React,Vue等"大杀器"引入的路由组件及组件依赖,让SPA落地变得非常简单,二者几乎就是SPA的代名词。React、Vue的应用生成器都内置了Webpack,之所以搭配Webpack,是因为Webpack的模块依赖概念和它们的组件依赖思路 高度砌合,可以说Webpack是React、Vue广泛流行的幕后功臣。
Node.js, DSL
Webpack是基于Node.js的Web打包DSL,(DSL: Domain-Specific Language)(Webpack之于Node.js,就像Gradle之于Groovy,Cocoapods基于Ruby)使用Webpack不是一定要熟悉Node.js,但如果熟悉,就能更充分、更有效地利用好Webpack.
API丰富灵活,生态好。
Webpack提供了丰富灵活的API,开发者可以很方便地扩展和定制。然而,常用的加载器和插件几乎已经覆盖了所有场景,很少需要自行开发。
核心概念
Webpack不复杂,核心概念(我认为)就4个:
入口(entry)
输出 (output)
加载器 (loader)
插件(plugin)
比如一个基本的配置文件:
入口:
入口可以指定一个,也可以指定多个。
// 单入口
module.exports = {
entry: './path/to/my/entry/file.js',
};
// 多入口
module.exports = {
entry: {
name1: './path/to/my/entry/file1.js',
name2: './path/to/my/entry/file2.js'
}
};
Webpack会从入口文件开始,递归查找所有的依赖,即 找到入口文件中依赖(import...from)的外部组件,再去外部组件中找外部组件依赖(import...from)的外部组件...... 顺藤摸瓜,不断找下去,最终 在内部构建一张依赖图,然后把它们“一网打尽”,即 全部做编译/转换,输出到一个js文件中。这个文件往往体积略大,毕竟它包括了所有依赖的组件嘛。
输出:
输出文件的目录及名称,可以明确指定,也可以使用变量。在有多个入口时,要使用变量。
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
// or
filename: '[name].[chunkhash:8].js' // 使用变量
},
};
变量是Webpack内置的,变量插值的语法是 使用中括号,name: 就是入口名称,对应entry对象中的属性名称。
Webpack中,把从一个入口及其依赖全部编译处理后产生的JS代码,称为chunk,有人译为 “组块”,也有人译为“模块”,(“模块”有歧义,因为module也译为模块,module和chunk不一样)。chunkhash 表示 块指纹,其实就是 对块的内容做哈希算法后,产生的哈希值。冒号后面的数字,指定截取前多少个字符。如果不指定,默认是20字符。
Loader:
Webpack 只能直接处理 javascript 格式的代码。任何非 js 文件都必须被预先处理转换为 js 代码,才可以参与打包。loader(加载器)就是这样一个代码转换器。
比如要加载css, 就要用到 css-loader,要加载sass/scss,就要用到 sass-loader, 加载less就用less-loader。
而加载 jsx/tsx时,会用到babel-loader,可能还要babel-preset-react-app。
加载图片文件时,可以用 file-loader 或 url-loader.
语法大概如下:
module: {
rules: [
{
test:/\.css$/,
use: [
"style-loader", // loader 可以串联多个,执行顺序是从右向左,从下向上
"css-loader",
]
},
{
test: /\.js$/,
exclude: /(node_modules)/, // 可以排除某些文件
use: {
loader: "babel-loader",
options: { // 可以给loader传入参数
presets:['@babel/preset-env']
}
}
},
{
test:/\.(png|svg|jpg|gif)$/,
use: [
{
loader: "url-loader",
options: {
limit: 10240,
name: '[name]_[hash:8].[ext]', // 可以使用变量
outputPath: 'assets/',
}
}
]
}
]
}
Plugin:
webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。
webpack 在编译代码过程中,会触发一系列 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。
webpack的插件开发文档,详细说明了 构建的每个阶段 有哪些事件 以及 事件发生时可读取的信息 等。
plugins: [
new CleanWebpackPlugin(), // 每次打包前 把输出目录清空
new MiniCssExtractPlugin({ // 提取出CSS文件
filename: '[name]_[contenthash:8].css' // 截取8位指纹字符
}),
new HtmlWebpackPlugin({
filename: "answer.html", // 输出名称为 answer.html 的文件
template: "./public/template.html", // 以哪个文件为模板
title: "用户答卷", // 指定html的 <title>
chunks:['answer'], // 依赖哪些 组块
minify: { // 指定最小化选项
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}),
]
知乎上有篇描述Plugin的 文章 , 写得不错,可以参考。
提取共用部分
对于多页应用,也就是多入口打包,每个入口都会产生对应的chunk,这些chunk可能会包含重复的部分。
如下图所示:入口A依赖X、Y, 入口B也依赖X、Y,编译后,两个chunk中都含有X、Y, 这有两个明显的缺点,
总体积变大了。
不利于浏览器缓存。
被依赖的模块,比如三方库(React,Lodash等),往往比较稳定,不容易变化,而业务模块(图中的A、B)往往容易变化, 如果它们打包在一起,业务模块稍有变化 就会使得整个chunk文件(指纹)发生变化,浏览器就必须下载最新的文件。 把不易变的提取出来,就能很好地解决这个问题。
// 官方文档 给的举的示例
optimization: {
splitChunks: {
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial', // all, async, initial
minChunks: 2, // 至少2个「组块」引用过
},
vendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, // 匹配 react 或 react-dom
name: 'vendor',
chunks: 'all',
},
},
},
},
webpack-bundle-analyzer
webpack-bundle-analyzer 是一个用于分析 webpack 打包结果的插件。它提供了一个可视化的报告,帮助开发人员更好地理解和优化构建产物的大小和依赖关系。
Tree shaking
摇树,能把树上的枯枝败叶 摇下来。
摇树,在webpack中比喻 把没有用到的代码(枯枝败叶)剔除。使用tree shaking要注意以下几点:
在生产环境,即mode: 'production', tree shaking默认打开。
Tree-shaking生效的前提是必须要使用ES Modules去组织代码,也就是说由webpack打包的代码必须使用ESM。
一些早期的库(比如 jQuery 早期版本),可能没有ESM版本,会将库的所有代码全部打包。有些库提供了ESM版本,比如 lodash-es。在使用三方库前建议看一下文档,如果不支持ESM,就去找一下有没有对应的ESM版本。
写代码时,减小export的颗粒度。
// 1. ---- bad ----
function getExtendInfo() {...}
function getUserRewardInfo() {...}
function receiveAward() {...}
export default const demoService {
getExtendInfo,
getUserRewardInfo,
receiveAward
}
// 2. ---- good ----
export function getExtendInfo() {...}
export function getUserRewardInfo() {...}
export function receiveAward() {...}
以上示例中,如果某个接口不再被引用了, 第一种导出方式,TreeShaking无法剔除, 第二种方式可以。
常见误区
虽然Webpack经常和React、Vue一起起用, 但Webpack其实和React、Vue没有任何关系。
Webpack是通用的Web打包器,无论是否依赖三方库,都可以使用Webpack打包,依赖jQuery、Zepto的可以,依赖React、Vue的可以,jQuery和React混在一起用的,也可以,混合使用,真的没有问题。
Create React App
React应用生成器,简称CRA,有个eject命令,译为"弹出",运行npm run eject,就能看到完整的Webpack配置。
CRA生成的应用包含一个README.md,其中是这样描述npm run eject 的:
Note: this is a one-way operation. Once you eject
, you can’t go back!
If you aren’t satisfied with the build tool and configuration choices, you can eject
at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except eject
will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use eject
. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
翻译:
注意:这个一个单向操作。一旦你eject
了,就不能再恢复到弹出前的状态了。
如果(CRA生成的)构建工具和配置选项不能满足你的需要,你随时可以eject
。这个命令将会把CRA内置的构建依赖从你的项目中移除。
作为替代,它将会把所有的配置文件和工具依赖(webpack, Babel, ESLint等)复制一份到你的项目中,让你可以完全控制它们。所有命令(除了eject
)仍然可以正常使用,只不过这个命令将会指向(已被复制到)你项目中的脚本,你可以调整这些脚本,这样你就可以做你想做的了。
你不是一定要使用eject
。(CRA内置的)精心挑选出的功能对于小型或中型部署来说还是蛮合适的,您不应该觉得有义务使用此功能。 然而我们知道如果你不会(或没有能力)定制它的话,这个命令也不会有什么作用。
有的人不建议“弹出”,我恰好相反,建议“弹出”,因为通过 「阅读它,理解它,修改它,定制它」这个渐进过程可以加深对Webpack的理解,同时可以最大化地利用好CRA。
只要每个团队成员都阅读、梳理过CRA生成的webpack配置,都具备修改、定制它的能力,就不存在 维护问题了。因为团队中任何一人,都能轻松理解另一个人做出的修改,自然就不会出现 解决不了(webpack问题)的情况。
eject 之后,未来升级node版本时,可能会报兼容错误,某些库依赖特定的node版本,排查和升级可能很麻烦。
作者回复: 嗯,你说的对。可以尝试将自己定制的部分提取到一个单独文件中,再import到内置流程中,以减少升级时的改动。