本文为稀土掘金技术社区首发签约文章,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
等;
如果小伙伴们还有其他的应用场景,欢迎在评论区留言哈,😄。