vite,号称是下一代前端开发和构建工具。vite的出现得益于浏览器对module的支持,利用浏览器的新特性去实现了极速的开发体验;能够极快的实现热重载(hmr).
开发模式下,利用浏览器的module支持,达到了极致的开发体验;
正式环境的编译打包,使用了首次提出tree-shaking的rollup进行构建;
vite提供了很多的配置选项,包括vite本身的配置,esbuild的配置,rollup的配置等等,今天带领大家从源码的角度看看vite。
vite其实是可以分为三部分的,一部分是开发过程中的client部分;一部分是开发过程中的server部分;另外一部分就是与生产有关系的打包编译部分,由于vite打包编译其实是用的rollup,我们不做解析,只看前两部分。

vite-client

vite的client其实是作为一个单独的模块进行处理的,它的源码是放在packages/vite/src/client;这里面有四个文件:

  • client.ts:主要的文件入口,下面着重介绍;
  • env.ts:环境相关的配置,这里会把我们在vite.config.js(vite配置文件)的define配置在这里进行处理;
  • overlay.ts: 这个是一个错误蒙层的展示,会把我们的错误信息进行展示;
  • tsconfig.json:这就是ts的配置文件了。

    工具部分

    client里面是提供了一系列工具函数,主要是为了hmr;
    image.png

    websocket部分

  • 建立websocket连接

  • 调用上面的overlay进行错误展示
  • Message通信

其中message通信部分有多种事件类型,可以参见下图:
image.png

举例说明

使用vite-app创建了一个简单的demo:
yarn create @vitejs/app my-react-ts-app —template react-ts
使用以上命令,可以简单的创建一个react-ts的vite应用。
npm installnpm run dev
执行以上命令,进行安装依赖,然后启动服务,打开浏览器: http://localhost:3000/,network界面,可以看到有如下请求:
image.png
我把这几种类型的数据进行了划分,按照不同的类型进行不同的划分:
image.png
咱们接下来来分析下,html的内容:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <script type="module" src="/@vite/client"></script>
  5. <script type="module">
  6. import RefreshRuntime from "/@react-refresh"
  7. RefreshRuntime.injectIntoGlobalHook(window)
  8. window.$RefreshReg$ = () => {}
  9. window.$RefreshSig$ = () => (type) => type
  10. window.__vite_plugin_react_preamble_installed__ = true
  11. </script>
  12. <meta charset="UTF-8" />
  13. <link rel="icon" type="image/svg+xml" href="favicon.svg" />
  14. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  15. <title>Vite App</title>
  16. </head>
  17. <body>
  18. <div id="root"></div>
  19. <script type="module" src="/src/main.tsx"></script>
  20. </body>
  21. </html>

可以看到,涉及到js的一共三块:

  • client,请求路径为/@vite/client,请注意这个路径,这是vite本身的依赖的路径;
  • react-refresh的模块代码,这是插件react-refresh注入的代码;代码内部又请求了@react-refresh,这是插件react-refresh的sdk的请求;
  • main,请求路径为/src/main/tsc,这是与咱们项目中的真实代码相关的;

除了上面的三个外,还有一个env,请求路径为/@vite/env.js,这个就是@vite/client内部发出的对env依赖的请求:import ‘/node_modules/vite/dist/client/env.js’;;
当然还有@react-refresh的sdk请求;
除了上面所提到的js之外,其他的请求其实就是我们项目代码里面的请求了;
client第一步要做的事情就是建立websocket通信通道,可以看到上面的websocket类型的localhost请求,这就是client与server端通信,进行热更新等的管道。

vite- server

说完了client,我们回到server部分,入口文件为packages/vite/src/node/serve.ts,最主要的逻辑其实是在packages/vite/src/node/server/index.ts;我们暂且把server端称为node端,node端主要包含几种类型文件的处理,毕竟这只是个代理服务器;
image.png
我们从几个部分来看看这几种类型的处理

node watcher

watcher的主要作用是对于文件变化的监听,然后与client端进行通信:image.png
监听的目录为整个项目的根目录,watchOptions为vite.config.js里面的server.watch配置,初始化代码如下:

  1. // 使用chokidar进行对文件目录的监听,
  2. const watcher = chokidar.watch(path.resolve(root), {
  3. ignored: ['**/node_modules/**', '**/.git/**', ...ignored],
  4. ignoreInitial: true,
  5. ignorePermissionErrors: true,
  6. ...watchOptions
  7. }) as FSWatcher

