首页 > 其他分享 >利用uplugin对比Webpack和Rollup插件系统

利用uplugin对比Webpack和Rollup插件系统

时间:2023-08-08 15:06:32浏览次数:52  
标签:load 插件 函数 plugin Rollup Webpack webpack compiler

本文由华为云云岭团队松塔同学分享~

江湖上一直流传一种说法:Rollup 的插件系统设计,相比与 webpack,要更加科学顺手。(网络上对 webpack 插件编写的吐槽不计其数)Talk is cheap,本文基于 unplugin 这个三方库来对比研究一下二者的插件系统。Unplugin 是一个插件编写工具,它可以让开发者用一套代码同时为主流 bundler 编写插件,包括 webpack、Rollup、Vite、esbuild、Rspack。

Unplugin hooks

Unplugin 以 Rollup 的 hooks 为基础,总共有 9 个生命周期钩子函数,其中包含 6 个 build hooks,1 个 output generation hook,和 2 个独立 hooks。借此我们可以大致了解不同 bundler 之间的通用能力。下面将简要介绍包含 unplugin 自身逻辑的钩子函数,其余请参考 Rollup 官方文档。

buildStart 和 buildEnd

与 Rollup 的钩子函数相同,分别代表一次 build 准备开始和 build 结束。不同的是 unplugin 将函数的 this 指向了自身定义的UnpluginBuildContext:

export interface UnpluginBuildContext {
  addWatchFile: (id: string) => void
  emitFile: (emittedFile: EmittedAsset) => void
  getWatchFiles: () => string[]
  parse: (input: string, options?: any) => AcornNode
}

该上下文提供了四个方法,unplugin 为每个 bundler 都实现了一遍,按需使用。

loadInclude 和 transformInclude

专门为 webpack 适配的钩子函数,用来过滤需要 load 或者 transform 的模块。由于 webpack loader 和 plugin 分离的设计,load 和 transform 的功能实际被 loader 所承载。如果没有过滤函数,会导致所有模块都被插件加载,影响 webpack 性能。

Unplugin Webpack模块实现

深入看 unplugin 对 webpack 模块的实现,可以观察到 Rollup 类的钩子函数是如何转换到 webpack 系统中的。

首先了解 webpack plugin 的设计。官方文档中给出的示例比较传统:一个具有 apply 函数的 class,通过 constructor 接收用户对插件的自定义设置。实际上,webpack 只需要一个带有 apply 方法的对象就够了。Unplugin 还额外包了一层生成函数,将用户配置传递到每个 bundler 的插件定义函数中,此外还提供了meta参数表明它要为哪个 bunlder 生成插件:

