不知道大家有没有好奇过构建工具是如何把我们平常编写的代码,能够在浏览器上面运行出来的,我本人还是挺好奇的,因为平常工作中vite 使用的比较多,启动速度快,热更新速度快,所以也是挺喜欢使用vite,本着兴趣去学习了一下vite的源码,想着会不会有和我一样,想去学习源码,但又无从下手,今天就来带你体验不一样的源码阅读。

源码目录

这里的vite 的版本为5.2.11,这个版本也方便我们进行debug。我们可以看到vite 这边采用的pnpm 的 monorepo来管理项目的,而且vite 的代码结构非常的清晰,接下来我们主要要了解的就是 src/node 这里面的内容

今天我们来看一下vite 在开发环境下是如何启动一个服务器的,并且在启动服务器这期间都做了哪些事情,让我们深入源码,揭开它的面纱。

npm run dev

梦开始的地方,在package.json 文件中,这是我们最熟悉的命令了,执行完 npm run dev 后,会启动一个服务器,并且自动打开浏览器(配置open),然后就能显现出页面内容,那么这一切都是如何进行的呢?

 1  "scripts": {
 2    "dev": "vite"
 3  },

在实际的项目开发中,dev 这个命令一般都会拼接很多参数,这些处理大部分都是给vite传递参数(行内参数),同时我们更多的是通过 vite.config.ts(.js) 这个文件,来配置vite的。后续我们会讲解这一块,先继续往后看,最重要的核心就是执行了vite 这个命令

我们来看看vite 这个命令里面做了什么,在bin/vite.js 这个文件中

 1#!/usr/bin/env node
 2import { performance } from 'node:perf_hooks'
 3
 4if (!import.meta.url.includes('node_modules')) {
 5  try {
 6    
 7    await import('source-map-support').then((r) => r.default.install())
 8  } catch (e) {}
 9}
10
11global.__vite_start_time = performance.now()
12
13
14const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg))
15const filterIndex = process.argv.findIndex((arg) =>
16  /^(?:-f|--filter)$/.test(arg),
17)
18const profileIndex = process.argv.indexOf('--profile')
19
20if (debugIndex > 0) {
21  let value = process.argv[debugIndex + 1]
22  if (!value || value.startsWith('-')) {
23    value = 'vite:*'
24  } else {
25    
26    value = value
27      .split(',')
28      .map((v) => `vite:${v}`)
29      .join(',')
30  }
31  process.env.DEBUG = `${
32    process.env.DEBUG ? process.env.DEBUG + ',' : ''
33  }${value}`
34
35  if (filterIndex > 0) {
36    const filter = process.argv[filterIndex + 1]
37    if (filter && !filter.startsWith('-')) {
38      process.env.VITE_DEBUG_FILTER = filter
39    }
40  }
41}
42
43function start() {
44  return import('../dist/node/cli.js')
45}
46
47if (profileIndex > 0) {
48  process.argv.splice(profileIndex, 1)
49  const next = process.argv[profileIndex]
50  if (next && !next.startsWith('-')) {
51    process.argv.splice(profileIndex, 1)
52  }
53  const inspector = await import('node:inspector').then((r) => r.default)
54  const session = (global.__vite_profile_session = new inspector.Session())
55  session.connect()
56  session.post('Profiler.enable', () => {
57    session.post('Profiler.start', start)
58  })
59} else {
60  start()
61}

我们最主要的就是看start 这个函数,我们主要研究主流程,细枝末节的,感兴趣的可以去了解下。

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

这里使用的打包后的cli文件,源码位置/packages/vite/src/node/cli.ts

这一块是配置一些配置项,重点放在下面的 dev 执行的

 1cli
 2  .option('-c, --config <file>', `[string] use specified config file`)
 3  .option('--base <path>', `[string] public base path (default: /)`, {
 4    type: [convertBase],
 5  })
 6  .option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
 7  .option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
 8  .option('-d, --debug [feat]', `[string | boolean] show debug logs`)
 9  .option('-f, --filter <filter>', `[string] filter debug logs`)
