哈喽,很高兴你能点开这篇博客,本博客是针对 Vite 源码的解读系列文章,认真看完后相信你能对 Vite 的工作流程及原理有一个简单的了解。

我将会使用图文结合的方式,尽量让本篇文章显得不那么枯燥(显然对于源码解读类文章来说,这不是个简单的事情)。

如果你还没有使用过 Vite,那么你可以看看我的前两篇文章,我也是刚体验没两天呢。(如下)

本篇文章解读的主要是 vite 源码本体,vite 通过 connect 库提供开发服务器,通过中间件机制实现多项开发服务器配置。而 vite 在本地开发时没有借助 webpack 或是 rollup 这样的打包工具,而是通过调度内部 plugin 实现了文件的转译,从而达到小而快的效果。

好了,话不多说,我们开始吧!

vite dev

项目目录

本文阅读的 Vite 源码版本是 2.8.0-beta.3,如果你想要和我一起阅读的话,你可以在这个地址下载 Vite 源码

我们先来看看 Vite 这个包的项目目录吧。(如下图)

这是一个集成管理的项目,其核心就是在 packages 里面的几个包,我们来分别看看这几个包是做什么的吧。(如下)

viteVite 主库,负责 Vite 项目的本地开发(插件调度)和生产产物构建(Rollup 调度)
create-vite用于创建新的 Vite 项目,内部存放了多个框架(如 react、vue)的初始化模板
plugin-vueVite 官方插件,用于提供 Vue 3 单文件组件支持
plugin-vue-jsxVite 官方插件,用于提供 Vue 3 JSX 支持(通过 专用的 Babel 转换插件)。
plugin-reactVite 官方插件,用于提供完整的 React 支持
plugin-legacyVite 官方插件,用于为打包后的文件提供传统浏览器兼容性支持
playgroundVite 内置的一些测试用例及 Demo

这几个源码仓库其实有阅读的价值,但是我们这次还是先专注一下我们本期的主线 —— Vite,从 Vite 开始吧。

接下来我们重点解读 vite 本地开发服务命令 —— vite / vite dev / vite serve

vite dev

我们来了解一下 vite dev 命令,也就是本地开发服务的内部工作流程。

vite dev 调用了内部的 createServer 方法创建了一个服务,这个服务利用中间件(第三方)支持了多种能力(如 跨域静态文件服务器等),并且内部创建了 watcher 持续监听着文件的变更,进行实时编译和热重载。

createServer 做的事情就是我们需要关注的核心逻辑。

createServer 方法中,首先进行了对配置的收集工作 —— resolveConfig

vite 支持的配置

我们正好可以通过源码看看 vite 项目支持的配置,你也可以直接参照 Vite 官方文档。(如下)

configFile配置文件,默认读取根目录下的 vite.config.js 配置文件
envFile环境变量配置文件,默认读取根目录下的 .env 环境变量配置文件
root项目的根目录,默认值是执行命令的目录 —— process.cwd()
base类似于 webpack 中的 publicPath,也就是资源的公共基础路径
server本地运行时的服务设置,比如设置 host(主机地址)、port(运行端口)...详细配置可以参考 vite 文档
build构建生产产物时的选项,可以参考 vite 文档
preview预览选项,在使用了 build 命令后,可以运行 vite preview 对产物进行预览,具体配置可以参考 vite 文档
publicDir静态资源目录,用于放置不需要编译的静态资源,默认值是 public 目录
cacheDir缓存文件夹,用于放置 vite 预编译好的一些缓存依赖,加速 vite 编译速度
mode编译模式,本地运行时默认值是 development,构建生产产物时默认是 production
define定义全局变量,其中开发环境每一项会被定义在全局,而生产环境将会被静态替换
plugins配置 vite 项目的插件
resolveresolve 支持的配置较多,可以参考 vite 文档
css关于 css 文件的编译选项,可以参考 vite 文档
json关于 json 文件的编译选项,可以参考 vite 文档
esbuild看官方文档是用于转换文件的,但是不太清楚具体的工作是做什么的,有了解的麻烦在评论区留言解惑一下
assetsInclude设置需要被 picomatch 模式(一种文件匹配模式)独立处理的文件型
optimizeDeps依赖优化选项,具体可以参考 vite 文档
ssrssr 的相关选项,具体可以参考 vite 文档
logLevel调整控制台输出的级别,默认为 info
customLogger自定义 logger,该选项没有暴露,是一个内部选项
clearScreen默认为 true,配置为 false 后,每次重新编译不会清空之前的内容
envDir用于加载环境变量配置文件 .env 的目录,默认为当前根目录
envPrefix环境变量的前缀,带前缀的环境变量将会被注入到项目中
worker配置 bundle 输出类型、plugins 以及 Rollup 配置项

