webpack 学习笔记

webpack 是什么?

它是一个模块打包工具

Loader 是什么?

webpack 不能识别 JavaScript 之外的文件,需要 loader 对它时行识别

file-loader

file-loader 就是在 JavaScript 代码里 import/require 一个文件时,会将该文件生成到输出目录,并且在 JavaScript 代码里返回该文件的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|jpeg|gif|svg)/,
use: {
loader: "file-loader",
options: {
// placeholders 占位符
name: "[name].[ext]", // 自定义打包文件名
outputPath: "images/", // 自定义输出目录
},
},
},
],
},
};

url-loader

url-loader 功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: "url-loader",
options: {
limit: 8192,
},
},
],
},
],
},
};

处理 Sass

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
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
// 处理过程是从下到上的,从右到左的
{
loader: "style-loader", // style-loader 会在 head 里插入一个 `<style>` 标签,并且将 CSS 写入这个标签内。
},
{
loader: "css-loader", // 将 CSS 转化成 CommonJS 模块
},
{
loader: "sass-loader", // 将 Sass 编译成 CSS
},
{
loader: "postcss-loader", // 添加私有属性
options: {
plugins: [require("autoprefixer")({})],
},
},
],
},
],
},
};

处理 CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
module: {
rules: [
{
test: /\.(css)$/,
use: [
{
loader: "style-loader",
},
{ loader: "css-loader" },
],
},
],
},
};

Plugins 是什么?

在 webpack 运行到某个时刻的时做一些事情

HtmlWebpackPlugin

会在打包结束后,自动生成一个 html 文件,并把打包生成的 JS 文件自动引入到这个 html 文件里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var HtmlWebpackPlugin = require("html-webpack-plugin");
var path = require("path");

module.exports = {
entry: "index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "index_bundle.js",
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html", // 通过模板来生成html
}),
],
};

CleanWebpackPlugin

用于在下一次打包时清除之前打包的文件

1
2
3
4
5
6
7
8
9
10
11
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
var path = require("path");

module.exports = {
entry: "index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "index_bundle.js",
},
plugins: [new CleanWebpackPlugin()],
};

SourceMap

可以将编译后的代码映射回原始源代码,用来追踪到 error(错误) 和 warning(警告) 在源代码中的原始位置。

1
2
3
4
5
6
7
8
module.exports = {
devtool: "source-map",
};

// inline-source-map: 不生成.map文件,会以 base64 的字符串加入到打包文件中,报错信息会精确到行和列
// cheap-inline-source-map: 不生成.map文件,会以 base64 的字符串加入到打包文件中,报错信息只会精确到行(打包性能会提升)
// cheap-module-eval-source-map: 默认它只会处理业务中错误,不会处理module里错误,如果加入module,就会处理第三方模块的错误
// eval:这种打包方式最快,性能最好,但错误提示比较弱

最佳实践

1
2
devtool:"cheap-module-eval-source-map",// 开发环境配置
devtool:"cheap-module-source-map", // 线上生成配置

devServer

1
2
3
4
5
6
7
8
9
10
let path = require("path");

module.exports = {
//...
devServer: {
contentBase: path.join(__dirname, "dist"), // 默认情况下,将使用当前工作目录作为提供内容的目录。
open: true, // 告诉 dev-server 在 server 启动后打开浏览器。默认禁用。
port: 9000,
},
};

Hot Module Replacement

它允许在项目在修改的时候,无需完全刷新页面。

1
2
3
4
5
6
7
8
9
const webpack = require("webpack");

module.exports = {
devServer: {
hot: true, // 启用 HMR
hotOnly: true, // 在 HMR 失效时,不会刷新页面
},
plugins: [new webpack.HotModuleReplacementPlugin()],
};

使用 Bable 编译 ES6

bable 转 ES6 相关:

  1. babel-loader: 负责 es6 语法转化
  2. babel-preset-env: 包含 es6、7 等版本的语法转化规则
  3. babel-polyfill: es6 内置方法和函数转化垫片
  4. babel-plugin-transform-runtime: 避免 polyfill 污染全局变量

需要注意的是, babel-loaderbabel-polyfill。前者负责语法转化,比如:箭头函数;后者负责内置方法和函数,比如:new Set()

1
2
npm install --save-dev babel-loader @babel/core
npm install @babel/preset-env --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
];
}

如果需要对Promise、map 之类的方法做兼容处理则需要安装babel-polyfill

