前言
经过前边两个小节的准备工作,本小节,就可以正式踏入vite
源码的学习工作中了。本节的任务是创建一个http
服务器,这是vite
在dev
阶段的起点
源码获取
更新进度
公众号:更新至第7
节
博客:更新至第3
节
源码分析
找到packages\vite\bin\vite.js
,这是在package.json
的scripts
中使用vite
调起的文件,对应的package.json
文件中的配置如下
"bin": {
"vite": "bin/vite.js"
}
它核心只调用了一个start
函数,该函数的body
对应的是对cli.ts
打包后的文件的调用
function start() {
return import("../dist/node/cli.js");
}
进入packages\vite\src\node\cli.ts
文件,它通过cac快速创建cli
const cli = cac("vite");
根据该库的文档说明,它通过command
来定义指令,使用option
来收集参数,借助action
来客制化操作,以vite dev
为例
它定义了别名为dev
的指令,接收port
作为参数,当命中指令时回调action
cli
.command('[root]', 'start dev server') // default command
.alias('dev')
.option('--port <port>', `[number] specify port`)
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
const { createServer } = await import('./server')
try{
const server = await createServer(/*收集的用户参数*/)
await server.listen()
}catch(){
process.exit(1)
}
})
关键的逻辑点在action
里,故进入packages\vite\src\node\server\index.ts
,并定位到 _createServer
函数
export async function _createServer(
inlineConfig: InlineConfig = {},
options: { ws: boolean },
): Promise<ViteDevServer>{
// 获取配置
const config = await resolveConfig(inlineConfig, 'serve')
...
// 创建http
const httpServer = await resolveHttpServer(config.serverConfig,connect())
...
// 启动http
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number, ...args: any[]) => {
return listen(port, ...args)
}) as any
...
return httpServer
}
在上边的代码中,resolveConfig
是一次配置合并和normalize
的过程,没什么特别复杂的点,这里就不展开说了;resolveHttpServer
才是我们本节重点关注的内容,可以看到,它就是调用node
原生的http
模块实现的开发服务器的创建
export async function resolveHttpServer(
{ proxy }: CommonServerOptions,
app: Connect.Server,
httpsOptions?: HttpsServerOptions,
): Promise<HttpServer> {
if (!httpsOptions) {
const { createServer } = await import('node:http')
return createServer(app)
}
...
}
最后,vite
对http
的listen
方法进行了重写,这是因为它要确保同时开启ws
服务,并且也要在服务启动阶段自定义一些处理逻辑,比如比较常用的buildStart
钩子函数,以及大名鼎鼎的预构建处理等。不过在此处与服务器启动相关的只有startServer
函数
const server: ViteDevServer = {
...,
async listen(port?: number, isRestart?: boolean) {
await startServer(server, port)
...
}
}
找到startServer
函数,如下,它只是做了下参数的处理和校验,真正的开启处是httpServerStart
async function startServer(
server: ViteDevServer,
inlinePort?: number
): Promise<void> {
const httpServer = server.httpServer;
...
const options = server.config.server;
const port = inlinePort ?? options.port ?? DEFAULT_DEV_PORT;
const hostname = await resolveHostname(options.host);
await httpServerStart(httpServer, {
port,
strictPort: options.strictPort,
host: hostname.host,
logger: server.config.logger,
});
}
找到httpServerStart
,它在packages\vite\src\node\http.ts
,可以看到,它本质上调用的是原生的listen
方法来开启服务器的
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 }) => {
...
}
httpServer.on('error', onError)
httpServer.listen(port, host, () => {
httpServer.removeListener('error', onError)
resolve(port)
})
})
}
代码实现
在搭建工程的时候已经设置好了bin
字段,并且它的名称为svite
,后续我们将使用svite
来进行调用
"bin": {
"svite": "bin/vite.js"
}
packages\vite\bin\vite.js
文件中的代码很简单,就是导入并执行cli.js
文件
import("../dist/node/cli.js");
cli.js
现在还没有,故到packages\vite\src\node
中创建一个,并且从上述对vite
源码的分析,可以知道,它只做了两件事:配置合并、创建http
服务器
首先,安装下cac
这个包
pnpm i cac -D
参数部分我们目前先不处理
import { cac } from "cac";
import { UserConfig } from "./index";
const cli = cac("mini-vite");
cli
.command("[root]", "start dev server")
.alias("server")
.option("--port <port>", "[number] specify port")
.action(loadAndCreateHttp);
现在来实现loadAndCreateHttp
函数,为了后续可扩展,需要对参数进行下normalize
,比如当前阶段是为了启动开发服务器,所以我们用一个server
字段来集中管理
function normalizeConfig(option: any, root: string) {
const config = {
server: {},
root,
};
if (option.port) {
config.server.port = option.port;
}
return config satisfies UserConfig;
}
为了后续方便扩展功能和管理,我们在packages\vite\src\node
下新建server\index.ts
文件夹,并在cli.ts
中导入createServer
函数,同时将处理后的config
传入
async function loadAndCreateHttp(root: string, option: any) {
const nomalizedOption = normalizeConfig(option, root);
const { createServer } = await import("./server");
try {
await createServer(nomalizedOption);
} catch (_) {
process.exit(1);
}
}
现在进入server\index.ts
,进行http
的创建,它应该导出一个createServer
函数
export function createServer(config: UserConfig): Promise<ViteDevServer> {
return _createServer(config);
}
在_createServer
中,首先要对用户配置和vite
内置配置进行下处理,由于后续还会有plugin
、.env
等一系列环境变量,所以也将配置相关的内容单独划分模块,故在packages\vite\src\node
文件夹下新建config.ts
文件,并导出一个resolveConfig
用于合并配置,只是目前来说内置配置还没有
import type { UserConfig } from "./index";
export async function resolveConfig(userConf: UserConfig) {
const internalConf = {};
return {
...userConf,
...internalConf,
};
}
接着在packages\vite\src\node
下新建http.ts
文件,并导出一个createHttpServer
的函数用于创建http
服务器,目前它不支持https
,也不支持处理proxy
export async function createHttpServer(app: Connect.Server) {
const { createServer } = await import("node:http");
return createServer(app);
}
再然后只需要将创建好的http
服务器返回,并在适当的时机由外部对erver.listen
进行调用就好啦
async function _createServer(userConfig: UserConfig) {
const config = await resolveConfig(userConfig);
const middlewares = connect() as Connect.Server;
const httpServer = await createHttpServer(config.server, middlewares);
const server: ViteDevServer = {
config,
httpServer,
listen: httpServer.listen as any,
};
return server;
}
最后,还需要将cli
作为打包入口在rollup.config.ts
中进行下配置就可以了
const config = defineConfig({
...,
input: {
...,
cli:path.resolve(__dirname, 'src/node/cli.ts'),
},
...
});
调试
启动playground/dev
下的示例,没有报错即说明http
创建成功
总结
通过cac
这个包帮助svite
快速创建了cli
应用与用户完成交互
而开发服务器的创建靠的是对原生的http
模块的调用