在上面这些配置中,有一部分可以在启动时,通过命令行参数添加,比如通过 vite --base / --mode development 的形式进行设置。

如果你希望该配置可以通过配置读取,也可以全部通过 vite.config.js 来进行配置。

配置断点调试

在粗略看过一遍 vite 支持的配置后,我们回到 createServer 函数,准备开始阅读。

在此之前,如果我们能够直接运行 vite dev 命令并打上断点,能够更好地帮助我们更好的阅读源码,所以我们先来配置一下。

我们需要先进入 vite/packages/vite,安装依赖,然后在 scripts 中运行 npm run build,将 vite 构建到 dist 目录中。

然后,我们使用 vscode 的调试功能,创建一个 launch.json(如下),运行我们的一个 vite 项目。

// launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "pwa-node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "program": "packages/vite/bin/vite.js",
      "args": ["/Users/Macxdouble/Desktop/ttt/vite-try"]
    }
  ]
}

调试配置完成后,我们可以在 resolveConfig 函数中打一个断点,查看效果(文件位置在 dist 目录中,大家需要根据自己的引用找到对应文件)。

加载配置文件

resolveConfig 的第一步就是加载项目目录的配置文件,如果没有指定配置文件位置,会自动在根目录下寻找 vite.config.jsvite.config.mjsvite.config.tsvite.config.cjs

如果没有找到配置文件,则直接会中止程序。

在读取配置文件后,会将配置文件和初始化配置(优先级更高,有部分配置来自于命令行参数)进行合并,然后得到一份配置。(如下图)

配置收集 - resolveConfig

createServer 的开头,调用了 resolveConfig 函数,进行配置收集。

我们先来看看 resolveConfig 都做了哪些事情吧。

处理插件执行顺序

首先,resolveConfig 内部处理了插件排序规则,对应下面的排序规则。

在后续处理的过程中,插件将按照对应的排序规则先后执行,这样能够让插件更好地工作在各个生命周期节点。

合并插件配置

在插件排序完成后,vite插件 暴露了一个配置 config 字段,可以通过设置该属性,使插件能够新增或改写 vite 的一些配置。(如下图)

处理 alias

然后,resolveConfig 内部处理了 alias 的逻辑,将指定的 alias 替换成对应的路径。

读取环境变量配置

接下来,resolveConfig 内部找到 env 的配置目录(默认为根目录),然后在目录中读取对应的 env 环境变量配置文件。我们可以看看内部的读取规则优先级(如下图)

可以看出,读取的优先级分别是 .env.[mode].local.env.[mode]。如果不存在对应 mode 的配置文件,则会尝试去寻找 .env.local.env 配置文件,读取到配置文件后,使用 doteenv 将环境变量写入到项目中;如果这些环境变量配置文件都不存在的话,则会返回一个空对象。

该环境变量配置文件并不影响项目运行,所以不配置也没有什么影响。

导出配置

接下来,vite 初始化了构建配置,也就是文档中的 build 属性,详情可以参照 构建选项文档

最后,resolveConfig 处理了一些 publicDircacheDir 目录后,导出了下面这份配置。