启动对文件的监听:

  1. // 如果发生改变,调用handleHMRUpdate,
  2. watcher.on('change', async (file) => {
  3. file = normalizePath(file)
  4. // invalidate module graph cache on file change
  5. moduleGraph.onFileChange(file)
  6. if (serverConfig.hmr !== false) {
  7. try {
  8. await handleHMRUpdate(file, server)
  9. } catch (err) {
  10. ws.send({
  11. type: 'error',
  12. err: prepareError(err)
  13. })
  14. }
  15. }
  16. })
  17. // 增加文件连接
  18. watcher.on('add', (file) => {
  19. handleFileAddUnlink(normalizePath(file), server)
  20. })
  21. // 减少文件连接
  22. watcher.on('unlink', (file) => {
  23. handleFileAddUnlink(normalizePath(file), server, true)
  24. })

监听对应的事件所对应的处理函数在packages/vite/src/node/server/hmr.ts文件里面。再细节的处理,我们不做说明了,其实里面逻辑是差不太多的,最后都是调用了websocket,发送到client端。

node 依赖类型

依赖类型,其实也就是node_modules下面的依赖包,例如:
image.png这些包属于基本不会变的类型,vite的做法是把这些依赖,在服务启动的时候放到.vite目录下面,收到的请求直接去.vite下面获取,然后返回。

node 静态资源

静态资源其实也就是我们了解和熟悉的public/下面的或者static/下面的内容,这些资源属于静态文件,例如:
image.png这样的数据,vite不做任何处理,直接返回。

node html

对于入口文件index.html,我们这里暂且只讲单入口文件,多入口文件vite也是支持的,详情可见多页面应用;

  1. // 删减后的代码如下
  2. // @file packages/vite/src/node/server/middlewares/indexHtml.ts
  3. export function indexHtmlMiddleware(server){
  4. return async (req, res, next) => {
  5. const url = req.url && cleanUrl(req.url)
  6. const filename = getHtmlFilename(url, server)
  7. try {
  8. // 从本地读取index.html的内容
  9. let html = fs.readFileSync(filename, 'utf-8')
  10. // dev模式下调用createDevHtmlTransformFn转换html的内容,插入两个script
  11. html = await server.transformIndexHtml(url, html)
  12. // 把html的内容返回。
  13. return send(req, res, html, 'html')
  14. } catch (e) {
  15. return next(e)
  16. }
  17. }
  18. }

