webpack 和 babel
一、webpack 的基本使用
1. 拆分配置和 merge
- 通用配置
- 开发环境配置
- 生产环境配置
通过引用webpack-merge
插件的merge
或smart
方法合并。配置要点如下代码:
// webpack.common.js
// 通用配置,配置开发环境和生产环境都通用的一些插件和加载器
module.exports = {
module: {
rules: [],
plugins: [],
},
};
// webpack.dev.js
// 开发环境专属配置
module.exports = {
mode: "development",
module: {
rules: [],
plugins: [
new webpack.DefinePlugin({
ENV: JSON.stringify("development"),
}),
],
},
};
// webpack.prod.js
// 生产环境专属配置
module.exports = {
mode: "production",
module: {
rules: [],
plugins: [
new webpack.DefinePlugin({
ENV: JSON.stringify("production"),
}),
],
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2. 启动服务
// webpack.dev.js
// 开发环境专属配置
module.exports = {
mode: 'development',
module: {
rules: [],
plugins: [
new webpack.DefinePlugin({
ENV: JSON.stringify('development')
})
]
},
devServer: {
port: 8000,
progress: true, // 显示打包进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动gzip压缩
// 设置代理,可以解决跨域问题
proxy: {
// 将 /api 代理到 http://localhost:3000
'/api': 'http://localhost:3000',
'/api2': ...,
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
3. 处理 ES6
使用babel-loader
将 ES6 及以上版本的语法转换为 ES5 语法,配置如下:
(1)webpack 配置
rules: [
{
test: /\.js$/,
loader: ["babel-loader"],
include: srcPath,
exclude: /node_modules/,
},
];
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
(2)babel 配置
在.babelrc
文件中进行配置:
{
"presets": ["@babel/preset-env"],
"plugins": []
}
1
2
3
4
2
3
4
3. 处理样式
(1)webpack 配置
rules: [
{
test: /\.css$/,
// loader 的执行顺序:从后往前
loader: ["style-loader", "css-loader", "postcss-loader"],
},
{
test: /\.less$/,
// loader 的执行顺序:从后往前
loader: ["style-loader", "css-loader", "less-loader"],
},
];
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
postcss-loader
,可以处理浏览器兼容性,比如增加 css 属性前缀-webkit-
css-loader
解析 cssstyle-loader
将 css 以<style>
标签插入 html 中less-loader
解析 less,输出 css
(2)postcss 配置
在postcss.config.js
中配置:
module.exports = {
// 增加css属性前缀,以兼容浏览器
plugins: [require("autoprefixer")],
};
1
2
3
4
2
3
4
4.处理图片
module.exports = {
rules: [
// dev,图片解析,解析通过 import './xxx.png'导入的文件
{
test: /\.(png | jpg | jpeg | gif)$/,
loader: ["file-loader"],
},
// prod,生产环境根据图片大小分别处理,小于某个值时使用 base64,否则直接通过路径引用
{
test: /\.(png | jpg | jpeg | gif)$/,
use: {
loader: "url-loader",
options: {
// 小于 5kb 的图片用 base64 格式产出,直接在js中插入 base64 字符串
limit: 5 * 1024,
// 如果 大于 5kb 的图片,输出到 img 目录下,在标签中以路径形式引用
ouputPath: "/img/",
// 将图片放到cdn中,在标签中以 cdn 路径引用
// publishPath: 'http://cdn.abc.com'
},
},
},
],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
二、webpack 的高级配置
1. 多入口配置
(1)入口配置
module.exports = {
entry: {
// 这里的 index 和 other 表示 chunk name,有两个 chunk
index: path.join(srcPath, "index.js"),
other: path.join(srcPath, "other.js"),
},
};
1
2
3
4
5
6
7
2
3
4
5
6
7
(2)输出配置
module.exports = {
output: {
// 1. 输出的 bundle 的文件名,
// 2. name 对应 entry 中的 key,即 chunk name
// 3. contentHash:8 根据内容的8位hash,内容不改变时,hash不变,浏览器可以命中缓存,提升加载速度
filename: "[name].[contentHash:8].js",
// 输出路径
path: distPath,
},
};
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
(3)html-webpack-plugin 配置
module.exports = {
plugins: [
// 多入口 - 生成 index.html,并将 生成的 bundle 通过 <script> 标签插入到 index.html中
new HtmlWebpackPlugin({
template: path.join(srcPath, "index.html"),
filename: "index.html",
// chunks 表示该页面要引用哪些 chunk,若不写,则引用全部 chunk
chunks: ["index"], // 只引用 index.js
}),
// 多入口 - 生成 other.html,并将 生成的 bundle 通过 <script> 标签插入到 other.html中
new HtmlWebpackPlugin({
template: path.join(srcPath, "other.html"),
filename: "other.html",
chunks: ["other"], // 只引用 other.js
}),
],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2. 抽离压缩 css 文件
步骤:
- 生产环境中使用
MiniCssExtractPlugin.loader
代替style-loader
,开发环境不变 - 在
plugins
中增加mini-css-extract-plugin
配置,css 的生产路径和文件名 - 在
optimization
中增加terser-webpack-plugin
和optimize-css-assets-webpack-plugin
配置,对 css 进行压缩
module.exports = {
rules: [
// dev,开发环境下,不需要抽离和压缩
{
test: /\.css$/,
loader: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.less$/,
loader: ['style-loader', 'css-loader', 'less-loader']
}
// prod,生产环境下,抽离和压缩css(需要导入 mini-css-extract-plugin)
{
test: /\.css$/,
loader: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
{
test: /\.less$/,
loader: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader', 'postcss-loader']
}
],
plugins: [
// prod
new MiniCssExtractPlugin({
filename: 'css/main.[contentHash:8].css'
})
],
// prod 压缩
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
3. 抽离公共代码
开发模式下不需要设置,生成环境下抽离
module.exports = {
optimization: {
splitChunks: {
// 抽离的处理模式,有以下三个选择:
// 1. initial 入口 chunk,对于异步导入的文件不处理
// 2. async 异步 chunk,只对异步导入的文件处理
// 3. all 全部 chunk
chunks: "all",
// 缓存分组
cacheGroups: {
// 第三方模块,一般是通过 npm 安装的第三方依赖
vendor: {
name: "vendor", // chunk 名称
priority: 1, // 优先级,越大优先级越高,优先抽离
test: /node_modules/,
minSize: 1, // 模块的大小限制,小于这个值时,不在抽离
minChunks: 1, // 最少复用过多少次就要分离到这个 chunk 中
},
// 公共模块
common: {
name: "common", // chunk 名称
priority: 0, // 优先级,权限更高,优先抽离
test: /node_modules/,
minSize: 1, // 公共模块的大小限制,小于这个值时,不在抽离
minChunks: 2, // 最少复用过多少次就要分离到这个 chunk 中
},
},
},
},
plugins: [
// 多入口 - 生成 index.html,并将 生成的 bundle 通过 <script> 标签插入到 index.html中
new HtmlWebpackPlugin({
template: path.join(srcPath, "index.html"),
filename: "index.html",
// chunks 表示该页面要引用哪些 chunk,若不写,则引用全部 chunk
chunks: ["index", "common", "vendor"], // 只引用 index common 和 vendor
}),
// 多入口 - 生成 other.html,并将 生成的 bundle 通过 <script> 标签插入到 other.html中
new HtmlWebpackPlugin({
template: path.join(srcPath, "other.html"),
filename: "other.html",
chunks: ["other", "vendor"], // 只引用 other 和 vendor
}),
],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
4. 懒加载(异步加载)
- 通过
import()
导包 - 异步组件
- vue-router 懒加载
通过以上三种方式使用异步加载,webpack 自动生成以数字开头的bundle,如 0.bundle.js
5. 处理 JSX
通过 babel 设置 jsx 预处理 presets
6. 处理 Vue
vue-loader
7. module chunk 和 bundle 的区别
- modle,表示各个源文件,包括 js,less,css,png 等资源,在 webpack 中一切皆模块
- chunk,多个 module 合成,有三个来源:
entry
,splitChunk的cacheGroups中的chunk
和异步import()导入的模块
- bundle,最终的输出文件,一般一个 chunk 对应一个 bundle
三、webpack 的性能优化
- 优化构建速度-开发体验和效率
- 优化产出代码吗-产品性能
从分析问题,解决问题的角度进行阐述,不要背诵
1. 构建速度
- 优化 babel-loader
- IgnorePlugin
- noParse
- happyPack
- ParallelUglifyPlugin
- 自动刷新
- 热更新
- DllPlugin
(1)优化 babel-loader
{
test: /\.js$/,
use: ['babel-loader?cacheDirectory'], // 开启缓存
include: path.resolve(__dirname, 'src'), // 明确范围
// 排除范围,和include二选一
// exclude: path.resolve(__dirname, 'node_modules')
}
1
2
3
4
5
6
7
2
3
4
5
6
7
(2)IgnorePlugin 避免引用无用模块
以moment
库引入为例,moment
库包含各个语言资源,忽略掉没用到的语言
1. 在代码中引入
import moment from "moment";
// 引入中文语言包
import "moment/locale/zh-cn";
// 设置语言为中文
moment.locale("zh-cn");
1
2
3
4
5
2
3
4
5
2. 在 webpack 中配置忽略规则
module.exports = {
plugins: [new wepack.IgnorePlugin(/\.\/locale/, /moment/)],
};
1
2
3
2
3
(3)noParse 避免重复打包
有些引用的包发布时,已经经过打包处理,无需重复打包,例如xxx.min.js
的包,下面以react.min.js
为例:
module.exports = {
module: {
// 忽略 `react.min.js` 文件的递归解析处理
noParse: [/react\.min\.js$/],
},
};
1
2
3
4
5
6
2
3
4
5
6
IgnorePlugin
和noParse
的区别
IgnorePlugin
不引用,代码中没有,提升构建速度,且优化产出noParse
引入,但不打包
(4)happyPack 多进程打包
- JS 是单线程,所以项目大且复杂时,打包时速度慢
- 开启多进程打包,提高构建速度
const HappyPack = require("happypack");
module.exports = {
module: {
rules: [
// 修改babel-loader配置,替换成happypack中的babel loader
{
test: /\.js$/,
use: ["happypack/loader?id=babel"], // id 对应 plugins 中 HappyPack配置的id
},
],
},
plugins: [
new HappyPack({
// 用唯一的 id 来代表当前的 HappPack 用来处理某一类特定的文件
id: "babel",
// 如何处理 js 文件,用法和 loader 配置一样
loaders: ["babel-loader?cacheDirectory"],
}),
],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(5)ParallelUglifyPlugin 多进程压缩 JS
- webpack 内置 Uglify 工具压缩 JS
- JS 是单线程,通过插件开启多进程压缩,提升压缩速度
const ParallelUglifyPlugin = require("webpack-parallel-uglify-plugin");
module.exports = {
plugins: [
new ParallelUglifyPlugin({
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有注释
},
compress: {
drop_console: true, // 删除 console 语句
collapse_vars: true, // 内嵌定义了但是只用到一次的变量
reduce_vars: true, // 提取出现多次单没有定义成变量去引用的静态值
},
},
}),
],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
关于开启多进程打包和压缩:
- 项目大,打包速度慢,开启多进程能提高速度
- 项目小,打包速度快,开启多进程会降低速度(进程开销)
- 要按需使用
(6)热更新
- 热更新有代码侵入,只能在开发环境下使用,开发完成后需要删除侵入代码
- 根据项目情况使用
// 1. webpack dev中配置
const HotModuleReplacementPlugin = require("webpack/lib/HotModuleReplacementPlugin");
module.exports = {
entry: {
// 这里变成了数组
index: [
"webpack-dev-server/client?http//localhost:8080/",
"webpack/hot/dev-server",
path.join(srcPath, "index.js"),
],
},
plugins: [new HotModuleReplacementPlugin()],
devServer: {
hot: true,
},
};
// 2. 在引用的js中侵入测试代码,css,less等无需侵入,修改后直接生效
import { sum } from "./math";
// 增加,开启热更新后的逻辑代码
if (module.hot) {
// 更改 math 内,会触发热更新
module.hot.accept(["./math"], () => {
const sumRes = sum(10, 20);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(7)DllPlugin 动态链接库插件的使用
- 前端框架如 vue react,体积打,构建慢
- 较稳定,不常升级版本
- 同一个版本只构建一次即可,不用每次都重新构建
简单说明
- webpack 已内置 DllPlugin 的支持
- DllPlugin-打包出 dll 文件
- DllReferencePlugin-使用 dll 文件
配置步骤
1. 新建一个 webpack.dll.js,使用 npm webpack 生产 dll
const path = require("path");
const DllPlugin = require("webpack/lib/DllPlugin");
const { srcPath, distPath } = require("./paths");
module.exports = {
mode: "development",
// JS 执行入口文件
entry: {
// 把 React 相关模块的放到一个单独的动态链接库
react: ["react", "react-dom"],
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
// 也就是 entry 中配置的 react 和 polyfillfile
name: "[name].dll.js",
// 输出的文件都放到 dist 目录下
path: distPath,
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_ 是为了防止全局变量冲突
library: "_dll_[name]",
},
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
// 例如 react.manifest.json 中就有 "name": "_dll_react"
name: "_dll_[name]",
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(distPath, "[name].manifest.json"),
}),
],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<script src="./react.dll.js" />
方式引用
2. 在 html 模板中如 index.html 中通过3. 在 webpack.dev.js 中使用 DllReferencePlugin 配置
// 第一,引入 DllReferencePlugin
const DllReferencePlugin = require("webpack/lib/DllReferencePlugin");
modlue.exports = {
mode: "development",
module: {
rules: [
{
test: /\.js$/,
loader: ["babel-loader"],
include: srcPath,
exclude: /node_modules/, // 第二,不要转换 node_modules
},
],
},
plugins: [
// 第三,告诉 webpack 使用了哪些动态链接库
new DllReferencePlugin({
// 描述 react 动态链接库的文件内容
manifest: require(path.join(distPath, "react.manifest.json")),
}),
],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2. webpack 优化构建速度(可用于生产环境)
- 优化 babel-loader(缓存-开发环境,明确和忽略范围)
- IgnorePlugin(减少产出代码体积)
- noParse(开发,生产皆可)
- happyPack(按项目情况使用,开发,生产均可)
- ParallelUglifyPlugin(若使用,必须使用生产环境)
3. webpack 优化构建速度(不可用于生产环境)
- 自动刷新
- 热更新
- DllPlugin
4. webpack 性能优化-产出代码
- 体积更小
- 合理分包,不重复加载
- 速度更快、内存使用更少
- 小图片 base64 编码(减少网络请求)
- bundle 加 hash(命中缓存)
- 懒加载-
import()
- 提取公共代码
- IgnorePlugin(输出的 bundle 体积减少)
- 使用 CDN 加速(publicPath)
- 使用 production
- 默认启动代码压缩
- Vue React 会自动删除调试代码,如 warning
- 默认启动 Tree-Shaking
- Scope Hosting
- 代码体积更小
- 创建函数作用域更少
- 代码可读性更好
const MudleConcatenationPlugin = require("webpack/lib/optimize/MudleConcatenationPlugin");
module.exports = {
resolve: {
// 针对 npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFiles: ["jsnext:main", "brower", "main"],
},
plugins: [new MudleConcatenationPlugin()],
};
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
四、babel
将 ES6 及以上语法或 JSX 代码转换成 ES5 语法代码,它只负责解析语法并转换,不对符合规范的 API 进行解析,所以要使用到 babel-polyfill
- 基本配置
- babel-polyfill
- babel-runtime
1. 基本配置
// .babelrc 文件配置
{
presets: ['@babel/presets-env'],
plugins: [ ],
}
1
2
3
4
5
2
3
4
5
2. presets 和 plugins
presets 是多个 plugin 的集合
3. babel-polyfill
- core-js 和 regenerator
- core-js 集成了所有 ES6 语法的 polyfill 补丁
- regenerator:core-js 不支持 generator 函数,regenerator 弥补了 generator
babel-polyfill 是 core-js 和 regenerator 的集合
babel-polyfill 在 Babel 7.4 之后弃用 babel-polyfill,并推荐直接使用 core-js 和 regenerator
(1)babel-polyfill 的按需引入
babel-polyfill 中包含所有的 ES6 语法,全部引入会引入不必要的代码,需要按需引入
// .babelrc 文件配置
{
presets: [
'@babel/presets-env',
{
"useBuiltIns": "useage", // 语法的所属阶段
"corejs": 3 // core-js版本
}
],
plugins: [ ],
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
(2)babel-polyfill 的问题
- babel-polyfill 引入的代码会污染全局变量
- 如果做一个独立的 web 系统,则无碍
- 如果做一个第三方的 lib,则会污染全局变量,可能会和用户使用的全局变量冲突
**解决方案:**babel-runtime
4. babel-runtime
解决 babel-polyfill 全局变量冲突
// .babelrc 文件配置
{
presets: [
'@babel/presets-env',
{
"useBuiltIns": "useage", // 语法的所属阶段
"corejs": 3 // core-js版本
}
],
plugins: [
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 3,
"helpers": true,
"regenerator": true,
"useESModule": false
}
],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20