Vite-babysitter 像月嫂?保姆?照顾孩子一般为你讲解Vite源码。

前言

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

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

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

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

目前【依赖解析和预构建】章节已经施工完毕,插件和HMR的源码解析将在两周内上线。

Vite的版本为2.3.3。

查看线上文档体验更佳 查看文档 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 模块。

    CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。

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

    Code shared between multiple entry points is split off into a separate shared file that both entry points import. That way if the user first browses to one page and then to another page, they don’t have to download all of the JavaScript for the second page from scratch if the shared part has already been downloaded and cached by their browser.

    Code referenced through an asynchronous import() expression will be split off into a separate file and only loaded when that expression is evaluated. This allows you to improve the initial download time of your app by only downloading the code you need at startup, and then lazily downloading additional code if needed later.

  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 点个赞,后续还有相应的源码解析或分享,谢谢。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值