# Webpack

Webpack初识

认识Webpack

  • 事实上随着前端的快速发展,目前前端的开发已经变的越来越复杂了:

    • 比如开发过程中我们需要通过模块化的方式来开发;
    • 比如也会使用一些高级的特性来加快我们的开发效率或者安全性,比如通过 ES6+、TypeScript 开发脚本逻辑,通过sass、less 等方式来编写 css 样式代码;
    • 比如开发过程中,我们还希望实时的监听文件的变化来并且反映到浏览器上,提高开发的效率;
    • 比如开发完成后我们还需要将代码进行压缩、合并以及其他相关的优化;
    • 等等….
  • 但是对于很多的前端开发者来说,并不需要思考这些问题,日常的开发中根本就没有面临这些问题:

    • 这是因为目前前端开发我们通常都会直接使用三大框架来开发:Vue、React、Angular;
    • 但是事实上,这三大框架的创建过程我们都是借助于脚手架(CLI)的;
    • 事实上 Vue-CLI、create-react-app、Angular-CLI 都是基于 webpack 来帮助我们支持模块化、less、 TypeScript、打包优化等的;

常见框架的脚手架

  • 事实上我们上面提到的所有脚手架都是依赖于 webpack 的:

1663588762851

Webpack的作用

  • 官方的解释:webpack is a static module bundler for modern JavaScript applications.
    • webpack 是一个静态的模块化打包工具,为现代的 JavaScript 应用程序;
  • 我们来对上面的解释进行拆解:
    • 打包 bundler:webpack 可以将帮助我们进行打包,所以它是一个打包工具
    • 静态的 static:这样表述的原因是我们最终可以将代码打包成最终的静态资源(部署到静态服务器);
    • 模块化 module:webpack 默认支持各种模块化开发,ES Module、CommonJS、AMD等;
    • 现代的 modern:我们前端说过,正是因为现代前端开发面临各种各样的问题,才催生了 webpack 的出现和发展;

1663588868726

Vue项目的打包内容

  • JavaScript 的打包:

    • 将 ES6 转换成 ES5 的语法;

    • TypeScript 的处理,将其转换成 JavaScript;

  • CSS 的处理:

    • CSS文件模块的加载、提取;
    • Less、Sass等预处理器的处理;
  • 资源文件 img、font:

    • 图片 img 文件的加载;
    • 字体 font 文件的加载;
  • HTML 资源的处理:

    • 打包 HTML 资源文件;
  • 处理 vue 项目的 SFC 文件 .vue 文件;

webpack的使用前提

  • webpack 的官方文档是 : https://webpack.js.org/

  • webpack 的中文官方文档是https://webpack.docschina.org/

    • DOCUMENTATION:文档详情,也是我们最关注的
  • Webpack 的运行是依赖 Node 环境的,所以我们电脑上必须有 Node 环境

    • 所以我们需要先安装 Node.js,安装 node 的同时也会安装 npm;
      • 查看 node 是否安装成功以及查看 node 版本,终端中输入指令 : node –version , 出现 node 版本即为安装成功
      • 查看 npm 版本 : npm –version
    • 我当前电脑上的 node 版本是 v14.15.5,npm 版本是 6.14.11(你也可以使用 nvm 来管理Node版本);
  • Node官方网站:https://nodejs.org/

1663589375701

webpack的安装

  • webpack 的安装目前分为两个:webpack、webpack-cli
  • 那么它们是什么关系呢?
    • 执行 webpack 命令,会执行 node_modules 下的 .bin 目录下的 webpack;
    • webpack 在执行时是依赖 webpack-cli 的,如果没有安装就会报错;
    • 而 webpack-cli 中代码执行时,才是真正利用 webpack 进行编译和打包的过程;
    • 所以在安装 webpack 时,我们需要同时安装 webpack-cli
      • 第三方的脚手架事实上是没有使用 webpack-cli 的,而是类似于自己的 vue-service-cli 的东西

1663589730730

1
2
3
npm install webpack webpack-cli –g # 全局安装

npm install webpack webpack-cli –D # 局部安装

Webpack的默认打包

Webpack的默认打包

./src/js/math.js

1
2
3
4
// ES Module
export function sum(num1, num2) {
return num1 + num2
}

./src/js/format.js

1
2
3
4
5
6
7
8
// CommomJS
const priceFormat = function () {
return "¥99.88"
}

module.exports = {
priceFormat
}

./src/index.js

1
2
3
4
5
import { sum } from './js/math'
const { priceFormat } = require('./js/format')

console.log(sum(20, 30))
console.log(priceFormat())
  • 01 我们可以通过 webpack 进行打包,之后运行打包之后的代码
    • 在目录下直接执行命令: webpack ,就会执行默认打包,使用的是全局的 webpack
  • 02 执行上面的命令后,生成一个 dist 文件夹,里面存放一个 main.js 的文件,就是我们打包之后的文件:
    • 这个文件中的代码被压缩和丑化了;
    • 在项目中使用 ./index.html 文件,在 body 结束标签之前,引入该 main.js 即可在浏览器中执行
    • 另外我们发现代码中依然存在 ES6 的语法,比如箭头函数、const 、let 等,这是因为默认情况下 webpack 并不清楚我们打包后的文件是否需要转成 ES5 之前的语法,后续我们需要通过 babel 来进行转换和设置;
  • 03 我们发现是可以正常进行打包的,但是有一个问题,webpack 是如何确定我们的入口的呢?
    • 事实上,当我们运行 webpack 命令时,webpack 会查找当前目录下的 ./src/index.js 作为入口;
    • 所以,如果当前项目中没有存在 ./src/index.js 文件,那么会报错;
  • 当然,我们也可以通过配置来指定入口和出口
    • 指定入口文件为 ./src/main.js , 打包输入目录为 ./build
1
npx webpack --entry ./src/main.js --output-path ./build  

创建局部Webpack

  • 前面我们直接执行webpack命令使用的是全局的webpack,如果希望使用局部的可以按照下面的步骤来操作。
  • 第一步:执行命令 npm init 或者 npm init -y 创建 package.json 文件,用于管理项目的信息、库依赖等
  • 第二步:安装局部的 webpack
1
npm install webpack webpack-cli -D
  • 第三步:使用局部的 webpack 执行打包, 命令 : npx webpack , npx 命令是有 npm 提供的,无需另外安装
  • 第四步:在 package.json 中创建以下 scripts 脚本,执行脚本命令 npm run build 打包即可
1
2
3
4
// package.json
"scripts": {
"build":"webpack", // 新增
},

Webpack的配置文件

  • 在通常情况下,webpack 需要打包的项目是非常复杂的,并且我们需要一系列的配置来满足要求,默认配置必然是不可以的。
  • 我们可以在根目录下创建一个 webpack.config.js 文件,来作为 webpack 的配置文件:
    • 配置文件的文件名规定默认就是叫 webpack.config.js , 如果配置文件是其他名称, 需要另外配置命令专门指定
1
2
3
4
5
6
7
8
9
10
11
const path = require('path') // 导入node的内置path模块

module.exports = {
// 打包入口文件
entry: './src/main.js',
// 打包出口
output: {
path: path.resolve(__dirname, './build'), // 出口目录
filename: 'bundle.js', // 出口文件名
},
}
  • 继续执行 npm run build 命令,依然可以正常打包

指定配置文件

  • 但是如果我们的配置文件并不是 webpack.config.js 的名字,而是其他的名字呢?
    • 比如我们将 webpack.config.js 修改成了 wk.config.js
    • 这个时候我们可以通过 --config 来指定对应的配置文件;
      • 在执行打包的同时指定配置文件, 命令 : webpack –config wk.config.js
  • 但是每次这样执行命令来对源码进行编译,会非常繁琐,所以我们可以在 package.json 文件中增加一个新的脚本:
