Vite 是如何兼容 Rollup 插件生态的

简介: Vite 是如何兼容 Rollup 插件生态的

我们知道,Vite 开发时,用的是 esbuild 进行构建,而在生产环境,则是使用 Rollup 进行打包。

为什么生产环境仍需要打包?为什么不用 esbuild 打包?

Vite 官方文档已经做出解析:尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)

虽然 esbuild 快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建应用的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面。就目前来说,Rollup 在应用打包方面更加成熟和灵活。尽管如此,当未来这些功能稳定后,我们也不排除使用 esbuild 作为生产构建器的可能。

由于生产环境的打包,使用的是 Rollup,Vite 需要保证,同一套 Vite 配置文件和源码,在开发环境和生产环境下的表现是一致的

想要达到这个效果,只能是 Vite 在开发环境模拟 Rollup 的行为 ,在生产环境打包时,将这部分替换成 Rollup 打包


1686387681006.png

Vite 兼容了什么


要讲 Vite 如何进行兼容之前,首先要搞清楚,兼容了什么?

我们用一个例子来类比一下:

1686387666964.png

我们可以得到一下信息:

  • 洗烘一体机可以替代洗衣机,它们能做到一样的效果
  • 洗烘一体机,可以使用洗衣机的生态

这时候我们可以说,洗烘一体机,兼容洗衣机的生态,洗烘一体机能完全替代洗衣机

兼容关系,是不同层级的东西进行兼容。

替代关系,是同一层级的东西进行替代

那回到 vite,我们根据 Rollup 和 Vite 的关系,可以推出:

  • Vite 不是兼容 rollup,说兼容 Rollup 其实是不严谨的
  • Vite 是部分兼容 Rollup 的插件生态
  • Vite 可以做到部分替代 Rollup

1686387653471.png

这里强调一下,是部分兼容、部分替代,不是完全的,因为 Vite 的部分实现是与 Rollup 不同的


如何兼容 Rollup 的插件生态


想要兼容 Rollup 生态,就必须要实现 Rollup 的插件机制


Rollup 插件是什么?


Rollup 插件是一个对象,对象具有一个或多个属性、build构建钩子output generation输出生成钩子

插件应该作为一个包分发,它导出一个可以传入特定选项对象的函数,并返回一个对象

下面是一个简单的例子:


// rollup-plugin-my-example.js
export default function myExample () {
  return {
    name: 'my-example',
    resolveId ( source ) {
      if (source === 'virtual-module') {
        return source; // 这表明 Rollup 不应该检查文件系统来找到这个模块的 id
      }
      return null; // 其他模块照常处理
    },
    load ( id ) {
      if (id === 'virtual-module') {
        return 'export default "This is virtual!"'; // 返回 "virtual-module" 的代码
      }
      return null; // 其他模块照常处理
    }
  };
}
// rollup.config.js
import myExample from './rollup-plugin-my-example.js';
export default ({
  input: 'virtual-module', 
  plugins: [myExample()], // 使用插件
  output: [{
    file: 'bundle.js',
    format: 'es'
  }]
});
// bundle.js
import text from "virtual-module"
console.log(text) // 输出:This is virtual!
当 import text from "virtual-module" 时,相当于引入了这段代码:export default "This is virtual!"


宏观层面的兼容架构


Vite 需要兼容 Rollup 插件生态,就需要 Vite 能够像 Rollup 一样,能够解析插件对象,并对插件的钩子进行正确的执行和处理

1686387600789.png


这需要 Vite 在其内部,实现一个模拟的 Rollup 插件机制,实现跟 Rollup 一样的对外的插件行为,才能兼容 Rollup 的插件生态

Vite 里面包含的一个模拟 rollup,由于只模拟插件部分,因此在 Vite 源码中,它被称为 PluginContainer(插件容器)


微观层面的实现


实现 Rollup 的插件行为,实际上是实现相同的插件钩子行为。

插件钩子是在构建的不同阶段调用的函数。钩子可以影响构建的运行方式提供有关构建的信息或在构建完成后修改构建

钩子行为,主要包括以下内容:

  • 实现 Rollup 插件钩子的调度
  • 提供 Rollup 钩子的 Context 上下文对象
  • 对钩子的返回值进行相应处理
  • 实现钩子的类型

什么是钩子的调度?

按照一定的规则,在构建对应的阶段,执行对应的钩子。

例如:当 Rollup 开始运行时,会先调用 options 钩子,然后是 buildStart

下图为 Rollup 的 build 构建钩子(output generation 输出生成钩子不在下图)

1686387565283.png

什么是钩子的  Context 上下文对象?

在 Rollup 的钩子函数中,可以调用 this.xxx 来使用一些 Rollup 提供的实用工具函数,Context 提供属性/方法可以参考 Rollup 官方文档

而这个 this 就是钩子的 Context 上下文对象。

Vite 需要在运行时,实现一套相同的 Context 上下文对象,才能保证插件能够正确地执行 Context 上下文对象的属性/方法。

什么是对钩子的返回值做相应的处理?

部分钩子的返回值,是会影响到 Rollup 的行为。

例如:


export default function myExample () {
  return {
    name: 'my-example',
    options(options) {
      // 修改 options
      return options
    }
  };
}

options 钩子的返回值,会覆盖当前 Rollup 当前的运行配置,从而影响到 Rollup 的行为。

Vite 同样需要实现这个行为 —— 根据返回值做相应的处理。每个钩子的返回值(如果有),对应的处理是不同的,都需要实现

什么是钩子类型?

钩子分为 4 种类型:

  • async:钩子函数可以是 async 异步的,返回 Promise
  • first:如果多个插件都实现了这个钩子,那么这些钩子会依次运行直到一个钩子返回的不是 null 或 undefined的值为止。
  • sequential:如果有几个插件实现了这个钩子,串行执行这些钩子
  • parallel:如果多个插件都实现了这个钩子,并行执行这些钩子

例如: options 钩子,是 asyncsequential 类型,options 钩子可以是异步的,且是串行执行的,因为配置会按顺序依次被覆盖修改,如果是并行执行 options,那么最终的配置就会不可控

Vite 同样需要实现这些钩子类型


插件容器


前面小节已经说过,插件容器,是一个小的 Rollup,实现了 Rollup 的插件机制

插件容器实现的功能如下:

  • 提供 Rollup 钩子的 Context 上下文对象
  • 对钩子的返回值进行相应处理
  • 实现钩子的类型

注意:插件容器的实现,不包含调度。调度是 Vite 在其运行过程中,使用插件容器的方法实现的

插件容器的简化实现如下:


const container = {
  // 钩子类型:异步、串行
  options: await (async () => {
    let options = rollupOptions
    for (const plugin of plugins) {
      if (!plugin.options) continue
      // 实现钩子类型:await 实现和异步和串行,下一个 options 钩子,需要等待当前钩子执行完成
      // 实现对返回值进行处理:options 钩子返回值,覆盖当前 options
      options = (await plugin.options.call(minimalContext, options)) || options
    }
    return options;
  })(),
  // 钩子类型:异步、并行
  async buildStart() {
    // 实现并行的钩子类型:用 Promise.all 执行
    await Promise.all(
      plugins.map((plugin) => {
        if (plugin.buildStart) {
          return plugin.buildStart.call(
            new Context(plugin) as any,
            container.options as NormalizedInputOptions
          )
        }
      })
    )
  },
  // 钩子类型:异步、first 优先 
  async resolveId(rawId, importer) {
    // 上下文对象,后文介绍
    const ctx = new Context()
    let id: string | null = null
    const partial: Partial<PartialResolvedId> = {}
    for (const plugin of plugins) {
      const result = await plugin.resolveId.call(
        ctx as any,
        rawId,
        importer,
        { ssr }
      )
      // 如果有函数返回值 result,就直接 return,不执行后面钩子了
      if (!result) continue;
      return result;
    }
  }
  // 钩子类型:异步、优先
  async load(id, options) {
    const ctx = new Context()
    for (const plugin of plugins) {
      const result = await plugin.load.call(ctx as any, id, { ssr })
      if (result != null) {
        return result
      }
    }
    return null
  },
  // 钩子类型:异步、串行
  async transform(code, id, options) {
    // transform 钩子的上下文对象,不太一样,因为多了一些需要处理的工具函数。不需要深究
    const ctx = new TransformContext(id, code, map as SourceMap)
    for (const plugin of plugins) {
      let result: TransformResult | string | undefined
      try {
        result = await plugin.transform.call(ctx, code, id)
      } catch (e) {
        ctx.error(e)
      }
      if (!result) continue;
      code = result;
    }
    return {
      code,
      map: ctx._getCombinedSourcemap()
    }
  },
  // ...省略 buildEnd 和 closeBundle
}

上面代码,已经是实现了下面的两个内容:

  • 对钩子的返回值进行相应处理
  • 实现钩子的类型

Context 上下文对象,提供了很多实用工具函数


class Context implements PluginContext {
  parse(code: string, opts: any = {}) {
  // 省略实现
  }
  async resolve(
    id: string,
    importer?: string,
    options?: { skipSelf?: boolean }
  ) {
   // 省略实现
  }
  // ...省略
}

我们大概知道有这么个东西就行了,不需要知道具体的实现工具函数是怎么实现的。感兴趣的可以查看 Rollup 文档

插件的调度是如何实现的?

插件容器要怎么使用?

这两个问题,其实是同一个问题,当需要调度时,就要使用插件容器了。

