Webpack 配置指南

webpack 配置指南

  1. 搭建环境
1
2
3
npm init -y # 生成 package.json
npm i webpack --save-dev # 引入 webpack
npm i webpack-cli --save-dev # 引入 webpack-cli
1
2
3
"scripts": {
"build": "webpack",
},

webpack 旧版本必须在 webpack.config.js 通过 entry 属性定义入口,现在默认为./src/index.js

新建./src/index.js文件.执行命令后,dist/main.js为默认输出.

  1. 生产和开发模式
    webpack 分为 ‘development’ 或 ‘production’ 或 ‘none’.
    production 可以开箱即用地进行各种优化。 包括压缩,作用域提升,tree-shaking 等
    development 针对速度进行了优化,仅仅提供了一种不压缩的 bundle.
1
2
3
4
"scripts": {
"build": "webpack --mode production",
"dev": "webpack --mode development"
},
  1. 默认覆盖 entry 和 output
    webpack 支持 ES6, CommonJS, AMD 规范
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* webpack 支持 ES6、CommonJs 和 AMD 规范
*/

// ES6
import sum from './vendor/sum'
console.log('sum(1, 2) = ', sum(1, 2))

// CommonJs
var minus = require('./vendor/minus')
console.log('minus(1, 2) = ', minus(1, 2))

// AMD
require(['./vendor/multi'], function (multi) {
console.log('multi(1, 2) = ', multi(1, 2))
})

webpack.config.js 是 webpack 默认的配置文件名,在根目录下创建.we

1
2
3
4
5
6
7
8
9
10
11
12
const path = require('path')

module.exports = {
entry: {
app: './app.js', // 需要打包的文件入口
},
output: {
publicPath: __dirname + '/dist/', // js 引用的路径或者 CDN 地址
path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
filename: 'bundle.js', // 打包后生产的 js 文件
},
}

path.resolve() 方法会把一个路径或路径片段的序列解析为一个绝对路径。
__dirname: 当前模块的文件夹路径。

执行 build 后会生成多个 bundle 文件,这跟 AMD 的引入方式有关,在实际写代码的时候,最好使用 ES6 和 CommonJS 的规范来写.

// CleanWebpackPlugin 插件可以自动删除 dist 旧文件在打包.

1
npm install clean-webpack-plugin --save-dev
1
2
3
4
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins: [
new CleanWebpackPlugin(), // 默认情况下,此插件将删除 webpack output.path目录中的所有文件,以及每次成功重建后所有未使用的 webpack 资产。
]
  1. babel 转译

webpack 使用 loader 进行转译.babel-loader 可以将 es6 转译为 es5.
我们使用 babel7 版本.
@babel/core
@babel/preset-env: 包含 ES6、7 等版本的语法转化规则
@babel/plugin-transform-runtime: 可以重复使用 Babel 注入的程序代码来节省代码,减小体积.避免 polyfill 污染全局变量,减小打包体积
@babel/polyfill: ES6 内置方法和函数转化垫片,所谓垫片也就是垫平不同浏览器或者不同环境下的差异
babel-loader: 负责 ES6 语法转化

1
2
npm i @babel/core babel-loader @babel/preset-env @babel/plugin-transform-runtime --save-dev
npm i @babel/polyfill @babel/runtime

在项目根目录创建.babelrc配置 babel

1
2
3
4
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}

webpack 配置 loader.

1
2
3
4
5
6
7
8
9
10
11
module: {
rules: [
{
test: /\.js$/, // 使用正则来匹配 js 文件
exclude: /node_modules/, // 排除依赖包文件夹
use: {
loader: 'babel-loader', // 使用 babel-loader
},
},
]
}

在入口文件处全局引入import @babel/polyfill,执行 build 命令打包.
全局引入 @babel/polyfill 的这种方式可能会导入代码中不需要的 polyfill,从而使打包体积更大.
更改 .babelrc,只转译我们使用到的.

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage"
}
]
],
"plugins": ["@babel/plugin-transform-runtime"]
}

注释入口文件全局引入的import @babel/polyfill,执行 build 命令打包.打包体积明显变小.

.browserslistrc用于在不同前端工具之间共享目标浏览器和 Node.js 版本的配置,或在 package 文件中加入

1
2
3
4
5
"browserslist": [
"> 1%",
"last 2 version",
"not ie <= 8"
]
  1. 代码切割 code splitting
1
npm i lodash

新建src/index.js并添加 lodash 相关代码,更改 webpack 配置

1
2
3
4
5
6
7
8
9
entry: {
main: './src/index.js'
},
output: {
publicPath: __dirname + '/dist/', // js 引用的路径或者 CDN 地址
path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
filename: '[name].bundle.js', // 代码打包后的文件名
chunkFilename: '[name].js' // 代码拆分后的文件名
},