对于入口文件index.html,vite首先会从硬盘上读取文件的内容,经过一系列操作后,把操作后的内容进行返回,我们来看看这个一系列操作:

  • 调用createDevHtmlTransformFn去获取处理函数: ```javascript // @file packages/vite/src/node/plugins/html.ts export function resolveHtmlTransforms(plugins: readonly Plugin[]) { const preHooks: IndexHtmlTransformHook[] = [] const postHooks: IndexHtmlTransformHook[] = []

    for (const plugin of plugins) { const hook = plugin.transformIndexHtml if (hook) {

    1. if (typeof hook === 'function') {
    2. postHooks.push(hook)
    3. } else if (hook.enforce === 'pre') {
    4. preHooks.push(hook.transform)
    5. } else {
    6. postHooks.push(hook.transform)
    7. }

    } }

    return [preHooks, postHooks] }

// @file packages/vite/src/node/server/middlewares/indexHtml.ts export function createDevHtmlTransformFn(server: ViteDevServer) { const [preHooks, postHooks] = resolveHtmlTransforms(server.config.plugins) return (url: string, html: string): Promise => { return applyHtmlTransforms( html, url, getHtmlFilename(url, server), […preHooks, devHtmlHook, …postHooks], server ) } }

  1. 此处,我们还是拿react项目为例,**react-refresh的插件被插入到了postHooks里面**;最后其实是返回了一个无名的promise类型的函数;此处也就是闭包了。无名函数里面调用的是applyHtmlTransforms,我们来看下参数:
  2. - html为根目录下面的index.html的内容
  3. - url为/index.html
  4. - 第三个参数的执行结果为/index.html
  5. - 第四个参数为一个大数组,prehooks是空的,第二个为是vite自己的/@vite/client链接的返回函数,第三个是有一个react-refresh的插件在里面的
  6. - 第五个参数为当前server
  7. 接下来是applyHtmlTransforms的调用时刻,此处会改写html内容,然后返回。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/25375507/1640659024076-e6a68985-634b-41a3-ada6-510832973a8a.png#clientId=u49bae742-d520-4&crop=0&crop=0&crop=1&crop=1&errorMessage=unknown%20error&from=paste&id=u33263743&margin=%5Bobject%20Object%5D&name=image.png&originHeight=574&originWidth=1080&originalType=url&ratio=1&rotation=0&showTitle=false&size=110467&status=error&style=none&taskId=u8e1e1bc7-c684-4891-a2a1-ffcd0571d96&title=)
  8. 最后处理好的html的内容,就是我们上面看到的html的内容。
  9. <a name="k6E2z"></a>
  10. ## node 其他类型
  11. 暂时把其他类型都算为其他类型,包括@vite开头的/@vite/client和业务相关的请求;这些请求都会走同一个transformMiddleware中间件。此中间件所做的工作如下:<br />// @file packages/vite/src/node/server/middlewares/transform.ts<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/25375507/1640659024200-0d3b7205-0fa5-4868-879d-e2a3b08cdaa8.png#clientId=u49bae742-d520-4&crop=0&crop=0&crop=1&crop=1&errorMessage=unknown%20error&from=paste&id=ua61f9c4f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=721&originWidth=1080&originalType=url&ratio=1&rotation=0&showTitle=false&size=132284&status=error&style=none&taskId=u4e6c4165-36ad-4ccd-a475-0dca3f4f37c&title=)
  12. 其实上面的逻辑正常走下来,是会到命中缓存和未命中缓存中的,二选一,命中就直接返回了,没有命中的话,就是走到了transform,接下来我们看下调用transform的过程:
  13. ```javascript
  14. // @file packages/vite/src/node/server/transformRequest.ts
  15. // 调用插件获取当前请求的id,如/@react-refresh,当然也有获取不到的情况;
  16. const id = (await pluginContainer.resolveId(url))?.id || url
  17. // 调用插件获取插件返回的内容,如/@react-refresh,肯定有不是插件返回的情况,
  18. const loadResult = await pluginContainer.load(id, ssr)
  19. // 接下来是重点
  20. // 如果没有获取到结果,也就是不是插件类型的请求,如我们的入口文件/src/main.tsx
  21. if (loadResult == null) {
  22. // 从硬盘读取非插件提供的返回结果
  23. code = await fs.readFile(file, 'utf-8')
  24. } else {
  25. if (typeof loadResult === 'object') {
  26. code = loadResult.code
  27. map = loadResult.map
  28. } else {
  29. code = loadResult
  30. }
  31. }
  32. }
  33. // 启动文件监听,调用watcher,和上面讲到的watcher遥相呼应
  34. ensureWatchedFile(watcher, mod.file, root)
  35. // 代码运行到这里,是获取到内容了不假,不过code还是源文件,也就是编写的文件内容
  36. // 下面的transform是开始进行替换
  37. const transformResult = await pluginContainer.transform(code, id, map, ssr)
  38. code = transformResult.code!
  39. map = transformResult.map
  40. return (mod.transformResult = {
  41. code,
  42. map,
  43. etag: getEtag(code, { weak: true })
  44. } as TransformResult)

大体的流程如下:
image.png

  1. async transform(code, id, inMap, ssr) {
  2. const ctx = new TransformContext(id, code, inMap as SourceMap)
  3. ctx.ssr = !!ssr
  4. for (const plugin of plugins) {
  5. if (!plugin.transform) continue
  6. ctx._activePlugin = plugin
  7. ctx._activeId = id
  8. ctx._activeCode = code
  9. let result
  10. try {
  11. result = await plugin.transform.call(ctx as any, code, id, ssr)
  12. } catch (e) {
  13. ctx.error(e)
  14. }
  15. if (!result) continue
  16. if (typeof result === 'object') {
  17. code = result.code || ''
  18. if (result.map) ctx.sourcemapChain.push(result.map)
  19. } else {
  20. code = result
  21. }
  22. }
  23. return {
  24. code,
  25. map: ctx._getCombinedSourcemap()
  26. }
  27. },

image.png

其实到这里,我们对于vite server所实现的功能基本是已经清楚了,代理服务器,然后对引用修改为自己的规则,对自己的规则进行解析处理。尤为重要的其实是vite:import-analysis这个插件。