《vite技术揭秘、还原与实战》第3节--创建本地开发服务器

前言

经过前边两个小节的准备工作,本小节,就可以正式踏入vite源码的学习工作中了。本节的任务是创建一个http服务器,这是vitedev阶段的起点

源码获取

传送门

更新进度

公众号:更新至第7

博客:更新至第3

源码分析

找到packages\vite\bin\vite.js,这是在package.jsonscripts中使用vite调起的文件,对应的package.json文件中的配置如下

json 复制代码
"bin": {
    "vite": "bin/vite.js"
}

它核心只调用了一个start函数,该函数的body对应的是对cli.ts打包后的文件的调用

js 复制代码
function start() {
  return import("../dist/node/cli.js");
}

进入packages\vite\src\node\cli.ts文件,它通过cac快速创建cli

js 复制代码
const cli = cac("vite");

根据该库的文档说明,它通过command来定义指令,使用option来收集参数,借助action来客制化操作,以vite dev为例

它定义了别名为dev的指令,接收port作为参数,当命中指令时回调action

ts 复制代码
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函数

ts 复制代码
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模块实现的开发服务器的创建

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)
  }
  ...
}

最后,vitehttplisten方法进行了重写,这是因为它要确保同时开启ws服务,并且也要在服务启动阶段自定义一些处理逻辑,比如比较常用的buildStart钩子函数,以及大名鼎鼎的预构建处理等。不过在此处与服务器启动相关的只有startServer函数

ts 复制代码
const server: ViteDevServer = {
  ...,
  async listen(port?: number, isRestart?: boolean) {
    await startServer(server, port)
    ...
  }
}

找到startServer函数,如下,它只是做了下参数的处理和校验,真正的开启处是httpServerStart

ts 复制代码
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方法来开启服务器的

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 }) => {
      ...
    }

    httpServer.on('error', onError)

    httpServer.listen(port, host, () => {
      httpServer.removeListener('error', onError)
      resolve(port)
    })
  })
}

代码实现

在搭建工程的时候已经设置好了bin字段,并且它的名称为svite,后续我们将使用svite来进行调用

json 复制代码
"bin": {
    "svite": "bin/vite.js"
}

packages\vite\bin\vite.js文件中的代码很简单,就是导入并执行cli.js文件

js 复制代码
import("../dist/node/cli.js");

cli.js现在还没有,故到packages\vite\src\node中创建一个,并且从上述对vite源码的分析,可以知道,它只做了两件事:配置合并、创建http服务器

首先,安装下cac这个包

js 复制代码
pnpm i cac -D

参数部分我们目前先不处理

ts 复制代码
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字段来集中管理

ts 复制代码
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传入

ts 复制代码
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函数

ts 复制代码
export function createServer(config: UserConfig): Promise<ViteDevServer> {
  return _createServer(config);
}

_createServer中,首先要对用户配置和vite内置配置进行下处理,由于后续还会有plugin.env等一系列环境变量,所以也将配置相关的内容单独划分模块,故在packages\vite\src\node文件夹下新建config.ts文件,并导出一个resolveConfig用于合并配置,只是目前来说内置配置还没有

ts 复制代码
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

ts 复制代码
export async function createHttpServer(app: Connect.Server) {
  const { createServer } = await import("node:http");
  return createServer(app);
}

再然后只需要将创建好的http服务器返回,并在适当的时机由外部对erver.listen进行调用就好啦

ts 复制代码
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中进行下配置就可以了

ts 复制代码
const config = defineConfig({
  ...,
  input: {
    ...,
    cli:path.resolve(__dirname, 'src/node/cli.ts'),
  },
  ...
});

调试

启动playground/dev下的示例,没有报错即说明http创建成功

总结

通过cac这个包帮助svite快速创建了cli应用与用户完成交互

而开发服务器的创建靠的是对原生的http模块的调用

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la7 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui7 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui