2023.19 vite源码学习-Vite是怎么处理模块加载的(二)

347 阅读3分钟

大家好,我是wo不是黄蓉,今年学习目标从源码共读开始,希望能跟着若川大佬学习源码的思路学到更多的东西。有想法的同学也可以加我微信进行交流:hp1256003949

上节讲了怎么创建服务,并且引出了在哪儿处理文件转换和解析的。

这节接上次内容,来看下启动本地服务后,进入首页可以看到很多的请求,有些请求甚至不是我们写的文件,这些文件是从哪里加载的?

而且即便是我只引入了组件但是没有引用,为什么vite还要帮我加载哪些没有引用的组件呢?

引入了组件但是没有使用.png

浏览器加载的组件.png

server启动之后做了什么事情?

上次看到启动服务后,在浏览器打开http://127.0.0.1:5173/就会调用transformMiddleware方法,处理请求内容。在看transformMiddleware方法之前,我发现vite会先遍历整个项目的文件和文件夹,并对遍历到的文件进行标记,标记是否是想要的文件夹,是否是想要的文件等,目前还没看出来哪些文件会被标记


class ReaddirpStream extends Readable {
  constructor(options = {}) {
    this._maxDepth = opts.depth;
    this._wantsDir = [DIR_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE].includes(type);
    this._wantsFile = [FILE_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE].includes(type);
    this._wantsEverything = type === EVERYTHING_TYPE;
    this._root = sysPath$3.resolve(root);
    this._isDirent = ('Dirent' in fs$8) && !opts.alwaysStat;
    this._statsProp = this._isDirent ? 'dirent' : 'stats';
    this._rdOptions = { encoding: 'utf8', withFileTypes: this._isDirent };
​
    // Launch stream with one parent, the root dir.
    this.parents = [this._exploreDir(root, 1)];
    this.reading = false;
    this.parent = undefined;
  }
​
  async _read(batch) {
    if (this.reading) return;
    this.reading = true;
​
    try {
      while (!this.destroyed && batch > 0) {
        const { path, depth, files = [] } = this.parent || {};
​
        if (files.length > 0) {
          const slice = files.splice(0, batch).map(dirent => this._formatEntry(dirent, path));
          for (const entry of await Promise.all(slice)) {
            if (this.destroyed) return;
​
            const entryType = await this._getEntryType(entry);
            if (entryType === 'directory' && this._directoryFilter(entry)) {
              if (depth <= this._maxDepth) {
                this.parents.push(this._exploreDir(entry.fullPath, depth + 1));
              }
​
              if (this._wantsDir) {
                this.push(entry);
                batch--;
              }
            } else if ((entryType === 'file' || this._includeAsFile(entry)) && this._fileFilter(entry)) {
              if (this._wantsFile) {
                this.push(entry);
                batch--;
              }
            }
          }
        } else {
          const parent = this.parents.pop();
          if (!parent) {
            this.push(null);
            break;
          }
          this.parent = await parent;
          if (this.destroyed) return;
        }
      }
    } catch (error) {
      this.destroy(error);
    } finally {
      this.reading = false;
    }
  }
}

接下来看transformMiddleware关键部分代码

//忽略监听的文件
const knownIgnoreList = new Set(['/', '/favicon.ico']);
function transformMiddleware(server) {
    const { config: { root, logger }, moduleGraph, } = server;
    return async function viteTransformMiddleware(req, res, next) {
        //判断请求方法是否为get,或者请求的url不在监听列表就跳过,直接返回
        if (req.method !== 'GET' || knownIgnoreList.has(req.url)) {
            return next();
        }
        let url;
        try {
            url = decodeURI(removeTimestampQuery(req.url)).replace(NULL_BYTE_PLACEHOLDER, '\0');
        }
        catch (e) {
            return next(e);
        }
        //去掉查询参数
        const withoutQuery = cleanUrl(url);
        try {
            //获取到public文件夹的绝对路径
            const publicDir = normalizePath$3(server.config.publicDir);
            //获取根目录的绝对路径
            const rootDir = normalizePath$3(server.config.root);
            if (publicDir.startsWith(rootDir)) {
                 //...处理公共目录下文件访问地址
            }
            //判断是否是js、import、css或html请求
            if (isJSRequest(url) ||
                isImportRequest(url) ||
                isCSSRequest(url) ||
                isHTMLProxy(url)) {
                //处理css请求的文件
                if (isCSSRequest(url) &&
                    !isDirectRequest(url) &&
                    req.headers.accept?.includes('text/css')) {
                    url = injectQuery(url, 'direct');
                }
               
                // transformRequest,方法
                const result = await transformRequest(url, server, {
                    html: req.headers.accept?.includes('text/html'),
                });
                    //返回代码内容
                if (result) {
                    return send$1(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,
                    });
                }
            }
        }
        next();
    };
}

transformRequest方法核心代码

function transformRequest(url, server, options = {}) {
    const request = doTransform(url, server, options, timestamp);
    return request;
}

doTransform方法核心代码

async function doTransform(url, server, options, timestamp) {
    const result = loadAndTransform(id, url, server, options, timestamp);
    return result;
}

发现上面这两个方法并没有什么用,最终做转换的是loadAndTransform,这种编码的思路我们可以借鉴,每个函数做的工作都是比较明确的,有依赖关系的可以利用返回值进行处理

