100行代码实现一个webpack组件统计插件

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 前言大家好,我是Fly哥, 好久没写文章了。最近组长无意间提了个需求,如何统一项目中组件的引用的次数??? 听到这个立马来了兴趣, 心想着可以webpack 的 loader 去做, 当时脑子里的第一时间想法 是用这个去做的,后面从优化的角度的考虑还是开发了一个自定义plugin 目前已经发布到npm 上了如图:图片image-20220319214923462读完本篇文章你可以学到什么如何开发一个webpack 插件如何使用 rollup + ts 进行打包发布一个属于自己的npmwebpackWebpack 想必大家都很熟悉, 面试天天问, 你会webpack?

前言



大家好,我是Fly哥, 好久没写文章了。最近组长无意间提了个需求,如何统一项目中组件的引用的次数??? 听到这个立马来了兴趣, 心想着可以webpack 的 loader 去做, 当时脑子里的第一时间想法 是用这个去做的,后面从优化的角度的考虑还是开发了一个自定义plugin 目前已经发布到npm 上了


如图:


image.png


image-20220319214923462


读完本篇文章你可以学到什么


  1. 如何开发一个webpack 插件


  1. 如何使用 rollup + ts 进行打包


  1. 发布一个属于自己的npm


webpack


Webpack 想必大家都很熟悉, 面试天天问, 你会webpack? 你知道 webpack 的原理是什么?? 这里我就不都说了, 已经好多文章说过, 但是看别人写的, 自己不去写一遍,永远是学不会的。


手写webpack核心原理


https://github.com/wzf1997/webpack-study


上面 这两个网址 一篇讲webpack 核心原理的, 另一个 是我自己手写的简易版本的webpack, 实现起来 还是比较简单的。


我们先看下webpack 的大体构建流程:


  1. image.gifwebpack

第一步初始化参数阶段


这一步会从我们配置的webpack.config.js中读取到对应的配置参数和shell命令中传入的参数进行合并得到最终打包配置参数

所以一般在实际开发中 会用 webpack-merge


第二步准备编译阶段


这一步我们会通过调用webpack()方法返回一个compiler方法,创建我们的compiler对象,并且注册各个Webpack Plugin。找到配置入口中的entry代码,调用compiler.run()方法进行编译。这一步 后面我会重点分析


第三步模块编译


从入口模块进行分析,调用匹配文件的loaders对文件进行处理。同时分析模块依赖的模块,递归进行模块编译工作。常用的loader: ts-loader, thread-loader, cache-loader,  css-loader,babel-loader。这里就是会涉及到 AST ,而大家写自定义loader 就可以在这里操作了, loader 本质是拿到source, 经过一些处理, 在return 出去,有利于下一个loader 去拿对应的source, 有点像promise的链式调用, 这里 我就不带大家演示了,推荐这一篇文章,


image.gifloader


多图详解webpack loader 机制  https://juejin.cn/post/6992754161221632030


第四五步完成编译和输出文件


在递归完成后,每个引用模块通过loaders处理完成同时得到模块之间的相互依赖关系。输出文件阶段 整理模块依赖关系,同时将处理后的文件输出到ouput的磁盘目录中。

webpack 插件



这时候有人就会问, 那么webpack 自定义的插件 是发生在哪里的。 Webpack中的插件机制就是基于Tapable实现与打包流程解耦,插件的所有形式都是基于Tapable实现。

Tapable包本质上是为我们更方面创建自定义事件和触发自定义事件的库,类似于Nodejs中的EventEmitter Api


所以在webpack 存在了各种各样的钩子,既然有了这些钩子,我们就可以监听webpack 开始的过程 ,完成的过程,或者说所有静态资源打包的过程,我们都可以进行一些骚操作,然后 就出现了 市面上的各种各样的插件。其实webpack 的这种架构模式,我们是值得思考的,看后面业务是不是也可以用到类似的操作。


image.png


发布订阅模式


有接触过webpack插件开发的同学,或多或少可能都有了解过。任何一个webpack插件都是一个类(当然类本质上都是funciton的语法糖),每个插件都必须存在一个apply方法


这个apply方法会接受一个compiler对象。我们上边做的就是依次调用传入的pluginapply方法并且传入我们的compiler对象。

这里我请你记住上边的流程,日常我们编写webpack plugin时本质上就是操作compiler对象从而影响打包结果进行。

apply

