Vite源码学习(五)——Vite内置插件(四)

832 阅读6分钟

前言

在这篇文章中,我们再聊三个插件,Vite在构建阶段的主要插件基本上就告一段落了。

vite:terser

VITE集成terser进行代码压缩,关于terser这个库,只要对构建工具稍微有了解的同学应该都知道吧,我就不多说了。

我们来看一下VITE是如何处理的。

VITE不会对每个JS文件进行处理,VITE处理的是打包之后的Chunk,所以这个插件选择在renderChunk这个生命周期进行处理的。

只有配置了minify的选项,VITE才会应用terser的压缩。 image.png 以下是加载terser的worker的实现,加载了terser,并且将terser的minify方法暴露出去: image.png 最后是压缩Chunk的源码,返回压缩的结果。 image.png 最后多说一句题外话,Rollup也提供了terser的插件集成,VITE开发这个插件的意义我有点儿没搞明白。

vite:esbuild-transpile

之前我们已经聊过了vite的内置插件vite:esbuild,使用这个插件可以用来编译TS文件等操作。

既然都已经有这个插件了,vite为什么还要提供vite:esbuild-transpile这个插件呢,我个人的一点儿理解是从生命周期的选择上不同。vite:esbuild采用的是transform生命周期,而vite:esbuild-transpile这个插件采用的是renderChunktransform生命周期是会对每个命中的文件进行转换,而renderChunk这个生命周期执行的时候,某些文件集合已经打包成功了,我猜测vite:esbuild-transpile插件的目的是在Chunk上进行最后的编译优化

vite:esbuild-transpile的实现跟vite:esbuild大致差不多,唯独就是处理的时机不同。 image.png 那个transformWithEsbuild这个方法跟vite:esbuild调用的是完全一样的,我们在此就不再关注了。

vite:build-html

在之前我们聊VITE的构建阶段时,为了了解VITE扩展的生命周期transformIndexHtml,我们简单的看过一下这个插件,但是当时我们的重点不在这个插件上,所以就草草了事了。

这篇文章,这个插件是我们研究的重点。

image.png 在插件的开头,VITE就在解析hook,我们接下来看一下resolveHtmlTransforms这个方法: image.png 它把实现了transformIndexHtml的插件都捞出来了,然后按pre->normal->post的顺序返回。

然后,我们看VITE添加的几个自定义Hook: image.png image.png 遍历define的配置,用以替换Html文件中的import.meta.env.*引用到的环境变量: image.png 看起来是修正某些标签的内容,对于我们来说,不用特别关注。 image.png 再次处理一下importMap: image.png 前期的准备工作做完了,我们接着准备看一下核心代码实现。

我们先看transform生命周期里面做了什么:

只处理html文件: image.png 接着是预处理html,将之前准备好的preHook集合应用。 image.png 我们需要看一下applyHtmlTransforms这个方法里面完成了什么工作。

目前看起来还是算比较简单的,以sequential的方式执行这些hook,上一个hook返回的内容(若有),传递个下一个hook,然后根据最终返回过来的内容,决定是以字符串处理,还是以标签的形式处理。 image.png image.png 在VITE的transformIndexHtml生命周期中,我们既可以以字符串的形式处理Html,又可以以更高阶的办法处理html。

下面是钩子的完整签名,所以现在大家明白为什么了吧。

type IndexHtmlTransformHook = (
  html: string,
  ctx: {
    path: string
    filename: string
    server?: ViteDevServer
    bundle?: import('rollup').OutputBundle
    chunk?: import('rollup').OutputChunk
  },
) =>
  | IndexHtmlTransformResult
  | void
  | Promise<IndexHtmlTransformResult | void>

type IndexHtmlTransformResult =
  | string
  | HtmlTagDescriptor[]
  | {
      html: string
      tags: HtmlTagDescriptor[]
    }

interface HtmlTagDescriptor {
  tag: string
  attrs?: Record<string, string>
  children?: string | HtmlTagDescriptor[]
  /**
   * 默认: 'head-prepend'
   */
  injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
}

