Appearance
vite 命令源码摘要
调试步骤:
- 1、
vite源码在vite项目中路径为:vite/packages/vite。 - 2、在
package.json中查看vite命令路径为:bin/vite.js。 - 3、
bin/vite.js中,从start()函数开始。实际是引入了node/cli.js。
ts
function start() {
return import("../dist/node/cli.js");
}- 4、对应找到
src/node/cli.ts,在合适位置打上断点,就可以调试了。
bash
# 调试命令
tsx cli.ts --open --port 9000vite [dev/serve]
ts
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),
});
// 监听服务
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`
)
: "";
info(
`\n ${colors.green(
`${colors.bold("VITE")} v${VERSION}`
)} ${startupDurationString}\n`,
{ clear: !server.config.logger.hasWarned }
);
// 打印信息
server.printUrls();
}createServer
ts
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// 解析、获取全部配置
const config = await resolveConfig(inlineConfig, "serve");
// 获取根目录、服务器配置
const { root, server: serverConfig } = config;
// 解析文件watch配置
const resolvedWatchOptions = resolveChokidarOptions(config, {
disableGlobbing: true,
...serverConfig.watch,
});
// 声明中间件
const middlewares = connect() as Connect.Server;
// 创建http服务对象
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
// 创建文件监听器,用于监听文件变动更新
const watcher = chokidar.watch(
path.resolve(root),
resolvedWatchOptions
) as FSWatcher;
// 初始化模块图谱,以存储模块信息及引用关系
const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
container.resolveId(url, undefined, { ssr })
);
// 创建插件容器,返回Vite封装的特色钩子体系
const container = await createPluginContainer(config, moduleGraph, watcher);
// 服务实例真身
const server: ViteDevServer = {
config,
middlewares,
httpServer,
watcher,
pluginContainer: container,
moduleGraph,
// 监听服务
async listen(port?: number, isRestart?: boolean) {
// 开启服务,比如自动打开浏览器
await startServer(server, port, isRestart);
if (httpServer) {
// 解析组装url链接 如http://localhost:3062/
server.resolvedUrls = await resolveServerUrls(
httpServer,
config.server,
config
);
}
return server;
},
// 打印信息
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."
);
}
},
};
// 收集transformIndexHtml钩子任务
server.transformIndexHtml = createDevHtmlTransformFn(server);
// 执行configureServer钩子
const postHooks: ((() => void) | void)[] = [];
for (const hook of config.getSortedPluginHooks("configureServer")) {
postHooks.push(await hook(server));
}
// 执行一系列中间件
// 执行中间件:是否支持跨域,默认支持
const { cors } = serverConfig;
if (cors !== false) {
middlewares.use(corsMiddleware(typeof cors === "boolean" ? {} : cors));
}
// 执行中间件:代理设置
const { proxy } = serverConfig;
if (proxy) {
middlewares.use(proxyMiddleware(httpServer, proxy, config));
}
// 执行configureServer 后置钩子
postHooks.forEach((fn) => fn && fn());
// 初始化服务
let initingServer: Promise<void> | undefined;
let serverInited = false;
const initServer = async () => {
if (serverInited) {
return;
}
if (initingServer) {
return initingServer;
}
initingServer = (async function () {
// 执行buildStart钩子
await container.buildStart({});
if (isDepsOptimizerEnabled(config, false)) {
// non-ssr
await initDepsOptimizer(config, server);
}
initingServer = undefined;
serverInited = true;
})();
return initingServer;
};
if (!middlewareMode && httpServer) {
// overwrite listen to init optimizer before server start
const listen = httpServer.listen.bind(httpServer);
httpServer.listen = (async (port: number, ...args: any[]) => {
try {
await initServer();
} catch (e) {
httpServer.emit("error", e);
return;
}
return listen(port, ...args);
}) as any;
} else {
await initServer();
}
return server;
}流程辅助函数
执行 config 系钩子(resolveConfig)
解析配置。执行 config,configResolved 钩子。
ts
// 解析配置
export async function resolveConfig(...){
if (configFile !== false) {
// 加载配置文件
const loadResult = await loadConfigFromFile(...)
if (loadResult) {
// 合并配置
config = mergeConfig(loadResult.config, config)
configFile = loadResult.path
configFileDependencies = loadResult.dependencies
}
}
// 插件分类、排序
const [prePlugins, normalPlugins, postPlugins] =
sortUserPlugins(rawUserPlugins)
// 执行config钩子
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
config = await runConfigHook(config, userPlugins, configEnv)
// 解析服务配置
const server = resolveServerOptions(resolvedRoot, config.server, logger)
// 执行worker config钩子
workerConfig = await runConfigHook(workerConfig, workerUserPlugins, configEnv)
// 定义返回配置
const resolved: ResolvedConfig = {
...config,
...resolvedConfig,
}
// 执行configResolved钩子
await Promise.all([
...resolved
.getSortedPluginHooks('configResolved')
.map((hook) => hook(resolved)),
...resolvedConfig.worker
.getSortedPluginHooks('configResolved')
.map((hook) => hook(workerResolved)),
])
// 返回处理完结的配置
return resolved
}解析设置 http 服务(resolveHttpServer)
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);
}
// #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
) as unknown as HttpServer;
}
}创建 Vite 钩子,执行 options 钩子
实现 rollup 插件上下文接口,执行 options 钩子,返回 Vite 钩子。
ts
export async function createPluginContainer(
config: ResolvedConfig,
moduleGraph?: ModuleGraph,
watcher?: FSWatcher
): Promise<PluginContainer> {
// 引入rollup插件上下文
import type { MinimalPluginContext, PluginContext as RollupPluginContext } from "rollup";
const minimalContext: MinimalPluginContext = {
meta: {
rollupVersion,
watchMode: true,
},
};
function getModuleInfo(id: string) {
const module = moduleGraph?.getModuleById(id);
if (!module) {
return null;
}
if (!module.info) {
module.info = new Proxy(
{ id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo,
ModuleInfoProxy
);
}
return module.info;
}
// 通过Context实现Rollupjs的PluginContext接口,为异步钩子创建上下文。
// 官方注释如下:
// we should create a new context for each async hook pipeline so that the
// active plugin in that pipeline can be tracked in a concurrency-safe manner.
// using a class to make creating new contexts more efficient
class Context implements PluginContext {...}
// 定义返回值
const container: PluginContainer = {
// options是异步立即执行函数
options: await (async () => {
let options = rollupOptions;
for (const optionsHook of getSortedPluginHooks("options")) {
// 执行options钩子
options = (await optionsHook.call(minimalContext, options)) || options;
}
if (options.acornInjectPlugins) {
parser = acorn.Parser.extend(
...(arraify(options.acornInjectPlugins) as any)
);
}
return {
acorn,
acornInjectPlugins: [],
...options,
};
})(),
// 获取模块信息功能函数
getModuleInfo,
async buildStart() {
// 执行buildStart钩子
await hookParallel(
"buildStart",
(plugin) => new Context(plugin),
() => [container.options as NormalizedInputOptions]
);
},
async resolveId(rawId, importer = join(root, 'index.html'), options) {...},
async load(id, options) {...},
async transform(code, id, options) {...},
async close() {...}
};
return container;
}开启 http 服务,执行 buildsStart 钩子
ts
async function startServer(
server: ViteDevServer,
inlinePort?: number,
isRestart: boolean = false
): Promise<void> {
const httpServer = server.httpServer;
if (!httpServer) {
throw new Error("Cannot call server.listen in middleware mode.");
}
const options = server.config.server;
// 端口
const port = inlinePort ?? options.port ?? DEFAULT_DEV_PORT;
// host设置
const hostname = await resolveHostname(options.host);
// 协议
const protocol = options.https ? "https" : "http";
const serverPort = await httpServerStart(httpServer, {
port,
strictPort: options.strictPort,
host: hostname.host,
logger: server.config.logger,
});
// 是否自动打开浏览器
if (options.open && !isRestart) {
const path =
typeof options.open === "string" ? options.open : server.config.base;
openBrowser(
path.startsWith("http")
? path
: new URL(path, `${protocol}://${hostname.name}:${serverPort}`).href,
true,
server.config.logger
);
}
}
// 初始化启动http服务,执行buildsStart钩子
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);
});
});
}其他函数摘要
ts
// 从配置文件中加载配置
export async function loadConfigFromFile(...){
if (configFile) {
// 如果入参指定了配置文件,就取路径
resolvedPath = path.resolve(configFile)
} else {
// 否则,就地查询是否有 vite.config.js/mjs/ts/cjs/mts/cts 后缀文件
for (const filename of DEFAULT_CONFIG_FILES) {
const filePath = path.resolve(configRoot, filename)
if (!fs.existsSync(filePath)) continue
resolvedPath = filePath
break
}
}
// 找不到配置文件就提示报错信息
if (!resolvedPath) {
debug('no config file found.')
return null
}
// 读取配置信息
const userConfig = await loadConfigFromBundledFile(
resolvedPath,
bundled.code,
isESM,
)
// 返回配置
return {
path: normalizePath(resolvedPath),
config,
dependencies: bundled.dependencies,
}
}
// 执行config钩子
async function runConfigHook(...){
let conf = config;
for (const p of getSortedPluginsByHook("config", plugins)) {
const hook = p.config;
const handler = hook && "handler" in hook ? hook.handler : hook;
if (handler) {
const res = await handler(conf, configEnv);
// 如果有返回值,就将其合并到配置中
if (res) {
conf = mergeConfig(conf, res);
}
}
}
return conf;
}vite build
调用 Rollup.js 打包,详情参见 Rollup.js 源码摘要。
ts
const { build } = await import("./build");
try {
await build({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
optimizeDeps: { force: options.force },
build: buildOptions,
});
} catch (e) {
createLogger(options.logLevel).error(
colors.red(`error during build:\n${e.stack}`),
{ error: e }
);
process.exit(1);
} finally {
stopProfiler((message) => createLogger(options.logLevel).info(message));
}
// Bundles the app for production.
// Returns a Promise containing the build result.
export async function build(
inlineConfig: InlineConfig = {}
): Promise<RollupOutput | RollupOutput[] | RollupWatcher> {
// 解析、获取配置
const config = await resolveConfig(
inlineConfig,
"build",
"production",
"production"
);
// 调用rollup打包核心函数
let bundle: RollupBuild | undefined
const { rollup } = await import("rollup");
bundle = await rollup(rollupOptions);
// 返回打包产物
const res = [];
for (const output of normalizedOutputs) {
res.push(await bundle[options.write ? "write" : "generate"](output));
}
return Array.isArray(outputs) ? res : res[0];
} catch (e) {
outputBuildError(e)
throw e
} finally {
if (bundle) await bundle.close()
}hmr:热更新/热替换
hmr: hot module refresh/hot module replacement。
- 用chokidar 库,观察文件,执行回调函数返回文件变动结果。
- 用ws 库创建
websocket连接,服务端以websocket方式与客户端保持即时通信,将变动结果消息发送给客户端。 客户端根据消息类型,执行相应更新级别、动作。
观察文件,获取变动结果
ts
// 创建观察实例
const watcher = chokidar.watch(
[root, ...config.configFileDependencies, config.envDir],
resolvedWatchOptions
) as FSWatcher;
// 监听观察事件
watcher.on("change", async (file) => {
file = normalizePath(file);
moduleGraph.onFileChange(file);
await onHMRUpdate(file, false);
});
const onHMRUpdate = async (file: string, configOnly: boolean) => {
if (serverConfig.hmr !== false) {
try {
await handleHMRUpdate(file, server, configOnly) {
// 如果是配置相关更新,则重启服务器
if(isConfig || isConfigDependency || isEnv) {
try {
await server.restart()
}
}
// 如果是客户端变动,就重载页面
if (file.startsWith(normalizedClientDir)) {
ws.send({
type: 'full-reload',
path: '*',
})
}
// 执行 vite 特色钩子 handleHotUpdate
for (const hook of config.getSortedPluginHooks('handleHotUpdate')) {
...
}
...
updateModules(shortFile, hmrContext.modules, timestamp, server) {
...
if (needFullReload) {
ws.send({ type: 'full-reload' })
return
}
...
ws.send({ type: 'update', updates })
}
};
} catch (err) {
ws.send({
type: "error",
err: prepareError(err),
});
}
}
};
watcher.on("add", onFileAddUnlink);
watcher.on("unlink", onFileAddUnlink);创建 websocket node 服务端
ts
// vite/src/node/server/index.ts
const ws = createWebSocketServer(httpServer, config, httpsOptions) {
if (wsServer) {
// ws库创建websocket node服务端
wss = new WebSocketServerRaw({ noServer: true })
wsServer.on('upgrade', (req, socket, head) => {...})
}
// 服务端监听事件
wss.on('connection', (socket) => {
socket.on('message', (raw) => {...})
socket.on('error', (err) => {})
socket.send(JSON.stringify({ type: 'connected' }))
})
return {
listen: () => {
wsHttpServer?.listen(port, host)
},
on: ((event: string, fn: () => void) => {...})
get clients() {
return new Set(Array.from(wss.clients).map(getSocketClient))
},
send(...args: any[]) {}
close() {}
}
}创建 websocket 客户端
ts
// vite/src/client/client.ts
socket = setupWebSocket(socketProtocol, socketHost, fallback) {
// 创建实例
const socket = new WebSocket(`${protocol}://${hostAndPath}`, 'vite-hmr')
// 监听事件
socket.addEventListener('open', () => {}, { once: true })
socket.addEventListener('message', async ({ data }) => {
// 根据消息类型,分别处理
handleMessage(JSON.parse(data)) {
// data -> payload
switch (payload.type) {
case 'connected': ...,
case 'update': ...,
case 'custom': ...,
case 'full-reload': ...,
case 'prune': ...,
case 'error': ...,
default: ...
}
}
})
socket.addEventListener('close', async ({ wasClean }) => {...})
return socket
}