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
也能看到 命令很简单
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- 开头,因此可以通过
npm init @vitejs/app [pathName] [params]
启动项目。除了指定路径名,还可以指定模板参数。
通过阅读 package.json 可以看到
main=index.js
,直接调用走的是 index.js- 三个依赖
- enquirer 选择弹窗
- kolorist 类似chalk 颜色
- minimist 简单的argv解析
阅读 index.js 本身也很简单
没啥阅读难度。
Vite
这是个大包,后续可能要单独抽出去分析。
先查看 package.json
bin
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
看上图这段代码。如果用户执行 vite
走 action
核心代码很简单:
const { createServer } = await import('./server')
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
server: cleanOptions(options) as ServerOptions
})
await server.listen()
既然是node项目,启动 server 服务就完了。
再深入 ./server
之前,先看传入的配置,也没啥。这里的options是空的对象,因为命令行没有参数。
源码断点调试
把刚才下载的 vite 源代码打开
cd vite-main
yarn # 安装依赖
cd packages/vite
yarn dev # 进入vite开发模式
# 能找到 dist 文件夹 里面有 js 和 map 文件
yarn link # 讲vite注册到全局
有了vite的软链接,我们就可以正式调试了。 yarn link vite
此时再观察node_modules/vite 就有箭头标志了,说明已经是软链接。
讲vite demo 项目的 package.json 添加 脚本,意思是执行 cli.js 但是通过 inspect + brk 的方式(目前只会这种debug方式,不过不影响)
"debug": "node --inspect-brk node_modules/vite/dist/node/cli.js",
(这里我实在不会从 .bin/vite.js 开始打断点)
启动 yarn debug
之后,打开浏览器的开发者模式,点击
进入面板,会发现断点已经生效。
有了以上操作,就可以正式观察 vite 的启动流程了。定义到这里:
源码阅读
刚才看到通过导入 ./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
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
,我们进入继续分析:
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
在上面的转过程中有这个过程:const result = await transform(code, resolvedOptions)
在transofrm
其中就有 importAnalysis
插件,通过 es-module-lexer
即系import,把当前模块放入图中。
建依赖关系。
有点累了,先放一下。
-1 参考
- 可参考这篇,受益匪浅 https://juejin.cn/post/6917449999416557582