Vite系列:如何对模块请求进行transform
当我们创建一个vite+vue3的项目,然后执行npm run dev,很短的时间内,一个vue项目就run起来了。这个过程和传统的webpack项目很像,但是在通过浏览器访问localhost:5173时,在开发者工具的Network中我们能发现vite项目有一个很大的特点。
会发现加载的资源中有.vue后缀的文件,这与webpack先把所有的模块打包成一个chunk后,再由script标签加载一个打包好的chunk不同。
本文旨在聊一聊在dev模式下,Vite的是如何创建一个devServer的。过程中会涉及到浏览器支持module、rollup等知识,顺带着也会简单的介绍一下。
入口
在默认场景下,vue3是spa应用,浏览器客户端访问index.html后,通过script标签加载js模块
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
如示例项目中,src="/src/main.js"
,即当访问该html,会从相对路径下去访问main.js文件,并且加载脚本内容
观察响应内容,会发现返回的内容有些地方比较特殊
- import这种语法没有被转译,而是保留了es module的写法 [1]
- 导入依赖的path有和源码中相比有变化:从
import { createApp } from 'vue'
变成了import { createApp } from '/node_modules/.vite/deps/vue.js?v=f014d468'
[2]
关于ES Module
这个段落能够解答上面的[1]问题
需要补充一个背景,自ES6开始,第一次在语言层面上实现了模块功能即ES Module,能够取代commonJS及AMD规范,并且适用于浏览器客户端及服务端。
在当前已经全面放弃IE,开始拥抱现代浏览器的浪潮下,该模块规范也已经被现代浏览器如Chrome、Edge等实现。自此,浏览器支持原生的使用ES Module,只需要在<script>
中添加 type="module"
属性便能开启。
所以在Vite中,不再需要像Webpack一样事先把所有的JS模块进行编译,打包输出到一个chunk中。而是能按需的通过import
导入功能模块,浏览器会根据import
标签来实现目标资源的加载。下面举个例子:
如上图中在main.js
中,通过
import { createApp } from '/node_modules/.vite/deps/vue.js?v=f014d468'`
就能实现了发起一个http://localhost:5173/node_modules/.vite/deps/vue.js?v=f014d468
资源加载的请求,从而获得createApp
这个API。
关于ES Module暂时先讲这么多,后续会有一篇专门讲ES Module的文章,再给大家详细的说说它的原理、优点等。
Transform
从这儿开始解答上面的[2]问题
网络请求加载到的main.js
内容和源码不一样,这是因为在Vite的Server中,对main.js
的源码进行了一次transform,识别出了其中的依赖信息,并且将其修改为真实的path,简单画个示意图:
中间件
简单提一嘴什么是中间件,如果有用express或koa框架写过node应用的同学应该知道,如果没有相关经验也不慌,一个简单示意图了解一下:
通常我们创建一个Server后,就可以访问该Server,从请求到响应的返回,就经过图中的流程,可以看见请求会被各种中间件处理后一番,结果才返回给客户端。
关键词:洋葱模型。有兴趣的同学可以再搜索深入了解一下。
流程
我们把场景代入一下,比如其中某个中间件换成上文说的transformMiddleware
,请求/src/main.js
的,中间件去项目目录下寻址到对应的文件,读取其中的内容,发现import { createApp } from 'vue'
是一种从依赖中读取模块的行为,那么就把实际vue
指向实际的js模块文件(/node_modules/.vite/deps/vue.js?v=f014d468
)。又因为浏览器设置了type='module'
会识别到import
而发起请求加载模块,这样就实现了所有模块类似递归一样被陆续加载进来。
原理就是这样,不过也出现了几个新的点需要去了解
- transform这个过程代码里是怎么写的 [3]
- 为什么需要把
vue
转换为/node_modules/.vite/deps/vue.js?v=f014d468
[4]
transformMiddleware实现
从这里有较大的篇幅回答上文的问题[3]
在启动Vite Server时,Vite会注册一个transformMiddleware中间件
// main transform middleware
middlewares.use(transformMiddleware(server))
那我们就可以在transformMiddleware
中打一个断点,然后刷新页面就能够监控到其中的变化。
transformRequest
如图中所示,这已经在中间件的回调函数中,并且请求的url为/src/main.js
。可以见得:
- Vite对请求会做缓存处理,如果是相同的请求会直接缓存
- 若是第一次请求,则会通过transformRequest处理
进入该函数后,会发现一个名为doTransform的函数
doTransform
在doTransform中
- 会先判断
moduleGraph
这个map是否记录了对应模块,若有则直接返回缓存内容 - 若没有缓存,会讲url进行一次resolveId处理,这里是为了处理url中的一些特殊标识符
- 得到id后,又讲流程交给了
loadAndTransform
函数
loadAndTransform
继续进入loadAndTransform
-
一开始会通过
pluginContainer.load
来处理对应的文件id,若得到结果会赋值给loadResult这里的pluginContainer.load其实是类似webpack的loader,将一些非JS模块进行处理,后续我们会有「Vite 插件」介绍到pluginContainer
-
由于我们加载的是js模块,不需要loader,所以图中的返回结果为null
-
Vite通过
fs.readFile
读取文件并以utf-8
编码直接获取/src/main.js
的内容,图中可以看到code的值,是原本的源码信息
继续往下,开始对模块源码进行处理
pluginContainer.transform
继续进入pluginContainer.transform
-
发现在函数内部,存在一个循环,会遍历目前所有的plugin,去处理原始的code信息
-
从右侧的监视中,有一个导入分析的插件,即名为
vite:import-analysis
的plugin对code进行transform后续的「vite插件」文章中会介绍到一个vite插件会有哪些hooks,比如resolveId,load,transform等。
importAnalysisPlugin
在插件中,会根据源码,通过语法分析出import,并将其放入imports数组变量中
关于图中的parseImports方法其实是es-module-lexer中的parse方法
import { init, parse as parseImports } from 'es-module-lexer'
在后续的流程中,会对imports进行遍历
在遍历到vue时,会通过normalizeUrl将其处理为node_modules/.vite/*
形式
normalizeUrl
继续进入normalizeUrl
跟踪url的变化
在normalizeUrl
中会对url进行一次resolve,并且处理后的resolved.id重新赋值给url。需要再将resolve方法打开。
resolve方法内部本质上使用pluginContainer的resolveId方法,遍历所有的plugin,利用plugin的resolveId钩子,处理一个url得到对应的id。图中能看到是一个名为vite:resolve
的plugin对处理得到的结果
plugin/resolve.ts
继续打开看plugin/resolve
这个插件看一下
可以发现里面有对很多url的类型处理,比如/
、./
根路径、相对路径的处理。
在图中找到标记的地方,有一段对依赖包引入的处理方法
重点在tryOptimizedResolve
,这个方法会使用预构建的依赖,对vue
这种从node_module引入的模块进行替换。
图中的depsOptimizer
可以理解为在Vite Server启动时,会对项目目录进行一次扫描,比如识别package.json
中dependences,并且将这些依赖缓存到/node_modules/.vite/deps
目录下。
这里能解答上文中提出的问题[4]
总结
通过上面的流程可以了解到从Vite是如何实现从一个入口index.html逐渐加载模块,并且处理模块之中的依赖的一个过程。这也是Vite中最核心、复杂的逻辑,除了文中举例的src/main.js
以外,像.vue``.less
这一类的文件,也会经过类型的处理,转换成能被浏览器原生识别的ES模块。
后续还会有文中出现的「插件 pluginContainer」和「预构建 depsOptimizer」相关的文章进行补充,方便大家更好的理解。
转载自:https://juejin.cn/post/7179512170672029755