1
2
3
"scripts": {
"build":"webpack --config wk.config.js",// 新增
},
  • 之后我们执行 npm run build 来打包即可。

Webpack的依赖图

  • webpack 到底是如何对我们的项目进行打包的呢?
    • 01 事实上 webpack 在处理应用程序时,它会根据命令或者配置文件找到入口文件
    • 02 从入口开始,会生成一个依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块(比如 .js 文件、css 文件、图片、字体等);
    • 03 然后遍历图结构,打包一个个模块(根据文件的不同使用不同的 loader 来解析);

1663591539500

webpack打包案例

  • 我们创建一个 element.js
    • 通过 JavaScript 创建了一个元素,并且希望给它设置一些样式;

./src/js/element.js

1
2
3
4
5
6
7
import "../css/style.css"

const divEl = document.createElement('div')
divEl.className = 'title'
divEl.innerHtml = '你好,李银河'

document.body.appendChild(divEl)

./src/css/style.css

1
2
3
4
5
.title {
color: red;
font-weight: 700;
font-size: 30px;
}

./src/index.js

1
import "./js/element"

执行打包命令: npm run build , 此时会报错模块解析失败,需要 loader 解析模块, 解决方法见 CSS-loader 章节

1663643517400

CSS与Less的打包

CSS-loader

安装css-loader

  • 上面的错误信息告诉我们需要一个 loader 来加载这个 css 文件,但是 loader 是什么呢?
    • loader 可以用于对模块的源代码进行转换;
    • 我们可以将 css 文件也看成是一个模块,我们是通过 import 来加载这个模块的;
    • 在加载这个模块时,webpack 其实并不知道如何对其进行加载,我们必须制定对应的 loader 来完成这个功能;
  • 那么我们需要一个什么样的 loader 呢?
    • 对于加载 css 文件来说,我们需要一个可以读取 css 文件的 loader;
    • 这个 loader 最常用的是 css-loader;
  • css-loader 的安装:npm install css-loader -D

配置css-loader

  • 如何使用这个 loader 来加载 css 文件呢?有三种方式:

    • 内联方式, (不方便管理,不推荐使用);
    • CLI方式( webpack5 中不再使用);
    • 配置方式 (推荐使用);
  • 01 内联方式:内联方式使用较少,因为不方便管理;

    • 在引入的样式前加上使用的 loader,并且使用 ! 分割;
1
import "css-loader!../css/style.css"
  • 02 CLI 方式

    • 在 webpack5 的文档中已经没有了 --module-bind
    • 实际应用中也比较少使用,因为不方便管理;
  • 03 配置方式

    • 表示的意思是在我们的 webpack.config.js 文件中写明配置信息:
      • module.rules 中允许我们配置多个 loader(因为我们也会继续使用其他的 loader,来完成其他文件的加载);
      • 这种方式可以更好的表示 loader 的配置,也方便后期的维护,同时也让你对各个 Loader 有一个全局的概览;
    • module.rules 的配置如下:
      • rules属性对应的值是一个数组:[Rule]
      • 数组中存放的是一个个的Rule,Rule是一个对象,对象中可以设置多个属性:
        • test 属性:用于对 resource(资源)进行匹配的,通常会设置成正则表达式;
        • use 属性:对应的值时一个数组:[UseEntry]
          • UseEntry 是一个对象,可以通过对象的属性来设置一些其他属性
            • loader:必须有一个 loader属性,对应的值是一个字符串;
            • options:可选的属性,值是一个字符串或者对象,值会被传入到loader中;
            • query:目前已经使用 options 来替代;
          • 传递字符串(如:use: [ ‘style-loader’ ])是 loader 属性的简写方式(如:use: [ { loader: ‘style-loader’} ]);
      • loader属性: Rule.use: [ { loader } ] 的简写。
    • webpack.config.js :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const path = require('path') // 导入node的内置path模块

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './build'), // 出口目录
filename: 'bundle.js', // 出口文件名
},
// 模块
module: {
rules: [
//匹配处理 .CSS 文件
{
test: /\.css$/,
// loader: 'css-loader', // 简写,写法一
// use: ["css-loader"], // 写法二
// 写法三: 完整写法
use: [
{
loader: 'css-loader',
},
],
},
],
},
}
  • 配置好 CSS-loader 后,即可执行 npm run build 打包 .css 文件,但是此时 CSS 样式依然还是未生效,解决方法参考 style-loader 章节

style-loader

安装style-loader

  • 我们已经可以通过 css-loader 来加载 css 文件了
    • 但是你会发现这个 css 在我们的代码中并没有生效(页面没有效果)。
  • 这是为什么呢?
    • 因为 css-loader 只是负责将 .css 文件进行解析,并不会将解析之后的 css 插入到页面中;
    • 如果我们希望再完成插入 style 的操作,那么我们还需要另外一个 loader,就是 style-loader;
  • 安装 style-loader:npm install style-loader -D

配置style-loader

  • 那么我们应该如何使用 style-loader:
    • 在配置文件中,添加 style-loader;
    • 注意:因为 loader 的执行顺序是从右向左(或者说从下到上,或者说从后到前的),所以我们需要将 style-loader写到 css-loader 的前面;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path') // 导入node的内置path模块

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './build'), // 出口目录
filename: 'bundle.js', // 出口文件名
},
// 模块
module: {
rules: [
//匹配处理 .CSS 文件
{
test: /\.css$/,
use: ['style-loader', 'css-loader'], // 只需要在 css-loader 的前面添加 style-loader 即可
},
],
},
}
  • 重新执行编译 npm run build,可以发现打包后的 css 已经生效了:
    • 当前我们的 css 是通过页内样式的方式添加进来的;
    • 后续我们也会讲如何将 css 抽取到单独的文件中,并且进行压缩等操作;

less-loader

处理less工具

  • 在我们开发中,我们可能会使用less、sass、stylus的预处理器来编写css样式,效率会更高。
  • 那么,如何可以让我们的环境支持这些预处理器呢?
    • 首先我们需要确定,less、sass 等编写的 css 需要通过工具转换成普通的 css;
  • 比如我们编写如下的 less 样式:

./src/css/title.less

1
2
3
4
5
6
@bgColor: pink;
@textDecoration: underline;
.title {
background-color: @bgColor;
text-decoration: @textDecoration;
}

./src/js/element.js , 将 less 文件添加到依赖关系图

1
2
3
4
5
6
7
8
9
import "../css/style.css"
import "../css/title.less"

const divEl = document.createElement('div')

divEl.className = 'title'
divEl.innerHTML = '你好,李银河'

document.body.appendChild(divEl)
  • 我们可以使用 less 工具来完成它的编译转换:
    • less 工具是一个独立的工具,跟 webpack 没有关系 , lessc 是安装 less 后,由 less 提供的
    • 安装 less : npm install less -D
    • 执行命令: npx lessc ./src/css/title.less title.css , 将 title.less 编译成 title.css

使用less-loader

  • 但是在项目中我们会编写大量的 css,它们如何可以自动转换呢?

    • 这个时候我们就可以使用 less-loader,来自动使用 less 工具转换 less 到 css;
    • less-loader 的原理很简单,就是调用 less 库提供的方法,转译 less 语法后输出
  • 01 安装 less-loader : npm install less less-loader -D

    • 使用 less-loader,也需要安装 less
  • 02 在 webpack.config.js 中配置 less-loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const path = require('path') // 导入node的内置path模块

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './build'), // 出口目录
filename: 'bundle.js', // 出口文件名
},
// 模块
module: {
rules: [
//匹配处理 .CSS 文件
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
//匹配处理 .less 文件
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
],
},
}
  • 03 执行 npm run build 执行打包, less 就可以自动转换成 css,并且页面也会生效了