把自定义的transformIndexHtml实现之后,VITE会尝试处理HTML中的引用了。

定义处理Html中资源的函数: image.png 接着,需要对Html进行抽象语法树的遍历了,所以我们需要先看一下traverseHtml是什么? image.png 在这个方法里面,VITE加载的是parse5,然后把遍历AST的vistor让外界传入。

这儿,有的同学可能会有疑问,为什么要用parse5还要进行AST的遍历,搞这么复杂干嘛?直接用DOM的API操作不就行了吗?

您还别说,这是真不行,因为在这个位置我们所处的环境是Node,并没有原生的DOM操作API,所以我们才会大费周章的用AST的方式来遍历HTML。

我们展开看一下是如何处理script的: image.png 首先是获取到脚本的信息,如果是来源于public目录的,只需要修正一下base即可。

如果是ESM类型的脚本,还需要考虑TreeShaking相关的问题(因为ESM是可以静态分析的,才有TreeShaking的可能)。 image.png 对于脚本,如果是<script src="xxx" type="module"></script>的话,VITE会插入普通的引用: image.png 如果是<script type="module"> /*写入了一些脚本代码*/ </script>,VITE会插入代理引用: image.png 然后对于其它非script资源,也做这个处理。 image.png image.png 经过上面的操作之后,资源和脚本基本上处理完成了,是时候把处理的结果反应到结果上了。 image.png 最后,如果配置了modulePolyfill的话,需要把VITE内部添加的那个polyfill资源也要附加在源码上,transform阶段就算完成了。 image.png 最终,我们比较一下源码和编译得到的结果:

源码: image.png 编译结果: image.png 可以看出,在这个阶段,VITE完成的工作主要就是处理自定义的transformIndexHtml生命周期,然后把html引入的脚本和资源进行转换,等待下一阶段的处理,对于非lib模式,这个index.html其实就是我们在Rollup学习到的打包入口文件。

下一个阶段是generateBundle的处理了。

generateBundle中,定义的那些复杂的函数,我们一律删繁就简,直接看主要的处理过程。 image.png 拿到HTML的内容: image.png 此刻拿到的HTML内容已经经过预处理了,比如script和css已经提取出来了,但是现在还没有把打包结果注入回html中,所以我们看到的内容是这样的: image.png 然后找到主入口Chunk: image.png chunk里面的信息: image.png 接着,把准备把打包结果的script注入回html文件中: image.png 然后,把css也注入回html中: image.png 现在的html中已经多了一些内容了,但是此刻还是VITE内部的占位符: image.png 再次处理transformIndexHtml生命周期: image.png 把资源标识符替换成最终生成的内容: image.png 可以看到,此时引用的内容已经是打包之后的产物了: image.png 最后,通过Rollup的emitFile上下文,把这个结果添加到bundle中: image.png 大功告成! image.png

我们简单总结一下vite:build-html这个插件的处理流程,在transform中解析到html中所有导入的scriptstyle、其它静态资源,然后作为index.html的依赖资源绑定给index.html,然后在generateBundle生命周期把index.html依赖的资源找到,处理好之后再写回至index.html,最后把得到的index.html的内容写入Rollup的bundle中。

在这个处理期间,VITE对transformIndexHtml为pre的生命周期提取到transform生命周期中执行,normalpost的生命周期提取到generateBundle中执行,因此,这个特性,使得如果我们不了解源码的话,编写生命周期处理html时需要尤其小心。

结语

在这篇文章中,我们又学习了三个插件,尤其是对vite:build-html插件进行了细致的分析,明确了VITE扩展的transformIndexHtml生命周期是在Rollup的已有的transformgenerateBundle生命周期中执行的。

所以,如果我们需要对index.html进行处理的时候,尤其需要注意插件的执行时机的问题。

在最近的3篇文章中,我们已经研究了很多VITE的核心内置插件了,这篇文章结束后我们将暂时不探讨插件了,从下篇文章开始,我们将会正式对VITE的DEV流程进行探索和分析。