likes
comments
collection
share

[陈同学i前端] 一起学Vite|插件机制与流水线

作者站长头像
站长
· 阅读数 16

前言

大家好,我是陈同学,一枚野生前端开发者,感谢各位的点赞、收藏、评论

近年来,前端领域技术更新迭代节奏较快,前端工程师们为了更好的进行项目开发、测试、构建、部署,开发出了各种各样的构建工具

像常见的Webpack、Rollup、Esbuild、Vite,每一类工具都有它的特点,均致力于提高前端领域的工程化水平

而工具出现的目标是解决前端工程当中的一些影响通性问题

常见的痛点(需求点)有:模块化需求(ESM)、兼容高级语法、代码质量测试、静态资源处理、代码压缩、开发效率等

本节我们继续进行Vite知识的学习,具体安排如下:

  • 一起学Vite|初识下一代的前端工具链
  • 一起学Vite|原来这玩意叫依赖预构建
  • 一起学Vite|实现第一个Vite插件
  • 一起学Vite|插件机制与流水线(本节)
  • 一起学Vite|HMR,你好👋
  • 一起学Vite|模块联邦——代码共享的终极解决方案
  • 一起学Vite|简单手写开发服务器
  • 一起学Vite|简单手写打包器

本文阅读成本与收益如下:

阅读耗时:7mins

全文字数:7k+

预期效益

  • 了解Vite插件机制
  • 了解Vite插件钩子分类
  • Vite插件钩子执行顺序
  • Vite插件流水线顺序

Vite插件机制

Vite 的插件机制是与 Rollup 兼容的,但它在开发和生产环境下的实现稍有差别

  • 生产环境: Vite 直接调用 Rollup 进行打包
  • 开发环境: Vite 模拟了 Rollup 的插件机制,通过 PluginContainer 插件容器对象调度执行各个插件

[陈同学i前端] 一起学Vite|插件机制与流水线

如上图所示,Vite 插件机制是通过 mock Rollup插件机制进行实现,核心实现主要基于两个部分:

  • PluginContainer插件容器:实现开发环境下Rollup插件钩子调度
  • PluginContext上下文对象:实现插件钩子内部执行时共享上下文信息
export interface PluginContainer {
  options: InputOptions
  getModuleInfo(id: string): ModuleInfo | null
  buildStart(options: InputOptions): Promise<void>
  resolveId(
    id: string,
    importer?: string,
    options?: {
      assertions?: Record<string, string>
      custom?: CustomPluginOptions
      skip?: Set<Plugin>
      ssr?: boolean
      /**
       * @internal
       */
      scan?: boolean
      isEntry?: boolean
    }
  ): Promise<PartialResolvedId | null>
  transform(
    code: string,
    id: string,
    options?: {
      inMap?: SourceDescription['map']
      ssr?: boolean
    }
  ): Promise<SourceDescription | null>
  load(
    id: string,
    options?: {
      ssr?: boolean
    }
  ): Promise<LoadResult | null>
  close(): Promise<void>
}

type PluginContext = Omit<
  RollupPluginContext,
  // not supported
  | 'load'
  // not documented
  | 'cache'
  // deprecated
  | 'moduleIds'
>

插件容器

插件容器实际上是一个包含多种插件钩子调度执行方法的对象,每种钩子的调度执行方法有着独特的调度特点以及逻辑