postcss-loader

认识postCSS工具

  • 什么是 PostCSS 呢?
    • PostCSS 是一个通过 JavaScript 来转换样式的工具;
    • 这个工具可以帮助我们进行一些 CSS 的转换和适配,比如自动添加浏览器前缀、css 样式的重置;
    • 但是实现这些功能,我们需要借助于 PostCSS 对应的插件;
  • 如何使用 PostCSS 呢?主要就是两个步骤:
    • 第一步:查找 PostCSS 在构建工具中的扩展,比如 webpack 中的 postcss-loader;
    • 第二步:选择可以添加你需要的 PostCSS 相关的插件;

使用postCSS

  • 当然,我们能不能也直接在终端使用 PostCSS 呢?

    • 也是可以的,但是我们需要单独安装一个工具 postcss-cli;
  • 我们可以安装一下它们:postcss、postcss-cli

    • 命令 : npm install postcss postcss-cli -D
  • 我们编写一个需要添加前缀的 css:

    • https://autoprefixer.github.io/
    • 我们可以在上面的网站中查询一些添加 css 属性的样式,并将该 css 文件添加到关系依赖图(即在某个文件中导入)
1
2
3
.title {
user-select: none;
}

插件autoprefixer

  • 因为我们需要自动添加前缀,所以要安装 autoprefixer:npm install autoprefixer -D

  • 直接使用使用 postcss 工具,并且制定使用 autoprefixer :

    • npx postcss –use autoprefixer -o ./src/css/end.css ./src/css/style.css
    • 通过 postcss ,使用 autoprefixer 插件,给 ./src/css/style.css 添加前缀,输出到 ./src/css/end.css
    • 处理结果如下:
1
2
3
4
5
6
7
.title {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}

/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QuY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO0VBQ0UseUJBQWlCO0tBQWpCLHNCQUFpQjtVQUFqQixpQkFBaUI7QUFDbkIiLCJmaWxlIjoiZW5kLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIi50aXRsZSB7XHJcbiAgdXNlci1zZWxlY3Q6IG5vbmU7XHJcbn1cclxuIl19 */

postcss-loader

  • 真实开发中我们必然不会直接使用命令行工具来对 css 进行处理,而是可以借助于构建工具:

    • 在 webpack 中使用 postcss 就是使用 postcss-loader 来处理的;
  • 我们来安装 postcss-loader:npm install postcss-loader -D

  • 我们修改加载 css 的 loader:(配置文件已经过多,给出一部分了)

    • 注意:因为 postcss 需要有对应的插件才会起效果,所以我们需要配置它的 plugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const path = require('path') // 导入node的内置path模块

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './build'), // 出口目录
filename: 'bundle.js', // 出口文件名
},
// 模块
module: {
rules: [
//匹配处理 .CSS 文件
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{ // 若将插件抽离出去成独立文件,则此处只需要像 css-loader 这样的格式添加 postcss-loader 即可
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('autoprefixer')],
},
},
},
],
},
//匹配处理 .less 文件
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
],
},
}

单独的postcss配置

  • 当然,我们也可以将这些配置信息放到一个单独的文件中进行管理:
    • 在根目录下创建 postcss.config.js
      • 打包时会先去 webpack.config.js 中查看配置,有就使用,没有再去 postcss.config.js 中查看配置

postcss.config.js

1
2
3
module.exports = {
plugins: [require('autoprefixer')],
}

postcss-preset-env

  • 事实上,在配置 postcss-loader 时,我们配置插件并不需要使用 autoprefixer。
  • 我们可以使用另外一个插件:postcss-preset-env
    • postcss-preset-env 也是一个 postcss 的插件;
    • 它可以帮助我们将一些现代的 CSS 特性,转成大多数浏览器认识的 CSS,并且会根据目标浏览器或者运行时环境添加所需的 polyfill;
    • 也包括会自动帮助我们添加 autoprefixer(所以相当于已经内置了 autoprefixer );
  • 首先,我们需要安装 postcss-preset-env:npm install postcss-preset-env -D
  • 之后,我们直接修改掉之前 postcss.config.js 文件中的 autoprefixer 即可:
    • 注意:我们在使用某些 postcss 插件时,也可以直接传入字符串
    • 该插件也可以将十六进制的颜色值,转换为 rgb 值,测试时数值只能是 6 位或者 8 位 , 其他位数编辑器会报错,浏览器报警告属性值无效

postcss.config.js

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

Webpack打包静态资源

打包图片资源

  • 为了演示我们项目中可以加载图片,我们需要在项目中使用图片,比较常见的使用图片的方式是两种:
    • img 元素,设置 src 属性;
    • 其他元素(比如 div ),设置 background-image 的 css 属性;
1
2
3
4
5
6
// element.js
import '../css/image.css'

// 设置背景图片
const bgDivEl = document.createElement("div")
bgDivEl.className = "image-bg"
1
2
3
4
5
6
7
/** image.css */
.image-bg {
width: 300px;
height: 200px;
background: url('../img/01.jpg') no-repeat;
background-size: 100% 100%;
}
  • 这个时候,打包会报错
    • (测试时直接就打包过去了,并没有报错,也可以正常显示在页面中,并没有配置 file-loader ,与视频中的操作不同)

file-loader

file-loader的作用

  • 要处理 jpg、png 等格式的图片,我们也需要有对应的 loader:file-loader
  • file-loader 的作用就是帮助我们处理 import/require() 方式引入的一个文件资源,并且会将它放到我们输出的文件夹中;
    • 如果通过原生JS给img添加图片,需要将图片作为一个模块导入,不能直接赋值图片的url地址
  • 当然我们待会儿可以学习如何修改它的名字和所在文件夹;

file-loader的安装

  • 命令 : npm install file-loader -D

file-loader的配置

  • 配置处理图片的 Rule:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const path = require('path') // 导入node的内置path模块

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './build'), // 出口目录
filename: 'bundle.js', // 出口文件名
},
// 模块
module: {
rules: [
//匹配处理其他资源....

//匹配处理图片资源
{
test: /\.(jpg|jpeg|gif|png|svg)$/,
use: [
{
loader: 'file-loader',
options: {
esModule: false,// webpack5 中必须要添加这两项,否则打包的css文件中的背景图片无法显示在页面中
},
},
],
type: 'javascript/auto',// webpack5 中必须要添加这两项,否则打包的css文件中的背景图片无法显示在页面中
},
],
},
}
  • 注意点: 如果通过原生 JS 给 img 添加图片,需要将图片作为一个模块导入,不能直接赋值图片的 url 地址,否则图片可以打包但不能显示在页面中
1
2
3
4
5
6
7
8
// element.js
import meinvImg from "../img/02.jpg"

// 创建 img 标签
const imgEl = document.createElement("img")
imgEl.src = meinvImg

document.body.appendChild(imgEl)

文件的命名规则

  • 有时候我们处理后的文件名称按照一定的规则进行显示:

    • 比如保留原来的文件名、扩展名,同时为了防止重复,包含一个 hash 值等;
  • 这个时候我们可以使用 PlaceHolders 来完成,webpack 给我们提供了大量的 PlaceHolders 来显示不同的内容:

  • 我们这里介绍几个最常用的 placeholder:

    • [ext]:处理文件的扩展名;
    • [name]:处理文件的名称;
    • [hash]:文件的内容,使用 MD4 的散列函数处理,生成的一个 128 位的 hash 值(32 个十六进制);
    • [contentHash]:在 file-loader 中和 [hash] 结果是一致的(在 webpack 的一些其他地方不一样,后面会讲到);
    • [hash:length]:截图 hash 的长度,默认 32 个字符太长了;
    • [path]:文件相对于 webpack 配置文件的路径;

