前言:最近在 No.js 里实现了一个简单的模块加载器,本文简单介绍一下加载器的实现。
因为 JS 本身没有模块加载的概念,随着前端的发展,各种加载技术也发展了起来,早期的seajs,requirejs,现在的 webpack,Node.js等等,模块加载器的背景是代码的模块化,因为我们不可能把所有代码写到同一个文件,所以模块加载器主要是解决模块中加载其他模块的问题,不仅是前端语言,c语言、python、php同样也是这样。No.js 参考的是 Node.js的实现。比如我们有以下两个模块。module1.js
- constfunc=require("module2");func();
module2.js
- module.exports=()=>{
- //somecode
- }
我们看看如何实现模块加载的功能。首先看看运行时执行的时候,是如何加载第一个模块的。No.js 在初始化时会通过 V8 执行 No.js文件。
- const{
- loader,
- process,
- }=No;
- functionloaderNativeModule(){
- //原生JS模块列表
- constmodules=[
- {
- module:'libs/module/index.js',
- name:'module'
- },
- ];
- No.libs={};
- //初始化
-
for(leti=0;i
;i++){ - constmodule={
- exports:{},
- };
- loader.compile(modules[i].module).call(null,loader.compile,module.exports,module);
- No.libs[modules[i].name]=module.exports;
- }
- }
- functionrunMain(){
- No.libs.module.load(process.argv[1]);
- }
- loaderNativeModule();runMain();
No.js文件的逻辑主要是两个,加载原生 JS 模块和执行用户的 JS。首先来看一下如何加载原生JS模块,模块加载是通过loader.compile实现的,loader.compile是 V8 函数的封装。
- voidNo::Loader::Compile(V8_ARGS){
- V8_ISOLATE
- V8_CONTEXT
-
String::Utf8Valuefilename(isolate,args[0].As
()); - intfd=open(*filename,0,O_RDONLY);
- std::stringcontent;
- charbuffer[4096];
- while(1)
- {
- memset(buffer,0,4096);
- intret=read(fd,buffer,4096);
- if(ret==-1){
- returnargs.GetReturnValue().Set(newStringToLcal(isolate,"readfileerror"));
- }
- if(ret==0){
- break;
- }
- content.append(buffer,ret);
- }
- close(fd);
- ScriptCompiler::Sourcescript_source(newStringToLcal(isolate,content.c_str()));
-
Local
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。执行以下代码后
- constret=CompileFunctionInContext("1+1",["require","exports","module"])
ret变成
- function(require,exports,module){
- 1+1;
- }
所以CompileFunctionInContext的作用是把代码封装到一个函数中,并且可以设置该函数的形参列表。回到原生 JS 的加载过程。
-
for(leti=0;i
;i++){ - constmodule={
- exports:{},
- };
- loader.compile(modules[i].module).call(null,loader.compile,module.exports,module);
- No.libs[modules[i].name]=module.exports;
- }
首先通过loader.compile和模块内容得到一个函数,然后传入参数执行该函数。我们看看原生JS 模块的代码。
- classModule{
- //...
- };
- module.exports=Module;
最后导出了一个Module函数并记录到全局变量 No中。原生模块就加载完毕了,接着执行用户 JS。
- functionrunMain(){
- No.libs.module.load(process.argv[1]);
- }
我们看看No.libs.module.load。
- staticload(filename,...args){
- if(map[filename]){
- returnmap[filename];
- }
- constmodule=newModule(filename,...args);
- return(map[filename]=module.load());
- }
新建一个Module对象,然后执行他的load函数。
- load(){
- constresult=loader.compile(this.filename);
- result.call(this,Module.load,this.exports,this);
- returnthis.exports;
- }
load函数最终调用loader.compile拿到一个函数,最后传入三个参数执行该函数,就可以通过module.exports拿到模块的导出内容。从中我们也看到,模块里的require、module和exports到底是哪里来的,内容是什么。
原文链接:https://mp.weixin.qq.com/s/8Jo9SXRN8TxULeAiTJf5tA