Vite
Vite 是已成为现代前端开发最重要的基础设施之一
vite dev 发生了什么
vite 的 CLI 工具使用 cacjs/cac 作为框架,如果对此工具不了解,可以先去它的仓库看看
在 cli.ts 中,定义了一个根命令,就是不带二级参数,即 vite 命令,以及别名 vite serve 和 vite dev,所以三者是完全等价的,都代表启动一个开发服务
// 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: cleanGlobalCLIOptions(options),
})
if (!server.httpServer) {
throw new Error('HTTP server not available')
}
await server.listen()
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)
}
})上面有两个关键步骤,通过 createServer 创建一个服务,以及调用 server.listen() 监听 5173 端口。
node 方面的初始化工作就完成了,具体里面的细节我们下面再说
浏览器访问
我们打开浏览器,访问 http://localhost:5173,假设我们是使用 create-vite 创建的 vue 的模版项目,我们看到的应该是下面的内容:

具体发生了什么呢?
我们逐步分享浏览器的请求,以及 vite 服务器的响应,首先,浏览器向服务器请求根路径 http://localhost:5173
在 vite 服务这边,NodeJs 服务的创建是直接调用的原生的 node:http 模块的 createServer 方法,中间件框架使用了 senchalabs/connect
export async function resolveHttpServer(
{ proxy }: CommonServerOptions,
app: Connect.Server,
httpsOptions?: HttpsServerOptions,
): Promise<HttpServer> {
if (!httpsOptions) {
const { createServer } = await import('node:http')
return createServer(app)
}
// #484 fallback to http1 when proxy is needed.
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,
)
}
}在中间件中,对于根请求的处理在:
if (config.appType === 'spa' || config.appType === 'mpa') {
// transform index.html
middlewares.use(indexHtmlMiddleware(root, server))
// handle 404s
middlewares.use(notFoundMiddleware())
}我们默认的就是一个 spa 程序,对 index.html 的处理,在 indexHtmlMiddleware 中间件中:
export function indexHtmlMiddleware(
root: string,
server: ViteDevServer | PreviewServer,
): Connect.NextHandleFunction {
const isDev = isDevServer(server)
const fsUtils = getFsUtils(server.config)
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return async function viteIndexHtmlMiddleware(req, res, next) {
if (res.writableEnded) {
return next()
}
const url = req.url && cleanUrl(req.url)
// htmlFallbackMiddleware appends '.html' to URLs
if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') {
let filePath: string
if (isDev && url.startsWith(FS_PREFIX)) {
filePath = decodeURIComponent(fsPathFromId(url))
} else {
filePath = path.join(root, decodeURIComponent(url))
}
if (fsUtils.existsSync(filePath)) {
const headers = isDev
? server.config.server.headers
: server.config.preview.headers
try {
let html = await fsp.readFile(filePath, 'utf-8')
if (isDev) {
html = await server.transformIndexHtml(url, html, req.originalUrl)
}
return send(req, res, html, 'html', { headers })
} catch (e) {
return next(e)
}
}
}
next()
}
}主要的工作就是读取你项目中的那个 index.html 文件的原始内容,然后调用 server.transformIndexHtml 方法处理,再返回处理后的内容,这个方法内部调用了 createDevHtmlTransformFn 方法,它的代码如下:
export function createDevHtmlTransformFn(
config: ResolvedConfig,
): (
server: ViteDevServer,
url: string,
html: string,
originalUrl?: string,
) => Promise<string> {
const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms(
config.plugins,
config.logger,
)
const transformHooks = [
preImportMapHook(config),
injectCspNonceMetaTagHook(config),
...preHooks,
htmlEnvHook(config),
devHtmlHook,
...normalHooks,
...postHooks,
injectNonceAttributeTagHook(config),
postImportMapHook(),
]
return (
server: ViteDevServer,
url: string,
html: string,
originalUrl?: string,
): Promise<string> => {
return applyHtmlTransforms(html, transformHooks, {
path: url,
filename: getHtmlFilename(url, server),
server,
originalUrl,
})
}
}上面的 resolveHtmlTransforms 作用就是从 vite.config.ts 中读取所有的 plugins 插件,拿到里面的针对 HTML 转换的钩子 resolveHtmlTransforms,然后对插件的调用时机排序,得到一个 transformHooks 数组,然后依次链式调用,最终得到转换后的 html 文件,返回浏览器,即:

由于默认的模版并没有插件进行转换,所以我们看到了内容和原始本地的 index.html 文件没什么区别
后续请求的处理
根据返回的 html 内容可以看到,浏览器首先会请求 /@vite/client 这个 script 标签的 src,这个是 HMR 相关的 js,我们先忽略
然后重中之重就是请求了 src/main.ts 文件,这个是我们的主要内容的入口,主要内容的处理交给 transformMiddleware 中间件:
// main transform middleware
middlewares.use(transformMiddleware(server))他的内容,如果是 JS/CSS/Import 请求,都会走到这里来处理:
export function transformMiddleware(
server: ViteDevServer,
): Connect.NextHandleFunction {
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
// check if public dir is inside root dir
const { root, publicDir } = server.config
const publicDirInRoot = publicDir.startsWith(withTrailingSlash(root))
const publicPath = `${publicDir.slice(root.length)}/`
return async function viteTransformMiddleware(req, res, next) {
if (
isJSRequest(url) ||
isImportRequest(url) ||
isCSSRequest(url) ||
isHTMLProxy(url)
) {
const result = await transformRequest(environment, url, {
html: req.headers.accept?.includes('text/html'),
})
if (result) {
const depsOptimizer = environment.depsOptimizer
const type = isDirectCSSRequest(url) ? 'css' : 'js'
const isDep =
DEP_VERSION_RE.test(url) || depsOptimizer?.isOptimizedDepUrl(url)
return send(req, res, result.code, type, {
etag: result.etag,
// allow browser to cache npm deps!
cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache',
headers: server.config.server.headers,
map: result.map,
})
}
}
next()
}transformRequest 方法是 vite 转换的重点和难点,核心的流程都在这个步骤里面