const plugins = [pluginA, pluginB, pluginC, pluginD, pluginE] // plugins为Vite根据用户配置已经整理好的插件数组,每个元素都为一个插件对象
const container = {
  // 异步串行钩子
  options: await (async () => {
    let options = rollupOptions
    for (const plugin of plugins) {
      if (!plugin.options) continue
      options =
        (await plugin.options.call(minimalContext, options)) || options
    }
    return options;
  })(),
  // 异步并行钩子,此处省略了sequential情况处理
  async buildStart() {
    await Promise.all(
      plugins.map((plugin) => {
        if (plugin.buildStart) {
          return plugin.buildStart.call(
            new Context(plugin) as any,
            container.options as NormalizedInputOptions
          )
        }
      })
    )
  },
  // 异步优先钩子
  async resolveId(rawId, importer, options) {
    // 上下文对象,后文介绍
    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,
        {
            // ...
        }
      )
      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, {
        // ...
      })
      if (result != null) {
        return result
      }
    }
    return null
  },
  // 异步串行钩子
  async transform(code, id, options) {
    const inMap = options?.inMap
    const ssr = options?.ssr
    const ctx = new TransformContext(id, code, inMap as SourceMap)
    ctx.ssr = !!ssr
    for (const plugin of plugins) {
      let result: TransformResult | string | undefined
      try {
        result = await plugin.transform.call(ctx as any, code, id, { ssr })
      } catch (e) {
        ctx.error(e)
      }
      if (!result) continue;
      // 省略 SourceMap 合并的逻辑
      code = result;
    }
    return {
      code,
      map: ctx._getCombinedSourcemap()
    }
  },
  async close() {
    if (closed) return
    const ctx = new Context()
    await hookParallel(
        'buildEnd',
        () => ctx,
        () => []
    )
    await hookParallel(
        'closeBundle',
        () => ctx,
        () => []
    )
    closed = true
  }
}

有了插件容器,Vite便可以在适当的时候调用对应的插件容器方法进行各个插件钩子逻辑的调度执行

开发者只需要按照约定开发好一个插件,在Vite配置的plugins属性添加插件对象,后续的此插件内的逻辑执行时机便由Vite插件容器进行控制

这里为了加深理解,通过load钩子进行说明

[陈同学i前端] 一起学Vite|插件机制与流水线

load属于异步优先钩子函数,在每个传入模块请求(import)时被调用

Vite依次调度每个插件的load实现逻辑过程中,只要有一个插件的load方法实现能够正确处理并返回结果,则忽略后续插件实现的load方法逻辑

插件上下文

Vite中的插件上下文指的是在插件容器的某一钩子(如:load)管道开始执行时,新创建用于记录每一个异步钩子管道当前状态的对象,以便可以并发安全地跟踪该管道中的活动插件,并提供一些工具方法进一步提高插件钩子函数的灵活性

故在各种插件中的钩子被调用的时候,Vite插件容器的调度逻辑会强制为钩子函数的 this 绑定一个上下文对象

上下文对象类型结构如图所示:

[陈同学i前端] 一起学Vite|插件机制与流水线

Vite插件钩子

独有钩子与属性

  • enforce: pre 或 postpre 先执行
  • apply: buildserve函数,标记当前插件仅在 buildserve 执行环境下使用
  • config(config, env): 在 Vite 配置被解析之前修改 Vite 的相关配置。钩子函数接收 config(原始用户配置) 和 env(描述配置环境的变量)入参
  • configResolved(resolvedConfig): 在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置
  • configureServer(server): 主要用来 配置开发服务器
    • 注入前置中间件:钩子函数在内部中间件被安装前调用,可为 dev-server 添加自定义的前置中间件
    • 注入后置中间件:钩子函数返回值为一个函数(定义后置中间件)时,可为 dev-server 添加自定义的后置中间件
    • 存储服务器访问引用:实现其他插件钩子能够访问开发服务器实例
  • transformIndexHtml(html): 转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串转换上下文。上下文在开发期间暴露ViteDevServer实例,在构建期间暴露 Rollup 输出的包
  • handleHotUpdate(ctx): 执行自定义HMR更新,可以通过ws往客户端发送自定义的事件

通用钩子-构建阶段

  • options(options): 在服务器启动时被调用-获取、操纵 Rollup 选项,(严格意义上来讲,它执行于构建阶段之前)
  • buildStart(options): 开始构建流程时调用
  • resolveId(source, importer, options): 在每个传入模块请求(import)时被调用,用于处理传入模块文件的路径
  • load(id): 在每个传入模块请求(import)时被调用,通过自定义加载器,可定制返回的自定义格式内容
  • transform(code, id): 在每个传入模块请求(import)时被调用,用于转换单个模块文件内容
  • buildEnd(error?: Error): 在构建阶段结束后被调用

