Vite为什么快?一文搞懂Vite启动本地开发服务的过程

872 阅读15分钟

前言:Vite由于其快速的构建速度和高效的开发体验越来越受到开发者的欢迎,相信很多小伙伴都在用它来开发和构建前端项目, 那么Vite为什么那么快呢?Vite又是如何启动本地开发项目的,让我们一起来看一下:

Vite为什么快?

vite对项目模块类型进行区分,从而针对处理

  1. 依赖:在开发过程中不会变动频繁变动的纯Javascript文件,Vite使用esBuild去处理,以加快速度。
  2. 源码:会在开发过程中频繁变动的文件,通常不是纯Javascript文件,需要进行转换(如JSX,CSS 或者 Vue/Svelte 组件等),这部分代码Vite使用原生ESM的方式去处理,让浏览器接管构建工具的部分功能。

冷启动(首次启动项目)过程:

传统的构建工具: 在启动服务之前,所有的依赖会被解析,并进行打包,等待项目中所有的依赖被打包完成之后,服务才会被启动。
image.png Vite: 启动过程中利用ESM的特点,在确定入口文件entry的情况下,当访问的页面中有导入的模块时,浏览器会自动去请求相应的模块Vite会配合其进行加载,加载过程是动态完成的,省去了打包整个项目依赖的过程,避免了当前无用的依赖参与构建。 image.png

2.热更新(HMR)过程

传统的构建工具: 每次修改文件时,基本上整个项目都得重新构建并重新加载对应页面,即使一些构建工具采用了将构建工具保存在内存之中,或者采取HMR的方法,随着项目体量的增大,热更新速度也会变得越来越慢。
Vite: HMR是在ESM的基础上进行,配合依赖预构建,能够维护好项目中模块之间的依赖关系,从而可以对更新的模块进行更精确的HMR处理,确保更新速度。并且由于上文中提到的区分了代码的类型,Vite可以通过HTTP配合缓存来加快对模块的读取速度:

  • 对于依赖: 由于其不会经常变动的特点,使用强缓存进行处理Cache-Control: max-age=31536000,immutable
  • 对于源码:使用协商缓存进行处理 304 Not Modified

Vite启动过程

先来看一下Vite项目的目录 image.png 在执行了npm run dev命令后 文件中的核心代码如下


function start() {
  return import('../dist/node/cli.js')
}

start()

调用了一个start函数,引入了文件dist/node/cli.js,其对应的打包之前的文件为 vite/node/cli.ts在这之中会通过createServer函数启动开发服务。

const { createServer } = await import("./_server-AX22Z7AY.js");

vite启动之后的两个部分

1.client

在Vite项目启动之后,可以看到,在项目的入口文件中引入了一个脚本/@/vite/client , client 运行在浏览器中用于和本地运行的服务进行交互。 image.png

2.Server

运行在本地的服务器,进行配置解析,预构建,Vite插件处理,热更新等。 image.png

启动的核心函数createServer

    const server = await createServer({
      root,
      base: options.base,
      mode: options.mode,
      configFile: options.config,
      logLevel: options.logLevel,
      clearScreen: options.clearScreen,
      optimizeDeps: { force: options.force },
      server: cleanOptions(options)
    });

接下来我们通过分析createServer内部的执行来研究开发服务器的启动过程

一、解析vite配置

  • resolveConfig:会将用户在命令行中输入的配置项和配置文件进行合并,处理插件的应用环境并进行排序,执行对应的配置钩子,处理别名等;最终得到一个config,其它函数通过处理config得到public文件、https、watcher 相关的配置。
  const config = await resolveConfig(inlineConfig, "serve");
  const initPublicFilesPromise = initPublicFiles(config);
  const { root, server: serverConfig } = config;
  const httpsOptions = await resolveHttpsConfig(config.server.https);
  const { middlewareMode } = serverConfig;
  const resolvedOutDirs = getResolvedOutDirs(
    config.root,
    config.build.outDir,
    config.build.rollupOptions?.output
  );
  const emptyOutDir = resolveEmptyOutDir(
    config.build.emptyOutDir,
    config.root,
    resolvedOutDirs
  );
  const resolvedWatchOptions = resolveChokidarOptions(
    config,
    {
      disableGlobbing: true,
      ...serverConfig.watch
    },
    resolvedOutDirs,
    emptyOutDir
  );

二、初始化http服务

