> 文章列表 > webpack 5 实战(1)

webpack 5 实战(1)

webpack 5 实战(1)

一、为什么使用webpack

个人将前端开发分为三个阶段:

1.1 Web1.0

Web1.0前端主要工作:

  • 前端主要编写静态页面
  • 对于JavaScript的使用,主要是进行表单验证和动画效果制作

1.2 Web2.0之AJAX

伴随着AJAX的诞生,前端的工作模式也发生了很大变化,前端不仅仅是展示界面,而且还可以管理数据以及和用户进行数据的交互。在这样的阶段过程中,诞生了像jQuery这样的优秀前端工具库。

1.3 大前端开发(现代Web开发)

在这个阶段中,前端的工作变得更加多样化和复杂化,例如现在前端不仅仅需要开发PC端Web界面,还有移动端的Web界面,以及小程序和公众号,甚至有些时候还需要做App和桌面客户端。

伴随着需要做的事情越来越多,流程也越来越复杂,因此就会出现一些问题。比如说:

现代Web开发“问题”

  • 采用模块化开发

    • 不同浏览器对模块化的支持不同,而且模块化本身存在多种实现规范,这些给最终产出带来了影响
  • 使用新特性提高效率保证安全性

    • 编码过程中,为了提高开发效率,还会使用ES6+、TypeScript、Saas、Less,这些条件浏览器在默认情况下不能正常处理
  • 实时监听开发过程使用热更新

  • 项目结果打包压缩优化

需要有一个工具站出来解决问题,可以让开发者在入口的地方随心所欲,用个人喜欢的技术栈完成开发,从而不需要关系过程,但是最终的结果可以在浏览器上正常展示,因此这里就会用到打包工具。当前Vue、React、Angular本身集成Webpack。

二、Webpack 上手

Webpack定义:为现代JavaScript应用提供静态模块打包

Webpack功能:

  • 打包:将不同类型资源按模块处理进行打包。可以把js、css、img等资源按照模块的方式处理,然后统一的打包输出
  • 静态:打包后最终产出静态资源
  • 模块:Webpack支持不同规范的模块化开发(ES Module、CommonJS、AMD等)

构件如图目录结构,并编码

webpack 5 实战(1)
在web server中进行预览,发现虽然在index.html中使用了type="module",但是依然无法同时识别ES Module和CommonJS。

webpack 5 实战(1)
此时,提前安装好的Webpack就起了作用,在命令行终端输入:Webpack,这时发现目录中输出了dist目录。这里需要注意,Webpack打包会默认找到项目目录下的src目录,并且找到index.js作为入口文件,对依赖进行打包处理,并输出到dist目录中,输出结果默认为main.js。如下:

webpack 5 实战(1)

(() => { var o = { 50: o => { o.exports = () => ({ name: "zce", age: 40 }) } }, e = {}; function r(s) { var t = e[s]; if (void 0 !== t) return t.exports; var n = e[s] = { exports: {} }; return o[s](n, n.exports, r), n.exports } (() => { "use strict"; const o = r(50); console.log(30), console.log(100), console.log(o()) })() })();

观察main.js,发现当前Webpack并未解决ES6+的语法兼容问题

此时将index.html中引入的js文件变更为dist/main.js。

<body><script src="./dist/main.js"></script>
</body>

三、webpack 配置文件

  • 通过命令行参数进行打包
    • yarn Webpack --entry ./src/main.js --output-path ./build,其中--entry指定入口文件,--output-path指定输入路径
  • 通过package.json配置简短命令
...
"scripts": {"build": "Webpack --entry ./src/main.js --output-path ./build"
}
...

通过命令行运行yarn build进行打包

  • 通过webpack.config.js配置文件进行配置
const path = require('path')
module.exports = {entry: './src/index.js',output: {filename: 'build.js',path: path.resolve(__dirname, 'dist')  // 必须使用绝对路径,不然会Webpack抛出错误}
}

四、webpack 依赖图

webpack 5 实战(1)

目录结构:
webpack 5 实战(1)
在index.js中引入lg.js,随后在index.html中引入dist目录下的main.js。Webpack在打包过程中会自动寻找依赖关系并引入,最终打包为main.js。

上文提到可以使用命令行参数对入口文件、输出目录及输出文件名做配置,这里使用–config参数可以对Webpack配置文件进行自定义。例如:

...
{
"scripts" : {
"build": "Webpack --config my-Webpack-config.js"
}
}
...

此时Webpack就会从my-Webpack-config.js读取配置进行打包处理。

五、CSS-Loader

index.html中script引入打包后的main.js

index.js入口文件中引入login.js

// login.js
function createDom() {const h2 = document.createElement('h2')h2.innerHTML = '拉勾教育'h2.className = 'title'return h2
}
document.body.appendChild(createDom())

此时,需要.title添加css样式

/*
login.css
*/
.title {color: red
}

在login.js中引入login.css,并进行Webpack打包,此时会抛出异常,css文件并不是一个模块。

import '../css/login.css'function login() {const oH2 = document.createElement('h2')oH2.innerHTML = '拉勾教育前端'oH2.className = 'title'return oH2
}document.body.appendChild(login())

loader是什么

loader是一个模块,内部使用js实现具体逻辑,比如现在需要一个loader让login.css代码转换为Webpack能识别的模块。

css-loader

安装css-loader

yarn add css-loader

webpack4中对loader的使用一般分为三种:

  • 行内loader
  • 配置文件loader
  • Webpack-cli命令行中使用loader

Webpack5中对cli中使用loader不建议使用,已废弃

行内使用loader,多个loader使用英文**!**进行分隔

import 'css-loader!../css/login.css'function login() {const oH2 = document.createElement('h2')oH2.innerHTML = '拉勾教育前端'oH2.className = 'title'return oH2
}document.body.appendChild(login())

重新执行yarn webpack,虽然没有语法报错,但是样式并未生效。还需要使用一个style-loader

配置文件中使用css-loader

const path = require('path')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist')},module: {rules: [// {//   test: /\\.css$/, // 一般就是一个正则表达式,用于匹配我们需要处理的文件类型//   use: [//     {//       loader: 'css-loader'//     }//   ]// },// {//   test: /\\.css$/,//   loader: 'css-loader'// },{test: /\\.css$/,use: ['css-loader']}]}
}

六、style-loader 使用

然后回到webpack.config.js下,将入口文件的路径指向新创建的css文件。随后配置loader组件,test值为正则表达式/.css$/,use配置一个数组,分别为style-loader以及style-loader

const path = require('path')module.exports = {mode: 'none',entry: './src/main.css',output: {filename: 'bundle.js',path: path.join(__dirname, 'dist')},module: {// rules数组针对于其它资源模块的加载规则,每个规则对象的都需要设置两个属性。rules: [{test: /.css$/,  // test用来去匹配在打包过程当中所遇到的文件路径// use用来去指定我们匹配到的文件,需要去使用的loaderuse: ['style-loader','css-loader']}]}
}

命令行启动,yarn Webpack,通过serve . 运行,在浏览器中访问就可以看到我们的css生效了。

ps:use中,如果配置了多个loader,其执行顺序是从数组最后一个元素往前执行。所以这里一定要把css-loader放到最后,因为我们必须要先通过css-loader把css代码转换为模块才可以正常打包。

style-loader工作代码在bundle.js中,部分代码如下:
webpack 5 实战(1)
loader是webpack实现整个前端模块化的核心,通过不同的loader就可以实现加载任何类型的资源。

七、less-loader

在项目中使用less编写css代码,首先在login.js中正常引入login.less,正常使用Webpack进行编译,发现报错基本与未使用css-loader相同。Webpack默认不支持less文件的编译,所以按照思路,需要先将less文件编译为css文件,然后使用css-loader与style-loader搭配使用,将css样式引入到index.html。下面进行尝试:

首先安装less,尝试把login.less编译为index.css

npm i less -D  # 安装less
npx less ./src/css/login.less index.css  # 使用less将login.less编译为index.css

webpack 5 实战(1)
其次在login.js中将其引入:

// import 'css-loader!../css/login.css'
import '../css/login.css'
import '../css/login.less'function login() {const oH2 = document.createElement('h2')oH2.innerHTML = '拉勾教育前端'oH2.className = 'title'return oH2
}document.body.appendChild(login())

此时运行Webpack进行打包,发现会抛出错误,错误类型与上面提到的Webpack无法编译css文件时相同。

回到初始思路上,我们需要less-loader将less文件编译为css文件,其次使用css-loader搭配style-loader,将css样式编译至html文件中,所以需要进行配置,配置思路与css相同。如下代码:

const path = require('path')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist')},module: {rules: [{test: /\\.css$/,use: ['style-loader', 'css-loader']},{test: /\\.less$/,use: ['style-loader', 'css-loader', 'less-loader']}]}
}