运行 build 打包我们会发现,业务代码和第三方框架一起被打包,业务代码更新频繁,而第三方框架不会变,这会造成资源浪费和低效率.
浏览器是有缓存的,如果文件没变动的话,就不用再去发送 http 请求,而是直接从缓存中取,这样在刷新页面或者第二次进入的时候可以加快网页加载的速度。
所以我们可以利用 webpack 的代码分割功能,将第三方代码与业务代码分开,这样及时业务变动,浏览器也可以读取第三方框架的缓存.

webpack4 使用 内置的 splitChunksPlugins 进行代码分割. all 表示分割所有代码, async 为默认值,表示分割异步代码.

1
2
3
4
5
optimization: {
splitChunks: {
chunks: 'all'
}
},

执行 build 命令,发现文件为dist/**.bundle.js dist/vendors-***.js,表示代码分割成功.
对分割名称进行更改.添加 cacheGroups 对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
name: 'vendors',
chunks: 'all',
// test 正则过滤
// priority 优先级
// minChunks 最小公用多少次才打包
// minSize 超过多少进行压缩
// reuseExistingChunk: true // 公共模块必开启,如果当前块已从主模块拆分出来,则将重用它而不是生成新的块

},
default: {
chunks: 'all',
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
  1. 懒加载,Prefetching 预加载

懒加载就是通过 import 去异步的加载一个模块,类似于 vue-router 中的懒加载component: () => import('***.vue').
懒加载并不是 webpack 里的概念,而是 ES6 中的 import 语法,webpack 只是能够识别 import 语法,能进行代码分割而已。
页面加载时,异步的代码不会执行但是确下载下来了,浪费了页面性能,我们希望 webpack 可以把异步代码放到一个模块中.当我们需要时,才会去加载这些代码.这也是为什么 webpack 的 splitChunks 中的 chunks 默认是 async,异步的.

问题又来了,如果异步的代码模块较大,而当我们需要加载时就要等待,体验不好.webpack 的 Prefetching/Preloading 预加载功能可以解决这个问题.它会在网络带宽空闲的时候预先帮我们加载.

webpackPrefetch: 等待核心代码加载完之后,有空闲之后再去加载
webpackPreload: 和核心的代码并行加载,不推荐

使用方式import(/* webpackPrefetch: true */, '......js').then...

  1. 自动生成 html 文件并自动引入相关资源
1
npm i html-webpack-plugin html-loader --save-dev

更改 webpack 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
// 打包输出HTML
title: '自动生成 HTML',
minify: {
// 压缩 HTML 文件
removeComments: true, // 移除 HTML 中的注释
collapseWhitespace: true, // 删除空白符与换行符
minifyCSS: true, // 压缩内联 css
},
filename: 'index.html', // 生成后的文件名
template: 'index.html', // 根据此模版生成 HTML 文件
}),
]

由于使用了 title, 在 index.html 中加<title><%= htmlWebpackPlugin.options.title %></title>,执行命令,dist 中生成 index 的 html 文件.我们发现其中 js 引入为绝对路径,需要更改为相对路径.找到 output 输出配置,更改 publicPath 公共路径,修改为 ./

  1. 处理 css 文件

这次我们需要用到 css-loader,style-loader 等 loader.
css-loader:负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,例如 @import 和 url()等引用外部文件的声明.
style-loader 会将 css-loader 解析的结果转变成 JS 代码,运行时动态插入 style 标签来让 CSS 代码生效。

1
npm i css-loader style-loader --save-dev

更改配置文件

1
2
3
4
5
6
rules: [
{
test: /\.css$/, // 针对 .css 后缀的文件设置 loader
use: ['style-loader', 'css-loader'],
},
]

可以发现在 style 标签中出现了相关 css,如果要单独打包成文件,需要 mini-css-extract-plugin 插件

1
npm i mini-css-extract-plugin --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
})

rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader',
],
},
]

执行 build,生成了单独的 css 文件,但是并没有压缩,引入 optimize-css-assets-webpack-plugin 插件来实现 css 压缩.
webpack5 使用 css-minimizer-webpack-plugin 插件.

1
npm install css-minimizer-webpack-plugin --save-dev

更改配置

1
2
3
4
5
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')

optimization: {
minimizer: [new CssMinimizerPlugin()]
}

处理 scss 或 less.

1
npm i node-sass sass-loader --save-dev

更改 loader:

1
2
3
4
5
6
7
8
9
10
{
test: /\.(scss|css)$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
'css-loader',
'sass-loader'
]
}

根据 webpack 规则:放在最后的 loader 首先被执行,从上往下写的话是下面先执行,从左往右写的话是右边先执行。

为 css 加上浏览器前缀.

1
npm install postcss-loader autoprefixer --save-dev

新建 postcss.config.js ,并修改 loader 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
plugins: [require('autoprefixer')]
}
{
test: /\.(scss|css)$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
'css-loader',
'postcss-loader'
'sass-loader'
]
}
  1. Tree Shaking
    字面意思是摇树,项目中没有使用的代码会在打包的时候丢掉。JS 的 Tree Shaking 依赖的是 ES6 的模块系统 import 和 export.
    对于经常使用的第三方库,要安装 ES 写法的版本,比如 lodash-es