通过serverConfig中的配置调用resolveHttpServer创建http服务,http服务主要是用来处理来自于client的请求,在使用ESM的情况下,页面中对于导入的模块浏览器会发起http请求加载,此时启动的http服务可以响应对应的请求,并在此过程中对模块进行相应的处理。

  const httpServer = middlewareMode ? null : await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// vite\src\node\http.ts
export async function resolveHttpServer(
  { proxy }: CommonServerOptions,
  app: Connect.Server,
  httpsOptions?: HttpsServerOptions,
): Promise<HttpServer> {
  if (!httpsOptions) {
    const { createServer } = await import('node:http')
    return createServer(app)
  }

  if (proxy) {
    const { createServer } = await import('node:https')
    return createServer(httpsOptions, app)
  } else {
    const { createSecureServer } = await import('node:http2')
    return createSecureServer(
      {
        // Manually increase the session memory to prevent 502 ENHANCE_YOUR_CALM
        // errors on large numbers of requests
        maxSessionMemory: 1000,
        ...httpsOptions,
        allowHTTP1: true,
      },
      // @ts-expect-error TODO: is this correct?
      app,
    )
  }
}

三、初始化webSocket服务

使用createWebSocketServer在上一步创建的httpServer的基础上初始化websSocket服务主要用于客户端和服务端之间的通信,实现热更新。

const ws = createWebSocketServer(httpServer, config, httpsOptions)

createWebSocketServer函数中:

  • 根据是否指定了热更新服务器的情况处理不同的逻辑创建一个webSocket服务;
  • 返回一个webSocket服务对象,对象中有对应的方法onoff、 send 和 close等,用于处理服务的开启和关闭,并且定义给client发送不同的信息(比如发送了一个update消息提醒client文件更新信息等)。
export function createWebSocketServer(
  server: HttpServer | null,
  config: ResolvedConfig,
  httpsOptions?: HttpsServerOptions,
): WebSocketServer {
    ...省略部分代码
    if (wsServer) {
    let hmrBase = config.base
    const hmrPath = hmr ? hmr.path : undefined
    if (hmrPath) {
      hmrBase = path.posix.join(hmrBase, hmrPath)
    }
    wss = new WebSocketServerRaw({ noServer: true })
    hmrServerWsListener = (req, socket, head) => {
      if (
        req.headers['sec-websocket-protocol'] === HMR_HEADER &&
        req.url === hmrBase
      ) {
        wss.handleUpgrade(req, socket as Socket, head, (ws) => {
          wss.emit('connection', ws, req)
        })
      }
    }
    wsServer.on('upgrade', hmrServerWsListener)
  } else {
    // http server request handler keeps the same with
    // https://github.com/websockets/ws/blob/45e17acea791d865df6b255a55182e9c42e5877a/lib/websocket-server.js#L88-L96
    const route = ((_, res) => {
      const statusCode = 426
      const body = STATUS_CODES[statusCode]
      if (!body)
        throw new Error(`No body text found for the ${statusCode} status code`)

      res.writeHead(statusCode, {
        'Content-Length': body.length,
        'Content-Type': 'text/plain',
      })
      res.end(body)
    }) as Parameters<typeof createHttpServer>[1]
    if (httpsOptions) {
      wsHttpServer = createHttpsServer(httpsOptions, route)
    } else {
      wsHttpServer = createHttpServer(route)
    }
    // vite dev server in middleware mode
    // need to call ws listen manually
    wss = new WebSocketServerRaw({ server: wsHttpServer })
  }
  
  ...省略部分代码
  
  return {
    name: 'ws',
    listen: () => {
      wsHttpServer?.listen(port, host)
    },
    on: ((event: string, fn: () => void) => {
      if (wsServerEvents.includes(event)) wss.on(event, fn)
      else {
        if (!customListeners.has(event)) {
          customListeners.set(event, new Set())
        }
        customListeners.get(event)!.add(fn)
      }
    }) as WebSocketServer['on'],
    off: ((event: string, fn: () => void) => {
      if (wsServerEvents.includes(event)) {
        wss.off(event, fn)
      } else {
        customListeners.get(event)?.delete(fn)
      }
    }) as WebSocketServer['off'],

    get clients() {
      return new Set(Array.from(wss.clients).map(getSocketClient))
    },

    send(...args: any[]) {
      let payload: HMRPayload
      if (typeof args[0] === 'string') {
        payload = {
          type: 'custom',
          event: args[0],
          data: args[1],
        }
      } else {
        payload = args[0]
      }

      if (payload.type === 'error' && !wss.clients.size) {
        bufferedError = payload
        return
      }

      const stringified = JSON.stringify(payload)
      wss.clients.forEach((client) => {
        // readyState 1 means the connection is open
        if (client.readyState === 1) {
          client.send(stringified)
        }
      })
    },

    close() {
      // should remove listener if hmr.server is set
      // otherwise the old listener swallows all WebSocket connections
      if (hmrServerWsListener && wsServer) {
        wsServer.off('upgrade', hmrServerWsListener)
      }
      return new Promise((resolve, reject) => {
        wss.clients.forEach((client) => {
          client.terminate()
        })
        wss.close((err) => {
          if (err) {
            reject(err)
          } else {
            if (wsHttpServer) {
              wsHttpServer.close((err) => {
                if (err) {
                  reject(err)
                } else {
                  resolve()
                }
              })
            } else {
              resolve()
            }
          }
        })
      })
    },
  }
}