这时候有同学还是不理解,可能是有点蒙, 我们回到上面的第二步 准备编译阶段, 这个准备编译阶段 到底做了什么事情


  1. 合并参数


  1. 创建 compiler 对象


  1. 然后加载插件函数 ,也就是遍历插件数组,然后调用每个插件实例的apply 方法

我下面写下伪代码 方面你理解


function webpack(options) {
  // 合并参数
  const mergeOptions = _mergeOptions(options);
  // 创建compiler对象
  const compiler = new Compiler(mergeOptions);
  // 加载插件
  _loadPlugin(options.plugins, compiler);
  return compiler;
}
// 加载插件函数
function _loadPlugin(plugins, compiler) {
  if (plugins && Array.isArray(plugins)) {
    plugins.forEach((plugin) => {
      plugin.apply(compiler);
    });
  }
}
module.exports = webpack;


这下你应该就该看明白了吧, 这就解释了为什么 我们编写的 webpack plugin 总是要有apply 方法了。


然后这个compiler 上面就有各种各样的钩子, 你就可以结合自己的需求去写一个自定义插件了。


统计组件次数



需求背景


为什么要有这个东西,技术本质上是用来解决问题的。我们增长C端 游戏组,沉淀了一套公共游戏组件库, 其实是推动设计去做统一样式规范, 减少测试周设计验收时长,不然每次改样式太痛苦了, 如果改的是公共组件,一人修改,别人用了也不会出错, 从我们的角度思考, 如何知道哪一个组件的引用次数比较多,进行重点封装, 进行组件升级, 而且也足够有理由推动设计去说,你在这么多业务都用的这个组件, 这个也和之前的一样吧, 大大减少开发时间。他们也减少验收时间,何乐而不为呢??所以就去搞了下,难度不大,但是意义重大。


技术分析


首先分析了我们公共组件的引用方式 都是下面这种方式的


import { ViewPager, ViewPagerRef, Image } from '@growth/ui'


其实就可以生成AST 的过程中, 我们做一个统计就好了,我们看下面这张图:


image.gifAST


其实对于我们而言,我不需要改变生成的结果, 我只需要生产之前,做一个数据统计。这里有同学就会问, 我哪里知道 上面经过词法分析、语法分析后 会是什么样子呢? 这里给大家分享一个网站

image.png

Ast 在线解析 https://astexplorer.net/


在这里我输入我们所需要的


image.png


ast 解析

然后我们会看到这么一个结构, 然后 可以找到 我们的 组件 名 和  引用的包名字,那么我可以去监听webpack 遍历所有文件, 一但碰到对应的 节点 ,我们将他存储下来, 就可以了。


我这个时候去 webpack 官网 我们找到了这个hook

image.gif

compiler 使用 NormalModuleFactory 模块生成各类模块。从入口点开始,此模块会分解每个请求,解析文件内容以查找进一步的请求,然后通过分解所有请求以及解析新的文件来爬取全部文件。在最后阶段,每个依赖项都会成为一个模块实例。


NormalModuleFactory 类扩展了 Tapable 并提供了很多了生命周期的钩子, 大体就是上面 一个模块从 解析到生成的各个过程,这里我们找到parser, 也就是解析这个文件 头部  有import 这里的时候

image.png

image.gifparser


这里的其实对于我们要处理的 类型 就是  esm, 或者是全部的auto , 这里的话 我就选择   auto 我写下了 下面 这段代码:


apply(compiler: Compiler) {
    const parser = (factory: any) => {
      factory.hooks.parser.for('javascript/auto').tap('count-webpack-plugin', (parser: any) => {
      })
    }
    compiler.hooks.normalModuleFactory.tap('count-webpack-plugin', parser)
  }


Tap  后面的名字是 自定义的 你可以随便取, 保持全局单一性就可以。


而我们拿到parser 他其实也是一个钩子


可以看到下面这张图:

image.png

image.gifparser HOOK


这里其实就是webpack 处理文件, 进行AST 留下的各种各样的钩子,我们自然就可以找到  importSpecifier, 我们看下下面这个例子:


index.js


import _, { has } from 'lodash';


myPlugin


parser.hooks.importSpecifier.tap(
  'MyPlugin',
  (statement, source, exportName, identifierName) => {
    /* First call
    source == 'lodash'
    exportName == 'default'
    identifierName == '_'
  */
    /* Second call
    source == 'lodash'
    exportName == 'has'
    identifierName == 'has'
  */
  }
);


一看 原理, 钩子的 callback 对应的四个参数 而我们想要的 可能就是  最后一个

identifierName, 还有一个是source


