前言

若文章有误,欢迎读者留言反馈

💻Installation

1
git clone https://github.com/coding327/learn_webpack5.git

webpack插件

经过前面一系列操作基本上大部分结构都有了,但是这里还有一些不太好的地方

  1. 每次打包生成的build文件夹,下次打包时还要删掉原来的build再打包
  2. 观察我们的build文件夹,可以看到里面有fontimgmain.js,但是缺少index.html作为我们整个静态资源的入口,注意外面的index.html并不是的,因为最后部署的是build文件夹,这里的index.html还需要打包

先解决第一个问题,,每次打包时自动删除build文件夹

注意这里不是使用loaderloader仅是加载某个模块时候使用,这个时候需要的是Plugin即插件

认识Plugin

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

  • While loaders are used to transform certain types of modules, plugins can be leveraged to perform awider range of tasks like bundle optimization, asset management and injection of environment variables.
    上面表达的含义翻译过来就是∶
  • Loader是用于特定的模块类型进行转换;
  • Plugin可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等;

Plugin就相当于loader解决不了的,由Plugin来处理,插件功能非常强大,贯穿了整个webpack生命周期,很多地方都会用到插件

认识plugin

其实我们可以发现,浏览器审查这个index.html时,样式style是插入进去的,它也是可以通过插件把这个样式给它分离出去的【这里不细说】

清理打包文件夹的CleanWebpackPlugin插件

前面我们演示的过程中,每次修改了一些配置,重新打包时,都需要手动删除打包文件夹:

  • 我们可以借助于一个插件来帮助我们完成,这个插件就是CleanWebpackPlugin;

安装CleanWebpackPlugin插件

1
npm install clean-webpack-plugin -D

回到webpack.config.js文件中配置,注意这个配置不是在rules里面配置,它是在最外面引入CleanWebpackPlugin类,然后再到导出里面配置

1
2
3
4
5
6
7
8
9
10
11
12
const path = require('path')
// 我们需要从clean-webpack-plugin插件中取出CleanWebpackPlugin类,因为插件一般给它封装成class
const { CleanWebpackPlugin } = require("clean-webpack-plugin")

// webpack是运行在node环境下的,不要使用ES6 module导出【如果非要使用它还需要做额外配置】
// 注意出口文件中的path得是绝对路径,需要使用到path模块,做路径拼接即可,__dirname就是当前编写代码文件所在目录【绝对路径】
module.exports = {
...
plugins: [
new CleanWebpackPlugin() // 格式:根据CleanWebpackPlugin类创建出对象
] // plugins是个数组【这个不用管顺序】,里面放的是一个个插件对象【其实它源码里面是拿到我们导出的这个大的对象,然后去取到我们所有的plugins,之后对它做了个for循环,for循环后对它做个注入,到时候就可以根据不同的hook的生命周期来回调这个插件里面对象的某个方法】
}

我们可以在build打包文件夹里面新建个abc.txt文件,然后打包就可以发现abc.txt文件没用了,说明配置没有问题
它其实会自动去读取上下文里面的output找到打包文件夹从而删除掉

一句话总结loaderPlugin的区别?

  • loader只是在加载模块的时候,它通过我们的一个test去匹配这个模块,然后去使用不同的loader来处理这个模块,这就是loader
  • plugin可以在我们的webpack里面做任何的事情,因为它是贯穿于整个webpack的生命周期的

接着解决第二个问题

帮助生成HTML模板的HtmlWebpackPlugin插件

还有一个不太规范的地方:

  • 我们的HTML文件是编写在根目录下的,而最终打包的build文件夹中是没有index.html文件的;
  • 在进行项目部署的时,必然也是需要有对应的入口文件 index.html ;
  • 所以我们也需要对index.html进行打包处理;

HTML进行打包处理我们可以使用另外一个插件: HtmlWebpackPlugin ;
安装HtmlWebpackPlugin插件

1
npm install html-webpack-plugin -D

当我们有了这个插件之后,我们这个项目根目录下的index.html可以删掉了,因为最后会把它往build文件夹里打包一个的,主要原因是在这个插件里面有个EJS模板,它会根据那个EJS模板自动在我们打包文件夹里生成对应的html