设置文件的名称

  • 那么我们可以按照如下的格式编写:
    • 这个也是 vue 的写法;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//匹配图片资源
{
test: /\.(jpg|jpeg|gif|png|svg)$/,
use: [
{
loader: 'file-loader',
options: {
esModule: false,
// outputPath: 'img',//指定将图片打包到输出到 dist 中的 img 文件夹,也可以直接写到 name 属性中
name: 'img/[name]_[hash:6].[ext]', // 指定输出的文件名为原文件名,6位hash值以及原文件的扩展名
},
},
],
type: 'javascript/auto',
},

文件的存放路径

  • 当然,我们刚才通过 img/ 已经设置了文件夹,这个也是 vue、react 脚手架中常见的设置方式:
    • 其实按照这种设置方式就可以了;
    • 当然我们也可以通过 outputPath 来设置输出的文件夹;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 模块
module: {
rules: [
//...

//匹配图片资源
{
test: /\.(jpg|jpeg|gif|png|svg)$/,
use: [
{
loader: 'file-loader',
options: {
esModule: false,
// outputPath: 'img',//指定将图片打包到输出到 dist 中的 img 文件夹,也可以直接写到 name 属性中
name: 'img/[name]_[hash:6].[ext]', // 指定输出的文件名为原文件名,6位hash值以及原文件的扩展名
},
},
],
type: 'javascript/auto',
},
]
}

url-loader

  • url-loade 和 file-loader 的工作方式是相似的,但是可以将较小的文件,转成 base64 的 URI
    • 就是会将体积较小的文件,会以 base64 编码打包压缩到出口文件 ./dist/bundle.js 文件中

url-loader的安装

  • 安装 url-loader:npm install url-loader -D

url-loader的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 模块
module: {
rules: [
//...

//匹配图片资源
{
test: /\.(jpg|jpeg|gif|png|svg)$/,
use: [
{
loader: 'url-loader',
options: {
esModule: false,
// outputPath: 'img',//指定将图片打包到输出到 dist 中的 img 文件夹,也可以直接写到 name 属性中
name: 'img/[name]_[hash:6].[ext]', // 指定输出的文件名为原文件名,6位hash值以及原文件的扩展名
limit: 100 * 1024, // 小于 100k 的文件会打包压缩到出口文件 bundle.js 中,大于的直接打包不压缩
},
},
],
type: 'javascript/auto',
},
]
}
  • 显示结果是一样的,并且图片可以正常显示;

  • 但是在 dist 文件夹中,我们会看不到图片文件:

  • 这是因为我的两张图片的大小分别是 38kb 和 295kb;

  • 默认情况下 url-loader 会将所有的图片文件转成 base64 编码

url-loader的limit

  • 但是开发中我们往往是小的图片需要转换,但是大的图片直接使用图片即可
    • 这是因为小的图片转换 base64 之后可以和页面一起被请求,减少不必要的请求过程;
    • 而大的图片也进行转换,反而会影响页面的请求速度;
  • 那么,我们如何可以限制哪些大小的图片转换和不转换呢?
    • url-loader 有一个 options 属性 limit,可以用于设置转换的限制;
    • 下面的代码 38kb 的图片会进行 base64 编码,而 295kb 的不会;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 模块
module: {
rules: [
//...
//匹配图片资源
{
test: /\.(jpg|jpeg|gif|png|svg)$/,
use: [
{
loader: 'url-loader',
options: {
esModule: false,
// outputPath: 'img',//指定将图片打包到输出到 dist 中的 img 文件夹,也可以直接写到 name 属性中
name: 'img/[name]_[hash:6].[ext]', // 指定输出的文件名为原文件名,6位hash值以及原文件的扩展名
limit: 100 * 1024, // 小于 100k 的文件会打包压缩到出口文件 bundle.js 中,大于的直接打包不压缩
},
},
],
type: 'javascript/auto',
},
]
}

asset module type

认识 asset module type

  • 我们当前使用的 webpack 版本是 webpack5:
    • 在webpack5 之前,加载这些资源我们需要使用一些 loader,比如raw-loader 、url-loader、file-loader;
    • 在webpack5 开始,我们可以直接使用资源模块类型(asset module type),来替代上面的这些 loader;
  • 资源模块类型(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 module type

  • 比如加载图片,我们可以使用下面的方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 模块
module: {
rules: [
//...
//匹配图片资源
{
test: /\.(jpg|jpeg|gif|png|svg)$/,
type: 'asset', // 直接使用资源模块类型来替代 loader
generator: {
filename: 'img/[name]_[hash:6].[ext]',
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024,
},
},
},
]
}
  • 但是,如何可以自定义文件的输出路径和文件名呢?
    • 方式一:修改 output,添加 assetModuleFilename 属性;
    • 方式二:在 Rule 中,添加一个 generator 属性,并且设置 filename;
1
2
3
4
5
6
// 方式一:
output: {
path: path.resolve(__dirname, './dist'), // 出口目录
filename: 'bundle.js', // 出口文件名
assetModuleFilename: 'img/[name]_[hash:6].[ext]',
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 方式二:

// 模块
module: {
rules: [
//...
//匹配图片资源
{
test: /\.(jpg|jpeg|gif|png|svg)$/,
type: 'asset', // 直接使用资源模块类型来替代 loader
generator: {
filename: 'img/[name]_[hash:6].[ext]',
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024,
},
},
},
]
}

limit

  • 我们需要两个步骤来实现:
    • 步骤一:将 type 修改为 asset;
    • 骤二:添加一个 parser 属性,并且制定 dataUrl 的条件,添加 maxSize 属性;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 模块
module: {
rules: [
//...
//匹配图片资源
{
test: /\.(jpg|jpeg|gif|png|svg)$/,
type: 'asset', // 直接使用资源模块类型来替代 loader
generator: {
filename: 'img/[name]_[hash:6].[ext]',
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024,
},
},
},
]
}

打包字体文件

加载字体文件

  • 如果我们需要使用某些特殊的字体或者字体图标,那么我们会引入很多字体相关的文件,这些文件的处理也是一样的。
  • 首先,我从阿里图标库中下载了几个字体图标:
  • 在 component 中引入,并且添加一个 i 元素用于显示字体图标:
1
2
3
4
5
6
7
8
// element.js
import '../font/iconfont.css'

// 字体图标
const iEl = document.createElement("i")
iEl.className = "iconfont icon-duigou"

document.body.appendChild(iEl)

字体文件的打包

  • 这个时候打包会报错,因为无法正确的处理 eot、ttf、woff 等文件:
    • (测试时并没有报错,而是直接打包过去了,也可以正常显示在页面中)
      • 被打包到了 img 文件夹中,img 图片使用的是 asset 模块打包
    • 我们可以选择使用 file-loader 来处理,也可以选择直接使用 webpack5 的资源模块类型来处理;
  • 使用 file-loader 来处理:
    • 能正常打包,但是也同时打包到了 img 文件夹中,img 图片使用的是 asset 模块打包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 模块
module: {
rules: [
//...

//匹配字体资源
{
test: /\.(eot|ttf|woff2?)$/,
use: {
loader: "file-loader",
options: {
name:"font/[name]_[hash:6].[ext]"
}
}
}
]
}
  • 使用 webpack5 的资源模块类型来处理:
    • 正常打包,图片打包到 img 文件夹, 字体打包到 font 文件夹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 模块
