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

前言

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

源码获取

传送门

更新进度

公众号:更新至第7

博客:更新至第3

源码分析

找到packages\vite\bin\vite.js,这是在package.jsonscripts中使用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)
  }
  ...
}

最后,vitehttplisten方法进行了重写,这是因为它要确保同时开启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创建成功

image.png

总结

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容