2021-3-16 初稿

注意:本文写于2021-3-16 Vite仓库对应2.1.0,随着时间发展部分内容可能会过期。

Vite 是啥不必多说,本文尝试阅读 Vite仓库源码

准备工作

首先你需要下载克隆仓库 https://github.com/vitejs/vite 并完成依赖安装。
可考虑删除 playwright-chromium 这个依赖,加快安装时间。

下载可考虑删除下列文件:

  • .circleci CI工具
  • .github 工作流相关
  • appveryor.yml 无关配置
  • docs 文档文件夹,直接看官网就好

经过删除,工作区会相对清爽。

观察目录结构

vite仓库是一个 monorepo ,但没有使用 lerna,使用的是 yarn 的 workspaces,因此:

  • packages
    • create-app 脚手架
    • playground 忽略
    • plugin-legacy 插件兼容IE不支持esm的问题,vue3目前可无视
    • plugin-react-refresh 插件支持react
    • plugin-vue 插件支持vue3
    • plugin-vue-jsx 插件支持 vue写 jsx
    • vite 本地
  • script 脚本

本次分析精力有限,预计只分析 create-app plugin-vue vite 三个包。

通过阅读首页的 package.json#scripts 也能看到 命令很简单

  1. cd packages/vite && yarn build # 这就算打包了

scripts/realese.js

发包用的脚本: scripts/release.js

自己实现了一个类似 lerna 的功能

  • 发包选择 发包类型 patch minor major 等
  • 基于 semver.inc 自动递增版本号
  • 利用 execa 运行命令 比如 build, changelog, git 操作

    run(‘yarn’, [‘build’]) ==> execa(bin, args, { stdio: ‘inherit’, …opts })
    这么一看还真实尽可能简单来。

create-app 包

这是一个脚手架,因为是 create- 开头,因此可以通过

  1. npm init @vitejs/app [pathName] [params]

启动项目。除了指定路径名,还可以指定模板参数。

image.png
通过阅读 package.json 可以看到

  • main=index.js ,直接调用走的是 index.js
  • 三个依赖
  • enquirer 选择弹窗
  • kolorist 类似chalk 颜色
  • minimist 简单的argv解析

阅读 index.js 本身也很简单
没啥阅读难度。

Vite

这是个大包,后续可能要单独抽出去分析。

先查看 package.json

bin

bin 指向 bin/vite.js

scripts 脚本

  • dev 走 run-p 并行运行 dev-client dev-node
    • dev-client 用tsc编译ts src/client
    • dev-node 用tsc编译 src/node
  • build 走run-s 串行 build-bundle build-types
    • build-bundle : rollup -c
    • build-types 继续运行几个命令,先忽略吧
      • build-temp-types 用tsc只生成声明文件 src/node —> temp/node
      • patch-types 走脚本 scripts/patchTypes
      • roll-types 用 api-extractor run

其他的 lint format changlog release 忽略

依赖

  • esbuild 大名鼎鼎
  • postcss
  • resolve
  • rollup

常规依赖

  • rollup 一堆插件
  • 一堆 types
  • @vue/compiler-dom
  • arcon 相关忽略
  • chalk 颜色
  • connect 核心:中间件
  • es-module-lexer 核心:es词法分析

开发依赖太多了。用到再说。

因为我们日常使用就三个:

  • vite
  • vite build
  • vite preview

我们已知:

  • vite 开发使用了 connect启动后端服务做拦截解析、使用esbuild 编译文件
  • 但esbuild打包css会有问题,因此vite打包用的rollup

我们的主线还是开发。打包不是主线先忽略。

因此 bin/cli 的主流程如下:

路线很清晰,我们需要研究 node/cli.ts

这里补充前置知识 cac 依赖,和之前 create-app 包里的 minimist 一样,cac也是一个很简单的命令行工具(尤大就是没用传统的commander/yargs)

看了下cac https://www.npmjs.com/package/cac

Command And Conquer is a JavaScript library for building CLI apps. CAC 是一个用来构建cli的js类库

特点:

  • 特别轻量,0依赖,只有一个文件
  • 容易学习,就四个命令
    • options
    • version
    • help
    • parse
  • 用ts写的
  • 内置了一些常见功能

核心就是 parse 可以解析。

对 node/cli.ts 进行折叠查看,定义了一堆 options 今天不是来学习cli的,只关注 dev

