1. 前言

最近在学习 Webpack 相关的原理,以前只知道 Webpack 的配置方法,但并不知道其内部流程,经过一轮的学习,感觉获益良多,为了巩固学习的内容,我决定尝试自己动手写一个插件。

这个插件实现的功能比较简单:

  • 默认清除 js 代码中的 console.log 的打印输出;
  • 可通过传入配置,实现移除 console 的其它方法,如 console.warnconsole.error 等;

2. Webpack 的构建流程以及 plugin 的原理

2.1 Webpack 构建流程

Webpack 的主要构建流程,可以分为三个阶段:

  • 初始化阶段:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  • 编译阶段:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  • 生成阶段:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

如果 Webpack 打包生产环境文件时,只会执行一次构建,以上阶段会按顺序执行一遍。但是在开启监听模式时,如开发环境,Webpack 会持续的进行构建。

2.2 plugin 原理

Webpack 插件通常是一个带有 apply 函数的类,其中 constructor 可以接收传入的配置项。插件被安装时,apply 函数会被调用一次,并接收 Compiler 对象,然后我们可以在 Compiler 对象上监听不同的事件钩子,从而进行插件功能的开发。

  1. // 定义一个插件
  2. class MyPlugin {
  3. // 构造函数,接收插件的配置项 options
  4. constructor(options) {
  5. // 获取配置项,初始化插件
  6. }
  7. // 插件安装时会调用 apply,并传入 compiler
  8. apply(compiler) {
  9. // 获取 comolier 独享,可以监听事件钩子
  10. // 功能开发 ...
  11. }
  12. }

2.3 compiler 和 compilation 对象

在开发 Plugin 过程中最常用的两个对象就是 CompilerCompilation

  • Compiler 对象在 Webpack 启动时被实例化,该对象包含了 Webpack 环境所有的配置信息,包括 optionsloadersplugins 等。在整个 Webpack 构建过程中,Compiler 对象是全局唯一的, 它提供了很多事件钩子回调供插件使用。
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象在 Webpack 构建过程中并不是唯一的,如果在开发模式下 Webpack 开启了文件检测功能,每当文件变化时,Webpack 会重新构建,此时会生成一个新的 Compilation 对象。Compilation 对象也提供了很多事件回调供插件做扩展。

3. 插件开发

3.1 项目目录

该插件实现的功能比较简单,文件目录也不复杂。首先新建一个空文件夹 remove-console-Webpack-plugin,并在该文件夹目录下运行 npm init,根据提示来填写 package.json 相关信息。然后再新建一个 src 文件夹,插件主要代码就放在 src/index.js 里面。如果你需要把项目放到 github 上,最好也添加一下 .gitignoreREADME.md 等文件。

  1. // remove-console-Webpack-plugin
  2. ├─src
  3. └─index.js
  4. ├─.gitignore
  5. ├─package.json
  6. └─README.md

3.2 插件代码