四、创建HMR广播器

HMR 广播器主要是可以在管理多个通道的情况下,向客户端发送自定义的热更新消息。

  const hot = createHMRBroadcaster()
    .addChannel(ws)
    .addChannel(createServerHMRChannel())
// 创建HMR广播器
export function createHMRBroadcaster(): HMRBroadcaster {
  // HMR通道数组
  const channels: HMRChannel[] = []
  // 已经连接的通道
  const readyChannels = new WeakSet<HMRChannel>()
  const broadcaster: HMRBroadcaster = {
    // 获取所有的通道
    get channels() {
      return [...channels]
    },
    // 添加新的通道
    addChannel(channel) {
      if (channels.some((c) => c.name === channel.name)) {
        throw new Error(`HMR channel "${channel.name}" is already defined.`)
      }
      channels.push(channel)
      return broadcaster
    },
    // 发送消息
    on(event: string, listener: (...args: any[]) => any) {
      // emit connection event only when all channels are ready
      if (event === 'connection') {
        // make a copy so we don't wait for channels that might be added after this is triggered
        const channels = this.channels
      // 在所有通道上注册一个事件监听器。
      // 如果事件是 connection,则确保所有通道连接时才调用监听器。
        channels.forEach((channel) =>
          channel.on('connection', () => {
            readyChannels.add(channel)
            if (channels.every((c) => readyChannels.has(c))) {
              listener()
            }
          }),
        )
        return
      }
      channels.forEach((channel) => channel.on(event, listener))
      return
    },
    // 所有通道注销事件监听器
    off(event, listener) {
      channels.forEach((channel) => channel.off(event, listener))
      return
    },
    // 向所有通道发送消息
    send(...args: any[]) {
      channels.forEach((channel) => channel.send(...(args as [any])))
    },
    // 监听所有通道
    listen() {
      channels.forEach((channel) => channel.listen())
    },
    // 关闭所有通道
    close() {
      return Promise.all(channels.map((channel) => channel.close()))
    },
  }
  return broadcaster
}

五、使用chokidar初始化监听文件变化

通过chokidar创建文件监听器

  const watcher = watchEnabled
    ? (chokidar.watch(
        // config file dependencies and env file might be outside of root
        [
          root,
          ...config.configFileDependencies,
          ...getEnvFilesForMode(config.mode, config.envDir),
          // Watch the public directory explicitly because it might be outside
          // of the root directory.
          ...(publicDir && publicFiles ? [publicDir] : []),
        ],
        resolvedWatchOptions,
      ) as FSWatcher)
    : createNoopWatcher(resolvedWatchOptions)

在文件发生变动时会触发对应的逻辑如:

  • 处理HMR
  • 重新处理模块图(moduleGraph)
  • 触发插件中监听文件变动的钩子函数等
  watcher.on('change', async (file) => {
    file = normalizePath(file)
    await container.watchChange(file, { event: 'update' })
    // invalidate module graph cache on file change
    moduleGraph.onFileChange(file)
    await onHMRUpdate('update', file)
  })

  getFsUtils(config).initWatcher?.(watcher)

  watcher.on('add', (file) => {
    onFileAddUnlink(file, false)
  })
  watcher.on('unlink', (file) => {
    onFileAddUnlink(file, true)
  })

六、创建模块图

const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
    container.resolveId(url, undefined, { ssr }),
)

