一篇带给你 No.js 的模块加载器实现
前言:最近在 No.js 里实现了一个简单的篇带模块加载器,本文简单介绍一下加载器的模块实现。
因为 JS 本身没有模块加载的加载概念,随着前端的器实发展,各种加载技术也发展了起来,篇带早期的模块seajs,requirejs,加载现在的器实 webpack,Node.js等等,篇带模块加载器的模块背景是代码的模块化,因为我们不可能把所有代码写到同一个文件,加载所以模块加载器主要是器实解决模块中加载其他模块的问题,不仅是篇带前端语言,c语言、模块python、加载php同样也是这样。No.js 参考的是服务器租用 Node.js的实现。比如我们有以下两个模块。module1.js
const func = require("module2");func();module2.js
module.exports = () => { // some code }我们看看如何实现模块加载的功能。首先看看运行时执行的时候,是如何加载第一个模块的。No.js 在初始化时会通过 V8 执行 No.js文件。
const { loader, process, } = No; function loaderNativeModule() { // 原生 JS模块列表 const modules = [ { module: libs/module/index.js, name: module }, ]; No.libs = { }; // 初始化 for (let i = 0; i < modules.length; i++) { const module = { exports: { }, }; loader.compile(modules[i].module).call(null, loader.compile, module.exports, module); No.libs[modules[i].name] = module.exports; } } function runMain() { No.libs.module.load(process.argv[1]); } loaderNativeModule();runMain();No.js文件的逻辑主要是两个,加载原生 JS 模块和执行用户的 JS。首先来看一下如何加载原生JS模块,模块加载是通过loader.compile实现的,loader.compile是 V8 函数的封装。
void No::Loader::Compile(V8_ARGS) { V8_ISOLATE V8_CONTEXT String::Utf8Value filename(isolate, args[0].As<String>()); int fd = open(*filename, 0 , O_RDONLY); std::string content; char buffer[4096]; while (1) { memset(buffer, 0, 4096); int ret = read(fd, buffer, 4096); if (ret == -1) { return args.GetReturnValue().Set(newStringToLcal(isolate, "read file error")); } if (ret == 0) { break; } content.append(buffer, ret); } close(fd); ScriptCompiler::Source script_source(newStringToLcal(isolate, content.c_str())); Local<String> params[] = { newStringToLcal(isolate, "require"), newStringToLcal(isolate, "exports"), newStringToLcal(isolate, "module"), }; MaybeLocal<Function> fun = ScriptCompiler::CompileFunctionInContext(context, &script_source, 3, params, 0, nullptr); if (fun.IsEmpty()) { args.GetReturnValue().Set(Undefined(isolate)); } else { args.GetReturnValue().Set(fun.ToLocalChecked()); } }Compile首先读取模块的内容,然后调用CompileFunctionInContext函数。CompileFunctionInContext函数的原理如下。假设文件内容是 1 + 1。亿华云计算执行以下代码后
const ret = CompileFunctionInContext("1+1", ["require", "exports", "module"])ret变成
function (require, exports, module) { 1 + 1; }所以CompileFunctionInContext的作用是把代码封装到一个函数中,并且可以设置该函数的形参列表。回到原生 JS 的加载过程。
for (let i = 0; i < modules.length; i++) { const module = { exports: { }, }; loader.compile(modules[i].module).call(null, loader.compile, module.exports, module); No.libs[modules[i].name] = module.exports; }首先通过loader.compile和模块内容得到一个函数,然后传入参数执行该函数。我们看看原生JS 模块的代码。
class Module { // ... }; module.exports = Module;最后导出了一个Module函数并记录到全局变量 No中。原生模块就加载完毕了,接着执行用户 JS。
function runMain() { No.libs.module.load(process.argv[1]); }我们看看No.libs.module.load。
static load(filename, ...args) { if (map[filename]) { return map[filename]; } const module = new Module(filename, ...args); return (map[filename] = module.load()); }新建一个Module对象,然后执行他的load函数。
load() { const result = loader.compile(this.filename); result.call(this, Module.load, this.exports, this); return this.exports; }load函数最终调用loader.compile拿到一个函数,最后传入三个参数执行该函数,就可以通过module.exports拿到模块的导出内容。从中我们也看到,模块里的require、module和exports到底是哪里来的,内容是什么。源码下载