async function loadAndTransform(id, url, server, options, timestamp) {
    const { config, pluginContainer, moduleGraph, watcher } = server;
    const { root, logger } = config;
    const prettyUrl = isDebug$2 ? prettifyUrl(url, config.root) : '';
    const ssr = !!options.ssr;
    const file = cleanUrl(id);
    let code = null;
    let map = null;
    // load
    const loadStart = isDebug$2 ? performance.now() : 0;
    //这边传入的id就是要加载的文件的绝对路径
    const loadResult = await pluginContainer.load(id, { ssr });
    // ensure module in graph after successful load
    const mod = await moduleGraph.ensureEntryFromUrl(url, ssr);
    ensureWatchedFile(watcher, mod.file, root);
    // transform
    const transformStart = isDebug$2 ? performance.now() : 0;
    //pluginContainer-》const container = await createPluginContainer(config, moduleGraph, watcher);
    const transformResult = await pluginContainer.transform(code, id, {
        inMap: map,
        ssr,
    });
    const originalCode = code;
   
    const result = {
            code,
            map,
            etag: etag_1(code, { weak: true }),
        };
    return result;
}

createPluginContainer相关代码,关键代码result = await handler.call(ctx, code, id, { ssr });

但是不知道handler到底是个啥东西,看到后面发现是插件本身,插件本身暴露出来有一个transform方法,因此执行 await handler.call(ctx, code, id, { ssr })就是插件将代码转换过程,返回的result就是转换后的代码。


const container = {        
        async transform(code, id, options) {
            const ctx = new TransformContext(id, code, inMap);
            for (const plugin of getSortedPlugins('transform')) {
                try {
                    //这里的handler是什么?handler就是transform自己,tranform又是插件里面提供的一个函数,因此代码分析部分是在这里做的
                    result = await handler.call(ctx, code, id, { ssr });
                }
                catch (e) {
                    ctx.error(e);
                }
                if (!result)
                    continue;
                if (isObject$1(result)) {
                    if (result.code !== undefined) {
                        //获取请求文件中code部分
                        code = result.code;
                    }
                }
                else {
                    code = result;
                }
            }
            return {
                code,
                map: ctx._getCombinedSourcemap(),
            };
        }, 
}

从上面流程看下来,好像没有解析代码的相关的代码,那么是如何加载到不同文件呢?

transform钩子函数

回到createServer这些插件是从哪儿插入的?

答:resolveConfig还有resolvePlugins这两个地方对插件进行了处理。

  const container = await createPluginContainer(config, moduleGraph, watcher);

resolvePlugins能看到一些熟悉的插件htmlInlineProxyPlugin

async function resolvePlugins(config, prePlugins, normalPlugins, postPlugins) {
    return [
        htmlInlineProxyPlugin(config),
        cssPlugin(config),
        config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
        jsonPlugin({
            namedExports: true,
            ...config.json,
        }, isBuild),
        webWorkerPlugin(config),
        assetPlugin(config),
        ...normalPlugins,
        definePlugin(config),
        cssPostPlugin(config),
        isBuild && buildHtmlPlugin(config),
        assetImportMetaUrlPlugin(config),
        ...buildPlugins.pre,
        dynamicImportVarsPlugin(config),
        importGlobPlugin(config),
        ...postPlugins,
        ...buildPlugins.post,
        // internal server-only plugins are always applied after everything else
        ...(isBuild
            ? []
            : [clientInjectionsPlugin(config), importAnalysisPlugin(config)]),
    ].filter(Boolean);
}

最后在'vite:import-analysis'这个插件上找到了分析代码的模块,并且返回内容为


"import { createApp } from "/node_modules/.vite/deps/vue.js?v=c570bfa5"\nimport ElementPlus from "/node_modules/.vite/deps/element-plus.js?v=c570bfa5"\nimport "/node_modules/element-plus/dist/index.css"\nimport "/src/index.scss"\nimport App from "/src/App.vue"\nimport axios from "/node_modules/.vite/deps/axios.js?v=c570bfa5"\n//引入vue路由\nimport Router from "/src/router/index.ts"\n\nconst app = createApp(App)\n\n//vue3挂载全局组件\napp.config.globalProperties.$axios = axios\napp.use(Router)\napp.use(ElementPlus, { autoInsertSpace: true }).mount("#app")\n"

所以当你引用的内容是项目外的代码,vite会自动将加载node_modules中的代码,然后把这个文件再进行递归,最后翻译成浏览器可以识别的代码

翻译工作完成后开始执行加载操作load方法


        async load(id, options) {
            const ssr = options?.ssr;
            const ctx = new Context();
            ctx.ssr = !!ssr;
            for (const plugin of getSortedPlugins('load')) {
                if (!plugin.load)
                    continue;
                ctx._activePlugin = plugin;
                const handler = 'handler' in plugin.load ? plugin.load.handler : plugin.load;
                const result = await handler.call(ctx, id, { ssr });
                if (result != null) {
                    if (isObject$1(result)) {
                        updateModuleInfo(id, result);
                    }
                    return result;
                }
            }
            return null;
        },

执行加载方法时如果遇到还需要继续解析的文件,就还是回到loadAndTransform继续进行递归解析文件的操作,以此类推

这样一个文件的解析工作就完成了。

了解了怎么创建一个vite服务,并且vite是怎么处理文件加载的,下节目标,自己能够实现一下。