Source 其实就是对应着我们 插件 传入的文件名,  所以 构建整个webpack 的过程 中, 有多少个符合, 我们的callback  就会执行多少次,所以结果还是非常准确的。


const parser = (factory: any) => {
      if (!this.opts.startCount) {
        return
      }
      factory.hooks.parser.for('javascript/auto').tap('count-webpack-plugin', (parser: any) => {
        parser.hooks.importSpecifier.tap('count-webpack-plugin', (_statement: string, source: string, _exportName: string, identifierName: string) => {
          if (source.includes(this.opts.pathname)) {
            this.total.len = this.total.len + 1
            const key = identifierName
            this.total.components[key] = this.total.components[key]
              ? this.total.components[key] + 1
              : 1
          }
        })
      })
    }


这里 其实就是数据进行一个统计, 统计完之后你需要去展示嘛 ,


compiler.hooks.normalModuleFactory.tap('count-webpack-plugin', parser)
compiler.hooks.done.tap('count-webpack-plugin-done',done)


所以在 apply 方法的时候 同样也监听了 打包结束的钩子, 这个我们就可以在打包结束在控制 进行输出了。


为了让控制台 变得 有颜色 ,与其他打包 输出 不形成 干扰, 引入了  chalk

这是我统计我们项目组件的截图


image.gif最后出现这样的结果


这下其实就是一目了然了。


打包和发布



好了整个文件写好了, 我看了下没有100行代码, 所以这里的打包,我选择了rollup ,相对于webpack,  rollup对 treeShaking 支持友好。而且没有辣么重, 对于npm 包 或者是 js-sdk 去打包 感觉用起来 也很方便。因为我们只需要打包一个简答的 bundle, 不需要太多其他的功能。我们在实际工作中,还是灵活选择对应的工具。


安装TS


yarn add typescript -D


生成 tsconfig.json 文件,用来配置 ts 的编译选项:


npx tsc --init


然后项目的根目录 就会生成  tsconfig.json


这里给大家推荐一篇文章:


https://juejin.cn/post/6844904178234458120 掌握 tsconfig.json


安装rollup


到了关键的环节,我们的代码是 ESM 规范的,并且是由 TS 书写的。我们要将它打包,提供给 Node 或者浏览器直接使用,因此我们可以用 rollup 对代码进行处理。


在项目文件 新建 rollup.config.js


由于我们是ts 文件 ,所以安装   rollup-plugin-typescript2  用于处理ts 文件


yarn add rollup-plugin-typescript2 -D


紧接着我们定义我们的输入


const path = require('path')
import ts from 'rollup-plugin-typescript2'
const getPath = _path => path.resolve(__dirname, _path)
// 解析pa
const pkg =  require('./package.json')
const extensions = [
    '.js',
    '.ts',
]
const tsPlugin = ts({
    tsconfig: getPath('./tsconfig.json'), // 导入本地ts 配置
    extensions
})
const outputs = [
    {file: pkg.main, format: 'cjs' },
    {file: pkg.module, format: 'esm'}
]
export default {
    input: 'src/main.ts', // 入口
    output: outputs,  // 输入的文件类型
    plugins: [
        tsPlugin
    ]
}


就这么一个简单的配置我们 就配置好了,这里我讲一下 package.json

image.png

image.gifpkg


main  其实就是对应的 是npm 包的入口文件 ,一般是common js


module 字段就是引入npm 包  如果支持 esm 优先会加载这个文件


types  就是对应的 打包的后 d.ts 文件


然后我们进行打包 rollup


rollup -c


然后 会发现 打包发现控制台

image.png

image.gif终端

就是我们这依赖的 没有打包进去


我们看下common js 的目录:

image.png

image.gifcontent


果然是没有打包进去的, 其实就rollup 在打包过程中 , 面对当前模块 引用其他模块的时候,会导致打包失败,这里我们安装一下这个插件


yarn add rollup-plugin-node-resolve -D


在某些时候,您的项目可能取决于从NPM安装到node_modules文件夹中的软件包。


与Webpack和Browserify等其他捆绑软件不同,Rollup不知道如何``开箱即用''如何处理这些依赖项-我们需要添加一些插件配置。


rollup.js编译源码中的模块引用默认只支持 ES6+的模块方式import/export。然而大量的npm模块是基于CommonJS模块方式,这就导致了大量 npm 模块不能直接编译使用。


所以辅助rollup.js编译支持 npm模块和CommonJS模块方式的插件就应运而生。


