前言
上一章我们讲到了vite 开发模式下,启动到打开浏览器的过程,要想编写的代码能够正常的访问,vite 期间做了很多事情,本章主要讲解vite中间件,下面我们来介绍vite 中比较重要的 几个中间件。
1. 目录
下面这段代码为 createServer 过程中,vite 所使用的一些中间件,vite 使用的 connect 来作为开发
const middlewares = connect() as Connect.Server
// Internal middlewares ------------------------------------------------------
// request timer
if (process.env.DEBUG) {
middlewares.use(timeMiddleware(root))
}
// cors (enabled by default)
const { cors } = serverConfig
if (cors !== false) {
middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
}
middlewares.use(cachedTransformMiddleware(server))
// proxy
const { proxy } = serverConfig
if (proxy) {
const middlewareServer =
(isObject(middlewareMode) ? middlewareMode.server : null) || httpServer
middlewares.use(proxyMiddleware(middlewareServer, proxy, config))
}
// base
if (config.base !== '/') {
middlewares.use(baseMiddleware(config.rawBase, !!middlewareMode))
}
// open in editor support
middlewares.use('/__open-in-editor', launchEditorMiddleware())
// ping request handler
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
middlewares.use(function viteHMRPingMiddleware(req, res, next) {
if (req.headers['accept'] === 'text/x-vite-ping') {
res.writeHead(204).end()
} else {
next()
}
})
// serve static files under /public
// this applies before the transform middleware so that these files are served
// as-is without transforms.
if (publicDir) {
middlewares.use(servePublicMiddleware(server, publicFiles))
}
// main transform middleware
middlewares.use(transformMiddleware(server))
// serve static files
middlewares.use(serveRawFsMiddleware(server))
middlewares.use(serveStaticMiddleware(server))
// html fallback
if (config.appType === 'spa' || config.appType === 'mpa') {
middlewares.use(
htmlFallbackMiddleware(
root,
config.appType === 'spa',
getFsUtils(config),
),
)
}
// run post config hooks
// This is applied before the html middleware so that user middleware can
// serve custom content instead of index.html.
postHooks.forEach((fn) => fn && fn())
if (config.appType === 'spa' || config.appType === 'mpa') {
// transform index.html
middlewares.use(indexHtmlMiddleware(root, server))
// handle 404s
middlewares.use(notFoundMiddleware())
}
// error handler
middlewares.use(errorMiddleware(server, !!middlewareMode))
vite 用到了以下中间件:
- timeMiddleware
- corsMiddleware
- cachedTransformMiddleware
- proxyMiddleware
- baseMiddleware
- launchEditorMiddleware
- viteHMRPingMiddleware
- servePublicMiddleware
- transformMiddleware
- serveRawFsMiddleware
- serveStaticMiddleware
- htmlFallbackMiddleware
- indexHtmlMiddleware
- notFoundMiddleware
cachedTransformMiddleware 缓存转换中间件,处理缓存的转换,以提高性能,这里先不讲,放到预构建依赖那一章讲解
本文主要讲解:
- servePublicMiddleware 静态文件服务
- transformMiddleware 文件转换中间件
- serveRawFsMiddleware 静态文件服务
- serveStaticMiddleware 静态文件服务
- htmlFallbackMiddleware html 回退中间件
- indexHtmlMiddleware index.html 转换中间件
2. connect
connect 是一个基于 Node.js 的中间件框架,常用于创建 HTTP 服务器。Vite 使用 connect 作为其开发服务器的基础,用来处理请求和响应
connect 的功能和作用:
- 中间件机制:connect 提供了一种机制,可以将一系列中间件函数按顺序组合起来处理 HTTP 请求。每个中间件函数可以处理请求、响应或者将控制权交给下一个中间件。
- 简洁的 API:connect 提供了一组简洁的 API,用于添加、删除和处理中间件。
- 兼容性强:connect 与 express 等流行的 Node.js 框架兼容,可以与多种中间件库一起使用。
3. servePublicMiddleware
这个用于处理 Vite 开发服务器上公共目录的中间件
出现在vite/src/node/server/index.ts 中的_createServer中
// 在 /public 目录下提供静态文件服务
// 这适用于转换中间件之前,这样这些文件就可以按原样提供而不需要转换。
if (publicDir) {
middlewares.use(servePublicMiddleware(server, publicFiles));
}
这里使用sirv来提供静态文件服务。源码位置server/middlewares/static.ts
export function servePublicMiddleware(
server: ViteDevServer,
publicFiles?: Set<string>
): Connect.NextHandleFunction {
const dir = server.config.publicDir;
const serve = sirv(
dir,
sirvOptions({
getHeaders: () => server.config.server.headers,
})
);
// 用于将请求的 URL 转换为文件路径
const toFilePath = (url: string) => {
let filePath = cleanUrl(url);
// 如果 URL 包含百分号编码(%),尝试解码它
if (filePath.indexOf("%") !== -1) {
try {
filePath = decodeURI(filePath);
} catch (err) {
/* malform uri */
}
}
return normalizePath(filePath);
};
return function viteServePublicMiddleware(req, res, next) {
// 这是实际的中间件函数,它被返回并用于处理请求
/**
* 为了避免' existsSync '对每个请求的性能影响,我们检查内存中已知的公共文件集。
* 该集合在重启时更新。也要跳过导入请求和内部请求' /@fs/ /@vite-client '等…
*/
if (
// 检查 publicFiles 集合是否存在,并且请求的文件路径是否在该集合中。如果不在集合中,则调用 next() 跳过此中间件
(publicFiles && !publicFiles.has(toFilePath(req.url!))) ||
// 检查请求是否为导入请求(isImportRequest)
isImportRequest(req.url!) ||
// 是否为内部请求
isInternalRequest(req.url!)
) {
// 如果是,则调用 next() 跳过此中间件
return next();
}
// 调用 serve 函数处理请求,提供 publicDir 目录中的静态文件
serve(req, res, next);
};
}
sirv 是一个用于提供静态文件服务器的库,支持多种配置和优化,适用于开发和生产环境
- 高性能:
sirv经过优化,可以快速响应对静态文件的请求。 - 易于使用:只需要几行代码就可以启动一个静态文件服务器。
- 支持缓存控制:可以配置缓存策略,以提高静态资源的加载速度。
- gzip 和 brotli 压缩:支持对静态文件进行压缩,以减少传输的文件大小。
4. serveRawFsMiddleware
这个用于处理特定路径前缀的请求,并从文件系统的根目录提供这些文件。 这在某些情况下很有用,比如在链接的 monorepo 项目中,需要访问根目录之外的文件
import /@fs/..../package/vite/dist/client/env.mjs 这种导入请求就是在这里做的处理
export function serveRawFsMiddleware(
server: ViteDevServer
): Connect.NextHandleFunction {
// sirv 是一个静态文件服务器中间件
// "/" 表示从根目录开始提供服务
const serveFromRoot = sirv(
"/",
sirvOptions({ getHeaders: () => server.config.server.headers })
);
// 保留命名函数。这个名字可以通过“debug =connect:dispatcher…”在调试日志中看到。
// 返回中间件函数
return function viteServeRawFsMiddleware(req, res, next) {
// 这是实际的中间件函数,处理传入的请求
// 解析请求的 URL,将双斜杠替换为单斜杠,避免路径错误
const url = new URL(req.url!.replace(/^\/{2,}/, "/"), "http://example.com");
/**
* 在某些情况下(例如链接的单节点),根目录之外的文件将引用同样不在服务根目录中的资产。
* 在这种情况下,路径被重写为' /@fs/ '前缀路径,并且必须通过基于fs根的搜索来提供。
*/
// 检查 URL 路径是否以 FS_PREFIX 开头,这是一个特殊前缀,用于标识需要从文件系统根目录提供的文件
if (url.pathname.startsWith(FS_PREFIX)) {
// 解码 URL 路径
const pathname = decodeURI(url.pathname);
//限制' fs.allow '之外的文件
// 确保文件的路径在允许的范围内,如果不允许访问,则终止请求处理
if (
!ensureServingAccess(
slash(path.resolve(fsPathFromId(pathname))),
server,
res,
next
)
) {
return;
}
// 去掉前缀 FS_PREFIX
let newPathname = pathname.slice(FS_PREFIX.length);
// 处理 Windows 系统的路径,将驱动器号去掉,如 C: 或 D: 将 C:\path\to\file 变为 \path\to\file
if (isWindows) newPathname = newPathname.replace(/^[A-Z]:/i, "");
// 更新 URL 路径
url.pathname = encodeURI(newPathname);
/**
* 更新请求路径,使其仅包含路径和查询参数部分,而不包含协议和主机名
*
* url.href 返回整个 URL 字符串。例如,http://example.com/path/to/file
* url.origin 返回 URL 的协议和主机名部分。例如,http://example.com
*
* 通过slice 提取子字符串,起始位置为 url.origin.length,即从主机名之后的部分开始,
* 结果字符串为 /path/to/file,即仅包含路径和查询参数部分
*
* 假设初始 URL 为 http://example.com/@fs/C:/path to/file:
* newPathname = '/path to/file',去除了驱动器号
* url.pathname = encodeURI('/path to/file') 结果为 /path%20to/file,空格被编码为 %20
* 最终请求 URL 为 /@fs/path%20to/file,确保路径被正确编码和理解
*/
req.url = url.href.slice(url.origin.length); //以便后续中间件能够正确处理请求
// 提供文件
serveFromRoot(req, res, next);
} else {
// 如果 URL 路径不以 FS_PREFIX 开头,直接调用 next 处理下一个中间件
next();
}
// 这个中间件的作用是处理带有特定前缀的请求,通过文件系统根目录提供相应的文件
};
}
5. serveStaticMiddleware
这个用于在 Vite 开发服务器中提供静态文件服务,并对请求路径进行了一些处理和检查
export function serveStaticMiddleware(
server: ViteDevServer
): Connect.NextHandleFunction {
const dir = server.config.root;
// 提供静态文件服务
const serve = sirv(
dir,
sirvOptions({
getHeaders: () => server.config.server.headers,
})
);
// 保留命名函数。这个名字可以通过“debug =connect:dispatcher…”在调试日志中看到。
return function viteServeStaticMiddleware(req, res, next) {
// 这是实际的中间件函数,处理传入的请求
/**
* 只有当它不是HTML请求或以' / '结尾时才提供文件,
* 这样HTML请求可以通过我们的HTML中间件进行特殊处理,也可以跳过内部请求' /@fs/ /@vite-client '等
*
* HTML 请求通常需要经过特殊处理(例如注入脚本),因此需要跳过静态文件处理,交给专门处理 HTML 请求的中间件
* 而以 / 结尾的请求通常表示目录请求,需要进一步处理或重定向
* 内部请求是 Vite 自身使用的模块或功能,不应通过静态文件中间件处理,应该交给其他专门的中间件或处理逻辑
*/
// 去掉请求 URL 中的查询参数和哈希部分
const cleanedUrl = cleanUrl(req.url!);
if (
cleanedUrl[cleanedUrl.length - 1] === "/" ||
path.extname(cleanedUrl) === ".html" ||
isInternalRequest(req.url!)
) {
// 如果 URL 以 / 结尾,或者是一个 .html 文件,或者是内部请求,
// 则直接跳过静态文件处理,调用 next() 进入下一个中间件
return next();
}
// 将请求 URL 转换为标准 URL 对象,并解码路径名
const url = new URL(req.url!.replace(/^/{2,}/, "/"), "http://example.com");
const pathname = decodeURI(url.pathname);
// 对静态请求也应用别名
let redirectedPathname: string | undefined;
// 根据 Vite 配置中的别名规则,对请求路径进行替换,支持字符串匹配和正则表达式匹配
for (const { find, replacement } of server.config.resolve.alias) {
const matches =
typeof find === "string"
? pathname.startsWith(find)
: find.test(pathname);
if (matches) {
redirectedPathname = pathname.replace(find, replacement);
break;
}
}
if (redirectedPathname) {
// 检查它是否以 dir 结尾的斜杠形式开头 dir 是根目录
if (redirectedPathname.startsWith(withTrailingSlash(dir))) {
// 截取为不包含 dir 的路径
redirectedPathname = redirectedPathname.slice(dir.length);
}
}
// 解析完成的路径名
const resolvedPathname = redirectedPathname || pathname;
// 构建文件路径:将 dir 和去掉开头斜杠的 resolvedPathname 组合成完整的文件路径
let fileUrl = path.resolve(dir, removeLeadingSlash(resolvedPathname));
// 处理路径结尾斜杠:
if (
resolvedPathname[resolvedPathname.length - 1] === "/" &&
fileUrl[fileUrl.length - 1] !== "/"
) {
// 如果请求的路径以斜杠结尾,并且构建的 fileUrl 不以斜杠结尾,则给构建的路径添加斜杠
fileUrl = withTrailingSlash(fileUrl);
}
// 检查访问权限:检查请求的 fileUrl 是否可以被访问。如果无法访问,则返回
if (!ensureServingAccess(fileUrl, server, res, next)) {
return;
}
// 更新请求对象 req 的 url 属性为经过编码后 redirectedPathname
if (redirectedPathname) {
url.pathname = encodeURI(redirectedPathname);
req.url = url.href.slice(url.origin.length);
}
// 最终调用 serve 函数来处理静态文件的请求
serve(req, res, next);
};
}
6. htmlFallbackMiddleware
这个用于处理 Vite 项目中的 HTML 文件请求,其主要功能是在处理不同路径请求时,检查对应的 HTML 文件是否存在,并进行相应的路径重写。这个中间件很重要,经过这个中间件处理后,后一个中间件 indexHtmlMiddleware才能去处理根目录下面的index.html
// html 回退,为单页应用(SPA)或多页应用(MPA)提供 HTML 回退功能
if (config.appType === "spa" || config.appType === "mpa") {
middlewares.use(
htmlFallbackMiddleware(root, config.appType === "spa", getFsUtils(config))
);
}
export function htmlFallbackMiddleware(
root: string,
spaFallback: boolean,
fsUtils: FsUtils = commonFsUtils
): Connect.NextHandleFunction {
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteHtmlFallbackMiddleware(req, res, next) {
if (
// 该中间件只处理 GET 和 HEAD 请求,其他方法的请求会直接调用 next() 跳过此中间件。
(req.method !== "GET" && req.method !== "HEAD") ||
// 请求路径为 /favicon.ico 的请求会直接跳过此中间件
req.url === "/favicon.ico" ||
// 该中间件要求 Accept 头部包含 text/html 或 */*,否则请求会被跳过。
!(
req.headers.accept === undefined || // equivalent to `Accept: */*`
req.headers.accept === "" || // equivalent to `Accept: */*`
req.headers.accept.includes("text/html") ||
req.headers.accept.includes("*/*")
)
) {
return next();
}
// 对请求路径进行清理和解码,以便后续处理
const url = cleanUrl(req.url!);
const pathname = decodeURIComponent(url);
// .html文件不被serveStaticMiddleware处理,所以我们需要检查该文件是否存在
if (pathname.endsWith(".html")) {
const filePath = path.join(root, pathname);
if (fsUtils.existsSync(filePath)) {
// 存在则重写请求路径
debug?.(`Rewriting ${req.method} ${req.url} to ${url}`);
req.url = url;
return next();
}
}
// 尾斜杠应该检查是否有回退index.html
else if (pathname[pathname.length - 1] === "/") {
// 末尾为 "/" 的,统一添加 index.html
const filePath = path.join(root, pathname, "index.html");
if (fsUtils.existsSync(filePath)) {
// 存在的话,重写路径,这里访问"/" 目录的时候,会走到这里,从而添加index.html
// 被后面的中间件去处理
const newUrl = url + "index.html";
debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`);
req.url = newUrl;
return next();
}
}
// 非尾随斜杠应该检查是否有回退 .html
else {
// 非尾随斜杠的会添加.html 后缀,文件存在后重写后,调用next() 交给下一个中间件
const filePath = path.join(root, pathname + ".html");
if (fsUtils.existsSync(filePath)) {
const newUrl = url + ".html";
debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`);
req.url = newUrl;
return next();
}
}
// 以上的情况都不满足,启用单页面应用的回退处理,将所有未匹配的路径重写为 /index.html
if (spaFallback) {
debug?.(`Rewriting ${req.method} ${req.url} to /index.html`);
req.url = "/index.html";
}
next();
};
}
debug调试过程
当vite 启动完成,打开浏览器后,会访问根路径“/”,会进入到这个中间件,我们来看这个中间件的处理流程
我们可以看到,目前请求访问的是 /,这里会清理url,保证干净的url,以便后续的处理
//匹配 URL 中以 ? 或 # 开头的字符序列,包括 ? 或 # 本身及其后面的任意字符
const postfixRE = /[?#].*$/;
//用于清理 URL,移除其末尾的查询字符串和哈希部分。
export function cleanUrl(url: string): string {
return url.replace(postfixRE, "");
}
因为不是.html 结尾,所以会走到这一步,执行这一块的逻辑
经过这一块的逻辑处理后,我们会发现req.url 变成了 /index.html,这样的话,就会去访问项目根目录下面的index.html,现在知道为什么vite需要把index.html 放在根目录下面了吧,因为经过这个中间件的处理,会把这个html文件返回给浏览器,但是在返回给浏览器之前,还需要经过一个中间件的处理,那就是indexHtmlMiddleware 这个中间件
7. indexHtmlMiddleware
这个用于在 Vite 开发服务器或预览服务器上处理请求,特别是处理 .html 文件的请求。
//对于单页应用(SPA)或多页应用(MPA),提供 index.html 的转换和 404 错误处理。
if (config.appType === "spa" || config.appType === "mpa") {
// transform index.html
middlewares.use(indexHtmlMiddleware(root, server));
// handle 404s
middlewares.use(notFoundMiddleware());
}
export function indexHtmlMiddleware(
root: string,
server: ViteDevServer | PreviewServer
): Connect.NextHandleFunction {
// 判断服务器是否处于开发模式
const isDev = isDevServer(server);
// 获取文件系统工具,主要用于检查文件是否存在
const fsUtils = getFsUtils(server.config);
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return async function viteIndexHtmlMiddleware(req, res, next) {
if (res.writableEnded) {
// 如果响应已经结束,则直接调用 next 传递给下一个中间件
return next();
}
const url = req.url && cleanUrl(req.url);
// htmlFallbackMiddleware appends '.html' to URLs
// 这个中间件很关键,添加完 html 后缀后,可以进入到下面的处理,从而解析根目录下的index.html 并发送给客户端
if (url?.endsWith(".html") && req.headers["sec-fetch-dest"] !== "script") {
let filePath: string;
// 确定文件路径
if (isDev && url.startsWith(FS_PREFIX)) {
// 如果是FS_PREFIX开头的,则调用fsPathFromId 去除前缀
filePath = decodeURIComponent(fsPathFromId(url));
} else {
// 将 URL 解码并与 root 拼接来确定文件路径
filePath = path.join(root, decodeURIComponent(url));
}
// 检查文件是否存在并读取内容
if (fsUtils.existsSync(filePath)) {
// 根据模式的不同,选择的不同的请求头
const headers = isDev
? server.config.server.headers
: server.config.preview.headers;
try {
// 读取文件内容
let html = await fsp.readFile(filePath, "utf-8");
if (isDev) {
// 开发模式下 调用transformIndexHtml 转换html 内容
html = await server.transformIndexHtml(url, html, req.originalUrl);
}
// 通过 send 函数将 HTML 内容发送给客户端
return send(req, res, html, "html", { headers });
} catch (e) {
// 出错的话,调用next 传递给下一个中间件
return next(e);
}
}
}
next();
};
}
debug 调试
经过html 回退中间件处理后,req.url 变成了 /index.html,同时也来到了这个中间件处理,专门用于处理index.html
可以看到req.url = /index.html
进去下面逻辑进行处理,橙色框圈出来的地方很重要!在开发模式下,对html进行转换,然后返回给浏览器
// 将 URL 解码并与 root 拼接来确定文件路径
filePath = path.join(root, decodeURIComponent(url));
这样就将filePath 拼接为项目根目录下面的index.html 了
我们来看看 server.transformIndexHtml 这个函数到底做了什么,这个是vite 实例身上的一个方法。可以看到内部其实调用了devHtmlTransformFn,我们接着看
transformIndexHtml(url, html, originalUrl) {
return devHtmlTransformFn(server, url, html, originalUrl);
},
在创建server 的时候初始化,再来看createDevHtmlTransformFn
createDevHtmlTransformFn
在 Vite 中用于创建一个开发环境下的 HTML 转换函数,这个函数会根据配置的插件顺序和一些内置的转换钩子,对 HTML 内容进行一系列的处理和修改
export function createDevHtmlTransformFn(
config: ResolvedConfig
): (
server: ViteDevServer,
url: string,
html: string,
originalUrl?: string
) => Promise<string> {
// 解析 HTML 转换钩子
const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms(
config.plugins,
config.logger
);
// 定义一系列用于转换 HTML 的钩子
const transformHooks = [
// 在 HTML 中导入 map 之前的预处理钩子
preImportMapHook(config),
// 注入 CSP (Content Security Policy) nonce 元标签的钩子
injectCspNonceMetaTagHook(config),
// 用户插件的预处理钩子
...preHooks,
// 处理环境变量相关的钩子
htmlEnvHook(config),
// 开发环境下的 HTML 处理钩子
devHtmlHook,
// 用户插件的正常处理钩子
...normalHooks,
// 用户插件的后处理钩子
...postHooks,
// 注入 nonce 属性标签的钩子
injectNonceAttributeTagHook(config),
// 在 HTML 中导入 map 之后的后处理钩子
postImportMapHook(),
];
// 返回转换函数
return (
server: ViteDevServer,
url: string,
html: string,
originalUrl?: string
): Promise<string> => {
// 应用所有的转换钩子
return applyHtmlTransforms(html, transformHooks, {
path: url,
filename: getHtmlFilename(url, server),
server,
originalUrl,
});
};
}
applyHtmlTransforms
用于应用一系列 HTML 转换钩子来处理 HTML 内容,将html文件处理完成之后返回
export async function applyHtmlTransforms(
html: string,
hooks: IndexHtmlTransformHook[],
ctx: IndexHtmlTransformContext
): Promise<string> {
// 循环遍历每个钩子并应用转换
for (const hook of hooks) {
const res = await hook(html, ctx);
if (!res) {
continue;
}
if (typeof res === "string") {
// 如果钩子返回一个字符串,将其作为新的 html
html = res;
} else {
// 如果钩子返回的是一个对象或数组,进行进一步处理
let tags: HtmlTagDescriptor[];
if (Array.isArray(res)) {
// 如果返回值是数组,则认为它是包含标签的数组
tags = res;
} else {
// 如果返回值是对象,提取 html 和 tags 属
html = res.html || html;
tags = res.tags;
}
// 分类标签并注入到相应位置
let headTags: HtmlTagDescriptor[] | undefined;
let headPrependTags: HtmlTagDescriptor[] | undefined;
let bodyTags: HtmlTagDescriptor[] | undefined;
let bodyPrependTags: HtmlTagDescriptor[] | undefined;
for (const tag of tags) {
// 根据 injectTo 属性将标签分类存储到不同的数组中
switch (tag.injectTo) {
case "body":
(bodyTags ??= []).push(tag);
break;
case "body-prepend":
(bodyPrependTags ??= []).push(tag);
break;
case "head":
(headTags ??= []).push(tag);
break;
default:
(headPrependTags ??= []).push(tag);
}
}
// 用于检查和处理需要插入到 <head> 中的标签
headTagInsertCheck(
[...(headTags || []), ...(headPrependTags || [])],
ctx
);
// 用于将标签插入到 HTML 的相应位置
if (headPrependTags) html = injectToHead(html, headPrependTags, true);
if (headTags) html = injectToHead(html, headTags);
if (bodyPrependTags) html = injectToBody(html, bodyPrependTags, true);
if (bodyTags) html = injectToBody(html, bodyTags);
}
}
// 通过这个函数,Vite 可以灵活地处理 HTML 文件,注入脚本、样式等资源,
// 以实现插件系统和各种自定义功能。
return html;
}
经过这个钩子处理后,页面上能够正常的显现出index.html 文件的内容
但是我们发现红色框框里面的东西并没有生效,这是为什么呢?
眼尖的同学发现了index.html 多了一些东西 ,
<script type="module" src="/@vite/client"></script>,这是注入的热更新代码,是在devHtmlHook 这个钩子中执行的,还记得indexHtmlMiddleware 这个中间件的代码吗
就是通过这里注入的
这个以后讲热更新时候会再次提到,我们发现index.html 中引入了 /src/main.ts,所以就会有一个get请求,去请求相应的资源到浏览器
这里我们可以是这样导入的vue,这样浏览器是不认识的,所以会报错
这里会有两个错误,网站图标的错误请忽略,浏览器需要我们提供相对路径或者绝对路径,而不是像import { createApp} from vue 这种裸导入
上面的错误同样是因为注入了热更新的代码
要解决这个错误,涉及到几个方面:
- 预构建依赖
- 导入替换(importAnalysis 这个插件来完成的)
还记得我们本章需要讲解的几个中间件吗,这里还剩一个没有提到,就是用来处理这种的中间件
8. transformMiddleware
主要的文件转换中间件,这个中间件很重要,访问的文件都是通过这个中间件去做转换。 比如 /src/main.ts 中涉及到 import {createApp} from 'vue',就是通过这个中间件调用 transform 方法做转换,实际的转换发生在 importAnaysis 这个插件里面做的转换
之前的报错,都是通过这个中间件来做处理的
这里最主要的步骤就是通过调用 transformRequest 来对文件进行处理,并将处理完成的文件返回给浏览器
这里需要预构建依赖,通过 initDepsOptimizer 来完成,在创建vite 实例种的 initServer 方法种
还需要通过导入分析插件来完成代码的替换,resolvePlugins 这个函数在创建vite 实例的时候执行
导入分析插件,源码位置vite/src/node/plugins/importAnalysis.ts
主要就是在 transform 这个钩子做的处理,这里给大家看一下
这里需要的前置知识很多,所以这里中间件不作过多讲解,等完成后续的 预构建依赖和导入分析插件后,再来完成。给大家看看完成这些之后的代码会变成什么样子
import { createApp } from 'vue'
// 会变成
import { createApp } from '/node_modules/.vite/deps/vue.js?v=6cd8be50'
这样浏览器就能够正常寻找到文件,/node_modules/.vite/deps 就是存在依赖的地方,这个目录可以通过传入配置项更改,这里做了缓存。后面拼接?v=6cd8be50 是为了能够获取到最新的依赖版本
import '@vite/env'
// 变成
import "/@fs/C:/Users/.../packages/vite/dist/client/env.mjs";
可以发现这里的路径很奇怪,以@fs 开头的,还记得开头的中间件吗,没错就是 serverRawFsMiddleware,它就是专门用来处理@fs 开头的请求
结语
看到这里相信你对vite也有一个大概的了解,后面的文章,我们会慢慢揭开vite的面纱
源码系列文章: