likes
comments
collection
share

手把手带你开发webpack5.x - plugin(保姆教程)

作者站长头像
站长
· 阅读数 14

前言

正值国庆,祝伟大的祖国节日快乐!!!

什么是plugin

1. webpack打包基础

作者立志于每篇分享都能让让刚接触前端的的小白白也能看的明明白白,所以我们还是从最基础的开始,先解释一下 plugin 是个什么,我们通过一个demo来解释,创建一个 write-plugin 的文件夹,npm init -y 之后,执行 npm i webpack webpack-cli, 创建如图所示的目录结构:

手把手带你开发webpack5.x - plugin(保姆教程)

在index.js中写上一点代码

// src > index.js

function add(a, b) {
  return a + b
}
add(1, 2)

console.log('hello world 123');

我希望这个js文件能被打包为index.html所使用,所以现在去做webpack的配置

// webpack.config.js

const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: path.resolve(__dirname, './src/index.js')
  },
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, './dist')
  }
}

在package.json中配置一个 build 命令方便我们运行打包

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.config.js"
  },

完成这些操作后执行 npm run build,你会看到在根目录下打包出来一个dist文件夹里面打包了一个main.js文件,这就是我们src > index.js 被打包之后的样子,做完这些基础操作我们再聊 plugin 的问题

2. plugin 的应用

看到这个打包出来的 main.js, 现在要将它引入到 html 文件中才能生效,所以我们会在 根目录下的 index.html 中引入 <script src="./dist/main.js"></script>,但是因为CDN网络分发的内容在服务端会留有缓存,所以我们会在每次更新代码之后打包时打包出来一个带hash值的文件名(通常情况下),比如 修改 webpack.config.js中的代码

// webpack.config.js
(...省略部分代码)

output: {
  filename: 'main.[hash:8].js',  // +++带上hash值
  path: path.resolve(__dirname, './dist')
},

再次运行 npm run build 你会看到打包的文件名发生了变化:

手把手带你开发webpack5.x - plugin(保姆教程)

那么第一个问题来了,每次更新修改代码都会打包出来一个新的文件名,我们需要手动修改html里面引入的js地址,太麻烦了!如果有什么方法能直接实现我打包出来的文件自动引入到 html 那该多美好,事实上这样的方法很常见,给项目安装 html-webpack-plugin, 添加 webpack 的配置:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // +++ 引入插件

module.exports = {
  (...省略部分代码)
  
  plugins: [
    // 使用插件
    new HtmlWebpackPlugin({
      // 指明该插件需要处理的html模版
      template: path.resolve(__dirname, './index.html') 
    })
  ]
}

在此打包之后你胡看到在dist目录下出现了index.html文件,和最新的 main.js 文件,打开index.html

手把手带你开发webpack5.x - plugin(保姆教程) 诶!这样就解决了我们刚刚纠结的问题,并且再也不需要我们手动引入了

但是这里我们遇到了又一个问题就是每次打包之后就会出现一个新的 main.js,而上一次打包的依然保留在 dist 目录下,那岂不是我修改一百次代码 就多出来 99 个没有意义的残留文件,像个办法解决这个问题。

其实这个问题早已经有非常优雅的解决方案了,安装 clean-webpack-plugin 插件,并使用

// webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')  // +++ 引入插件

module.exports = {
 (...省略部分代码)
 
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './index.html')
    }),
    new CleanWebpackPlugin() // 使用插件
  ]
}

再次执行 npm run build, 再也没有残留的文件了。以上都是最基础的使用,现在你明白 plugin 在webpack中扮演的角色了。

webpack的生命周期

看到以上的效果之后,现在我们一起思考一个问题,我们使用的两个插件,html-webpack-plugin(帮助我们做js的引入)clean-webpack-plugin(删除上一次打包的文件),这两个插件谁工作谁后工作呢?三秒钟的思考你应该就有答案了,clean-webpack-plugin先工作,删除所有的文件之后,html-webpack-plugin再生成新的打包文件,如果颠倒顺序,打包出来的文件会马上被删除

从这里我们可以看出,webpack在执行打包的过程中,是有时间轴的概念的。确实如此,webpack官方提供了28个生命周期hooks webpack-hooks 官网,点开文档你会看到 compiler模块,我们需要认真搞明白它

1. 一段话看懂compiler

Compiler 模块是 webpack 的主要引擎,它通过 CLI 或者 Node API 传递的所有选项创建出一个 compilation 实例(稍微介绍它)。 它扩展(extends)自 Tapable 类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler 上注册。

webpack的所有生命周期hooks(钩子函数)都是由 compiler 暴露出来的。我们可以把compiler看做是webpack提供的一个实例化上下文对象

列举几个常见的 compiler-hooks:

entryOption:webpack读取完entry(入口文件)后调用

afterPlugins:设置完初始化内部插件之后

compilation:编译这件事被创建时,生成文件前

done:编译完成之后

emit:生成资源到output目录之前

2. 一段话看懂compilation

Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

也就是说webpack提供了一个Compilation 模块,上下文对象Compiler可以调用这个模块来创建一个新的对象,该对象也能可以访问到webpack所有的模块,且这个对象会编译代码中需要的依赖。并且 Compilation 类扩展自 Tapable,也提供了一些列的 生命周期钩子

列举几个常见的 compilation-hooks:

buildModule:在模块构建开始之前触发,可以用来修改模块

rebuildModule: 在重新构建一个模块之前触发

succeedModule:模块构建成功时执行

failedModule:模块构建失败时执行

搞明白这两个模块的作用才能奠定plugin开发的基础

如何开发一个plugin

1. plugin的原理

在动手开发一个自己的plugin之前,我们先看官网!官网!网!官网告诉我们一下几点:

1. plugin是一个具名函数

2. 会在webpack的某一个生命周期执行

3. 它原型上有一个apply方法(想一想vue的插件中的install函数,你会透彻很多)

2. 拟定一个需求

有了这些基础做铺垫,我们就可以开始尝试开发一个plugin了,先拟定一个需求,我们希望webpack每次打包出来的文件名都能不一样是为了规避缓存的影响,所以选择在打包时在文件名后面添加 hash 值。如果我们不添加 hash 值呢?有没有别的方式可以实现让html每次引用的文件名都能不一样?

试想: 如果我们能让打包完的 html 中<script defer src="main.js?拼接上时间戳"></script>能干成这件事,也一样能保证每次要请求的 js 资源都是不一样的,所以也不会命中缓存!那就不用在打包的时候带上hash了。

需要注意的是:

  1. 拼接时间戳应该在 html 文件生成出来之前完成,

  2. 我们的plugin应该在 html-webpack-plugin往 index.html 文件植入<script defer src="main.js"></script>脚本之前执行

3. 开发一个plugin

既然思路有了,那就不要仅停留在思考阶段了,我们需要一个如上的 plugin, 在根目录下创建一个 stamp-webpack-plugin.js 文件, 重要的解释都在代码的注释中:

// 根目录 > stamp-webpack-plugin.js

class StampPlugin { // 创建一个时间戳插件
  apply(compiler) {
    // 使用 compilation 生命周期保证 plugin 执行在 文件创建之前
    // compiler 创建的 compilation 对象在回调中被使用
    compiler.hooks.compilation.tap('StampWebpackPlugin', (compilation, callback) => { // 注册一个StampWebpackPlugin方法
      // 测试compilation对象在模块构建之前能得到什么
      // 将逻辑注册在同一个 StampWebpackPlugin 方法上
      compilation.hooks.buildModule.tap('StampWebpackPlugin',
        (data, cb) => {
          console.log(data);
        })
    })
  }
}


module.exports = StampPlugin

webpack提供三种触发钩子的方法:

tap:以同步的方式触发钩子

tapAsync:以异步的方式触发钩子

tapPromise:以异步的方式触发钩子,返回Promise

我们用上自己的 StampPlugin 插件,当前完整的webpack配置文件:

// webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const StampWebpackPlugin = require('./stamp-webpack-plugin') // +++ 引入我们写的plugin

module.exports = {
  mode: 'development',
  entry: {
    main: path.resolve(__dirname, './src/index.js')
  },
  output: {
    filename: 'main.js',  // +++ 打包的文件不再需要 hash
    path: path.resolve(__dirname, './dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './index.html')
    }),
    new CleanWebpackPlugin(),
    new StampWebpackPlugin()  // 使用我们的plugin
  ]
}

从新执行打包,可以看见 compilation 回调函数的参数 data 被打印出来(你也可以直接打印data.source看效果):

