vite源码梳理

1,330 阅读4分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,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命令引用了./servercreateServer,并触发listen()进行监听

code.png

/node/server/index.ts

createServer就是要创建并返回一个server,具体的,做了以下几件事:

  1. 整合配置文件vite.config.js 和 命令行里的配置到config中
const config = await resolveConfig(inlineConfig, "serve", "development");
  1. 启动一个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);
  1. 使用 chokidar监听文件变化(这是进行热更新的基础)
const watcher = chokidar.watch(path.resolve(root), {
    ignored: ["**/node_modules/**", "**/.git/**", ...ignored],
    ignoreInitial: true,
    ignorePermissionErrors: true,
    disableGlobbing: true,
    ...watchOptions,
  }) as FSWatcher;
  1. 将所有的plugin统一进行处理,保存到container中
const container = await createPluginContainer(config, watcher);
  1. 根据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.
    */

  1. 初始化后面要返回的vite-dev-server,绑定了一些属性和方法
const server: ViteDevServer = {
...
}
  1. watcher发生变化的时候,进行相应的热更新处理
watcher.on('change', fn)
watcher.on('add', fn)
watcher.on('unlink', fn)
  1. 执行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));
    }
  }
  1. 内部中间件的使用
...
middlewares.use(corsMiddleware(typeof cors === "boolean" ? {} : cors));

middlewares.use(proxyMiddleware(httpServer, config));
...

  1. 执行posHooks里的plugins
postHooks.forEach((fn) => fn && fn());
  1. 转换index.html
middlewares.use(indexHtmlMiddleware(server));
  1. 在listen()之前
    1. 执行vite钩子 buildStart
    2. 执行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();
  }
 
  1. 返回 server

vite build

cli.ts

build命令就是引入了./build文件,并执行build()

code.png

rollup 打包

vite使用rollup进行打包的,在阅读相关方法之前,有必要对rollup进行一些基本了解。以下是rollup官网对其打包过程的代码描述。rollup javascript API image.png

可以看出rollup一般的打包需要

  1. 打包的配置参数inputOptions
  2. 打包生成文件的配置参数 outputOptions
  3. 调用rollup.rollup()返回一个bundle对象
  4. 调用bundle.generate() 或者bundle.write() 完成打包 那么vite的build也应该是按照这个流程来的

build.ts

build.tsbuild方法主要是指行了dobuild方法,dobuild做了以下几件事(ssr相关的先不考虑):

  1. 整理配置参数=> config
const config = await resolveConfig(inlineConfig, "build", "production");
  1. 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);
    },
  };
  1. rollup打包输出参数 outputs(一般我们在项目开发中outputs就是个obj,但在构建库时可能需要生成不同格式的包,所以outputs也可能是个数组)
const outputs = resolveBuildOutputs(
  options.rollupOptions?.output,
  libOptions,
  config.logger
);
  1. 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;
}

  1. 生成bundle对象
const bundle = await rollup.rollup(rollupOptions);
  1. 调用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'的都是我看的过程中加的注释

image.png