module: {
rules: [
//...

//匹配字体资源
{
test: /\.(eot|ttf|woff2?)$/,
type: 'asset/resource',
generator: {
filename: 'font/[name]_[hash:6].[ext]',
},
},
]
}

Plugin

认识Plugin

  • Webpack的另一个核心是 Plugin,官方有这样一段对 Plugin 的描述:

    • While loaders are used to transform certain types of modules, plugins can be leveraged to perform a wider range of tasks like bundle optimization, asset management and injection of environment variables.
  • 上面表达的含义翻译过来就是:

    • Loader是用于特定的模块类型进行转换;
    • Plugin可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等;

1663663891628

CleanWebpackPlugin

  • 前面我们演示的过程中,每次修改了一些配置,重新打包时,都需要手动删除 dist 文件夹:
    • 我们可以借助于一个插件来帮助我们完成,这个插件就是 CleanWebpackPlugin;
  • 首先,我们先安装这个插件:npm install clean-webpack-plugin -D
  • 之后在配置文件中配置插件:
    • 第一步: 导入 CleanWebpackPlugin
    • 第二步: 在 plugins 中创建 CleanWebpackPlugin 类的实例对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const path = require('path') // 导入node的内置path模块
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './dist'), // 出口目录
filename: 'bundle.js', // 出口文件名
assetModuleFilename: 'img/[name]_[hash:6].[ext]',
},
// 模块
module: {
rules: [
//...
]
},
plugins: [new CleanWebpackPlugin()],
}

HtmlWebpackPlugin

  • 另外还有一个不太规范的地方:
    • 我们的HTML文件是编写在根目录下的,而最终打包的dist文件夹中是没有index.html文件的。
    • 在进行项目部署的时,必然也是需要有对应的入口文件index.html;
    • 所以我们也需要对index.html进行打包处理;
  • 对 HTML 进行打包处理我们可以使用另外一个插件:HtmlWebpackPlugin;
  • 安装插件 : npm install html-webpack-plugin -D
  • 配置插件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path') // 导入node的内置path模块
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './dist'), // 出口目录
filename: 'bundle.js', // 出口文件名
assetModuleFilename: 'img/[name]_[hash:6].[ext]',
},
// 模块
module: {
rules: [
//...
]
},
plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin()],
}

自定义HTML模板

自定义HTML模板

  • 我们会发现,现在自动在 dist 文件夹中,生成了一个 index.html 的文件:
    • 该文件中也自动添加了我们打包的 bundle.js 文件;
  • 这个文件是如何生成的呢?
    • 默认情况下是根据 ejs 的一个模板来生成的;
    • 在 html-webpack-plugin 的源码中,有一个 default_index.ejs 模块;
  • 如果我们想在自己的模块中加入一些比较特别的内容:
    • 比如添加一个 noscript 标签,在用户的 JavaScript 被关闭时,给予响应的提示;
    • 比如在开发 vue 或者 react 项目时,我们需要一个可以挂载后续组件的根标签 <div id="app"></div>
  • 这个我们需要一个属于自己的 index.html 模块:

./public/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</head>

<body>
<div class="app"></div>
</body>

</html>
  • 上面的代码中,会有一些类似这样的语法 <% 变量 %>,这个是 EJS 模块填充数据的方式。
  • 在配置 HtmlWebpackPlugin 时,我们可以添加如下配置:
    • template:指定我们要使用的模块所在的路径;
    • title:在进行 htmlWebpackPlugin.options.title 读取时,就会读到该信息;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const path = require('path') // 导入node的内置path模块
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './dist'), // 出口目录
filename: 'js/bundle.js', // 出口文件名
assetModuleFilename: 'img/[name]_[hash:6].[ext]',
},
module:{
//...
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html', // 模板文件
title:"hahaha~" // 根据模板生成的 html 的 title 属性
}),
new DefinePlugin({
BASE_URL: "'./'", // 定义全局常量
}),
],
}
  • 但是,这个时候编译还是会报错,因为在我们的模块中还使用到一个 BASE_URL 的常量:

DefinePlugin的介绍

  • 这是因为在编译template模块时,有一个BASE_URL:
    • <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    • 但是我们并没有设置过这个常量值,所以会出现没有定义的错误;
  • 这个时候我们可以使用 DefinePlugin 插件;

DefinePlugin的使用

  • DefinePlugin 允许在编译时创建配置的全局常量,是一个 webpack 内置的插件(不需要单独安装):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const path = require('path') // 导入node的内置path模块
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './dist'), // 出口目录
filename: 'js/bundle.js', // 出口文件名
assetModuleFilename: 'img/[name]_[hash:6].[ext]',
},
module:{
//...
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html', // 模板文件
title:"hahaha~" // 根据模板生成的 html 的 title 属性
}),
new DefinePlugin({
BASE_URL: "'./'", // 定义全局常量
}),
],
}
  • 这个时候,编译 template 就可以正确的编译了,会读取到 BASE_URL 的值;

CopyWebpackPlugin

  • 在 vue 的打包过程中,如果我们将一些文件放到 public 的目录下,那么这个目录会被复制到 dist 文件夹中。

    • 这个复制的功能,我们可以使用 CopyWebpackPlugin 来完成;
  • 安装 CopyWebpackPlugin 插件:

    • npm install copy-webpack-plugin -D
  • 接下来配置 CopyWebpackPlugin 即可:

    • 复制的规则在 patterns 中设置;
      • from:设置从哪一个源中开始复制;
      • to:复制到的位置,可以省略,会默认复制到打包的目录下;
      • globOptions:设置一些额外的选项,其中可以编写需要忽略的文件:
        • .DS_Store:mac 目录下回自动生成的一个文件;
        • index.html:也不需要复制,因为我们已经通过 HtmlWebpackPlugin 完成了 index.html 的生成;
    • 第一步: 导入 CopyWebpackPlugin
    • 第二步: 创建 CopyWebpackPlugin 的实例对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const path = require('path') // 导入node的内置path模块
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin') // 01

module.exports = {
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, './dist'), // 出口目录
filename: 'js/bundle.js', // 出口文件名
assetModuleFilename: 'img/[name]_[hash:6].[ext]',
},
module:{
//...
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html', // 模板文件
title:"hahaha~" // 根据模板生成的 html 的 title 属性
}),
new DefinePlugin({
BASE_URL: "'./'", // 定义全局常量
}),
new CopyWebpackPlugin({ // 02
patterns: [
{
from: 'public', // 从哪里复制
to: './', // 复制到那里
globOptions: {
ignore: ['**/index.html'],//忽略那些文件不复制
},
},
],
}),
],
}

Mode与source-map

Mode

  • 前面我们一直没有讲 mode。
  • Mode 配置选项,可以告知 webpack 使用响应模式的内置优化:
    • 默认值是 production(什么都不设置的情况下);
    • 可选值有:**’none’ | ‘development’ | ‘production’**;
  • 这几个选项有什么样的区别呢?

1663679419906

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
// 模式 development,开发阶段使用 | production,生产上线阶段使用
mode: 'development',
// source-map 建立js映射文件,方便调试代码和错误
devtool: 'source-map',

// 打包入口
entry: './src/index.js',

// ...
}

source-map

  • source-map 的作用 : 建立 js 映射文件,方便调试代码和错误
1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
// 模式 development,开发阶段使用 | production,生产上线阶段使用
mode: 'development',

// source-map 建立js映射文件,方便调试代码和错误
devtool: 'source-map',

// 打包入口
entry: './src/index.js',

// ...
}

babel