通用钩子-输出阶段

  • outputOptions(options): 接收输出参数
  • renderStart(outputOptions, inputOptions): bundle.generatebundle.write 调用时都会被触发
  • augmentChunkHash(chunkInfo): 为 chunk 增加自定义格式的 hash
  • renderChunk(code, chunk, options): 转译单个的 chunk 时触发。Rollup 输出每一个 chunk 文件的时候都会调用
  • generateBundle(options, bundle, isWrite): 在调用 bundle.write 之前立即触发这个 hook
  • writeBundle(options, bundle): 在调用 bundle.write 后,所有的 chunk 都写入文件后,最后会调用一次 writeBundle
  • closeBundle(): 服务器关闭时调用

插件钩子执行顺序

[陈同学i前端] 一起学Vite|插件机制与流水线

插件流水线生成顺序

Vite 所有的插件都通过resolvePlugins被收集起来

export async function resolvePlugins(
  config: ResolvedConfig, // 经过处理后的配置
  prePlugins: Plugin[], // 用户插件数组|enforce: 'pre'
  normalPlugins: Plugin[], // 用户插件数组|未设置enforce
  postPlugins: Plugin[] // 用户插件数组|enforce: 'post'
): Promise<Plugin[]> {
  const isBuild = config.command === 'build'
  // 收集生产环境构建的插件
  const buildPlugins = isBuild
    ? (await import('../build')).resolveBuildPlugins(config)
    : { pre: [], post: [] }

  return [
    // 1. 别名插件
    isBuild ? null : preAliasPlugin(),
    aliasPlugin({ entries: config.resolve.alias }),
    // 2. 用户插件(enforce: 'pre')
    ...prePlugins,
    // 3. Vite 核心构建插件
    // ...
    // 4. 用户插件(不带 enforce 属性)
    ...normalPlugins,
    // 5. Vite 生产环境插件
    definePlugin(config),
    cssPostPlugin(config),
    ...buildPlugins.pre,
    // 6. 用户插件(enforce: 'post')
    ...postPlugins,
    ...buildPlugins.post,
    // 7. 一些开发阶段特有的插件
    ...(isBuild
      ? []
      : [clientInjectionsPlugin(config), importAnalysisPlugin(config)])
  ].filter(Boolean) as Plugin[]
}

通过resolvePlugins处理后,我们能总结出Vite的插件执行顺序:

  • 别名处理:Alias
  • 设置enforce: 'pre'的用户插件
  • Vite 核心插件
  • 未设置enforce的用户插件
  • Vite 生产环境构建插件
  • 设置enforce: 'post'的用户插件
  • Vite 生产环境构建后置插件(minify, manifest, reporting)
  • 一些开发阶段特有的插件

即在每一个需要调度插件钩子能力的时机,Vite便会按照以上的顺序进行插件钩子函数的调用

讲到最后

在上一节文章中我们简单实现了一个Vite插件掌握了基本的插件开发技巧

本节我们开始对Vite的插件机制进行学习,首先我们明确了Vite插件机制是与Rollup兼容的,在生产环境下Vite直接调用RollupAPI进行打包,而在开发环境下Vite通过插件容器以及插件上下文模拟 Rollup 的插件机制,从而可以实现在开发环境下兼容Rollup插件机制的同时扩展Vite独有的能力

接着我们尝试理解插件容器内调度钩子函数的行为,发现每一种钩子函数有着对应的调度时机、特点以及逻辑,并且在调用插件容器内异步钩子管道方法时会创建一个上下文对象,记录每一个异步钩子管道当前状态的对象

最终我们通过查看resolvePlugins方法,了解到插件流水线生成顺序

阅读完本篇文章,希望大家都能够对Vite插件有一个更深层次的认知

谢谢大家,我们下节再见!!!

感谢各位看到这里,如果你觉得本节内容还不错的话,欢迎各位的点赞、收藏、评论,大家的支持是我做内容的最大动力

本文为作者原创,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利

参考补充

Vite官方文档

Rollup官方文档

Esbuild官方文档

Vue3文档

转载自:https://juejin.cn/post/7195218538863755324
评论
请登录