export function getWebpackPlugin<UserOptions = {}>(
  factory: UnpluginFactory<UserOptions>,
): UnpluginInstance<UserOptions>['webpack'] {
  return (userOptions?: UserOptions) => {
      return {
          apply(compiler: WebpackCompiler) {
              // implementation
          }
      }
  }

代码中factory就是定义插件在各生命周期中执行具体逻辑的函数,例如:

(options, meta) => {
    return {
        load() {
            // load 钩子函数
        }
    }
}

在执行钩子函数之前,有一系列初始化工作。首先在 webpack compiler 中注入自身上下文。

const injected = compiler.$unpluginContext || {}
compiler.$unpluginContext = injected

接着调用factory函数拿到插件定义:

const rawPlugins = toArray(factory(userOptions!, meta))

unplugin 支持多个插件同时定义,所以这里统一用toArray转换成数组处理。然后遍历数组,给插件增加公共属性:

const plugin = Object.assign(
    rawPlugin,
    {
        __unpluginMeta: meta,
        __virtualModulePrefix: VIRTUAL_MODULE_PREFIX,
    },
) as ResolvedUnpluginOptions

// inject context object to share with loaders
injected[plugin.name] = plugin

compiler.hooks.thisCompilation.tap(plugin.name, (compilation) => {
    compilation.hooks.childCompiler.tap(plugin.name, (childCompiler) => {
        childCompiler.$unpluginContext = injected
    })
})

注意这里给 childCompiler 也同样注入了上下文。这一系列注入上下文的动作,是让整个 webpack 都能拿到插件的定义。这在 webpack loader 中拿到 plugin 的定义是有作用的,因为 loader 定义中,它只是一个接受 source code 的函数,然后返回转译过的 source code。通过全局注入,我们就能在 loader 的定义函数中拿到 plugin 的load函数和transform函数。

接下来按照钩子函数的执行顺序,逐一解析其源码。

buildStart

if (plugin.watchChange || plugin.buildStart) {
    compiler.hooks.make.tapPromise(plugin.name, async (compilation) => {
        const context = createContext(compilation)
        if (plugin.watchChange && (compiler.modifiedFiles || compiler.removedFiles)) {
            // implementation
        }

        if (plugin.buildStart)
            return await plugin.buildStart.call(context)
    })
}

buildStartwatchChange被放在一起处理,因为他们都要用到上下文。具体看buildStart,仅仅提供context并执行plugin.buildStart。对应到 webpack 插件生命周期是make。查阅 webpack 文档我们可以发现,unplugin 略过了一系列 webpack 初始化的钩子函数,例如读取 config,初始化 compiler,调用插件等等。因为这些是 webpack 的自有逻辑,和 Rollup 也无法兼容。make会在一次 compliation 创建完后触发,即将开始从 entry 读取文件。符合 Rollup 的buildStart定义。

watchChange

watchChange是独立于执行顺序之外的钩子函数。当 bundler 以 watch 模式运行时,当被监测的文件发生变化时触发。在 webpack 中,unplugin 利用了 compiler 的modifiedFilesremovedFiles来获取对应的文件。由于每次文件变化 Webpack 都会重新执行一次 compilation,因此modifiedFilesremovedFiles也对应更新。

modifiedFiles是 Webpack 5 新增的属性。

resolveId

Rollup 的resolveId存在三个入参sourceimporteroptions :

type ResolveIdHook = (
    source: string,
    importer: string | undefined,
    options: {
        assertions: Record<string, string>;
        custom?: { [plugin: string]: any };
        isEntry: boolean;
    }
) => ResolveIdResult;

Webpack 中 resolve 相关概念位于 config 中的 resolve 对象,比较常见的设置如 alias。Webpack 对 resolve 专门提供了一个插件的设置,它不同于普通的 plugin,属于ResolvePluginInstance,unplugin 利用这个设置传入resolveId函数。

resolver
  .getHook('resolve')
  .tapAsync(plugin.name, async (request, resolveContext, callback) => {
    if (!request.request)
      return callback()

    // filter out invalid requests
    if (normalizeAbsolutePath(request.request).startsWith(plugin.__virtualModulePrefix))
      return callback()

    const id = normalizeAbsolutePath(request.request)

    const requestContext = (request as unknown as { context: { issuer: string } }).context
    const importer = requestContext.issuer !== '' ? requestContext.issuer : undefined
    const isEntry = requestContext.issuer === ''

    // call hook
    const resolveIdResult = await plugin.resolveId!(id, importer, { isEntry })
    // ...
  }
// ...
compiler.options.resolve.plugins = compiler.options.resolve.plugins || []
compiler.options.resolve.plugins.push(resolverPlugin)

可以看到idimporter都来自于resolve这个钩子函数传入的参数,可惜在 webpack 文档中缺乏相关说明。options参数中,只提供了isEntry属性。最后我们看到resolverPlugin被手动创建出来后,放进了 compiler options 中。可见 webpack 插件的能力包括修改 config 文件,能力其实完全覆盖了 loader,这在后续的loadtransform函数中同样能见到。

从源码中我们会看到 virtual module 相关的代码,本文为简化场景会略过。下同。

load

Webpack 中 loader 定义在 config 中,例如:

module.exports = {
  module: {
    rules: [{ test: /.txt$/, use: 'raw-loader' }],
  },
};

用正则表示文件类型,然后指定 loader。Unplugin 通过手动实现一个 loader,然后插入 rules 来实现load的功能:

if (plugin.load) {
  compiler.options.module.rules.unshift({
    include(id) {
      if (id.startsWith(plugin.__virtualModulePrefix))
        id = decodeURIComponent(id.slice(plugin.__virtualModulePrefix.length))

      // load include filter
      if (plugin.loadInclude && !plugin.loadInclude(id)) return false

      // Don't run load hook for external modules
      return !externalModules.has(id)
    },
    enforce: plugin.enforce,
    use: [
      {
        loader: LOAD_LOADER,
        options: {
          unpluginName: plugin.name,
        },
      },
    ],
  })
}

Loader 除了用test正则匹配外,也支持用函数过滤,以被 import 的资源 path 为入参,这正是loadInclude的设计来源。从上述代码中我还会发现一个属性enforce,它是用来控制 loader 执行时机的。这也是 unplugin 要用unshift插入 rules 数组的原因。(默认最后加载 unplugin 插件)

具体看下LOAD_LOADER实现:

export default async function load(this: LoaderContext<any>, source: string, map: any) {
  const callback = this.async()
  const { unpluginName } = this.query
  const plugin = this._compiler?.$unpluginContext[unpluginName]
  let id = this.resource

  if (!plugin?.load || !id)
    return callback(null, source, map)

  const context: UnpluginContext = {
    error: error => this.emitError(typeof error === 'string' ? new Error(error) : error),
    warn: error => this.emitWarning(typeof error === 'string' ? new Error(error) : error),
  }

  if (id.startsWith(plugin.__virtualModulePrefix))
    id = decodeURIComponent(id.slice(plugin.__virtualModulePrefix.length))

  const res = await plugin.load.call(
    Object.assign(this._compilation && createContext(this._compilation) as any, context),
    normalizeAbsolutePath(id),
  )

  if (res == null)
    callback(null, source, map)
  else if (typeof res !== 'string')
    callback(null, res.code, res.map ?? map)
  else
    callback(null, res, map)
}

通过 webpack 上下文,可以拿到资源 id、找到对应 unplugin 插件。接着就是提供 unplugin 自身上下文然后调用load函数。根据 Rollup 对load返回结果的定义,调用callback传参。

transform

transform 函数和load函数类似,同样是自定义一个 loader,然后插入rules。唯一的区别是处理 transform 逻辑时,没有用到 include 函数,而是在 use 函数中再执行transformInclude进行过滤。(这是令人困惑的地方,因为这和前文所述 unplugin 设计transformInclude的理由矛盾。没有 include 函数会导致所有模块都被插件加载)

Unplugin会先处理 transform 逻辑,由于用 unshift 插入 rules,会导致load生成的 rule 在transform 之前,按照 webpack 默认的加载 loader 顺序,transform 会先于 load 被触发。不知是 bug 还是 unplugin 的预期行为。

buildEnd

buildEnd对应 webpack 的emit钩子函数。

if (plugin.buildEnd) {
  compiler.hooks.emit.tapPromise(plugin.name, async (compilation) => {
    await plugin.buildEnd!.call(createContext(compilation))
  })
}

writeBundle

writeBundle对应 webpack 的afterEmit钩子函数。没有任何传参和上下文的调用,意味着拿不到所有 bundler 创建出的文件。

if (plugin.writeBundle) {
  compiler.hooks.afterEmit.tap(plugin.name, () => {
    plugin.writeBundle!()
  })
}

总结

我们可以发现 unplugin 实际用到的 webpack hooks 只有三个:make, emitafterEmitloadtransform的功能由 webpack loader 所承载。make对 webpack 是一个很关键钩子函数,它表明了 webpack 一系列初始化的工作已完成,开始从入口文件出发编译每一个模块。

webpack 的插件系统对比 rollup 来说,一大特点是钩子函数特别多。除了本文提到的 compiler 具有钩子函数外,包括 compilation、ContextModuleFactory、 NormalModuleFactory、甚至 JavaScript 的 parser 都有一系列钩子函数。同时 plugin、loader、resolver 分离的设计也增加了系统的复杂度。因此网络上的观点更偏爱于 rollup 也不无道理。复杂的系统不代表不好,但是无疑增加了用户的学习成本。

关于 OpenTiny

OpenTiny 是一套企业级组件库解决方案,适配 PC 端 / 移动端等多端,涵盖 Vue2 / Vue3 / Angular 多技术栈,拥有主题配置系统 / 中后台模板 / CLI 命令行等效率提升工具,可帮助开发者高效开发 Web 应用。

核心亮点:

  1. 跨端跨框架:使用 Renderless 无渲染组件设计架构,实现了一套代码同时支持 Vue2 / Vue3,PC / Mobile 端,并支持函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强。
  2. 组件丰富:PC 端有80+组件,移动端有30+组件,包含高频组件 Table、Tree、Select 等,内置虚拟滚动,保证大数据场景下的流畅体验,除了业界常见组件之外,我们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP地址输入框、Calendar 日历、Crop 图片裁切等
  3. 配置式组件:组件支持模板式和配置式两种使用方式,适合低代码平台,目前团队已经将 OpenTiny 集成到内部的低代码平台,针对低码平台做了大量优化
  4. 周边生态齐全:提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供包含 10+ 实用功能、20+ 典型页面的 TinyPro 中后台模板,提供覆盖前端开发全流程的 TinyCLI 工程化工具,提供强大的在线主题配置平台 TinyTheme

联系我们:

更多视频内容也可以关注OpenTiny社区,B站/抖音/小红书/视频号。

标签:load,插件,函数,plugin,Rollup,Webpack,webpack,compiler
From: https://blog.51cto.com/u_16152776/7009027

相关文章

  • eclipse插件开发
    1.Eclipse简介和插件开发Eclipse是一个很让人着迷的开发环境,它提供的核心框架和可扩展的插件机制给广大的程序员提供了无限的想象和创造空间。目前网上流传相当丰富且全面的开发工具方面的插件,但是Eclipse已经超越了开发环境的概念,可以想象Eclipse将成为未来的集成的桌面环......
  • 真不是吹,这款能减少 BUG 的 IDEA 插件你可能没用过!
     前言 单元测试是一个伟大的发明,同时也是一个操蛋的发明。只要团队碰它,几乎很难全身而退。如果是我们自己写的代码,那么,写写单元测试也无伤大雅。但我们绝大多数人,都是跟在别人后面打扫狗屎,或者是留给别人一堆狗屎。这时候,单元测试写起来,就有一种不情不愿的味道。没错,就是不......
  • kettle之添加geometry插件支持并使用
    参考:https://blog.csdn.net/aganliang/article/details/104949538为了能够处理geometry类型的数据,PDI需要安装pentaho-gis-plugins该插件下载地址:https://github.com/atolcd/pentaho-gis-plugins/releases根据自己的kettle版本,下载所需的插件即可,我的是9的,所以下载的是1.4 ......
  • Ubuntu装进U盘(Ventoy 插件)避坑指南
    注意:本教程不是用Ventoy制作Ubuntu的U盘启动盘!!而是用Ventoy插件,把Ubuntu装进U盘里实现即插即用Ubuntu。本教程参看原教程:利用ventoy,将ubuntu安装到U盘中,实现即插即用。本教程尊重原创,笔者在参考原教程操作时所遇大小坑以此记录,算是对原教程的补充。避坑避坑0:本地硬......
  • webpack学习
    目录1.Webpack的作用?2.Node中的CommonJS规范3.包管理工具-NPM4.Node的工具集-path/url/util/zlib5.Node的文件操作能力-fs6.Node的缓冲(Buffer)和流(stream)7.Node的事件机制-EventEmitter8.Node的HTTP处理-请求与响应9.Node的事件循环-EventLoop10.Node的进程集群-Cluster1.Webpack......
  • 一些不错的VSCode设置和插件
    设置同步设置我们做的各项设置,不希望再到其他机器的时候还得再重新配置一次。VSCode中我们可以登陆微软账号或者GitHub账号,登陆后我们可以开启同步设置。开启设置同步,根据提示登陆即可。允许侧边栏水平滑动在目录层次较深或者文件名比较长时,侧边栏就无法完整显示文件名了。默......
  • VPP 插件分析与开发
    [email protected],2023DescriptionVPP自定义插件开发demo在之前的博客:自定义插件中,我们给出了FD.ioVPP的sample插件构建方式,但是并没有去真正开发一个插件。这篇博客给出一个打印数据包IP头部的完整示例。1.ping插件分析插件的例......
  • vue图片压缩插件
    图片压缩插件1.安装插件npmijs-image-compressor2.引入importImageCompressorfrom'js-image-compressor'3.使用compressionImage(file){returnnewPromise((resolve,reject)=>{//eslint-disable-next-lineno-newnewImageCom......
  • vim 文件树插件 nerdtree
    安装"在.vimrc中加入Plug'scrooloose/nerdtree'"nerdtree插件Plug'ryanoasis/vim-devicons'"nerdtree的文件图标----推荐下载配置letg:NERDTreeDirArrowExpandable='ʃ'"展开目录图标letg:NERDTreeDirArrowCollapsibl......
  • Idea-EasyCode插件配置
    1.Idea插件设置1.1.EasyCode插件  具体操作省略,按照后如下截图:    1.2.EasyCode模板1.2.1模板清单 1.2.2模板-MybatisPlusConfig.vm##设置回调$!callback.setFileName($tool.append("MybatisPlusConfig",".java"))$!callback.setSavePath($tool.append($tab......