babel的作用

  • 事实上,在开发中我们很少直接去接触 babel,但是 babel 对于前端开发来说,目前是不可缺少的一部分:
    • 开发中,我们想要使用 ES6+ 的语法,想要使用T ypeScript,开发 React 项目,它们都是离不开 Babel 的;
    • 所以,学习 Babel 对于我们理解代码从编写到线上的转变过程至关重要;
  • 那么,Babel 到底是什么呢?
    • Babel 是一个工具链,主要用于旧浏览器或者环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript;
    • 包括:语法转换、源代码转换等;
1
2
3
4
5
6
const message = 'Hello World'
const names = ['abc', 'cba', 'nba']

names.forEach((item) => {
console.log(item)
})

Babel命令行使用

  • babel 本身可以作为一个独立的工具(和 postcss 一样),不和 webpack 等构建工具配置来单独使用。

  • 如果我们希望在命令行尝试使用 babel,需要安装如下库:

    • @babel/core:babel 的核心代码,必须安装;
    • @babel/cli:可以让我们在命令行使用 babel;
  • 安装babel : npm install @babel/cli @babel/core -D

  • 使用 babel 来处理我们的源代码:npx babel ./src/js/demo.js –out-dir dist

    • 将指定文件通过 babel 转换后,输出到 dist 文件夹 ;
    • 如果要输出成指定文件 : npx babel ./src/js/demo.js –out-file test.js

插件的使用

  • 比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件:

  • 安装插件 : npm install @babel/plugin-transform-arrow-functions -D

  • 使用插件编译文件: npx babel demo.js –out-dir dist --plugins=@babel/plugin-transform-arrow-functions

  • 查看转换后的结果:我们会发现 const 并没有转成 var

    • 这是因为 plugin-transform-arrow-functions,并没有提供这样的功能;
    • 我们需要使用 plugin-transform-block-scoping 来完成这样的功能;
    • 安装插件 : npm install @babel/plugin-transform-block-scoping -D
  • 使用插件编译文件: npx babel demo.js –out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions

Babel的预设preset

  • 但是如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(preset):
    • 后面我们再具体来讲预设代表的含义;
  • 安装 @babel/preset-env 预设:
    • npm install @babel/preset-env -D
  • 执行如下命令编译文件:
    • npx babel src –out-dir dist --presets=@babel/preset-env
    • npx babel demo.js –out-dir dist --presets=@babel/preset-env
  • 编译后的文件默认开启严格模式

Babel的底层原理

  • babel 是如何做到将我们的一段代码(ES6、TypeScript、React)转成另外一段代码(ES5)的呢?

    • 从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?
      • 就是编译器,事实上我们可以将 babel 看成就是一个编译器。
  • Babel 编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码;

  • Babel也拥有编译器的工作流程:

    • 解析阶段(Parsing)
    • 转换阶段(Transformation)
    • 生成阶段(Code Generation)
  • https://github.com/jamiebuilds/the-super-tiny-compiler

Babel的编译流程

  • Babel的执行阶段
    • 当然,这只是一个简化版的编译器工具流程,在每个阶段又会有自己具体的工作:

1663683935940

  • 详细的编译流程:

1663683985575

Babel-loader

  • 在实际开发中,我们通常会在构建工具中通过配置 babel 来对其进行使用的,比如在 webpack 中。
  • 那么我们就需要去安装相关的依赖:
    • 如果之前已经安装了 @babel/core,那么这里不需要再次安装;
      • npm install babel-loader @babel/core -D
  • 我们可以设置一个规则,在加载 js 文件时,使用我们的 babel:我们必须指定使用的插件才会生效
1
2
3
4
5
6
7
8
9
10
11
12
13
// 匹配 js 文件
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-transform-block-scoping',
'@babel/plugin-transform-arrow-functions',
],
},
},
},

babel-preset

  • 如果我们一个个去安装使用插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset,webpack会根据我们的预设来加载对应的插件列表,并且将其传递给babel。

  • 比如常见的预设有三个:

    • env
    • preact
    • TypeScript
  • 安装 preset-env:npm install @babel/preset-env

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 匹配 js 文件
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
// plugins: [
// '@babel/plugin-transform-block-scoping',
// '@babel/plugin-transform-arrow-functions',
// ],
presets: ['@babel/preset-env'],
},
},
},

Babel的配置文件

  • 像之前一样,我们可以将 babel 的配置信息放到一个独立的文件中,babel 给我们提供了两种配置文件的编写:
    • babel.config.json(或者.js,.cjs,.mjs)文件;
    • babelrc.json(或者.babelrc,.js,.cjs,.mjs)文件;
  • 它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式( babel 本身、element-plus、umi等);
    • babelrc.json:早期使用较多的配置方式,但是对于配置 Monorepos 项目是比较麻烦的;
    • babel.config.json(babel7):可以直接作用于 Monorepos 项目的子包,更加推荐;

babel.config.js

1
2
3
module.exports = {
presets: ['@babel/preset-env'],
}

webpack.config.js

1
2
3
4
5
// 匹配 js 文件
{
test: /\.js$/,
loader: 'babel-loader',
},

对Vue文件的打包

对 vue 代码的打包

  • 安装 vue3: npm install vue@next

element.js

1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue'
const app = createApp({
template: '<h2>我是vue渲染出来的</h2>',
data() {
return {
title: 'Hello world~',
}
},
})
app.mount('#app')
  • 界面上是没有效果的:
    • 并且我们查看运行的控制台,会发现有警告信息;

对 vue 不同版本的解析

  • vue(.runtime).global(.prod).js:
    • 通过浏览器中的 <script src="..."> 直接使用;
    • 我们之前通过 CDN 引入和下载的Vue版本就是这个版本;
    • 会暴露一个全局的 Vue 来使用;
  • vue(.runtime).esm-browser(.prod).js:
    • 用于通过原生 ES 模块导入使用 (在浏览器中通过<script type="module">来使用)。
  • vue(.runtime).esm-bundler.js:
    • 用于 webpack,rollup 和 parcel 等构建工具;
    • 构建工具中默认是 vue.runtime.esm-bundler.js;
    • 如果我们需要解析模板 template,那么需要手动指定 vue.esm-bundler.js;
  • vue.cjs(.prod).js:
    • 服务器端渲染使用;
    • 通过 require() 在 Node.js 中使用;

对 template 模板的解析

  • 在 Vue 的开发过程中我们有三种方式来编写 DOM 元素:

    • 方式一:template 模板的方式(之前经常使用的方式);
    • 方式二:render 函数的方式,使用h函数来编写渲染的内容;
    • 方式三:通过.vue 文件中的 template 来编写模板;
  • 它们的模板分别是如何处理的呢?

    • 方式二中的 h 函数可以直接返回一个虚拟节点,也就是 Vnode 节点;
    • 方式一和方式三的 template 都需要有特定的代码来对其进行解析:
      • 方式三 .vue 文件中的template可以通过在 vue-loader 对其进行编译和处理;
      • 方式一中的 template 我们必须要通过源码中一部分代码来进行编译;
  • 所以,Vue 在让我们选择版本的时候分为 运行时+编译器 vs 仅运行时

    • 运行时+编译器包含了对 template 模板的编译代码,更加完整,但是也更大一些;
    • 仅运行时没有包含对 template 版本的编译代码,相对更小一些;
  • 我们会发现控制台还有另外的一个警告:

1663768532446

在 GitHub 上的文档中我们可以找到说明:

  • 这是两个特性的标识,一个是使用 Vue 的 Options,一个是 Production 模式下是否支持 devtools 工具;
  • 虽然他们都有默认值,但是强烈建议我们手动对他们进行配置;
