0.webpack5 学习
0.1 webpack 配置文件
- 更改入口文件并构建:
webpack --entry ./src/main.js
- 更改出口文件并构建:
webpack --entry ./src/main.js --output-path ./build
- 通过设置 package.json 使之支持
npm run build
命令:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --entry ./src/main.js --output-path ./build"
}
2
3
4
0.2 修改 webpack.config.js 名称
设置 package.json:
"build": "webpack --config lg.webpack.js"
0.3 loader 知识补充
- webpack5 loader 分为行内 loader 和 配置里的 loader。如果不用类似上面的配置 loader,而是用行内 loader,写法如下:
import 'css-loader!../css/login.css'
- loader 在配置里的两种简写:
const { resolve } = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
// {
// test: /\.css$/,
// use: [
// {
// loader: 'css-loader'
// }
// ]
// }
// 简写一
// {
// test: /\.css$/,
// loader: 'css-loader'
// }
// 简写三
{
test: /\.css$/,
use: ['css-loader']
}
]
}
}
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
4.20 总结
- 提升了开发体验
- 使用
source-map
让开发或上线时代码报错更加准确的错误提示
- 使用
- 提升了 webpack 打包构建速度
- 使用
HotModuleReplacement
让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而更新速度更快 - 使用
OneOf
让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快 - 使用
Include/Exclude
排除或只检测某些文件,处理的文件更少,速度更快 - 使用
Cache
对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快 - 使用
Thread
多线程处理eslint
和babel
任务,速度更快(代码较多时开启才显著)
- 使用
- 减少代码体积
- 使用
Tree shaking
剔除了没有使用的多余代码,让代码积极更小 - 使用
@babel/plugin-transform-runtime
插件对 babel 进行处理,让辅助代码从中引入 ,而不是每个文件都生成辅助代码,从而体积更小 - 使用
image Minimizer
对项目中的图片进行压缩,体积更小,请求速度更快(对静态图片才需要进行压缩)
- 使用
- 优化代码运行性能
- 使用
Code split
对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快,并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源 Preload/Prefetch
对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好- 使用
Network cache
能对输出资源文件进行更好的命名,将来好做缓存 ,从而用户体验更好 - 使用
core-js
对 js 进行兼容性处理,让我们代码能运行在低版本浏览器 - 使用
pwa
能让代码离线也能访问,从而提升用户体验
- 使用
0.5 importLoaders 属性 - 3.3 的一些内容补充
当 css 文件中引入了另一个 css 文件时,被引入的文件并不会进行代码兼容化处理。这时候就要设置 importLoaders
const { resolve } = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
/**
* postcss-loader 将处理好的 css 给 css-loader 后,
* css-loader 发现其提交的 css 有引入另一个 css,
* 这时候会往前一步,即 importLoaders: 1
*/
importLoaders: 1
}
},
'postcss-loader' // 会自动找 postcss.config.js 配置文件
]
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'postcss-loader', // 会自动找 postcss.config.js 配置文件
'less-loader'
]
}
]
}
}
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
1.webpack 简介
1.1 webpack 是什么
- webpack 是一种前端静态资源打包工具,一个静态模块打包器(module bundler)。在 webpack 看来,前端的所有资源文件(js/json/css/img/less/...)都会作为模块处理,它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源(bundle)
- 当使用
import $ from jquery
这个 es6 语法或者 less 语法时,浏览器都不能识别,需要一个构建工具将这些语法编译成浏览器能识别的代码。(就是将前端的一些编译小工具整合起来) - 资源文件需要构建工具处理,先用打包起点(入口文件)开始打包,把每个依赖关系记录好形成依赖关系树状图,将类似 less、jQ 等形成 chunk(代码块),再进行一系列操作(类似将 less 编译成 css,es6 变成 es5 等)形成 bundle(静态资源)
webpack 本身功能:
- 开发模式:仅能编译 JS 中的 ES MODULE 语法
- 生产模式:能编译 JS 中的 ES MODULE 语法,还能压缩代码
开发模式介绍:
- 编译代码,让代码运行更加健壮
- 代码质量检测,树立代码规范
1.2 五个核心概念
- Entry:指示哪个文件为入口开始打包,分析构建内部依赖图
- Output:指示打包后的资源 bundles 输出到哪里去,以及如何命名
- Loader:Loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript 和 JSON)
- Plugins:插件可以用于执行范围更广的任务,插件的范围包括,从打包优化和压缩,一直到重新定义环境的变量
- Mode:模式指示 webpack 使用相应模式的配置:
选项 | 描述 | 特点 |
---|---|---|
development | 会将 process.env.NODE_ENV 的值设为 development,启用 NamedChunksPlugin 和 NamedModulesPlugin | 能让代码本地调试的环境 |
production | 会将 process.enc.NODE_ENV 的值设为 production,启用 FlagDependencyUsagePlugin、FlagincludedChunksPlugin、ModuleConcatenationPlugin、NoEmitOnErrorsPlugin、OccurrenceOrderPlugin、SideEffectsFlagPlugin 和 UglifyJsPlugin | 能让代码优化上线运行的环境 |
2.webpack 基础
2.1 基本框架
npm init -y
:初始化项目npm i webpack webpack-cli -g
:全局安装以便快捷使用指令,否则每次都要写npx
npm i webpack webpack-cli -D
:安装开发依赖- 新建 src 目录:源代码目录
- 新建 build 目录:通过打包处理后输出的目录
- 在 src 目录新建 index.js 作为入口文件
- index.js 代码如下:
/*
1.运行指令:
开发环境:webpack --entry ./src/index.js -o ./build --output-filename built.js --mode=development
翻译:webpack 会以 ./src/index.js 为入口文件开始打包,打包后输出到 ./build/main.js,整体打包环境是开发环境
生产环境:webpack --entry ./src/index.js -o ./build --output-filename built.js --mode=production
2.结论:
一、webpack 能处理 js 和 json 资源,不能处理 css/img 等其他资源
二、生产环境比开发环境多一个压缩 js 代码
三、生产环境和开发环境将 ES6 模块化编译成浏览器能识别的模块化
*/
import data from './data.json'
// import './index.css'
console.log(data)
function add(x, y) {
return x + y
}
console.log(add(1, 2))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 使用
npx webpack ./src/main.js --mode=development
就能进行打包了
2.2 打包样式资源 - css-loader
在入口(src 下的 index.js)中引入同级目录下的 index.css:
import './index.css'
会报错,这时候需要打包 css 样式资源!
- 在 src 目录同级创建 webpack.config.js,作用是:当运行 webpack 指令时,会加载里面的配置
- 最基本的配置 webpack.config.js:(直接使用
npx webpack
就能执行最基本的 webpack 打包)
const path = require('path')
/**
* @type {import('webpack').Configuration}
*/
const config = {
// 入口。entry 要用相对路径
entry: './src/main.js',
// 输出
output: {
// path 要使用绝对路径
path: path.resolve(__dirname, 'dist'),
// 入口文件打包输出的文件名,如果为 js/main.js 则 main.js 输出到 dist 目录下的 js 目录下的 main.js 里
filename: 'main.js'
},
// 加载器
module: {
rules: [
// loader 的配置
]
},
plugins: [
// plugin 的配置
],
mode: 'development'
}
module.exports = config
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
- 打包样式资源:
npm i css-loader style-loader -D
// resolve 用来拼接绝对路径的方法
const { resolve } = require('path')
module.exports = {
// webpack 配置
// 入口起点
entry: './src/index.js',
// 输出
output: {
// 输出文件名
filename: 'built.js',
// 输出路径
// __dirname nodejs 的变量,代表当前文件的目录绝对路径
path: resolve(__dirname, 'build')
},
// loader 的配置
module: {
rules: [
// 详细 loader 配置
{
// 匹配哪些文件
test: /\.css$/,
// 使用哪些 loader 进行处理
use: [
// use 数组中 loader 执行书序:从右到左,从下到上,依次执行
// 创建 style 标签,将 js 中的 css 样式资源插入进去,添加到 head 中生效
'style-loader',
// 将 css 文件变成 commonjs 模块加载到 js 中,里面内容是样式字符串
'css-loader'
]
}
]
},
// plugins 的配置
plugins: [],
// 模式
mode: 'development'
// mode: 'production'
}
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
- 在 build 目录下创建 index.html 进行测试样式是否生效:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webpack</title>
</head>
<body>
<script src="./built.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
- 打开网页,发现样式生效,打包成功!
当需要编译 less 文件时,就可以借鉴上面的经验:
- 创建 index.less 写入样式,并引入到 index.js 入口文件中
- 新建一个 loader:
module: {
rules: [
// 详细 loader 配置
// 不同文件必须配置不同 loader 处理
{
// 匹配哪些文件
test: /\.css$/,
// 使用哪些 loader 进行处理
use: [
// use 数组中 loader 执行书序:从右到左,从下到上,依次执行
// 创建 style 标签,将 js 中的 css 样式资源插入进去,添加到 head 中生效
'style-loader',
// 将 css 文件变成 commonjs 模块加载到 js 中,里面内容是样式字符串
'css-loader'
]
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
// 将 less 文件编译成 css 文件
'less-loader',
]
}
]
},
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
- 下载:
npm i less-loader less -D
(如果安装了 less,就可以在控制台输入:npx less less文件 目录/目标.css
来输出 css 文件) - 在 build 文件夹下的 index.html 中引入:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webpack</title>
</head>
<body>
<h1 id="title">hello less</h1>
<script src="./built.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
- less 文件也成功编译!
- sass 文件同理,只是
test: /\.s[ac]ss$/
就行
2.3 打包 html 资源到 build
public 目录下有 index.html,直接
webpack
发现打包不到 build 目录,引入下文。注意:index.html 不要引入任何文件!
- 下载插件:
npm i html-webpack-plugin -D
- 在 webpack.config.js 中配置:
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, 'src')
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, 'public/index.html')
})
]
2
3
4
5
6
7
8
9
10
11
12
13
14
- 使用
webpack
命令打包,会发现 build 文件夹中多出一个 html 文件,成功!
2.4 打包图片资源
- webpack5 废弃了 webpack4 中利用
url-loader
和file-loader
打包静态资源的方案,而采用 asset-module(资源模块)来打包图片资源 - 可以使用
module.generator
在一个地方配置所有生成器的选项
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(jpg|png|gif|svg|jpe?g)/i,
type: 'asset',
// 解析
parser: {
// 转 base64 的条件
dataUrlCondition: {
maxSize: 8 * 1024 // 25kb
}
},
generator: {
// 局部指定静态资源输出位置(可以类比一下 output.filename,因为 output.filename 是将输出的 js 目录进行修改)
// 与 output.assetModuleFilename 是相同的,这个写法引入时也会添加好这个路径
// [hash:10]:取图片的 hash 的前 10 位
// [ext] 取文件原来拓展名
// [query] 请求时所携带的参数
filename: 'img/[hash:10][ext][query]'
}
}
]
}
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
- 问题:处理不了 html 中 img 图片,解决方法:增加一个
html-loader
:npm i html-loader -D
{
test: /\.html$/,
// 处理 html 文件的 img 图片(负责引入 img,从而能被 asset 进行处理)
loader: 'html-loader'
}
2
3
4
5
2.5 output 的 clean 属性
webpack4 写法:
- 下载:
npm i clean-webpack-plugin -D
- webpack.config.js:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins: [new CleanWebpackPlugin()]
2
3
webpack5 写法:
output: {
// 自动清空上次打包的内容
// 原理:在打包前,将 path 整个目录内容清空,再进行打包
clean: true
}
2
3
4
5
2.6 打包其他资源(字体图标)
由于 asset-module 替代了 file-loader 与 url-loader,所以字体图标仍使用 asset-module 来进行打包。因为 iconfont 文件里面又引入了其他文件,webpack 将它们作为模块打包,又因为缺少合适的 loader,所以 webpack5 并不认识他们,就会报错
解决办法:
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
// 打包其他资源(除了 html、js、css 资源以外的资源)
{
test: /\.(ttf|woff2?)$/,
// 原封不动地打包
type: 'asset/resource',
generator: {
// 字体文件打包目录
filename: 'font/[name].[hash:3][ext]'
}
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2.7 处理其他资源
开发中还会有一些其他资源,例如音视频等
webpack.config.js:
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
}
2
3
4
5
6
7
2.8 js 语法检查 eslint
官方文档:eslint-webpack-plugin (docschina.org)open in new window
官方文档:eslint-config-airbnb-base - npm (npmjs.com)open in new window
.eslintrc.*
:新建文件,位于项目根目录.eslintrc
.eslintrc.js
eslintrc.json
- package.json 中的
eslintConfig
具体配置:
.eslintrc.js:
module.exports = {
// 解析选项
parserOptions: {},
// 具体检查规则
rules: {},
// 继承其他规则
extends: [],
...
}
2
3
4
5
6
7
8
9
- 下载:
npm install eslint eslint-webpack-plugin --save-dev
- 把插件添加到你的 webpack 配置:
const ESLintPlugin = require('eslint-webpack-plugin')
module.exports = {
// ...
plugins: [
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, 'src'),
// 自动修复代码
fix: true
})
]
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- .eslintrc.js:
module.exports = {
extends: ['eslint:recommended'],
env: {
node: true, // 启用 node 中全局变量
browser: true // 启用浏览器中全局变量
},
parserOptions: {
ecmaVersion: 6, // es6
sourceType: 'module' // es module
},
rules: {
'no-var': 2 // 不能使用 var 定义变量
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- webpack 后,成功运行!一个小知识点:可以在一行代码上面添加注释
// eslint-disable-next-line
,这样就能忽略该行代码的 eslint 检查 - 以下代码会报错:
var a = 10 // 使用 var 定义变量
- 由于 vscode 的 eslint 会对 dist 文件夹内容进行检查、报错。解决办法:创建一个
.eslintignore
文件:
dist
2.9 js 兼容性处理 babel
兼容全部 js:@babel/preset-env · Babel 中文文档 (docschina.org)open in new window
配置文件写法:
babel.config.*
:babal.config.js
、babel.config.json
.babelrc.*
:.babelrc
、.babelrc.js
、.babelrc.json
- 下载:
npm install -D babel-loader @babel/core @babel/preset-env
- 在 webpack.config.js 中配置
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
// 下面这个可以在外面单独配置(babel.config.js、.babelrc.json...)
options: {
// 智能预设:只能处理 es6 的语法
presets: ['@babel/preset-env']
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 结果:
// 原先
const add = (x, y) => {
return x + y
}
console.log(add(2, 5))
// 现在
eval(
'var add = function add(x, y) {\n return x + y;\n};\n\nconsole.log(add(2, 5));\n\n//# sourceURL=webpack:///./src/js/index.js?'
)
2
3
4
5
6
7
8
9
10
可以不直接使用 options 里的 presets,而是新建一个 babel.config.js 去配置预设
- webpack.config.js:
{
test: /\.js$/,
use: ['babel-loader']
// use: [{
// loader: 'babel-loader',
// options: {
// presets: ['@babel/preset-env', {
// targets: {chrome: 91}
// }]
// }
// }]
}
2
3
4
5
6
7
8
9
10
11
12
- babel.config.js:
module.exports = {
presets: ['@babel/preset-env']
}
2
3
2.10 devServer
方法一:
- 下载
webpack-dev-server
:npm i webpack-dev-server -D
,使用npx webpack server
就能运行该包 - 在与 entry、output、module、plugins、mode 同级的下面添加代码创建开发服务器:
devServer: {
static: {
directory: resolve(__dirname, 'build')
},
// 启动 gzip 压缩
compress: true,
// 服务器域名
host: 'localhost',
// 端口号,
port: 3000,
// 自动打开浏览器
open: true
}
2
3
4
5
6
7
8
9
10
11
12
13
- 输入网址:
localhost:3000
,即可访问打包后的页面(支持热刷新) - 配置 package.json 可以快速运行:(若没有
--config xxx
,则为默认的 webpack.config.js)
"serve": "webpack serve --config lg.webpack.js"
方法二:(不推荐)要搭配 live server 插件使用,不足:① 当一个文件改变,所有源代码都会重新编译,效率差;② 每次编译成功之后都要需要进行文件读写;③live server 为 vscode 生态的,非 webpack 生态的;④ 不能实现局部刷新
- package.json:其中
--config lg.webpack.js
只是为了改 webpack.config.js 的名字,--watch
才是实现热刷新
"build": "webpack --config lg.webpack.js --watch"
- 或者,在 webpack.config.js 中:
watch: true
2.11 总结前面
开发环境配置:能让代码运行即可
*webpack.dev.js:
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
/**
* @type {import('webpack').Configuration}
*/
const config = {
// 虽然文件位于 config 目录下,但是运行时仍然是在根目录下,所以不需要回到上一级
entry: './src/main.js',
// 输出
output: {
// 开发模式没有输出,因为是在内存编译的
path: undefined,
// 入口文件打包输出文件名
filename: 'static/js/main.js'
},
// 加载器
module: {
rules: [
// loader 的配置
{
test: /\.css$/i, // 只检测 .css 文件
use: [
// 将 js 中的 css 通过创建 style 标签添加到 html 中生效
'style-loader',
// 将 css 资源编译成 commentjs 的模块到 js 中
'css-loader'
]
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
},
generator: {
// 输出图片名称
filename: 'static/images/[hash:8][ext][query]'
}
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
},
{
test: /\.js$/,
exclude: /node_modules/, // 排除 node_modules 中的 js 文件
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
},
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src')
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, '../public/index.html')
})
],
mode: 'development',
devServer: {
host: 'localhost', // 启动服务器域名
port: 3000,
open: true
}
}
module.exports = config
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
注意:
webpack
会将打包结果输出出去npx webpack-dev-server
只会在内存中编译打包,没有输出- 图片资源与字体图标资源如果想打包进特定文件夹,只需要设置 generator 属性的 filename:
filename: 'imgs/[hash:10].[ext]'
3.生产环境
3.1 改造开发模式和生产模式
- 由于生产环境要负责运行 bundle,需要优化
- 样式如果在 js 中,所以生产环境中需要将 js 里的样式提取出来放到 css 中
- 代码需要压缩处理
- 兼容性问题需要处理
- 总之,开发环境要求速度更快且更加平稳
config/webpack.dev.js:
- 相对路径不需要更改;
- 绝对路径需要到上一级
运行:
npx webpack serve --config ./config/webpack.dev.js
为开发模式运行:
npx webpack --config ./config/webpack.dev.js
为生产模式
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
/**
* @type {import('webpack').Configuration}
*/
const config = {
// 虽然文件位于 config 目录下,但是运行时仍然是在根目录下,所以不需要回到上一级
entry: './src/main.js',
// 输出
output: {
// 开发模式没有输出,因为是在内存编译的
path: undefined,
// 入口文件打包输出文件名
filename: 'static/js/main.js'
},
// 加载器
module: {
rules: [
// loader 的配置
{
test: /\.css$/i, // 只检测 .css 文件
use: [
// 将 js 中的 css 通过创建 style 标签添加到 html 中生效
'style-loader',
// 将 css 资源编译成 commentjs 的模块到 js 中
'css-loader'
]
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
},
generator: {
// 输出图片名称
filename: 'static/images/[hash:8][ext][query]'
}
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
},
{
test: /\.js$/,
exclude: /node_modules/, // 排除 node_modules 中的 js 文件
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
},
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src')
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, '../public/index.html')
})
],
mode: 'development',
devServer: {
host: 'localhost', // 启动服务器域名
port: 3000,
open: true
}
}
module.exports = config
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
config/webpack.prod.js:
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
/**
* @type {import('webpack').Configuration}
*/
const config = {
// 入口。entry 要用相对路径
entry: './src/main.js',
// 输出
output: {
// path 要使用绝对路径
path: path.resolve(__dirname, '../dist'),
// 入口文件打包输出文件名
filename: 'static/js/main.js',
clean: true
},
// 加载器
module: {
rules: [
// loader 的配置
{
test: /\.css$/i, // 只检测 .css 文件
use: [
// 将 js 中的 css 通过创建 style 标签添加到 html 中生效
'style-loader',
// 将 css 资源编译成 commentjs 的模块到 js 中
'css-loader'
]
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
},
generator: {
// 输出图片名称
filename: 'static/images/[hash:8][ext][query]'
}
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
},
{
test: /\.js$/,
exclude: /node_modules/, // 排除 node_modules 中的 js 文件
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
},
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src')
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, '../public/index.html')
})
],
mode: 'production'
}
module.exports = config
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
如果嫌敲命令麻烦,可以去 package.json 中配置:
"scripts": {
"start": "yarn dev",
"dev": "webpack serve --config ./config/webpack.dev.js",
"build": "webpack --config ./config/webpack.prod.js"
}
2
3
4
5
3.2 提取 css 成单独文件
由于 js 中的 css 会导致闪屏、性能差等问题,故将 css 转为 js 后可以通过提取 js 中的 css 成单独文件
- 下载
mini-css-extract-plugin
:npm i mini-css-extract-plugin -D
- 引入插件,并如下示:
module: {
rules: [
{
test: /\.css$/,
use: [
// 创建 style 标签,将样式放入
// 'style-loader',
// 这个 loader 取代 style-loader。作用:提取 js 中的 css 成单独文件
MiniCssExtractPlugin.loader,
// 将 css 文件整合到 js 文件中
'css-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: 'static/css/main.css'
})
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 这样 build 里就有一个 css 文件夹,里面有 built.css
3.3 css 兼容性处理
postcss:JavaScript 转换样式的工具
- 安装 postcss-loader、postcss-preset-env:
npm i postcss postcss-loader postcss-preset-env -D
- webpack.config.js 设置:
module: {
rules: [
{
test: /\.css$/,
use: [
// 创建 style 标签,将样式放入
// 'style-loader',
// 这个 loader 取代 style-loader。作用:提取 js 中的 css 成单独文件
MiniCssExtractPlugin.loader,
// 将 css 文件整合到 js 文件中
'css-loader',
/**
* css 兼容性处理:postcss --> postcss-loader postcss-preset-env
* 帮 postcss 找到 package.json 中的 browserslist 里面的配置,通过配置加载指定的 css 兼容性样式
*/
{
loader: 'portcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env']
}
}
}
]
}
]
}
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
- package.json 设置:(node_modules 里面在刚创建 webpack 项目时就有 browserslist 文件夹,里面的 index.js 就是向 caniuse 网站发送请求查询浏览器份额,也可以手动输入
npx browserslist
进行查询)
"browserslist": {
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
"production": [
"> 1%",
"not dead",
"last 2 version"
]
}
2
3
4
5
6
7
8
9
10
11
12
- 除了在 package.json 可以配置,也可以在根目录下创建 .browserslistrc 文件进行配置,里面可以写:
1%
last 2 version
not dead
2
3
- 测试:webpack.config.js:
// 设置 nodejs 环境变量
process.env.NODE_ENV = 'production'
2
- 打包出来的 built.css 成功兼容许多浏览器!
3.4 压缩 css
官方文档:[CssMinimizerWebpackPlugin | webpack 中文文档 (docschina.org)open in new window]
- 下载:
npm install css-minimizer-webpack-plugin --save-dev
- 代码如下:
// 1.引入
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
// 2.与 module、plugins 等同级(也可以放在 plugins 里面)
optimization: {
minimizer: [
new CssMinimizerPlugin()
],
// 在开发环境下启用 CSS 优化
minimize: true
},
2
3
4
5
6
7
8
9
10
11
3.5 总结前面
``webpack.prod.js`:
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
/**
* @type {import('webpack').Configuration}
*/
const config = {
// 虽然文件位于 config 目录下,但是运行时仍然是在根目录下,所以不需要回到上一级
entry: './src/main.js',
// 输出
output: {
// 开发模式没有输出,因为是在内存编译的
path: undefined,
// 入口文件打包输出文件名
filename: 'static/js/main.js'
},
// 加载器
module: {
rules: [
// loader 的配置
{
test: /\.css$/i, // 只检测 .css 文件
use: [
// 将 js 中的 css 通过创建 style 标签添加到 html 中生效
'style-loader',
// 将 css 资源编译成 commentjs 的模块到 js 中
'css-loader'
]
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
},
generator: {
// 输出图片名称
filename: 'static/images/[hash:8][ext][query]'
}
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
},
{
test: /\.js$/,
exclude: /node_modules/, // 排除 node_modules 中的 js 文件
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
},
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src')
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, '../public/index.html')
})
],
mode: 'development',
devServer: {
host: 'localhost', // 启动服务器域名
port: 3000,
open: true
}
}
module.exports = config
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
4.webpack 高级
4.1 封装样式 loader 函数
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 用来获取处理样式的方法
function getStyleLoader(pre) {
return [
// 提取 css 成单独的文件
MiniCssExtractPlugin.loader,
// 将 css 资源编译成 commentjs 的模块到 js 中
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env' // 能解决大多数样式兼容问题
]
}
}
},
pre
// [].filter(Boolean) 等价于 [].filter(item => Boolean(item))
].filter(Boolean)
}
/**
* @type {import('webpack').Configuration}
*/
const config = {
// 入口。entry 要用相对路径
entry: './src/main.js',
// 输出
output: {
// path 要使用绝对路径
path: path.resolve(__dirname, '../dist'),
// 入口文件打包输出文件名
filename: 'static/js/main.js',
clean: true
},
// 加载器
module: {
rules: [
// loader 的配置
{
test: /\.css$/i, // 只检测 .css 文件
use: getStyleLoader()
},
{
test: /\.less$/,
use: getStyleLoader('less-loader')
},
{
test: /\.s[ac]ss$/,
use: getStyleLoader('sass-loader')
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
},
generator: {
// 输出图片名称
filename: 'static/images/[hash:8][ext][query]'
}
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
},
{
test: /\.js$/,
exclude: /node_modules/, // 排除 node_modules 中的 js 文件
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
},
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src')
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, '../public/index.html')
}),
new MiniCssExtractPlugin({
filename: 'static/css/main.css'
})
],
mode: 'production'
}
module.exports = config
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
4.2 source map
源代码映射是生成源代码与构建后代码——映射的文件的方案。它会生成一个 xx.map 的文件,里面包含源代码和构建后代码每一行、每一列的映射关系,会通过 xxx.map 文件 ,从构建后的代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码出错位置,帮助找到错误根源
在开发环境中我们使用:cheap-module-source-map / eval-cheap-module-source-map
在生产环境中我们使用:hidden-source-map。
参数 | 参数解释 |
---|---|
eval | 打包后的模块都使用 eval() 执行,行映射可能不准;不产生独立的 map 文件 |
cheap | map 映射只显示行不显示列,忽略源自 loader 的 source map |
inline | 映射文件以 base64 格式编码,加在 bundle 文件最后,不产生独立的 map 文件 |
module | 增加对 loader source map 和第三方模块的映射 |
source-map
这个是正版的 source-map 的生成方式,是最全面 main 的生成方式,生成 .map 文件, 会在 bundle 文件末尾追加 sourceURI= 指定 .map 文件路径,会在浏览器开发者工具中看到 webpack://的文件夹。
eval
eval 做的就是将源代码包在了 eval 函数中,并在结尾将 sourceURL 指向了源文件通过 webpack 编译后的文件。这种方式并没有生成 map 文件,而且很明显无法在生产环境使用。eval 是在 development 模式下默认的配置,速度非常快,看到的代码可能掺杂一些 webpack 相关的代码。
;[
/* 0 */
function (module, exports) {
eval("throw new Error('error 1');\r\n\n\n//# sourceURL=webpack:///./src/index.js?")
}
]
2
3
4
5
6
inline-source-map
inline 的含义就是不产生独立的 .map 文件,而把 source-map 的内容以 dataURI 的方式追加到 bundle 件末尾。这种方式会造成打包后文件过大,所以 inline 不适用于线上。
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndl...
cheap-source-map
cheap 的含义是忽略列信息,source map 被简化为每行一个映射。这通常意味着每个语句只有一个映射,这么做的好处就是会极大的减少计算量,提升打包速度,与压缩后的代码组合后,映射关系是不可能实现的,因为压缩工具通常只会输出一行。所以 cheap 方式也是不适合生产环境的。
"mappings": ";;AAAA,AACA,AADA,AADA,AAFA,AAAA,AAAA"
cheap-module-source-map
module 的作用是 map 到 loader 处理前的文件,如果不加 module, 实际上是 map 到源文件经过对应 loader 处理后的样子。简单来说,如果不加 module,sourcesContent 中就是 var,如果加入 module,会是 var 经过 loader 前的样子,可能是 let 或是 const。
webpack 的大部分配置都是 inline,cheap 和 module 这几种的排列组合,就不一一分析了。
hidden-source-map
以上的几种方式,都会在打包后文件的末尾增加 sourceURL,也就是说,浏览器端是可以拿到 source map 文件的,也就存在源码泄露的可能,hidden-source-map,就是不在 bundle 文件结尾处追加 sourceURL 指定其 sourcemap 文件的位置,但是仍然会生成 sourcemap 文件。这样,浏览器开发者工具就无法应用 sourcemap, 目的是避免把 sourcemap 文件发布到生产环境,造成源码泄露。生产环境应该用错误报告工具结合 sourcemap 文件来查找问题。
nosources-source-map
sourcemap 中不带有源码,这样,sourcemap 可以部署到生产环境而不会造成源码泄露,同时一旦出了问题,error stacks 中会显示准确的错误信息,比如发生在哪个源文件的哪一行。其实就是去掉了 map 文件中的 sourcesContent 字段。
4.3 提升打包构建速度 - HMR
开发时候如果修改了其中一个模块代码,webpack 会默认将所有模块全部重新打包编译,速度很慢。HotModuleReplacement(热模块替换)能在程序运行时替换、添加和删除模块,而无需加载整个页面
webpack.dev.js:
devServer: {
host: 'localhost', // 启动服务器域名
port: 3000,
open: true,
hot: true // 热模块替换,默认是开启的
}
2
3
4
5
6
style-loader 自带热模块替换,但是 js 默认不行。解决办法如下:
main.js:
if (module.hot) {
// 判断是否支持热模块替换功能
// 实际开发项目时,会有 vue-loader、react-hot-loader 等 loader 来帮助进行模块热替换
module.hot.accept('./js/count')
module.hot.accept('./js/sum')
}
2
3
4
5
6
4.4 OneOf
打包每个文件都会经过所有 loader 处理,虽然因为
test
正则原因实际没有处理上,但是都要过一遍,比较慢
webpack.dev.js:
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
// 用来获取处理样式的方法
function getStyleLoader(pre) {
return [
// 提取 css 成单独的文件
MiniCssExtractPlugin.loader,
// 将 css 资源编译成 commentjs 的模块到 js 中
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env' // 能解决大多数样式兼容问题
]
}
}
},
pre
// [].filter(Boolean) 等价于 [].filter(item => Boolean(item))
].filter(Boolean)
}
/**
* @type {import('webpack').Configuration}
*/
module.exports = {
// 入口。entry 要用相对路径
entry: './src/main.js',
// 输出
output: {
// path 要使用绝对路径
path: path.resolve(__dirname, '../dist'),
// 入口文件打包输出文件名
filename: 'static/js/main.js',
clean: true
},
// 加载器
module: {
rules: [
{
oneOf: [
// loader 的配置
{
test: /\.css$/i, // 只检测 .css 文件
use: getStyleLoader()
},
{
test: /\.less$/,
use: getStyleLoader('less-loader')
},
{
test: /\.s[ac]ss$/,
use: getStyleLoader('sass-loader')
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
},
generator: {
// 输出图片名称
filename: 'static/images/[hash:8][ext][query]'
}
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
},
{
test: /\.js$/,
exclude: /node_modules/, // 排除 node_modules 中的 js 文件
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
}
]
},
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src')
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, '../public/index.html')
}),
new MiniCssExtractPlugin({
filename: 'static/css/main.css'
}),
new CssMinimizerPlugin()
],
mode: 'production',
devtool: 'hidden-source-map'
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
4.5 include/Exclude
- Include:只处理 xxx 文件
- exclude:排除,除了 xxx 文件以外其他文件都处理
{
test: /\.js$/,
// exclude: /node_modules/, // 排除 node_modules 中的 js 文件
include: path.resolve(__dirname, '../src'),
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
2
3
4
5
6
7
8
9
4.6 cache
为什么?
每次打包 js 都要经过 eslint 检查和 babel 编译,速度比较慢。可以缓存之前的 eslint 检查和 babel 编译结果。这样第二次打包就会很快了
是什么?
对 eslint 检查和 babel 编译结果进行缓存
babel 开启:
{
test: /\.js$/,
// exclude: /node_modules/, // 排除 node_modules 中的 js 文件
include: path.resolve(__dirname, '../src'),
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启 babel 缓存
cacheCompression: false // 关闭缓存文件压缩
}
}
2
3
4
5
6
7
8
9
10
eslint 开启:
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src'),
exclude: 'node_modules',
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache') // 缓存位置
}),
2
3
4
5
6
7
8
4.7 多进程 thread
- 获取 cpu 核数:
const os = require('os')
// cpu 核数
const threads = os.cpus().length
2
3
- 下载包:
yarn add thread-loader -D
- 使用:
webpack.prod.js:
const os = require('os')
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
const threads = os.cpus().length
// 用来获取处理样式的方法
function getStyleLoader(pre) {
return [
// 提取 css 成单独的文件
MiniCssExtractPlugin.loader,
// 将 css 资源编译成 commentjs 的模块到 js 中
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env' // 能解决大多数样式兼容问题
]
}
}
},
pre
// [].filter(Boolean) 等价于 [].filter(item => Boolean(item))
].filter(Boolean)
}
/**
* @type {import('webpack').Configuration}
*/
module.exports = {
// 入口。entry 要用相对路径
entry: './src/main.js',
// 输出
output: {
// path 要使用绝对路径
path: path.resolve(__dirname, '../dist'),
// 入口文件打包输出文件名
filename: 'static/js/main.js',
clean: true
},
// 加载器
module: {
rules: [
{
oneOf: [
// loader 的配置
{
test: /\.css$/i, // 只检测 .css 文件
use: getStyleLoader()
},
{
test: /\.less$/,
use: getStyleLoader('less-loader')
},
{
test: /\.s[ac]ss$/,
use: getStyleLoader('sass-loader')
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
},
generator: {
// 输出图片名称
filename: 'static/images/[hash:8][ext][query]'
}
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
},
{
test: /\.js$/,
// exclude: /node_modules/, // 排除 node_modules 中的 js 文件
include: path.resolve(__dirname, '../src'),
use: [
{
loader: 'thread-loader', // 开启多进程
options: {
workers: threads // 进程数量
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启 babel 缓存
cacheCompression: false // 关闭缓存文件压缩
}
}
]
}
]
}
]
},
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src'),
exclude: 'node_modules',
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache'),
threads // 开启多进程和设置进程数量
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, '../public/index.html')
}),
new MiniCssExtractPlugin({
filename: 'static/css/main.css'
})
// new CssMinimizerPlugin(),
// new TerserWebpackPlugin({
// parallel: threads // 开启多进程和设置进程数量
// })
],
mode: 'production',
devtool: 'hidden-source-map',
optimization: {
// 压缩的操作
minimizer: [
// 压缩 css
new CssMinimizerPlugin(),
// 压缩 js
new TerserWebpackPlugin({
parallel: threads // 开启多进程和设置进程数量
})
]
}
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
webpack.dev.js:
const os = require('os')
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const threads = os.cpus().length
/**
* @type {import('webpack').Configuration}
*/
module.exports = {
// 虽然文件位于 config 目录下,但是运行时仍然是在根目录下,所以不需要回到上一级
entry: './src/main.js',
// 输出
output: {
// 开发模式没有输出,因为是在内存编译的
path: undefined,
// 入口文件打包输出文件名
filename: 'static/js/main.js'
},
// 加载器
module: {
rules: [
{
// 每个文件只能被其中一个 loader 配置处理
oneOf: [
// loader 的配置
{
test: /\.css$/i, // 只检测 .css 文件
use: [
// 将 js 中的 css 通过创建 style 标签添加到 html 中生效
'style-loader',
// 将 css 资源编译成 commentjs 的模块到 js 中
'css-loader'
]
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
},
generator: {
// 输出图片名称
filename: 'static/images/[hash:8][ext][query]'
}
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
},
{
test: /\.js$/,
// exclude: /node_modules/, // 排除 node_modules 中的 js 文件
include: path.resolve(__dirname, '../src'), // 只处理 src 下的文件,其他文件不处理
use: [
{
loader: 'thread-loader',
options: {
workers: threads
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启 babel 缓存
cacheCompression: false // 关闭缓存文件压缩
}
}
]
}
]
}
]
},
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src'),
exclude: 'node_modules', // 默认值
cache: true, // 开启缓存
threads
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, '../public/index.html')
})
],
mode: 'development',
devServer: {
host: 'localhost', // 启动服务器域名
port: 3000,
open: true,
hot: true // 热模块替换,默认是开启的
},
devtool: 'cheap-module-source-map'
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
4.8 tree-shaking
开发时会引入一些工具函数库等,如果打包时就会引入整个库 ,但实际上我们可能只使用其中的一小部分的功能。tree shaking 就是用于移除 js 中没有使用的代码。注意:它依赖 es module
默认是开启的。举例子:
math.js:
export function add(x, y) {
return x + y
}
export function mul(x, y) {
return x * y
}
2
3
4
5
6
main.js:
import { add } from './js/math'
console.log(add(1, 2))
2
3
这时候打包时,mul 函数将不会被打包。
4.9 减少 babel 生成文件的体积
@babel/plugin-transform-runtime
:禁用了 Babel 自动对每个文件的 runtime 注入,而是引入@babel/plugin-transform-runtime 并且使所有辅助函数从这里引入
- 安装:
yarn add @babel/plugin-transform-runtime -D
- 代码:
{
test: /\.js$/,
// exclude: /node_modules/, // 排除 node_modules 中的 js 文件
include: path.resolve(__dirname, '../src'), // 只处理 src 下的文件,其他文件不处理
use: [
{
loader: 'thread-loader',
options: {
workers: threads
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启 babel 缓存
cacheCompression: false, // 关闭缓存文件压缩
plugins: ['@babel/plugin-transform-runtime'] // 减少代码体积
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
4.10 image Minimizer
对图片进行压缩。如果项目中都是在线图片,就不需要了 。本地项目静态图片才需要进行压缩
- 安装 :
yarn add image-minimizer-webpack-plugin imagemin -D
- 无损压缩的话安装:
yarn add imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
- 有损压缩的话安装:
yarn add imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -d
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
['gifsicle', { interlaced: true }],
['jpegtran', { progressive: true }],
['optipng', { optimizationLevel: 5 }],
[
'svgo',
{
plugins: [
'preset-default',
'prefixIds',
{
name: 'sortAttrs',
params: {
xmlnsOrder: 'alphabetica'
}
}
]
}
]
]
}
}
})
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
由于用的是 mac,无法下载对应程序,就跳过这章了。有需要再来看看
4.11 代码分割 code split
打包代码会把所有 js 文件打包到一个文件中,体积太大。如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。所以要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就加载哪个 js 文件
功能:
- 分割文件:将打包生成的文件进行分割,生成多个 js 文件
- 按需加载:需要哪个文件就加载哪个文件
webpack.config.js:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// entry: './src/main.js', // 只有一个入口文件,单入口
entry: {
// 有多个入口文件,多入口
app: './src/app.js',
main: './src/main.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js' // webpack 命名方式,[name] 以文件名自己命名
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html')
})
],
mode: 'production'
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
4.12 多入口文件提取公共模块
当 main.js 和 app.js 共同使用了一个函数模块 add(),打包后该模块分别编译到不同入口文件中。这样是不好的。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// entry: './src/main.js', // 只有一个入口文件,单入口
entry: {
// 有多个入口文件,多入口
app: './src/app.js',
main: './src/main.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js' // webpack 命名方式,[name] 以文件名自己命名
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html')
})
],
mode: 'production',
optimization: {
splitChunks: {
// 代码分割配置
chunks: 'all', // 对所有模块都进行分割
// // 以下是默认值
// minSize: 20000, // 分割代码最小的大小
// minRemainingSize: 0, // 类似于 minSize,最后确保提取的文件大小不能为 0
// minChunks: 1, // 至少被引用的次数 ,满足条件才会代码分割
// maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
// enforceSizeThreshold: 50000, // 超过 50kb 一定会单独打包(此时会忽略 minRemainingSize、maxAsyncRequests、maxInitialRequests)
// cacheGroups: {
// // 组,哪些模块要打包到一个组
// defaultVendors: {
// // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包在一起的模块
// priority: 10, // 权重(越大越高)
// reuseExistingChunk: true // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则他将被重用,而不是生成新的模块
// },
// default: {
// // 其他没有写的配置会使用上面的默认值
// minChunks: 2, // 这里 的 minChunks 权重更大
// priority: -20,
// reuseExistingChunk: true
// }
// },
// 修改配置
cacheGroups: {
// // 组,哪些模块需要打包到一个组
// defaultVendors: {
// // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
// priority: 10, // 权重(越大越高)
// reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分的模块,则它将被重用,而不是生成新的模块
// }
default: {
// 其他没有写的配置会使用上面的默认值
minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
minChunks: 2,
priority: 20,
reuseExistingChunk: true
}
}
}
}
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
单文件应用直接用:
optimization: { splitChunks: { chunks: 'all' } }
1
2
3
4
5就行
4.13 按需加载、动态导入
当网站上有一个按钮,点击后计算出别的模块传来的 count 函数的结果。如果不进行按需加载,则该模块会被提前加载,影响性能。这时可以使用动态加载。
document.getElementById('btn').onclick = function () {
// import 动态导入,会将动态导入的文件代码分割(拆分为单独模块),在需要使用的时候自动加载
import('./count')
.then(res => {
console.log('模块加载成功', res.default(2, 1))
})
.catch(err => {
console.log('模块加载失败', err)
})
console.log(count(1, 2))
}
2
3
4
5
6
7
8
9
10
11
4.14 给模块命名
- 魔法命名
main.js:
document.getElementById('btn').onclick = function () {
// eslint 不识别动态导入语法,需要额外增加配置
// /* webpackChunkName: "math" */ webpack 魔法命名
import(/* webpackChunkName: "math" */ './js/math').then(({ mul }) => {
console.log(mul(3, 3))
})
}
2
3
4
5
6
7
webpack.prod.js:
output: {
// path 要使用绝对路径
path: path.resolve(__dirname, '../dist'),
// 入口文件打包输出文件名
filename: 'static/js/main.js',
// 打包输出的其他文件命名
chunkFilename: 'static/js/[name].js',
clean: true
},
2
3
4
5
6
7
8
9
4.15 统一命名
已有的命名:
- output 中的 filename:入口文件输出的文件名
- output 中的 chunkFilename:chunk 块的名字
- 图片中 generator 里的 filename:图片的文件名和字体图标的文件名
- plugins 里 MiniCssExtractPlugin 中的 filename:打包样式的文件名
webpack.prod.js:
const os = require('os')
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin')
const threads = os.cpus().length
// 用来获取处理样式的方法
function getStyleLoader(pre) {
return [
// 提取 css 成单独的文件
MiniCssExtractPlugin.loader,
// 将 css 资源编译成 commentjs 的模块到 js 中
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env' // 能解决大多数样式兼容问题
]
}
}
},
pre
// [].filter(Boolean) 等价于 [].filter(item => Boolean(item))
].filter(Boolean)
}
/**
* @type {import('webpack').Configuration}
*/
module.exports = {
// 入口。entry 要用相对路径
entry: './src/main.js',
// 输出
output: {
// path 要使用绝对路径
path: path.resolve(__dirname, '../dist'),
// 入口文件打包输出文件名
filename: 'static/js/[name].js',
// 打包输出的其他文件命名
chunkFilename: 'static/js/[name].chunk.js',
// 对图片、字体等通过 type: asset 资源进行统一命名
assetModuleFilename: 'static/media/[hash:10][ext][query]',
clean: true
},
// 加载器
module: {
rules: [
{
oneOf: [
// loader 的配置
{
test: /\.css$/i, // 只检测 .css 文件
use: getStyleLoader()
},
{
test: /\.less$/,
use: getStyleLoader('less-loader')
},
{
test: /\.s[ac]ss$/,
use: getStyleLoader('sass-loader')
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
}
// generator: {
// // 输出图片名称
// filename: 'static/images/[hash:10][ext][query]'
// }
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource'
// generator: {
// filename: 'static/media/[hash:10][ext][query]'
// }
},
{
test: /\.js$/,
// exclude: /node_modules/, // 排除 node_modules 中的 js 文件
include: path.resolve(__dirname, '../src'),
use: [
{
loader: 'thread-loader', // 开启多进程
options: {
workers: threads // 进程数量
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启 babel 缓存
cacheCompression: false, // 关闭缓存文件压缩
plugins: ['@babel/plugin-transform-runtime'] // 减少代码体积
}
}
]
}
]
}
]
},
plugins: [
// plugin 的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src'),
exclude: 'node_modules',
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache'),
threads // 开启多进程和设置进程数量
}),
new HtmlWebpackPlugin({
// 模板,以 public/index.html 文件为模板创建新的 html 文件
// 特点:1.结构与原来一致;2.自动引入打包输出的资源
template: path.resolve(__dirname, '../public/index.html')
}),
new MiniCssExtractPlugin({
filename: 'static/css/[name].css',
chunkFilename: 'static/css/[name].chunk.css'
})
// new CssMinimizerPlugin(),
// new TerserWebpackPlugin({
// parallel: threads // 开启多进程和设置进程数量
// })
],
mode: 'production',
devtool: 'hidden-source-map',
optimization: {
// 压缩的操作
minimizer: [
// 压缩 css
new CssMinimizerPlugin(),
// 压缩 js
new TerserWebpackPlugin({
parallel: threads // 开启多进程和设置进程数量
})
],
splitChunks: {
chunks: 'all'
// 其他都用默认值
}
}
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
4.16 preload / prefetch
如果想在浏览器空闲时间,加载后续需要使用的资源 ,就需要使用 preload 和 prefetch 技术
是什么?
Preload
:告诉浏览器立即加载资源Prefetch
:告诉浏览器在空闲时才开始加载资源
区别:
Preload
加载优先级高,Prefetch
加载优先级低Preload
只能加载当前页面需要使用的资源,Prefetch
可以加载当前页面资源,也可以加载下一个页面需要使用的资源
总结:
- 当前页面优先级高的资源用
Preload
加载 - 下一个页面需要使用的资源用
Prefetch
加载
- 安装:
yarn add @vue/preload-webpack-plugin -D
- 写入:
new PreloadWebpackPlugin({
rel: 'preload',
as: 'script'
// rel: 'prefetch'
})
2
3
4
5
- preload build 之后的内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>webpack</title>
<script defer="defer" src="static/js/main.js"></script>
<link href="static/js/math.chunk.js" rel="preload" as="script" />
<link href="static/css/main.css" rel="stylesheet" />
</head>
<body>
<h1>hello webpack</h1>
<div class="box1"></div>
<div class="box2"></div>
<button id="btn">计算</button>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
4.17 网络缓存 contenthash
当一个入口文件引用一个 chunk(通过动态引入等方式)文件时,每次 chunk 内容发生改变时,入口文件内容也要重新加载一遍
// 输出
output: {
// path 要使用绝对路径
path: path.resolve(__dirname, '../dist'),
// 入口文件打包输出文件名
filename: 'static/js/[name].[contenthash:10].js',
// 打包输出的其他文件命名
chunkFilename: 'static/js/[name].chunk.[contenthash:10].js',
// 对图片、字体等通过 type: asset 资源进行统一命名
assetModuleFilename: 'static/media/[hash:10][ext][query]',
clean: true
}
optimization: {
// 压缩的操作
minimizer: [
// 压缩 css
new CssMinimizerPlugin(),
// 压缩 js
new TerserWebpackPlugin({
parallel: threads // 开启多进程和设置进程数量
})
],
splitChunks: {
chunks: 'all'
// 其他都用默认值
},
// 当两个或以上的文件引入同一个模块时,当模块发生改变,两文件都会重载,这是应该设置一个 runtimeChunk
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}.js`
}
}
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
4.18 js 兼容性问题 Core.js
core-js
专门用来做 ES6 以及以上 API 的 polyfill
完整引用:
虽然打包后的代码仍有
new Promise()
字样,但是已经是兼容了 ie 浏览器的写法了。完整引入引入了其他兼容的解决方案,所以体积太大,不适合项目开发
- 安装:
yarn add core-js
- main.js 引入:
import 'core-js'
按需引入:
main.js 引入:import 'core-js/es/promise'
按需引入且自动引入:
.browserlistrc 不要设置 ,否则可能会无效
babel.config.js:
module.exports = {
// 智能预设,能够编译 ES6 的语法
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage', // 按需加载自动引入
corejs: 3
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
4.19 PWA
网络离线时,web 应用就无法访问了。pwa(progressive web application)是一种可以提供类似于 native app(原生应用程序)体验的 web app 技术。离线(offline)时应用程序能够继续运行功能。通过 service workers 技术实现的
- 安装:
yarn add workbox-webpack-plugin -D
- main.js 写入:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(registration => {
console.log('SW registered: ', registration)
})
.catch(registrationError => {
console.log('SW registration failed: ', registrationError)
})
})
}`
2
3
4
5
6
7
8
9
10
11
12
- 运行:
serve dist
,运行 build 后的文件,就能实现 pwa 的效果了
5.其他内容
5.1 output 中的 publicPath 和 devServer 中的 publicPath
强烈建议将 output 里的 publicPath 与 devServer 里的 publicPath 设置为一样的
5.3 proxy 代理设置
index.js:
import axios from 'axios'
axios.get('/api/users').then(res => {
console.log(res.data)
})
2
3
4
5
webpack.config.js:(devServer 里面的)
proxy: {
// /api/users
// http://localhost:4000/api/users => https://api.github.com/api/users
'/api': {
target: 'https://api.github.com',
// 如果目标地址为 https://api.github.com/info/users,就需要写成 {'^api': 'info'}
pathRewrite: {'^/api': ''},
// github 对 4000 端口的请求有判断,所以要欺骗 github
changeOrigin: true
}
}
2
3
4
5
6
7
8
9
10
11
5.4 resolve 解析规则
index.js:引入模块时有时候省略后缀名会报错
import axios from 'axios' // 从 node_modules 里面找
import Home from './componenets/Home.jsx'
import About from './components/About' // 省略了后缀名会报错!
2
3
webpack.config.js:解决办法:
module.exports = {
//...
resolve: {
extensions: ['.js', '.json', '.wasm', '.jsx', '.vue']
}
}
2
3
4
5
6
webpack.config.js:对路径起别名:
resolve: {
extensions: ['.js', '.vue', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src')
}
}
2
3
4
5
6
这样的话,效果如下:
index.js:
// import Home from './components/Home'
import Home from '@/coomponents/Home'
2
5.6 ts-loader 编译 TS
- 安装 typescript:
npm i -g typescript
- 初始化 typescript:
tsc --init
,这时候控制台输入:tsc ./src/index.ts
就能编译出 index.js 在 index.ts 的同级目录下了 - 安装 ts-loader:
npm i -D ts-loader
- webpack.config.js 配置:但是,这时候编译不了新的 es6 语法,需要配合 babel-loader 进行编译
{
test: /\.ts$/,
use: ['babel-loader']
}
2
3
4
- babel.config.js :
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3
}
],
['@babel/preset-typescript']
]
}
2
3
4
5
6
7
8
9
10
11
12
- 安装 babel-loader:
npm i -D babel-loader @babel/preset-typescript core-js
npm run build
:编译成功!但是仍然有问题:在编译阶段不能暴露出语法问题,只有在运行阶段才会报错,这时候还是得看 ts-loader
最完美的解决方法:
package.json:只利用 ts-loader 的语法校验功能:
"build": "npm run ck && webpack",
"ck": "tsc --noEmit"
2
webpack.config.js:
{
test: /\.ts$/,
use: ['babel-loader']
}
2
3
4
babel.config.js:
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3
}
],
['@babel/preset-typescript']
]
}
2
3
4
5
6
7
8
9
10
11
12
6.ReactCli 项目
6.1 使用 history 导致的刷新后路由丢失问题
devServer.hostoryApiFallback:设置为 true 就行了
6.2 复制 public 目录下的 favicon.ico 到 dist 目录
- 安装:
yarn add -D copy-webpack-plugin
- webpack.prod.js:
// <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ['**/index.html']
}
}
]
})
2
3
4
5
6
7
8
9
10
11
12
6.3 合并之后的配置
const path = require('path')
const EslintWebpackPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
// 获取 cross-env 定义的环境变量
const isProduction = process.env.NODE_ENV === 'production'
// 返回处理样式 loader 函数
const getStyleLoaders = pre => {
return [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
{
// 处理 css 兼容性问题
// 配合 package.json 中的 browserlist 来指定兼容性
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env']
}
}
},
pre && {
loader: pre,
options:
pre === 'less-loader'
? {
lessOptions: {
// 如果使用less-loader@5,请移除 lessOptions 这一级直接配置选项。
modifyVars: {
'primary-color': '#eee',
'link-color': '#1DA57A',
'border-radius-base': '2px'
},
javascriptEnabled: true
}
}
: {}
}
].filter(Boolean)
}
module.exports = {
entry: './src/main.js',
output: {
path: isProduction ? path.resolve(__dirname, '../dist') : undefined,
filename: isProduction ? 'static/js/[name].[contenthash:10].js' : 'static/js/[name].js',
// static/js/[name].[contenthash:10].chunk.js
chunkFilename: isProduction
? 'static/js/[name].[contenthash:10].chunk.js'
: 'static/js/[name].chunk.js',
assetModuleFilename: 'static/media/[hash:10][ext][query]',
clean: true
},
module: {
rules: [
// 处理 css
{
test: /\.css$/,
use: getStyleLoaders()
},
// 处理 less
{
test: /\.less$/,
use: getStyleLoaders('less-loader')
},
// 处理 sass
{
test: /\.s[ac]ss$/,
use: getStyleLoaders('sass-loader')
},
// 处理 stylus
{
test: /\.styl$/,
use: getStyleLoaders('stylus-loader')
},
// 处理图片
{
test: /\.(jpe?g|png|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
}
},
// 处理其他资源
{
test: /\.(woff2?|ttf)$/,
type: 'asset/resource'
},
// 处理 js
{
test: /\.jsx?$/,
include: path.resolve(__dirname, '../src'),
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false,
plugins: [!isProduction && 'react-refresh/babel'].filter(Boolean)
}
}
]
},
plugins: [
new EslintWebpackPlugin({
context: path.resolve(__dirname, '../src'),
exclude: 'node_modules',
cache: true,
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/.eslintcache')
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html')
}),
isProduction &&
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:10].css',
chunkFilename: 'static/css/[name].[contenthash:10].chunk.css'
}),
isProduction &&
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ['**/index.html']
}
}
]
}),
!isProduction && new ReactRefreshWebpackPlugin()
].filter(Boolean),
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'hidden-source-map' : 'cheap-module-source-map',
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// react react-dom react-router-dom 一起打包
// antd 单独打包
// 剩下 node_modules 单独打包
react: {
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
name: 'chunk-react',
priority: 40
},
antd: {
test: /[\\/]node_modules[\\/]antd(.*)?[\\/]/,
name: 'chunk-antd',
priority: 30
},
libs: {
test: /[\\/]node_modules[\\/]/,
name: 'chunk-libs',
priority: 20
}
}
},
// 代码分割可能会导致缓存失效
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}.js`
},
// 是否需要进行压缩
minimize: isProduction,
minimizer: [new CssMinimizerWebpackPlugin(), new TerserWebpackPlugin()]
},
// webapck 解析模块加载选项
resolve: {
// 自动补全后缀名
extensions: ['.jsx', '.js', '.json']
},
devServer: {
host: 'localhost',
port: 3000,
open: true,
hot: true,
historyApiFallback: true // 解决前端路由 history 模式的 404 问题
},
performance: false // 关闭性能分析,进一步加快打包速度
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
7.Vue 项目
7.1 完整配置
webpack.config.js:
const path = require('path')
const EslintWebpackPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const { DefinePlugin } = require('webpack')
const isProduction = process.env.NODE_ENV === 'production'
// 返回处理样式 loader 函数
const getStyleLoaders = pre => {
return [
isProduction ? MiniCssExtractPlugin.loader : 'vue-style-loader',
'css-loader',
{
// 处理 css 兼容性问题
// 配合 package.json 中的 browserlist 来指定兼容性
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env']
}
}
},
pre
].filter(Boolean)
}
module.exports = {
entry: './src/main.js',
output: {
path: isProduction ? path.resolve(__dirname, '../dist') : undefined,
filename: isProduction ? 'static/js/[name].[contenthash:10].js' : 'static/js/[name].js',
chunkFilename: isProduction
? 'static/js/[name].[contenthash:10].chunk.js'
: 'static/js/[name].chunk.js',
assetModuleFilename: 'static/media/[hash:10][ext][query]',
clean: true
},
module: {
rules: [
// 处理 css
{
test: /\.css$/,
use: getStyleLoaders()
},
// 处理 less
{
test: /\.less$/,
use: getStyleLoaders('less-loader')
},
// 处理 sass
{
test: /\.s[ac]ss$/,
use: getStyleLoaders('sass-loader')
},
// 处理 stylus
{
test: /\.styl$/,
use: getStyleLoaders('stylus-loader')
},
// 处理图片
{
test: /\.(jpe?g|png|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
}
},
// 处理其他资源
{
test: /\.(woff2?|ttf)$/,
type: 'asset/resource'
},
// 处理 js
{
test: /\.js?$/,
include: path.resolve(__dirname, '../src'),
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new EslintWebpackPlugin({
context: path.resolve(__dirname, '../src'),
exclude: 'node_modules',
cache: true,
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/.eslintcache')
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html')
}),
isProduction &&
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:10].css',
chunkFilename: 'static/css/[name].[contenthash:10].chunk.css'
}),
isProduction &&
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ['**/index.html']
}
}
]
}),
new VueLoaderPlugin(),
// cross-env 定义的环境变量给 webpack 打包工具使用
// DefinePlugin 定义环境变量给源代码使用,解决 vue3 页面警告的问题
new DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
})
].filter(Boolean),
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'hidden-source-map' : 'cheap-module-source-map',
optimization: {
splitChunks: {
chunks: 'all'
},
// 代码分割可能会导致缓存失效
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}.js`
},
minimize: isProduction,
minimizer: [new CssMinimizerWebpackPlugin(), new TerserWebpackPlugin()]
},
// webapck 解析模块加载选项
resolve: {
// 自动补全后缀名
extensions: ['.vue', '.js', '.json']
},
devServer: {
host: 'localhost',
port: 3000,
open: true,
hot: true,
historyApiFallback: true // 解决前端路由 history 模式的 404 问题
}
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
8.Loader
8.1 loader 介绍
帮助 webpack 将不同类型的文件转换成 webpack 可识别的模块
- 分类(可以在
modules
中的rules
规则中指定enforce: 'pre'
这样来指定 loader 模式)pre
:配置loader
normal
:普通loader
inline
:内联loader
post
:后置loader
- 执行顺序
- 四类 loader 执行优先级:
pre > normal > inline > post
- 相同优先级的 loader 执行顺序为:从右到左,从下到上
- 四类 loader 执行优先级:
- 使用 loader 的方式
- 配置方式:在 webpack.config.js 中指定 loader(pre、normal、post loader)
- 内联方式:在每个
import
语句中显式指定 loader(inline loader)
- inline loader
- 用法:
import Styles from 'style-loader!css-loader?modules!./styles.css'
- 含义:使用
css-loader
和style-loader
处理styles.css
文件 - 通过
!
将资源中的 loader 分开 inline-loader
可以加不同的前缀,跳过其他类型的 loader!
跳过 normal loader:import Styles from '!style-loader!css-loader?modules!./styles.css'
-!
跳过 pre 和 normal loader:import Styles from '-!style-loader!css-loader?modules!./styles.css
!!
跳过 pre、normal 和 post loader:import Styles from '!!style-loader!css-loader?modules!./styles.css
- 用法:
8.2 开发一个 loader
loader/test-loader.js:
/*
loader就是一个函数
当 webpack 解析资源时,会调用相应的 loader 去处理
loader 接收到文件内容作为参数,返回内容出去
content 文件内容
map sourceMap
mata 别的 loader 传递的数据
*/
module.exports = function (content, map, meta) {
console.log(content) // 输出 3 次 hello main
return content
}
2
3
4
5
6
7
8
9
10
11
12
webpack.config.js:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'js/[name].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
loader: './loaders/test-loader.js'
},
{
test: /\.js$/,
loader: './loaders/test-loader.js'
},
{
test: /\.js$/,
loader: './loaders/test-loader.js'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html')
})
],
mode: 'development'
}
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
8.3 同步 loader
// module.exports = function (content) {
// return content
// }
module.exports = function (content, map, meta) {
console.log('test1')
/*
第一个参数,err 代表是否有错误
第二个参数,content 处理后的内容
第三个参数,source-map 继续传递 source-map
第四个参数,meta 给下一个 loader 传递参数
*/
this.callback(null, content, map, meta)
// 同步 loader 中不能进行异步操作
// setTimeout(() => {
// console.log('test1')
// this.callback(null, content, map, meta)
// }, 1000)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
8.4 异步 loader
module.exports = function (content, map, meta) {
const callback = this.async()
setTimeout(() => {
console.log('test2', content)
callback(null, content, map, meta)
}, 1000)
}
2
3
4
5
6
7
8.5 raw loader 实现返回 buffer 数据
// // raw loader 接收到 content 是 Buffer 数据
// module.exports = function (content) {
// console.log(content)
// return content
// }
// module.exports.raw = true
function Test3Loader(content) {
return content
}
Test3Loader.raw = true
module.exports = Test3Loader
2
3
4
5
6
7
8
9
10
11
12
8.6 pitch loader 优先执行
module.exports = function (content) {
console.log('normal loader 1')
return content
}
// 加了 pitch 会优先执行
module.exports.pitch = function () {
console.log('pitch loader 1')
}
2
3
4
5
6
7
8
8.7 loader API
方法名 | 含义 | 用法 |
---|---|---|
this.async | 异步回调 loader,返回 this.callback | const callback = this.async() |
this.callback | 可以同步或者异步调用的并返回多个结果的函数 | this.callback(err, content, sourceMap?, meta?) |
this.emitFile | 产生一个文件 | this.emitFile(name, content, sourceMap) |
this.utils.contextify | 返回一个相对路径 | this.utils.contextify(context, request) |
this.utils.absolutify | 返回一个绝对路径 | this.urils.absolutify(context, request) |
this.getOptions(schema) | 获取 loader 的 options | this.getOptions(schema) |
8.8 自定义 clean-log-loader
module.exports = function (content) {
// 清除文件内容中的 console.log(xxx)
return content.replace(/console\.log\(.*?\);?/g, '')
}
2
3
4
8.9 自定义 banner-loader
webpack.config.js:
{
test: /\.js$/,
loader: './loaders/banner-loader/banner-loader.js',
options: {
author: '张三'
}
}
2
3
4
5
6
7
banner-loader.js:
const schema = require('./schema.json')
module.exports = function (content) {
// schema 对 options 的验证规则
// schema 符合 JSON schema 的规则
const options = this.getOptions(schema)
const prefix = `
/*
* Author: ${options.author}
*/
`
return prefix + content
}
2
3
4
5
6
7
8
9
10
11
12
schema.json:
{
"type": "object",
"properties": {
"author": {
"type": "string"
},
"additionalProperties": false
}
}
2
3
4
5
6
7
8
9
8.10 babel-loader
- 安装:
yarn add -D @babel/core @babel/preset-env
- 代码如下:
main.js:
const sum = (...args) => args.reduce((p, c) => p + c, 0)
webpack.config.js:
{
test: /\.js$/,
loader: './loaders/babel-loader/index.js',
options: {
presets: ['@babel/preset-env']
}
}
2
3
4
5
6
7
babel-loader/index.js:
const babel = require('@babel/core')
const schema = require('./schema.json')
module.exports = function (content) {
const callback = this.async()
const options = this.getOptions(schema)
// 使用 babel 对代码进行编译
babel.transform(content, options, function (err, result) {
if (err) callback(err)
callback(null, result.code)
})
}
2
3
4
5
6
7
8
9
10
11
12
schema.json:
{
"type": "object",
"properties": {
"presets": {
"type": "array"
}
},
"additionalProperties": true
}
2
3
4
5
6
7
8
9
8.11 原理 - 自定义 file-loader
- 安装:
yarn add -D loader-utils
- 代码:
file-loader/index.js:
const loaderUtils = require('loader-utils')
module.exports = function (content) {
// 1.根据文件内容生成带 hash 值的文件名
let interpolatedName = loaderUtils.interpolateName(this, '[hash].[ext][query]', {
content
})
interpolatedName = `images/${interpolatedName}` // 输出到 images 文件夹中
// 2.将文件输出出去
this.emitFile(interpolatedName, content)
// 3.返回 module.exports = '文件路径(文件名)'
return `module.exports = "${interpolatedName}"`
}
// 处理图片、字体等文件,它们都是 buffer 数据
// 需要使用 raw-loader 才能处理
module.exports.raw = true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
webpack.config.js:
{
test: /\.(png|jpe?g|gif)$/,
loader: './loaders/file-loader',
type: 'javascript/auto' // 阻止 webpack 默认处理图片资源,只使用 file-loader 处理
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
2
3
4
5
6
7
8
9
8.12 原理 - 自定义 style-loader
module.exports = function (content) {
/**
* 1.直接使用 style-loader,只能处理样式,不能处理样式中引入其他资源
* use: ['./loaders/style-loader']
* 2.需要借助 css-loader 解决样式中引入其他资源的问题
* use: ['./loaders/style-loader', 'css-loader']
* 问题是 css-loader 暴露了一段 js 代码,style-loader 需要执行 js 代码,得到返回值,再动态创建 style 标签,插入到页面上,不好操作
* 3.style-loader 使用 pitch-loader 用法操作
*/
}
module.exports.pitch = function (remainingRequest) {
// remainingRequest 剩下还需要处理的 loader
// console.log(remainingRequest) // G:\Microsoft desktop\webpack\source\node_modules\css-loader\dist\cjs.js!G:\Microsoft desktop\webpack\source\src\css\index.css
// 1.将 remainingRequest 中绝对路径改成相对路径(后面只能使用相对路径进行操作)
const relatePath = remainingRequest
.split('!')
.map(absolutePath => {
// 返回相对路径,this.context 即本文件的路径
return this.utils.contextify(this.context, absolutePath)
})
.join('!')
// console.log(relatePath) // ../../node_modules/css-loader/dist/cjs.js!./index.css
// 2.引入 css-loader 处理后的资源
// 3.创建 style,将内容插入到页面中生效
const script = `
import style from "!!${relatePath}"
const styleEl = document.createElement('style')
styleEl.innerHTML = style
document.head.appendChild(styleEl)
`
// 终止后面 loader 执行
return script
}
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
9.Plugins
9.1 plugin 介绍
通过插件我们可以拓展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力
工作原理:
webpack 就像一条生产线,要经过过一系列处理流程后才能将源文件转换成输出结果。这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下个流程去处理。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 tapable 来阻止这条复杂的生产线。webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。webpack 的事件流机制保证了插件的有序性,使得整个系统拓展性很好。站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应从钩子,往上挂上自己的任务,也就是注册事件。这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了
钩子:
钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被称作 hooks
(钩子)。开发插件,离不开钩子
Tapable:
Tapable 暴露了三个方法给插件,用于注入不同类型的自定义构建行为
tap
:可以注册同步钩子和异步钩子tapAsync
:回调方式注册异步钩子tapPromise
:Promise 方式注册异步钩子
Plugin 构建对象:
Compiler
:compiler 对象保存着 webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二、仅仅会创建一次的对象。在该对象可以访问到 webpack 的主环境配置,如 loader、plugin 等配置信息。它有一下主要属性:compiler.options
:可以访问本次启动 webpack 所有的配置文件,包括但不限于 loader、plugin、entry、output 等compiler.inputFileSystem
和compiler.outputFileSystem
可以进行文件操作,相当于 nodejs 中的 fscompiler.hooks
可以注册 tapable 的不同种类 Hook,从而可以在 compiler 的生命周期中植入不同逻辑
Compilation
:代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、分块(chunk)、哈西(hash)和重新创建(restore)。主要有以下主要属性:compilation.modules
:可以访问所有模块,打包的每一个文件都是模块compilation.chunks
:chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunkcompilation.assets
:可以访问本次打包生成所有文件的结果compilation.hooks
:可以注册 tapable 的不同种类 hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改
webpack 基本流程:
- 创建 compiler 对象,里面保存着 webpack 完整配置 ->
compiler
->compiler.run()
->compiler.compilation()
->compiler.make()
创建 compilation 对象,包含一次资源完整构建过程 compiler.make()
->compilation.buildModule()
->compilation.seal()
->compilation.optimize()
->compilation.reviveChunks()
->compilation.seal()
- 结束
compiler.seal()
->compiler.afterCompile()
->compile.emit()
->compiler.emitAssets()
最后将资源输出到 dist 上去
9.2 第一个 plugin
/**
* 1.webpack 加载 webpack.config.js 中所有配置,此时就会 new TestPlugin(),这样插件的 constructor
* 2.webpack 创建 compiler 对象
* 3.遍历所有 plugins 中插件,调用插件的 apply 方法
* 4.执行剩下编译流程(触发各个 hooks 事件)
*/
class TestPlugin {
constructor() {
console.log('TestPlugin constructor')
}
apply(compiler) {
console.log('TestPlugin apply')
}
}
module.exports = TestPlugin
2
3
4
5
6
7
8
9
10
11
12
13
14
15
9.3 注册 hooks
/**
* 1.webpack 加载 webpack.config.js 中所有配置,此时就会 new TestPlugin(),这样插件的 constructor
* 2.webpack 创建 compiler 对象
* 3.遍历所有 plugins 中插件,调用插件的 apply 方法
* 4.执行剩下编译流程(触发各个 hooks 事件)
*/
class TestPlugin {
constructor() {
console.log('TestPlugin constructor')
}
apply(compiler) {
console.log('TestPlugin apply')
// 由文档可知,environment 是同步串行钩子,所以用 tap
compiler.hooks.environment.tap('TestPlugin', () => {
console.log('TestPlugin environment')
})
// // emit 是异步串行钩子 AsyncSeriesHook
compiler.hooks.emit.tap('TestPlugin', compilation => {
console.log('TestPlugin emit 111')
})
compiler.hooks.emit.tapAsync('TestPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('TestPlugin emit 222')
callback()
}, 1000)
})
compiler.hooks.emit.tapPromise('TestPlugin', compilation => {
return new Promise(resolve => {
setTimeout(() => {
console.log('TestPlugin emit 333')
}, 1000)
})
})
// make 是异步并行钩子 AsyncParallelHook
compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
// 需要在 compilation hooks 触发前注册才能使用
compilation.hooks.seal.tap('TestPlugin', () => {
console.log('TestPlugin seal')
})
setTimeout(() => {
console.log('TestPlugin make 111')
callback()
}, 3000)
})
compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('TestPlugin make 222')
callback()
}, 2000)
})
compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('TestPlugin make 333')
callback()
}, 1000)
})
}
}
module.exports = TestPlugin
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
48
49
50
51
52
53
54
55
56
57
58
59
9.4 查看 compiler 和 compilation 对象
- 在可以得到 compiler 对象的下一行写
debugger
- package.json 中写入:
"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
,brk
是在代码第一行打断点的意思,整个命令是运行 webpack 的命令
9.5 自定义 BannerWebpackPlugin
- 作用:给打包输出文件添加注释
- 开发思路:
- 需要打包输出前添加注释,需要使用
compiler.hooks.emit
钩子,它是打包输出前触发 - 如何获取打包输出的资源:
compilation.assets
可以获取所有即将输出的资源文件
- 需要打包输出前添加注释,需要使用
- 实现:
webpack.config.js:
new BannerWebpackPlugin({
author: '老王'
})
2
3
banner-webpack-plugin:
class BannerWebpackPlugin {
constructor(options = {}) {
this.options = options
}
apply(compiler) {
// 资源输出前触发
compiler.hooks.emit.tapAsync('BannerWebpackPlugin', (compilation, callback) => {
debugger
const extensions = ['css', 'js']
// 1.获取即将输出的资源文件- compilation.assets
// 2.过滤只保留 js 和 css 资源
const assets = Object.keys(compilation.assets).filter(assetPath => {
// 将文件名切割 ['xxx', 'js'] ['xxx', 'css']
const splitted = assetPath.split('.')
// 获取最后一个文件扩展名
const extension = splitted[splitted.length - 1]
// 判断是否包含
return extensions.includes(extension)
})
const prefix = `/*
* Author: ${this.options.author}
*/
`
// 3.遍历剩下资源加上注释
assets.forEach(asset => {
// 获取原来内容
const source = compilation.assets[asset].source()
// 拼接上注释
const content = prefix + source
// 修改资源
compilation.assets[asset] = {
// 最终资源输出时,调用 source 方法,source 方法的返回值就是资源的具体内容
source() {
return content
},
// 资源大小
size() {
return content.length
}
}
})
callback()
})
}
}
module.exports = BannerWebpackPlugin
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
48
9.6 自定义 CleanWebpackPlugin
- 作用:在 webpack 打包输出前将上次打包内容清空
- 开发思路:
- 如何在打包输出前执行?需要使用
compiler.hooks.emit
钩子,它是打包输出前触发 - 如何清空上次打包内容?
- 获取打包输出目录,通过 compiler 对象
- 通过文件操作清空内容,通过
compiler.outputFileSystem
操作文件
- 如何在打包输出前执行?需要使用
- 实现:
class CleanWebpackPlugin {
apply(compiler) {
// 2.获取打包输出的目录
const outputPath = compiler.options.output.path
const fs = compiler.outputFileSystem
// 1.注册钩子,在打包输出之前 emit
compiler.hooks.emit.tap('CleanWebpackPlugin', compilation => {})
// 3.通过 fs 删除打包输出的目录下的所有文件
this.removeFiles(fs, outputPath)
}
removeFiles(fs, filePath) {
// 想要删除打包输出下所有资源,需要先将目录下的资源删除,不能删除这个目录
// 1.读取目录下所有资源
const files = fs.readdirSync(filePath)
// 2.遍历一个个删除
files.forEach(file => {
// 2.1 遍历所有文件,判断是文件还是文件夹
const path = `${filePath}/${file}`
const fileStat = fs.statSync(path)
console.log(fileStat)
if (fileStat.isDirectory()) {
// 2.2 是文件夹,就得删除下面所有的文件,才能删除文件夹
this.removeFiles(fs, path)
} else {
// 2.3 是文件,直接删除
fs.unlinkSync(path)
}
})
}
}
module.exports = CleanWebpackPlugin
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
9.7 自定义 AnalyzeWebpackPlugin
- 作用:分析
webpack
打包资源大小,并输出分析文件 - 开发思路:
- 在哪放?
compiler.hooks.emit
,它是在打包输出前触发,我们需要分析资源大小同时添加上分析后的 md 文件
- 在哪放?
- 实现:
class AnalyzeWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tap('AnalyzeWebpackPlugin', compilation => {
// 1.遍历所有即将输出文件,得到其大小
// 将对象变成一个二维数组
const assets = Object.entries(compilation.assets)
/**
* md 中表格语法
* | 资源名称 | 资源大小 |
* | --- | --- |
* | xxx.js | 10kb |
*/
let content = `| 资源名称 | 资源大小 |
| --- | --- |`
assets.forEach(([filename, file]) => {
content += `\n| ${filename} | ${Math.ceil(file.size() / 1024)}kb |`
})
// 2.生成一个 md 文件
compilation.assets['analyze.md'] = {
source() {
return content
},
size() {
return content.length
}
}
})
}
}
module.exports = AnalyzeWebpackPlugin
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