Webpack
认识 Webpack
作用
- 前端开发越来越复杂,比如:
- 通过模块化的方式进行开发
- 使用一些高级的特性来提高开发效率、安全性
- 使用 ES6+、TypeScript 进行开发
- 使用 sass、less 等方式来编写 CSS 样式
- 监听文件的变化来实时反映到浏览器上,提高开发效率
- 开发完成后将代码进行压缩、合并及其他优化等
- 三个前端框架 React、Vue、Angular 的脚手架也是基于 Webpack 构建
定义
webpack is a static module bundler for modern JavaScript applications.
-
webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具
- bundler(打包):webpack 是一个打包工具,可以打包各种静态资源
- static(静态的):编写的代码最终都会打包成静态资源,然后部署到静态服务器
- module(模块化):webpack 默认支持各种模块化开发,ES Module、CommonJS、AMD 等
- modern(现代的):正因为现代前端开发面临各种各样的问题,才催生了 webpack 的出现和发展
-
webpack 的安装目前分为两个:webpack 和 webpack-cli
-
命令行执行
webpack命令,会执行./node_modules/.bin/webpack -
webpack-cli负责合并配置选项,并调用webpack 函数进行打包和编译 -
一些第三方的脚手架(比如 Vue 和 React 脚手架)并没有使用 webpack-cli,而是通过一些脚本去调用 webpack 进行打包编译
const webpack = require('webpack');
const webpackConfig = require('./webpack.config.js');
const compiler = webpack(webpackConfig);
compiler.run((err, stats) => {
if (err) {
console.error(err)
} else {
console.log(stats)
}
}) -
基本设置
打包命令
-
webpack命令可以执行打包,默认情况下以当前工作目录下src/index.js作为打包入口 -
在打包时默认不会更改代码中除
import和export语句以外的部分- 即 webpack 默认只具备模块化打包能力,不具备语法转换功能
- 额外的 ES6+ 语法需要借助类似 Babel 的编译工具进行语法转换
-
默认检查当前路径下有没有webpack.config.js文件
- 如果需要手动指定配置文件,可以使用
--config传递配置文件路径
webpack --config ./scripts/webpack.config.js - 如果需要手动指定配置文件,可以使用
打包流程
-
根据命令行配置参数或配置文件找到入口文件
-
从入口开始,webpack 会递归构建一个 依赖关系图,包含应用程序中引入的所有模块(比如 js、css、图片等),不存在依赖图中的模块不会打包进去
-
遍历图结构,打包一个个模块(根据文件类型的不同,使用不同的 loader 来处理)
配置文件
-
配置文件支持函数或对象
-
导出一个函数
- 参数一是环境
- 参数二是传递给 webpack 的配置项
module.exports = function(env, argv) {
return {
mode: env.production ? 'production' : 'development',
devtool: env.production ? 'source-map' : false,
plugins: [
new TerserPlugin({
terserOptions: {
compress: argv.mode === 'production' // only if `--mode production` was passed
}
})
]
};
}; -
导出一个对象
module.exports = {
mode: 'production',
entry: './src/index.js'
};
-
-
支持 TypeScript 配置文件
-
安装相关依赖
npm install -D webpack webpack-cli typescript ts-node @types/node @types/webpack
# 如果使用版本低于 v4.7.0 的 webpack-dev-server,还需要安装以下依赖
npm install --save-dev @types/webpack-dev-server -
编写配置文件 webpack.config.ts
import { resolve } from 'path';
import { Configuration } from 'webpack';
// in case you run into any typescript error when configuring `devServer`
import 'webpack-dev-server';
const config: Configuration = {
mode: 'production',
entry: './src/index.js',
output: {
path: resolve(__dirname, 'dist'),
filename: 'bundle.js',
}
};
export default config; -
更改配置文件 tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "Node"
},
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
}
}
-
基本概念
module
- 每个引入的文件都是一个模块(module)
- 通过互相引用,这些模块会形成一个图(ModuleGraph) 数据结构
chunk
-
在打包过程中,模块会被合并成 chunk
- chunk 合并成 chunk 组,并形成一个通过模块互相连接的图
-
一个 chunk 组中可能有多个 chunk
-
例如 SplitChunksPlugin 会将一个 chunk 拆分为一个或多个 chunk
-
每个 chunk 都有对应的输出文件
-
-
chunk 有两种形式
- initial:在
entry中配置的 chunk,此 chunk 包含为入口起点指定的所有模块及其依赖项 - non-initial:可以异步(延迟)加载的块,可能会出现在使用动态导入 或 SplitChunksPlugin时
- initial:在
-
如下示例,生成结果为
-
创建出一个名为
main的 initial chunk,包括- ./src/index.jsx
- react
- react-dom
- 除 app.jsx 外的所有依赖包
-
同时还会为
./app.jsx创建一个 non-initial chunk,因为该模块是动态导入
- ./webpack.config.js
- ./src/index.jsx
module.exports = {
entry: './src/index.jsx',
};import React from 'react';
import ReactDOM from 'react-dom';
import('./app.jsx').then((App) => {
ReactDOM.render(<App />, root);
}); -
Loader
-
webpack 默认只能处理 JavaScript 和 JSON 文件,这是 webpack的自带能力
- loader 用于对模块的源代码进行转换
- 让 webpack 能够去处理其他类型的文件,以供应用程序使用,以及被添加到依赖图中
-
在 webpack 的配置中,可以通过
module.rules配置 loadermodule.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
]
}
]
}
}
配置选项
核心配置选项
context — 上下文
-
context用于指定基础目录- 必须使用 绝对路径
- 默认使用 Node.js 进程的工作目录(
process.cwd()) - 用于从配置中解析入口点 (entry point) 和 加载器 (loader)
-
比如当
entry使用相对路径时,会以当前配置的context作为基础路径
entry — 入口
-
entry用于指定 webpack 应该使用哪个模块来作为构建其内部依赖图的入口- 动态加载的模块 不是 入口起点
- 规则:每个 HTML 页面都有一个入口起点
-
常用配置方式
-
单入口(SPA 应用)
const config1 = {
entry: './src/index.js'
}
const config2 = {
// 也可以传递一个数组,数组中的模块都将被处理
entry: ['./src/index.js', './src/math.js']
} -
多入口(MPA 应用)
const config = {
entry: {
home: './home.js',
about: './about.js',
contact: './contact.js',
}
}
-
-
当通过插件生成入口时,可以传递空对象
{}
mode — 模式
-
mode用于告知 webpack 使用相应模式的内置优化 -
可选值:
'none'|'development'|'production'- 如果没有设置,webpack 会默认赋值
production - 设置
none则不使用任何 默认优化选项
module.exports = {
mode: 'development'
} - 如果没有设置,webpack 会默认赋值
-
development模式- 设置
process.env.NODE_ENV为 development - 设置
optimization.chunkIds为named
- 设置
-
production模式- 设置
process.env.NODE_ENV为 production - 设置
optimization.chunkIds为deterministic - 同时会启用一系列优化配置
- 设置
output — 输出
output 用于指定 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件
-
path:指定打包文件输出目录。默认为path.join(context, 'dist') -
clean:在生成文件之前对输出目录做一些清理操作,该属性可以替代插件 clean-webpack-plugin -
chunkFilename:指定 non-initial chunk 文件的名称,默认使用[id].js -
filename:指定每个输出 bundle 的名称,这些 bundle 将写入到output.path选项指定的目录下- 此选项不会影响那些 non-initial chunk 的输出文件,它只影响 initial chunk
const { resolve } = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: resolve(__dirname, 'build'),
clean: true, // 在生成文件之前清空 output 目录
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[contenthash].js'
}
}
Template strings — 模板字符串
在指定某些文件名、chunk 名等时,可以使用 webpack 内部提供的 Template strings,相当于一个占位符
-
编译层面的占位符
[fullhash]:compilation 完整的 hash 值,替代已被废弃的[hash]
-
chunk 层面的占位符
模板 描述 [id] 此 chunk 的 ID [name] 如果设置,则为此 chunk 的名称,否则使用 chunk 的 ID [chunkhash] 此 chunk 的 hash 值,包含该 chunk 的所有元素(一个chunk 组使用一个相同的 chunkhash) [contenthash] 此 chunk 的 hash 值,只包括该内容类型的元素(受 optimization.realContentHash影响) -
模块层面的占位符
模板 描述 [id] 模块的 ID [hash] 模块的 Hash 值 [contenthash] 模块内容的 Hash 值 -
文件层面的占位符
模板 描述 [file] filename 和路径,不含 query 或 fragment [query] 带前缀 ?的 query[fragment] 带前缀 #的 fragment[base] 只有 filename(包含扩展名),不含 path [path] 只有 path,不含 filename [name] 只有 filename,不含扩展名或 path [ext] 带前缀 .的扩展名(对 output.filename 不可用)
module.rules — 规则
-
该配置用于定义资源处理规则,该属性对应一个数组,数组里面存放每个Rule 对象,每个 Rule 对象有以下属性
-
test:匹配资源的条件(Condition 类型) -
exclude:排除所有符合条件的模块的规则,同样资源下,exclude 优先级高于 include(Condition 类型) -
include:引入符合条件的模块的规则(Condition 类型) -
oneOf:规则数组,当规则匹配时,只使用第一个匹配规则 -
sideEffects:布尔值。指定是否含有副作用,用于Tree Shaking -
use:一个应用于模块的 UseEntries 对象数组。每个入口 (entry) 指定使用一个 loader,每个 UseEntries 对象有如下属性loader:要使用的 loader 名称options:传递给该 loader 的配置
// 写法 1 —— loader/options
{
test: /\.css$/,
loader: 'css-loader',
options: { importLoaders: 1 }
}
// 写法 2 —— use loader字符串
{
test: /\.css$/,
use: 'css-loader'
}
// 写法 3 —— use loader数组
{
test: /\.less$/,
use: ['css-loader', 'less-loader']
}
// 写法 4(最完整写法,其他写法都是该写法的简写) —— Array<loader 对象>
{
test: /\.css$/,
use: [
{
loader: 'css-loader',
options: { importLoaders: 1 }
}
]
} -
-
上述所说的 Condition 类型,可以是以下构成
- 字符串。目录的绝对路径或文件绝对路径
- 正则表达式
- 函数。调用输入的函数,必须返回一个布尔值以匹配
- 条件数组。至少匹配一个条件
- 对象:匹配所有属性。每个属性都有一个定义行为
{ and: [Condition] }:必须匹配数组中的所有条件{ or: [Condition] }:匹配数组中任何一个条件{ not: [Condition] }:必须排除这个条件
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /\.css$/,
include: [
// will include any paths relative to the current directory starting with `app/styles`
// e.g. `app/styles.css`, `app/styles/styles.css`, `app/stylesheet.css`
path.resolve(__dirname, 'app/styles'),
// add an extra slash to only include the content of the directory `vendor/styles/`
path.join(__dirname, 'vendor/styles/')
]
}
]
}
}; -
loader 可以链式调用,链中的每个 loader 都将对资源进行转换
- 对于 Normal Loader,链默认会逆序执行
- 第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推
resolve — 解析
resolve 用于配置模块如何解析
-
alias:配置路径别名 -
extensions:尝试按顺序解析的后缀名配置数组- 如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀
- 默认值为
['.js', '.json', '.wasm'],使用'...'可以访问默认拓展名
-
fallback:当正常解析失败时,重定向模块请求- webpack 5 不再自动 polyfill Node.js 的核心模块
- 如果需要在浏览器或类似的环境中运行的代码中使用它们,必须从 NPM 中安装兼容的模块,并自己包含它们
-
mainFiles:解析目录时要使用的文件名- 默认为
['index'],即当 import 一个目录时,默认会查找该目录下的 index 文件
- 默认为
-
modules:解析模块时应该搜索的目录- 默认为
['node_modules'],即查找不带相对或绝对路径时会去该配置路径下查找 - 这也很好的说明了为什么 import 一个第三方模块时,webpack会去node_modules下进行查找
- 默认为
const { resolve } = require('path');
module.exports = {
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'pages': resolve(__dirname, './src/pages')
},
// 使用 '...' 访问默认拓展名
extensions: ['.jsx', '.tsx', '...'],
modules: ['../node_modules'],
fallback: {
querystring: require.resolve('qs')
}
}
}
plugins — 插 件
-
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务
- 包括:打包优化,资源管理,注入环境变量
-
插件贯穿于 webpack 的整个生命周期,各种配置参数也是以插件的形式注入到 webpack 的各个生命周期
-
想要使用一个插件,只需要引入它,然后把它添加到
plugins数组中const HTMLWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HTMLWebpackPlugin({ template: './index.html' })
]
}
其他配置选项
资源路径
output.publicPath
-
用于指定应用程序中所有资源的基础路径,默认为
auto-
实质上,发送到
output.path目录的每个文件,都将从output.publicPath位置引用 -
这也包括通过代码分离创建 的子 chunk 和作为依赖图一部分的所有其他资源
静态资源最终访问路径 = output.publicPath + 资源 loader 或插件等配置路径 -
-
该属性可以在入口文件中动态设置
-
webpack 暴露了一个名为
__webpack_public_path__的全局变量__webpack_public_path__ = 'xxx'; -
如果在入口文件中使用 ES Module,则会在 import 之后进行赋值
- 在这种情况下,必须将赋值操作移至一个独立模块中
- 然后将它的
import语句放置在入口文件最上面
index.jsimport './public-path';
// ... 其他模块
-
-
配置示例
-
设置为
output.publicPath = "/",打包后的 html 文件会通过服务根目录引用静态资源- 此时会在前面拼接上域名后去查找该资源
<!-- 通过 https://example.com/main.js 下载该资源 -->
<script defer src="/main.js"></script> -
设置为
output.publicPath = "./",打包后的 html 文件会通过相对路径引用静态资源- 这个时候会相对 html 文件使用相对路径去查找资源
<script defer src="./main.js"></script>
-
devServer.static
-
directory:告诉服务器从哪里提供内容- 只有在希望提供静态文件时才需要这样做
- 开启 webpack-dev-server 服务时,
devServer.static.directory目录下的文件会直接被映射到devServer.static.publicPath下 - 这样可以直接通过 域名 + devServer.static.publicPath + 静态资源名访问资源
-
devServer.static.publicPath:告诉服务器在哪个 URL 上提供devServer.static.directory的内容 -
配置示例
-
假如根目录下的 static 目录下有一个 a.txt 文件
module.exports = {
devServer: {
static: {
publicPath: '/web',
directory: resolve(__dirname, './static')
},
port: 8080
}
} -
这时可以通过 http://localhost:8080/web/a.txt 访问该文件
-
模块 id 生成算法
optimization.chunkIds 告知 webpack 当生成模块 id 时使用哪种生成算法
-
对于 开发环境,会被设置成
'named' -
对于 生产环境,会被设置成
'deterministic' -
如果上述的条件都不符合,会被默认设置为
'natural'
| 选项值 | 描述 |
|---|---|
| natural | 按使用顺序的数字 id(不利于缓存) |
| named | 对调试更友好的可读的 id — 使用模块所在的路径作为产物名称 |
| deterministic | 在不同的编译中不变的短数字 id,有益于长期缓存。在生产模式中会默认开启 |
| size | 让初始下载包大小更小的数字 id |
| total-size | 让总下载包大小更小的数字 id |
资源处理
样式资源
-
css-loader:处理 CSS 样式模块 -
style-loader:先将样式处理好- 通过创建一个style标签的方式
- 通过页内样式的方式添加到 document 中的
head中
-
一些 CSS 预处理器相关的 loader
-
例如
less-loader、sass-loader等,这些 loader 本身并不能处理这些语言 -
需要借助这些预处理器相关的编译工具才可以正常解析,比如 less、sass 工具
-
-
postcss:可以处理 CSS 的浏览器兼容性
{
test: /\.scss$/,
use: [
"style-loader"
{
loader: 'css-loader',
options: {
importLoaders: 2 // 需要的前置Loader个数
}
},
"postcss-loader",
"sass-loader"
]
}
asset module — 资源模块
-
资源模块(asset module) 是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader
-
在 webpack 5 之前,通常使用
url-loader将文件作为 data URI 内联到 bundle 中file-loader将文件发送到输出目录
-
Webpack 5 新增了资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader
-
asset/resource发送一个单独的文件并导出 URL。之前通过使用file-loader实现 -
asset/inline导出一个资源的 data URI。之前通过使用url-loader实现 -
asset/source导出资源的源代码。之前通过使用raw-loader实现 -
asset在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader实现
-
-
-
处理图片资源
-
使用
asset/resource,该处理方式类似于将静态资源拷贝到输出目录{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset/resource",
// 设置打包生成文件名
generator: {
filename: "img/[name][hash][ext]"
}
} -
使用
asset/inline,该方式会将图片资源转换为 base64 编码 URI{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset/inline"
} -
使用
asset,webpack 将按照默认条件,自动地在resource和inline之间进行选择- 小于 8kb 的 文件,将会视为
inline模块类型,否则会被视为resource模块类型 - 设置
Rule.parser.dataUrlCondition.maxSize选项来修改此条件
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset",
generator: {
filename: "img/[name][hash:8][ext]"
},
parser: {
dataUrlCondition: {
// 10kb 以下转 base64,10kb 以上直接导出文件
maxSize: 10 * 1024
}
}
} - 小于 8kb 的 文件,将会视为
-
-
处理字体文件
{
test: /\.ttf|eot|woff2?$/i,
type: "asset-resource",
generator: {
filename: "font/[name][hash:8][ext]"
}
}
处理 ts/tsx 文件
-
处理方式一:
ts-loader- 编译处理:该 loader 处理 ts 代码需要依赖 typescript 中的
tsc命令进行编译 - 优点:在处理 ts 代码时会做类型检验,检验不通过则编译失败
- 缺点:无法注入 Polyfill
- 编译处理:该 loader 处理 ts 代码需要依赖 typescript 中的
-
处理方式二:
babel-loader- 编译处理:无需依赖 typescript 的
tsc命令,使用@babel/preset-typescript预设即可处理 ts 代码 - 优点:可以注入Polyfill
- 缺点:无法做类型检验
- 编译处理:无需依赖 typescript 的
-
最佳实践:打包前先使用
tsc进行类型检测(tsc --noEmit),通过后借助babel编译 ts 代码{
test: /\.tsx?$/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
"@babel/preset-react",
'@babel/preset-typescript'
]
}
}
]
}
处理 vue 文件
-
安装对应依赖
npm install -D vue-loader vue-template-compiler -
配置 webpack
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
module: {
rules: [
// ... 其它规则
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// 请确保引入这个插件!
new VueLoaderPlugin()
]
}
source-map
认识 source-map
-
前端代码通常运行在浏览器上时,是通过 打包压缩 的
-
即真实跑在浏览器上的代码和编写的源码是有很大差异的
-
ES6+ 的代码可能被转换成 ES5
-
对应的代码行号、列号在经过打包后与源码不一致
-
代码进行混淆压缩时,会将变量名称等修改
-
使用 TypeScript 编写的源码,会被编译成 JavaScript
-
-
当需要进行调试时,调试打包后的代码是非常困难的
-
source-map 用于还原源代码
- source-map 是从已转换的代码,映射到原始的源文件
- source-map 文件可以使浏览器重构原始源并在调试器中显示重建的原始源
-
webpack 打包时,通过配置
devtool属性可以生成源文件对应的 source-map 文件- 在转换完成后,在打包后的代码中添加一个注释,指向生成的 source-map 文件
//# sourceMappingURL=main.js.map
文件结构
-
version:当前使用的版本 -
sources:从哪些文件转换过来的 source-map 和打包的代码(从初始的文件) -
names:转换前的变量和属性名称(代码混淆时进行的备份) -
mappings:source-map 用来和源文件映射的信息(比如位置信息等),一串 base64 可变长度值编码 -
file:打包后的文件(浏览器加载的文件) -
sourceContent:转换前的具体源代码信息(和 sources 是对应的关系) -
sourceRoot:所有的 sources 相对的根目录
devtool 配置选项
-
缺省值,即不配置
devtool选项,不生成 source-map(production模式下的默认值) -
false:不生成任何和 source-map 相关的内容 -
eval:development模式下的默认值,不生成 source-map-
会在 eval 执行的代码中,添加
//# sourceURL= -
在执行时会被浏览器解析,并且在调试面板中生成对应的一些文件目录,方便调试代码
-
-
source-map(最完整的 source-map)-
生成一个独立的 source-map 文件,并且在打包后文件中有一个注释,指向 source-map 文件
-
开发工具会根据这个注释找到 source-map,解析后还原源码
main.js//# sourceMappingURL=main.js.map
-
-
eval-source-map:会生成 source-map,但是以DataUrl(base64编码串)添加到 eval 函数后面
-
inline-source-map:会生成 source-map,是以DataUrl添加到打包后的文件的最后面
-
cheap-source-map:会生成 source-map,但是会更加高效,因为没有生成列映射-
使用 source-map(还需要借助 babel,不然无法生成列信息)
-
使用 cheap-source-map
-
-
cheap-module-source-map-
会生成 source-map,类似于 cheap-source-map,但是对源自 loader 的 source-map 的处理会更好
-
比如如下代码,在经过 @babel/preset-env 转换成 ES5 代码后,转换的代码与源代码会有一些差异
-
源代码
const { dateFormat } = require('./js/format');
console.log(c);
console.log(dateFormat(new Date())); -
通过
cheap-source-map映射的源代码
-
-
此时通过启用
cheap-module-source-map就可以得到更好的源码映射
-
-
hidden-source-map- 会生成 source-map,但是不会自动对生成的文件引用,相当于删除了打包后文件对 source-map 文件的引用注释
- 如果手动添加引用注释,则 source-map 依然可以起作用
-
nosources-source-map:生成 source-map 只有错误提示,无法映射源文件