1
2
3
4
5
6
7
8
9
plugins: [
// ...

new DefinePlugin({
BASE_URL: "'./'",
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
}),
],

对vue单文件组件的打包

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<h2 class="header">我是vue渲染出来的{{ title }}</h2>
</div>
</template>

<script>
export default {
components: {},
data() {
return { title: 'Hello world~' }
},
methods: {},
}
</script>

<style scoped>
.header {
color: red;
}
</style>
  • 我们对代码打包会报错:我们需要合适的 Loader 来处理文件。
  • 这个时候我们需要使用 vue-loader:
    • 安装 vue-loader : npm install vue-loader -D
  • 在 webpack 的模板规则中进行配置:
1
2
3
4
5
// 匹配 .vue 文件
{
test: /\.vue$/,
loader: 'vue-loader',
},
  • 打包依然会报错,这是因为我们必须添加 @vue/compiler-sfc 来对 template 进行解析:

  • 安装插件 : npm install @vue/compiler-sfc -D

  • 另外我们还需要配置对应的 Vue 插件:

1
2
3
4
5
6
const { VueLoaderPlugin } = require('vue-loader/dist/index')

plugins: [
//...
new VueLoaderPlugin(),
],

重新打包即可支持 App.vue 的写法, 另外,我们也可以编写其他的 .vue 文件来编写自己的组件;

搭建本地服务

搭建本地服务器

  • 目前我们开发的代码,为了运行需要有两个操作:
    • 操作一:npm run build,编译相关的代码;
    • 操作二:通过 live server 或者直接通过浏览器,打开 index.html 代码,查看效果;
  • 这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成 编译 和 展示;
  • 为了完成自动编译,webpack 提供了几种可选的方式:
    • webpack watch mode;
    • webpack-dev-server(常用);
    • webpack-dev-middleware;

实时自动编译

  • webpack 给我们提供了 watch 模式:
    • 在该模式下,webpack 依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译;
    • 我们不需要手动去运行 npm run build 指令了;
  • 如何开启 watch 呢?两种方式:
    • 方式一:在导出的 webpack.config.js 配置中,添加 watch: true
    • 方式二:在启动 webpack 的命令中,添加 –watch 的标识;
  • 这里我们选择方式二,在 package.json 的 scripts 中添加一个 watch 脚本
1
2
3
4
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --watch"
},

实时刷新页面

  • 上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的:

    • 当然,目前我们可以在 VSCode 中使用 live-server 来完成这样的功能;
    • 但是,我们希望在不适用 live-server 的情况下,可以具备 live reloading(实时重新加载)的功能;
  • 安装 webpack-dev-server,提供本地服务,实现实时刷新网页

  • 安装插件 : npm install webpack-dev-server -D

  • package.json 中,添加 serve 指令:

package.json

1
2
3
4
"scripts": {
"build": "webpack --watch",
"serve": "webpack serve"
},
  • webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中:
    • 事实上 webpack-dev-server 使用了一个库叫 memfs(memory-fs webpack 自己写的)

热替换HMR

认识HMR

  • 什么是 HMR 呢?
    • HMR 的全称是 Hot Module Replacement,翻译为模块热替换;
    • 模块热替换是指在 应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面;
  • HMR通过如下几种方式,来提高开发的速度:
    • 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失;
    • 只更新需要变化的内容,节省开发的时间;
    • 修改了 css、js 源代码,会立即在浏览器更新,相当于直接在浏览器的 devtools 中直接修改样式;
  • 如何使用 HMR 呢?
    • 默认情况下,webpack-dev-server 已经支持 HMR,我们只需要开启即可;
    • 在不开启 HMR 的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是 live reloading;

开启HMR

  • 修改 webpack.config.js 的配置:
1
2
3
4
5
6
7
8
// web 环境
target: 'web',

// 本地服务
devServer: {
// contentBase: './public', // 测试时添加 contentBase 会报错
hot: true, // 开启热替换
},
  • 浏览器可以看到如下效果:

  • 但是你会发现,当我们修改了某一个模块的代码时,依然是刷新的整个页面:

    • 这是因为我们需要去指定哪些模块发生更新时,进行 HMR;

index.js

1
2
3
4
5
6
import './js/element'
if (module.hot) {
module.hot.accept('./js/element.js', () => {
console.log('element模块发生了更新~~')
})
}

框架的HMR

  • 有一个问题:在开发其他项目时,我们是否需要经常手动去写入 module.hot.accpet相关的API呢?
    • 比如开发 Vue、React 项目,我们修改了组件,希望进行热更新,这个时候应该如何去操作呢?
    • 事实上社区已经针对这些有很成熟的解决方案了:
      • 比如 vue 开发中,我们使用 vue-loader,此 loader 支持 vue 组件的 HMR,提供开箱即用的体验;
      • 比如 react 开发中,有 React Hot Loader,实时调整 react 组件(目前 React 官方已经弃用了,改成使用 react-refresh);
  • 接下来我们来演示一下 Vue 实现一下 HMR 功能。

HMR的原理

  • 那么 HMR 的原理是什么呢?如何可以做到只更新一个模块中的内容呢?
    • webpack-dev-server 会创建两个服务:提供静态资源的服务(express)和 Socket 服务(net.Socket);
    • express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);
  • HMR Socket Server,是一个 socket 的长连接:
    • 长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端);
    • 当服务器监听到对应的模块发生变化时,会生成两个文件 .json(manifest文件)和 .js 文件(update chunk);
    • 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器);
    • 浏览器拿到两个新的文件后,通过 HMR runtime 机制,加载这两个文件,并且针对修改的模块进行更新;

1663753369134

