手把手带你学webpack(4)-- Webpack Plugin初体验

828 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

本篇文章对应源码:github.com/Plasticine-…

前面我们讲了webpack的一个核心概念 -- loader,它能够用于转换指定类型的模块,比如可以将css、图片文件、字体文件等资源文件作为js的模块进行处理

而今天要讲的就是webpack的另一个核心概念 -- pluginplugin能够帮助我们实现更多的功能,比如打包优化、资源管理以及环境变量的注入,下面就来具体看看这些功能的效果吧!

1. 什么是Plugin?

根据webpack5官方文档的介绍:

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.

plugin能够实现许多额外的功能,还能够允许我们根据plugin interface去扩展webpack的功能,实现你想要的各种个性化自定义需求,自定义这一部分我们留到之后再讲,现在先了解plugin就是用来扩展webpack实现各种丰富功能这一事实就行了


2. 怎么使用Plugin?

先看看官方文档对plugin使用的介绍

Since plugins can take arguments/options, you must pass a new instance to the plugins property in your webpack configuration.

In order to use a plugin, you need to require() it and add it to the plugins array. Most plugins are customizable through options. Since you can use a plugin multiple times in a configuration for different purposes, you need to create an instance of it by calling it with the new operator.

总结一下,关键就是如下几点:

  1. 使用CommonJS模块化规范导入plugin
  2. 使用new操作符创建plugin实例
  3. plugin实例添加到webpack.config.js中,应放在plugins配置项中,该配置项是一个数组,说明webpack支持使用多个plugin

说了那么多,不如直接上例子来得实在,下面就先看几个常用的plugin吧!


3. 体验常用的Plugin

首先看一下webpack官方给我们提供了哪些plugin

官方plugins:webpack.js.org/plugins/ 第三方plugins:webpack.js.org/awesome-web…

我们先来使用一下官方提供的plugin -- HtmlWebpackPlugin

3.1 使用HtmlWebpackPlugin

还记得前面两篇文章中的那些案例场景吗?当时打包出来的结果都是没有html文件的,这样的话我们每次都需要在打包之后在打包结果的目录下创建一个index.html文件,然后手动去引入打包的main.js,这样未免太麻烦了

这时候HtmlWebpackPlugin就能够派上用场了,这也体现了plugin的意义所在,就是用于扩展webpack的,因为这个本身不是webpack的功能,但是装了相应的plugin后,我们就可以扩展webpack使其具有我们想要的功能了

The plugin will generate an HTML5 file for you that includes all your webpack bundles in the body using script tags.

这个plugin最基本的功能就是能够为我们的打包结果中添加一个HTML5文件,并且该文件会自动引入项目打包出来的js文件

3.1.1 基本使用

首先是安装HtmlWebpackPlugin

pnpm i html-webpack-plugin -D

然后在webpack.config.js中配置plugin

const HtmlWebpackPlugin = require('html-webpack-plugin');

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  plugins: [new HtmlWebpackPlugin()],
};

创建src/index.js,写一个alert函数测试一下

// src/index.js
alert('hello webpack plugin!');

运行打包命令,得到打包结果如下 image.png index.html的内容如下

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Webpack App</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <script defer="defer" src="main.js"></script>
  </head>
  <body></body>
</html>

可以看到,自动帮我们引入了打包出来的main.js,省去了自己创建index.html再引入的操作 这里的**index.html**是我用**prettier**格式化后贴上来的,为了方便阅读,但实际上打包出来的**index.html**是压缩过的,也就是全部的空格都被删去了,减小打包文件的体积


3.1.2 使用自定义模板

事实上HtmlWebpackPlugin能做更多的事情,它允许我们自定义模板,这是什么意思呢?看看官方的解释

You can either let the plugin generate an HTML file for you, supply your own template using lodash templates, or use your own loader.

所谓模板,就是说在生成html文件的时候,不使用它默认的html文件,而是我们提供的html文件,这个文件中可以放一些我们需要用到的内容,还能够使用各种插值表达式去使用配置HtmlWebpackPlugin时传入的一些参数,比如网站的标题等

使用过vue的读者都知道,vue是需要一个根标签<div id="app"></div>去挂载组件的,就以这个使用场景为例,我们可以创建一个我们自己的模板,并在里面放一个idappdiv标签

<!-- src/public/index.html -->
<!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" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

<title>标签中的内容就是插值表达式,能够使用HtmlWebpackPlugin配置项中的参数值,这种语法是EJS的语法,有兴趣的可以去EJS官网看看它的语法,上手很快的

接下来需要修改配置HtmlWebpackPlugin时传入的参数,因为模板中使用到了title这个属性,因此我们还需要在配置的时候传入才能让模板读取到

const HtmlWebpackPlugin = require('html-webpack-plugin');

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack plugin demo',
      template: './src/public/index.html',
    }),
  ],
};

template参数用于指定模板存放目录,可以使用相对路径,也可以使用绝对路径 image.png 可以看到,默认的模板是src/index.ejs,其他各种参数请参考官方文档


3.2 使用DefinePlugin

3.2.1 全局变量的意义

大家应该都知道,html可以在link标签中指定rel="icon"的图标,这个图标会显示在浏览器标签中,这个图标的文件名一般会命名为favicon.ico

如果我们希望实现这样一个功能,能够在模板中使用一个全局变量BASE_URL指定favicon.ico的存放路径,这样就可以灵活修改全局变量,而不需要去修改模板了

因为模板一般来说很少会去进行改动的,而BASE_URL指的是网站部署后的基础地址,比如今天我将网站部署在http://plasticine.com/下,那么favicon.ico的读取地址就是http://plasticine.com/favicon.ico,但是如果明天我的网站部署在http://plasticine.com/temp/下呢?那么我就可以通过修改BASE_URL这个全局变量即可,而不需要去修改模板

可能你会觉得直接修改模板也没多麻烦,但是BASE_URL可能会在多处用到,如果不使用BASE_URL全局变量,而是直接硬编码写死的话,每次网站部署的基础地址改变后,就需要逐一找出和基础地址有关的地方去修改它们,效率非常低

基于这样的使用场景,DefinePlugin就可以帮助我们定义全局变量,注入到整个webpack的运行环境中使用,这样我们就可以在模板中直接使用到BASE_URL这个全局变量了


3.2.2 体验

DefinePlugin是一个webpack内置plugin,不需要我们去单独安装,这也体现了webpack认为全局变量很重要,也很常用,因此直接内置了该plugin

使用很简单,也是直接配置即可

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

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  plugins: [
    new DefinePlugin({
      // BASE_URL: JSON.stringify('/'),
      BASE_URL: '"/"',
    }),
  ],
};

需要注意,配置项为字符串时,会被当成js代码解析,而不是纯字符串

If the value is a string it will be used as a code fragment. If the value isn't a string, it will be stringified (including functions). If the value is an object all keys are defined the same way. If you prefix typeof to the key, it's only defined for typeof calls.

因此需要在单引号中再套一层双引号,以表示"/"字符串而不是/代码,如果觉得阅读性不好的话,也可以使用JSON.stringify

然后我们修改我们的模板,添加对favicon的引用

<!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 id="app"></div>
  </body>
</html>

打包后会发现并不能正常读取到favicon.ico,因为我们的打包目录下其实还没有favicon.ico这样一个文件,目前我们可以先手动复制favicon.ico文件到dist目录下,这样是麻烦了些,之后会讲到另一个plugin可以帮我们解决这一痛点


3.3 使用CopyWebpackPlugin

用过vue的读者都知道,放在public目录下的内容会被直接复制到打包结果目录下,该功能就是通过CopyWebpackPlugin实现的,使用该插件,就能够解决上面遇到的手动复制favicon.ico问题了

首先安装该插件

pnpm i copy-webpack-plugin -D

然后配置插件

const CopyPlugin = require('copy-webpack-plugin');

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: 'public',
          globOptions: {
            ignore: ['**/index.html'],
          },
        },
      ],
    }),
  ],
};

from表示要将哪个目录的内容复制到打包结果目录下,还可以指定to将它们复制到打包结果目录的子目录下

globOptions中可以配置一些该插件的全局配置,这里我们配置了ignore,意思是忽略public目录下的任何index.html文件以及public子目录下的任何index.html文件

因为我们已经通过HtmlWebpackPlugin去复制html模板文件了,因此不需要让CopyWebpackPlugin处理

具体配置项可以参考CopyWebpackPlugin文档

现在就可以正常将public目录下的文件直接复制到dist目录下了 image.png image.png


3.4 使用CleanWebpackPlugin

我们每次打包的时候,为了避免旧的打包文件残留的问题,总是需要手动去删除dist目录,这太麻烦了,这时候就可以使用CleanWebpackPlugin去帮我们删除

该插件不是官方插件,也不在awesome-webpack中,是一个第三方插件,可以在npmjs.com查看它的说明

首先安装该插件

pnpm i clean-webpack-plugin -D

然后配置插件,默认不需要做任何配置就可以达到我们想要的效果了

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  plugins: [
    new CleanWebpackPlugin(),
  ],
};

但是我发现在webpack5中该插件好像没作用,但是我又发现webpack5中内置了一个CleanPlugin,并且该插件是有作用的

const { CleanPlugin } = require('webpack');

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  plugins: [
    new CleanPlugin(),
  ],
};

如果有知道为什么CleanWebpackPlugin失效的读者欢迎留言告诉我一下~