image.png
看上图这段代码。如果用户执行 vite 走 action
核心代码很简单:

  1. const { createServer } = await import('./server')
  2. const server = await createServer({
  3. root,
  4. base: options.base,
  5. mode: options.mode,
  6. configFile: options.config,
  7. logLevel: options.logLevel,
  8. clearScreen: options.clearScreen,
  9. server: cleanOptions(options) as ServerOptions
  10. })
  11. await server.listen()

既然是node项目,启动 server 服务就完了。

再深入 ./server 之前,先看传入的配置,也没啥。这里的options是空的对象,因为命令行没有参数。

到这里,难度开始陡增了,我们还是通过断点进行调试。

源码断点调试

把刚才下载的 vite 源代码打开

  1. cd vite-main
  2. yarn # 安装依赖
  3. cd packages/vite
  4. yarn dev # 进入vite开发模式
  5. # 能找到 dist 文件夹 里面有 js 和 map 文件
  6. yarn link # 讲vite注册到全局

有了vite的软链接,我们就可以正式调试了。 yarn link vite 此时再观察node_modules/vite 就有箭头标志了,说明已经是软链接。

讲vite demo 项目的 package.json 添加 脚本,意思是执行 cli.js 但是通过 inspect + brk 的方式(目前只会这种debug方式,不过不影响)

  1. "debug": "node --inspect-brk node_modules/vite/dist/node/cli.js",

(这里我实在不会从 .bin/vite.js 开始打断点)

启动 yarn debug 之后,打开浏览器的开发者模式,点击

image.png

进入面板,会发现断点已经生效。

image.png
有了以上操作,就可以正式观察 vite 的启动流程了。定义到这里:

image.png

源码阅读

刚才看到通过导入 ./server/index.ts 异步得到了 createServer 方法。

server/index.ts

接下来继续阅读 server/index.ts 大致思路如下,其实很复杂,省略了一些:

说到底,启动了一个http服务,等待请求进来处理完再返回。没有打包的过程,举例,比如:

  • 前端请求 xxx.js 返回转义之后的 xxx.js
  • 请求 xxx.ts 返回转义之后的 xxx.js
  • 请求 App.vue 返回转义之后的 xxx.js

这一块之前使用 koa2 实现的,不过不重要,思路是一致的。返回的内容是自己定义的,返回的是中间件处理之后的数据内容。其中最核心的是 transform 过程。

transformMiddleware

观察下 transformMiddleware

  1. src/node/server/middlewares/transform.ts#transformMiddleware

if(req.method !== 'GET' 打断点。在 transformMiddleware 中观察 /src/main.ts 的旅程。

  • 不是非GET和忽略名单,继续
  • 不是 _pendingReload 状态继续
  • url很干净,不需要整理,继续
  • 不是soucemap
  • 不是 /public 路径
  • 是 jsRequeset 进入
    • 剥离 ?import
    • 剥离 id
    • 不是css
    • 是否匹配304
    • 进入 transforRequeset (核心)等待返回result
    • 返回结果整理返回响应,旅途结束。

还是用流程图,这里要打断点进来,要不然很费力

transformRequest

上一步最核心的还是 transformRequest ,我们进入继续分析:

  1. src/node/server/transformRequest.ts#transformRequest
  • 接收参数
    • url 当前url
    • {config, pluginContainer, moduleGraph, watcher} 暂时没啥
  • 移除时间戳
  • 检查moduleGraph 是否有缓存
  • resolve 解析成绝对路径 file
  • load 调用 pluginContainer.load,如果null直接fs读
  • fs 读取 file得到源代码有了code,
  • 把当前请求放到图中 mod 并添加 watcher监听
  • transform接下来对code进行编译和转换 调用 pluginCOntainer.transform
  • 缓存并返回

插件提供的三个钩子函数:

  • pluginContainer.resolveId 把url解析成对应的文件
  • pluginContainer.load 把url解析成对应的文件
  • pluginContainer.transform 不同文件进行转换

    迷人的transform

    在上面的转过程中有这个过程:
    1. const result = await transform(code, resolvedOptions)

在transofrm

其中就有 importAnalysis 插件,通过 es-module-lexer 即系import,把当前模块放入图中。
建依赖关系。

有点累了,先放一下。

-1 参考