# Webpack
`webpack` 是一个为现代的 JavaScript 应用程序进行模块化打包的工具
- `webpack` 是一个打包工具
- `webpack` 可以将打包打包成最终的静态资源,来部署到静态服务器
- `webpack` 默认支持各种模块化开发, ESModule、CommonJS、AMD等

`Webpack` 的运行依赖 `Node` 环境,需要先安装 `Node.js`
`Webpack` 的安装从 4 版本之后,需要安装两个: `Webpack`(核心功能) 和 `Webpack-cli` (脚手架)
执行 `webpack` 命令会执行 `node_modules` 下的 `bin` 目录下的 `webpack`。 `webpack` 再执行时是依赖 `webpack-cli` 的,`webpack-cli` 在执行时,会利用 `webpack` 进行编译和打包过程
> 也就是说,`webpack` 是必须的。如果不使用 `webpack` 命令,而是自己写命令的话是可以不用 `webpack-cli` 的
使用命令行 `npm install webpack webpack-cli -g` 直接进行**全局安装**即可
使用命令行 `npm install webpack webpack-cli -D` 直接进行**局部安装**即可
## 第一次使用
正如当前 `src` 目录中的 `01` 项目所示,项目结构简单,一个 `html` 和三个 `js` 文件
```
│ index.html
│
└─src
│ index.js
│
└─util
add.js
data.js
```
在 `index.html` 根据引入 js 的方式不同,会出现不同的情况
| 引入代码 | 出现错误 | 错误原因 |
| --- | --- | --- |
| `` | Cannot use import statement outside a module (at index.js:1:1) | 这是因为浏览器不支持 import 语句,需要修改引入代码为 `` |
| `` | 01/src/util/add net::ERR_ABORTED 404 | 这是因为浏览器不会自动查找指定文件夹下同名的 js 文件,也就是说找不到 `./util/add` 这个文件,需要修改为 `./util/add.js`, 对 `./util/data` 找不到也是同理 |
通过给 `script` 标签添加 `type` 属性和修改模块引入路径,成功让代码运行起来了
为了解决上面的问题,直接使用 `webpack` 命令来打包项目试试
直接使用 `webpack` 打包的时候,会检索当前目录下的 `src/index.js` 文件,并在同级目录下生成 `dist/main.js`
```js
```
成功运行,因为 `main.js` 是一个单独的文件,没有引入其他模块,所以不需要指定 `type="module"`,也不需要修改 `import` 模块的路径
> 注意: 直接运行 `webpack` 会搜索当前目录下的 `src/index.js` 所以要注意执行命令时所在的文件路径,如果没有会报错 `Module not found: Error: Can't resolve './src'`
一般会使用 `npm init -y` 来创建 `package.json`, 进而通过局部安装的方式来安装所需的模块
可以通过 `npx webpack` 来运行局部安装的 `webpack`
> `npx` 会执行当前项目的 `node_modules/bin` 中的模块
可以通过向 `package.json` 的 `script` 中添加命令的方式来运行 `webpack`,使用 `npm run build` 来执行 `build` 命令
```json
"scripts": {
"build": "webpack"
},
```
`package.json` 在执行命令的时候会优先查找当前目录中的 `node_modules/bin`
## 配置选项
### 入口文件
正如前面所讲,直接使用 `webpack` 命令会以 `src/index.js` 为入口(`entry`),查找依赖图结构
不是所有的项目的入口都是 `src/index.js`,打包之后的路径也不都是 `dist` 文件夹,所以 `webpack` 提供指定入口文件和输出路径的 `option`
```bash
npx webpack --entry ./src/main.js --output-path ./build
```
> 以 02 文件夹中的项目为例运行上述命令,会以 `src/main.js` 为入口,并将输出的 `main.js` 打包到 `build` 文件夹中
在 [官方文档](https://webpack.docschina.org/api/cli/) 中也有详细说明

当然,如果全部使用命令进行 `flag` 配置,会非常麻烦,尤其是配置项目多了之后,会导致配置项难以查找,所以一般都是创建一个 `webpack` 的配置文件 `webpack.config.js`
`webpack` 是通过 `commonjs` 的方式来读取 `webpack.config.js`,所以使用 `module.export = {}` 方式来导出配置
> 毕竟最终 `webpack` 也是依赖 `node` 来运行的,所以会使用 `node` 的模块导入机制
[官网](https://webpack.docschina.org/api/cli/#config)中也有对 `config` 的解释说明

```js
const path = require("path")
module.exports = {
entry: "./src/main.js",
output: {
filename: "./bundle.js",
path: path.resolve(__dirname, "./build")
}
}
```
将上述代码写入 `webpack.config.js` 文件中,效果等同于 `--entry ./src/main.js --output-path ./build`
为什么 `output.path` 需要指定为 `path.resolve(__dirname, "./build")` ?
因为 `webpack.config.js` 的 `output.path` 需要指定为绝对路径,这个是 `webpack` 要求的,所以可以用到 `node` 内置的 `path` 模块,来获取当前文件(`webpack.config.js`) 的绝对路径
然后直接执行 `npx webpack`,自动读取当前项目目录中的 `webpack.config.js` 文件来进行操作,从而会生成 `build/bundle.js` 文件
如果当前目录中不存在 `webpack.config.js` 文件,而是 `wp.config.js`,这个时候需要指定配置文件的名称

```bash
npx webpack --config ./wp.config.js
```
### 依赖图
如果有一个 js 文件名为 `test.js`,这个文件没有被其他任何模块 `import`,那么这个文件就不会打包到 `webpack` 的最终产物中去
`webpack` 在处理应用程序时,会根据命令或配置文件找到入口文件(比如 `main.js`),从入口开始,会生成一个**依赖关系图**,这个**依赖关系图**会包含应用程序所需的所有模块,然后遍历图结构,打包一个个模块(根据文件的不同使用不同的 `loader` 来解析)
> `webpack` 提供 `tree-shaking` 消除无用代码
也就是说,有一个 `test.js` 文件
```js
console.log(`hello world`);
export function foo()
{
console.log(`test.js foo`);
}
```
在 `main.js` 中
如果使用如下代码,则不会引入 `test.js`
```js
console.log(`main.js`);
```
如果使用如下代码,则会引入 `test.js` 但开启 `tree-shaking` 之后会消除 `foo` 函数,因为 `foo` 并没有被使用
```js
import "./test"
console.log(`main.js`)
```
如果使用如下代码,则会将 `foo` 函数打包到最终文件中,因为 `foo` 被使用了
```js
import * as test from "./test"
test.foo();
console.log(`main.js`)
```
### loader
前面提到过,不同类型文件需要不同的 loader 进行处理,比如 js、html、css 等
如果不对 `webpack` 做任何处理,运行 `03` 项目
> 官方案例 `https://webpack.docschina.org/guides/asset-management/#loading-css`
在 `component.js` 中通过 `import ../css/index.css` 会出现以下错误
```bash
ERROR in ./src/css/index.css 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> .content {
| color: red;
| }
@ ./src/util/component.js 1:0-25
@ ./src/index.js 1:0-25
```
根据 `You may need an appropriate loader` 你需要一个合适的 `loader` 来处理这个 css 文件
`loader` 可以用于对模块的源代码进行转换,这里将 `index.css` 看作是一个模块,通过 `import ../css/index.css` 来加载这个模块,但是 `webpack` 并不知道如何对这个模块进行加载,所以报错
为了让 `webpack` 知道如何加载,需要指定对应的 `loader` 来完成加载功能

直接使用 npm 局部安装 `css-loader` 和 `style-loader` 即可安装所需 `loader`
```bash
npm install css-loader style-loder -D
```
安装了 `loader` 只是表示你有这工具,但是如何使用这个工具需要通过配置来告诉 `webpack`
官网对 loader 的配置也有[说明](https://webpack.docschina.org/concepts/loaders/#configuration)
- 内联方式 指明 `loader`
```js
import "style-loader!css-loader!../css/index.css"
```
- 配置方式
```js
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: 'css-loader',
},
],
},
],
},
```
`module` 属性可以配置 `rules` 规则数组
`rules` 是一个包含多个 `rule` 的数组
```js
{
test: /\.css$/,
use: [
{
loader: 'css-loader',
},
],
},
```
`test` 用于正则匹配,对匹配上的资源,使用该规则中配置的 `loader`
> `/\.css$/` 由于 `.` 在正则中是特殊符号,需要用 `\` 转译;`$` 表示匹配末尾
官网中对 `loader` 也有[详细说明](https://webpack.docschina.org/configuration/module/#rule)
| 写法一 | 写法二 | 写法三 |
| --- | --- | --- |
| `use: [ { loader: "css-loader" } ]` | `loader: "css-loader"` | `use: [ "css-loader" ]` |
> 上述三种写法等价
通过上述修改, 终于能够让 `webpack` 命令正常运行, 得到最后的 `bundle.js` 文件
但是实际运行的时候, `.content` 样式并没有正确显示在页面中, 这是因为 `css-loader` 只是负责解析 css 文件,并不负责插入 css, 所以还需要使用 `style-loader` 插入 style
常见的样式引入有三种方法
1. 行内样式, 即直接写在标签中
2. 页内样式, 即在 html 文件中通过 `style` 标签进行设置
3. 外部样式, 即通过外部的 `.css` 文件进入引入
```js
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
],
},
```
**注意**: `webpack` 加载 `loader` 的顺序是数组序号从大到小执行的
也就是根据上述配置, `webpack` 会先执行 `css-loader` 再执行 `style-loader`. 这个顺序就是正确的, 如果反向的话会执行 `style-loader` 再执行 `css-loader`, 但是没有 `css-loader` 解析 `style-loader` 怎么插入呢?
通过观察生成的 `bundle.js` 可以看到 `webpack` 是如何处理 `css` 的. 它通过创建一个 `style` 的方式将内容通过页内样式插入到 `html` 中
```js
e.exports = function(e) {
var t = document.createElement("style");
return e.setAttributes(t, e.attributes), e.insert(t, e.options), t
}
```

可能会用 `less`、`sass`、`stylus` 的预处理器来编写 css 样式,效率更高,那么如何支持呢?
以 `less` 为例,想要将 `less` 转为 `css`,需要使用 less 这个库。想要解析 `less` 需要使用使用 `less-loader` 库
> `less-loader` 的执行需要 `less`, 所以两个库都要安装
所以需要执行以下代码
```bash
npm install less less-loader -D
```
那么根据流水线操作,针对 `.less` 结尾的文件,需要首先使用 `less-loader` 将其转为 `css`,然后使用 `css-loader` 解析,最后使用 `style-loader` 插入到界面中
所以最终得到 `less` 的 `rule` 内容如下
```js
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
"less-loader"
]
}
```
> 记得 `rule` 不同的等效写法吗
### 浏览器兼容性
针对不同的浏览器支持的特性,比如 css 特性、js 语法等,会导致各种兼容性问题
那么某个功能如果存在兼容性问题,是否需要针对这个功能对不同的浏览器做特殊处理呢?很多情况都是根据浏览器的市场占有率来决定的
[caniuse](https://caniuse.com/usage-table) 是一个判断某些功能能否使用的网站,其在 `usage-table` 界面中提供了市场占有率

很多项目存在 `.browserslistrc` 文件,内容可能如下
```bash
> 1%
last 2 versions
not dead
```
这里的 `> 1%` 就是指市场占有率大于 1%,根据当前运行的浏览器的版本,查找 `caniuse` 网站中的占有率,进而判断是否需要支持
通过 `Browserslist` 工具来共享兼容性配置给其他工具(`babel`、`autoprefixer`等)使用
`Browserslist` 是一个在不同的前端工具之间,共项**目标浏览器**和**Node.js**版本的配置
上述例子有一个叫 `not dead` 的配置,译为没有死亡的。 `Browserslist` 对 `dead` 的定义是 24 个月内没有官方支持或更新的浏览器
上述例子有一个叫 `last 2 versions` 的配置,表示每个浏览器的最后 2 个版本。比如 `last 2 Chrome versions` 就是 Chrome 浏览器最近的两个版本
除此之外,还有针对 node 的版本规则、针对指定平台浏览器的规则、支持特定功能浏览器的规则 等
首先通过 `npm` 安装 `browserslist`
```bash
npm install browserslist -D
```
然后就可以使用 `browserslist` 查询支持的浏览器了
```bash
cmd: npx browserslist ">1%, last 2 versions, not dead"
and_chr 132
and_ff 132
and_qq 14.9
and_uc 15.5
android 132
chrome 132
chrome 131
chrome 109
edge 132
edge 131
firefox 134
firefox 133
ios_saf 18.3
ios_saf 18.2
ios_saf 18.1
ios_saf 17.6-17.7
kaios 3.0-3.1
kaios 2.5
op_mini all
op_mob 80
opera 114
opera 113
safari 18.3
safari 18.2
samsung 27
samsung 26
```
> 在命令中 `,` 等价于 `or` 也就是只要满足其中一个条件即可
```bash
cmd: npx browserslist ">1% and last 2 versions and not dead"
and_chr 132
chrome 132
chrome 131
edge 132
edge 131
firefox 134
ios_saf 18.2
op_mob 80
samsung 27
```
> 在命令中 and 表示所有条件必须全部满足才行
那么如何在项目中进行配置呢?
- 通过在 `package.json` 中新增 `browserslist` 属性进行配置,其他工具会根据该配置自动适配
```json
{
"name": "01",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
// ..
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
// ..
},
"browserslist": [
">1%",
"last 2 version",
"not dead",
]
}
```
- 通过新增 `.browserslistrc` 文件,进行配置
```
>1%"
last 2 version"
not dead"
```
### CSS 的浏览器适配
在前端开发中,CSS 属性需要加上浏览器前缀(如 `-webkit-`、`-moz-`、`-ms-` 等)的原因主要是为了兼容不同浏览器的实验性功能或尚未成为标准的 CSS 特性
浏览器前缀是为了让浏览器厂商能够在 CSS 标准尚未完全确定或实现时,提前引入实验性功能。不同浏览器可能会以不同的方式实现某些特性,前缀可以确保这些实验性功能不会影响其他浏览器的正常渲染
- `-webkit-`:用于基于 WebKit 内核的浏览器(如 `Chrome`、`Safari`、旧版 `Edge`)
- `-moz-`:用于基于 Gecko 内核的浏览器(如 `Firefox`)
- `-ms-`:用于基于 Trident 内核的浏览器(如 `Internet Explorer` 和旧版 `Edge`)
- `-o-`:用于基于 Presto 内核的浏览器(如旧版 `Opera`)
以 `transition` 为例
```css
-webkit-transition: all 2s ease; /* 兼容 WebKit 内核浏览器 */
-moz-transition: all 2s ease; /* 兼容 Gecko 内核浏览器 */
-ms-transition: all 2s ease; /* 兼容 Trident 内核浏览器 */
-o-transition: all 2s ease; /* 兼容 Presto 内核浏览器 */
transition: all 2s ease; /* 标准写法 */
```
> `transition` 是为了让 css 属性值发生变化时不会立刻变化,而是线性的过渡变化。比如从红色设置为黑色,会逐渐变化
为了浏览器兼容性,单单 `transition` 这个属性就要添加好几个带浏览器前缀的属性,而且我们不知道那些属性需要添加浏览器属性,所以人工手动添加是费时费力的
为了浏览器兼容性和方便编写,这个时候就需要用到 `autoprefixer`,能够自动为 `css` 添加浏览器前缀
配合 `autoprefixer` 还需要使用一个 `postcss`, `PostCSS` 是一种 `JavaScript` 工具,可将你的 `CSS` 代码转换为抽象语法树 (`AST`),然后提供 `API`(应用程序编程接口)用于使用 `JavaScript` 插件对其进行分析和修改
基本执行概念就是,先使用 `postcss` 将指定的 `css` 文件转化为抽象语法树,然后使用 `autoprefixer` 来解析这个抽象语法树,并输出为带有浏览器前缀的新 `css` 文件
`autoprefixer` 并不会将所有 `css` 属性都添加上浏览器前缀,而是根据前面所讲的 `browserslist` 配置,将所有需要兼容 `css` 属性添加上浏览器前缀
比如:`transition` 其实现在新的浏览器都已经实现了该功能,不需要添加上浏览器前缀了,那么 `transition` 这个属性就不会特殊处理添加上 浏览器前缀
需要使用 npm 安装 `postcss` 和 `autoprefixer`
```bash
npm install postcss postcss-cli -D
npm install autoprefixer -D
```
然后使用 `postcss` 对 css 文件进行处理
```bash
npx postcss --use autoprefixer -o nnn.css src/css/prefix.css
```
原文件如下
```css
.content {
transition: all 2s ease;
user-select: none;
}
```
转换之后的文件如下
```css
.content {
transition: all 2s ease;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNyYy9jc3MvcHJlZml4LmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtJQUNJLHVCQUF1QjtJQUN2Qix5QkFBaUI7T0FBakIsc0JBQWlCO1lBQWpCLGlCQUFpQjtBQUNyQiIsImZpbGUiOiJubm4uY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLmNvbnRlbnQge1xyXG4gICAgdHJhbnNpdGlvbjogYWxsIDJzIGVhc2U7XHJcbiAgICB1c2VyLXNlbGVjdDogbm9uZTtcclxufSJdfQ== */
```
很明显,根据浏览器兼容性配置,不需要再对 `transition` 属性做特殊处理了,但是针对 `user-select` 还是需要特殊处理的
那么如何在 `webpack` 中使用呢?
根据处理顺序
1. 使用 `postcss` 解析,并交给 `autoprefixer` 插件进行处理得到新的 css
2. 使用 `css-loader` 进行解析
3. 使用 `style-loader` 插入到界面样式中
最终得到配置如下
```js
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
require("autoprefixer")
]
}
}
}
],
},
```
记得安装 `postcss-loader`
```bash
npm install postcss-loader -D
```

不过 `autoprefixer` 也是有局限性的,它只会添加浏览器前缀,对于一些写法并不会做优化,比如
```css
.content {
color: #12345678;
}
```
上述写法使用了 16 进制表现颜色的 `RGBA`,可惜的是一些旧的浏览器可能并不支持这种十六进制的写法,如果使用 `autoprefixer` 并不会将这些写法进行优化,使其兼容旧浏览器
为了让 css 兼容大部分旧的浏览器,一般不仅仅使用 `autoprefixer`,还要使用 `postcss-preset-env`
```bash
npm install postcss-preset-env -D
```
```js
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
require("postcss-preset-env"),
]
}
}
}
],
},
```

> `postcss-preset-env` 中集成了 `autoprefixer` 的特性,所以不需要再使用 `autoprefixer` 了
这里不得不提到 `css` 的 `import` 语法,以 `index.css` 为例
```css
@import "./test.css";
.demo {
color: red;
}
```
根据 `css` 的处理规则,依次 `postcss-loader`、`css-loader`、`style-loader`,那么 `@import "./test.css"` 的处理时机其实是在 `css-loader` 阶段,进而导致 `test.css` 这个文件没有被 `postcss-loader` 处理,那么浏览器兼容性就会失效
> `index.css` 是在 `js` 文件中 `import` 的,所以走正常处理流程
> `test.css` 是在 `css` 文件中 `import` 的,所以无法全部流程
为了处理上述问题,需要对 `css` 的 `rule` 做一些修改
```js
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
require("postcss-preset-env"),
]
}
}
}
],
},
```
添加 `importLoaders: 1` 表示当前 `loader` 的序号后 1 个的 `loader` 开始,这里 `css-loader` 后一个 `loader` 就是 `postcss-loader`
以 `less` 的 `rule` 为例
```js
{
test: /\.less$/,
use: [
"style-loader",
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
require("postcss-preset-env"),
]
}
}
},
"less-loader"
]
}
```
在 `less` 的 `rule` 中,`css-loader` 后面有 2 个 `loader`,为了让 `css` 中 `import` 的 `css` 能够走完整的流程,所以 `importLoaders = 2`