一篇帶給你 No.js 的模塊加載器實現(xiàn)
前言:最近在 No.js 里實現(xiàn)了一個簡單的模塊加載器,本文簡單介紹一下加載器的實現(xiàn)。
因為 JS 本身沒有模塊加載的概念,隨著前端的發(fā)展,各種加載技術(shù)也發(fā)展了起來,早期的seajs,requirejs,現(xiàn)在的 webpack,Node.js等等,模塊加載器的背景是代碼的模塊化,因為我們不可能把所有代碼寫到同一個文件,所以模塊加載器主要是解決模塊中加載其他模塊的問題,不僅是前端語言,c語言、python、php同樣也是這樣。No.js 參考的是 Node.js的實現(xiàn)。比如我們有以下兩個模塊。module1.js
- const func = require("module2");func();
module2.js
- module.exports = () => {
- // some code
- }
我們看看如何實現(xiàn)模塊加載的功能。首先看看運行時執(zhí)行的時候,是如何加載第一個模塊的。No.js 在初始化時會通過 V8 執(zhí)行 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 模塊和執(zhí)行用戶的 JS。首先來看一下如何加載原生JS模塊,模塊加載是通過loader.compile實現(xiàn)的,loader.compile是 V8 函數(shù)的封裝。
- 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首先讀取模塊的內(nèi)容,然后調(diào)用CompileFunctionInContext函數(shù)。CompileFunctionInContext函數(shù)的原理如下。假設(shè)文件內(nèi)容是 1 + 1。執(zhí)行以下代碼后
- const ret = CompileFunctionInContext("1+1", ["require", "exports", "module"])
ret變成
- function (require, exports, module) {
- 1 + 1;
- }
所以CompileFunctionInContext的作用是把代碼封裝到一個函數(shù)中,并且可以設(shè)置該函數(shù)的形參列表?;氐皆?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和模塊內(nèi)容得到一個函數(shù),然后傳入?yún)?shù)執(zhí)行該函數(shù)。我們看看原生JS 模塊的代碼。
- class Module {
- // ...
- };
- module.exports = Module;
最后導(dǎo)出了一個Module函數(shù)并記錄到全局變量 No中。原生模塊就加載完畢了,接著執(zhí)行用戶 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對象,然后執(zhí)行他的load函數(shù)。
- load() {
- const result = loader.compile(this.filename);
- result.call(this, Module.load, this.exports, this);
- return this.exports;
- }
load函數(shù)最終調(diào)用loader.compile拿到一個函數(shù),最后傳入三個參數(shù)執(zhí)行該函數(shù),就可以通過module.exports拿到模塊的導(dǎo)出內(nèi)容。從中我們也看到,模塊里的require、module和exports到底是哪里來的,內(nèi)容是什么。