例如:当 Server 启动时,会调用 listen 函数进行端口监听,这时候就会调用 containerbuildStart 函数,执行插件的 buildStart 钩子


httpServer.listen = (async (port: number, ...args: any[]) => {
  if (!isOptimized) {
    try {
      await container.buildStart({})
      // 其他逻辑
    } catch (e) {
      httpServer.emit('error', e)
      return
    }
  }
  return listen(port, ...args)
})

这就是在构建对应的阶段,执行对应的钩子

而在哪些阶段,分别调用了什么钩子,本篇文章则不过多介绍了


总结


至此,Vite 兼容 Rollup 的方式已经讲完了~

我们先介绍了兼容的概念, Vite 兼容的是 Rollup 插件生态,而不是 Rollup 这个工具。从而得出,Vite 需要实现 Rollup 插件生态的结论

然后围绕 Rollup 插件生态,我们介绍了什么是 Rollup 插件钩子,并从宏观和微观,分别介绍了兼容的架构(PluginContainer)和需要实现的细节:

  • 实现 Rollup 插件钩子的调度
  • 提供 Rollup 钩子的 Context 上下文对象
  • 对钩子的返回值进行相应处理
  • 实现钩子的类型

最后用简单的代码,实现了一个 PluginContainer,并介绍了,如何实现插件钩子的调度。

学完本篇内容,大概也就知道了 Rollup 钩子的相关生态了,如果我们需要实现一套插件生态,也可以对 Rollup 进行模仿。另外也学会了,如何用一个工具,去兼容另外一套工具的生态 —— 实现其对外的 API 能力


最后


如果这篇文章对您有所帮助,请帮忙点个赞👍,您的鼓励是我创作路上的最大的动力。

目录
相关文章
|
4月前
|
前端开发 JavaScript API
Vite 3.0 正式发布,下一代前端构建工具!
Vite 3.0 正式发布,下一代前端构建工具!
|
5月前
|
JSON 资源调度 前端开发
前端工具 Prettier 详细使用流程(兼容ESLint)
前端工具 Prettier 详细使用流程(兼容ESLint)
80 0
|
5月前
|
JSON 移动开发 资源调度
前端工具 Prettier 详细使用流程(兼容ESLint)
前端工具 Prettier 详细使用流程(兼容ESLint)
83 0
|
4月前
|
缓存 前端开发 JavaScript
深入了解rollup(三)插件机制
Rollup 插件是一个对象,具有属性]、构建钩子 和 输出生成钩子 中的一个或多个,并遵循我们的约定。插件应作为一个导出一个函数的包进行发布,该函数可以使用插件特定的选项进行调用并返回此类对象。 插件允许你通过例如在打包之前进行转译代码或在node_modules文件夹中查找第三方模块来自定义 Rollup 的行为。
63 1
|
4月前
|
JSON 前端开发 JavaScript
深入了解rollup(四)插件开发示例
Rollup是一个JavaScript模块打包器,它可以将多个模块打包成一个单独的文件,以便在浏览器中使用。与其他打包工具相比,Rollup的主要优势在于它可以生成更小、更快的代码。在本文中,我们将深入了解Rollup的插件开发。
40 1
|
8月前
|
JavaScript
为老的vueCli项目添加vite支持
为老的vueCli项目添加vite支持
77 0
为老的vueCli项目添加vite支持
|
10月前
|
缓存 前端开发 JavaScript
前端工具Vite的出现解决了什么?
在 ESM 出现之前,Javascript 是没有一个标准的模块方案。 比如说 `CJS` 是用于 Node 服务端的模块化方案,`AMD` 是用于浏览器的模块化方案。为了解决这个模块共用性问题,出现了 `UMD` 用于兼容这两种模块规范。 鉴于上面共用性问题,实际开发中配置的打包方式,采用的还是 UMD 模式。因为这样可以避免打包而产生的规范问题,并且在 ESM 不能使用的情况下也会选择 UMD。
90 0
前端工具Vite的出现解决了什么?
|
11月前
|
JavaScript 前端开发
从0搭建Vue3组件库(十一): 集成项目的编程规范工具链(ESlint+Prettier+Stylelint)
从0搭建Vue3组件库(十一): 集成项目的编程规范工具链(ESlint+Prettier+Stylelint)
164 0
|
11月前
|
前端开发 JavaScript 开发者
前端工程化打包工具之Rollup
Rollup是一个非常流行的前端工程化打包工具,它可以帮助开发者快速构建具有可重用性和可维护性的前端项目,并且提供了完善的依赖管理和打包机制。
148 3
|
11月前
|
JavaScript 前端开发 API
Vite 是如何使用 Rollup 进行构建的
Vite 是如何使用 Rollup 进行构建的
275 0