const resolved: ResolvedConfig = {
    ...config,
    configFile: configFile ? normalizePath(configFile) : undefined,
    configFileDependencies,
    inlineConfig,
    root: resolvedRoot,
    base: BASE_URL,
    resolve: resolveOptions,
    publicDir: resolvedPublicDir,
    cacheDir,
    command,
    mode,
    isProduction,
    plugins: userPlugins,
    server,
    build: resolvedBuildOptions,
    preview: resolvePreviewOptions(config.preview, server),
    env: {
      ...userEnv,
      BASE_URL,
      MODE: mode,
      DEV: !isProduction,
      PROD: isProduction
    },
    assetsInclude(file: string) {
      return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
    },
    logger,
    packageCache: new Map(),
    createResolver,
    optimizeDeps: {
      ...config.optimizeDeps,
      esbuildOptions: {
        keepNames: config.optimizeDeps?.keepNames,
        preserveSymlinks: config.resolve?.preserveSymlinks,
        ...config.optimizeDeps?.esbuildOptions
      }
    },
    worker: resolvedWorkerOptions
  }

resolveConfig 内部还有一些额外的工作处理,主要是收集内部插件集合(如下图),还有配置一些废弃选项警告信息。

本地开发服务 - createServer

回到 createServer 方法,该方法通过 resolveConfig 拿到配置后,第一时间处理了 ssr(服务端渲染)的逻辑。

如果使用了服务端渲染,则会通过别的方式进行本地开发调试。

如果不是服务端渲染,则会创建一个 http server 用于本地开发调试,同时创建一个 websocket 服务用于热重载。(如下图)

文件监听 + 热重载

然后,vite 创建了一个 FSWatcher 对象,用于监听本地项目文件的变动。(这里使用的是 chokidar 库)

  const watcher = chokidar.watch(path.resolve(root), {
    ignored: [
      // 忽略 node_modules 目录的文件变更
      '**/node_modules/**',
      // 忽略 .git 目录的文件变更
      '**/.git/**',
      // 忽略用户传入的 `ignore` 目录文件的变更
      ...(Array.isArray(ignored) ? ignored : [ignored])
    ],
    ignoreInitial: true,
    ignorePermissionErrors: true,
    disableGlobbing: true,
    ...watchOptions
  }) as FSWatcher

然后,vite 将多个属性和方法组织成了一个 server 对象,该对象负责启动本地开发服务,也负责服务后续的开发热重载。

接下来,我们看看 watcher 是如何做页面热重载的吧,原理就是监听到文件变更后,重新触发插件编译,然后将更新消息发送给客户端。(如下图)

插件容器

接下来,vite 创建了插件容器(pluginContainer),用于在构建的各个阶段调用插件的钩子。(如下图)

中间件机制

接下来是一些内部中间件的处理,当配置开发服务器选项时,vite 内部通过 connect 框架的中间件能力来提供支持。(如下图)

其中,对 public 目录、公共路径等多项配置都是通过 connect + 中间件实现的,充分地利用了第三方库的能力,而没有重复造轮子。

预构建依赖

接下来,vite 内部对项目中使用到的依赖进行的预构建,一来是为了兼容不同的 ES 模块规范,二来是为了提升加载性能。(如下图)

准备工作就绪后,vite 内部调用 startServer 启动本地开发服务器。(如下)

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

小结

至此,vite 本身的源码部分就解析完了。

可以看出,在本地开发时,vite 主要依赖 插件 + 中间件体系 来提供能力支持。因为本地开发时只涉及到少量编译工作,所以非常的快。只有在构建生产产物时,vite 才用到了 rollup 进行构建。

我们用一张流程图来最后梳理一遍 vite 本地开发服务 内部的工作流程吧。

那么本期文章就到此结束,在下一篇文章,我会挑选 1 - 2 个比较典型的插件或是 build 篇(生产产物构建)来进行源码解析。

最后一件事

如果您已经看到这里了,希望您还是点个赞再走吧~

您的点赞是对作者的最大鼓励,也可以让更多人看到本篇文章!

如果觉得本文对您有帮助,请帮忙在 github 上点亮 star 鼓励一下吧!

03-05 14:03