【长文】玩转 webpack 学习笔记
写在最前面
最近工作中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和NamedModulesPluginproduction:生产环境,设置process.env.NODE_ENV的值为production。开启FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitonErrorsPlugin,OccurrenceOrderPlugin,SideEffectsFlagPlugin和TerserPluginnone:不开启任何优化选项
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:默认为falsewatchOptions:只有watch为true时,才生效ignored:需要忽略监听的文件或者文件夹,默认为空,忽略node——modules性能会有所提升。aggregateTimeout:监听到文件变化后,等待的时间,默认300mspoll:轮询时间,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组成,可以通过代码分割成多个chunkbundle:打包生成的最终文件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.ensureES6: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"], // 查找入口文件
},
};转载自:https://segmentfault.com/a/1190000040646081