本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
在上一篇文章 - 我是如何利用持久化缓存策略来提升 Vite 开发模式下首屏性能的? 中,小编给大家展示了如何使用持久化缓存策略来优化 Vite 开发模式下的首屏性能,不知道小伙伴们看到之后有没有尝试一下呢?
在这个优化策略中,最关键的一步就是能获取到 moduleGraph 数据并保存到本地,而小编也通过 Vite 提供的 ViteDevServer 这个 API 顺利完成了这一步。在文章发布以后,小编又回去对 ViteDevServer 做了一下深入研究,发现这个 API 在 Vite 中有非常重要的地位。借助这个 API,我们可以做好多事情。
今天,小编就通过本文给大家介绍一下 ViteDevServer 的基本情况以及它可以应用的场景。
本文的目录结构如下:
初识 ViteDevServer
首先,小编给大家直接展示一下 viteDevServer 这个 API 的结构:
interface ViteDevServer {
// 解析以后的 vite config 对象
config: ResolvedConfig;
// 一个 connect app 实例,用于给 dev server 添加 middleware
middlewares: Connect.Server;
// 基于 http 模块实现的一个 http server
httpServer: http.Server | null;
// Chokidar watcher 实例
watcher: FSWatcher;
// websocket server 实例
ws: WebSocketServer;
// 插件容器实例,用于触发指定 plugin hook 的执行
pluginContainer: PluginContainer;
// 源文件对应的模块依赖图
moduleGraph: ModuleGraph;
// ??
resolvedUrls: ResolvedServerUrls | null;
// 根据请求,执行 resolve、load、transform 操作,并返回 transform 以后的内容
transformRequest( url: string, options?: TransformOptions ): Promise<TransformResult | null>;
// 用于对 index.html 文件做修改;
transformIndexHtml(url: string, html: string): Promise<string>;
// ssr 相关,暂不介绍
ssrLoadModule( url: string, options?: { fixStacktrace?: boolean } ): Promise<Record<string, any>>;
// ssr 相关,暂不介绍
ssrFixStacktrace(e: Error): void;
// 热更新时更新 moduleGraph 中的相关 module
reloadModule(module: ModuleNode): Promise<void>;
// 启动 server
listen(port?: number, isRestart?: boolean): Promise<ViteDevServer>;
// 重新启动 server
restart(forceOptimize?: boolean): Promise<void>;
// 关闭 server
close(): Promise<void>
}
·
观察上面的数据结构,我们可以发现 ViteDevServer 其实就是一个普通的 js 对象,并没有要特别要注意的,关键还是要看这个对象内部的属性和方法。不过要想对这些属性有个清晰的认识,小编还是要先给大家介绍一下 Vite 开发模式下的整个工作过程。
非常简单,整个工作过程可以理解为下面这 3 个步骤:
-
第一步,启动一个本地
server,用于响应浏览器发起的请求。由于浏览器发起的请求类型不同,如
html、js、css、接口等,不同类型有不同的处理逻辑,因此需要给server添加一些middlewares来分别处理这些请求。另外,为了实现热更新功能,还需要实现一个
watcher和websocket server。其中,watcher用于监听源文件是否发生编辑,websocket server用于推送模块更新消息。 -
第二步,浏览器发起请求,
server端根据请求url返回合适的response给浏览器。在这个过程中,
server端会根据请求携带的url、etag信息,判断浏览器的缓存是否可用。如果可用,直接返回
304;如果不可用,解析请求url为真实路径,读取源文件内容,并对源文件内容做转换,然后将转换以后的内容返回给浏览器,并且缓存起来,方便下一次收到相同请求时进行处理。 -
第三步,关闭
server,结束工作。
有了上面的铺垫,那么再理解 ViteDevServer 对象内的各个属性和方法,就非常简单了:
-
config,保存vite.config.ts文件提供的配置项信息; -
httpServer, 基于http模块实现的server实例,响应浏览器发起的请求; -
middleware, 给httpServer添加各种需要的中间件; -
watcher、ws、reloadModule, 配合实现热更新; -
pluginContainer, 插件容器,用于触发plugin hook; -
transformIndexHtml, 对html类型的文件进行内容转换; -
transformRequest, 将浏览器发起的请求url,解析为源文件的真实路径,加载并转换源文件,将转换后的源文件内容返回给浏览器; -
moduleGraph,缓存transformRequest返回的源文件内容; -
listen, 启动http server; -
close, 关闭http server、ws、pluginContainer、watcher; -
restart, 重新启动http server; -
ssrLoadModule、ssrFixStacktrace,ssr相关,由于小编还未理解,暂不解释;
在使用 Vite 时,如果想在外部程序中访问到 ViteDevServer,我们只能借助 configureServer hook。这个 hook 会在 ViteDevServer 初始化成功以后触发。
const myPlugin = () => ({
name: 'configure-server',
configureServer(server) {
// 可以在这里使用 ViteDevServer
...
}
})
上面这种方式,ViteDevServer 只能在 configureServer hook 内部使用,我们可以对插件做些稍微改造,使得 ViteDevServer 可以在其他 hook 中使用。
const myPlugin = () => {
let _server = null;
return () => {
name: 'configure-server',
configureServer: async (server) {
_server = server;
},
buildStart: async () => {
// 可以通过 _server 使用 ViteDevServer
}
}
};
我们可以使用 ViteDevServer 做些什么
了解了 ViteDevServer 的详细情况以后,我们再来看看使用 ViteDevServer 可以做些什么。
读/写 vite config
通过 ViteDevServer.config, 我们可以获取到 vite.config.ts 文件中的配置项信息,然后对 config 信息进行读写。
实际上,Vite 已经提供了两个 hook 来对 config 信息进行读写 - config hook 和 configResolved hook。其中, config hook 用于读写解析以前的 config 信息,即 vite.config.ts 直接提供的配置项;configResolved hook 用于读写解析以后的 config 信息。
而 ViteDevServer.config 又给我们提供了一种读写解析以后的 config 信息的方式。
有了 configResolved hook 和 ViteDevServer.config, 我们以后就可以这样读写 config 信息:
const myPlugin = () => {
let _server = null;
let _config = null
return () => {
name: 'configure-server',
configResolved: async (config) => {
_config = config
},
configureServer: async (server) {
_server = server;
},
buildStart: async () => {
// 可以通过 _config 读写 config 信息
// 也可以通过 _server.config 读写 config 信息
}
}
};
添加自定义 middleware
通过 ViteDevServer.middleware, 我们可以给 http server 添加自定义中间件, 对浏览器发起的请求做一些自定义操作。
添加自定义 middleware,有两种方式:
const myPlugin = () => {
let _server = null;
return () => {
name: 'configure-server',
configureServer: async (server) {
server.middlewares.use((req, res, next) => {
// 自定义逻辑操作
...
if (xx) {
// 后续的 middleware 不执行
return res.end();
}
// 后续的 middleware 继续执行
next();
})
}
}
};
const myPlugin = () => {
let _server = null;
return () => {
name: 'configure-server',
configureServer: async (server) {
return () => {
server.middlewares.use((req, res, next) => {
// 自定义逻辑操作
...
if (xx) {
// 后续的 middleware 不执行
return res.end();
}
// 后续的 middleware 继续执行
next();
});
}
}
}
};
这两种方式的唯一区别:
-
第一种,自定义
middleware在Vite内部middleware之前触发; -
第二种,自定义
middleware在Vite内部middleware之后触发;
在实际应用中,我们可以根据自身情况,合理选择添加自定义 middleware 的方式。
监听、读取 moduleGraph 中的模块信息
通过 ViteDevServer.moduleGraph,我们可以读取和监听已经缓存的源文件信息。
首先,我们可以先来看看 moduleGraph 的源码结构。
class ModuleGraph {
// map,根据请求 url 获取对应的源文件 module 信息
urlToModuleMap = new Map<string, ModuleNode>();
// map,根据 id 获取对应的源文件 module 信息
idToModuleMap = new Map<string, ModuleNode>();
// map, 根据源文件真实路径获取源文件对应的 module 信息
fileToModulesMap = new Map<string, Set<ModuleNode>>();
safeModulesPath = new Set<string>();
...
// 根据请求 url 获取源文件 module 信息
async getModuleByUrl(rawUrl: string, ssr?: boolean): Promise<ModuleNode | undefined> {
const [url] = await this.resolveUrl(rawUrl, ssr)
return this.urlToModuleMap.get(url)
}
// 根据 id 获取源文件 module 信息
getModuleById(id: string): ModuleNode | undefined {
return this.idToModuleMap.get(removeTimestampQuery(id))
}
// 根据源文件真实路径获取源文件 module 信息
getModulesByFile(file: string): Set<ModuleNode> | undefined {
return this.fileToModulesMap.get(file)
}
...
// 将请求 url 解析为源文件真是路径
async resolveUrl(url: string, ssr?: boolean): Promise<ResolvedUrl> {
url = removeImportQuery(removeTimestampQuery(url))
const resolved = await this.resolveId(url, !!ssr)
const resolvedId = resolved?.id || url
const ext = extname(cleanUrl(resolvedId))
const { pathname, search, hash } = parseUrl(url)
if (ext && !pathname!.endsWith(ext)) {
url = pathname + ext + (search || '') + (hash || '')
}
return [url, resolvedId, resolved?.meta]
}
}
在上面的代码中,小编列举了 moduleGraph 中几个关键的属性和方法, 其中 urlToModuleMap / idToModuleMap / fileToModulesMap 用来存储请求 url / id / 源文件真实路径和源文件 module 对象的映射关系,getModuleByUrl / getModuleById / getModulesByFile 用来根据请求 url / id / 源文件真实路径来获取源文件 module 对象。
其中,getModuleByUrl 比较特殊,返回一个 promise 对象,因此我们使用时要配合 async / await 一起使用。
另外,如果我们需要将请求 url 转化为源文件真实路径,可以使用 resolveUrl。
接下来,我们再看看 Module 对象的源码结构:
class ModuleNode {
url: string
id: string | null = null
file: string | null = null
type: 'js' | 'css'
info?: ModuleInfo
meta?: Record<string, any>
// 父 module
importers = new Set<ModuleNode>()
// 子 module
importedModules = new Set<ModuleNode>()
acceptedHmrDeps = new Set<ModuleNode>()
acceptedHmrExports: Set<string> | null = null
importedBindings: Map<string, Set<string>> | null = null
isSelfAccepting?: boolean
// 源文件转换以后的内容
transformResult: TransformResult | null = null
ssrTransformResult: TransformResult | null = null
ssrModule: Record<string, any> | null = null
ssrError: Error | null = null
lastHMRTimestamp = 0
lastInvalidationTimestamp = 0
...
}
interface TransformResult {
code: string // 转换以后的源文件内容字符串
map: SourceMap | null
etag?: string // etag 信息,协商缓存使用
deps?: string[] // 静态依赖
dynamicDeps?: string[] // 动态依赖
}
通过 module.importers 和 module.importedModules, 我们可以获取到当前模块依赖的父模块和子模块,并且通过 module.transformResult,可以获取到源文件转换以后的内容、etag 信息等。
有了这些信息以后,我们就可以在实际应用中利用 moduleGraph 做一些事情:
-
读取
moduleGraph中的相关信息,并且保存到本地,如我们上一篇文章中提到的持久化缓存插件persistent-cache-plugin; -
监听
moduleGraph内容的变化,然后根据变化做一些自定义修改,如社区中的vite-plugin-optimize-persist插件的实现; -
对
moduleGraph中收集的源文件module中transformResult进行修改; -
...
手动触发文件的处理
通过 ViteDevServer 提供的 transformIndexHtml 和 transformRequest 这两个 api,我们可以在某些场景下,手动触发对请求文件的处理。
针对这两个 api,我们可以这样使用:
-
在一个自定义
middleware中,如果我们想手动触发html文件的转换,我们可以直接调用transformIndexHtml这个方法,然后将转换以后的内容返回给浏览器器。 -
某些请求,我们想禁用协商缓存策略,可以在自定义
middleware中手动调用transformRequest方法,不走 Vite 原来的那一套逻辑。 -
像上一篇
persistent-cache-plugin中一样,手动调用transformRequest方法,完成对moduleGraph内容的填充。 -
...
手动触发指定类型的 plugin hook
通过 ViteDevServer.pluginContainer, 我们可以手动触发指定类型的 plugin hook。
我们可以先来看看 pluginContainer 的源码结构:
interface PluginContainer {
options: InputOptions
getModuleInfo(id: string): ModuleInfo | null
buildStart(options: InputOptions): Promise<void>
// 触发 resolveId 类型的 hook
resolveId(
id: string,
importer?: string,
options?: {
custom?: CustomPluginOptions
skip?: Set<Plugin>
ssr?: boolean
/**
* @internal
*/
scan?: boolean
isEntry?: boolean
}
): Promise<PartialResolvedId | null>
// 触发 transform 类型的 hook
transform(
code: string,
id: string,
options?: {
inMap?: SourceDescription['map']
ssr?: boolean
}
): Promise<SourceDescription | null>
// 触发 load 类型的 hook
load(
id: string,
options?: {
ssr?: boolean
}
): Promise<LoadResult | null>
// 触发 buildEnd、closeBundle 类型的 hook
close(): Promise<void>
}
在实际应用中,我们可以通过 ViteDevServer.pluginContainer 的 resolveId、transform、load、close 方法,手动触发对应的 plugin hook。
手动关闭 dev server
通过 ViteDevServer.close 方法,我们可以手动关闭 http server、ws、watcher,并触发 buildEnd、closeBundle 类型的 hook。
close 方法的源码如下:
async close() {
if (!middlewareMode) {
process.off('SIGTERM', exitProcess)
if (process.env.CI !== 'true') {
process.stdin.off('end', exitProcess)
}
}
await Promise.all([
// 关闭 watcher
watcher.close(),
// 关闭 websocket server
ws.close(),
// // 触发 buildEnd、closeBundle 类型的 hook
container.close(),
// 关闭 http server
closeHttpServer()
])
server.resolvedUrls = null
},
在之前的 persistent-cache-plugin 中,小编就是借助 ViteDevServer.close 方法,实现了 dev server 的手动关闭。
结束语
到这里,关于 ViteDevServer 的介绍以及使用 ViteDevServer 可以做些什么的梳理就结束了。
最后我们做一个简单的总结:
- 通过
configureServer hook获取ViteDevServer; - 借助
ViteDevServer,我们可以完成读/写vite config、添加自定义middleware、监听、读取moduleGraph中的模块信息、手动触发文件的处理、手动触发指定类型的plugin hook、手动关闭dev server等;
如果小伙伴们还有其他的应用场景,欢迎在评论区留言哈,😄。