Vite系列:如何对模块请求进行transform

lxf2023-04-09 19:03:01

当我们创建一个vite+vue3的项目,然后执行npm run dev,很短的时间内,一个vue项目就run起来了。这个过程和传统的webpack项目很像,但是在通过浏览器访问localhost:5173时,在开发者工具的Network中我们能发现vite项目有一个很大的特点。

Vite系列:如何对模块请求进行transform

会发现加载的资源中有.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文件,并且加载脚本内容

Vite系列:如何对模块请求进行transform

观察响应内容,会发现返回的内容有些地方比较特殊

  1. import这种语法没有被转译,而是保留了es module的写法 [1]
  2. 导入依赖的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,简单画个示意图:

Vite系列:如何对模块请求进行transform

中间件

简单提一嘴什么是中间件,如果有用express或koa框架写过node应用的同学应该知道,如果没有相关经验也不慌,一个简单示意图了解一下:

Vite系列:如何对模块请求进行transform

通常我们创建一个Server后,就可以访问该Server,从请求到响应的返回,就经过图中的流程,可以看见请求会被各种中间件处理后一番,结果才返回给客户端。

关键词:洋葱模型。有兴趣的同学可以再搜索深入了解一下。

流程

我们把场景代入一下,比如其中某个中间件换成上文说的transformMiddleware,请求/src/main.js的,中间件去项目目录下寻址到对应的文件,读取其中的内容,发现import { createApp } from 'vue'是一种从依赖中读取模块的行为,那么就把实际vue指向实际的js模块文件(/node_modules/.vite/deps/vue.js?v=f014d468)。又因为浏览器设置了type='module'会识别到import而发起请求加载模块,这样就实现了所有模块类似递归一样被陆续加载进来。

原理就是这样,不过也出现了几个新的点需要去了解

  1. transform这个过程代码里是怎么写的 [3]
  2. 为什么需要把vue转换为/node_modules/.vite/deps/vue.js?v=f014d468 [4]

transformMiddleware实现

从这里有较大的篇幅回答上文的问题[3]

在启动Vite Server时,Vite会注册一个transformMiddleware中间件

  // main transform middleware
  middlewares.use(transformMiddleware(server))

那我们就可以在transformMiddleware中打一个断点,然后刷新页面就能够监控到其中的变化。

Vite系列:如何对模块请求进行transform

transformRequest

如图中所示,这已经在中间件的回调函数中,并且请求的url为/src/main.js。可以见得:

  1. Vite对请求会做缓存处理,如果是相同的请求会直接缓存
  2. 若是第一次请求,则会通过transformRequest处理

进入该函数后,会发现一个名为doTransform的函数

Vite系列:如何对模块请求进行transform

doTransform

在doTransform中

  1. 会先判断moduleGraph这个map是否记录了对应模块,若有则直接返回缓存内容
  2. 若没有缓存,会讲url进行一次resolveId处理,这里是为了处理url中的一些特殊标识符
  3. 得到id后,又讲流程交给了loadAndTransform函数

Vite系列:如何对模块请求进行transform

loadAndTransform

继续进入loadAndTransform

  1. 一开始会通过pluginContainer.load来处理对应的文件id,若得到结果会赋值给loadResult

    这里的pluginContainer.load其实是类似webpack的loader,将一些非JS模块进行处理,后续我们会有「Vite 插件」介绍到pluginContainer

  2. 由于我们加载的是js模块,不需要loader,所以图中的返回结果为null

  3. Vite通过fs.readFile 读取文件并以utf-8编码直接获取/src/main.js的内容,图中可以看到code的值,是原本的源码信息

Vite系列:如何对模块请求进行transform

继续往下,开始对模块源码进行处理

Vite系列:如何对模块请求进行transform

pluginContainer.transform

继续进入pluginContainer.transform

  1. 发现在函数内部,存在一个循环,会遍历目前所有的plugin,去处理原始的code信息

  2. 从右侧的监视中,有一个导入分析的插件,即名为vite:import-analysis的plugin对code进行transform

    后续的「vite插件」文章中会介绍到一个vite插件会有哪些hooks,比如resolveId,load,transform等。

Vite系列:如何对模块请求进行transform

importAnalysisPlugin

在插件中,会根据源码,通过语法分析出import,并将其放入imports数组变量中

关于图中的parseImports方法其实是es-module-lexer中的parse方法

import { init, parse as parseImports } from 'es-module-lexer'

Vite系列:如何对模块请求进行transform

在后续的流程中,会对imports进行遍历

Vite系列:如何对模块请求进行transform

在遍历到vue时,会通过normalizeUrl将其处理为node_modules/.vite/*形式

Vite系列:如何对模块请求进行transform

normalizeUrl

继续进入normalizeUrl跟踪url的变化

Vite系列:如何对模块请求进行transform

Vite系列:如何对模块请求进行transform

normalizeUrl中会对url进行一次resolve,并且处理后的resolved.id重新赋值给url。需要再将resolve方法打开。

Vite系列:如何对模块请求进行transform

resolve方法内部本质上使用pluginContainer的resolveId方法,遍历所有的plugin,利用plugin的resolveId钩子,处理一个url得到对应的id。图中能看到是一个名为vite:resolve的plugin对处理得到的结果

Vite系列:如何对模块请求进行transform

plugin/resolve.ts

继续打开看plugin/resolve这个插件看一下

可以发现里面有对很多url的类型处理,比如/./根路径、相对路径的处理。

在图中找到标记的地方,有一段对依赖包引入的处理方法

Vite系列:如何对模块请求进行transform

重点在tryOptimizedResolve,这个方法会使用预构建的依赖,对vue这种从node_module引入的模块进行替换。

Vite系列:如何对模块请求进行transform

图中的depsOptimizer可以理解为在Vite Server启动时,会对项目目录进行一次扫描,比如识别package.json中dependences,并且将这些依赖缓存到/node_modules/.vite/deps目录下。

这里能解答上文中提出的问题[4]

总结

通过上面的流程可以了解到从Vite是如何实现从一个入口index.html逐渐加载模块,并且处理模块之中的依赖的一个过程。这也是Vite中最核心、复杂的逻辑,除了文中举例的src/main.js以外,像.vue``.less这一类的文件,也会经过类型的处理,转换成能被浏览器原生识别的ES模块。

后续还会有文中出现的「插件 pluginContainer」和「预构建 depsOptimizer」相关的文章进行补充,方便大家更好的理解。