10  .option('-m, --mode <mode>', `[string] set env mode`)

npm run dev 执行的就是 vite,也就是这里执行的dev下面的 action里面的方法

 1
 2cli
 3  .command('[root]', 'start dev server') 
 4  .alias('serve') 
 5  .alias('dev') 
 6  .option('--host [host]', `[string] specify hostname`, { type: [convertHost] })
 7  .option('--port <port>', `[number] specify port`)
 8  .option('--open [path]', `[boolean | string] open browser on startup`)
 9  .option('--cors', `[boolean] enable CORS`)
10  .option('--strictPort', `[boolean] exit if specified port is already in use`)
11  .option(
12    '--force',
13    `[boolean] force the optimizer to ignore the cache and re-bundle`,
14  )
15  .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
16    filterDuplicateOptions(options)
17
18    
19    
20    const { createServer } = await import('./server')
21    try {
22      const server = await createServer({
23        root,
24        base: options.base,
25        mode: options.mode,
26        configFile: options.config,
27        logLevel: options.logLevel,
28        clearScreen: options.clearScreen,
29        optimizeDeps: { force: options.force },
30        server: cleanOptions(options),
31      })
32
33      if (!server.httpServer) {
34        throw new Error('HTTP server not available')
35      }
36
37      await server.listen()
38
39      const info = server.config.logger.info
40
41      const viteStartTime = global.__vite_start_time ?? false
42      const startupDurationString = viteStartTime
43        ? colors.dim(
44            `ready in ${colors.reset(
45              colors.bold(Math.ceil(performance.now() - viteStartTime)),
46            )} ms`,
47          )
48        : ''
49      const hasExistingLogs =
50        process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0
51
52      info(
53        `\n  ${colors.green(
54          `${colors.bold('VITE')} v${VERSION}`,
55        )}  ${startupDurationString}\n`,
56        {
57          clear: !hasExistingLogs,
58        },
59      )
60
61      server.printUrls()
62      const customShortcuts: CLIShortcut<typeof server>[] = []
63      if (profileSession) {
64        customShortcuts.push({
65          key: 'p',
66          description: 'start/stop the profiler',
67          async action(server) {
68            if (profileSession) {
69              await stopProfiler(server.config.logger.info)
70            } else {
71              const inspector = await import('node:inspector').then(
72                (r) => r.default,
73              )
74              await new Promise<void>((res) => {
75                profileSession = new inspector.Session()
76                profileSession.connect()
77                profileSession.post('Profiler.enable', () => {
78                  profileSession!.post('Profiler.start', () => {
79                    server.config.logger.info('Profiler started')
80                    res()
81                  })
82                })
83              })
84            }
85          },
86        })
87      }
88      server.bindCLIShortcuts({ print: true, customShortcuts })
89    } catch (e) {
90      const logger = createLogger(options.logLevel)
91      logger.error(colors.red(`error when starting dev server:\n${e.stack}`), {
92        error: e,
93      })
94      stopProfiler(logger.info)
95      process.exit(1)
96    }
97  })

执行cli

dev 模式下主要执行的就是下面两个步骤

  1. createServer
  2. server.listen

除了执行这两个以外,还做了一些交互上的处理,如:打印服务器的url、绑定一些快捷命令(直接在命令行输入指令)

打印服务器的 URL server.printUrls();

自定义快捷键

 1 
 2server.bindCLIShortcuts({ print: true, customShortcuts });
 3
 4
 5const BASE_DEV_SHORTCUTS: CLIShortcut<ViteDevServer>[] = [
 6  {
 7    key: "r",
 8    description: "restart the server",
 9    async action(server) {
10      await restartServerWithUrls(server);
11    },
12  },
13  {
14    key: "u",
15    description: "show server url",
16    action(server) {
17      server.config.logger.info("");
18      server.printUrls();
19    },
20  },
21  {
22    key: "o",
23    description: "open in browser",
24    action(server) {
25      server.openBrowser();
26    },
27  },
28  {
29    key: "c",
30    description: "clear console",
31    action(server) {
32      server.config.logger.clearScreen("error");
33    },
34  },
35  {
36    key: "q",
37    description: "quit",
38    async action(server) {
39      await server.close().finally(() => process.exit());
40    },
41  },
42];
43

