「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
简介
vite一共有dev | build | optimize | preview
四种命令,文章对vite整个流程做了梳理,对于dev | build
的主要步骤做了简答的拆分,便于大家对vite源码形成一个整体的认识,有不对的地地方欢迎大家指正。
启动
vite项目默认的两条命令npm run serve
npm run build
都是启动了vite命令,打开/node_modules/.bin
目录,找到vite
文件,发现其最主要的流程就是执行start
函数,加载node/cli文件
// 此处源码如下
...
function start() {
require('../dist/node/cli')
}
...
start()
...
然后到到vite源码的cli文件vite/src/node/cli.ts
vite脚手架/src/node/cli.ts
可以看出vite脚手架一共包括四个命令,dev | build | optimize | preview
,本文主要是梳理dev | build
dev 开发环境
build 构建
preview vite预览
optimize 优化
vite dev
cli.ts
cli.ts
中关于dev命令引用了./server
的createServer
,并触发listen()
进行监听
/node/server/index.ts
createServer
就是要创建并返回一个server,具体的,做了以下几件事:
- 整合配置文件vite.config.js 和 命令行里的配置到config中
const config = await resolveConfig(inlineConfig, "serve", "development");
- 启动一个http(s)server,并升级为websocket(当然前一步要先把httpserver的相关配置参数处理)
const httpsOptions = await resolveHttpsConfig(config);
const middlewares = connect() as Connect.Server;
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
const ws = createWebSocketServer(httpServer, config, httpsOptions);
- 使用 chokidar监听文件变化(这是进行热更新的基础)
const watcher = chokidar.watch(path.resolve(root), {
ignored: ["**/node_modules/**", "**/.git/**", ...ignored],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
...watchOptions,
}) as FSWatcher;
- 将所有的plugin统一进行处理,保存到container中
const container = await createPluginContainer(config, watcher);
- 根据container生成moduleGraph(这里还没有细读,vite中的解释是moduleGraph用于记录import的关系、url到file的映射及热更新相关
- and hmr state`)
const moduleGraph = new ModuleGraph(container);
// 下面是moduleGraph的ts定义
/**
* Module graph that tracks the import relationships, url to file mapping
* and hmr state.
*/
- 初始化后面要返回的vite-dev-server,绑定了一些属性和方法
const server: ViteDevServer = {
...
}
- watcher发生变化的时候,进行相应的热更新处理
watcher.on('change', fn)
watcher.on('add', fn)
watcher.on('unlink', fn)
- 执行vite 钩子
configureServer
,这里postHooks只收集有configureServer的plugin
const postHooks: ((() => void) | void)[] = [];
for (const plugin of plugins) {
if (plugin.configureServer) {
// WK 执行配置了configureServer的plugin
postHooks.push(await plugin.configureServer(server));
}
}
- 内部中间件的使用
...
middlewares.use(corsMiddleware(typeof cors === "boolean" ? {} : cors));
middlewares.use(proxyMiddleware(httpServer, config));
...
- 执行posHooks里的plugins
postHooks.forEach((fn) => fn && fn());
- 转换index.html
middlewares.use(indexHtmlMiddleware(server));
- 在listen()之前
- 执行vite钩子 buildStart
- 执行runOptimize(),进行启动前的优化
if (!middlewareMode && httpServer) {
// overwrite listen to run optimizer before server start
const listen = httpServer.listen.bind(httpServer);
httpServer.listen = (async (port: number, ...args: any[]) => {
try {
await container.buildStart({});
await runOptimize();
} catch (e) {
httpServer.emit("error", e);
return;
}
return listen(port, ...args);
}) as any;
httpServer.once("listening", () => {
// update actual port since this may be different from initial value
serverConfig.port = (httpServer.address() as AddressInfo).port;
});
} else {
await container.buildStart({});
await runOptimize();
}
- 返回 server
vite build
cli.ts
build
命令就是引入了./build
文件,并执行build()
rollup 打包
vite使用rollup进行打包的,在阅读相关方法之前,有必要对rollup进行一些基本了解。以下是rollup官网对其打包过程的代码描述。rollup javascript API
可以看出rollup一般的打包需要
- 打包的配置参数
inputOptions
- 打包生成文件的配置参数
outputOptions
- 调用
rollup.rollup()
返回一个bundle对象 - 调用
bundle.generate()
或者bundle.write()
完成打包 那么vite的build也应该是按照这个流程来的
build.ts
build.ts
中build
方法主要是指行了dobuild
方法,dobuild
做了以下几件事(ssr相关的先不考虑):
- 整理配置参数=> config
const config = await resolveConfig(inlineConfig, "build", "production");
- rollup 打包输入参数=> RollupOptions,在此之前处理了一下对于RollupOptions对象比较重要的input参数和external
const RollupOptions: RollupOptions = {
input,
preserveEntrySignatures: ssr
? "allow-extension"
: libOptions
? "strict"
: false,
...options.rollupOptions,
plugins,
external,
onwarn(warning, warn) {
onRollupWarning(warning, warn, config);
},
};
- rollup打包输出参数 outputs(一般我们在项目开发中outputs就是个obj,但在构建库时可能需要生成不同格式的包,所以outputs也可能是个数组)
const outputs = resolveBuildOutputs(
options.rollupOptions?.output,
libOptions,
config.logger
);
- rollup还提供了一个watch功能,vite这里也进行相应的实现直达rollup-watch
if (config.build.watch) {
config.logger.info(chalk.cyanBright(`\nwatching for file changes...`));
const output: OutputOptions[] = [];
if (Array.isArray(outputs)) {
for (const resolvedOutput of outputs) {
output.push(buildOuputOptions(resolvedOutput));
}
} else {
output.push(buildOuputOptions(outputs));
}
const watcherOptions = config.build.watch;
const watcher = rollup.watch({
...rollupOptions,
output,
watch: {
...watcherOptions,
chokidar: {
ignored: [
"**/node_modules/**",
"**/.git/**",
...(watcherOptions?.chokidar?.ignored || []),
],
ignoreInitial: true,
ignorePermissionErrors: true,
...watcherOptions.chokidar,
},
},
});
watcher.on("event", (event) => {
if (event.code === "BUNDLE_START") {
config.logger.info(chalk.cyanBright(`\nbuild started...`));
if (options.write) {
prepareOutDir(outDir, options.emptyOutDir, config);
}
} else if (event.code === "BUNDLE_END") {
event.result.close();
config.logger.info(chalk.cyanBright(`built in ${event.duration}ms.`));
} else if (event.code === "ERROR") {
outputBuildError(event.error);
}
});
// stop watching
watcher.close();
return watcher;
}
- 生成bundle对象
const bundle = await rollup.rollup(rollupOptions);
- 调用bundle.write方法,写到文件中,大功告成。在这之前还调用
prepareOutDir
方法(确认了打包目录是否存在及清理了该目录)
if (options.write) {
prepareOutDir(outDir, options.emptyOutDir, config);
}
if (Array.isArray(outputs)) {
const res = [];
for (const output of outputs) {
res.push(await generate(output));
}
return res;
} else {
return await generate(outputs);
}
后续计划
精读vite,看一些细节部分的实现,如对import { createApp } from 'vue'
的处理
其他
本次阅读相关的注释在vite源码阅读,可以安装vscode插件todo tree, 前缀'WK'的都是我看的过程中加的注释