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 能力


最后


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

目录
相关文章
|
6月前
|
前端开发 JavaScript API
Vite 3.0 正式发布,下一代前端构建工具!
Vite 3.0 正式发布,下一代前端构建工具!
136 0
|
3天前
|
JavaScript 前端开发
如何在项目中集成 Babel?
如何在项目中集成 Babel?
12 3
|
3天前
|
JavaScript 前端开发 安全
Babel 插件的未来发展趋势是什么?
Babel 插件的未来发展趋势是什么?
12 4
|
19天前
|
开发框架 移动开发 前端开发
除了 HMR 插件,还有哪些技术可以实现热更新?
【10月更文挑战第23天】不同的热更新技术都有其特点和适用场景。开发者需要根据项目的具体需求和技术架构,选择合适的热更新技术来提高开发效率和用户体验。同时,随着技术的不断发展,热更新技术也在不断创新和完善,未来可能会出现更多更先进的热更新技术和方法。
|
21天前
|
前端开发 JavaScript 开发工具
Vite 4.0 发布,下一代的前端工具链
【10月更文挑战第21天】Vite 4.0 的发布标志着前端开发领域的又一次重要进步。它为开发者带来了更高效、更智能、更具创新性的开发体验,正逐渐成为下一代前端工具链的引领者。
|
4月前
|
JavaScript
Vite 项目中如何去集成 Sass
Vite 项目中如何去集成 Sass
62 0
|
6月前
|
资源调度 JavaScript 前端开发
Vite:下一代前端构建工具的快速上手
Vite 是由 Vue.js 的作者尤雨溪开发的下一代前端构建工具,它以其快速的冷启动、按需编译和热更新能力而受到广泛关注。Vite 通过利用浏览器原生的 ES 模块导入功能,提供了几乎即时的开发环境启动速度和高度优化的开发体验。
99 2
|
6月前
|
资源调度 JavaScript 开发者
插件使用:扩展Vue功能与第三方插件
【4月更文挑战第23天】Vue凭借其轻量级和灵活性在Web开发中备受青睐,而插件则进一步增强了其功能。本文探讨了如何在Vue项目中选择、安装、配置和管理插件,以适应不同需求。要点包括:选择可靠且兼容的插件,使用npm或yarn安装,根据文档配置,以及注意性能影响。明智使用插件能提升开发效率,但需避免过度依赖,确保与项目目标和技术栈匹配。不断学习新插件,可保持技术领先并优化项目实践。
73 0
|
6月前
|
缓存 前端开发 JavaScript
深入了解rollup(三)插件机制
Rollup 插件是一个对象,具有属性]、构建钩子 和 输出生成钩子 中的一个或多个,并遵循我们的约定。插件应作为一个导出一个函数的包进行发布,该函数可以使用插件特定的选项进行调用并返回此类对象。 插件允许你通过例如在打包之前进行转译代码或在node_modules文件夹中查找第三方模块来自定义 Rollup 的行为。
136 1
|
JavaScript
为老的vueCli项目添加vite支持
为老的vueCli项目添加vite支持
131 0
为老的vueCli项目添加vite支持