使用HtmlWebpackPlugin插件,做配置
回到webpack.config.js文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require('path')
// 我们需要从clean-webpack-plugin插件中取出CleanWebpackPlugin类,因为插件一般给它封装成class
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
// 这个HtmlWebpackPlugin插件不需要做解构,因为它导出的就是一个类,不同的插件有不同的封装方式,而且这种第三方的很难统一规范的
const HtmlWebpackPlugin = require("html-webpack-plugin")

// webpack是运行在node环境下的,不要使用ES6 module导出【如果非要使用它还需要做额外配置】
// 注意出口文件中的path得是绝对路径,需要使用到path模块,做路径拼接即可,__dirname就是当前编写代码文件所在目录【绝对路径】
module.exports = {
...
plugins: [
new CleanWebpackPlugin(), // 格式:根据CleanWebpackPlugin类创建出对象
new HtmlWebpackPlugin()
] // plugins是个数组【这个不用管顺序】,里面放的是一个个插件对象【其实它源码里面是拿到我们导出的这个大的对象,然后去取到我们所有的plugins,之后对它做了个for循环,for循环后对它做个注入,到时候就可以根据不同的hook的生命周期来回调这个插件里面对象的某个方法】
}

然后进行打包,我们就会发现打包文件夹里会多出来一个html文件,而且你把这个文件在浏览器打开是可以正常显示的

我们项目根目录下发现还有个js文件,一般js文件也会单独放到一个js文件夹里面,所以这里我们可以对js文件进行优化
回到webpack.config.js文件中,对于打包出口文件夹filename添加个js文件夹即可

1
2
3
4
5
6
7
8
9
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, './build/'),
filename: "js/main.js",
// assetModuleFilename: 'img/[name]_[hash:6][ext][query]', // webpack5最新自定义文件名,generator.filename优先级比它高
}
...
}

再次打包,这样这个目录层次和原生的目录层次其实差别不大
上面就是HtmlWebpackPlugin插件基本使用,但是在真实开发里面,一般情况下,我们用的不是HtmlWebpackPlugin插件默认给的那个模板,而是自定义一个模板

这里就不得不说一下,vue或者是react脚手架创建的项目了,不难发现它们这些项目下面都会有个public文件夹,而其中有个index.html,没错就是这个index.html它就是自定义的一个模板

那么我们就来仿照它这种,在我们webpack创建的项目下,新建一个public文件夹,再新建一个index.html,其中的内容我们把vue3创建项目的public/index.html里的内容拿过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="">

<head>
<meta charset="utf-8">
<meta http-equiv="X一UA-Compatible" content="TE=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>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected-->
</body>

</html>

那么如何去使用咱们这个模板呢?
回到webpack.config.js文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const path = require('path')
// 我们需要从clean-webpack-plugin插件中取出CleanWebpackPlugin类,因为插件一般给它封装成class
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
// 这个HtmlWebpackPlugin插件不需要做解构,因为它导出的就是一个类,不同的插件有不同的封装方式,而且这种第三方的很难统一规范的
const HtmlWebpackPlugin = require("html-webpack-plugin")

// webpack是运行在node环境下的,不要使用ES6 module导出【如果非要使用它还需要做额外配置】
// 注意出口文件中的path得是绝对路径,需要使用到path模块,做路径拼接即可,__dirname就是当前编写代码文件所在目录【绝对路径】
module.exports = {
...
plugins: [
new CleanWebpackPlugin(), // 格式:根据CleanWebpackPlugin类创建出对象
new HtmlWebpackPlugin({
template: "./public/index.html"
}) // 可以传入一个指定模板【不指定它有个默认模板】
] // plugins是个数组【这个不用管顺序】,里面放的是一个个插件对象【其实它源码里面是拿到我们导出的这个大的对象,然后去取到我们所有的plugins,之后对它做了个for循环,for循环后对它做个注入,到时候就可以根据不同的hook的生命周期来回调这个插件里面对象的某个方法】
}

接着进行打包,我们发现控制台报错,报错信息如下:

1
2
3
ERROR in Template execution failed: ReferenceError: BASE_URL is not defined

ERROR in ReferenceError: BASE_URL is not defined

大概意思是说BASE_URL没有定义,这个其实是我们html模板里面的那个favicon图标,我们在模板里面是有如下代码的

1
<link rel="icon" href="<%= BASE_URL %>favicon.ico">

这个其实就涉及到webpack的一些细节了,上面href里面用的是EJS语法填充数据,但是这个BASE_URL常量我们并没有在哪定义过,那么到时候它就不知道怎么填充数据,所以就报错
解决方法:

  1. 把这行代码删掉,就不报错了,然后就可以正常打包【不推荐】
  2. 这时候就涉及到一个DefinePlugin插件,这个插件是webpack内置的一个插件

我们这里采用方案2,进入webpack.config.js文件中配置,同时提前准备了一个favicon.ico放在public文件夹下【顺便把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
const path = require('path')
// 我们需要从clean-webpack-plugin插件中取出CleanWebpackPlugin类,因为插件一般给它封装成class
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
// 这个HtmlWebpackPlugin插件不需要做解构,因为它导出的就是一个类,不同的插件有不同的封装方式,而且这种第三方的很难统一规范的
const HtmlWebpackPlugin = require("html-webpack-plugin")
// `DefinePlugin`插件,这个插件是`webpack`内置的一个插件
const { DefinePlugin } = require("webpack")

// webpack是运行在node环境下的,不要使用ES6 module导出【如果非要使用它还需要做额外配置】
// 注意出口文件中的path得是绝对路径,需要使用到path模块,做路径拼接即可,__dirname就是当前编写代码文件所在目录【绝对路径】
module.exports = {
...
plugins: [
new CleanWebpackPlugin(), // 格式:根据CleanWebpackPlugin类创建出对象
new HtmlWebpackPlugin({
template: "./public/index.html",
favicon: "./public/favicon.ico", // 配置favicon
title: "巧克力真美味" // 模板里的htmlWebpackPlugin.options.title中的htmlWebpackPlugin是new出来的对象,options就是传入的配置项,title就是我们这里配置的title
}), // 可以传入一个指定模板【不指定它有个默认模板】
new DefinePlugin({
BASE_URL: '"./"'
}) // 定义BASE_URL的值,注意这个引号里还要再加个引号,有点类似eval,会把引号里面内容当js语法解析
] // plugins是个数组【这个不用管顺序】,里面放的是一个个插件对象【其实它源码里面是拿到我们导出的这个大的对象,然后去取到我们所有的plugins,之后对它做了个for循环,for循环后对它做个注入,到时候就可以根据不同的hook的生命周期来回调这个插件里面对象的某个方法】
}

重新打包,我们再看看刚刚那个代码以及title变成了什么

1
2
3
<link rel="icon" href="./favicon.ico">

<title>巧克力真美味</title>

然后在浏览器上看看效果,发现favicon.ico没效果,因为打包文件夹下没这个文件,但是我们发现vue脚手架打包,它的打包文件夹下怎么就有这个文件呢,当然网上也有别人在配置项里配置favicon后面配个路径,但是它会在html文件里添加一行link引入这样代码,会造成两行,可能有的人说把模板里的那个删掉,反正它自己会生成一行link,这也是一个办法,但我们这里主要是想看vue脚手架它是怎么做的!!!
这个favicon.ico它其实是复制过去的,在vue脚手架创建项目中,像public这个文件夹里的文件index.html模板外其实最后都是会被复制到打包文件夹里,那么想实现复制这种功能就需要用到copy-webpack-plugin插件
其实这里我们也能慢慢发现,想要某个功能webpack就有,而不需要我们再去造轮子,webpack这个生态、社区还是相当强大的

复制功能copy-webpack-plugin插件

局部安装该插件

1
npm install copy-webpack-plugin -D