webpack5 可以通过 package.json 的 “sideEffects”(意为副作用) 属性,通知 webpack 安全的删除未使用的 export.而 optimization.usedExports: true,依赖于 terser 去检测语句中的副作用.

css tree shaking: 略.

  1. 图片字体资源的处理
1
npm install url-loader file-loader --save-dev

更改配置

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
rules: [
{
test: /\.(png|jpg|jpeg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
name: '[name]-[hash:5].min.[ext]',
outputPath: 'images/', //输出到 images 文件夹
limit: 20000, //把小于 20kb 的文件转成 Base64 的格式
},
},
],
},
{
test: /\.(eot|woff2?|ttf|svg)$/,
use: [
{
loader: 'url-loader',
options: {
name: '[name]-[hash:5].min.[ext]',
limit: 5000, // fonts file size <= 5KB, use 'base64'; else, output svg file
publicPath: 'fonts/',
outputPath: 'fonts/',
},
},
],
},
]
  1. 处理第三方 js 库
1
2
3
4
5
6
7
8
9
10
resolve: {
alias: {
jQuery$: path.resolve(__dirname, 'src/vendor/jquery.min.js')
}
},

new webpack.ProvidePlugin({
$: 'jquery', // npm
jQuery: 'jQuery' // 本地Js文件
})
  1. webpack-dev-server
1
npm i webpack-dev-server --save-dev
1
2
3
4
"scripts": {
"dev": "webpack-dev-server --open",
"build": "webpack --mode production"
},

配置

1
2
3
4
5
6
mode: 'development', // 开发模式
devtool: 'source-map', // 开启调试
devServer: {
port: 8080,
//...
}
  1. DllPlugin 加快打包速度

新建 webpack.dll.js 文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const path = require('path')

module.exports = {
mode: 'production',
entry: {
vendors: ['lodash', 'jquery'],
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, 'dll'),
library: 'dll_[name]',
},
plugins: [
new webpack.DllPlugin({
name: 'dll_[name]',
path: path.join(__dirname, 'dll', '[name].manifest.json'),
context: __dirname,
}),
],
}

json 文件中新增命令

1
"build:dll": "webpack --config ./build/webpack.dll.js"

在 webpack 配置中添加

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
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin') // dll自动引入

// dll配置
const dllReference = (config) => {
config.plugin('vendorDll').use(webpack.DllReferencePlugin, [
{
context: __dirname,
manifest: require('./dll/vendor.manifest.json'),
},
])

config
.plugin('addAssetHtml')
.use(AddAssetHtmlPlugin, [
[
{
filepath: require.resolve(
path.resolve(__dirname, 'dll/vendor.dll.js')
),
outputPath: 'dll',
publicPath: '/dll',
},
],
])
.after('html')
}

chainWebpack: (config) => {
dllReference(config)
}
  1. 多页面打包
    在 webpack.base.conf.js 中配置 entry,配置两个入口.然后多次调用 new HtmlWebpackPlugin({})插件.或写个函数自动生成配置.

  2. 其他

  • webpack-merge 用来合并 webpack 配置
  • workbox-webpack-plugin 用来配置 PWA
  • ts-loader 以及 ts.config.js 用来配置 ts
  • eslint-loader 以及 .eslintrc.js
  1. 编写 loader

新建一个 js 文件

1
2
3
module.exports = function (source) {
return source.replace('world', this.query.name)
}

source 参数就是我们的源代码,这里是将源码中的 world 替换成我们自定义的 字符串.

修改 webpack 配置添加自定义 loader.

1
2
3
4
5
6
7
8
9
10
11
12
13
rules: [
{
test: /.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
options: {
name: 'xh',
},
},
], // 引入自定义 loader
},
]
  1. 编写 plugin

新建 xxx-webpack-plugin 的 js 文件,plugin 是一个类, 调用的时候要 new 这个类生成实例.

1
2
3
4
5
6
7
8
9
10
class CopyrightWebpackPlugin {
constructor(options) {
console.log('插件被使用了', '参数为', options)
}
apply(compiler) {
// compiler 是 webpack 的实例
}
}

module.exports = CopyrightWebpackPlugin

调用:

1
2
3
4
5
6
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')
plugins: [
new CopyrightWebpackPlugin({
name: 'xx'
})
],
  1. webpack 原理

初始化阶段: - 初始化参数: 从配置文件或 shell 获取参数 - 创建编译器对象: 根据参数创建 compile 对象 - 初始化编译环境: 注入内置插件,rule 规则,加载的插件. - 开始编译: 执行 compile 的 run 方法 - 确定入口: 根据配置的 entry 找到入口,将入口文件转换为 dependence 对象

构建阶段: - 编译模块(make): 根据 entry 对应的 dependence 创建 module 对象,调用 loader 将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理. - 完成编译: 得到了每个模块被翻译后的内容以及它们之间的 依赖关系图

生成阶段: - 输出资源: 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表 - 写入文件系统(emitAssets): 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统.