Libon

Vite 源码分析(一)

#vite
分析 Vite@3 源码实现之 vite/bin/vite.js

ToC

前置工作

下载源码

# --depth=1 只保留最后一次 commit 记录,加快克隆下载的速度
git clone http://github.com/vitejs/vite.git --depth=1
cd vite
pnpm i && code ./packages/vite

ViteJS 使用的方式是 vite ,那说明这个包一定包含了一个可执行的文件,在现有的 node npm package 体系中的体现则是它一定是有一个 bin 命令的,通过查看 package.json 可以看到入口是 bin/vite.js ,所以我们从它开始。

判断使用方式,收集 bin 参数

第一行的 #!/usr/bin/env node 表示从当前用户的可执行环境中查找 node 来作为这个文件的解析器,然后又引入了 import { performance } from 'node:perf_hooks' 依赖,performance 其实和浏览器中的 performance 类似,performance.now() 表示了从调用 node 开始到执行这一条语句中间间隔了多长时间,单位是毫秒。

下一行有一个 if 判断语句 if (!import.meta.url.includes('node_modules')),其中使用到了 import.meta.url ,这个属性保存的是当前模块在磁盘中的文件路径。需要注意的是,import.meta 属性只能在 es6 及以上的版本才可以使用,因为它只对 JavaScript Module 提供,所以你在 package.json 中也能找到一个 "type": "module" 属性,这是为了表示,vite 这个包是现代 ESModule 格式,同时 Vite 本身也是一个主打 "Next generation frontend tooling.(下一代前端工具。)" 的开发依赖,所以肯定还是会以现代浏览器的标准和兼容性来编写代码。回到代码中,会发现代码中判断了当前文件是否包含 node_modules ,这么做的目的是为了在使用 npx vite 的时候也能获取到 vite 对应的 sourcemap 以定位问题。同时这里还使用到了一个比较新的特性:`“top-level await”,即:在模块的顶级使用 await 语句,而非将其包含在 async 函数中。

global.__vite_start_time = performance.now() 则是将服务启动时间保存到全局环境中,在 node 中,global 等同于 window ,也是所有的开始。

const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg)) // 判断是否有设置 --debug || -d,并找到其对应的 index 位置
const filterIndex = process.argv.findIndex((arg) => /^(?:-f|--filter)$/.test(arg)) // 判断是否有设置 --filter || -f,并找到其对应的 index 位置
const profileIndex = process.argv.indexOf('--profile') // 判断是否有设置 --profile,并找到对应的索引位置

bin debug params

你可以自己新建一个 vite 的示例项目,或是直接使用源码中的 playground 文件夹中的示例来进行测试,都可以。vite 命令可以接收三个调试类的命令行参数,分别为:--debug --filter --profile

—debug/-d

{
  "scripts": {
    "dev": "vite --debug"
  }
}

在项目的 package.json/scripts/dev 命令后添加 --debug 命令可以看到 vite 在此次运行时加载的所有配置项和服务启动后加载每个文件所需要的耗时。

  vite:spa-fallback Rewriting GET / to /index.html +0ms
  vite:time 25.28ms /index.html +0ms
  vite:spa-fallback Rewriting GET / to /index.html +34ms
  vite:time 2.92ms /index.html +15ms
  vite:load 5.08ms [fs] /@vite/client +0ms
  vite:load 5.21ms [fs] /src/main.js +4ms
  vite:transform 4.10ms /src/main.js +0ms
  vite:time 11.45ms /src/main.js +35ms

日志的类型多且杂,各个文件和各个阶段的操作日志都会输出出来,非常不方便查看,但同时,它还可以指定一个过滤属性,用于在打印日志时只输出某一类型的日志:--debug load

  vite:load 6.73ms [fs] /@vite/client +0ms
  vite:load 7.78ms [fs] /src/main.js +5ms
  vite:load 12.16ms [fs] /src/style.css +18ms
  vite:load 13.69ms [fs] /src/App.vue +1ms
  vite:load 0.18ms [plugin] /src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css +23ms
  vite:load 0.79ms [plugin] plugin-vue:export-helper +1ms
  vite:load 37.98ms [plugin] /node_modules/.vite/deps/vue.js?v=eb57b1a7 +1ms
  vite:load 7.85ms [fs] /src/components/HelloWorld.vue +3ms
  vite:load 0.04ms [plugin] /src/components/HelloWorld.vue?vue&type=style&index=0&scoped=e17ea971&lang.css +13ms

还可以指定多个类型:--debug load,resolve,transform

—filter/-f

开启这个参数的前提是需要设置 debug ,因为这个参数的作用是屏蔽掉众多文件的加载信息,只保留参数后面的文件路径的加载信息,包括 debug 中的 vite 配置项也会一并屏蔽,只保留对应文件的加载/转换日志,如下:

{
  "scripts": {
    "dev": "vite -d -f /src/style.css"
  }
}
vite:resolve 0.19ms ./style.css -> /Volumes/code/sourcecode/test/vite-process-test/src/style.css +0ms
vite:resolve 0.10ms /src/style.css -> /Volumes/code/sourcecode/test/vite-process-test/src/style.css +1ms
vite:load 10.10ms [fs] /src/style.css +0ms
vite:transform 72.46ms /src/style.css +0ms
vite:time 79.41ms /src/style.css +0ms

不过这个参数只支持对单个文件的过滤,不能同时设置对多个文件加载信息的过滤,也就是不能以 /src/style.css,/src/main.js 这种形式设置。

—profile

这个参数是一个布尔类型的 flag,后面无需再接其他参数(即便增加了其他参数,也会在运行时被删除,比如:vite --profile test ,服务启动后,会将 --profiletest 一并删除,最终变成 vite )。增加这个参数可以在服务启动以后,引入 NodeJS 的 inspector 内置模块,并通过 new inspector.Session() 创建一个 session 实例与 Node 的 V8 引擎建立连接,最后在启动服务的项目根路径下创建一个 vite-profile.cpuprofile 文件,里面保存着 Node 处理文件的耗时。这段代码的处理在这:

if (profileIndex > 0) {
  console.log(process.argv)
  process.argv.splice(profileIndex, 1)
  const next = process.argv[profileIndex]

  console.log(next)
  if (next && !next.startsWith('-')) {
    process.argv.splice(profileIndex, 1)
  }
  const inspector = await import('node:inspector').then((r) => r.default)
  console.log('inspector', inspector)
  const session = (global.__vite_profile_session = new inspector.Session())
  console.log('session', session)
  session.connect()
  session.post('Profiler.enable', () => {
    session.post('Profiler.start', start) // 在开始记录后启动服务器
  })
} else {
  start()
}

在完成以上工作后,就准备启动服务器了,启动服务器的代码在 ../dist/node/cli.js 文件中,bin/vite.js 文件中以 start() 函数的形式将这个文件动态加载了进来,而 ../dist/node/cli.js 文件对应的源文件路径是:packages/vite/src/node/cli.ts ,这个文件则是对 cli 参数的进一步解析和设置命令别名,因为这个文件没有被任何作用域或是函数包裹着,所以只要把文件引入就会被立即执行。

以上。


CD ..