ps:记住loader加载使用顺序是:从右到左从下到上

webpack 5 实战(1)

八、.browserslistrc (帮助兼容不同平台) 工作流程-处理兼容性

https://caniuse.com/

.browserslistrc 是在不同的前端工具之间共用目标浏览器和 node 版本的配置文件。它主要被以下工具使用:

Autoprefixer
Babel
post-preset-env
eslint-plugin-compat
stylelint-unsupported-browser-features
postcss-normalize

Webpack默认会安装browserlistrc

前端工程需要在package.json中配置

{"browserslist": ["last 1 version","> 1%","maintained node versions","not dead"]
}

也可在.browserslistrc文件中配置

# 注释是这样写的,以#号开头
last 1 version #最后的一个版本
> 1%  #代表全球超过1%使用的浏览器
maintained node versions #所有还被 node 基金会维护的 node 版本
not dead

不配置默认为:> 0.5%, last 2 versions, Firefox ESR, not dead
在当前目录下查询目标浏览器npx browserslist

查询条件列表
你可以用如下查询条件来限定浏览器和 node 的版本范围(大小写不敏感):

> 5%: 基于全球使用率统计而选择的浏览器版本范围。>=,<,<=同样适用。
> 5% in US : 同上,只是使用地区变为美国。支持两个字母的国家码来指定地区。
> 5% in alt-AS : 同上,只是使用地区变为亚洲所有国家。这里列举了所有的地区码。
> 5% in my stats : 使用定制的浏览器统计数据。
cover 99.5% : 使用率总和为99.5%的浏览器版本,前提是浏览器提供了使用覆盖率。
cover 99.5% in US : 同上,只是限制了地域,支持两个字母的国家码。
cover 99.5% in my stats :使用定制的浏览器统计数据。
maintained node versions :所有还被 node 基金会维护的 node 版本。
node 10 and node 10.4 : 最新的 node 10.x.x 或者10.4.x 版本。
current node :当前被 browserslist 使用的 node 版本。
extends browserslist-config-mycompany :来自browserslist-config-mycompany包的查询设置
ie 6-8 : 选择一个浏览器的版本范围。
Firefox > 20 : 版本高于20的所有火狐浏览器版本。>=,<,<=同样适用。
ios 7 :ios 7自带的浏览器。
Firefox ESR :最新的火狐 ESR(长期支持版) 版本的浏览器。
unreleased versions or unreleased Chrome versions : alpha 和 beta 版本。
last 2 major versions or last 2 ios major versions :最近的两个发行版,包括所有的次版本号和补丁版本号变更的浏览器版本。
since 2015 or last 2 years :自某个时间以来更新的版本(也可以写的更具体since 2015-03或者since 2015-03-10)
dead :通过last 2 versions筛选的浏览器版本中,全球使用率低于0.5%并且官方声明不在维护或者事实上已经两年没有再更新的版本。目前符合条件的有 IE10,IE_Mob 10,BlackBerry 10,BlackBerry 7,OperaMobile 12.1。
last 2 versions :每个浏览器最近的两个版本。
last 2 Chrome versions :chrome 浏览器最近的两个版本。
defaults :默认配置> 0.5%, last 2 versions, Firefox ESR, not dead。
not ie <= 8 : 浏览器范围的取反。
可以添加not在任和查询条件前面,表示取反

注意:
1.可以使用如下写法,从另外一个输出 browserslist 配置的包导入配置数据:

"browserslist": ["extends browserslist-config-mycompany"
]

为了安全起见,额外的配置包只支持前缀 browserslist-config- 的包命名. npm包作用域也同样支持 @scope/browserslist-config,例如:
@scope/browserslist-config or @scope/browserslist-config-mycompany.

#When writing a shared Browserslist package, just export an array.
#browserslist-config-mycompany/index.js:
module.exports = ['last 1 version','> 1%','ie 10'
]

2.环境的差异化配置
你可以为不同的环境配置不同的浏览器查询条件。Browserslist 将依赖BROWSERSLIST_ENV 或者 NODE_ENV查询浏览器版本范围。如果两个环境变量都没有配置正确的查询条件,那么优先从 production 对应的配置项加载查询条件,如果再不行就应用默认配置。
在 package.json:

  "browserslist": {"production": ["> 1%","ie 10"],"development": ["last 1 chrome version","last 1 firefox version"]}

在配置文件.broswerslistrc

[production staging]
> 1%
ie 10[development]
last 1 chrome version
last 1 firefox version

九、postcss 工作流程

官网说:“PostCSS,一个使用 JavaScript 来处理CSS的框架”。这句话高度概括了 PostCSS 的作用,但是太抽象了。按我理解,PostCSS 主要做了三件事:

  1. parse:把 CSS 文件的字符串解析成抽象语法树(Abstract Syntax Tree)的框架,解析过程中会检查 CSS 语法是否正确,不正确会给出错误提示。
  2. runPlugin: 执行插件函数。PostCSS 本身不处理任何具体任务,它提供了以特定属性或者规则命名的事件。有特定功能的插件(如 autoprefixer、CSS Modules)会注册事件监听器。PostCSS 会在这个阶段,重新扫描 AST,执行注册的监听器函数。
  3. generate: 插件对 AST 处理后,PostCSS 把处理过的 AST 对象转成 CSS string。

webpack 5 实战(1)
「如果没有插件」,那么初始传入的 CSS string 和 generate 生成的 CSS string 是一样的。由此可见,PostCSS 本身并不处理任何具体的任务,只有当我们为其附加各种插件之后,它才具有实用性。

第一阶段:parse

CSS 语法简述

CSS 规则集(rule-set)由选择器和声明块组成:

webpack 5 实战(1)

  • 选择器指向您需要设置样式的 HTML 元素。
  • 声明块包含一条或多条用分号分隔的声明。
  • 每条声明都包含一个 CSS 属性名称和一个值,以冒号分隔。
  • 多条 CSS 声明用分号分隔,声明块用花括号括起来。

五类对象

AST 用五类对象描述 CSS 语法。这里举个具体的例子,再打印出对应的 AST 结果,对照了解 AST 五类对象和 CSS 语法的对应关系。

app.css 文件中写如下内容:

@import url('./app-02.css');.container {color: red;
}

Declaration(声明) 对象

Declaration 对象用来描述 CSS 中的每一条声明语句。

  • type 标记当前对象的类型
  • parent 记录父对象的实例
  • prop 记录声明中的属性名
  • value 记录声明中的值
  • raws 字段记录声明前的字符串、声明属性和值之间的符号的字符串
  • 其余字段解释见代码中的注释。

上边 CSS 文件中的color: red;会被描述成如下对象:

{parent: Rule,       // 外层的选择器被转译成 Rule 对象,是当前声明对象的 parentprop: "color",      // prop 字段记录声明的属性raws: {             // raws 字段记录声明前、后的字符串,声明属性和值之间的字符串,以及前边语句是否分号结束。before: '\\n ',  // raws.before 字段记录声明前的字符串between: ': ', // raws.between 字段记录声明属性和值之间的字符串},source: {          // source 字段记录声明语句的开始、结束位置,以及当前文件的信息start: { offset: 45, column: 3, line: 4 },end: { offset: 55, column: 13, line: 4 },input: Input {css: '@import url('./app-02.css');\\n\\n.container {\\n  color: red;\\n}',file: '/Users/admin/temp/postcss/app.css',hasBOM: false,Symbol(fromOffsetCache): [0, 29, 30, 43, 57]}},Symbol('isClean'): false,  // Symbol(isClean) 字段默认值都是 false,用于记录当前对象关联的 plugin 是否执行。plugin 会在后续解释Symbol('my'): true,        // Symbol(my) 字段默认值都是 true,用于记录当前对象是否是对应对象的实例,如果不是,可以根据类型把对象的属性设置为普通对象的 prototype 属性type: 'decl',            // type 记录对象类型,是个枚举值,声明语句的 type 固定是 declvalue: "red"             // value 字段记录声明的值
}

每个字段的含义和功能已经以注释的形式进行了解释。

Rule 对象

Rule 对象是描述选择器的。

  • type 记录对象的类型
  • parent 记录父对象的实例
  • nodes 记录子对象的实例
  • selector 记录选择器的字符串
  • raws 记录选择器前的字符串、选择器和大括号之间的字符串、最后一个声明和结束大括号之间的字符串
  • 其余字段解释见代码中的注释。

上边 app.css 文件中.container经过 postcss 转译后的对象是(每个字段的含义和功能已经以注释的形式进行了解释):

{nodes: [Declaration], // nodes 记录包含关系,Rule 对象包含 Declaration 对象parent: Root,        // 根对象是 Root 对象,是当前声明对象的 parentraws: {              // raws 字段记录如下before: '\\n\\n',  // raws.before 字段记录选择器前的字符串between: ' ',    // raws.between 字段记录选择器和大括号之间的字符串semicolon: true, // raws.semicolon 字段记录前置声明语句是正常分号结束after: '\\n'      // raws.after 字段记录最后一个声明和结束大括号之间的字符串},selector:'.container', // selector 记录 selectorsource: {            // source 字段记录选择器语句的开始、结束位置,以及当前文件的信息start: { offset: 30, column: 1, line: 3 },input: Input {css: '@import url('./app-02.css');\\n\\n.container {\\n  color: red;\\n}',file: '/Users/admin/temp/postcss/app.css',hasBOM: false,Symbol(fromOffsetCache): [0, 29, 30, 43, 57]},end: { offset: 57, column: 1, line: 5 }},Symbol('isClean'): false,  // Symbol(isClean) 字段默认值都是 false,用于记录当前对象关联的 plugin 是否执行。plugin 会在后续解释Symbol('my'): true,        // Symbol(my) 字段默认值都是 true,用于记录当前对象是否是对应对象的实例,如果不是,可以根据类型把对象的属性设置为普通对象的 prototypetype: 'rule'           // type 记录对象类型,是个枚举值,声明语句的 type 固定是 rule
}

Root 对象

Root 对象是 AST 对象的根对象。

type 记录当前对象的类型
nodes 属性记录子节点对应对象的实例。
上边 app.css 文件中 root 对象是(每个字段的含义和功能已经以注释的形式进行了解释):

{nodes: [AtRule, Rule], // nodes 记录子对象(选择器和 @开头的对象),AtRule 对象会在后边提到raws: {                // raws 字段记录如下semicolon: false,  // raws.semicolon 最后是否是分号结束after: ''          // raws.after 最后的空字符串},source: {              // source 字段记录根目录语句的开始,以及当前文件的信息start: { offset: 0, column: 1, line: 1 },input: Input {css: '@import url('./app-02.css');\\n\\n.container {\\n  color: red;\\n}',file: '/Users/admin/temp/postcss/app.css',hasBOM: false,Symbol(fromOffsetCache): [0, 29, 30, 43, 57]}},Symbol('isClean'): false,  // Symbol(isClean) 字段默认值都是 false,用于记录当前对象关联的 plugin 是否执行。plugin 会在后续解释Symbol('my'): true,        // Symbol(my) 字段默认值都是 true,用于记录当前对象是否是对应对象的实例,如果不是,可以根据类型把对象的属性设置为普通对象的 prototypetype: 'root'           // type 记录对象类型,是个枚举值,声明语句的 type 固定是 root
}

AtRule 对象

CSS 中除了选择器,还有一类语法是 @ 开头的,例如 @import@keyframes@font-face,PostCSS 把这类语法解析成 AtRule 对象。

  • type 记录当前对象的类型
  • parent 记录当前对象的父对象
  • name 记录@紧跟着的单词
  • params 记录 name 值

例如 @import url("./app-02.css"); 将被解析成如下对象:

{name: "import",                  // name 记录 @ 紧跟着的单词params: "url('./app-02.css')",   // params 记录 name 值parent: Root,                    // parent 记录父对象raws: {                          // raws 字段记录如下before: '',                  // raws.before 记录 @语句前的空字符串between: '',                 // raws.between 记录 name 和 { 之间的空字符串afterName: '',                // raws.afterName 记录 name 和 @ 语句之间的空字符串after: '',                   // raws.after 记录大括号和上一个 rule 之间的空字符串semicolon: false             // raws.semicolon 上一个规则是否是分号结束},source: {                        // source 字段记录@语句的开始,以及当前文件的信息start: { offset: 0, column: 1, line: 1 },end: { offset: 27, column: 28, line: 1 },input: Input {css: '@import url('./app-02.css');\\n\\n.container {\\n  color: red;\\n}',file: '/Users/admin/temp/postcss/app.css',hasBOM: false,Symbol(fromOffsetCache): [0, 29, 30, 43, 57]}},Symbol('isClean'): false,  // Symbol(isClean) 字段默认值都是 false,用于记录当前对象关联的 plugin 是否执行。plugin 会在后续解释Symbol('my'): true,        // Symbol(my) 字段默认值都是 true,用于记录当前对象是否是对应对象的实例,如果不是,可以根据类型把对象的属性设置为普通对象的 prototypetype: 'atrule'          // type 记录对象类型,是个枚举值,声明语句的 type 固定是 atrule
}

Comment 对象

css 文件中的注释被解析成 Comment 对象。text 字段记录注释内容。/* 你好 */被解析成:

{parent: Root,             // parent 记录父对象raws: {                   // raws 字段记录如下before: '',           // raws.before 记录注释语句前的空字符串left: ' ',            // raws.left 记录注释语句左侧的空字符串right: ' '            // raws.right 记录注释语句右侧的空字符串},source: {                 // source 字段记录注释语句的开始、结束位置,以及当前文件的信息start: {}, input: Input, end: {}},Symbol('isClean'): false,  // Symbol(isClean) 字段默认值都是 false,用于记录当前对象关联的 plugin 是否执行。plugin 会在后续解释Symbol('my'): true,        // Symbol(my) 字段默认值都是 true,用于记录当前对象是否是对应对象的实例,如果不是,可以根据类型把对象的属性设置为普通对象的 prototypetext: '你好',             // text 记录注释内容type: 'comment'          // type 记录对象类型,是个枚举值,声明语句的 type 固定是 comment
}

图解五类对象之间的继承关系

从上一段可以知道,CSS 被解析成 Declaration、Rule、Root、AtRule、Comment 对象。这些对象有很多公共方法,PostCSS 用了面向对象的继承思想,把公共方法和公共属性提取到了父类中。