1. createServer

重点在创建服务器启动服务器这两个步骤,接下来我们详细来看。位置在packages/vite/src/node/server/index.ts 后续的步骤都是在 vite/src/node 下面的目录中,之后我就以vite/来表示文件存在的位置了

cli.ts 中导入了createServer 这个方法,然而这个方法实际上调用的是_createServer

 1export function createServer(
 2  inlineConfig: InlineConfig = {},
 3): Promise<ViteDevServer> {
 4  return _createServer(inlineConfig, { hotListen: true })
 5}

这个函数涉及到的源码过多,我展示主要的部分

 1export async function _createServer(
 2  inlineConfig: InlineConfig = {},
 3  options: { hotListen: boolean },
 4): Promise<ViteDevServer> {
 5  const config = await resolveConfig(inlineConfig, 'serve')
 6
 7  const initPublicFilesPromise = initPublicFiles(config);
 8
 9  const httpsOptions = await resolveHttpsConfig(config.server.https);
10  
11  const middlewares = connect() as Connect.Server;
12  const httpServer = middlewareMode
13    ? null
14    : await resolveHttpServer(serverConfig, middlewares, httpsOptions);
15
16  const ws = createWebSocketServer(httpServer, config, httpsOptions)
17  const hot = createHMRBroadcaster()
18    .addChannel(ws)
19    .addChannel(createServerHMRChannel())
20
21  
22
23  const container = await createPluginContainer(config, moduleGraph, watcher)
24
25  const devHtmlTransformFn = createDevHtmlTransformFn(config)
26
27    let server: ViteDevServer = {
28    config,
29    middlewares,
30    httpServer,
31    watcher,
32    pluginContainer: container,
33    ws,
34    hot,
35    moduleGraph,
36    resolvedUrls: null, 
37    transformRequest(url, options) {
38      return transformRequest(url, server, options)
39    },
40    async warmupRequest(url, options) {
41      try {
42        await transformRequest(url, server, options)
43      } catch (e) {
44        if (
45          e?.code === ERR_OUTDATED_OPTIMIZED_DEP ||
46          e?.code === ERR_CLOSED_SERVER
47        ) {
48          
49          return
50        }
51        
52        server.config.logger.error(`Pre-transform error: ${e.message}`, {
53          error: e,
54          timestamp: true,
55        })
56      }
57    },
58    transformIndexHtml(url, html, originalUrl) {
59      return devHtmlTransformFn(server, url, html, originalUrl)
60    },
61       
62    async ssrFetchModule(url: string, importer?: string) {
63      return ssrFetchModule(server, url, importer)
64    },
65   
66    async listen(port?: number, isRestart?: boolean) {
67      await startServer(server, port)
68      if (httpServer) {
69        server.resolvedUrls = await resolveServerUrls(
70          httpServer,
71          config.server,
72          config,
73        )
74        if (!isRestart && config.server.open) server.openBrowser()
75      }
76      return server
77    },
78    openBrowser() {
79      const options = server.config.server
80      const url =
81        server.resolvedUrls?.local[0] ?? server.resolvedUrls?.network[0]
82      if (url) {
83        const path =
84          typeof options.open === 'string'
85            ? new URL(options.open, url).href
86            : url
87
88        
89        
90        
91        
92        if (server.config.server.preTransformRequests) {
93          setTimeout(() => {
94            const getMethod = path.startsWith('https:') ? httpsGet : httpGet
95
96            getMethod(
97              path,
98              {
99                headers: {
100                  
101                  Accept: 'text/html',
102                },
103              },
104              (res) => {
105                res.on('end', () => {
106                  
107                  
108                })
109              },
110            )
111              .on('error', () => {
112                
113              })
114              .end()
115          }, 0)
116        }
117
118        _openBrowser(path, true, server.config.logger)
119      } else {
120        server.config.logger.warn('No URL available to open in browser')
121      }
122    },
123    async close() {
124      if (!middlewareMode) {
125        process.off('SIGTERM', exitProcess)
126        if (process.env.CI !== 'true') {
127          process.stdin.off('end', exitProcess)
128        }
129      }
130      await Promise.allSettled([
131        watcher.close(),
132        hot.close(),
133        container.close(),
134        crawlEndFinder?.cancel(),
135        getDepsOptimizer(server.config)?.close(),
136        getDepsOptimizer(server.config, true)?.close(),
137        closeHttpServer(),
138      ])
139      
140      
141      
142      
143      
144      
145      while (server._pendingRequests.size > 0) {
146        await Promise.allSettled(
147          [...server._pendingRequests.values()].map(
148            (pending) => pending.request,
149          ),
150        )
151      }
152      server.resolvedUrls = null
153    },
154    printUrls() {
155      if (server.resolvedUrls) {
156        printServerUrls(
157          server.resolvedUrls,
158          serverConfig.host,
159          config.logger.info,
160        )
161      } else if (middlewareMode) {
162        throw new Error('cannot print server URLs in middleware mode.')
163      } else {
164        throw new Error(
165          'cannot print server URLs before server.listen is called.',
166        )
167      }
168    },
169    bindCLIShortcuts(options) {
170      bindCLIShortcuts(server, options)
171    },
172
173
174    waitForRequestsIdle,
175    _registerRequestProcessing,
176    _onCrawlEnd,
177
178    _setInternalServer(_server: ViteDevServer) {
179      
180      
181      server = _server
182    },
183    _restartPromise: null,
184    _importGlobMap: new Map(),
185    _forceOptimizeOnRestart: false,
186    _pendingRequests: new Map(),
187   
188  }
189
190  
191
192  
193  
194    middlewares.use(cachedTransformMiddleware(server))
195 
196  
197
198
199  
200    const initServer = async () => {
201      if (serverInited) return
202      if (initingServer) return initingServer
203  
204      initingServer = (async function () {
205        await container.buildStart({})
206        
207        
208        if (isDepsOptimizerEnabled(config, false)) {
209          await initDepsOptimizer(config, server)
210        }
211        warmupFiles(server)
212        initingServer = undefined
213        serverInited = true
214      })()
215      return initingServer
216    }
217
218    if (!middlewareMode && httpServer) {
219      
220      
221      const listen = httpServer.listen.bind(httpServer);
222
223    
224    httpServer.listen = (async (port: number, ...args: any[]) => {
225        try {
226          
227          hot.listen();
228          
229          await initServer();
230        } catch (e) {
231          
232          httpServer.emit("error", e);
233          return;
234        }
235        
236        return listen(port, ...args);
237      }) as any;
238    } else {
239      
240      if (options.hotListen) {
241        
242        hot.listen();
243      }
244      
245      await initServer();
246    }
247
248  
249
250  
251  return server
252}

