写在最前面
最近工作中webpack
用的比较多,想系统的学习一下,找到了极客时间的教程,跟着视频上讲的学了一遍,收获挺大的。虽然视频教程是2019年出的,有点老,有些知识已经过时了,但并不妨碍学习。那些过时的知识全当了解,可以搜索寻找替代方案。学习资源:玩转webpack,下面是我的学习笔记。
构建工具
前端技术不断发展,比如:React
的jsx
,Vue
和Angular
指令,CSS
预处理器,最新的ES
语法等,浏览器都不能识别。
构建工具的作用就是将这些浏览器不能识别的语法(高级语法)转换成了浏览器能识别的语法(低级语法)。
还有一个作用是将代码压缩混淆,在压缩代码体积的同时也让代码不易阅读。
webpack
是现在前端流行的构建工具。
初识webpack
配置script
脚本
安装好webpack
后,在命令行中使用webpack
时,有两种方式:
- 指定路径
./node_modules/.bin/webpack
- 使用
npx
工具,npx webpack
这两种方法使用挺麻烦的。有种简单的方式使用package.json
中的scripts
,它能够读到node_modules/.bin
目录下的命令。
在命令行中执行npm run build
即可。
"scripts": {
"build": "webpack"
}
配置文件
webpack
的默认配置文件webpack.config.js
,可以通过webpack --config
来指定配置文件。
比如,生产环境的配置文件webpack.config.prod.js
,开发环境的配置文件是webpack.config.dev.js
。
"scripts": {
"build": "webpack --config webpack.config.prod.js",
"build:dev": "webpack --config webpack.config.dev.js"
}
核心概念
webpack
有 5 个核心概念:entry
、output
、mode
、loaders
、plugins
。
entry
打包时的入口文件,默认是./src/index.js
entry
是有两种方式:单入口、多入口。
单入口用于单页面应用,entry
是个字符串
module.exports = {
entry: "./src/index.js",
};
多入口用于多页面应用,entry
是对象形式
module.exports = {
entry: {
index: "./src/index.js",
app: "./src/app.js",
},
};
output
打包后的输出文件,默认是./dist/main.js
。
entry
是单入口,output
可通过修改参数path
和filename
。
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
},
};
entry
是多入口,output
的filename
需要用[name]
占位符,用来指定打包后的名称,对应的是entry
中的key
const path = require("path");
module.exports = {
entry: {
index: "./src/index.js",
app: "./src/app.js",
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
},
};
mode
可设置为development
、production
、none
,默认是production
。
development
:开发环境,设置process.env.NODE_ENV
的值为development
。开启NamedChunksPlugin
和NamedModulesPlugin
production
:生产环境,设置process.env.NODE_ENV
的值为production
。开启FlagDependencyUsagePlugin
,FlagIncludedChunksPlugin
,ModuleConcatenationPlugin
,NoEmitonErrorsPlugin
,OccurrenceOrderPlugin
,SideEffectsFlagPlugin
和TerserPlugin
none
:不开启任何优化选项
ps:development
模式下开启的两个插件在代码热更新阶段,会在控制台打印出哪个模块发生了热更新,这个模块的路径是啥。production
模式下开启的插件会在压缩代码。
loaders
webpack
只支持js
和json
两种文件类型,loader
的作用就是用来处理其他的文件,并将它们添加到依赖图中。
loader
是个函数,接收源文件作为参数,返回转换后的结果。
一般loader
命名的方式都是以-loader
为后缀,比如css-loader
、babel-loader
。
test
是指定匹配的规则,use
是指定使用loader
的名称
module.exports = {
module: {
rules: [
{
test: /.less$/,
use: ["style-loader", "css-loader", "less-loader"],
},
],
},
};
一个loader
一般只做一件事,在解析less
时,需要用less-loader
将less
转成css
,由于webpack
不能识别css
,又需要用css-loader
将css
转换成commonjs
对象放到js
中,最后需要用style-loader
将css
插入到页面的style
中。
ps:loader
的组合通常由两种方式,一种是从左到右(类似unix
的pipe
),另一种是从右到左(compose
)。webpack
选择的是compose
,从右到左一次执行loader
plugins
任何loader
没法做的事情,都可以用plugin
解决,它主要用于文件优化、资源管理、环境变量注入,作用于整个构建过程。
一般plugin
命名的方式是以-webpack-plugin
为后缀结尾,也有是以-plugin
为后缀结尾的。
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
plugins: [new CleanWebpackPlugin()],
};
webpack
资源解析
解析es6
需要安装@babel/core
,@babel/preset-env
,babel-loader
在根目录下面新建.babelrc
文件,将@babel/preset-env
添加到presets
中
{
"presets": ["@babel/preset-env"]
}
webpack
中配置babel-loader
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: "babel-loader",
},
],
},
};
还有一种配置的方式,不使用.babelrc
文件,将它配置在use
的参数options
中
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
};
解析css
需要安装css-loader
和style-loader
。
module.exports = {
moudle: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
};
解析less
需要安装less-loader
,css-loader
,style-loader
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"],
},
],
},
};
解析图片和字体
需要安装file-loader
module.exports = {
module: {
rules: [
{
test: /.(png|jpeg|jpg|gif)$/,
use: ["file-loader"],
},
{
test: /.(woff|woff2|eot|ttf|otf)$/,
use: ["file-loader"],
},
],
},
};
url-loader
url-loader
可以将较小的图片转换成base64
。
module.exports = {
module: {
rules: [
{
test: /.(png|jpeg|jpg|gif)$/,
use: {
loader: "url-loader",
options: {
limit: 10240, // 小于 10k 图片,webpack 在打包时自动转成 base64
},
},
},
],
},
};
配置vue
配置vue
开发环境,需要安装vue
,vue-loader
,vue-template-compiler
const { VueLoaderPlugin } = require("vue-loader");
module.export = {
plugins: [new VueLoaderPlugin()],
module: {
rules: [
{
test: /\.vue$/,
use: "vue-loader",
},
],
},
};
ps: 这里使用的vue-loader
是15.x
版本,我安装最新的版本16.x
有问题,一直没有解决。
配置react
配置react
开发环境需要安装react
,react-dom
,@babel/preset-react
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
},
],
},
};
webpack
文件监听及热更新
文件监听
每次修改代码后,都需要手动构建,影响开发效率。webpack
提供了文件监听的作用。开启监听时,webpack
会调用node
中fs
模块来判断文件是否发生变化,如果发生了变化,自动重新构建输出新的文件。
webpack
开启监听模式,有两种方式:(需要手动刷新浏览器)
- 启动
webpack
命令时,带上--watch
参数
"scripts": {
"watch": "webpack --watch"
}
webpack.config.js
中设置watch: true
module.exports = {
watch: true,
};
文件监听分析
webpack
文件监听判断依据是看文件的最后编辑时间是否发生变化。
它会将修改的时间保存起来,当文件修改时,它会和上一次的修改时间进行比对。
如果发现不一致,它并不会马上告诉监听者,而是先把文件的修改缓存起来,等待一段时间,如果这段时间内还有其他文件发生变化,它就会把这段时间内的文件列表一起构建。这个等待的时间就是aggregateTimeout
。
module.exports = {
watch: true,
watchOptions: {
ignored: /node_modules/,
aggregateTimeout: 300,
poll: 1000,
}
}
watch
:默认为false
watchOptions
:只有watch
为true
时,才生效ignored
:需要忽略监听的文件或者文件夹,默认为空,忽略node——modules
性能会有所提升。aggregateTimeout
:监听到文件变化后,等待的时间,默认300ms
poll
:轮询时间,1s
一次
热更新
热更新需要webpack-dev-server
和HotModuleReplacementPlugin
两个插件背后使用。
与watch
相比,它不输出文件,直接方式内存中,所以它的构建熟读更快。
热更新在开发模式中才会使用。
const webpack = require("webpack");
const path = require("path");
module.exports = {
mode: "development",
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: {
contentBase: path.join(__dirname, "dist"),
hot: ture,
},
};
在package.json
中配置命令
webpack4.x
"scripts": {
"dev": "webpack-dev-server --open"
}
webpack5.x
"scripts": {
"dev": "webpack server"
}
ps:webpack5.x
和webpack-dev-server
有冲突,不能使用--open
打开浏览器。
HotModuleReplacementPlugin
热更新的核心是HMR Server
和HMR Runtime
。
HMR Server
:是服务端,用来将变化的js
模块通过websocket
的消息通知给浏览器端HMR Runtime
:是浏览器端,用于接收HMR Server
传递过来的模块数据,浏览器端可以看到.hot-update.json
文件
hot-module-replacement-plugin
的作用:webpack
本身构建出来的bundle.js
本身是不具备热更新的,HotModuleReplacementPlugin
的作用就是HMR Runtime
注入到bundle.js
,使得bundle.js
可以和HMR Server
建立websocket
的通信能力。一旦磁盘里的文件发生修改,那么HMR Server
将有修改的js
模块通过websocket
发送给HMR Runtime
,然后HMR Runtime
去局部更新页面的代码,这种方法不会刷新浏览器。
webpack-dev-server
的作用:提供bundle server
的能力,就是生成的bundle.js
可以通过localhost://xxx
的方式去访问,另外也提供livereload
(浏览器自动刷新)。
文件指纹
什么是文件指纹
文件指纹是指打包后输出的文件名的后缀。比如:index_56d51795.js
。
通常用于用于版本管理
文件指纹类型
hash
: 和项目的构建相关,只要项目文件改变,构建项目的hash
值就会改变。采用hash
计算的话,每一次构建后的哈希值都不一样,假如说文件内容没有发生变化,那这样子是没法实现缓存的。chunkhash
:和webpack
打包的chunk
有关,不同的entry
会生成不同的chunkhash
。生产环境会将一些公共库和源码分开来,单独用chunkhash
构建,只要不改变公共库的代码,它的哈希值就不会变,就可以实现缓存。contenthash
:根据文件内容来定义hash
,文件内容不变,则contenthash
不变。一般用于css
资源,如果css
资源使用chunkhash
,那么修改了js
,css
资源就会变化,缓存就会失效,所以css
使用contenthash
。
ps:
- 使用
hash
的场景要结合mode
来考虑,如果mode
是development
,在使用HMR
情况下,使用chunkhash
是不适合的,应该使用hash
。而mode
是production
时,应该用chunkhash
。js
使用chunkhash
是便于寻找资源,因为js
的资源关联度更高。css
使用contenthash
是因为css
一般是根据不同的页面书写的,css
资源之间的关联度不高,也就不用在其他资源修改时,重新更新css
。
js
文件指纹
设置output
的filename
,使用[chunkhash]
,
const path = require("path");
module.exports = {
output: {
path: path.join(__dirname, "dist"),
filename: "[name]_[chunkhash:8].js", // 取 hash 的前 8位
},
};
css
文件指纹
需要安装mini-css-extract-plugin
style-loader
是将css
插入到页面的head
中,mini-css-extract-plugin
是提取为单独的文件,它们之间是互斥的。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name]_[contenthash:8].css",
}),
],
};
图片文件指纹
需要安装file-loader
占位符名称 | 含义 |
---|---|
[ext] | 资源后缀名 |
[name] | 文件名称 |
[path] | 文件的相对路径 |
[folder] | 文件所在的文件夹 |
[contenthash] | 文件内容的hash ,默认是md5 |
[hash] | 文件内容的hash ,默认是md5 |
[emoji] | 一个随机的指代文件内容的emoji |
图片的hash
和css/js
的hash
概念不一样,图片的hash
是有图片的内容决定的。
module.exports = {
module: {
rules: [
{
test: /.(png|jpeg|jpg|gif)$/,
use: {
loader: "file-loader",
options: {
filename: "[folder]/[name]_[hash:8].[ext]",
},
},
},
{
test: /.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: "file-loader",
options: {
filename: "[name]_[hash:8].[ext]",
},
},
],
},
],
},
};
代码压缩
代码压缩主要分为js
压缩,css
压缩,html
压缩。
js
压缩
webpack
内置了uglifyjs-webpack-plugin
插件,默认打包后的文件都是压缩过的,这里无需额外配置。
可以手动安装这个插件,可以额外设置一下配置,比如开启并行压缩,需要将parallel
设置为true
const UglifyjsWebpackPlugin = require("uglifyjs-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new UglifyjsWebpackPlugin({
parallel: true,
}),
],
},
};
css
压缩
安装optimize-css-webpack-plugin
,同时安装css
预处理器cssnano
。
const OptimizeCssWebpackPlugin = require("optimize-css-webpack-plugin")
module.exports = {
plugins: [
new OptimizeCssWebpackPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require("cssnano)
})
]
}
html
压缩
需要安装html-webpack-plugin
,通过设置压缩参数。
ps: 多页面应用需要写多个new HtmlWebpackPlugin({...})
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "src/search.html"), //html 模版所在的位置,模版里面可以使用 ejs 的语法
filename: "search.html", // 打包出来后的 html 名称
chunks: ["search"], // 打包出来后的 html 要使用那些 chunk
inject: true, // 设置为 true,打包后的 js css 会自动注入到 HTML 中
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}),
new HtmlWebpackPlugin({...})
],
};
HtmlWebpackPlugin
参数minify
里面的minifyJS
和minifyCSS
的作用是用于压缩一开始就内联在html
里的js
(不能使用ES6
语法)和css
。
chunks
对应的是entry
中的key
。你希望哪个chunk
自动注入,就将哪个chunk
写到chunks
。
chunk
、bundle
、module
区别:
chunk
:每个chunk
是又多个module
组成,可以通过代码分割成多个chunk
bundle
:打包生成的最终文件module
:webpack
中的模块(js
、css
、图片)
自动清理构建目录
每次在构建的时候,会产生新的文件,造成输出目录output
文件越来越多。
最常见的清理方法用命令rm
去删除,在打包之前先执行rm -rf
命令将dist
目录删除,在进行打包。
"scripts": {
"build": "rm -rf ./dist && webpack"
}
另一种是使用rimraf
进行删除。
安装rimraf
,使用时也是先在打包前先将dist
目录进行删除,然后在打包。
"scripts": {
"build": "rimraf ./dist && webpack"
}
这两种方案虽然都能将dist
目录清空,但不太优雅。
webpack
提供clean-webpack-plugin
,它会自动清理output.path
下的文件。
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
plugins: [new CleanWebpackPlugin()],
};
自动补齐样式前缀
不同的浏览器厂商在实现css
特性的标准没有完全统一,比如display: flex
,在webkit
内核中要写成display: -webkit-box
。
在开发的时候一个个加上内核,将是一个巨大的工程。
在webpack
中可以用loader
去解决自动补齐css
前缀的问题。
安装postcss-loader
,以及它的插件autoprefixer
。
autoprefixer
是根据can i use
这个网站提供的css
兼容性进行不全前缀的。
autoprefixer
是后置处理器,它和预处理器不同,预处理器是在打包的时候处理,而autoprefixer
是在代码处理好之后,样式已经生成了再进行处理。
在webpack4.x
中安装postcss-loader@3.0.0
,autoprefixer@9.5.1
。
方式一: 直接在webpack.config.js
中配置
webpack.config.js
:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"less-loader",
{
loader: "postcss-loader",
options: {
plugins: () => [
require("autoprefixer")({
overrideBrowserslist: ["last 2 version", ">1%", "ios 7"], //最新的两个版本,用户大于1%,且兼容到 ios7
}),
],
},
},
],
},
],
},
};
方式二: 利用postcss
配置文件postcss.config.js
,webpack.config.js
直接写postcss-loader
即可。
webpack.config.js
:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"less-loader",
"postcss-loader",
],
},
],
},
};
postcss.config.js
:
module.exports = {
plugins: [
require("autoprefixer")({
overrideBrowserslist: ["last 2 version", ">1%", "ios 7"],
}),
],
};
方法三: 浏览器的兼容性可以写package.json
中,postcss.config.js
中只需加载autofixer
即可。
webpack.config.js
:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"less-loader",
"postcss-loader",
],
},
],
},
};
postcss.config.js
:
module.exports = {
plugins: [require("autofixer")],
};
package.json
:
"browserslist": {
"last 2 version",
"> 1%",
"ios 7"
}
资源内联
在一些场景中需要使用到资源内联,常见的有:
- 页面框架的初始化脚本
- 减少
http
网络请求,将一些小的图片变成base64
内容到代码中,较少请求 css
内联增加页面体验- 尽早执行的
js
,比如REM
方案
html
内联
在多页面项目中,head
中有很多公用的标签,比如meta
,想要提升维护性,会将它提取成一个模版,然后引用进来。
安装raw-loader@0.5.1
<head>
${require("raw-loader!./meta.html")}
</head>
js
内联
在REM
方案中,需要尽早的html
标签的font-size
,那么这段js
就要尽早的加载、执行。
<head>
<script>
${require('raw-loader!babel-loader!./calc-root-fontsize.js')}
</script>
</head>
css
内联
为了更好的体验,避免页面闪动,需要将打包好的css
内联到head
中。
安装html-inline-css-webpack-plugin
,这个插件需要放在html-webpack-plugin
后面。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlInlineCssWebpackPlugin = require("html-inline-css-webpack-plugin");
module.exports = {
module: {
rules: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name]_[contenthash:8].css",
}),
new HtmlWebpackPlugin(),
new HtmlInlineCssWebpackPlugin(),
],
};
需要先将css
提取成单独的文件才行。
ps:
style-laoder
和html-inline-css-webpack-plugin
的区别是:
style-loader
:插入样式是一个动态的过程,打包后的源码不会有style
标签,在执行的时候通过js
动态插入style
标签html-inline-css-webpack-plugin
:是在构建的时候将css
插入到页面的style
标签中
多页面应用
每个页面对应一个entry
,同时对应plugins
中一个html-webpack-plugin
。
这种方式有个缺点,每个增加一个entry
就要增加一个html-webpack-plguin
。
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: {
index: "./src/index.js",
search: "./src/search.js",
},
plugins: [
new HtmlWebpackPlugin({ template: "./src/index.html" }),
new HtmlWebpackPlugin({ template: "./src/search.html" }),
],
};
借助glob
可以实现通用化的配置方式。
安装glob
,同时所有的页面都要放在src
下面,并且入口文件都要叫做index.js
。
const path = require("path");
const glob = require("glob");
const HtmlWebpackPlugin = require("html-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,
plugins: [].concat(htmlWebpackPlugins),
};
sourceMap
使用方法
发布到生成环境中的代码,要将代码进行压缩混淆,但是在压缩混淆之后,看代码就像在看天书一样。
sourceMap
提供了压缩后的代码和源代码之间的映射关系。
sourceMap
在开发环境开启,线上环境中关闭,在线上排查问题时将source map
上传到错误监控系统。
source map
关键字
eval
:使用eval
包裹模块代码source map
:产生map
文件cheap
:不包含列信息inline
:将.map
作为DataURI
嵌入,不单独生成.map
文件module
:包含loader
的sourcemap
source map
类型
devtool | 首次构建 | 二次构建 | 是否适合生产环境 | 可以定位的代码 |
---|---|---|---|---|
(none) | +++ | +++ | yes | 最终输出的代码 |
eval | +++ | +++ | no | webpack 生成的代码(一个个的模块) |
cheap-eval-source-map | + | ++ | no | 经过 loader 转换后的代码(只能看到行) |
cheap-module-eval-source-map | o | ++ | no | 源代码(只能看到行) |
eval-source-map | -- | + | no | 源代码 |
cheap-source-map | + | o | yes | 经过loader 转换后的代码只能看到行) |
cheap-module-source-map | o | - | yes | 源代码(只能看到行) |
inline-cheap-source-map | + | - | no | 经过loader 转换后的代码(只能看到行) |
inline-cheap-module-source-map | o | - | no | 源代码(只能看到行) |
source-map | -- | -- | yes | 源代码 |
Inline-source-map | -- | -- | no | 源代码 |
hidden-source- map | -- | -- | yes | 源代码 |
提取页面公共资源
在项目中,多个页面中都会使用到一些基础库,比如react
、react-dom
,还有一写公共的代码,当在打包时这些都会被打包进最终的代码中,这是比较浪费的,而且打包后的体积也比较大。
webpack4
内置了SplitChunksPlugin
插件。
chunks
参数说明:
async
异步引入的库进行分离(默认)initial
同步引入的库进行分离all
所有引入的库进行分离(推荐)
抽离出基础库的名称cacheGroups
里中的filename
放到html-webpack-plugin
中chunks
中,会自动导入:
module.exports = {
plugins: [
new HtmlWebpackPlugin({
chunks: ["vendors", "commons", "index"], //打包出来后的 html 要使用那些 chunk
})
);
],
optimization: {
splitChunks: {
chunks: "async",
minSize: 30000, // 抽离的公共包最小的大小,单位字节
maxSize: 0, // 最大的大小
minChunks: 1, // 资源使用的次数(在多个页面使用到), 大于1, 最小使用次数
maxAsyncRequests: 5, // 并发请求的数量
maxInitialRequests: 3, // 入口文件做代码分割最多能分成3个js文件
automaticNameDelimiter: "~", // 文件生成时的连接符
automaticNameMaxLength: 30, // 自动自动命名最大长度
name: true, //让cacheGroups里设置的名字有效
cacheGroups: {
//当打包同步代码时,上面的参数生效
vendors: {
test: /[\\/]node_modules[\\/]/, //检测引入的库是否在node_modlues目录下的
priority: -10, //值越大,优先级越高.模块先打包到优先级高的组里
filename: "vendors.js", //把所有的库都打包到一个叫vendors.js的文件里
},
default: {
minChunks: 2, // 上面有
priority: -20, // 上面有
reuseExistingChunk: true, //如果一个模块已经被打包过了,那么再打包时就忽略这个上模块
},
},
},
},
};
tree shaking
(摇树优化)
在一个模块中有多个方法,用到的方法会被打包进bundle
中,没有用到的方法不会打包进去。
tree shaking
就是只把用到的方法打包到bundle
,其余没有用到的会在uglify
阶段擦除。
webpack
默认支持,在.babelrc
里设置modules: false
即可。production
阶段默认开启。
必须是ES6
语法,CJS
的方式不支持。
ps: 如果把ES6
转成ES5
,同时又要开启tree shaking
,需要在.babelrc
里设置module: false
,不然babel
默认会把ES6
转成CJS
规范的写法,这样就不能进行tree shaking
。
DCE
(deal code elimination)
DEC
全称 deal code elimination,中文意思是死代码删除,主要是下面三种:
- 代码不会被执行
if (false) {
console.log("代码不会被执行");
}
- 代码执行的结果不会被用到
function a() {
return "this is a";
}
let A = a();
- 代码只写不读
let a = 1;
a = 2;
tree shaking
原理
对代码进行静态分析,在编译阶段,代码有没有用到是要确定下来的,不能通过在代码运行时在分析哪些代码有没有用到,tree shaking
会把没用到的代码用注释标记出来,在uglify
阶段将这些无用代码擦除。
利用ES6
模块的特点:
- 只能作为模块顶层的语句出现
import
的模块名只能是字符串常量import binding
是immutable
删除无用的css
purgecss-webpack-plugin
:遍历代码,识别已经用到的css class
- 和
mini-css-extract-plugin
配合使用
- 和
uncss
:html
需要通过jsdom
加载,所有的样式通过postCSS
解析,通过document.querySelector
来识别在html
文件里面不存在的选择器
const PurgecssPlugin = require("purgecss-webpack-plugin");
const PATHS = {
src: path.join(__dirname, "src"),
};
module.exports = {
plugins: [
new PurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
],
};
scope hoisting
使用和原理分析
构建够的代码会产生大量的闭包,如下图所示:
当引入外部模块时,会用一个函数包裹起来。当引入的模块越多时,就会产生大量的函数包裹代码,导致体积变大,在运行的时候,由于创建的函数作用域越多,内存开销也会变大。
- 被
webpack
转换后的模块会被包裹一层 import
会被转换成__webpack_require
分析
- 打包出来的是一个
IIFE
(匿名闭包) modules
是一个数组,每一项是一个模块初始化函数__webpack_require
用来加载模块,返回module.exports
- 通过
WEBPACK_REQUIRE_METHOD(0)
启动程序
scope hoisting
原理
将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重名一些变量以防止变量名冲突。
模块调用有先后顺序,a
模块调用b
模块,因为有函数包裹,a
模块和b
模块的顺序无所谓,如果消除包裹代码的话,需要根据模块的引用顺序来排放模块,就要将b
模块放到a
模块之前,b
模块才能被a
模块调用。
通过scope hoisting
可以减少函数声明和内存开销。production
阶段默认开启。
必须是ES6
语法,CJS
不支持。
ps:scope hoisting
把多个作用域变成一个作用域。当模块被引用的次数大于1
次时,是不产生效果的。如果一个模块引用次数大于1
次,那么这个模块的代码会被内联多次,从而增加打包后文件的体积。
使用ESLint
是ESLint
能够统一团队的代码风格,能够帮助发现带错误。
两种使用方法:
- 与
CI/CD
集成 - 与
webpack
集成
webpack
与CI/CD
集成
需要安装husky
。
增加scripts
,通过lint-staged
检查修改文件。
"scripts": {
"precommit": "lint-staged"
},
"lint-staged": {
"linters": {
"*.{js,less}": ["eslint --fix", "git add"]
}
}
webpack
与ESLint
集成
使用eslint-loader
,构建是检查js
规范
安装插件babel-eslint
、eslint
、eslint-config-airbnb
、eslint-config-airbnb-base
、eslint-loader
、eslint-plugin-import
、eslint-plugin-jsx-ally
、eslint-plugin-react
。
新建.eslintrc.js
文件
module.exports = {
parser: "babel-eslint",
extends: "airbnb",
env: {
browser: true,
node: true,
},
rules: {
indent: ["error", 2],
},
};
webpack.config.js
文件配置eslint-loader
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ["babel-loader", "eslint-loader"],
},
],
},
};
代码分割和动态import
对于大的web
应用来说,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊情况下才会被用到。
webpack
有一个功能是将你的代码分割成chunks
(语块),当代码运行到需要它们的时候在进行加载。
使用场景:
- 抽离相同代码到一个共享块
- 脚本懒加载,使得初始下载的代码更小
懒加载js
脚本的方式
CJS
:require.ensure
ES6
:import
(目前还没有原生支持,需要bable
转换)
安装@bable/plugin-syntax-dynamic-import
插件
.babelrc
文件:
{
plugins: ["@bable/plugin-syntax-dynamic-import"];
}
例子:
class Index extends React.Component {
constructor() {
super(...arguments);
this.state = {
Text: null,
};
}
loadComponent = () => {
import("./text.js").then((Text) => {
this.setState({ Text: Text.default });
});
};
render() {
const { Text } = this.state;
return (
<div className="search">
react1
{Text && <Text />}
<div onClick={this.loadComponent}>点我</div>
<img src={img} alt="" />
</div>
);
}
}
这里要注意一点,当配置了cacheGroups
时,minChunks
设置了1
,上面设置的懒加载脚本就不生效了,因为import
在加载时是静态分析的。
cacheGroups: {
commons: {
name: "commons",
chunks: "all",
priority: -20, //值越大,优先级越高.模块先打包到优先级高的组里
minChunks: 1,
}
}
多进程/多实例:并行压缩
方法一:使用webpack-parallel-uglify-plugin
插件
const WebpackParalleUglifyPlugin = require("webpack-parallel-uglify-plugin");
module.exports = {
plugins: [
new WebpackParalleUglifyPlugin({
uglifyJs: {
output: {
beautify: false,
comments: false,
},
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true,
},
},
}),
],
};
方法二:使用uglifyjs-webapck-plugin
开启parallel
参数
const UglifyJsPlugin = require("uglifyjs-webpack-plugin")
modules.exports = {
plugins: [
UglifyJsPlugin: {
warnings: false,
parse: {},
compress: {},
mangle: true,
output: null,
toplevel: false,
nameCache: null,
ie8: false,
keep_fnames: false,
},
parallel: true
]
}
方法三:terser-webpack-plugin
开启parallel
参数(webpack4
推荐)
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4,
}),
],
},
};
提升构建速度
思路:将react
、react-dom
、redux
、react-redux
基础包和业务基础包打包成一个文件
方法:使用DLLPlugin
进行分包,DLLReferencePlugin
对manifest.json
引用
使用DLLPlugin
进行分包
const path = require("path");
const webpack = require("webpack");
module.exports = {
context: process.cwd(),
resolve: {
extensions: [".js", ".jsx", ".json", ".less", ".css"],
modules: [__dirname, "nodu_modules"],
},
entry: {
library: ["react", "react-dom", "redux", "react-redux"],
},
output: {
filename: "[name].dll.js",
path: path.resolve(__dirname, "./build/library"),
library: "[name]",
},
plugins: [
new webpack.DllPlugin({
name: "[name]",
path: "./build/library/[name].json",
}),
],
};
在webpack.config.js
引入
module.exports = {
plugins: [
new webapck.DllReferencePlugin({
manifest: require("./build/library/manifest.json"),
}),
],
};
在项目使用了webpack4
,对dll
的依赖没那么大了,使用dll
相对来说提升也不是特别明显,而hard-source-webpack-plugin
可以极大的提升二次构建。
不过从实际的前端工厂中来说,dll
还是很有必要的。对于一个团队而言,基本是使用相同的技术栈,要么react
,要么vue
。这时候,通常的做法都是把公共框架打成一个common bundle
文件供所有项目使用。dll
就可以很好的满足这种场景:将多个npm
包打成一个公共包。因此团队里面的分包方案使用dll
还是很有价值。
splitChunks
也可以做DllPlugin
的事情,但是推荐使用splitChunks
去提取页面间的公共js
文件,因为使用splitChunks
每次去提取基础包还是需要耗费构建时间的,如果是DllPlugin
只需要预编译一次,后面的基础包时间都是可以省略掉的。
提升二次构建速度
方法一:使用terser-webpack-plugin
开启缓存
module.exports = {
optimization: {
minimizer: [
new TerserWebpackPlugin({
parallel: true,
cache: true,
}),
],
},
};
方法二:使用cache-loader
或者hard-source-webpack-plugin
module.exports = {
plugins: [new HardSourceWebpackPlugin()],
};
缩小构建目标
比如babel-loader
不解析node_modules
module.exports = {
rules: {
test: /\.js$/,
loader: "happypack/loader",
// exclude: "node_modules"
/-- 或者 --/
// include: path.resolve("src"),
}
}
减少文件搜索范围
- 优化
resolve.modules
配置(减少模块搜索层级) 优化
resolve.mainFields
配置- 先查找
package.json
中main
字段指定的文件 -> 查找根目录的index.js
-> 查找lib/index.js
- 先查找
优化
resolve.extensions
配置- 模块路径的查找,
import xx from "index"
会先找.js
后缀的
- 模块路径的查找,
- 合理使用
alias
module.exports = {
resolve: {
alias: {
react: path.resolve(__dirname, "./node_modules/react/dist/react.min.js"),
},
modules: [path.resolve(__dirname, "node_modules")], // 查找依赖
extensions: [".js"], // 查找模块路径
mainFields: ["main"], // 查找入口文件
},
};
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。