前言
在这篇文章中,我们再聊三个插件,Vite在构建阶段的主要插件基本上就告一段落了。
vite:terser
VITE集成terser进行代码压缩,关于terser这个库,只要对构建工具稍微有了解的同学应该都知道吧,我就不多说了。
我们来看一下VITE是如何处理的。
VITE不会对每个JS文件进行处理,VITE处理的是打包之后的Chunk,所以这个插件选择在renderChunk这个生命周期进行处理的。
只有配置了minify的选项,VITE才会应用terser的压缩。
以下是加载terser的worker的实现,加载了terser,并且将terser的
minify方法暴露出去:
最后是压缩Chunk的源码,返回压缩的结果。
最后多说一句题外话,
Rollup也提供了terser的插件集成,VITE开发这个插件的意义我有点儿没搞明白。
vite:esbuild-transpile
之前我们已经聊过了vite的内置插件vite:esbuild,使用这个插件可以用来编译TS文件等操作。
既然都已经有这个插件了,vite为什么还要提供vite:esbuild-transpile这个插件呢,我个人的一点儿理解是从生命周期的选择上不同。vite:esbuild采用的是transform生命周期,而vite:esbuild-transpile这个插件采用的是renderChunk,transform生命周期是会对每个命中的文件进行转换,而renderChunk这个生命周期执行的时候,某些文件集合已经打包成功了,我猜测vite:esbuild-transpile插件的目的是在Chunk上进行最后的编译优化。
vite:esbuild-transpile的实现跟vite:esbuild大致差不多,唯独就是处理的时机不同。
那个
transformWithEsbuild这个方法跟vite:esbuild调用的是完全一样的,我们在此就不再关注了。
vite:build-html
在之前我们聊VITE的构建阶段时,为了了解VITE扩展的生命周期transformIndexHtml,我们简单的看过一下这个插件,但是当时我们的重点不在这个插件上,所以就草草了事了。
这篇文章,这个插件是我们研究的重点。
在插件的开头,VITE就在解析hook,我们接下来看一下
resolveHtmlTransforms这个方法:
它把实现了
transformIndexHtml的插件都捞出来了,然后按pre->normal->post的顺序返回。
然后,我们看VITE添加的几个自定义Hook:
遍历
define的配置,用以替换Html文件中的import.meta.env.*引用到的环境变量:
看起来是修正某些标签的内容,对于我们来说,不用特别关注。
再次处理一下importMap:
前期的准备工作做完了,我们接着准备看一下核心代码实现。
我们先看transform生命周期里面做了什么:
只处理html文件:
接着是预处理html,将之前准备好的
preHook集合应用。
我们需要看一下
applyHtmlTransforms这个方法里面完成了什么工作。
目前看起来还是算比较简单的,以sequential的方式执行这些hook,上一个hook返回的内容(若有),传递个下一个hook,然后根据最终返回过来的内容,决定是以字符串处理,还是以标签的形式处理。
在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中资源的函数:
接着,需要对Html进行抽象语法树的遍历了,所以我们需要先看一下
traverseHtml是什么?
在这个方法里面,VITE加载的是
parse5,然后把遍历AST的vistor让外界传入。
这儿,有的同学可能会有疑问,为什么要用parse5还要进行AST的遍历,搞这么复杂干嘛?直接用DOM的API操作不就行了吗?
您还别说,这是真不行,因为在这个位置我们所处的环境是Node,并没有原生的DOM操作API,所以我们才会大费周章的用AST的方式来遍历HTML。
我们展开看一下是如何处理script的:
首先是获取到脚本的信息,如果是来源于
public目录的,只需要修正一下base即可。
如果是ESM类型的脚本,还需要考虑TreeShaking相关的问题(因为ESM是可以静态分析的,才有TreeShaking的可能)。
对于脚本,如果是
<script src="xxx" type="module"></script>的话,VITE会插入普通的引用:
如果是
<script type="module"> /*写入了一些脚本代码*/ </script>,VITE会插入代理引用:
然后对于其它非script资源,也做这个处理。
经过上面的操作之后,资源和脚本基本上处理完成了,是时候把处理的结果反应到结果上了。
最后,如果配置了modulePolyfill的话,需要把VITE内部添加的那个polyfill资源也要附加在源码上,
transform阶段就算完成了。
最终,我们比较一下源码和编译得到的结果:
源码:
编译结果:
可以看出,在这个阶段,VITE完成的工作主要就是处理自定义的
transformIndexHtml生命周期,然后把html引入的脚本和资源进行转换,等待下一阶段的处理,对于非lib模式,这个index.html其实就是我们在Rollup学习到的打包入口文件。
下一个阶段是generateBundle的处理了。
在generateBundle中,定义的那些复杂的函数,我们一律删繁就简,直接看主要的处理过程。
拿到HTML的内容:
此刻拿到的HTML内容已经经过预处理了,比如script和css已经提取出来了,但是现在还没有把打包结果注入回html中,所以我们看到的内容是这样的:
然后找到主入口Chunk:
chunk里面的信息:
接着,把准备把打包结果的script注入回html文件中:
然后,把css也注入回html中:
现在的html中已经多了一些内容了,但是此刻还是VITE内部的占位符:
再次处理
transformIndexHtml生命周期:
把资源标识符替换成最终生成的内容:
可以看到,此时引用的内容已经是打包之后的产物了:
最后,通过Rollup的
emitFile上下文,把这个结果添加到bundle中:
大功告成!
我们简单总结一下vite:build-html这个插件的处理流程,在transform中解析到html中所有导入的script、style、其它静态资源,然后作为index.html的依赖资源绑定给index.html,然后在generateBundle生命周期把index.html依赖的资源找到,处理好之后再写回至index.html,最后把得到的index.html的内容写入Rollup的bundle中。
在这个处理期间,VITE对transformIndexHtml为pre的生命周期提取到transform生命周期中执行,normal或post的生命周期提取到generateBundle中执行,因此,这个特性,使得如果我们不了解源码的话,编写生命周期处理html时需要尤其小心。
结语
在这篇文章中,我们又学习了三个插件,尤其是对vite:build-html插件进行了细致的分析,明确了VITE扩展的transformIndexHtml生命周期是在Rollup的已有的transform和generateBundle生命周期中执行的。
所以,如果我们需要对index.html进行处理的时候,尤其需要注意插件的执行时机的问题。
在最近的3篇文章中,我们已经研究了很多VITE的核心内置插件了,这篇文章结束后我们将暂时不探讨插件了,从下篇文章开始,我们将会正式对VITE的DEV流程进行探索和分析。