vite源码解析记录(一、本地启动 / 转换中间件)

339 阅读4分钟

前言

vite在本地启动的时候提供了一系列内部的中间件在开发服务中使用,其中最为核心的就是transformMiddleware

参考文章juejin.cn/post/708292…

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方法返回结果格式如下 Snipaste_2022-10-10_10-43-08.png

总结

在 dev 服务器启动完,我们在浏览器输入地址后,浏览器根据import发起请求,请求会经过一系列的开发服务器中间件,其中最核心的中间件就是transformMiddleware,经过以上分析我们对这个中间件流程做总结,大体如下

image.png