哈喽,很高兴你能点开这篇博客,本博客是针对 Vite
源码的解读系列文章,认真看完后相信你能对 Vite
的工作流程及原理有一个简单的了解。
我将会使用图文结合的方式,尽量让本篇文章显得不那么枯燥(显然对于源码解读类文章来说,这不是个简单的事情)。
如果你还没有使用过 Vite
,那么你可以看看我的前两篇文章,我也是刚体验没两天呢。(如下)
本篇文章解读的主要是 vite
源码本体,vite
通过 connect
库提供开发服务器,通过中间件机制实现多项开发服务器配置。而 vite
在本地开发时没有借助 webpack
或是 rollup
这样的打包工具,而是通过调度内部 plugin
实现了文件的转译,从而达到小而快的效果。
好了,话不多说,我们开始吧!
vite dev
项目目录
本文阅读的 Vite
源码版本是 2.8.0-beta.3
,如果你想要和我一起阅读的话,你可以在这个地址下载 Vite 源码。
我们先来看看 Vite
这个包的项目目录吧。(如下图)
这是一个集成管理的项目,其核心就是在 packages
里面的几个包,我们来分别看看这几个包是做什么的吧。(如下)
vite | Vite 主库,负责 Vite 项目的本地开发(插件调度)和生产产物构建(Rollup 调度) |
create-vite | 用于创建新的 Vite 项目,内部存放了多个框架(如 react、vue )的初始化模板 |
plugin-vue | Vite 官方插件,用于提供 Vue 3 单文件组件支持 |
plugin-vue-jsx | Vite 官方插件,用于提供 Vue 3 JSX 支持(通过 专用的 Babel 转换插件)。 |
plugin-react | Vite 官方插件,用于提供完整的 React 支持 |
plugin-legacy | Vite 官方插件,用于为打包后的文件提供传统浏览器兼容性支持 |
playground | Vite 内置的一些测试用例及 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 项目的插件 |
resolve | resolve 支持的配置较多,可以参考 vite 文档 |
css | 关于 css 文件的编译选项,可以参考 vite 文档 |
json | 关于 json 文件的编译选项,可以参考 vite 文档 |
esbuild | 看官方文档是用于转换文件的,但是不太清楚具体的工作是做什么的,有了解的麻烦在评论区留言解惑一下 |
assetsInclude | 设置需要被 picomatch 模式(一种文件匹配模式)独立处理的文件型 |
optimizeDeps | 依赖优化选项,具体可以参考 vite 文档 |
ssr | ssr 的相关选项,具体可以参考 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.js
、vite.config.mjs
、vite.config.ts
、vite.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
处理了一些 publicDir
、cacheDir
目录后,导出了下面这份配置。
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
鼓励一下吧!