插件代码逻辑也并不复杂,主要有几点:

  1. 在构造函数中接收配置参数,并对参数进行合并,得到需要清除的 console 函数, 存放在 removed 数组中;
  2. apply 函数中监听 compiler.hook.compilation 钩子,该钩子触发后,拿到 compilation 后进一步监听它的钩子,这里 Webpack4Webpack5 的钩子不一样,需要做兼容;
  3. 定义 assetsHandler 方法来处理 js 文件,利用正则表达式清除 removed 中包括的 console 函数;
  1. class RemoveConsoleWebpackPlugin {
  2. // 构造函数接受配置参数
  3. constructor(options) {
  4. let include = options && options.include;
  5. let removed = ['log']; // 默认清除的方法
  6. if (include) {
  7. if (!Array.isArray(include)) {
  8. console.error('options.include must be an Array.');
  9. } else if (include.includes('*')) {
  10. // 传入 * 表示清除所有 console 的方法
  11. removed = Object.keys(console).filter(fn => {
  12. return typeof console[fn] === 'function';
  13. })
  14. } else {
  15. removed = include; // 根据传入配置覆盖
  16. }
  17. }
  18. this.removed = removed;
  19. }
  20. // Webpack 会调用插件实例的 apply 方法,并传入compiler 对象
  21. apply(compiler) {
  22. // js 资源代码处理函数
  23. let assetsHandler = (assets, compilation) => {
  24. let removedStr = this.removed.reduce((a, b) => (a + '|' + b));
  25. let reDict = {
  26. 1: [RegExp(`\\.console\\.(${removedStr})\\(\\)`, 'g'), ''],
  27. 2: [RegExp(`\\.console\\.(${removedStr})\\(`, 'g'), ';('],
  28. 3: [RegExp(`console\\.(${removedStr})\\(\\)`, 'g'), ''],
  29. 4: [RegExp(`console\\.(${removedStr})\\(`, 'g'), '(']
  30. }
  31. Object.entries(assets).forEach(([filename, source]) => {
  32. // 匹配js文件
  33. if (/\.js$/.test(filename)) {
  34. // 处理前文件内容
  35. let outputContent = source.source();
  36. Object.keys(reDict).forEach(i => {
  37. let [re, s] = reDict[i];
  38. outputContent = outputContent.replace(re, s);
  39. })
  40. compilation.assets[filename] = {
  41. // 返回文件内容
  42. source: () => {
  43. return outputContent
  44. },
  45. // 返回文件大小
  46. size: () => {
  47. return Buffer.byteLength(outputContent, 'utf8')
  48. }
  49. }
  50. }
  51. })
  52. }
  53. /**
  54. * 通过 compiler.hooks.compilation.tap 监听事件
  55. * 在回调方法中获取到 compilation 对象
  56. */
  57. compiler.hooks.compilation.tap('RemoveConsoleWebpackPlugin',
  58. compilation => {
  59. // Webpack 5
  60. if (compilation.hooks.processAssets) {
  61. compilation.hooks.processAssets.tap(
  62. { name: 'RemoveConsoleWebpackPlugin' },
  63. assets => assetsHandler(assets, compilation)
  64. );
  65. } else if (compilation.hooks.optimizeAssets) {
  66. // Webpack 4
  67. compilation.hooks.optimizeAssets.tap(
  68. 'RemoveConsoleWebpackPlugin',
  69. assets => assetsHandler(assets, compilation)
  70. );
  71. }
  72. })
  73. }
  74. }
  75. // export Plugin
  76. module.exports = RemoveConsoleWebpackPlugin;

4. 发布到npm

希望别人能使用到你的插件,就需要把插件发布到 npm 上,发布的主要流程:

  1. 首先在 npm 官网上注册账号,然后打开命令行工具,在任意目录下输入 npm login 并按提示登录;

  2. 登录后可用 npm whoami 查看是否登录成功;

  3. 发布前检查一下根目录下的 package.json 文件信息是否填写正确,主要字段:

    name:决定用户下载你的插件时用的名称,不可与 npm 上已有的第三方包重名,否则无法发布;

    main:插件主文件入口,Webpack 引入插件时,就从该目录导入;

    version:每次更新发布时,需要与上一版本的版本号不一样,否则上传不成功;

    repository:如果你的插件代码放在 githubgitee 等网站,可以填一下;

    private:不能设置为 true,否则无法发布;

  4. 一切准备就绪后,切换到插件所在的目录下,运行 npm publish 即可上传插件;

5. 结尾

本文是我学习了 Webpack 原理并开发了一个小插件后的总结,由于 Webpack 的内容实在太多了,所以可能会有理解不到位的地方,还请大佬们多多指正。另外,如果这篇文章对你有帮助,可以给我点个赞,或者给我的插件项目点个star,你的鼓励是我最大的动力哈~

80行代码教你写一个Webpack插件并发布到npm的更多相关文章

  1. 只有20行Javascript代码!手把手教你写一个页面模板引擎

    http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...

  2. 怎样写一个webpack loader

    div{display:table-cell;vertical-align:middle}#crayon-theme-info .content *{float:left}#crayon-theme- ...

  3. 半个小时教你写一个装(bi)逼(she)之地图搜租房

    半个小时教你写一个装(bi)逼(she)之地图搜租房 首先需要一个Python3环境,怎么准备我就不多说了,实在不会的出门右转看一下廖雪峰老师的博客. HTML部分 代码来自:高德API+Python ...

  4. iOS开发——实用技术OC篇&8行代码教你搞定导航控制器全屏滑动返回效果

    8行代码教你搞定导航控制器全屏滑动返回效果 前言 如果自定了导航控制器的自控制器的leftBarButtonItem,可能会引发边缘滑动pop效果的失灵,是由于 self.interactivePop ...

  5. 案例实战之如何写一个webpack plugin

    案例实战之如何写一个webpack plugin 1.写一个生成打包文件目录的file.md文件 // 生成一个目录项目目录的文件夹 class FileListPlugin { constructo ...

  6. 【vps】教你写一个属于自己的随机图API

    [vps]教你写一个自己的随机图API 前言 刚刚开始使用halo博客的时候,我就发现halo博客系统是可以使用随机图当背景的,所以也是使用了网上一些比较火的随机图API. 在上次发现了各种图片API ...

  7. 为PhoneGap写一个android插件

    为PhoneGap写一个android插件,要怎么做? 其实这句话应该反过来说,为android写一个PhoneGap插件,要怎么做? 这里以最简单的Hello World!为例,做个说明: 1.第一 ...

  8. 如何写一个jquery插件

      本文总结整理一下如何写一个jquery插件?虽然现今各种mvvm框架异常火爆,但是jquery这个陪伴我们成长,给我们带来很多帮助的优秀的库不应该被我们抛弃,写此文章,作为对以往欠下的笔记的补充, ...

  9. python 拼写检查代码(怎样写一个拼写检查器)

    原文:http://norvig.com/spell-correct.html 翻译:http://blog.youxu.info/spell-correct.html 怎样写一个拼写检查器 Pete ...

随机推荐

  1. Announcing cnblogs-hardening 1.0 Preview 1

    Release Notes Write about coding Note About coding Share about coding Talk about coding Comment abou ...

  2. 启用reuse_port参数让Nginx性能提升3倍

    为什么启用 reuse_port 记得 2008 年做性能测试的时候,新进7台 lenovo 4核4G 服务器用于性能测试. 当时资源紧张,这7台服务器都装了双系统(Win2003/CentOS5)空 ...

  3. DDOS攻击与防御简单阐述,列出DDOS的攻击方法和防御方法

    参考1:https://www.hi-linux.com/posts/50873.html#%E7%BD%91%E7%BB%9C%E5%B1%82-ddos-%E6%94%BB%E5%87%BB 什么 ...

  4. 什么是 Jenkins? 运用Jenkins持续集成

    [注]本文译自:https://www.edureka.co/blog/what-is-jenkins/   持续集成是 DevOps 最重要的部分,用于集成各个 DevOps 阶段.Jenkins ...

  5. gRPC在 ASP.NET Core 中应用学习(二)

    前言: 上一篇文章中简单的对gRPC进行了简单了解,并实现了gRPC在ASP.NET Core中服务实现.客户端调用:那么本篇继续对gRPC的4中服务方法定义.其他使用注意点进一步了解学习 一.gRP ...

  6. [Fundamental of Power Electronics]-PART I-3.稳态等效电路建模,损耗和效率-3.1 直流变压器模型

    3.1 直流变压器模型 如图3.1所示,任何开关变换器都包含三个部分:功率输入,功率输出以及控制输入.输入功率按控制输入进行特定的功率变换输出到负载.理想情况下,这些功能将以100%的效率完成,因此 ...

  7. 【笔记】《Redis设计与实现》chapter18 发布与订阅

    chapter18 发布与订阅 客户端订阅频道. 客户端向频道发送消息, 消息被传递至各个订阅者. 匹配模式 客户端订阅模式. 客户端向频道发送消息, 消息被传递给正在订阅匹配模式的订阅者. 另一个模 ...

  8. 网络编程Netty入门:Netty的启动过程分析

    目录 Netty的启动过程 Bootstrap 服务端的启动 客户端的启动 TCP粘包.拆包 图示 简单的例子 Netty编解码框架 Netty解码器 ByteToMessageDecoder实现类 ...

  9. JMeter发送get请求并分析返回结果

    在实际工作的过程中,我们通常需要模拟接口,来进行接口测试,我们可以通过JMeter.postman等多种工具来进行接口测试,但是工具的如何使用对于我们来说并不是最重要的部分,最重要的是设计接口测试用例 ...

  10. new、delete、析构函数、自动类型转换

    new 分配内存,返回指针 new 类型名T (初值列表) 功能:申请用于存放T类型对象的内存空间,并依初值列表赋以初值 结果值: 成功->T类型的指针,指向新分配的内存 失败->0(NU ...