注:本篇是组内PPT分享内容,说的部分会比较多。弄成文章后还未整理,后续整理一下。
0. 源码目录结构
https://github.com/vitejs/vite
1. 源码入口
1.1 运行npm run dev后发生了什么?
先准备一个vite脚手架生成的项目vite-vue3,当运行npm run dev
后发生了什么?在package.json中看到运行的是vite命令。
vite命令是在哪里注册的呢,在node_modules/vite/package.json
中查看bin字段。
"bin"
字段的作用是能让我们在命令窗口全局输入命令执行。可以看到 vite 命令:
"bin": {
"vite": "bin/vite.js"
}
打开vite.js看到,其中主要运行的是
function start() {
require('../dist/node/cli')
}
这里的/dist/node/cli.js是打包后的文件,可能有点长,可以配合vite打包前的源码一起阅读。
在/dist/node/cli.js中,首先使用 cac 命令工具处理用户的输入。
cli然后命令执行的是createServer
,这个createServer
从 ./chunks/dep-55830a1a.js
引入,这里也是打包后的代码,比较长。
1.2 createServer
我们找到了前面大概执行的顺序后,这里回到源码,在 packages/vite/src/node/server/index.ts
里面找到createServer
:
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// Vite 配置整合
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const root = config.root
const serverConfig = config.server
// 创建http服务
const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// 创建ws服务
const ws = createWebSocketServer(httpServer, config, httpsOptions)
// 创建watcher,设置代码文件监听
const watcher = chokidar.watch(path.resolve(root), {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(Array.isArray(ignored) ? ignored : [ignored])
],
...watchOptions
}) as FSWatcher
// 创建server对象
const server: ViteDevServer = {
config,
middlewares,
httpServer,
watcher,
ws,
moduleGraph,
listen,
...
}
// 文件监听变动,websocket向前端通信
watcher.on('change', async (file) => {
...
handleHMRUpdate()
})
// 服务 middleware
middlewares.use(...)
// optimize: 预构建
await initDepsOptimizer(config, server)
// 监听端口,启动服务
httpServer.listen = (async (port, ...args) => { ... })
return server
}
可以看到 createServer
做了很多事情,上面列举主要的几个:
resolveConfig:整合配置(resolvePlugins)
注册各种中间件(indexHtml、transformMiddleware、static)
HMR:使用chokidar监听文件的修改
optimizeDeps:预构建
创建httpServer,启动服务
...
2. 预构建
2.1 no-bundle vs bundle
怎么理解no-bundle?不打包?
bundle no-bundle
2.2 为什么要预构建?
1、Vite是基于浏览器原生支持ESM的能力实现的,要求用户的代码模块必须是ESM模块,因此必须将commonJs的文件提前处理,转化成 ESM 模块并缓存入 node_modules/.vite
2、减少模块和请求数量。例如,lodash-es 有超过 600 个内置模块。当我们执行 import { debounce } from 'lodash-es'
时,浏览器同时发出 600 多个 HTTP 请求!尽管服务器在处理这些请求时没有问题,但大量的请求会在浏览器端造成网络拥塞,导致页面的加载速度相当慢。
通过预构建 lodash-es 成为一个模块,我们就只需要一个 HTTP 请求了!
2.3 预构建的核心流程
预构建的核心流程,包括缓存判断、依赖扫描、依赖打包和元信息保存这四个主要的步骤。
image (5).png关于预构建的实现代码都在optimizeDeps
函数当中,在仓库源码的 packages/vite/src/node/optimizer/index.ts
查看 optimizeDeps
:
optimizeDeps
首先调用loadCachedDepOptimizationMetadata
获取node_modules/.vite/_metadata.json
中的元信息。
然后调用getDepHash
,这个函数是读取目录下的package-lock.json的内容,然后将文件内容进行hash得到一个hash值。
然后两个hash进行对比是否相等。
也就是说,预编译就是看node_modules的包有没有变化,如果不相等。会调用scanImports
去扫,在scanImports
中,得到入口文件后,对入口文件进行了解析,当然,具体的解析过程在依赖扫描阶段的 Esbuild 插件(esbuildScanPlugin
)中得以实现。
这里就会使用esbuild.build
去编译文件,其中esbuildDepPlugin
就是打包的插件:
生成出来保存到.vite下。最后,执行writeFile
,再将相关信息保存到_metadata.json
。
3. 核心编译流程
webpack中plugin和loader的区别?
3.1 Rollup插件机制
Rollup 的打包过程中,会定义一套完整的构建生命周期,从开始打包到产物输出,中途会经历一些标志性的阶段,并且在不同阶段会自动执行对应的插件钩子函数(Hook)。
Vite 的插件机制是基于 Rollup 来设计的。Vite 模拟了 Rollup 的插件机制,设计了一个 PluginContainer
对象来调度各个插件。
PluginContainer
的 实现 基于借鉴于 WMR 中的rollup-plugin-container.js,主要分为 2 个部分:
1、实现 Rollup 插件钩子的调度
2、实现插件钩子内部的 Context 上下文对象
PluginContainer的定义了一系列执行plugin的方法。如buildStart、resolveId、load、transform。
3.2 vite插件的接口定义
packages/vite/src/node/plugin.ts
:
export interface Plugin extends RollupPlugin {
enforce?: 'pre' | 'post'
apply?: 'serve' | 'build' | ((config: UserConfig, env: ConfigEnv) => boolean)
config?: (
config: UserConfig,
env: ConfigEnv
) => UserConfig | null | void | Promise<UserConfig | null | void>
configResolved?: (config: ResolvedConfig) => void | Promise<void>
configureServer?: ServerHook
configurePreviewServer?: PreviewServerHook
transformIndexHtml?: IndexHtmlTransform
handleHotUpdate?(
ctx: HmrContext
): Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
resolveId?(
this: PluginContext,
source: string,
importer: string | undefined,
options: {
custom?: CustomPluginOptions
ssr?: boolean
/**
* @internal
*/
scan?: boolean
}
): Promise<ResolveIdResult> | ResolveIdResult
load?(
this: PluginContext,
id: string,
options?: { ssr?: boolean }
): Promise<LoadResult> | LoadResult
transform?(
this: TransformPluginContext,
code: string,
id: string,
options?: { ssr?: boolean }
): Promise<TransformResult> | TransformResult
}
3.3 当浏览器一个JS请求到vite服务时,发生了什么?
例如:
<script type="module" src="/src/main.js"></script>
或者 import { get } from './utils';
// main transform middleware
middlewares.use(transformMiddleware(server))
可以看到pluginContainer会执行插件中的钩子。对于不同的资源会有不同的插件去处理。
vite的内置插件:
路径解析插件(packages/vite/src/node/plugins/resolve.ts)
路径解析插件(即vite:resolve)是 Vite 中比较核心的插件,几乎所有重要的 Vite 特性都离不开这个插件的实现,诸如依赖预构建、HMR、SSR 等等。
CSS 编译插件(packages/vite/src/node/plugins/css.ts)
import分析插件(packages/vite/src/node/plugins/importAnalysis.ts)
重写import语句,如import Vue from 'vue';
导入路径会重写为预构建文件夹的路径;
注入HMR客户端脚本。
Esbuild 转译插件(packages/vite/src/node/plugins/esbuild.ts)
用来进行 .js、.ts、.jsx和tsx,代替了传统的 Babel 或者 TSC 的功能,这也是 Vite 开发阶段性能强悍的一个原因。
4. HMR流程
打包工具实现热更新的思路都大同小异:主要是通过WebSocket创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。
浏览器文件是几时被注入的?在importAnalysis插件中:
if (hasHMR && !ssr) {
debugHmr(
`${
isSelfAccepting
? `[self-accepts]`
: acceptedUrls.size
? `[accepts-deps]`
: `[detected api usage]`
} ${prettyImporter}`
)
// inject hot context
str().prepend(
`import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` +
`import.meta.hot = __vite__createHotContext(${JSON.stringify(
importerModule.url
)});`
)
}
网友评论