2025年07月25日 Tags: Js
假设现在有一个目录为 /test
,该目录下有三个文件分别为 a.js
、b.js
和index.js
。
// a.js
export const a = 1;
// b.js
export const b = 2;
实现将该目录的文件导出集中到一个文件中后再导出。
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。
export * as a from './a.js';
export * as b from './b.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;
require-all
该包实现方式实际上也是通过 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;
exports.a = require('./a');
exports.b = require('./b');
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
‼️上述方式都使用了桶文件(将其他文件中的模块整合到一个文件中导出),如果模块文件很多,尤其是在大型项目中,会有明显的性能问题。
将多个模块文件引入到同一个文件后导出,方便有一个统一的入口去引入。
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
对导入进行编译转换。示例:import { moduleA } from "mylib";
转换成 import { moduleA } from "mylib/moduleA";
。这意味着可以直接跳过桶文件,导入目标文件,从而防止加载不必要的模块。
但这种方式存在很多问题:(1)适用于框架项目,对于普通开发用户不友好。(2)每个库的导入导出方式是不同的,可能需要做很多手动工作。(3)对于引入的第三方包,如果该包的导入导出内容发生了变化,所做的转换会变成无效的。
因此,很多框架对于第三方包都会进行动态分析,扫描代码,然后对包的导入导出进行转换。例如 Vite 的预构建过程,通过配置 optimize 可以对第三方包的导入导出方式进行优化,Next.js 的 optimizePackageImports。
🧐 这里上述优化的具体流程暂不做赘述,后续研究后再补充。
总结,非必要情况下,开发中尽量不要使用桶文件,尤其是在文件中导入导出模块较多的情况。