回到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
29
30
31
32
33
34
35
36
37
38
const path = require('path')
// 我们需要从clean-webpack-plugin插件中取出CleanWebpackPlugin类,因为插件一般给它封装成class
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
// 这个HtmlWebpackPlugin插件不需要做解构,因为它导出的就是一个类,不同的插件有不同的封装方式,而且这种第三方的很难统一规范的
const HtmlWebpackPlugin = require("html-webpack-plugin")
// `DefinePlugin`插件,这个插件是`webpack`内置的一个插件
const { DefinePlugin } = require("webpack")
// 复制功能copy-webpack-plugin插件
const CopyWebpackPlugin = require("copy-webpack-plugin")

// webpack是运行在node环境下的,不要使用ES6 module导出【如果非要使用它还需要做额外配置】
// 注意出口文件中的path得是绝对路径,需要使用到path模块,做路径拼接即可,__dirname就是当前编写代码文件所在目录【绝对路径】
module.exports = {
...
plugins: [
new CleanWebpackPlugin(), // 格式:根据CleanWebpackPlugin类创建出对象
new HtmlWebpackPlugin({
template: "./public/index.html",
title: "巧克力真美味" // 模板里的htmlWebpackPlugin.options.title中的htmlWebpackPlugin是new出来的对象,options就是传入的配置项,title就是我们这里配置的title
}), // 可以传入一个指定模板【不指定它有个默认模板】
new DefinePlugin({
BASE_URL: '"./"'
}), // 定义BASE_URL的值,注意这个引号里还要再加个引号,有点类似eval,会把引号里面内容当js语法解析
new CopyWebpackPlugin({
patterns: [
{
from: "public", // 从哪个文件夹里复制
to: "./", // 复制到哪个文件夹【注意它是在打包文件夹基础上,这里也可以不写,默认就是打包文件夹】
globOptions: {
ignore: [
"**/index.html" // 注意这两个**表示当前public文件夹下index.html以及子文件夹下的index.html
]
} // globOptions.ignore忽略某个文件,可以过滤掉某些不需要复制的文件如index.html模板
}
] // patterns匹配的意思
}) // 复制功能插件
] // plugins是个数组【这个不用管顺序】,里面放的是一个个插件对象【其实它源码里面是拿到我们导出的这个大的对象,然后去取到我们所有的plugins,之后对它做了个for循环,for循环后对它做个注入,到时候就可以根据不同的hook的生命周期来回调这个插件里面对象的某个方法】
}

然后打包,就可以看到打包文件夹下就有favicon.ico文件,浏览器运行正常

webpack的mode和devtool

进入我们的打包好的main.js文件中,可以发现文件非常大,它其实是会出现一个警告的,我们也能在终端看到最后一个警告,它希望我们做个代码分割,这里我们可以把图片限制改为10KB,然后还有一个警告,说我们这个图片太大了,建议图片大小244KB,这个只是建议,不用管它
这个打包的main.js有个问题,如果我们代码出错了,那么方便调试,找到源代码吗?
进入element.js文件中,加上如下代码:

1
2
// 测试错误代码
console.log(content.length)

我们重新打包一下,然后在浏览器打开,控制台报错,但是这个时候我们想知道代码哪里写的有问题,然后点进去,发现是那个打包压缩的代码,很难看懂
这个时候我们回到webpack.config.js文件中进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
// 设置模式
// development 开发阶段,会设置development
// production 准备打包上线的时候,设置production
mode: "development", // 开发模式,我们可以看到打包的js文件里有很多eval函数,这是因为devtool默认为eval
// 设置source-map,建立js映射文件,方便调试代码和错误
devtool: "source-map", // 默认为eval包裹着源代码,一般我们是设置为source-map,作用是在生成打包文件时,它也会生成source-map文件,再次打包发现项目根目录多了个main.js.map文件,它其实就是个映射文件,它可以把打包的js文件映射到真实开发环境的源代码里面
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, './build/'),
filename: "js/main.js",
// assetModuleFilename: 'img/[name]_[hash:6][ext][query]', // webpack5最新自定义文件名,generator.filename优先级比它高
},
...
}

然后重新打包,打开浏览器控制台,点击右侧报错文件,我们可以直接进入到真实开发环境的源代码里进行调试