模块图用于维护项目中各个模块的信息以及导入关系。主要通过ModuleGraph以及ModuleNode来实现。

1.ModuleNode包含的信息

export class ModuleNode {
  // 请求url
  url: string
  // 文件路径加参数
  id: string | null = null
  // 文件路径
  file: string | null = null
  // 模块类型
  type: 'js' | 'css'
  // 模块信息
  info?: ModuleInfo
  // 模块元信息
  meta?: Record<string, any>
  // 引入者:哪些模块引入了当前模块
  importers = new Set<ModuleNode>()
  // 引入的模块:当前模块引入了哪些模块
  clientImportedModules = new Set<ModuleNode>()
  // ssr引入的模块
  ssrImportedModules = new Set<ModuleNode>()
  // 接受的HMR模块
  acceptedHmrDeps = new Set<ModuleNode>()
  // 接受的HMR导出
  acceptedHmrExports: Set<string> | null = null
  // 引入的绑定
  importedBindings: Map<string, Set<string>> | null = null
  // 是否自我接受
  isSelfAccepting?: boolean
  // 转换结果
  transformResult: TransformResult | null = null
  // ssr转换结果
  ssrTransformResult: TransformResult | null = null
  // ssr模块
  ssrModule: Record<string, any> | null = null
  // ssr错误
  ssrError: Error | null = null
  // 最后的HMR时间
  lastHMRTimestamp = 0
  lastHMRInvalidationReceived = false
  // 最后无效时间
  lastInvalidationTimestamp = 0
  // 无效状态
  invalidationState: TransformResult | 'HARD_INVALIDATED' | undefined
  // ssr无效状态
  ssrInvalidationState: TransformResult | 'HARD_INVALIDATED' | undefined
  //静态导入的url
  staticImportedUrls?: Set<string>
  constructor(url: string, setIsSelfAccepting = true) {
    this.url = url
    this.type = isDirectCSSRequest(url) ? 'css' : 'js'
    if (setIsSelfAccepting) {
      this.isSelfAccepting = false
    }
  }
  // 获取引入的模块
  get importedModules(): Set<ModuleNode> {
    const importedModules = new Set(this.clientImportedModules)
    for (const module of this.ssrImportedModules) {
      importedModules.add(module)
    }
    return importedModules
  }
}

2.ModuleGraph:方便对ModuleNode进行操作:

ModuleGraph是一个类其中定义了一些用于映射到ModuleNode的属性:

  // url -> module 映射
  urlToModuleMap = new Map<string, ModuleNode>()
  // id -> module 映射
  idToModuleMap = new Map<string, ModuleNode>()
  // etag -> module 映射
  etagToModuleMap = new Map<string, ModuleNode>()
  // file -> modules 映射
  fileToModulesMap = new Map<string, Set<ModuleNode>>()
  // /@fs 的模块
  safeModulesPath = new Set<string>()

以及用于维护这些属性的方法:

  1. 查找ModuleNode:getModuleByUrl(通过url)、getModuleById(通过id)、getModulesByFile(通过文件路径)
  2. 更新ModuleNode信息:updateModuleInfo
  3. 文件变更时处理ModuleNode:onFileChangeonFileChange
  4. ModuleNode无效化:invalidateModuleinvalidateAll

3.模块图构建的依赖关系

以Vite的vue模板为例,App.vue文件的ModuleNode会类似于如下形式:

image.png

具体的ModuleNode如下:

{
    file:'C:/test/src/App.vue',
    id: 'C:/test/src/App.vue',
    importers: [
        {
            url: "/src/main.ts",
            id: "C:/test/src/main.ts",
            file: "C:/test/src/main.ts",
            type: "js",
            importers: {
                // ...
            },
            clientImportedModules: {
                // ...
            },
            // 省略部分代码
          }
    ],
    clientImportedModules:[
          {
            url: "/node_modules/.vite/deps/vue.js?v=8b4b62e4",
            id: "C:/test/node_modules/.vite/deps/vue.js?v=8b4b62e4",
            file: "C:/test/node_modules/.vite/deps/vue.js",
            type: "js",
            importers: {
                //
            },
            clientImportedModules: {
                //
            },
            // 省略部分代码
          },
          {
            url: "/src/components/HelloWorld.vue",
            id: "C:/test/src/components/HelloWorld.vue",
            file: "C:/test/src/components/HelloWorld.vue",
            type: "js",
            importers: {
                //
            },
            clientImportedModules: {
                //
            },
            // 省略部分代码
          }
          //...
    ]
    // ...省略部分代码
}

