前言
本文是应掘金的一个小伙伴的私信撰写的一篇文章,因为我个人其实之前也没有仔细分析过vite打包之后的构建产物,面对有些问题的时候就觉得好像有点儿含糊其辞,所以想着就趁着这个契机,好好去研究一番吧。
为什么要研究这个东西呢?因为只有明白了它的构建产物特征,才能够更好的优化项目的首屏渲染性能,所谓知己知彼,百战不殆。
本文的内容相对来说不是特别的难,对此有兴趣的读者可以参照我的方式自己动手做一下,分析一下自己项目的构建产物。
好了,废话就不多说了,我们开始进入正题吧。
1、项目配置
大家可以直接使用vite创建一个项目,然后写一些代码示例开始分析。我们主要分析的是组件是如何引入的,如果是异步组件,需要分析代码拆分之后是如何实现按需加载的。
为了不让第三方库干扰我们的业务代码,我们把Vue拆分出去,作为UMD的形式引入,这样打包结果除了vite生成的辅助代码,剩下的就全是业务代码,比较方便阅读。
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import inspect from "vite-plugin-inspect";
import legacy from "@vitejs/plugin-legacy";
import cdn from "vite-plugin-cdn-import";
// https://vitejs.dev/config/
export default defineConfig({
// base: "./",
build: {
minify: false,
rollupOptions: {},
},
plugins: [
vue(),
inspect(),
legacy(),
cdn({
prodUrl: 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js',
modules: [
{
name: "vue",
var: "Vue",
path: ``,
},
],
}),
],
});
以上是我的vite配置,给大家介绍一下为什么要这样配,配置minify
,可以使得最终打包的结果不会被压缩,配置legacy
插件主要是为了搞清楚在不支持ESM的浏览器,打包产物是如何做降级处理的,剩下的那个vite-plugin-cdn-import
插件就是为了把vue
作为UMD的方式引入(这个插件大家可以记一下,在实际的项目中,我们会用它把一些不常变的库(Vue, React,ReactDOM,axios,lodash等)抽离,采用UMD的形式加载,用户打开网页的时候,浏览器能够更好的利用缓存,从而提高网页的首屏加载速度)。
(大家可以看到我配了base
,最后又注释了,因为如果采用file协议加载的话,加载UMD的Vue会报跨域错误,所以我就不配了,通过LiveServer把vite构建产物跑起来,查看运行效果)
我定义了一个异步组件的导入:
其它就是正常的代码引入了,就跟大家平时写的业务代码是一样的,没有什么值得说道的,为了篇幅,就不再赘述。
2、产物分析
我们需要分别查看vite在是否引入legacy
插件的产物区别,探索vite是如何根据浏览器的能力进行兼容性处理的。
2.1、不含Legacy的产物分析
产物不多,但是还是值得说道,我们慢慢的分析。
在
index.html
中引入的js直接以浏览器原生支持的ESM的形式导入的,其它没有什么特别的,最顶层的script是插件生成的Vue的UMD导入。
在index.[hash].js中这个文件中有很多内容,我们逐一的来看。
首先是一个对于浏览器不支持modulepreload
的polyfill:
关于
modulepreload
的作用,大家请查阅MDN,modulepreload。
上述代码的主要目的是使用MutationObserver
监控DOM的变化,如果发现有link标签动态的插入,就使用fetch去加载这些内容。
接着,是vite定义的辅助函数preload:
上述代码中,baseModule是一个回调函数,当预加载成功或失败的时候,执行baseModule函数,其余就没有什么好说的了,主要是为了分辨预加载的是脚本还是样式表。
好了,接下来是一个辅助函数:
如果你阅读过我这个系列的前面两篇文章的话,这个辅助函数你应该非常熟悉了(而且这个函数还是从虚拟模块中导出的😁),这个函数的目的是为了让两个对象进行合并,Vue定义这个的目的是什么呢,是为了给组件挂载一个自己的
scopeId
,这样我们编写的scoped
的样式才能跟scopedId
匹配,形成可以能够针对组件管辖的DOM内容生效的样式,形成样式隔离。
再接着,我们调一个组件来看看,我就随便挑了一个:
从编译的结果可以得出结论,我们写在单文件组件中的
template
代码其实仅仅是Render函数的语法糖,最终都会转换成Render函数,因为直接编写Render函数对开发人员的能力要求比较高,而且写起来也不直接,而html对我们前端开发人员来说相当直接,申明式的语法学习难度较低,所以这也是为什么Vue能够迅速的风靡前端圈的一个重要原因。
然后,再看看异步组件是如何引入的:
因为没有依赖,所以这个
import
函数直接就被执行了,然后Vue的defineAsyncComponent
函数就知道如何去解析这个Promise
的内容,关于这个函数的源码,大家就自行查阅Vue的源码了,本文就不做解析了。
最后,不要忘了,这个辅助函数是对外导出了,因为别人也需要。
可以看到,在那个被拆分的组件里面用到了,这个被拆分的组件代码也是平平无奇的。
目前我们已经完成了至少60%的内容了,在不支持ESM的浏览器中,vite还需要再做一个处理,我们下一小节再分析。
2.2、开启Legacy插件配置
在使用这个插件时, 我是没有配任何的broswerlist信息的,如果你对兼容的浏览器还有要求,请参考broswerlist的配置。
在开启Legacy插件之后,可以明显看到多了几个带legacy前缀的文件,接下来,我们就来分析看看这些文件跟之前不带legacy前缀文件的差异。
首先,vite先做了一个浏览器的嗅探:
如果在不支持ESM的浏览器里面,
import.meta.url
直接就报错了,也就是说要都支持这些能力的话,就可以认为当前的浏览器是支持ESM的,就可以放心的进行标记了。
接着是处理不支持ESM的浏览器应该如何渲染页面的逻辑:
如果当前环境支持ESM就不要执行这段代码,否则,要使用
SystemJS
去加载后面引入的vite-legacy-entry
的代码。
@vitejs/plugin-legacy
是通过SystemJS来兼容较老的浏览器支持的,关于SystemJS,大家可以直接查阅Github的内容,本文不做解读,systemjs/systemjs: Dynamic ES module loader (github.com),根据我的开发经验,看起来跟RequireJS
有点儿像。(RequireJS是采用的AMD
加载风格,不过随着Webpack的兴起,我后续几乎也没有再看到谁家产品使用,几乎也算是退出了历史吧)。
在这些标签里面,都写上了nomodule
的配置,这是告诉浏览器,在支持ESM的浏览器上不要执行这些代码:
关于这个nomodule,大家可以自行查阅MDN,script。
上述的这个立即执行函数的含义如下:
-
检测浏览器是否支持 ES6 模块:通过检测
noModule
和onbeforeload
属性。 -
添加事件监听器:监听所有资源的
beforeload
事件,如果浏览器已经尝试加载过type="module"
的脚本,那么阻止带有nomodule
属性的脚本加载。 -
触发一次模块化加载:通过动态插入并移除一个
type="module"
的脚本,检测是否支持模块化脚本。 -
阻止
nomodule
脚本的加载:如果浏览器支持模块化脚本,则阻止带有nomodule
属性的脚本。
在一些不支持 nomodule
的旧浏览器中,<script nomodule>
标签不会自动被阻止执行。这段代码确保当浏览器支持模块时,带有 nomodule
的脚本不会被加载,以避免重复加载或加载不必要的代码。这对确保在现代浏览器中使用现代模块化代码,而在旧浏览器中回退到非模块化代码的策略至关重要。
关于那个polyfill-legacy-[hash].js
就不在本文中阐述了,完全是一些兼容代码,没有研究的价值。
接着是index-legacy主文件:
大部分代码我们都已经见识过了,是之前研读过的业务代码。只不过区别就是必须要包裹在SystemJS的辅助函数内中执行。
上一节中,我们提到过的用于生成scopeId
的辅助函数,也是通过SystemJS提供的exports关键字对外暴露的。
不过大家可以看到,这个函数明显变复杂了,因为当前环境不支持
Symbol.iterator
了,那么只能老老实实的去定义辅助函数,完成for-of
的遍历了呀。
然后,我们再看看异步组件是如何导入的:
是在SystemJS的能力支持下导入的。
最后,再看看那个被拆分出来的异步组件是什么样子了:
引入了之前index.js暴露出来的辅助函数,然后把自己要执行的内容挂载到SystemJS执行的核心回调函数上,等待执行。
至此,我们就把vite构建的产物分析完成了。
总结
vite通过@vitejs/plugin-legacy
提供传统浏览器的支持,有了这个插件的参与,vite会打包出两份运行时内容,一份是基于SystemJS的、能够在不支持ESM的浏览器上运行,同时也打包了一份直接再支持ESM的浏览器运行的内容。
vite构建的产物在运行之初就会嗅探浏览器对ESM的支持情况,进而决定使用什么策略的文件运行。虽然同时打包出了两份文件,但是vite通过自己的策略并不会使得两份文件同时执行,自然也不会加载两份文件,导致网络传输的压力增加。
同时,我们在学习的过程中还看到了Vue单文件组件中的template
的被打包成了Render
函数直接执行,一方面我们明白了template确实是Render函数的一个语法糖,在另一方面,因为打包产物已经不包含任何需要解析的template内容了,所以在这种场景下,我们可以仅仅加载Vue的运行时(即去除了编译器的Vue),这样也可以节省一部分的体积,从而有效提高网页的加载速度。
本文中没有对SystemJS做详细的解读,有兴趣的读者可以自行研究。
本文是笔者根据自己的开发经验结合网络上搜集的资料所著,文章中可能存在错误,如果有任何问题或纰漏可以联系作者修改,谢谢大家的支持。