Root、Rule、AtRule 都是可以有子节点的,都有 nodes 属性,他们三个继承自 Container 类,对 nodes 的操作方法都写在 Container 类中。Container、Declaration、Comment 继承自 Node 类,所有对象都有 Symbol(‘isClean’)、Symbol(‘my’)、raws、source、type 属性,都有toString()、error()等方法,这些属性和方法都定义在 Node 类中。

Container、Node 是用来提取公共属性和方法,不会生成他们的实例。

五个类之间的继承关系如下图所示:
webpack 5 实战(1)
图中没有穷举类的方法,好奇的同学可以看直接看源码文件: https://github.com/postcss/postcss/tree/main/lib 。

把 CSS 语法解析成 AST 对象的具体算法

算法对应源码中位置是:postcss/lib/parser.js中的parse方法,代码量不大,可自行查看。

第二阶段:runPlugin

PostCSS 本身并不处理任何具体的任务,只有当我们为其附加各种插件之后,它才具有实用性。

PostCSS 在把 CSS string 解析成 AST 对象后,会扫描一边 AST 对象,每一种 AST 的对象都可以有对应的监听器。在遍历到某类型的对象时,如果有对象的监听器,就会执行其监听器。

第一类监听器

PostCSS 提供的「以特定属性或者规则命名」的事件监听器,如下:

CHILDREAN 代表子节点的事件监听器。

// root
['Root', CHILDREN, 'RootExit']// AtRule
['AtRule', 'AtRule-import', CHILDREN, 'AtRuleExit', 'AtRuleExit-import']// Rule
['Rule', CHILDREN, 'RuleExit']// Declaration
['Declaration', 'Declaration-color', 'DeclarationExit', 'DeclarationExit-color']// Comment
['Comment', 'CommentExit']

