如何构建模块打包器:8 大核心技术完整指南及代码示例
Source: Dev.to
模块解析
解析器从入口文件(例如 index.js)开始,确定每个 import 的确切文件位置。
对于相对路径,它会将请求与导入文件所在的目录拼接;对于包名,它会向上遍历目录树寻找 node_modules 文件夹,读取包的 package.json 以找到主入口,并尝试常见的文件扩展名。
// A simplified look at the resolution logic
async resolve(request, fromDir) {
// Is it a direct path?
if (request.startsWith('.')) {
let candidate = path.resolve(fromDir, request);
// Does it have an extension? If not, add one.
if (!path.extname(candidate)) {
candidate = await this.tryWithExtensions(candidate);
}
return candidate;
}
// It's a package name, look in node_modules
let dir = fromDir;
while (dir !== path.parse(dir).root) {
const pkgPath = path.join(dir, 'node_modules', request);
if (await this.exists(pkgPath)) {
// Found the package folder, now find its main file
return this.resolvePackageMain(pkgPath);
}
// Go up one directory and check again
dir = path.dirname(dir);
}
throw new Error(`Module not found: ${request}`);
}
构建依赖图
为了了解模块之间的相互依赖,我们将每个文件解析成抽象语法树(AST)。AST 让我们能够可靠地定位 import 和 export 语句。
const parser = require('@babel/parser');
const code = `import { formatDate } from './dateUtils.js';`;
const ast = parser.parse(code, { sourceType: 'module' });
console.log(ast.program.body[0].type); // 'ImportDeclaration'
console.log(ast.program.body[0].source.value); // './dateUtils.js'
随后遍历 AST 并收集依赖:
const traverse = require('@babel/traverse').default;
const dependencies = [];
traverse(ast, {
ImportDeclaration(path) {
// Record where this import is pointing to
dependencies.push(path.node.source.value);
},
ExportNamedDeclaration(path) {
// Also check for re-exports: export { x } from './module'
if (path.node.source) {
dependencies.push(path.node.source.value);
}
}
});
// Now 'dependencies' contains ['./dateUtils.js']
结果是每个文件及其所需模块的映射,形成依赖图。
转换 Import 与 Export
打包需要把 ES 模块语法转换为运行时能够理解的格式。Import 会被转化为对自定义 __require__ 函数的调用,Export 则附加到 module.exports 上。
转换 Import
const t = require('@babel/types');
// Suppose we know the './stringLib' module has ID = 2
const moduleId = 2;
// Create the call to our runtime function: __require__(2)
const requireCall = t.callExpression(
t.identifier('__require__'),
[t.numericLiteral(moduleId)]
);
// Create the destructuring: const { capitalize } = __require__(2);
const newDeclaration = t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern([
t.objectProperty(
t.identifier('capitalize'),
t.identifier('capitalize'),
false,
true // shorthand
)
]),
requireCall
)
]);
// 'path' is the AST node for the import statement
path.replaceWith(newDeclaration);
转换 Export
Export 会被改写为给 module.exports 赋值:
// Example: export const version = '1.0';
module.exports.version = '1.0';
包装模块
每个已转换的模块都会被包装在一个函数中,以提供私有作用域,并向内部代码暴露 module、exports 和 __require__。
function (module, exports, __require__) {
// The transformed module code lives here
const { capitalize } = __require__(2);
module.exports.greet = (name) => capitalize(name);
}
运行时加载器
打包文件以一个小型运行时结束,该运行时将所有模块包装函数存放在数组中,并提供 __require__ 函数按需加载模块。它还会缓存已执行的模块,以处理循环依赖。
// The final bundle output starts with this runtime
(function (modules) {
// A cache for modules that have already been executed
const moduleCache = [];
// The require function our transformed code will call
function __require__(moduleId) {
// Return cached module if it exists
if (moduleCache[moduleId]) {
return moduleCache[moduleId].exports;
}
// Create a new module object for this id
const module = { exports: {} };
// Store it in cache immediately to handle circular dependencies
moduleCache[moduleId] = module;
// Get the module's wrapper function and execute it
// It receives the module object, its exports, and the __require__ function
modules[moduleId].call(module.exports, module, module.exports, __require__);
// Return the now-populated exports
return module.exports;
}
// Start the application by requiring the entry module (id 0)
__require__(0);
})([
// Our modules registry array starts here
// Each element is a wrapped module function
function (module, exports, __require__) {
// Transformed code for module ID 0 (entry point)
const utils = __require__(1);
console.log(utils);
},
// ...more wrapped modules...
]);