@rollup/plugin-commonjs
@rollup/plugin-node-resolve


配置到 插件 列表后 就可以了 。


发布


所有准备工作已经做好了, 如何发布到npm 包 上


  1. 去 npm 官网 注册一个账号


  1. 然后 进入 我们当前目录


npm login


输入账号 和密码  就可以了


然后输入


npm publish


每一次发布前,记得修改 package.json 的版本 ,然后发布就可以了


相关文章
|
26天前
|
测试技术 开发者
如何确保 Webpack plugin 与其他插件的兼容性?
【10月更文挑战第23天】确保 Webpack plugin 与其他插件的兼容性需要从多个方面进行考虑和努力。通过遵循规范、进行充分测试、保持沟通协作等方式,
|
26天前
|
监控 前端开发 JavaScript
Webpack 中 HMR 插件的工作原理
【10月更文挑战第23天】可以进一步深入探讨 HMR 工作原理的具体细节、不同场景下的应用案例,以及与其他相关技术的结合应用等方面的内容。通过全面、系统地了解 HMR 插件的工作原理,能够更好地利用这一功能,为项目的成功开发提供有力保障。同时,要不断关注技术的发展动态,以便及时掌握最新的 HMR 技术和最佳实践。
|
1月前
|
移动开发 JavaScript 前端开发
webpack学习四:使用webpack配置plugin,来使用HtmlWebpackPlugin、uglifyjs-webpack-plugin、webpack-dev-server等插件简化开发
这篇文章主要介绍了如何通过配置Webpack的插件,如HtmlWebpackPlugin、uglifyjs-webpack-plugin和webpack-dev-server,来简化前端开发流程。
50 0
webpack学习四:使用webpack配置plugin,来使用HtmlWebpackPlugin、uglifyjs-webpack-plugin、webpack-dev-server等插件简化开发
|
1月前
|
前端开发 JavaScript 数据可视化
Webpack加载器和插件之间有什么区别
【10月更文挑战第13天】Webpack加载器和插件之间有什么区别
|
3月前
|
前端开发 JavaScript 开发者
Angular与Webpack协同优化:打造生产级别的打包配置——详解从基础设置到高级代码拆分和插件使用
【8月更文挑战第31天】在现代前端开发中,优化应用性能和加载时间至关重要,尤其是对于使用Angular框架的项目。本文通过代码示例详细展示了如何配置Webpack,以实现生产级别的打包优化。从基础配置到生产环境设置、代码拆分,再到使用加载器与插件,每个步骤都旨在提升应用效率,确保快速加载和稳定运行。通过这些配置,开发者能更好地控制资源打包,充分发挥Webpack的强大功能。
103 0
|
3月前
|
前端开发 开发者
在前端开发中,webpack 作为模块打包工具,其 DefinePlugin 插件可在编译时动态定义全局变量,支持环境变量定义、配置参数动态化及条件编译等功能。
在前端开发中,webpack 作为模块打包工具,其 DefinePlugin 插件可在编译时动态定义全局变量,支持环境变量定义、配置参数动态化及条件编译等功能。本文阐述 DefinePlugin 的原理、用法及案例,包括安装配置、具体示例(如动态加载资源、配置接口地址)和注意事项,帮助开发者更好地利用此插件优化项目。
97 0
|
6月前
|
前端开发
【专栏】`webpack` 的 `DefinePlugin` 插件用于在编译时动态定义全局变量,实现环境变量差异化、配置参数动态化和条件编译
【4月更文挑战第29天】`webpack` 的 `DefinePlugin` 插件用于在编译时动态定义全局变量,实现环境变量差异化、配置参数动态化和条件编译。通过配置键值对,如 `ENV: JSON.stringify(process.env.NODE_ENV)`,可以在代码中根据环境执行相应逻辑。实际应用包括动态加载资源、动态配置接口地址和条件编译优化代码。注意变量定义的合法性和避免覆盖,解决变量未定义或值错误的问题,以提升开发效率和项目质量。
320 3
|
JavaScript 开发者
webpack----webpack中的插件
webpack----webpack中的插件
|
6月前
|
前端开发 JavaScript API
webpack插件开发必会Tapable
webpack插件开发必会Tapable
90 0
|
6月前
|
JSON 监控 测试技术
《Webpack5 核心原理与应用实践》学习笔记-> 提升插件健壮性
《Webpack5 核心原理与应用实践》学习笔记-> 提升插件健壮性
95 0
下一篇
无影云桌面