PostCSS 以深度优先的方式遍历 AST 树。

  • 遍历到 Root 根对象,第一步会执行所有插件注册的 Root 事件监听器,第二步检查 Root 是否有子对象,如果有,则遍历子对象,执行子对象对应的事件监听器;如果没有子对象,则直接进入第三步,第三步会执行所有插件注册的 RootExit 事件监听器。插件注册的 Root、RootExit 事件的监听器只能是函数。函数的第一个参数是当前访问的 AST 的 Root 对象,第二个参数是 postcss 的 Result 对象和一些其他属性,通过 Result 对象可以获取 css string、opts 等信息。
{Root: (rootNode, helps) => {},RootExit: (rootNode, helps) => {}
}
  • 遍历到 Rule 对象,则和访问 Root 根对象是一样的逻辑,先执行所有插件注册的 Rule 事件监听器,再遍历子对象,最后执行所有插件注册的 RuleExit 事件监听器。插件注册的 Rule、RuleExit 事件的监听器只能是函数。
{Rule: (ruleNode, helps) => {},RuleExit: (ruleNode, helps) => {}
}
  • 遍历到 AtRule 对象。插件注册的 AtRule 的事件监听器可以是函数,也可以是对象。对象类型的监听器,对象属性的 key 是 AtRule 对象的 name 值,value 是函数。AtRuleExit 是一样的逻辑。事件的执行顺序是:['AtRule', 'AtRule-import', CHILDREN, 'AtRuleExit', 'AtRuleExit-import']。CHILDREAN 代表子节点的事件。```// 函数 { AtRule: (atRuleNode, helps) => {} }
// 对象
{AtRule: {import: (atRuleNode, helps) => {},keyframes: (atRuleNode, helps) => {}}
}
  • 遍历到 Declaration 对象。插件注册的 Declaration 的事件监听器可以是函数,也可以是对象,对象属性的 key 是 Declaration 对象的 prop 值,value 是函数。DeclarationExitExit 是一样的逻辑。事件的执行顺序是:['Declaration', 'Declaration-color', 'DeclarationExit', 'DeclarationExit-color']。Declaration 没有子对象,只需要执行当前对象的事件,不需要深度执行子对象的事件。
// 函数
{Declaration: (declarationNode, helps) => {}
}// 对象
{Declaration: {color: (declarationNode, helps) => {},border: (declarationNode, helps) => {}}
}
  • 遍历到 Comment 对象。依次执行所有插件注册的 Comment 事件监听器,再执行所有插件注册的 CommentExit 事件监听器。

第二类监听器

除以特定属性或者规则命名的事件监听器,PostCSS 还有以下四个:

{postcssPlugin: string,prepare: (result) => {},Once: (root, helps) => {},OnceExit: (root, helps) => {},
}

PostCSS 插件事件的整体执行是:[prepare, Once, ...一类事件,OnceExit],postcssPlugin 是插件名称,不是事件监听器。

  • postcssPlugin:字符串类型,插件的名字,在插件执行报错,提示用户是哪个插件报错了。
  • prepare:函数类型,prepare 是最先执行的,在所有事件执行前执行的,插件多个监听器间共享数据时使用。prepare 的入参是 Result 对象,返回值是监听器对象,通过 Result 对象可以获取 css string、opts 等信息。
{postcssPlugin: "PLUGIN NAME",prepare(result) {const variables = {};return {Declaration(node) {if (node.variable) {variables[node.prop] = node.value;}},OnceExit() {console.log(variables);},};},
};
  • Once:函数类型,在 prepare 后,一类事件前执行,Once 只会执行一次。
{Once: (root, helps) => {}
}

插件源码截图

此时再看市面上流行的基于 postcss 的工具,有没有醍醐灌顶?

  • autoprefixer
  • postcss-import-parser
  • postcss-modules
  • postcss-modules

基于 postcss 的插件有很多,可查阅:https://github.com/postcss/postcss/blob/main/docs/plugins.md

第三阶段:generate

generate 的过程依旧是以深度优先的方式遍历 AST 对象,针对不同的实例对象进行字符串的拼接。算法对应源码中位置是:postcss/lib/stringifier.js中的stringify方法,代码量不大,可自行查看。

十、postcss-loader 处理css兼容

css3自动加前缀 -webkit

const path = require('path')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist')},module: {rules: [{test: /\\.css$/,use: ['style-loader','css-loader',{loader:'postcss-loader',options:{ // Webpack选项postcssOptions:{ // loader配置选项plugins:[require('autoprefixer')]}}}]},{test: /\\.less$/,use: ['style-loader','css-loader','postcss-loader','less-loader']}]}
}

处理颜色的8进制

color: #12345678后两位用于指定透明度

postcss-preset-env预设就是插件的集合,postcss-preset-env已经包含了autoprefixer,所以可以只使用postcss-preset-env

const path = require('path')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist')},module: {rules: [{test: /\\.css$/,use: ['style-loader','css-loader',{loader:'postcss-loader',options:{ // Webpack选项postcssOptions:{ // loader配置选项plugins:[require('postcss-preset-env')]}}}]},{test: /\\.less$/,use: ['style-loader','css-loader','postcss-loader','less-loader']}]}
}

npm i postcss-preset-env -D

单独使用配置文件配置postcss插件

postcss.config.js

module.exports = {plugins: [require('postcss-preset-env')]
}

十一、importLoaders 属性

问题:
test.css的内容如下:

.title {transition: all .5s;user-select: none;
}

login.css的内容如下:

/* 导入test.css */

@import './test.css';
.title {color: #12345678;
}

再次npm run build发现运行之后的test.css里面的代码并没有做兼容性处理。

问题分析:

  • login.css @import 语句导入了test.css
  • login.css可以被匹配,当它被匹配到之后就是postcss-loader进行工作
  • 基于当前的代码,postcss-loader拿到了login.css当中的代码之后分析基于我们的筛选条件并不需要做额外的处理
  • 最终就将代码交给了css-loader
  • 此时css-loader是可以处理@import media、 url … ,这个时候它又拿到了test.css文件,但是loader不会回头找
  • 最终将处理好的css代码交给style-loader进行展示

解决问题:修改Webpack.config.js给css-loader设置一些属性。

const path = require('path')
module.exports = {entry: './src/index.js',output: {filename: 'build.js',//output必须设置绝对路径,所以这里导入path模块path: path.resolve(__dirname, 'dist')},module: {rules: [{//简写方式test: /\\.css$/,//先执行style-loader再执行css-loader//顺序规则,从右往左,从下往上,因为兼容性处理要在css调用之前,所以需要将postcss-loader的配置放在css-loader右边use: ['style-loader', {loader: 'css-loader',options: {// css-loader工作时,遇到css文件时,再往前找一个loader,即追回到postcss-loaderimportLoaders: 1}}, 'postcss-loader']},{//简写方式test: /\\.less$/,//先执行style-loader再执行css-loader//顺序规则,从右往左,从下往上use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']}]}
}

webpack 5 实战(1)

再次运行成功。运行结果如下,test.css的内容也被修改成功。

十二、file-loader 处理图片

1 JS导入图片并写入HTML

在js文件中引入img图片并输出到页面上

要处理jpg、png等格式的图片,我们也需要有对应的loader: file-loader。file-loader的作用就是帮助我们处理import/require()等方式引入的一个文件资源,并且会将它放到我们输出的文件夹中;当然也可以修改它的名字和所在文件夹

安装file-loader

npm install file-loader -D

Image.js中导入图片并显示在页面上:

import oImgSrc from '../img/01.wb.png'function packImg() {// 01 创建一个容器元素const oEle = document.createElement('div')// 02 创建 img 标签,设置 src 属性const oImg = document.createElement('img')oImg.width = 600// 写法1:使用require...default取值// require导入默认一个对象,有一个default的键,代表的导入的内容// oImg.src = require('../img/01.wb.png').default// 写法2:lg.Webpack.js配置文件搭配使用,不需要写default取值// esModule: false // 不转为 esModule// oImg.src = require('../img/01.wb.png')// 写法3:使用import导入,不需要写default或者config配置esModuleoImg.src = oImgSrcoEle.appendChild(oImg)return oEle
}document.body.appendChild(packImg())

lg.Webpack.js

const path = require('path')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist')},module: {rules: [{test: /\\.css$/,use: ['style-loader',{loader: 'css-loader',options: {importLoaders: 1,esModule: false}},'postcss-loader']},{test: /\\.less$/,use: ['style-loader','css-loader','postcss-loader','less-loader']},{test: /\\.(png|svg|gif|jpe?g)$/,// use: [//   {//     loader: 'file-loader',//     options: {//       esModule: false // 不转为 esModule,在js导入时无需写default取值//     }//   }// ]use: ['file-loader']}]}
}

2 JS导入图片并设置到css样式

css-loader处理时,会默认将background-image: url('../img/02.react.png')处理为require的形式,而require会返回一个ESModule,所以需要在Webpack配置中添加css-loader的属性值->esModule: false

{test: /\\.css$/,use: ['style-loader',{loader: 'css-loader',options: {importLoaders: 1,esModule: false}},'postcss-loader']
},

img.css

.bgBox {width: 240px;height: 310px;border: 1px solid #000;background-image: url('../img/02.react.png');
}

Image.js

import oImgSrc from '../img/01.wb.png'
import '../css/img.css'function packImg() {// 01 创建一个容器元素const oEle = document.createElement('div')// 02 创建 img 标签,设置 src 属性const oImg = document.createElement('img')oImg.width = 600// 写法1:使用require...default取值// require导入默认一个对象,有一个default的键,代表的导入的内容// oImg.src = require('../img/01.wb.png').default// 写法2:lg.Webpack.js配置文件搭配使用,不需要写default取值// esModule: false // 不转为 esModule// oImg.src = require('../img/01.wb.png')// 写法3:使用import导入,不需要写default或者config配置esModuleoImg.src = oImgSrcoEle.appendChild(oImg)// 03 设置背景图片const oBgImg = document.createElement('div')oBgImg.className = 'bgBox'oEle.appendChild(oBgImg)return oEle
}document.body.appendChild(packImg())

lg.Webpack.js

const path = require('path')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist')},module: {rules: [{test: /\\.css$/,use: ['style-loader',{loader: 'css-loader',options: {importLoaders: 1,esModule: false}},'postcss-loader']},{test: /\\.less$/,use: ['style-loader','css-loader','postcss-loader','less-loader']},{test: /\\.(png|svg|gif|jpe?g)$/,// use: [//   {//     loader: 'file-loader',//     options: {//       esModule: false // 不转为 esModule//     }//   }// ]use: ['file-loader']}]}
}

十三、设置图片名称与输出

修改file-loader的options用于设置图片名称和输出。

常见占位符:

[ext]: 扩展名 
[name]: 文件名称 
[hash]: 文件内容+MD4生成128为占位置,作为文件名 
[contentHash]: 文件内容+MD4生成128为占位置,作为文件名 
[hash:<length>]: hash截取,作为文件名
[path]: 文件路径

lg.Webpack.js

{test: /\\.(png|svg|gif|jpe?g)$/,use: [{loader: 'file-loader',options: {name: 'img/[name].[hash:6].[ext]',// outputPath: 'img'}}]
}

其中,目录有两种写法,一种为添加outputPath: 'img',另一种为直接在name处写入img/重新打包

十四、url-loader 处理图片

什么是 url-loader

url-loader 会将引入的文件进行编码,生成 DataURL,相当于把文件翻译成了一串字符串,再把这个字符串打包到 JavaScript

什么时候使用

一般来说,我们会发请求来获取图片或者字体文件。如果图片文件较多时(比如一些 icon),会频繁发送请求来回请求多次,这是没有必要的。此时,我们可以考虑将这些较小的图片放在本地,然后使用 url-loader 将这些图片通过base64的方式引入代码中。这样就节省了请求次数,从而提高页面性能。

如何使用

安装 url-loader

npm install url-loader --save-dev

配置 webapck

module.exports = {module: {rules: [{test: /\\.(png|jpg|gif)$/,use: [{loader: 'url-loader',options: {},},],},],},
};

引入一个文件,可以是 import(或 require

import logo from '../assets/image/logo.png';
console.log('logo的值: ', logo); // 打印一下看看 logo 是什么

简单三步就搞定了。

见证奇迹的时刻

Webpack

执行Webpack之后,dist 目录只生成了一个 bundle.js。和 file-loader 不同的是,没有生成我们引入的那个图片。上文说过,url-loader 是将图片转换成一个 DataURL,然后打包到JavaScript代码中。

那我们就看看 bundle.js 是否有我们需要的 DataURL:

// bundle.js
(function(module, exports) {
module.exports = "data:image/jpeg;base64.........."; // 省略无数行
})

我们可以看到这个模块导出的是一个标准的 DataURL

一个标准的DataURL: data:[<mediatype>][;base64],<data>

通过这个 DataURL,我们就可以从本地加载这张图片了,也就不用将图片文件打包到 dist 目录下。

使用 base64 来加载图片也是有两面性的:

  • 优点:节省请求,提高页面性能
  • 缺点:增大本地文件大小,降低加载性能

所以我们得有取舍,只对部分小 size 的图片进行 base64 编码,其它的大图片还是发请求吧。

url-loader自然是已经做了这个事情,我们只要通过简单配置即可实现上述需求。

options

  • limit: 文件阈值,当文件大小大于 limit 的时候使用 fallbackloader 来处理文件
  • fallback: 指定一个 loader 来处理大于limit的文件,默认值是 file-loader

我们来试试设一个 limit

{test: /\\.(png|jpg|gif)$/,use: [{loader: 'url-loader',options: {name: 'img/[name].[hash:6].[ext]',limit: 25 * 1024  // 25kb}}]
},
/*** 01 url-loader base64 uri 文件当中,减少请求次数* 02 file-loader 将资源拷贝至指定的目录,分开请求* 03 url-loader 内部其实也可以调用 file-loader* 04 limit*/

重新执行 Webpack,由于我们引入的 logo.png 大于 1000,所以使用的是 file-loader 来处理这个文件。图片被打包到 dist 目录下,并且返回的值是它的地址:

(function(module, exports, __Webpack_require__) {
module.exports = __Webpack_require__.p + "dab1fd6b179f2dd87254d6e0f9f8efab.png";
}),

源码解析

file-loader 的代码也不多,就直接复制过来通过注释讲解了:

import { getOptions } from 'loader-utils'; // loader 工具包
import validateOptions from 'schema-utils'; // schema 工具包
import mime from 'mime';import normalizeFallback from './utils/normalizeFallback'; // fallback loader
import schema from './options.json'; // options schema// 定义一个是否转换的函数
/**@method shouldTransform*@param {Number|Boolean|String} limit 文件大小阈值*@param {Number} size 文件实际大小*@return {Boolean} 是否需要转换
*/
function shouldTransform(limit, size) {if (typeof limit === 'boolean') {return limit;}if (typeof limit === 'number' || typeof limit === 'string') {return size <= parseInt(limit, 10);}return true;
}export default function loader(src) {// 获取 Webpack 配置里的 optionsconst options = getOptions(this) || {};// 校验 optionsvalidateOptions(schema, options, {name: 'URL Loader',baseDataPath: 'options',});// 判断是否要转换,如果要就进入,不要就往下走// src 是一个 Buffer,所以可以通过 src.length 获取大小if (shouldTransform(options.limit, src.length)) {const file = this.resourcePath;// 获取文件MIME类型,默认值是从文件取,比如 "image/jpeg"const mimetype = options.mimetype || mime.getType(file);// 如果 src 不是 Buffer,就变成 Bufferif (typeof src === 'string') {src = Buffer.from(src);}// 构造 DataURL 并导出return `module.exports = ${JSON.stringify(`data:${mimetype || ''};base64,${src.toString('base64')}`)}`;}// 判断结果是不需要通过 url-loader 转换成 DataURL,则使用 fallback 的 loaderconst {loader: fallbackLoader,options: fallbackOptions,} = normalizeFallback(options.fallback, options);// 引入 fallback loaderconst fallback = require(fallbackLoader);// fallback loader 执行环境const fallbackLoaderContext = Object.assign({}, this, {query: fallbackOptions,});// 执行 fallback loader 来处理 srcreturn fallback.call(fallbackLoaderContext, src);
}// 默认情况下 Webpack 对文件进行 UTF8 编码,当 loader 需要处理二进制数据的时候,需要设置 raw 为 true
export const raw = true;

十五、asset 处理图片

Webpack 出现之前,前端开发人员会使用gruntgulp 等工具来处理资源,并
将它们从 /src 文件夹移动到 /dist /build目录中。Webpack 最出色的功能之一就是,除了引入 JavaScript,还可以内置的资源模块 Asset Modules 引入任何其他类型的文件。

Webpack4的时候以及之前,我们通常是使用file-loaderurl-loader来帮助我们加载其他资源类型。

1、Asset Modules Type的四种类型

而Webpack5可以使用资源模块来帮助我们,称之为Asset Modules,它允许我们打包其他资源类型,比如字体文件、图表文件、图片文件等。

其中,资源模块类型我们称之为Asset Modules Type,总共有四种,来代替loader,分别是:

  1. asset/resource:发送一个单独的文件并导出URL,替代file-loader
  2. asset/inline:导出一个资源的data URI,替代url-loader
  3. asset/source:导出资源的源代码,之前通过使用raw-loader实现
  4. asset:介于asset/resourceasset/inline之间,在导出一个资源data URI和发送一个单独的文件并导出URL之间做选择,之前通过url-loader+limit属性实现。

不过在介绍这四种资源模块类型之前,我们先说一下怎么自定义这些输出的资源模块的文件名

2 自定义资源模块名称

2.1 assetModuleFilename

第一种方式,就是在Webpack配置中设置 output.assetModuleFilename 来修改此模板字符串,其中assetModuleFilename默认会处理文件名后缀的点,所以无需手动添加点。此方式为公共的处理方法,当需要同时处理图片资源和字体资源时,通用方法会导致两种资源类型放在同一个目录下,此处不建议使用assetModuleFilename。

比如关于图片的输出文件名,我们可以让其都输出在images文件夹下面,[contenthash]表示文件名称,[ext]表示图片文件的后缀,比如.png、.jpg、.gif、jpeg等,[query]表可能存在的参数

output: {···assetModuleFilename: 'images/[contenthash][ext][query]' ···
},

2.2 geneator属性

第二种方式,就是在module.rules里面某种资源文件配置的时候,加上geneator属性,例如

rules: [{ test: /\\.png/, type: 'asset/resource', generator: { filename: 'images/[contenthash][ext][query]' } }
]

【注意】
generator 的优先级高于 assetModuleFilename

3 四种类型的导入

首先我们先新建一个文件夹来测试,文件夹目录如下,我们在src下面新建一个assets文件夹,里面放上事先准备好的集中不同类型的图片
webpack 5 实战(1)
index.js

import hello from './hello'
import img1 from './assets/man.jpeg'
import img2 from './assets/store.svg'
import img3 from './assets/women.jpg'
import Txt from './assets/wenzi.txt'
import dynamic from './assets/dongtu.gif'
hello()const IMG1 = document.createElement('img')
IMG1.src = img1
document.body.appendChild(IMG1)const IMG2 = document.createElement('img')
IMG2.src = img2
IMG2.style.cssText = 'width:200px;height:200px'
document.body.appendChild(IMG2)const IMG3 = document.createElement('img')
IMG3.src = img3
document.body.appendChild(IMG3)const TXT = document.createElement('div')
TXT.textContent = Txt
TXT.style.cssText = 'width:200px;height:200px;backGround:aliceblue'
document.body.appendChild(TXT)const DYNAMIC = document.createElement('img')
DYNAMIC.src = dynamic
document.body.appendChild(DYNAMIC)

hello.js

function hello(){console.log("hello-world!!!")
}export default hello

index.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>你是,永远的神</title>
</head>
<body>
</body>
</html>

Webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-Webpack-plugin')module.exports = {entry : './src/index.js',output : {filename:'bundle.js',path:path.resolve(__dirname,'./dist'),clean:true,//如果不设置,打包完之后资源会直接打包在dist目录下assetModuleFilename:'images/[contenthash][ext][query]'},mode : 'development',devtool:'inline-source-map',plugins:[new HtmlWebpackPlugin({template:'./index.html',filename:'app.html',inject:"body"})],devServer:{static:'./dist'},module:{rules:[{test:/\\.jpeg$/,type:"asset/resource",generator:{filename:'images/[contenthash][ext][query]'}},{test:/\\.svg$/,type:'asset/inline'},{test:/\\.txt$/,type:'asset/source'},{test:/\\.(gif|jpg)$/,type:'asset',parser:{dataUrlCondition:{maxSize : 10 * 1024 * 1024}}}]}}

3.1 resource 资源类型

asset/resource可以发送一个单独的文件并导出URL

我们将.jpeg后缀的图片设置type为asset/resource,我们在index.js里面导入该图片并插入在body中,即将其当成资源显示在页面上

npx Webpack打包之后,dist文件夹下的images文件中就出现该图片

3.2 inline资源类型

asset/inline导出一个资源的data URI

仿照上面的方式,我们将.svg后缀的图片设置type为asset/inline,我们在index.js里面导入该图片并插入在body中,即将其当成资源显示在页面上,同时我们简单设置一下样式

不过不同的是,npx Webpack打包之后,dist文件夹下面并没有打包过.svg类型的图片

npx Webpack-dev-server --open自动打开浏览器,我们在控制台中查看该图片类型,发现asset/inline类型确实可以导出Data URI形式的路径

3.3 source资源类型

source资源,导出资源的源代码

仿照上面的方式,我们创建一个.txt后缀的文本文件,设置type为asset/source,我们在index.js里面导入该文本并插入在body中,即将其当成资源显示在页面上,同时我们简单设置一下样式

不过不同的是,npx Webpack打包之后,dist文件夹下面并没有打包过.txt类型的文本文件

npx Webpack-dev-server --open自动打开浏览器,我们在控制台中查看该文本类型,发现asset/source类型确实可以导出资源的源代码

3.4 asset通用资源类型

asset会介于asset/resource和asset/inline之间,在发送一个单独的文件并导出URL和 导出一个资源data URI之间做选择

默认情况下,Webpack5会以8k为界限来判断:

当资源大于8k时,自动按asset/resource来判断
当资源小于8k时,自动按asset/inline来判断
我们可以手动更改临界值,设置parser(解析),其是个对象,里面有个固定的属性,叫dataUrlCondition,顾名思义,data转成url的条件,也就是转成bas64的条件,maxSize是就相当于Limit了

module:{rules:[···{test:/\\.(gif|jpg)$/,type:'asset',parser:{dataUrlCondition:{maxSize : 100 * 1024 }}}···]}

这里我们设置100 * 1024即100kb,来作为临界值
【1b * 1024 = 1kb,1kb * 1024 = 1M】

仿照上面的方式,我们将.gif.jpg后缀的图片设置type为asset资源类型,我们在index.js里面导入2张图片并插入在body中,即将其当成资源显示在页面上,其中.gif大小为128.11kb(超过了100kb的临界值),.jpg大小为12kb(未超过100kb的临界值)

npx Webpack打包之后,dist文件夹下面有打包过的.gif类型的图片,但是没有打包过.jpg类型的图片

npx Webpack-dev-server --open自动打开浏览器,我们在控制台中查看2种图片类型,发现.gif图片是单独一个文件的URL路径,而.jpg图片是Data URI格式的base64路径

十六、asset处理图标字体

同上面所说,处理字体图标文件时,需要将其视为resource资源直接复制,所以需要使用asset/resource。此时准备好的字体文件及其目录如下:

在font目录中,准备了iconfont.css及其字体文件,其中iconfont.css中对font-family进行赋值对应的字体。

单独常见font.js文件,并在文件中引入iconfont.css以及自定义的index.css文件,创建页面DOM元素并显示。

Font.js

import '../font/iconfont.css'
import '../css/index.css'function packFont() {const oEle = document.createElement('div')const oSpan = document.createElement('span')oSpan.className = 'iconfont icon-linggan lg-icon'oEle.appendChild(oSpan)return oEle
}document.body.appendChild(packFont())

当然,此时直接运行yarn build肯定会报错,因为此时Webpack不认识ttf/woff/woff2等资源,所以需要单独使用asset/resouce进行打包配置。

lg.Webpack.js

const path = require('path')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist'),// assetModuleFilename: "img/[name].[hash:4][ext]"},module: {rules: [{test: /\\.(ttf|woff2?)$/,type: 'asset/resource',  // 使用资源复制generator: {filename: 'font/[name].[hash:3][ext]'  // 指定字体文件输出路径}}]}
}

此时执行yarn build,我们发现在dist目录下新增了font目录,font目录中的字体文件为Webpack拷贝而来。打开页面可以看到iconfont.css以及自定义的index.css文件样式已经生效。

十七、Webpack 插件使用

插件机制是Webpack当中另外一个核心特性‌‌,它目的是为了增强Webpack项目自动化方面的能力‌‌,loader就是负责实现各种各样的资源模块的加载‌‌,从而实现整体项目打包‌‌,plugin则是用来去解决项目中除了资源以外,其它的一些自动化工作‌,例如:

  • plugin可以帮我们去实现自动在打包之前去清除dist目录‌‌,也就是上一次打包的结果‌‌;
  • 又或是它可以用来去帮我们拷贝那些不需要参与打包的资源文件到输出目录‌‌;
  • 又或是它可以用来去帮我们压缩我们打包结果输出的代码‌‌。

总之‌‌,有了plugin的Webpack,几乎无所不能的实现了前端工程化当中绝大多数经常用到的部分‌‌,这也正是很多初学者会有Webpack就是前端工程化的这种理解的原因‌‌。

clean-Webpack-plugin:自动清空dist目录

之前的测试中,每次都需要用户手动的删除dist目录,我们希望Webpack每次打包时,先将之前的dist目录删除,再进行打包,这里使用clean-Webpack-plugin进行处理。

同样的,需要先进行安装clean-Webpack-plugin

yarn add clean-Webpack-plugin -D

之后按照其使用方法,在lg.Webpack.js中进行插件配置。首先使用require导入clean-Webpack-plugin,其中导出东西过多,需要进行解构:const { CleanWebpackPlugin } = require('clean-Webpack-plugin')。其次每个导出对象都是一个类,都有其自己的构造函数constructor,在plugins中使用时需要new CleanWebpackPlugin。代码如下:

const path = require('path')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist'),// assetModuleFilename: "img/[name].[hash:4][ext]"},module: {rules: [{test: /\\.css$/,use: ['style-loader',{loader: 'css-loader',options: {importLoaders: 1,esModule: false}},'postcss-loader']},{test: /\\.less$/,use: ['style-loader','css-loader','postcss-loader','less-loader']},{test: /\\.(png|svg|gif|jpe?g)$/,type: 'asset',generator: {filename: "img/[name].[hash:4][ext]"},parser: {dataUrlCondition: {maxSize: 30 * 1024}}},{test: /\\.(ttf|woff2?)$/,type: 'asset/resource',generator: {filename: 'font/[name].[hash:3][ext]'}}]},plugins: [new CleanWebpackPlugin()  // 每个插件就是一个类]
}

先使用yarn build进行打包,生成dist目录,随后在dist目录中手动添加一个a.txt文件,如果再次执行yarn builda.txt被删除了,说明clean-Webpack-plugin已经正常工作了。

十八、html-webapck-plugin 使用

除了清理 dist的目录以外,‌‌还有一个非常常见的需求就是自动去生成使用打包结果的HTML,在这之前HTML‌‌都是通过硬编码的方式单独去存放在项目根目录下的。‌‌

index.html每次打包完成之后手动需要修改title,以及打包产生的文件由于分包过后文件类型或者数量比较多,需要用户手动的进行修改,这些行为都可以通过html-Webpack-plugin进行处理

默认情况下,不需要手动创建index.html文件,Webpack在使用html-Webpack-plugin插件后会默认在打包结果dist目录自动创建index.html文件。

首先手动删除准备好的index.html,没有使用html-Webpack-plugin插件时,执行yarn build进行打包,通过观察发现dist目录中并没有生成index.html文件。

1 使用默认index.html模板

在配置文件中,首先导入html-Webpack-plugin。

const HtmlWebpackPlugin = require(‘html-Webpack-plugin’)

在plugins字段中进行使用:

const HtmlWebpackPlugin = require('html-Webpack-plugin')
...
plugins: [new HtmlWebpackPlugin()
]
...

此时进行yarn build打包处理,可以发现dist目录中已经有了index.html文件。
此时index.html内容是html-Webpack-plugin默认提供的,可以在node_modules中找到html-Webpack-plugin中的default_index.ejs查看。

2 使用自定义index.html模板

其中默认模板中的占位符在官方文档中有详细描述。
对于占位符,我们可以在plugin中进行传参,赋予其默认值。

new HtmlWebpackPlugin({title: 'html-Webpack-plugin',  // title占位符
})
const path = require('path')
const { DefinePlugin } = require('Webpack')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist'),// assetModuleFilename: "img/[name].[hash:4][ext]"},module: {rules: [{test: /\\.css$/,use: ['style-loader',{loader: 'css-loader',options: {importLoaders: 1,esModule: false}},'postcss-loader']},{test: /\\.less$/,use: ['style-loader','css-loader','postcss-loader','less-loader']},{test: /\\.(png|svg|gif|jpe?g)$/,type: 'asset',generator: {filename: "img/[name].[hash:4][ext]"},parser: {dataUrlCondition: {maxSize: 30 * 1024}}},{test: /\\.(ttf|woff2?)$/,type: 'asset/resource',generator: {filename: 'font/[name].[hash:3][ext]'}}]},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({title: 'html-Webpack-plugin',  // title占位符})]
}

再次yarn build进行打包后,index.html的title已经更新了。
此时我们使用的是html-Webpack-plugin内置的html模板文件。但是在实际使用过程中,我们可能需要使用特殊的模板文件。此时使用template字段去定义自己的index.html模板。
webpack 5 实战(1)
此时使用yarn build打包后,就会使用自定义的index.html模板文件。
此时,网站图标的路径使用<link rel="icon" href="<%= BASE_URL %>favicon.ico">,再使用DefinePlugin(Webpack默认,无需安装)进行定义全局配置的常量。

new DefinePlugin({BASE_URL: '"./"'
})

此时,完整的配置文件如下:

const path = require('path')
const { DefinePlugin } = require('Webpack')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')module.exports = {entry: './src/index.js',output: {filename: 'main.js',path: path.resolve(__dirname, 'dist'),// assetModuleFilename: "img/[name].[hash:4][ext]"},module: {rules: [{test: /\\.css$/,use: ['style-loader',{loader: 'css-loader',options: {importLoaders: 1,esModule: false}},'postcss-loader']},{test: /\\.less$/,use: ['style-loader','css-loader','postcss-loader','less-loader']},{test: /\\.(png|svg|gif|jpe?g)$/,type: 'asset',generator: {filename: "img/[name].[hash:4][ext]"},parser: {dataUrlCondition: {maxSize: 30 * 1024}}},{test: /\\.(ttf|woff2?)$/,type: 'asset/resource',generator: {filename: 'font/[name].[hash:3][ext]'}}]},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({title: 'html-Webpack-plugin',template: './public/index.html'}),new DefinePlugin({BASE_URL: '"./"'  // Webpack会将常量原封不动的拿走,所以需要使用引号包裹})]
}

再次进行打包后,结果如下:
除了自定义输出文件的内容,同时输出多个页面文件也是一个非常常见的需求。其实配置非常简单,配置文件中添加一个新的HtmlWebpackPlugin对象,配置如下:

  plugins: [new CleanWebpackPlugin(),// 用于生成 index.htmlnew HtmlWebpackPlugin({title: 'Webpack Plugin Sample',meta: {viewport: 'width=device-width'},template: './src/index.html'}),// 用于生成 about.htmlnew HtmlWebpackPlugin({filename: 'about.html',  // 用于指定生成的文件名称,默认值是index.htmltitle: 'About html'})]

十九、copy-Webpack-plugin

在项目中,一般还有一些不需要参与构建的静态文件,‌‌它们最终也需要发布到线上,‌‌例如我们网站的favicon.icon,‌‌一般会把这一类的文件统一放在项目的public目录当中,‌‌希望Webpack在打包时,可以一并将它们复制到输出目录。

‌‌对于这种需求,可以借助于copy-Webpack-plugin,‌‌先安装一下这个插件‌‌,然后再去导入这个插件的类型,‌‌最后同样在这个plugin属性当中去添加一个这个类型的实例,‌‌这类型的构造函数它要求传入一个数组,‌‌用于去指定需要去拷贝的文件路径,它可以是一个通配符,也可以是一个目录或者是文件的相对路径,‌‌这里使用plugin,‌‌它表示在打包时会将所有的文件全部拷贝到输出目录,‌‌再次运行Webpack指令,‌‌打包完成过后,public目录下所有的文件就会同时拷贝到输出目录。

const path = require('path')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')
const CopyWebpackPlugin = require('copy-Webpack-plugin')module.exports = {mode: 'none',entry: './src/main.js',output: {filename: 'bundle.js',path: path.join(__dirname, 'dist'),// publicPath: 'dist/'},module: {rules: [{test: /.css$/,use: ['style-loader','css-loader']},{test: /.png$/,use: {loader: 'url-loader',options: {limit: 10 * 1024 // 10 KB}}}]},plugins: [new CleanWebpackPlugin(),// 用于生成 index.htmlnew HtmlWebpackPlugin({title: 'Webpack Plugin Sample',meta: {viewport: 'width=device-width'},template: './src/index.html'}),// 用于生成 about.htmlnew HtmlWebpackPlugin({filename: 'about.html'}),new CopyWebpackPlugin({patterns: [{from: 'public',globOptions: {ignore: ['**/index.html']  // 必须写入**/, ** 两个星号的意思是在当前路径}}]})]
}

二十、babel 使用

由于Webpack默认就能处理代码当中的importexport,‌‌所以很自然都会有人认为Webpack会自动编译的ES6代码。实则不然,‌‌那是Webpack的仅仅是对模块去完成打包工作,‌‌所以说它才会对代码当中的import和export做一些相应的转换,‌‌它并不能去转换我们代码当中其它的ES6特性。

如果需要将ES6的代码打包并编译为ES5的代码,需要一些其它的编译形加载器。这里安装一些额外的插件。

首先,Webpack是可以识别ES6+的语法的,这里来测试一下,在index.js中写入ES6+的语法,使用yarn build进行打包,观察打包过后的代码可以发现,Webpack原封不动的把index.js中的ES6+语法代码拿了过来,并没有进行任何处理。

所以针对ES6+语法,需要使用特殊工具进行处理,这里安装@babel/core以及命令行工具@babel/cli进行代码测试,看babel默认是否会帮助处理ES6+语法。

yarn add @babel/core @babel/cli
yarn babel 

使用后发现,babel仍然没有帮我们处理ES6+语法,这是为什么呢?原因是babel还需要使用特殊插件进行处理。

yarn babel 目标路径 --out-put 输出路径

yarn babel src --out-put build

因此,我们需要特殊的插件来对箭头函数或者const、let关键字进行处理。

  • @babel/plugin-transform-arrow-functions(处理箭头函数)
  • @babel/plugin-transform-block-scoping(处理块级作用域)
yarn add @babel/plugin-transform-arrow-functions @babel/plugin-transform-block-scoping
# 执行babel
yarn babel src --out-dir build --plugins=@babel/plugin-transform-arrow-functions,@babel/plugin-transform-block-scoping

重新执行后,发现箭头函数和letconst关键字作用域已经被处理成var关键字。

但是我们发现,每次需要处理不同的特殊情况,都需要安装不同的babel插件,特别不方便。因此babel将绝大多数有关ES6+语法以及stage草案的插件组合成一个集合@babel/preset-env,以后只需要使用这一个集合就可以处理绝大多数的ES6+语法。

# 安装@babel/preset-env
yarn add @babel/preset-env
# 使用babel进行编译
yarn babel src --out-dir build --presets=@babel/preset-env

清水丽人