美文网首页
2023.19 vite源码学习-Vite是怎么处理模块加载的(

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

作者: wo不是黄蓉 | 来源:发表于2023-04-21 20:42 被阅读0次
    vite源码学习-Vite是怎么处理模块加载的(二).png

    大家好,我是wo不是黄蓉,今年学习目标从源码共读开始,希望能跟着若川大佬学习源码的思路学到更多的东西。

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

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

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

    image.png image.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是怎么处理文件加载的,下节目标,自己能够实现一下。

    相关文章

      网友评论

          本文标题:2023.19 vite源码学习-Vite是怎么处理模块加载的(

          本文链接:https://www.haomeiwen.com/subject/etmjjdtx.html