前言
vite在本地启动的时候提供了一系列内部的中间件在开发服务中使用,其中最为核心的就是transformMiddleware
transformMiddleware中间件
function transformMiddleware(server) {
const { config: { root, logger }, moduleGraph } = server;
// 利用闭包函数特性返回中间件函数
return async function viteTransformMiddleware(req, res, next) {
try {
// 对req.url做格式上的处理,跳过非get请求等
// 处理sourcemap、publicdir
...
// 进入正题
// 正则判断,只有js、import查询、css和htmlProxy的请求可以转换
// 只要符合正则 /\.((j|t)sx?|m[jt]s|vue|marko|svelte|astro)($|\?)/ 的都可以通过isJSRequest
if (isJSRequest(url) ||
isImportRequest(url) ||
isCSSRequest(url) ||
isHTMLProxy(url)) {
// 去掉 import 的查询参数
url = removeImportQuery(url);
// 去除有效的 id 前缀。这是由 importAnalysis 插件在解析的不是有效浏览器导入说明符的 Id 之前添加的
url = unwrapId(url);
// 区分 css 请求和导入
if (isCSSRequest(url) &&
!isDirectRequest(url) &&
req.headers.accept?.includes('text/css')) {
url = injectQuery(url, 'direct');
}
// 二次加载,利用 etag 做协商缓存
const ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch &&
(await moduleGraph.getModuleByUrl(url, false))?.transformResult
?.etag === ifNoneMatch) {
isDebug && debugCache(`[304] ${prettifyUrl(url, root)}`);
res.statusCode = 304;
return res.end();
}
// 使用插件容器解析、加载和转换请求
const result = await transformRequest(url, server, {
html: req.headers.accept?.includes('text/html')
});
if (result) {
const depsOptimizer = getDepsOptimizer(server.config, false); // non-ssr
const type = isDirectCSSRequest(url) ? 'css' : 'js';
const isDep = DEP_VERSION_RE.test(url) || depsOptimizer?.isOptimizedDepUrl(url);
// 返回结果
return send(req, res, result.code, type, {
etag: result.etag,
// allow browser to cache npm deps!
cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache',
headers: server.config.server.headers,
map: result.map
});
}
}
}
catch (e) {
}
next();
};
}
流程总结:
当浏览器有一个请求到开发服务时,会经过一系列中间件。当来到transformMiddleware中间件时,先依据是否以 .map 后缀判断 sourcemap 请求,是的话直接将对应的 map 返回。然后检查公共目录与根目录的位置关系,如果一个请求 url 以公共路径/public打头,就会触发相应警告。之后对 url 做以下处理:移除 import 参数、移除 /@id 前缀(这玩意是在 importAnalysis 插件中添加,后面单独分析)、如果是 css import 的话,会加入 direct 参数。接着缓存如果命中,直接返回,否则就会进入 transformRequest
transformRequest函数
function transformRequest(url, server, options = {}) {
const cacheKey = (options.ssr ? 'ssr:' : options.html ? 'html:' : '') + url;
// 下面这一段我的理解是当浏览器根据import发起请求时,对应的模块都需要重新处理(doTransform方法)
// This module may get invalidated while we are processing it. For example
// when a full page reload is needed after the re-processing of pre-bundled
// dependencies when a missing dep is discovered. We save the current time
// to compare it to the last invalidation performed to know if we should
// cache the result of the transformation or we should discard it as stale.
//
// A module can be invalidated due to:
// 1. A full reload because of pre-bundling newly discovered deps
// 2. A full reload after a config change
// 3. The file that generated the module changed
// 4. Invalidation for a virtual module
//
// For 1 and 2, a new request for this module will be issued after
// the invalidation as part of the browser reloading the page. For 3 and 4
// there may not be a new request right away because of HMR handling.
// In all cases, the next time this module is requested, it should be
// re-processed.
//
// We save the timestamp when we start processing and compare it with the
// last time this module is invalidated
const timestamp = Date.now();
// 判断请求队列中是否存在当前的请求
const pending = server._pendingRequests.get(cacheKey);
if (pending) {
return server.moduleGraph
.getModuleByUrl(removeTimestampQuery(url), options.ssr)
.then((module) => {
if (!module || pending.timestamp > module.lastInvalidationTimestamp) {
// The pending request is still valid, we can safely reuse its result
return pending.request;
}
else {
// Request 1 for module A (pending.timestamp)
// Invalidate module A (module.lastInvalidationTimestamp)
// Request 2 for module A (timestamp)
// First request has been invalidated, abort it to clear the cache,
// then perform a new doTransform.
pending.abort();
return transformRequest(url, server, options);
}
});
}
// 如果不存在就解析、编译模块
const request = doTransform(url, server, options, timestamp);
...
return request;
}
进入 transformRequest,先生成 cacheKey,如果是 ssr 或 html,就在 url 前面加上对应的前缀。通过 server._pendingRequests 判断当前 url 是否还在请求中,如果存在就调用moduleGraph.getModuleByUrl,通过url获取模块;不存在就调用 doTransform 做转换,并将请求缓存到 _pendingRequests 中,最后请求完成后(不管请求成功还是失败)都会从 _pendingRequests 删除。接下来分析一下 doTransform
doTransform函数
async function doTransform(url, server, options, timestamp) {
...
// 获取绝对路径,例如url是'/@vite/client'
// 返回'D:/document/vue/ANALYSIS/vite/packages/vite/dist/client/client.mjs'
const id = (await pluginContainer.resolveId(url, undefined, { ssr }))?.id || url;
// 调用loadAndTransform方法
const result = loadAndTransform(id, url, server, options, timestamp);
getDepsOptimizer(config, ssr)?.delayDepsOptimizerUntil(id, () => result);
return result;
}
async function loadAndTransform(id, url, server, options, timestamp) {
...
// 调用全部插件的load方法,返回一个code和map的对象或者code字符串
const loadResult = await pluginContainer.load(id, { ssr });
if (loadResult == null) {
...
}
else {
if (isObject(loadResult)) {
code = loadResult.code;
map = loadResult.map;
}
else {
code = loadResult;
}
}
...
// 确保模块在模块图中正常加载
const mod = await moduleGraph.ensureEntryFromUrl(url, ssr);
// 确保模块文件被文件监听器监听
ensureWatchedFile(watcher, mod.file, root);
// 核心核心核心!调用转换钩子
const transformResult = await pluginContainer.transform(code, id, {
inMap: map,
ssr
});
// 省略 debug、sourcemap、ssr 的逻辑
// 返回最终结果
return result;
}
pluginContainer.transform方法返回结果格式如下
总结
在 dev 服务器启动完,我们在浏览器输入地址后,浏览器根据import发起请求,请求会经过一系列的开发服务器中间件,其中最核心的中间件就是transformMiddleware,经过以上分析我们对这个中间件流程做总结,大体如下