手把手带你开发webpack5.x - plugin(保姆教程) 这里的打印的内容过长,我截取一部分用来解释,可以看到其实我们打印出来的内容是一个对象,对象当中包含了三个模块(一个插件模块,一个js模块,一个html模块),因为当前我们的dome中只有这三个模块,换言之,compilation对象在模块构建之前能获取到webpack需要构建的所有模块

明白这一点之后,仿佛世界都明亮了,我想在 html-webpack-plugin 完成之前让我们的 plugin 先执行这件事变得可控了,开心的笑出了声。

三声过后再次陷入沉思,现在是可以在 webpack 读取到html-webpack-plugin模块时做点什么的,可是html-webpack-plugin它会干两件事, 一是将我们的根目录下的 html 文件解析一次在生成一份新的html到指定的 dist目录; 二是在新生成这个 html 文件中引入 <script defer src="main.js"></script>

解决这个问题的核心是:我能不能获取到html-webpack-plugin这个插件的执行时间节点,如果可以我就能在html-webpack-plugin执行到某个阶段的时候去执行我们自己的 plugin

打开html-webpack-plugin文档不禁感叹该插件的作者实在是太给力了,看到文档 Events模块, 作者已经考虑到这一点并提供了6个 hook 函数给我们用了(这里大家可以自行去看一下是哪6个,get好)。所以我们的 plugin 已经呼之欲出了

// 根目录 > stamp-webpack-plugin.js
 
const HtmlWebpackPlugin = require('html-webpack-plugin');  // +++ 引入html-webpack-plugin

class StampPlugin { // 创建一个时间戳插件
  apply(compiler) {
    // 使用 compilation 生命周期保证 plugin 执行在 文件创建之前
    // compiler 创建的 compilation 对象在回调中被使用
    compiler.hooks.compilation.tap('StampWebpackPlugin', (compilation, callback) => { // 注册一个StampWebpackPlugin方法
      
      // HtmlWebpackPlugin在webpack刚创建编译的时候执行自带的beforeAssetTagGeneration生命周期
      HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tap('StampWebpackPlugin', 
        (htmlPluginData, cb) => {
          console.log(htmlPluginData);
        }
      )
    })
  }
}

module.exports = StampPlugin

删掉对 compilation 对象的测试代码,将代码调整为如上,注释即解释。执行打包我们看看在 html-webpack-plugin执行到往index.html中插入<script defer src="main.js"></script>脚本之前我们拿了什么:

手把手带你开发webpack5.x - plugin(保姆教程)

注意看,此时html-webpack-plugin接收到的内容,只有一个 js 数组,这个 js 数组中存放的元素就是 html-webpack-plugin 要放在 <script defer src="main.js"></script>这个src属性里面的值,破案了!!!

那岂不是只要我修改这个 js 数组内的元素,src 里面放上的内容也会得到修改!还记得我们终极目标要干什么嘛?要拼时间戳<script defer src="main.js?拼接时间戳"></script>,搞定它

最终代码:

// 根目录 > stamp-webpack-plugin.js
 
const HtmlWebpackPlugin = require('html-webpack-plugin');  // +++ 引入html-webpack-plugin

class StampPlugin { // 创建一个时间戳插件
  apply(compiler) {
    // 使用 compilation 生命周期保证 plugin 执行在 文件创建之前
    // compiler 创建的 compilation 对象在回调中被使用
    compiler.hooks.compilation.tap('StampWebpackPlugin', (compilation, callback) => { // 注册一个StampWebpackPlugin方法
      
      // HtmlWebpackPlugin在webpack刚创建编译的时候执行自带的beforeAssetTagGeneration生命周期
      HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tap('StampWebpackPlugin', 
        (htmlPluginData, cb) => {
          let jsSrc = htmlPluginData.assets.js[0]  // ++++++
          // 直接修改js数组内的元素
          htmlPluginData.assets.js[0] = `${jsSrc}?${new Date().getTime()}` // +++++++
        }
      )
    })
  }
}

module.exports = StampPlugin

npm run build 打包看效果:

手把手带你开发webpack5.x - plugin(保姆教程)

完美!!!

有需要点这里:源码地址

结语

思考和动手只有一步之遥,希望本文章能帮助到你更好的认识webpack,以后面试中,工作中遇到 plugin 的开发时能不畏惧它

文章参考:

官网

html-webpack-plugin