这个函数最主要的作用就是在创建服务器实例的时候做了很多很多初始化的工作,后面的文章我们都会慢慢讲到的,不用心急,我们现在主要关注主流程即可

2. server.listen

这是cli中的代码,先创建vite 服务器,然后调用listen 去启动服务器

 1const server = await createServer({
 2    root,
 3    base: options.base,
 4    mode: options.mode,
 5    configFile: options.config,
 6    logLevel: options.logLevel,
 7    clearScreen: options.clearScreen,
 8    optimizeDeps: { force: options.force },
 9    server: cleanOptions(options),
10  })
11
12  if (!server.httpServer) {
13    throw new Error('HTTP server not available')
14  }
15
16  await server.listen()

await server.listen() 我们来看这个里面做了哪些事情。listen 方法是在server 中定义的,用于启动服务器

 1async listen(port?: number, isRestart?: boolean) {
 2    await startServer(server, port)
 3    if (httpServer) {
 4      server.resolvedUrls = await resolveServerUrls(
 5        httpServer,
 6        config.server,
 7        config,
 8      )
 9      if (!isRestart && config.server.open) server.openBrowser()
10    }
11    return server
12  },
  1. 调用 startServer,启动一个服务器
  2. 调用 resolveServerUrls,解析服务器的本地和网络 URL
  3. 如果不是重启且配置了open(打开浏览器),则会调用 server 身上的openBrowser 打开浏览器

startServer 这个函数用于启动 Vite 开发服务器,接收一个 ViteDevServer 实例和一个可选的端口号 inlinePort,并在特定条件下启动 HTTP 服务器

 1async function startServer(
 2  server: ViteDevServer,
 3  inlinePort?: number
 4): Promise<void> {
 5  const httpServer = server.httpServer;
 6  if (!httpServer) {
 7    
 8    throw new Error("Cannot call server.listen in middleware mode.");
 9  }
10
11  
12  const options = server.config.server;
13  
14  const hostname = await resolveHostname(options.host);
15  
16  const configPort = inlinePort ?? options.port;
17
18  
19   * 1. 非严格端口模式在开发服务器的配置中可以选择是否启用严格端口模式
20   *    非严格端口模式下开发服务器可以使用操作系统提供的可用端口而不仅限于配置中指定的端口
21   * 2. 端口可能不一致在重新启动服务器时如果之前使用的端口仍然可用开发服务器可能会选择重新使用该端口
22   *    这种情况下服务器当前运行的端口可能会与配置中指定的端口不同
23   * 3. 避免浏览器标签页切换为了避免正在运行的浏览器标签页因为端口变化而刷新或重新加载
24   *    开发服务器会尽量保持之前使用的端口不变除非配置中显式地更改了端口设置
25   *
26   * 这样的设计能够确保开发过程中开发服务器的端口变化对开发者在浏览器中打开的标签页造成的干扰最小化
27   * 提升开发体验的连续性和稳定性
28   */
29
30  
31  
32  const port =
33    (!configPort || configPort === server._configServerPort
34      ? server._currentServerPort
35      : configPort) ?? DEFAULT_DEV_PORT;
36
37  
38  server._configServerPort = configPort;
39
40  
41  const serverPort = await httpServerStart(httpServer, {
42    port,
43    strictPort: options.strictPort,
44    host: hostname.host,
45    logger: server.config.logger,
46  });
47  
48  server._currentServerPort = serverPort;
49}

startServer 里面实际调用了 httpServerStart 这个方法来去启动服务器(listen)

 1export async function httpServerStart(
 2  httpServer: HttpServer,
 3  serverOptions: {
 4    port: number;
 5    strictPort: boolean | undefined;
 6    host: string | undefined;
 7    logger: Logger;
 8  }
 9): Promise<number> {
10  let { port, strictPort, host, logger } = serverOptions;
11
12  return new Promise((resolve, reject) => {
13    const onError = (e: Error & { code?: string }) => {
14      if (e.code === "EADDRINUSE") {
15        if (strictPort) {
16          httpServer.removeListener("error", onError);
17          reject(new Error(`Port ${port} is already in use`));
18        } else {
19          logger.info(`Port ${port} is in use, trying another one...`);
20          httpServer.listen(++port, host);
21        }
22      } else {
23        httpServer.removeListener("error", onError);
24        reject(e);
25      }
26    };
27
28    httpServer.on("error", onError);
29
30    httpServer.listen(port, host, () => {
31      httpServer.removeListener("error", onError);
32      resolve(port);
33    });
34  });
35}

这里的重点就是 httpServer.listen ,还记得吗,在创建server 的时候,这里已经把listen 方法重写了,来让我们回顾一下:

  1. 先保存一份原始的listen 方法
  2. 重写listen方法,在执行原始的listen 方法之间做一些初始化的事情,例如:
    • 启动热更新服
    • 调用initServer 这个方法里面最重要的就是预构建依赖,其次是对一些文件预热

下面的这段代码出现在createServer 中

 1if (!middlewareMode && httpServer) {
 2    
 3    
 4    const listen = httpServer.listen.bind(httpServer);
 5
 6    
 7    httpServer.listen = (async (port: number, ...args: any[]) => {
 8      try {
 9        
10        hot.listen();
11        
12        await initServer();
13      } catch (e) {
14        
15        httpServer.emit("error", e);
16        return;
17      }
18      
19      return listen(port, ...args);
20    }) as any;
21  } else {
22    
23    if (options.hotListen) {
24      
25      hot.listen();
26    }
27    
28    await initServer();
29  }

我们再来看看initServer,这个函数主要用于初始化服务器。目的是为了确保在服务器启动时,一些关键的初始化步骤只执行一次,即使 httpServer.listen 被多次调用。这是为了避免重复执行 buildStart 以及其他初始化逻辑

 1 const initServer = async () => {
 2    
 3    if (serverInited) return;
 4    
 5    if (initingServer) return initingServer;
 6
 7    
 8    initingServer = (async function () {
 9      
10      await container.buildStart({});
11      
12      if (isDepsOptimizerEnabled(config, false)) {
13        
14        
15        await initDepsOptimizer(config, server);
16      }
17
18      
19      warmupFiles(server);
20
21      
22      initingServer = undefined;
23      
24      serverInited = true;
25    })();
26    return initingServer;
27  };

这里提一下 buildStart 这里通过插件容器去调用 插件的buildStart钩子函数,一些插件需要在此做一些初始化的事情

这里是pluginContainer 自身的buildStart,会并行执行所有插件的 buildStart 钩子

 1async buildStart() {
 2    await handleHookPromise(
 3      hookParallel(
 4        "buildStart",
 5        (plugin) => new Context(plugin),
 6        () => [container.options as NormalizedInputOptions]
 7      )
 8    );
 9  },

下面是客户端注入常量的插件,在buildStart 钩子做了一些初始化,在transform 钩子的时候去替换源码中定义的常量,将其转换为真实的常量。源码位置vite/src/node/plugins/clientInjections.ts

 1export function clientInjectionsPlugin(config: ResolvedConfig): Plugin {
 2  let injectConfigValues: (code: string) => string
 3
 4  return {
 5    name: 'vite:client-inject',
 6    async buildStart() {
 7      const resolvedServerHostname = (await resolveHostname(config.server.host))
 8        .name
 9      const resolvedServerPort = config.server.port!
10      const devBase = config.base
11
12      const serverHost = `${resolvedServerHostname}:${resolvedServerPort}${devBase}`
13
14      let hmrConfig = config.server.hmr
15      hmrConfig = isObject(hmrConfig) ? hmrConfig : undefined
16      const host = hmrConfig?.host || null
17      const protocol = hmrConfig?.protocol || null
18      const timeout = hmrConfig?.timeout || 30000
19      const overlay = hmrConfig?.overlay !== false
20      const isHmrServerSpecified = !!hmrConfig?.server
21      const hmrConfigName = path.basename(config.configFile || 'vite.config.js')
22
23      
24      
25      let port = hmrConfig?.clientPort || hmrConfig?.port || null
26      if (config.server.middlewareMode && !isHmrServerSpecified) {
27        port ||= 24678
28      }
29
30      let directTarget = hmrConfig?.host || resolvedServerHostname
31      directTarget += `:${hmrConfig?.port || resolvedServerPort}`
32      directTarget += devBase
33
34      let hmrBase = devBase
35      if (hmrConfig?.path) {
36        hmrBase = path.posix.join(hmrBase, hmrConfig.path)
37      }
38
39      const userDefine: Record<string, any> = {}
40      for (const key in config.define) {
41        
42        if (!key.startsWith('import.meta.env.')) {
43          userDefine[key] = config.define[key]
44        }
45      }
46      const serializedDefines = serializeDefine(userDefine)
47
48      const modeReplacement = escapeReplacement(config.mode)
49      const baseReplacement = escapeReplacement(devBase)
50      const definesReplacement = () => serializedDefines
51      const serverHostReplacement = escapeReplacement(serverHost)
52      const hmrProtocolReplacement = escapeReplacement(protocol)
53      const hmrHostnameReplacement = escapeReplacement(host)
54      const hmrPortReplacement = escapeReplacement(port)
55      const hmrDirectTargetReplacement = escapeReplacement(directTarget)
56      const hmrBaseReplacement = escapeReplacement(hmrBase)
57      const hmrTimeoutReplacement = escapeReplacement(timeout)
58      const hmrEnableOverlayReplacement = escapeReplacement(overlay)
59      const hmrConfigNameReplacement = escapeReplacement(hmrConfigName)
60
61      injectConfigValues = (code: string) => {
62        return code
63          .replace(`__MODE__`, modeReplacement)
64          .replace(/__BASE__/g, baseReplacement)
65          .replace(`__DEFINES__`, definesReplacement)
66          .replace(`__SERVER_HOST__`, serverHostReplacement)
67          .replace(`__HMR_PROTOCOL__`, hmrProtocolReplacement)
68          .replace(`__HMR_HOSTNAME__`, hmrHostnameReplacement)
69          .replace(`__HMR_PORT__`, hmrPortReplacement)
70          .replace(`__HMR_DIRECT_TARGET__`, hmrDirectTargetReplacement)
71          .replace(`__HMR_BASE__`, hmrBaseReplacement)
72          .replace(`__HMR_TIMEOUT__`, hmrTimeoutReplacement)
73          .replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement)
74          .replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement)
75      }
76    },
77    async transform(code, id, options) {
78      if (id === normalizedClientEntry || id === normalizedEnvEntry) {
79        return injectConfigValues(code)
80      } else if (!options?.ssr && code.includes('process.env.NODE_ENV')) {
81        
82        
83        
84        const nodeEnv =
85          config.define?.['process.env.NODE_ENV'] ||
86          JSON.stringify(process.env.NODE_ENV || config.mode)
87        return await replaceDefine(
88          code,
89          id,
90          {
91            'process.env.NODE_ENV': nodeEnv,
92            'global.process.env.NODE_ENV': nodeEnv,
93            'globalThis.process.env.NODE_ENV': nodeEnv,
94          },
95          config,
96        )
97      }
98    },
99  }
100}

让我们在回到主线流程上面来

 1 async listen(port?: number, isRestart?: boolean) {
 2    await startServer(server, port)
 3    if (httpServer) {
 4      server.resolvedUrls = await resolveServerUrls(
 5        httpServer,
 6        config.server,
 7        config,
 8      )
 9      if (!isRestart && config.server.open) server.openBrowser()
10    }
11    return server
12  },

启动完服务器后,会去调用openBrowser 打开浏览器,这里对windows 和 mac 系统做了不同的处理,windows 通过调用 open 这个包去打开浏览器,而mac 电脑 则通过node子进程 去执行一些命令来打开浏览器。源码位置vite/src/node/server/openBrowser.ts

至此,执行完 npm run dev 启动vite 服务器的大致流程就算完了,但是要想真正的能够正常的打开浏览器显示里面的内容 还需要很多工作的处理,如:

  • 中间件(静态文件服务中间件、文件转换中间件、html回退中间件、index.html 转换中间件等)
  • 预构建依赖(单独出一章讲解)
  • 转换代码(import {createApp} from ‘vue’ 转换为浏览器能够识别)

使用vscode 的debug 来调试源码

