导入并导出目录下模块文件的方法&使用桶文件的后果

2025年07月25日 Tags: Js


假设现在有一个目录为 /test,该目录下有三个文件分别为 a.jsb.jsindex.js

// a.js
export const a = 1;
// b.js
export const b = 2;

实现将该目录的文件导出集中到一个文件中后再导出。

ESM 导入导出

1. 使用 import.meta.glob()

示例:引入 /api 目录下的除 index.js 的所有 js 文件模块,合并到一个对象中导出。

const modules = import.meta.glob(["./*.js", "!./index.js"], { eager: true });
let apiModule = {};
for (let path in modules) {
  const moduleName = path.replace("./", "").replace(".js", "");
  apiModule[moduleName] = modules[path];
}

export default apiModule;

上述方式在 Vite 中可用,因为 Vite 使用的模块化方式是 ESM。

2. 手动导出(不推荐)

export * as a from './a.js';
export * as b from './b.js';

CJS 导入导出

1. 使用 Node.js 的 fs.readdirSync() 方法

const fs = require("fs");
const path = require("path");

const modules = {};
const files = fs.readdirSync("./test");

files.forEach((file) => {
  if (file.endsWith(".js") && file !== "index.js") {
    const moduleName = path.basename(file, ".js");
    modules[moduleName] = require(path.join(__dirname, "./test", file));
  }
});

module.exports = modules;

2. 使用第三方插件 require-all

NPM 包

该包实现方式实际上也是通过 fs.readdirSync() 方法。两者区别是:fs.readdirSync(path, { recursive: true }) 递归目录时,最终会将所有子目录下的文件进行拍扁,也就是得到的数组是一维的。而 require-all 插件在配置 recursive: true 时仍然会保持目录结构。同一个目录下的文件会在同一个对象中,该对象会作为上层目录对象的属性存在,属性名为目录名(因此,文件名和目录名在同一层级不能冲突)。另外 require-all 提供了一些配置方法例如 filter 属性,使用户可以对生成的对象进行更精细的控制。

const path = require("path");
const requireAll = require("require-all");

const modules = requireAll({
  dirname: path.join(__dirname, "./test"),
  filter: function (fileName) {
    if (fileName === "index.js") {
      return false;
    }
    return path.basename(fileName, ".js");
  },
});

exports = modules;

3. 手动导出(不推荐)

exports.a = require('./a');
exports.b = require('./b');

使用 Webpack 的 require.context() 方法

const requireApi = require.context('.', true, /.js$/);

const module = {};

requireApi.keys().forEach((key) => {
  if (key === './index.js') return;
  Object.assign(module, requireApi(key));
});

export default module;

使用第三方插件 glob

NPM 包

‼️上述方式都使用了桶文件(将其他文件中的模块整合到一个文件中导出),如果模块文件很多,尤其是在大型项目中,会有明显的性能问题

桶文件(Barrel Files)

什么是桶文件?

将多个模块文件引入到同一个文件后导出,方便有一个统一的入口去引入。

Vite 官方文档中也说明了,应该避免使用桶文件。因为 Vite 在开发阶段是不会进行 tree shaking 的,如果项目中存在桶文件并且一些模块并没有被使用,这无疑会大大增加构建时间和页面加载时间。vite Issues: Consider treeshaking module for serving files in dev mode #8237

使用桶文件带来的性能问题:

(1)影响模块分析效率。在 bundle 阶段,一是使用可以 tree shaking 的打包工具时,需要分析哪些模块是否被使用,从而减小打包文件体积,但同时增加了分析的开销,bundle time 增加;而是如果进行分包(code splitting),以便按需加载,提高 runtime 性能,但是在构建时需要计算哪些包需要被分开,bundle time 增加。也就是我们进行更多优化来利于 runtime 的同时,我们的构建成本实际上是增加的。 (2)不利于查找接口真正来源。

如何进行优化?

Vercel: How we optimized package imports in Next.js

Next.js: Modularize Imports

对导入进行编译转换。示例:import { moduleA } from "mylib"; 转换成 import { moduleA } from "mylib/moduleA"; 。这意味着可以直接跳过桶文件,导入目标文件,从而防止加载不必要的模块。

但这种方式存在很多问题:(1)适用于框架项目,对于普通开发用户不友好。(2)每个库的导入导出方式是不同的,可能需要做很多手动工作。(3)对于引入的第三方包,如果该包的导入导出内容发生了变化,所做的转换会变成无效的。

因此,很多框架对于第三方包都会进行动态分析,扫描代码,然后对包的导入导出进行转换。例如 Vite 的预构建过程,通过配置 optimize 可以对第三方包的导入导出方式进行优化,Next.js 的 optimizePackageImports。

🧐 这里上述优化的具体流程暂不做赘述,后续研究后再补充。

总结,非必要情况下,开发中尽量不要使用桶文件,尤其是在文件中导入导出模块较多的情况。

← 上一篇: Worker 之 SharedWorker 初探 —— 多个页面间共享资源

下一篇: → 语义化版本(SemVer)