前言
不知道大家有没有好奇过构建工具是如何把我们平常编写的代码,能够在浏览器上面运行出来的,我本人还是挺好奇的,因为平常工作中vite 使用的比较多,启动速度快,热更新速度快,所以也是挺喜欢使用vite,本着兴趣去学习了一下vite的源码,想着会不会有和我一样,想去学习源码,但又无从下手,今天就来带你体验不一样的源码阅读。
源码目录
这里的vite 的版本为5.2.11 ,这个版本也方便我们进行debug。我们可以看到vite 这边采用的pnpm 的 monorepo来管理项目的,而且vite 的代码结构非常的清晰,接下来我们主要要了解的就是 src/node 这里面的内容
今天我们来看一下vite 在开发环境下是如何启动一个服务器的,并且在启动服务器这期间都做了哪些事情,让我们深入源码,揭开它的面纱。
npm run dev
梦开始的地方,在package.json 文件中,这是我们最熟悉的命令了,执行完 npm run dev 后,会启动一个服务器,并且自动打开浏览器(配置open),然后就能显现出页面内容,那么这一切都是如何进行的呢?
json
"scripts": {
"dev": "vite"
},
在实际的项目开发中,dev 这个命令一般都会拼接很多参数,这些处理大部分都是给vite传递参数(行内参数),同时我们更多的是通过 vite.config.ts(.js) 这个文件,来配置vite的。后续我们会讲解这一块,先继续往后看,最重要的核心就是执行了vite 这个命令
我们来看看vite 这个命令里面做了什么,在bin/vite.js 这个文件中
js
#!/usr/bin/env node
import { performance } from 'node:perf_hooks'
if (!import.meta.url.includes('node_modules')) {
try {
// only available as dev dependency
await import('source-map-support').then((r) => r.default.install())
} catch (e) {}
}
global.__vite_start_time = performance.now()
// check debug mode first before requiring the CLI.
const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg))
const filterIndex = process.argv.findIndex((arg) =>
/^(?:-f|--filter)$/.test(arg),
)
const profileIndex = process.argv.indexOf('--profile')
if (debugIndex > 0) {
let value = process.argv[debugIndex + 1]
if (!value || value.startsWith('-')) {
value = 'vite:*'
} else {
// support debugging multiple flags with comma-separated list
value = value
.split(',')
.map((v) => `vite:${v}`)
.join(',')
}
process.env.DEBUG = `${
process.env.DEBUG ? process.env.DEBUG + ',' : ''
}${value}`
if (filterIndex > 0) {
const filter = process.argv[filterIndex + 1]
if (filter && !filter.startsWith('-')) {
process.env.VITE_DEBUG_FILTER = filter
}
}
}
function start() {
return import('../dist/node/cli.js')
}
if (profileIndex > 0) {
process.argv.splice(profileIndex, 1)
const next = process.argv[profileIndex]
if (next && !next.startsWith('-')) {
process.argv.splice(profileIndex, 1)
}
const inspector = await import('node:inspector').then((r) => r.default)
const session = (global.__vite_profile_session = new inspector.Session())
session.connect()
session.post('Profiler.enable', () => {
session.post('Profiler.start', start)
})
} else {
start()
}
我们最主要的就是看start 这个函数,我们主要研究主流程,细枝末节的,感兴趣的可以去了解下。
js
function start() {
return import('../dist/node/cli.js')
}
这里使用的打包后的cli文件,源码位置/packages/vite/src/node/cli.ts
这一块是配置一些配置项,重点放在下面的 dev 执行的
ts
cli
.option('-c, --config <file>', `[string] use specified config file`)
.option('--base <path>', `[string] public base path (default: /)`, {
type: [convertBase],
})
.option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
.option('-d, --debug [feat]', `[string | boolean] show debug logs`)
.option('-f, --filter <filter>', `[string] filter debug logs`)
.option('-m, --mode <mode>', `[string] set env mode`)
npm run dev 执行的就是 vite,也就是这里执行的dev下面的 action里面的方法
ts
// dev
cli
.command('[root]', 'start dev server') // default command
.alias('serve') // the command is called 'serve' in Vite's API
.alias('dev') // alias to align with the script name
.option('--host [host]', `[string] specify hostname`, { type: [convertHost] })
.option('--port <port>', `[number] specify port`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--cors', `[boolean] enable CORS`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option(
'--force',
`[boolean] force the optimizer to ignore the cache and re-bundle`,
)
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
filterDuplicateOptions(options)
// output structure is preserved even after bundling so require()
// is ok here
const { createServer } = await import('./server')
try {
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),
})
if (!server.httpServer) {
throw new Error('HTTP server not available')
}
await server.listen()
const info = server.config.logger.info
const viteStartTime = global.__vite_start_time ?? false
const startupDurationString = viteStartTime
? colors.dim(
`ready in ${colors.reset(
colors.bold(Math.ceil(performance.now() - viteStartTime)),
)} ms`,
)
: ''
const hasExistingLogs =
process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0
info(
`\n ${colors.green(
`${colors.bold('VITE')} v${VERSION}`,
)} ${startupDurationString}\n`,
{
clear: !hasExistingLogs,
},
)
server.printUrls()
const customShortcuts: CLIShortcut<typeof server>[] = []
if (profileSession) {
customShortcuts.push({
key: 'p',
description: 'start/stop the profiler',
async action(server) {
if (profileSession) {
await stopProfiler(server.config.logger.info)
} else {
const inspector = await import('node:inspector').then(
(r) => r.default,
)
await new Promise<void>((res) => {
profileSession = new inspector.Session()
profileSession.connect()
profileSession.post('Profiler.enable', () => {
profileSession!.post('Profiler.start', () => {
server.config.logger.info('Profiler started')
res()
})
})
})
}
},
})
}
server.bindCLIShortcuts({ print: true, customShortcuts })
} catch (e) {
const logger = createLogger(options.logLevel)
logger.error(colors.red(`error when starting dev server:\n${e.stack}`), {
error: e,
})
stopProfiler(logger.info)
process.exit(1)
}
})
执行cli
dev 模式下主要执行的就是下面两个步骤
- createServer
- server.listen
除了执行这两个以外,还做了一些交互上的处理,如:打印服务器的url、绑定一些快捷命令(直接在命令行输入指令)
打印服务器的 URL server.printUrls();
自定义快捷键
ts
//绑定cli 快捷命令
server.bindCLIShortcuts({ print: true, customShortcuts });
//开发模式下的一些快捷键
const BASE_DEV_SHORTCUTS: CLIShortcut<ViteDevServer>[] = [
{
key: "r",
description: "restart the server",
async action(server) {
await restartServerWithUrls(server);
},
},
{
key: "u",
description: "show server url",
action(server) {
server.config.logger.info("");
server.printUrls();
},
},
{
key: "o",
description: "open in browser",
action(server) {
server.openBrowser();
},
},
{
key: "c",
description: "clear console",
action(server) {
server.config.logger.clearScreen("error");
},
},
{
key: "q",
description: "quit",
async action(server) {
await server.close().finally(() => process.exit());
},
},
];
1. createServer
重点在创建服务器 和启动服务器这两个步骤,接下来我们详细来看。位置在packages/vite/src/node/server/index.ts 后续的步骤都是在 vite/src/node 下面的目录中,之后我就以vite/来表示文件存在的位置了
cli.ts 中导入了createServer 这个方法,然而这个方法实际上调用的是_createServer
ts
export function createServer(
inlineConfig: InlineConfig = {},
): Promise<ViteDevServer> {
return _createServer(inlineConfig, { hotListen: true })
}
这个函数涉及到的源码过多,我展示主要的部分
ts
export async function _createServer(
inlineConfig: InlineConfig = {},
options: { hotListen: boolean },
): Promise<ViteDevServer> {
const config = await resolveConfig(inlineConfig, 'serve')
const initPublicFilesPromise = initPublicFiles(config);
const httpsOptions = await resolveHttpsConfig(config.server.https);
const middlewares = connect() as Connect.Server;
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
const ws = createWebSocketServer(httpServer, config, httpsOptions)
const hot = createHMRBroadcaster()
.addChannel(ws)
.addChannel(createServerHMRChannel())
/* ...... */
const container = await createPluginContainer(config, moduleGraph, watcher)
const devHtmlTransformFn = createDevHtmlTransformFn(config)
let server: ViteDevServer = {
config,
middlewares,
httpServer,
watcher,
pluginContainer: container,
ws,
hot,
moduleGraph,
resolvedUrls: null, // will be set on listen
transformRequest(url, options) {
return transformRequest(url, server, options)
},
async warmupRequest(url, options) {
try {
await transformRequest(url, server, options)
} catch (e) {
if (
e?.code === ERR_OUTDATED_OPTIMIZED_DEP ||
e?.code === ERR_CLOSED_SERVER
) {
// these are expected errors
return
}
// Unexpected error, log the issue but avoid an unhandled exception
server.config.logger.error(`Pre-transform error: ${e.message}`, {
error: e,
timestamp: true,
})
}
},
transformIndexHtml(url, html, originalUrl) {
return devHtmlTransformFn(server, url, html, originalUrl)
},
/* ....*/
async ssrFetchModule(url: string, importer?: string) {
return ssrFetchModule(server, url, importer)
},
async listen(port?: number, isRestart?: boolean) {
await startServer(server, port)
if (httpServer) {
server.resolvedUrls = await resolveServerUrls(
httpServer,
config.server,
config,
)
if (!isRestart && config.server.open) server.openBrowser()
}
return server
},
openBrowser() {
const options = server.config.server
const url =
server.resolvedUrls?.local[0] ?? server.resolvedUrls?.network[0]
if (url) {
const path =
typeof options.open === 'string'
? new URL(options.open, url).href
: url
// We know the url that the browser would be opened to, so we can
// start the request while we are awaiting the browser. This will
// start the crawling of static imports ~500ms before.
// preTransformRequests needs to be enabled for this optimization.
if (server.config.server.preTransformRequests) {
setTimeout(() => {
const getMethod = path.startsWith('https:') ? httpsGet : httpGet
getMethod(
path,
{
headers: {
// Allow the history middleware to redirect to /index.html
Accept: 'text/html',
},
},
(res) => {
res.on('end', () => {
// Ignore response, scripts discovered while processing the entry
// will be preprocessed (server.config.server.preTransformRequests)
})
},
)
.on('error', () => {
// Ignore errors
})
.end()
}, 0)
}
_openBrowser(path, true, server.config.logger)
} else {
server.config.logger.warn('No URL available to open in browser')
}
},
async close() {
if (!middlewareMode) {
process.off('SIGTERM', exitProcess)
if (process.env.CI !== 'true') {
process.stdin.off('end', exitProcess)
}
}
await Promise.allSettled([
watcher.close(),
hot.close(),
container.close(),
crawlEndFinder?.cancel(),
getDepsOptimizer(server.config)?.close(),
getDepsOptimizer(server.config, true)?.close(),
closeHttpServer(),
])
// Await pending requests. We throw early in transformRequest
// and in hooks if the server is closing for non-ssr requests,
// so the import analysis plugin stops pre-transforming static
// imports and this block is resolved sooner.
// During SSR, we let pending requests finish to avoid exposing
// the server closed error to the users.
while (server._pendingRequests.size > 0) {
await Promise.allSettled(
[...server._pendingRequests.values()].map(
(pending) => pending.request,
),
)
}
server.resolvedUrls = null
},
printUrls() {
if (server.resolvedUrls) {
printServerUrls(
server.resolvedUrls,
serverConfig.host,
config.logger.info,
)
} else if (middlewareMode) {
throw new Error('cannot print server URLs in middleware mode.')
} else {
throw new Error(
'cannot print server URLs before server.listen is called.',
)
}
},
bindCLIShortcuts(options) {
bindCLIShortcuts(server, options)
},
waitForRequestsIdle,
_registerRequestProcessing,
_onCrawlEnd,
_setInternalServer(_server: ViteDevServer) {
// Rebind internal the server variable so functions reference the user
// server instance after a restart
server = _server
},
_restartPromise: null,
_importGlobMap: new Map(),
_forceOptimizeOnRestart: false,
_pendingRequests: new Map(),
/* ... */
}
/*....*/
//这里是注册一些中间件,后面详细说
middlewares.use(cachedTransformMiddleware(server))
/*...*/
// 这个函数在初始化vite 服务器
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
// 这里是vite 预构建依赖的地方
if (isDepsOptimizerEnabled(config, false)) {
await initDepsOptimizer(config, server)
}
warmupFiles(server)
initingServer = undefined
serverInited = true
})()
return initingServer
}
if (!middlewareMode && httpServer) {
// 确保不是中间件模式且存在 httpServer 实例。
// 将原始的 httpServer.listen 方法绑定到 listen 变量上,以便稍后调用。
const listen = httpServer.listen.bind(httpServer);
// 覆盖 httpServer.listen 方法,在实际调用原始 listen 方法之前,先执行一些初始化操作。
httpServer.listen = (async (port: number, ...args: any[]) => {
try {
//确保 WebSocket 服务器启动
hot.listen();
//initServer 确保某些组件或优化器在服务器启动前已经初始化
await initServer();
} catch (e) {
//捕获错误并发出 error 事件
httpServer.emit("error", e);
return;
}
//调用原始的 listen 方法:
return listen(port, ...args);
}) as any;
} else {
//处理中间件模式或没有 httpServer 的清空
if (options.hotListen) {
//options.hotListen 为 true,则启动 WebSocket 服务器
hot.listen();
}
//调用 initServer 进行初始化
await initServer();
}
/*....*/
// 将server 返回,cli中去调用listen启动服务器
return server
}
这个函数最主要的作用就是在创建服务器实例的时候做了很多很多初始化的工作,后面的文章我们都会慢慢讲到的,不用心急,我们现在主要关注主流程即可
2. server.listen
这是cli中的代码,先创建vite 服务器,然后调用listen 去启动服务器
ts
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),
})
if (!server.httpServer) {
throw new Error('HTTP server not available')
}
await server.listen()
await server.listen() 我们来看这个里面做了哪些事情。listen 方法是在server 中定义的,用于启动服务器
ts
async listen(port?: number, isRestart?: boolean) {
await startServer(server, port)
if (httpServer) {
server.resolvedUrls = await resolveServerUrls(
httpServer,
config.server,
config,
)
if (!isRestart && config.server.open) server.openBrowser()
}
return server
},
- 调用 startServer,启动一个服务器
- 调用 resolveServerUrls,解析服务器的本地和网络 URL
- 如果不是重启且配置了open(打开浏览器),则会调用 server 身上的openBrowser 打开浏览器
startServer 这个函数用于启动 Vite 开发服务器,接收一个 ViteDevServer 实例和一个可选的端口号 inlinePort,并在特定条件下启动 HTTP 服务器
ts
async function startServer(
server: ViteDevServer,
inlinePort?: number
): Promise<void> {
const httpServer = server.httpServer;
if (!httpServer) {
//不能在中间件模式下调用 server.listen。
throw new Error("Cannot call server.listen in middleware mode.");
}
//获取服务器配置选项。
const options = server.config.server;
//解析主机名
const hostname = await resolveHostname(options.host);
//确定端口,优先使用 inlinePort,否则使用配置中的端口
const configPort = inlinePort ?? options.port;
/**
* 1. 非严格端口模式:在开发服务器的配置中,可以选择是否启用严格端口模式。
* 非严格端口模式下,开发服务器可以使用操作系统提供的可用端口,而不仅限于配置中指定的端口。
* 2. 端口可能不一致:在重新启动服务器时,如果之前使用的端口仍然可用,开发服务器可能会选择重新使用该端口。
* 这种情况下,服务器当前运行的端口可能会与配置中指定的端口不同。
* 3. 避免浏览器标签页切换:为了避免正在运行的浏览器标签页因为端口变化而刷新或重新加载,
* 开发服务器会尽量保持之前使用的端口不变,除非配置中显式地更改了端口设置。
*
* 这样的设计能够确保开发过程中,开发服务器的端口变化对开发者在浏览器中打开的标签页造成的干扰最小化,
* 提升开发体验的连续性和稳定性
*/
//如果配置的端口为空或者等于服务器配置的端口,使用当前服务器端口
//否则使用 configPort,如果都没有,则使用默认端口 DEFAULT_DEV_PORT
const port =
(!configPort || configPort === server._configServerPort
? server._currentServerPort
: configPort) ?? DEFAULT_DEV_PORT;
// 更新服务器的配置端口
server._configServerPort = configPort;
// 启动 HTTP 服务器
const serverPort = await httpServerStart(httpServer, {
port,
strictPort: options.strictPort,
host: hostname.host,
logger: server.config.logger,
});
// 更新服务器当前端口
server._currentServerPort = serverPort;
}
startServer 里面实际调用了 httpServerStart 这个方法来去启动服务器(listen)
ts
export async function httpServerStart(
httpServer: HttpServer,
serverOptions: {
port: number;
strictPort: boolean | undefined;
host: string | undefined;
logger: Logger;
}
): Promise<number> {
let { port, strictPort, host, logger } = serverOptions;
return new Promise((resolve, reject) => {
const onError = (e: Error & { code?: string }) => {
if (e.code === "EADDRINUSE") {
if (strictPort) {
httpServer.removeListener("error", onError);
reject(new Error(`Port ${port} is already in use`));
} else {
logger.info(`Port ${port} is in use, trying another one...`);
httpServer.listen(++port, host);
}
} else {
httpServer.removeListener("error", onError);
reject(e);
}
};
httpServer.on("error", onError);
httpServer.listen(port, host, () => {
httpServer.removeListener("error", onError);
resolve(port);
});
});
}
这里的重点就是 httpServer.listen ,还记得吗,在创建server 的时候,这里已经把listen 方法重写了,来让我们回顾一下:
- 先保存一份原始的listen 方法
- 重写listen方法,在执行原始的listen 方法之间做一些初始化的事情,例如:
- 启动热更新服
- 调用initServer 这个方法里面最重要的就是预构建依赖,其次是对一些文件预热
下面的这段代码出现在createServer 中
ts
if (!middlewareMode && httpServer) {
// 确保不是中间件模式且存在 httpServer 实例。
// 将原始的 httpServer.listen 方法绑定到 listen 变量上,以便稍后调用。
const listen = httpServer.listen.bind(httpServer);
// 覆盖 httpServer.listen 方法,在实际调用原始 listen 方法之前,先执行一些初始化操作。
httpServer.listen = (async (port: number, ...args: any[]) => {
try {
//确保 WebSocket 服务器启动
hot.listen();
//initServer 确保某些组件或优化器在服务器启动前已经初始化
await initServer();
} catch (e) {
//捕获错误并发出 error 事件
httpServer.emit("error", e);
return;
}
//调用原始的 listen 方法:
return listen(port, ...args);
}) as any;
} else {
//处理中间件模式或没有 httpServer 的清空
if (options.hotListen) {
//options.hotListen 为 true,则启动 WebSocket 服务器
hot.listen();
}
//调用 initServer 进行初始化
await initServer();
}
我们再来看看initServer ,这个函数主要用于初始化服务器。目的是为了确保在服务器启动时,一些关键的初始化步骤只执行一次,即使 httpServer.listen 被多次调用。这是为了避免重复执行 buildStart 以及其他初始化逻辑
ts
const initServer = async () => {
//检查服务器是否已经初始化
if (serverInited) return;
//检查是否有正在进行的初始化过程
if (initingServer) return initingServer;
//开始初始化过程
initingServer = (async function () {
// 调用 buildStart 钩子函数,开始构建过程
await container.buildStart({});
// 在所有容器插件准备好后启动深度优化器
if (isDepsOptimizerEnabled(config, false)) {
//如果启用了依赖优化器,则初始化依赖优化器
/** 这里开始的依赖预构建 */
await initDepsOptimizer(config, server);
}
//调用 warmupFiles 函数,对一些文件进行预热,以提高性能
warmupFiles(server);
//初始化完成后,重置 initingServer 以允许将来的重新初始化
initingServer = undefined;
//设置 serverInited 为 true,表示服务器已经初始化完成
serverInited = true;
})();
return initingServer;
};
这里提一下 buildStart 这里通过插件容器去调用 插件的buildStart钩子函数,一些插件需要在此做一些初始化的事情
这里是pluginContainer 自身的buildStart,会并行执行所有插件的 buildStart 钩子
ts
async buildStart() {
await handleHookPromise(
hookParallel(
"buildStart",
(plugin) => new Context(plugin),
() => [container.options as NormalizedInputOptions]
)
);
},
下面是客户端注入常量的插件,在buildStart 钩子做了一些初始化,在transform 钩子的时候去替换源码中定义的常量,将其转换为真实的常量。源码位置vite/src/node/plugins/clientInjections.ts
ts
export function clientInjectionsPlugin(config: ResolvedConfig): Plugin {
let injectConfigValues: (code: string) => string
return {
name: 'vite:client-inject',
async buildStart() {
const resolvedServerHostname = (await resolveHostname(config.server.host))
.name
const resolvedServerPort = config.server.port!
const devBase = config.base
const serverHost = `${resolvedServerHostname}:${resolvedServerPort}${devBase}`
let hmrConfig = config.server.hmr
hmrConfig = isObject(hmrConfig) ? hmrConfig : undefined
const host = hmrConfig?.host || null
const protocol = hmrConfig?.protocol || null
const timeout = hmrConfig?.timeout || 30000
const overlay = hmrConfig?.overlay !== false
const isHmrServerSpecified = !!hmrConfig?.server
const hmrConfigName = path.basename(config.configFile || 'vite.config.js')
// hmr.clientPort -> hmr.port
// -> (24678 if middleware mode and HMR server is not specified) -> new URL(import.meta.url).port
let port = hmrConfig?.clientPort || hmrConfig?.port || null
if (config.server.middlewareMode && !isHmrServerSpecified) {
port ||= 24678
}
let directTarget = hmrConfig?.host || resolvedServerHostname
directTarget += `:${hmrConfig?.port || resolvedServerPort}`
directTarget += devBase
let hmrBase = devBase
if (hmrConfig?.path) {
hmrBase = path.posix.join(hmrBase, hmrConfig.path)
}
const userDefine: Record<string, any> = {}
for (const key in config.define) {
// import.meta.env.* is handled in `importAnalysis` plugin
if (!key.startsWith('import.meta.env.')) {
userDefine[key] = config.define[key]
}
}
const serializedDefines = serializeDefine(userDefine)
const modeReplacement = escapeReplacement(config.mode)
const baseReplacement = escapeReplacement(devBase)
const definesReplacement = () => serializedDefines
const serverHostReplacement = escapeReplacement(serverHost)
const hmrProtocolReplacement = escapeReplacement(protocol)
const hmrHostnameReplacement = escapeReplacement(host)
const hmrPortReplacement = escapeReplacement(port)
const hmrDirectTargetReplacement = escapeReplacement(directTarget)
const hmrBaseReplacement = escapeReplacement(hmrBase)
const hmrTimeoutReplacement = escapeReplacement(timeout)
const hmrEnableOverlayReplacement = escapeReplacement(overlay)
const hmrConfigNameReplacement = escapeReplacement(hmrConfigName)
injectConfigValues = (code: string) => {
return code
.replace(`__MODE__`, modeReplacement)
.replace(/__BASE__/g, baseReplacement)
.replace(`__DEFINES__`, definesReplacement)
.replace(`__SERVER_HOST__`, serverHostReplacement)
.replace(`__HMR_PROTOCOL__`, hmrProtocolReplacement)
.replace(`__HMR_HOSTNAME__`, hmrHostnameReplacement)
.replace(`__HMR_PORT__`, hmrPortReplacement)
.replace(`__HMR_DIRECT_TARGET__`, hmrDirectTargetReplacement)
.replace(`__HMR_BASE__`, hmrBaseReplacement)
.replace(`__HMR_TIMEOUT__`, hmrTimeoutReplacement)
.replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement)
.replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement)
}
},
async transform(code, id, options) {
if (id === normalizedClientEntry || id === normalizedEnvEntry) {
return injectConfigValues(code)
} else if (!options?.ssr && code.includes('process.env.NODE_ENV')) {
// replace process.env.NODE_ENV instead of defining a global
// for it to avoid shimming a `process` object during dev,
// avoiding inconsistencies between dev and build
const nodeEnv =
config.define?.['process.env.NODE_ENV'] ||
JSON.stringify(process.env.NODE_ENV || config.mode)
return await replaceDefine(
code,
id,
{
'process.env.NODE_ENV': nodeEnv,
'global.process.env.NODE_ENV': nodeEnv,
'globalThis.process.env.NODE_ENV': nodeEnv,
},
config,
)
}
},
}
}
让我们在回到主线流程上面来
ts
async listen(port?: number, isRestart?: boolean) {
await startServer(server, port)
if (httpServer) {
server.resolvedUrls = await resolveServerUrls(
httpServer,
config.server,
config,
)
if (!isRestart && config.server.open) server.openBrowser()
}
return server
},
启动完服务器后,会去调用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 文件
json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": [
"<node_internals>/**"
],
"cwd": "${workspaceFolder}\\packages\\vite",
"program": "${workspaceFolder}\\packages\\vite\\bin\\vite.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
},
{
"type": "node",
"request": "launch",
"name": "vite 调试 vue3",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev"
],
"skipFiles": [
"<node_internals>/**"
],
"cwd": "${workspaceFolder}\\packages\\vue-demo",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}
我这边有两个,是因为初次调试的时候使用的第一个,第二个是结合demo调试使用的,大家可以选择第二个配置项vite 调试 vue3,这个对象里面的配置
我们来到vite的源码的根目录,执行pnpm install 安装所有依赖,然后再来到packages/vite 这个目录,执行pnpm run dev,生成打包后的文件
接下来我们在packages 目录下面创建一个demo,这里使用的vue3
shell
mkdir vue-demo
cd vue-demo
pnpm init
pnpm install vue
pnpm install vite@workspace @vitejs/plugin-vue -D
这里创建目录,初始化,安装vue,安装vite,安装解析vue的插件。这里的安装vite需要注意,必须vite@workspace,因为这样安装的vite就是本项目下面的vite,这里也相当于是软连接。然后再package.json 是编写启动脚本 "dev": "vite"
index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script type="module" src="./src/main.ts"></script>
</html>
vite.config.ts
ts
import vue from '@vitejs/plugin-vue'
export default {
server: {
open: true,
},
plugins: [vue()],
}
src/App.vue
vue
<template>
<div>App.vue</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped></style>
src/main.ts
ts
import { createApp } from 'vue'
import root from './App.vue'
const app = createApp(root)
app.mount('#app')
以上的配置完成后,我们就可以开始debug调试vite源码啦 我们来到packages/vite/src/node/cli.ts 文件中,打上debug标记
然后打开debug 选项卡,点击运行,然后就会发现代码卡在了我们标记的地方,同时也出现了一些调试的按钮,现在你就可以像浏览器debug一样随心所欲的开始调试vite源码了
结束
在我的github项目中,对主流程的代码都有注释解释,目前进度是正常打开浏览器后能正常显示vue编写的代码,热更新、样式文件的处理等后续会慢慢更新,想要提前看的可以去代码里面看