1
npm install --save @babel/polyfill
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env"],
{
targets: {
// 根据目标版本号选择是否要进行转换
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns: "usage", // 按需引入polyfill,这样能大大减少打包编译后的体积
},
],
},
},
];
}

注意:使用 "useBuiltIns": "usage"后,不需要在项目里在引入import '@babel/polyfill'

默认polyfill会污染全局变量,如果开发一个第三方模块或者库的时候则需要用@babel/runtime

1
2
npm install --save @babel/runtime
npm install --save @babel/runtime-corejs2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
plugins: [
[
"@babel/plugin-transform-runtime",
{
absoluteRuntime: false,
corejs: 2,
helpers: true,
regenerator: true,
useESModules: false,
},
],
],
},
},
];
}

如果配置文件较多时,可以项目内新建一个.babelrc,然后把options里的内容放进去

.babelrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"plugins" : [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}

wepback.config.js

1
2
3
4
5
6
7
8
9
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
},
];
}

Tree Shaking

清除到代码中无用的js代码,只支持ES Module,也就是只支持import(静态引入)方式引入,不支持CommonJS(动态引入)的方式引入

modeproduction时就会进行Tree Shaking,为了方便你的调试可以在配置文件中加入如下代码:

1
2
3
optimization: {
usedExports: true;
}

package.json

1
2
3
4
5
// 假如我们在某个模块里需要引入一个库或者css文件,但它没有导出任何东西时,Tree Shaking 会忽略掉它,这时需要进行手动设置,例如:

sideEffects: false // false 对所有模块进行 Tree Shaking
sideEffects: ["@babel/polyfill"] // 不要对 @babel/polyfill 进行 Tree Shaking
sideEffects: ["*.css"] // 不要对 css 进行 Tree Shaking

Code Splitting

异步的代码 (importwebpack会自动的进行代码分割,同步代码则需要进行如下配置:

1
2
3
4
5
optimization: {
splitChunks: {
chunks: "all";
}
}

SplitChunksPlugin 配置详解

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
module.exports = {
//...
optimization: {
splitChunks: {
chunks: "async", // all:所有代码生效、async:只对异步代码生效、initial:只对同步代码生效
minSize: 30000, // 30kb 大余某个值才会做代码分割
minRemainingSize: 0,
maxSize: 0,
minChunks: 1, // 当对一个模块用了多少次的时候才进行代码分割
maxAsyncRequests: 6,
maxInitialRequests: 4, // 入口文件请求数量超过4个时就不会进行代码分割
automaticNameDelimiter: "~", // 打包后文件名的连接符
automaticNameMaxLength: 30,
cacheGroups: {
// 缓存组,根据cacheGroups来判断将打包文件放到哪个文件里去
vendors: {
// vendors 组
test: /[\\/]node_modules[\\/]/,
priority: -10, // 权重,数值越大权重则越大
},
default: {
// default 组
minChunks: 2,
priority: -20,
reuseExistingChunk: true, // 如果某个模块已经被打包过了,在打包的时就忽略,直接引用之前的
},
},
},
},
};

如果对代码进行代码分割,它会去看cacheGroups.vendors这个参数,如果是false则不会进行代码分割,反之,如果引入的库是cacheGroups.vendors.test里的,则会代码分割,如果不是cacheGroups.vendors.test里的,会去看cacheGroups.default这个参数,如果是false则不会进行代码分割,反之则会

为什么 splitChunks.chunks 的默认值是async

因为异步的代码才能提高打包的性能,而同步代码则只能增加缓存

CSS Code Splitting

1
npm install --save-dev mini-css-extract-plugin

webpack.config.js

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
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css", // 如果是被直接引用的就走这里
chunkFilename: "[id].css", // 如果是被间接引用的就走这里
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader, // 替换 style-loader,目前只适合在线上环境使用,因为不支持HMR
options: {
publicPath: "../",
},
},
"css-loader",
],
},
],
},
};

prefetch/preload module

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:

  • prefetch(预取):当核心代业务代码加载完成后,空闲时候后在加载需要的资源
  • preload(预加载):和核心代业务代码一起进行并行加载

下面这个 prefetch 的简单示例中,有一个 HomePage 组件,其内部渲染一个 LoginButton 组件,然后在点击后按需加载 LoginModal 组件。

LoginButton.js

1
import(/* webpackPrefetch: true */ "LoginModal");

这会生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js 文件。