4.模块图的意义

通过模块图维护的项目模块之间的依赖关系可以更准确的处理热更新,保证热更新过程的正确进行,提高效率;

七、处理插件容器

  const container = await createPluginContainer(config, moduleGraph, watcher)

PluginContainer用于管理和执行插件,在createPluginContainer中主要进行了如下处理:

  1. 插件注册与调度: 插件容器负责注册所有的 Vite 插件,并在合适的时机调度这些插件的钩子函数。
  2. 插件钩子执行: 插件容器确保按照插件定义的顺序和依赖关系执行各个钩子函数。这包括插件的 resolveIdloadtransformbuildStartbuildEnd 等钩子函数。
  3. 插件 API 提供: 插件容器提供了一些 API 供插件使用,这些 API 允许插件与 Vite 的核心功能进行交互。

八、初始化ViteDevServer对象

经过上面的初始化过程,得到的对象和方法会被挂载到一个对象ViteDevServer上。

export interface ViteDevServer {
  // 配置对象
  config: ResolvedConfig
  // 中间件
  middlewares: Connect.Server
  // http服务
  httpServer: HttpServer | null
  // 监听器
  watcher: FSWatcher
  // webSocket服务
  ws: WebSocketServer
  // HMR广播器
  hot: HMRBroadcaster
  // 插件容器 
  pluginContainer: PluginContainer
  // 模块图
  moduleGraph: ModuleGraph
  // Vite 在 CLI 上打印的解析后的 URL
  resolvedUrls: ResolvedServerUrls | null
  // 转换请求的url
  transformRequest(
    url: string,
    options?: TransformOptions,
  ): Promise<TransformResult | null>
  // 请求预热
  warmupRequest(url: string, options?: TransformOptions): Promise<void>
  // html转换
  transformIndexHtml(
    url: string,
    html: string,
    originalUrl?: string,
  ): Promise<string>
  // 将模块代码转换为ssr形式
  ssrTransform(
    code: string,
    inMap: SourceMap | { mappings: '' } | null,
    url: string,
    originalCode?: string,
  ): Promise<TransformResult | null>
  // 实例化模块用于加载ssr
  ssrLoadModule(
    url: string,
    opts?: { fixStacktrace?: boolean },
  ): Promise<Record<string, any>>
  // 获取vite ssr运行信息
  ssrFetchModule(id: string, importer?: string): Promise<FetchResult>
  // 重写堆栈跟踪
  ssrRewriteStacktrace(stack: string): string
  // 返回修复后的堆栈
  ssrFixStacktrace(e: Error): void
  // 在模块图中触发某个模块的HMR
  reloadModule(module: ModuleNode): Promise<void>
  // 启动服务
  listen(port?: number, isRestart?: boolean): Promise<ViteDevServer>
  // 停止服务
  close(): Promise<void>
  // 打印服务器URl
  printUrls(): void
  // 绑定CLI快捷键
  bindCLIShortcuts(options?: BindCLIShortcutsOptions<ViteDevServer>): void
  // 重启服务
  restart(forceOptimize?: boolean): Promise<void>
  // 打开浏览器
  openBrowser(): void
  // ... 省略部分内部方法
}

九、挂载vite内置的中间件

使用connect库初始化中间件服务,用来处理来自客户端的HTTP请求。在请求从客户端发送到服务器,并在服务器处理并响应之前,进行一系列的处理。

  const middlewares = connect() as Connect.Server

需要挂载中间件服务时通过如下方式使用

  middlewares.use(...)

这些中间件会根据vite中配置项去挂载,vite中总共有如下内置的中间件:

  1. timeMiddleware:用于记录响应时间;
  2. corsMiddleware:用于处理跨域;
  3. cachedTransformMiddleware:处理客户端缓存优化性能;
  4. proxyMiddleware: 用于配置代理;
  5. baseMiddleware:根据base路径对处理url;
  6. launchEditorMiddleware: 用于启动编辑器;
  7. viteHMRPingMiddleware:用于热更新过程中的心跳检测;
  8. servePublicMiddleware:处理public静态文件;
  9. transformMiddleware:核心中间件处理静态文件和模块的请求转换;
  10. serveRawFsMiddleware:/@fs/目录静态文件处理;
  11. serveStaticMiddleware:处理静态文件;
  12. indexHtmlMiddleware:处理入口html文件;
  13. notFoundMiddleware:处理请求404;
  14. errorMiddleware:处理请求错误;

