最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vite-babysitter 像月嫂?保姆?照顾孩子一般为你讲解Vite源码。

    正文概述 掘金(Kev1nzh)   2021-06-02   684

    前言

    该项目如名,像月嫂?保姆?照顾孩子一般为你讲解Vite源码。

    • NPM 依赖解析和预构建: 全面提升页面重载速度和强缓存依赖。

    • Plugins 插件:可以利用 Rollup 插件的强大生态系统,同时根据需要也能够扩展开发服务器和 SSR 功能。

    • 动态模块热重载(HMR):Vite 提供了一套原生 ESM 的 HMR API。 具有 HMR 功能的框架可以利用该 API 提供即时、准确的更新,而无需重新加载页面或删除应用程序状态。

    查看线上文档体验更佳 查看文档 Powered by dumi

    看完有帮助的可以进入github给我一个?小星星 谢谢!

    NPM 依赖解析和预构建

    目录

    1. 代码入口
    2. 预构建对象和前期准备
    3. 构建和插件
    4. 最后

    1. 代码入口

    在 cli.ts 文件中,接收命令行的运行参数。

    // 命令行输入命令启动vite
    npm run dev
    // 根据package调用vite并获取命令参数 如--force build...
    vite xxxx xxx xxx
    

    vite 运行的第一步,获取命令参数,最后创建 server 并运行 listen 函数。

    //cli.ts
    
    .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
            const { createServer } = await import('./server')
            try {
                    const server = await createServer({
                    ...
                    })
                    await server.listen()
            } catch (e) {
                    ...
            }
    })
    

    listen 函数中,runOptimize 函数就是预构建的核心代码。

    // server/index.ts => listen
    if (!middlewareMode && httpServer) {
      // overwrite listen to run optimizer before server start
      const listen = httpServer.listen.bind(httpServer);
      httpServer.listen = (async (port: number, ...args: any[]) => {
        try {
          await container.buildStart({});
          await runOptimize();
        } catch (e) {
          httpServer.emit('error', e);
          return;
        }
        return listen(port, ...args);
      }) as any;
      ...
    } else {
      await container.buildStart({});
      await runOptimize();
    }
    
    // server/index.ts
    import { DepOptimizationMetadata, optimizeDeps } from '../optimizer'
    
    const runOptimize = async () => {
      if (config.cacheDir) {
        server._isRunningOptimizer = true;
        try {
          server._optimizeDepsMetadata = await optimizeDeps(config);
        } finally {
          server._isRunningOptimizer = false;
        }
        server._registerMissingImport = createMissingImporterRegisterFn(server);
      }
    };
    
    // server/index.ts
    import { DepOptimizationMetadata, optimizeDeps } from '../optimizer'
    
    const runOptimize = async () => {
      if (config.cacheDir) {
        server._isRunningOptimizer = true;
        try {
          server._optimizeDepsMetadata = await optimizeDeps(config);
        } finally {
          server._isRunningOptimizer = false;
        }
        server._registerMissingImport = createMissingImporterRegisterFn(server);
      }
    };
    

    入口代码很简单,获取了vite命令行参数后,创建内部server,触发各个功能的构建。

    接下来进入详解optimizeDeps的章节。

    预构建对象和前期准备

    首先获取预缓存(metadata.json)的路径,以及预构建的hash值,以便后续比对。

    这个json文件为vite处理后导出的数据信息,当此文件存在时,会比对hash值,如果相同就会直接读取此文件中的依赖。

    // /optimizer.ts
    async function optimizeDeps(
      config: ResolvedConfig,
      force = config.server.force,
      asCommand = false,
      newDeps?: Record<string, string>,
    ) {
      const { root, logger, cacheDir } = config
       // 这边第三个args为 asCommand, 是否是命令行运行的
       // 为了讲述的流畅性,在上一章节代码入口没有提到, 在vite --force 后,会直接运行optimizeDeps函数,因此需要区分log的输出方式
       // vite --force    =>    await optimizeDeps(config, options.force, true)
      const log = asCommand ? logger.info : debug
    
      if (!cacheDir) {
        log(`No cache directory. Skipping.`)
        return null
    
      //这边首先获取 预构建模块路径
      const dataPath = path.join(cacheDir, '_metadata.json'); //预缓存路径
      // /.../my-vue-app/node_modules/.vite/_metadata.json
      const mainHash = getDepHash(root, config);
      // 创建一个data的对象,后面会用到
      const data: DepOptimizationMetadata = {
        hash: mainHash,
        browserHash: mainHash,
        optimized: {},
      };
    

    如何获取hash值?

    首先获取了预构建模块的路径,默认情况为 node_modules/.vite。

    以下为 metadata.json 的数据结构, 后续会说到。

    // node_modules/.vite/_metadata.json
    {
      "hash": "9a4fa980",
      "browserHash": "6f00d484",
      "optimized": {
        "vue": {
          "file": "/.../my-vue-app/node_modules/.vite/vue.js",
          "src": "/.../my-vue-app/node_modules/vue/dist/vue.runtime.esm-bundler.js",
          "needsInterop": false
        },
        "axios": {
          "file": "/.../new/my-vue-app/node_modules/.vite/axios.js",
          "src": "/.../new/my-vue-app/node_modules/axios/index.js",
          "needsInterop": true
        }
      }
    }
    

    接着我们看 getDepHash 函数。 官方文档中描述,Vite 在预构建之前,根据以下源来确定是否要重新运行预构建。

    • package.json 中的 dependencies 列表
    • 包管理器的 lockfile,例如 package-lock.json, yarn.lock,或者 pnpm-lock.yaml
    • 可能在 vite.config.js 相关字段中配置过的

    以下代码中,变量 lockfileFormats 就是包管理器的locakfile。

    // /optimizer.ts 
    const lockfileFormats = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
    
    // /optimizer.ts => getDepHash
    let cachedHash: string | undefined;
    
    function getDepHash(root: string, config: ResolvedConfig): string {
      if (cachedHash) {
        return cachedHash;
      }
      let content = lookupFile(root, lockfileFormats) || ''; //往下滑会有lookupFile函数的解释。
      // 这边已经获取了所有local file array 内的文件内容
    
      // also take config into account
      // only a subset of config options that can affect dep optimization
    
      content += JSON.stringify(
        {
          mode: config.mode,
          root: config.root,
          resolve: config.resolve,
          assetsInclude: config.assetsInclude,
          plugins: config.plugins.map((p) => p.name),
          optimizeDeps: {
            include: config.optimizeDeps?.include, // null
            exclude: config.optimizeDeps?.exclude, //null
          },
        },
        (_, value) => {
          if (typeof value === 'function' || value instanceof RegExp) {
            return value.toString();
          }
          return value;
        },
      );
      //这里不说了  最终返回 "9a4fa980" 八位数hash值。
      return createHash('sha256').update(content).digest('hex').substr(0, 8);
    }
    
    // /optimizer.ts => lookupFile
    function lookupFile(
      dir: string,
      formats: string[],
      pathOnly = false,
    ): string | undefined {
      for (const format of formats) {
        const fullPath = path.join(dir, format); //获取root + format路径
        // 路径对象是否存在 并且是文件
        // pathOnly 为true就只返回路径,不然就都默认返回utf-8的文件内容
        if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
          return pathOnly ? fullPath : fs.readFileSync(fullPath, 'utf-8');
        }
      }
      const parentDir = path.dirname(dir);
      if (parentDir !== dir) {
        return lookupFile(parentDir, formats, pathOnly);
      }
    }
    

    是否强制优化并处理.vite 文件夹

    获取了预构建的 hash 值后,让我退回到 optimizeDeps 函数中,继续往下看。

    通过参数 force 来判断是否需要强制优化,如果不需要那就对比老 hash 值,如果相等就返回老的 metadata.json 文件内容。

    最后处理.vite文件夹,为后续做准备。

    // /optimizer.ts
    ...
    const data: DepOptimizationMetadata = {
        hash: mainHash, //"9a4fa980"
        browserHash: mainHash, //"9a4fa980"
        optimized: {},
      };
    
    
    // 是否强制预先优化 不管是否已经更改。
    // force = config.server.force 来源于cli.ts,获取命令行参数中是否有 --force
    if (!force) {
      let prevData;
      try {
        // 尝试解析已经存在的metadata数据, 获取/.vite/metadata.json中的内容
        prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
      } catch (e) {}
      // hash is consistent, no need to re-bundle
      // 如果预dep数据的hash相同,那就直接跳过,如果需要覆盖就使用 --force
      if (prevData && prevData.hash === data.hash) {
        log('Hash is consistent. Skipping. Use --force to override.');
        return prevData;
      }
    }
    //如果 node_modules/.vite 存在,那就清空。
    if (fs.existsSync(cacheDir)) {
      emptyDir(cacheDir);
    } else {
      // 要不然就创建文件夹, 并且recursive:true 返回创建文件夹的路径
      fs.mkdirSync(cacheDir, { recursive: true });
    }
    

    获取需要编译依赖关系的模块路径

    解决.vite 文件夹后,我们跟着代码处理.vite 中的内容文件。

    这边创建了两个变量 deps 和 missing。

    deps: 需要处理依赖关系的路径对象。

    missing: 需要处理依赖关系但在 node_modules 中没有找到来源的数组对象。

    //deps
    {
      "vue": "/.../my-vue-app/node_modules/vue/dist/vue.runtime.esm-bundler.js",
      "axios": "/.../my-vue-app/node_modules/axios/index.js"
    }
    

    需要提前知道的是,newDeps 这个 args 参数区分了第一次编译和已启动后遇到新依赖关系导入重写运行的编译。

    // /optimizer.ts
    
    let deps: Record<string, string>, missing: Record<string, string>;
    // 在服务器已经启动之后,如果遇到一个新的依赖关系导入,
    // 而这个依赖关系还没有在缓存中,Vite 将重新运行依赖构建进程并重新加载页面。
    // 如上官方文档所述,最终会得出deps 和missing
    if (!newDeps) {
      // scanImports 这里就不展开了,他的作用就是获取导入源,用正则检测后,使用esbuild编译所有的入口依赖(entries)
      ({ deps, missing } = await scanImports(config));
    } else {
      deps = newDeps;
      missing = {};
    }
    // 重写更新了浏览器的哈希
    // update browser hash
    data.browserHash = createHash('sha256')
      .update(data.hash + JSON.stringify(deps))
      .digest('hex')
      .substr(0, 8);
    

    没有找到来源的模块处理(missing)

    下面代码很简单,处理在 node_modules 中没有找到来源的模块。

    // /optimizer.ts
    
    // missing是一个储存需要处理依赖关系但在 node_modules 中没有找到来源的数组对象,如果有的话直接error提醒一波。
    const missingIds = Object.keys(missing);
    if (missingIds.length) {
      throw new Error(
        `The following dependencies are imported but could not be resolved:\n\n  ${missingIds
          .map(
            (id) =>
              `${chalk.cyan(id)} ${chalk.white.dim(
                `(imported by ${missing[id]})`,
              )}`,
          )
          .join(`\n  `)}\n\nAre they installed?`,
      );
    }
    

    获取并导入 自定义的强制预构建(include)

    接着处理在 vite.config.js 中 optimizeDeps.include。

    如官方文档 API 所述,

    optimizeDeps.include: 默认情况下,不在 node_modules 中的,链接的包不会被预构建。使用此选项可强制预构建链接的包

    // /optimizer.ts
    
    //config中是否有需要强制构建的依赖项, 处理后再deps中加入
    const include = config.optimizeDeps?.include;
    if (include) {
      const resolve = config.createResolver({ asSrc: false });
      for (const id of include) {
        if (!deps[id]) {
          const entry = await resolve(id);
          if (entry) {
            deps[id] = entry;
          } else {
            throw new Error(
              `Failed to resolve force included dependency: ${chalk.cyan(id)}`,
            );
          }
        }
      }
    }
    

    命令行打印需要构建模块的信息

    // /optimizer.ts
    
    const qualifiedIds = Object.keys(deps);
    //不用说很简单,没有需要依赖的dep就跳过
    if (!qualifiedIds.length) {
      writeFile(dataPath, JSON.stringify(data, null, 2));
      log(`No dependencies to bundle. Skipping.\n\n\n`);
      return data;
    }
    
    // 这里也不用解释太多,基本上就是打印出信息的逻辑,然后绿色高亮告诉你要预缓存巴拉巴拉
    const total = qualifiedIds.length;
    const maxListed = 5;
    const listed = Math.min(total, maxListed);
    const extra = Math.max(0, total - maxListed);
    const depsString = chalk.yellow(
      qualifiedIds.slice(0, listed).join(`\n  `) +
        (extra > 0 ? `\n  (...and ${extra} more)` : ``),
    );
    if (!asCommand) {
      if (!newDeps) {
        // This is auto run on server start - let the user know that we are
        // pre-optimizing deps
        logger.info(
          chalk.greenBright(`Pre-bundling dependencies:\n  ${depsString}`),
        );
        logger.info(
          `(this will be run only when your dependencies or config have changed)`,
        );
      }
    } else {
      logger.info(chalk.greenBright(`Optimizing dependencies:\n  ${depsString}`));
    }
    

    创建预构建对象

    使用es-module-lexer模块获取每个deps中的预构建模块文件,输出引入和导出的数据并保存。

    // /optimizer.ts
    
    import { ImportSpecifier, init, parse } from 'es-module-lexer';
    
    // esbuild generates nested directory output with lowest common ancestor base
    // this is unpredictable and makes it difficult to analyze entry / output
    // mapping. So what we do here is:
    // 1. flatten all ids to eliminate slash
    // 2. in the plugin, read the entry ourselves as virtual files to retain the
    //    path.
    const flatIdDeps: Record<string, string> = {};
    const idToExports: Record<string, ExportsData> = {};
    const flatIdToExports: Record<string, ExportsData> = {};
    // 运行es-module-lexer的初始化函数,后续会用到
    await init;
    
    for (const id in deps) {
      // 替换id中的斜杠变成下划线 node/abc => node_abc
      const flatId = flattenId(id);
      flatIdDeps[flatId] = deps[id];
      // 获取每个依赖源的文件内容
      //{ vue: '/.../my-vue-app/node_modules/vue/dist/vue.runtime.esm-bundler.js',
      // 'element-plus': '/.../my-vue-app/node_modules/element-plus/lib/index.esm.js',
      //  axios: '/.../my-vue-app/node_modules/axios/index.js' }
      const entryContent = fs.readFileSync(deps[id], 'utf-8');
      // parse出自es-module-lexer,这个包是一个js模块语法词法分析器,体积非常小
      // 解析出后的ExportsData 是一个数组,[0]是imports, [1]是exports
      const exportsData = parse(entryContent) as ExportsData;
    
      /*
        ss/se => statement start/end 缩写, {number} import的开始和结束index
        这里以vue举例,parse返回的值 =>  ss = 0 se = 60
        entryContent.slice(0, 60) => "import { initCustomFormatter, warn } from '@vue/runtime-dom'"
        entryContent.slice(62, 94) => "export * from '@vue/runtime-dom"
        最后标注需要特殊处理的 export from
      */
      for (const { ss, se } of exportsData[0]) {
        const exp = entryContent.slice(ss, se);
        if (/export\s+\*\s+from/.test(exp)) {
          exportsData.hasReExports = true; //待定
        }
      }
      // 分别记录以id flatId的exportsData
      // exportsData数据太多这里就不贴了,总之里面包含每个构建模块中的import和export的数据。
      idToExports[id] = exportsData;
      flatIdToExports[flatId] = exportsData;
    
    }
    

    总结

    上述描述代码中,我们理一下当前的逻辑。

    1. 获取了预构建模块的内容(hash 值,优化对象等)。
    2. 获取包管理器的 lockfile 转换的 hash 值,判断是否需要重新运行预构建。
    3. 获取需要编译依赖关系的模块路径(deps)和需要编译但没找到来源的模块(missing)。
    4. 处理 missing 数组,打印 error 提示是否已安装来源。
    5. 获取 vite.config.js 中自定义强制预构建的模块路径(include),加入 deps 对象中。
    6. 命令行打印需要构建模块的信息。
    7. 创建预构建对象,获取预构建对象中的引入导出数据并记录。

    处理完各种琐事之后,我们获取了需要构建的 deps 对象,接下来进入下一章节来解析 deps 对象。

    3. 构建和插件

    此章节准备介绍构建和 vite 的自定义插件。

    构建(build)

    需要注意的几个参数:

    1. format设为esm,是 Vite 的目的之一,将所有的代码视为原生 ES 模块。

    2. splitting设为true,仅适用于 esm 输出下,拆分多个文件引入的模块至单独文件,浏览页面 a 时,加载了 axios,再进入页面 b 时,直接调用已经加载后的 axios,省去了再次请求 axios 的操作。

    3. plugins含有 Vite 插件esbuildDepPlugin: 下面会详细解释此插件。

    4. treeShaking设为ignore-annotations, 文档中提到的忽略无用的代码,以便减轻模块的体积。

    // /optimizer/index.ts
    
    // 最核心的地方,使用esBuild打包了
    const result = await build({
      entryPoints: Object.keys(flatIdDeps),
      bundle: true, //任何导入的依赖一起打包
      format: 'esm', // 符合vite 转换成esm
      external: config.optimizeDeps?.exclude, //不需要处理的模块
      logLevel: 'error', //日志级别,只显示错误
      //拆分代码,简单来说就是拆分入口内的共享import文件,在访问a页面时加载了axios,
      //进入了b页面直接使用a页面加载的axios省去了再次请求的过程。
      splitting: true,
      sourcemap: true, //这个不用多说哈
      outdir: cacheDir, //vite自定义的默认缓存文件夹, node_modules/.vite
      //修剪树枝? 默认删除无用的代码,ignore-annotations的话指忽略那些删掉会损坏包的无用代码
      treeShaking: 'ignore-annotations',
      metafile: true, // 生成meta json
      define, // 替换标识符
      plugins: [...plugins, esbuildDepPlugin(flatIdDeps, flatIdToExports, config)],
      ...esbuildOptions,
    });
    

    esbuild 插件

    了解 esbuild 的插件的可以直接跳过这节,此节简单解释了下插件的结构:

    (1) esbuild plugin 是一个包含namesetup的对象结构。 name为插件名,setup是一个接收build的函数。

    (2) 主要的逻辑在setup函数中,分别为build.onResolvebuild.onLoad

    build.onResolve: 此函数拦截相应的导入路径,修改路径并标记特定的命名空间。

    build.onLoad: 此函数接收并筛选所有标记命名空间为env-ns的传入项,告诉 esbuild 该如何处理。

    let envPlugin = {
      name: 'env',
      setup(build) {
        // 第一个参数为拦截规则。如下示例,用正则拦截了名为`env`的路径。
        // 第二个参数为函数,返回对象中包含路径(这里可以对路径修改并返回)和标记`env-ns`命名空间。
        build.onResolve({ filter: /^env$/ }, (args) => ({
          path: args.path,
          namespace: 'env-ns',
        }));
    
        // 第一个参数为接收命名空间为env-ns的路径并通过filter筛选。
        // 第二个参数为函数,告诉esbuild在env-ns命名空间中要返回json格式的环境变量。
        build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
          contents: JSON.stringify(process.env),
          loader: 'json',
        }));
      },
    };
    
    require('esbuild')
      .build({
        entryPoints: ['app.js'],
        bundle: true,
        outfile: 'out.js',
        plugins: [envPlugin],
      })
      .catch(() => process.exit(1));
    

    esbuildDepPlugin

    首先需要看下 Vite 插件的一些用到的函数:

    // /optimizer/esbuildDepPlugin.ts
    
    export function esbuildDepPlugin(
      qualified: Record<string, string>,
      exportsData: Record<string, ExportsData>,
      config: ResolvedConfig,
    ): Plugin;
    

    (1) 创建了两个解析器,分别对应 esmcommonjs

    // /optimizer/esbuildDepPlugin.ts
    
    // default resolver which prefers ESM
    const _resolve = config.createResolver({ asSrc: false });
    
    // cjs resolver that prefers Node
    const _resolveRequire = config.createResolver({
      asSrc: false,
      isRequire: true,
    });
    

    (2) 创建 resolve 函数,主要用来解决判断是什么类型的模块,并且返回相应的解析器结果。

    // /optimizer/esbuildDepPlugin.ts
    
    const resolve = (
      id: string,
      importer: string,
      kind: ImportKind,
      resolveDir?: string,
    ): Promise<string | undefined> => {
      let _importer;
      // explicit resolveDir - this is passed only during yarn pnp resolve for
      // entries
      // 传如果传入文件夹,那就获取绝对路径的文件夹路径
      if (resolveDir) {
        _importer = normalizePath(path.join(resolveDir, '*'));
      } else {
        // map importer ids to file paths for correct resolution
        /**
         * mporter是否在外部传入的flatIdDeps中,
         * {
         *  vue: '/Users/kev1nzh/Desktop/new/my-vue-app/node_modules/vue/dist/vue.runtime.esm-bundler.js',
         *  axios: '/Users/kev1nzh/Desktop/new/my-vue-app/node_modules/axios/index.js'
         * }
         * 如果在获取value的路径
         */
        _importer = importer in qualified ? qualified[importer] : importer;
      }
      //判断是否时以require开头,为了筛选出 kind为require-resolve, require-call的模块,调用resolveRequire
      const resolver = kind.startsWith('require') ? _resolveRequire : _resolve;
      // 返回解决完的路径,这个函数的代码后续会有章节详细讲
      return resolver(id, _importer);
    };
    

    (3) 创建resolveEntry函数,根据传入类型返回命名空间。

    function resolveEntry(id: string, isEntry: boolean, resolveDir: string) {
      const flatId = flattenId(id);
      if (flatId in qualified) {
        return isEntry
          ? {
              path: flatId,
              namespace: 'dep',
            }
          : {
              path: require.resolve(qualified[flatId], {
                paths: [resolveDir],
              }),
            };
      }
    }
    

    (4) Vite 的onResolve

    Vite 创建了两个onResolve, 一个处理 js 文件,一个处理非 js 类型的文件。

    处理非 js:

    // /optimizer/esbuildDepPlugin.ts
    
    // 这个onResolve为处理非js类型的文件
    
    // 非js类型的文件数组
    const externalTypes = [
      'css',
      'less',
      'sass',
      ...
    ];
    build.onResolve(
      {
        // 这边通过正则匹配出在externalTypes数组内格式的文件
        filter: new RegExp(`\\.(` + externalTypes.join('|') + `)(\\?.*)?$`),
      },
      async ({ path: id, importer, kind }) => {
        // importer {string} 要打包的导入模块路径
        // kind {string} 导入规则 | 'entry-point'| 'import-statement'| 'require-call'| 'dynamic-import'| 'require-resolve'| 'import-rule'| 'url-token'
        const resolved = await resolve(id, importer, kind);
        if (resolved) {
          // 返回标记特殊处理,并返回引入文件的路径
          return {
            path: resolved,
            external: true,
          };
        }
      },
    );
    

    处理 js 类型的文件:

    以下代码就是 Vite 最刺激的地方,我应该会新建一篇章节来解释这块代码。

    // /optimizer/esbuildDepPlugin.ts
    
    // 这个onResolve为处理js类型的文件
    
    build.onResolve(
      { filter: /^[\w@][^:]/ },
      async ({ path: id, importer, kind, resolveDir }) => {
        /**
          id:  vue
          importer:
          kind:  entry-point
    
          id:  @vue/runtime-dom
          importer:  /Users/kev1nzh/Desktop/new/my-vue-app/node_modules/vue/dist/vue.runtime.esm-bundler.js
          kind:  import-statement
    
          参数如上,vite把预打包的模块分为 入口模块和依赖模块,
          像axios vue之类的 我们在项目中import的模块,
          runtime-dom 这种模块则是在package-lock.json, 是项目中入口模块的依赖模块,
          然后经过以下代码来区分并处理。
        */
        const isEntry = !importer;
        // ensure esbuild uses our resolved entries
        let entry;
        // if this is an entry, return entry namespace resolve result
        // 如果他是入口,就返回名为dep的命名空间来做接下来操作
        if ((entry = resolveEntry(id, isEntry, resolveDir))) return entry;
    
        // check if this is aliased to an entry - also return entry namespace
        const aliased = await _resolve(id, undefined, true);
        if (aliased && (entry = resolveEntry(aliased, isEntry, resolveDir))) {
          return entry;
        }
    
        // use vite's own resolver
        // ok这里开始处理依赖模块的流程,这边resolve
        const resolved = await resolve(id, importer, kind);
        if (resolved) {
          // vite自定义的id const browserExternalId = '__vite-browser-external'
          // 返回命名空间和id,因为浏览器兼容问题,无法处理的忽略模块
          if (resolved.startsWith(browserExternalId)) {
            //返回给browser-external命名空间处理并返回id
            return {
              path: id,
              namespace: 'browser-external',
            };
          }
          // 是否是非js或者外部文件,和上一个onResolve一样返回处理
          if (isExternalUrl(resolved)) {
            return {
              path: resolved,
              external: true,
            };
          }
          return {
            path: path.resolve(resolved),
          };
        }
      },
    );
    

    (5) Vite 的onLoad

    dep命名空间处理,下面代码有点复杂,简单说下逻辑。

    第一步,获取每个入口模块的引入路径,例如axiosentryFile/.../my-vue-app/node_modules/axios/index.js,

    转换成路径relativePath并添加前缀node_modules/axios/index.js

    第二步,根据exportsData(之前 parse 后返回出的引入和导出的数据)来判断commonjs、default、export from类型,

    最后转换成contents => export default require("./node_modules/axios/index.js")

    第三步,根据入口模块的路径获取后缀ext

    最后返回对象。

    /**
     * loader {string} 告诉esbuild要解析成js/css/....
     * resolveDir {string} 模块导入路径
     * contents: {string} 加载内容
     */
    return {
      loader: ext as Loader,
      contents,
      resolveDir: root,
    };
    
    // 获取项目的路径
    const root = path.resolve(config.root);
    build.onLoad({ filter: /.*/, namespace: 'dep' }, ({ path: id }) => {
      // 入口文件 vue => /.../my-vue-app/node_modules/vue/dist/vue.runtime.esm-bundler.js
      const entryFile = qualified[id];
      // 获取原始路径
      let relativePath = normalizePath(path.relative(root, entryFile));
      // 这边来处理 .abc.js => ./abc.js
      if (!relativePath.startsWith('.')) {
        relativePath = `./${relativePath}`;
      }
    
      let contents = '';
      const data = exportsData[id];
      const [imports, exports] = data;
      // 下面都是处理不同模块的流程
      if (!imports.length && !exports.length) {
        // cjs
        // export default require("./node_modules/axios/index.js");
        contents += `export default require("${relativePath}");`;
      } else {
        if (exports.includes('default')) {
          // default
          // import d from "./node_modules/element-plus/lib/index.esm.js";export default d;
          contents += `import d from "${relativePath}";export default d;`;
        }
        if (data.hasReExports || exports.length > 1 || exports[0] !== 'default') {
          // hasReExports
          // export * from "./node_modules/vue/dist/vue.runtime.esm-bundler.js"
          contents += `\nexport * from "${relativePath}"`;
        }
      }
      // 获取入口文件的后缀
      let ext = path.extname(entryFile).slice(1);
      if (ext === 'mjs') ext = 'js';
      /**
       * loader {string} 告诉esbuild要解析成js/css/....
       * resolveDir {string} 模块导入路径
       * contents: {string} 加载内容
       *
       * 以下是一个处理vue runtime-dom的例子
       * {
       *  ext: 'js',
       *  contents: "export * from "./node_modules/vue/dist/vue.runtime.esm-bundler.js",
       *  resolveDir: '..../node_modules/vue/dist'
       * }
       */
      return {
        loader: ext as Loader,
        contents,
        resolveDir: root,
      };
    });
    

    总结

    1. 上一章节预构建对象和前期准备中获取deps对象后,调用esbuild的打包功能。

    2. 传入Vite自定义的插件中,以文件类型分类。

    3. 告诉 esbuild 分为入口模块和依赖模块并处理,最终打包文件写入至/node_modules/.vite文件夹中。

    4.最后

    所有依赖模块构建完毕后写入/node_modules/.vite文件中,如若依赖项新增或改变,则会重写构建.vite。每次启动项目时,如果有预构建文件,可以直接启动,不需要每次重写打包依赖项。

    ECMA Script Modules(esm), 虽然 2021 年了,很多前端都已经在用最新的技术和代码来做项目,但是还有很多很多很多非常好用的模块都是好几年前创建的,那些模块导出机制五花八门,由Vite统一转换成 esm 的方式,只提供源码,让浏览器接管了打包这一服务。当页面需要某个模块时,Vite 只要转换并返回 esm 方式的源码就行了。

    看完本章节有收获的朋友,可以去 github 点个赞,后续还有相应的源码解析或分享,谢谢。


    起源地下载网 » Vite-babysitter 像月嫂?保姆?照顾孩子一般为你讲解Vite源码。

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元