我们单从源码上去看,去理解vite的执行流程,这样会很费时间,通过debug模式,这样我们可以清晰的知道vite的执行流程是如何的,也极大的方便了我们阅读和理解源码。

这里使用的vite 版本是 5.2.11,这个版本打包后生成的文件会后map,map文件很关键,有了这个文件在debug的时候才能回到我们编写时的代码,不然的话就是打包后的代码,相信眼尖的同学在文章中已经看到过debug的标记了,现在就来讲一讲如何在vscode 中来开启debug

我们点击vscode 的 debug 选项卡,点击创建launch.json文件,选择调试环境为Node.js,这样就生成了一个.vscode/launch.json 文件

 1{
 2    
 3    
 4    
 5    "version": "0.2.0",
 6    "configurations": [
 7        {
 8            "type": "node",
 9            "request": "launch",
10            "name": "启动程序",
11            "skipFiles": [
12                "<node_internals>/**"
13            ],
14            "cwd": "${workspaceFolder}\\packages\\vite",
15            "program": "${workspaceFolder}\\packages\\vite\\bin\\vite.js",
16            "outFiles": [
17                "${workspaceFolder}/**/*.js"
18            ]
19        },
20        {
21            "type": "node",
22            "request": "launch",
23            "name": "vite 调试 vue3",
24            "runtimeExecutable": "npm",
25            "runtimeArgs": [
26                "run",
27                "dev"
28            ],
29            "skipFiles": [
30                "<node_internals>/**"
31            ],
32            "cwd": "${workspaceFolder}\\packages\\vue-demo",
33            "outFiles": [
34                "${workspaceFolder}/**/*.js"
35            ]
36        }
37    ]
38}

我这边有两个,是因为初次调试的时候使用的第一个,第二个是结合demo调试使用的,大家可以选择第二个配置项vite 调试 vue3,这个对象里面的配置

我们来到vite的源码的根目录,执行pnpm install 安装所有依赖,然后再来到packages/vite 这个目录,执行pnpm run dev,生成打包后的文件

接下来我们在packages 目录下面创建一个demo,这里使用的vue3

 1mkdir vue-demo
 2cd vue-demo
 3pnpm init
 4pnpm install vue
 5pnpm install vite@workspace @vitejs/plugin-vue -D

这里创建目录,初始化,安装vue,安装vite,安装解析vue的插件。这里的安装vite需要注意,必须vite@workspace,因为这样安装的vite就是本项目下面的vite,这里也相当于是软连接。然后再package.json 是编写启动脚本 “dev”: “vite”

index.html

 1<!DOCTYPE html>
 2<html lang="en">
 3
 4<head>
 5    <meta charset="UTF-8">
 6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 7    <title>Document</title>
 8</head>
 9
10<body>
11    <div id="app"></div>
12</body>
13<script type="module" src="./src/main.ts"></script>
14
15</html>

vite.config.ts

 1import vue from '@vitejs/plugin-vue'
 2
 3export default {
 4  server: {
 5    open: true,
 6  },
 7  plugins: [vue()],
 8}

src/App.vue

 1<template>
 2  <div>App.vue</div>
 3</template>
 4
 5<script setup lang="ts"></script>
 6
 7<style lang="scss" scoped></style>
 8

src/main.ts

 1import { createApp } from 'vue'
 2import root from './App.vue'
 3
 4const app = createApp(root)
 5
 6app.mount('#app')
 7

以上的配置完成后,我们就可以开始debug调试vite源码啦 我们来到packages/vite/src/node/cli.ts 文件中,打上debug标记

然后打开debug 选项卡,点击运行,然后就会发现代码卡在了我们标记的地方,同时也出现了一些调试的按钮,现在你就可以像浏览器debug一样随心所欲的开始调试vite源码了

在我的github项目中,对主流程的代码都有注释解释,目前进度是正常打开浏览器后能正常显示vue编写的代码,热更新、样式文件的处理等后续会慢慢更新,想要提前看的可以去代码里面看

项目地址

个人笔记记录 2021 ~ 2025