十、httpServer.listen 服务启动

httpServer.listen执行之后会调用initServer方法

httpServer.listen = (async (port: number, ...args: any[]) => {
      try {
        // ensure ws server started
        hot.listen()
        await initServer()
      } catch (e) {
        httpServer.emit('error', e)
        return
      }
      return listen(port, ...args)
    }) as any
  const initServer = async () => {
    if (serverInited) return
    if (initingServer) return initingServer

    initingServer = (async function () {
      await container.buildStart({})
      // start deps optimizer after all container plugins are ready
      if (isDepsOptimizerEnabled(config, false)) {
        await initDepsOptimizer(config, server)
      }
      warmupFiles(server)
      initingServer = undefined
      serverInited = true
    })()
    return initingServer
  }

在initServer中主要执行了container.buildStart方法并且启动了依赖预构建initDepsOptimizer

  const initServer = async () => {
    if (serverInited) return
    if (initingServer) return initingServer
    initingServer = (async function () {
      await container.buildStart({})
      // start deps optimizer after all container plugins are ready
      if (isDepsOptimizerEnabled(config, false)) {
        await initDepsOptimizer(config, server)
      }
      warmupFiles(server)
      initingServer = undefined
      serverInited = true
    })()
    return initingServer
  }

十一、进行依赖预构建

预构建的目的主要有以下两个:
1.CommonJS 和 UMD 兼容性:  在开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块。
2.性能:  为了提高后续页面的加载性能,Vite将那些具有许多内部模块的 ESM 依赖项转换为单个模块。

预构建的结果

预构建的依赖会存在于项目node_modules/.vite/deps目录之下

image.png 预构建的依赖列表会存入_metadate.json

 {
      "hash": "8947a8c7",
      "browserHash": "bd4507e3",
      "optimized": {
            "@antv/g2": {
              "src": "../../@antv/g2/esm/index.js",
              "file": "@antv_g2.js",
              "fileHash": "99bd86f9",
              "needsInterop": false
            },
            ...省略部分代码
        }
  }

其中:

  1. hash: 项目对应的哈希值,任何依赖项的变化都会导致这个哈希值的变化。
  2. browserHash: 类似项目hash,用于优化浏览器缓存。
  3. optimized中:
    src:模块预构建前的源码地址;
    file:预构建后的文件地址;
    fileHash: 预构建后的文件hash;
    needsInterop:标识是否是 CommonJS 转换而来的;

执行预构建的过程

在vite中对于依赖的预构建是在createDepsOptimizer函数中去处理的。

1.处理缓存

尝试对缓存的metadata进行读取,如果没有则进行初始化initDepsOptimizerMetadata会初始化默认的metadata.json中的内容。

const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr)
...
let metadata = cachedMetadata || initDepsOptimizerMetadata(config, ssr, sessionTimestamp)

2.扫描依赖

扫描依赖使用的是discoverProjectDependencies函数。

const deps = await discoverProjectDependencies(config).result

其最后会调用esbuild.context搭配自定义的esbuild插件(默认情况下使用vite:dep-scan)进行依赖的扫描。

vite\src\node\optimizer\scan.ts

async function prepareEsbuildScanner(
  config: ResolvedConfig,
  entries: string[],
  deps: Record<string, string>,
  missing: Record<string, string>,
  scanContext?: { cancelled: boolean },
): Promise<BuildContext | undefined> {
  const container = await createPluginContainer(config)

  if (scanContext?.cancelled) return

  const plugin = esbuildScanPlugin(config, container, deps, missing, entries)

  const { plugins = [], ...esbuildOptions } =
    config.optimizeDeps?.esbuildOptions ?? {}

  // The plugin pipeline automatically loads the closest tsconfig.json.
  // But esbuild doesn't support reading tsconfig.json if the plugin has resolved the path (https://github.com/evanw/esbuild/issues/2265).
  // Due to syntax incompatibilities between the experimental decorators in TypeScript and TC39 decorators,
  // we cannot simply set `"experimentalDecorators": true` or `false`. (https://github.com/vitejs/vite/pull/15206#discussion_r1417414715)
  // Therefore, we use the closest tsconfig.json from the root to make it work in most cases.
  let tsconfigRaw = esbuildOptions.tsconfigRaw
  if (!tsconfigRaw && !esbuildOptions.tsconfig) {
    const tsconfigResult = await loadTsconfigJsonForFile(
      path.join(config.root, '_dummy.js'),
    )
    if (tsconfigResult.compilerOptions?.experimentalDecorators) {
      tsconfigRaw = { compilerOptions: { experimentalDecorators: true } }
    }
  }

  return await esbuild.context({
    // 工作目录
    absWorkingDir: process.cwd(),
    // 不写入文件系统
    write: false,
    stdin: {
    //文件的入口
      contents: entries.map((e) => `import ${JSON.stringify(e)}`).join('\n'),
      //loader类型
      loader: 'js',
    },
    //启用打包
    bundle: true,
    //输出格式
    format: 'esm',
    // 日志级别
    logLevel: 'silent',
    // 自定义插件
    plugins: [...plugins, plugin],
    ...esbuildOptions,
    tsconfigRaw,
  })
}

3.根据扫描得到的依赖进行预构建 通过扫描得到的依赖经过处理然后传入runoptimizeDeps函数中,在其中prepareEsbuildOptimizerRun函数会被调用并使用 esbuild.context进行预构建。

async function prepareEsbuildOptimizerRun(
  resolvedConfig: ResolvedConfig,
  depsInfo: Record<string, OptimizedDepInfo>,
  ssr: boolean,
  processingCacheDir: string,
  optimizerContext: { cancelled: boolean },
): Promise<{
  context?: BuildContext
  idToExports: Record<string, ExportsData>
}> {
  //...省略部分代码
  const context = await esbuild.context({
    absWorkingDir: process.cwd(),
    entryPoints: Object.keys(flatIdDeps),
    bundle: true,
    // We can't use platform 'neutral', as esbuild has custom handling
    // when the platform is 'node' or 'browser' that can't be emulated
    // by using mainFields and conditions
    platform,
    define,
    format: 'esm',
    // See https://github.com/evanw/esbuild/issues/1921#issuecomment-1152991694
    banner:
      platform === 'node'
        ? {
            js: `import { createRequire } from 'module';const require = createRequire(import.meta.url);`,
          }
        : undefined,
    target: ESBUILD_MODULES_TARGET,
    external,
    logLevel: 'error',
    splitting: true,
    sourcemap: true,
    outdir: processingCacheDir,
    ignoreAnnotations: true,
    metafile: true,
    plugins,
    charset: 'utf8',
    ...esbuildOptions,
    supported: {
      ...defaultEsbuildSupported,
      ...esbuildOptions.supported,
    },
  })
  return { context, idToExports }

4.打包 通过准备得到的context,执行其中的rebuild方法进行生成对应的预构建产物

  const preparedRun = prepareEsbuildOptimizerRun(
    resolvedConfig,
    depsInfo,
    ssr,
    processingCacheDir,
    optimizerContext,
  )
  
    const runResult = preparedRun.then(({ context, idToExports }) => {
      // ...省略部分代码
    return context
      .rebuild()
      // ...省略部分代码
  })

至此服务启动完成


总结

本文介绍了Vite为什么快以及其启动本地开发项目的过程。

1. Vite的模块处理机制

Vite将项目模块分为依赖和源码两种类型进行处理。对于依赖模块,使用esBuild进行预构建;对于源码模块,利用原生ESM动态加载,加快构建速度。

2. 冷启动过程

传统构建工具在启动前需打包所有依赖,而Vite则依赖浏览器的ESM支持,仅在需要时动态加载模块,显著减少了冷启动时间。

3. 热更新机制

Vite基于ESM进行HMR处理,结合依赖预构建,能够精确更新模块,提高热更新速度。同时,通过HTTP缓存策略加快模块读取。

4. Vite启动过程

  • 解析配置:合并命令行和配置文件的配置项,处理插件和别名等。
  • 初始化HTTP服务:创建用于响应模块加载请求的HTTP服务。
  • 初始化WebSocket服务:用于客户端和服务端之间通信,实现热更新。
  • 创建HMR广播器:管理多个通道,向客户端发送热更新消息。
  • 监听文件变化:文件变化时处理依赖和热更新相关逻辑。
  • 构建模块图:构建moduleGraph记录模块间的依赖关系。
  • 初始化插件容器:管理插件生命周期、上下文等。
  • 处理中间件:挂载Vite内部的一些中间件。
  • 依赖预构建:处理ES模块提升性能。