主机/端口号/压缩格式

  • host 设置主机地址:

    • 默认值是 localhost;
    • 如果希望其他地方也可以访问,可以设置为 0.0.0.0;
  • localhost 和 0.0.0.0 的区别:

    • localhost:本质上是一个域名,通常情况下会被解析成 127.0.0.1
    • 127.0.0.1:回环地址 (Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收;
      • 正常的数据库包经常 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层 ;
      • 而回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的;
      • 比如我们监听 127.0.0.1时,在同一个网段下的主机中,通过ip地址是不能访问的
    • 0.0.0.0:监听IPV4上所有的地址,再根据端口找到不同的应用程序;
      • 比如我们监听 0.0.0.0时,在同一个网段下的主机中,通过ip地址是可以访问的;
  • port 设置监听的端口,默认情况下是 8080

  • open 是否打开浏览器:

    • 默认值是 false,设置为 true 会打开浏览器;
    • 也可以设置为类似于 Google Chrome等值;
  • compress 是否为静态文件开启 gzip compression:

    • 默认值是 false,可以设置为 true;

webpack.config.js

1
2
3
4
5
6
7
8
9
// 本地服务
devServer: {
// contentBase: './public',
hot: true, // 开启热替换
// host: '0.0.0.0', // 主机名
port: 7777, // 端口号
open: true, // 自动打开浏览器
compress: true, // 开启 gzip 压缩
},

Proxy

Proxy代理

  • proxy 是我们开发中非常常用的一个配置选项,它的目的设置代理来解决跨域访问的问题:
  • 我们可以进行如下的设置:
    • ptarget:表示的是代理到的目标地址,比如 /api-hy/moment 会被代理到 http://localhost:8888/api-hy/moment;
    • pathRewrite:默认情况下,我们的 /api-hy 也会被写入到 URL 中,如果希望删除,可以使用 pathRewrite;
    • secure:默认情况下不接收转发到 https 的服务器上,如果希望支持,可以设置为 false;
    • changeOrigin:它表示是否更新代理后请求的 headers 中 host 地址;
1

changeOrigin的解析

  • 这个 changeOrigin 官方说的非常模糊,通过查看源码我发现其实是要修改代理请求中的headers中的host属性:
1

historyApiFallback

  • historyApiFallback 是开发中一个非常常见的属性,它主要的作用是解决 SPA 页面在路由跳转之后,进行页面刷新时,返回 404 的错误。
  • boolean值:默认是 false
    • 如果设置为 true,那么在刷新时,返回 404 错误时,会自动返回 index.html 的内容;
  • object 类型的值,可以配置 rewrites 属性(了解):
    • 可以配置 from 来匹配路径,决定要跳转到哪一个页面;
  • 事实上 devServer 中实现 historyApiFallback 功能是通过 connect-history-api-fallback 库的:
    • 可以查看 connect-history-api-fallback 文档
1

resolve模块

resolve模块解析

  • resolve用于设置模块如何被解析:

    • 在开发中我们会有各种各样的模块依赖,这些模块可能来自于自己编写的代码,也可能来自第三方库;
    • resolve可以帮助webpack从每个 require/import 语句中,找到需要引入到合适的模块代码;
    • webpack 使用 enhanced-resolve 来解析文件路径;
  • webpack能解析三种文件路径:

  • 绝对路径

    • 由于已经获得文件的绝对路径,因此不需要再做进一步解析。
  • 相对路径

    • 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录;
    • 在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;
  • 模块路径

    • 在 resolve.modules 中指定的所有目录检索模块;
      • 默认值是 [‘node_modules’],所以默认会从 node_modules 中查找文件;
    • 我们可以通过设置别名的方式来替换初识模块路径,具体后面讲解 alias 的配置;
  • 如果是一个文件:

    • 如果文件具有扩展名,则直接打包文件;
    • 否则,将使用 resolve.extensions 选项作为文件扩展名解析;
  • 如果是一个文件夹:

    • 会在文件夹中根据 resolve.mainFiles 配置选项中指定的文件顺序查找;
      • resolve.mainFiles 的默认值是 [‘index’];
      • 再根据 resolve.extensions 来解析扩展名;

extensions和alias配置

  • extensions 是解析到文件时自动添加扩展名:
    • 默认值是 [‘.wasm’, ‘.mjs’, ‘.js’, ‘.json’];
    • 所以如果我们代码中想要添加加载 .vue 或者 jsx 或者 ts 等文件时,我们必须自己写上扩展名;
  • 另一个非常好用的功能是配置别名 alias:
    • 特别是当我们项目的目录结构比较深的时候,或者一个文件的路径可能需要 ../../../这种路径片段;
    • 我们可以给某些常见的路径起一个别名;
1
2
3
4
5
6
resolve: {
extensions: ['.js', '.json', '.mjs', '.vue'],
alias: {
'@': path.resolve(__dirname, './src'),
},
},

开发与生产环境的分离

区分开发环境

  • 目前我们所有的 webpack 配置信息都是放到一个配置文件中的:webpack.config.js
    • 当配置越来越多时,这个文件会变得越来越不容易维护;
    • 并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环境都会使用的;
    • 所以,我们最好对配置进行划分,方便我们维护和管理;
  • 那么,在启动时如何可以区分不同的配置呢?
    • 方案一:编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可;
    • 方式二:使用相同的一个入口配置文件,通过设置参数来区分它们;
1

入口文件解析

  • 我们之前编写入口文件的规则是这样的:./src/index.js,但是如果我们的配置文件所在的位置变成了 config 目录,我们是否应该变成 ../src/index.js呢?
    • 如果我们这样编写,会发现是报错的,依然要写成 ./src/index.js;
    • 这是因为入口文件其实是和另一个属性时有关的 context;
  • context 的作用是用于解析入口(entry point)和加载器(loader):
    • 官方说法:默认是当前路径(但是经过我测试,默认应该是webpack的启动目录)
    • 另外推荐在配置中传入一个值;
1

开发与生产环境的分离

  • 安装插件: npm install webpack-merge -D , 用于合并 webpack 配置

webpack.comm.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
const path = require('path') // 导入node的内置path模块

const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')

const { VueLoaderPlugin } = require('vue-loader/dist/index')

module.exports = {
// web 环境
target: 'web',
// 打包入口
entry: './src/index.js',
// 打包出口
output: {
path: path.resolve(__dirname, '../dist'), // 出口目录
filename: 'js/bundle.js', // 出口文件名
assetModuleFilename: 'img/[name]_[hash:6].[ext]',
},

resolve: {
extensions: ['.js', '.json', '.mjs', '.vue'],
alias: {
'@': path.resolve(__dirname, '../src'),
},
},

// 模块
module: {
rules: [
//匹配处理 .CSS 文件
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
// {
// loader: 'postcss-loader',
// options: {
// postcssOptions: {
// plugins: [require('autoprefixer')],
// },
// },
// },
],
},
//匹配处理 .less 文件
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
//匹配图片资源
// {
// test: /\.(jpg|jpeg|gif|png|svg)$/,
// use: [
// {
// loader: 'url-loader',
// options: {
// esModule: false,
// // outputPath: 'img',//指定将图片打包到输出到 dist 中的 img 文件夹,也可以直接写到 name 属性中
// name: 'img/[name]_[hash:6].[ext]', // 指定输出的文件名为原文件名,6位hash值以及原文件的扩展名
// limit: 100 * 1024, // 小于 100k 的文件会打包压缩到出口文件 bundle.js 中,大于的直接打包不压缩
// },
// },
// ],
// type: 'javascript/auto',
// },

//匹配图片资源
{
test: /\.(jpg|jpeg|gif|png|svg)$/,
type: 'asset',
// generator: {
// filename: 'img/[name]_[hash:6].[ext]',
// },
parser: {
dataUrlCondition: {
maxSize: 100 * 1024,
},
},
},

//匹配字体资源
// {
// test: /\.(eot|ttf|woff2?)$/,
// use: {
// loader: "file-loader",
// options: {
// name:"font/[name]_[hash:6].[ext]"
// }
// }
// }
{
test: /\.(eot|ttf|woff2?)$/,
type: 'asset/resource',
generator: {
filename: 'font/[name]_[hash:6].[ext]',
},
},

// 匹配 .js 文件
{
test: /\.js$/,
loader: 'babel-loader',
},

// 匹配 .vue 文件
{
test: /\.vue$/,
loader: 'vue-loader',
},
],
},

plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
title: 'hahaha~',
}),
new DefinePlugin({
BASE_URL: "'./'",
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
}),

new VueLoaderPlugin(),
],
}

webpack.dev.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.comm.config')

module.exports = merge(commonConfig, {
// 模式 development,开发阶段使用 | production,生产上线阶段使用
mode: 'development',
// source-map 建立js映射文件,方便调试代码和错误
devtool: 'source-map',
// 本地服务
devServer: {
// contentBase: './public',
hot: true, // 开启热替换
// host: '0.0.0.0', // 主机名
port: 7777, // 端口号
open: true, // 自动打开浏览器
compress: true, // 开启 gzip 压缩
// proxy: {
// '/api': {
// target: 'http://39.28.123.211:6666', //远程服务器的地址,写到端口号即可,发请求时填本地服务器地址,完整url地址
// pathRewrite: {
// '^/api': '',
// },
// secure: false,
// changeOrigin: true,
// },
// },
},
})

webpack.prod.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.comm.config')

module.exports = merge(commonConfig, {
// 模式 development,开发阶段使用 | production,生产上线阶段使用
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: './public',
to: './',
globOptions: {
ignore: ['**/index.html'],
},
},
],
}),
],
})