一篇带给你 No.js 的模块加载器实现

时间:2022-11-20 23:52:24

一篇带给你 No.js 的模块加载器实现

前言:最近在 No.js 里实现了一个简单的模块加载器,本文简单介绍一下加载器的实现。

因为 JS 本身没有模块加载的概念,随着前端的发展,各种加载技术也发展了起来,早期的seajs,requirejs,现在的 webpack,Node.js等等,模块加载器的背景是代码的模块化,因为我们不可能把所有代码写到同一个文件,所以模块加载器主要是解决模块中加载其他模块的问题,不仅是前端语言,c语言、python、php同样也是这样。No.js 参考的是 Node.js的实现。比如我们有以下两个模块。module1.js

  1. constfunc=require("module2");func();

module2.js

  1. module.exports=()=>{
  2. //somecode
  3.  
  4. }

我们看看如何实现模块加载的功能。首先看看运行时执行的时候,是如何加载第一个模块的。No.js 在初始化时会通过 V8 执行 No.js文件。

  1. const{
  2. loader,
  3. process,
  4.  
  5. }=No;
  6.  
  7.  
  8.  
  9. functionloaderNativeModule(){
  10. //原生JS模块列表
  11. constmodules=[
  12. {
  13. module:'libs/module/index.js',
  14. name:'module'
  15. },
  16. ];
  17. No.libs={};
  18. //初始化
  19. for(leti=0;i;i++){
  20. constmodule={
  21. exports:{},
  22. };
  23. loader.compile(modules[i].module).call(null,loader.compile,module.exports,module);
  24. No.libs[modules[i].name]=module.exports;
  25.  
  26. }
  27.  
  28. }
  29.  
  30.  
  31. functionrunMain(){
  32. No.libs.module.load(process.argv[1]);
  33.  
  34. }
  35.  
  36. loaderNativeModule();runMain();

No.js文件的逻辑主要是两个,加载原生 JS 模块和执行用户的 JS。首先来看一下如何加载原生JS模块,模块加载是通过loader.compile实现的,loader.compile是 V8 函数的封装。

  1. voidNo::Loader::Compile(V8_ARGS){
  2. V8_ISOLATE
  3. V8_CONTEXT
  4. String::Utf8Valuefilename(isolate,args[0].As());
  5. intfd=open(*filename,0,O_RDONLY);
  6. std::stringcontent;
  7. charbuffer[4096];
  8. while(1)
  9. {
  10. memset(buffer,0,4096);
  11. intret=read(fd,buffer,4096);
  12. if(ret==-1){
  13. returnargs.GetReturnValue().Set(newStringToLcal(isolate,"readfileerror"));
  14. }
  15. if(ret==0){
  16. break;
  17. }
  18. content.append(buffer,ret);
  19. }
  20. close(fd);
  21. ScriptCompiler::Sourcescript_source(newStringToLcal(isolate,content.c_str()));
  22. Localparams[]={
  23. newStringToLcal(isolate,"require"),
  24. newStringToLcal(isolate,"exports"),
  25. newStringToLcal(isolate,"module"),
  26. };
  27. MaybeLocal<Function>fun=
  28. ScriptCompiler::CompileFunctionInContext(context,&script_source,3,params,0,nullptr);
  29. if(fun.IsEmpty()){
  30. args.GetReturnValue().Set(Undefined(isolate));
  31. }else{
  32. args.GetReturnValue().Set(fun.ToLocalChecked());
  33. }
  34.  
  35. }

Compile首先读取模块的内容,然后调用CompileFunctionInContext函数。CompileFunctionInContext函数的原理如下。假设文件内容是 1 + 1。执行以下代码后

  1. constret=CompileFunctionInContext("1+1",["require","exports","module"])

ret变成

  1. function(require,exports,module){
  2. 1+1;
  3.  
  4. }

所以CompileFunctionInContext的作用是把代码封装到一个函数中,并且可以设置该函数的形参列表。回到原生 JS 的加载过程。

  1. for(leti=0;i;i++){
  2. constmodule={
  3. exports:{},
  4. };
  5. loader.compile(modules[i].module).call(null,loader.compile,module.exports,module);
  6. No.libs[modules[i].name]=module.exports;
  7. }

首先通过loader.compile和模块内容得到一个函数,然后传入参数执行该函数。我们看看原生JS 模块的代码。

  1. classModule{
  2. //...
  3.  
  4. };
  5.  
  6.  
  7. module.exports=Module;

最后导出了一个Module函数并记录到全局变量 No中。原生模块就加载完毕了,接着执行用户 JS。

  1. functionrunMain(){
  2. No.libs.module.load(process.argv[1]);
  3.  
  4. }

我们看看No.libs.module.load。

  1. staticload(filename,...args){
  2. if(map[filename]){
  3. returnmap[filename];
  4. }
  5. constmodule=newModule(filename,...args);
  6. return(map[filename]=module.load());
  7.  
  8. }

新建一个Module对象,然后执行他的load函数。

  1. load(){
  2. constresult=loader.compile(this.filename);
  3. result.call(this,Module.load,this.exports,this);
  4. returnthis.exports;
  5.  
  6. }

load函数最终调用loader.compile拿到一个函数,最后传入三个参数执行该函数,就可以通过module.exports拿到模块的导出内容。从中我们也看到,模块里的require、module和exports到底是哪里来的,内容是什么。

原文链接:https://mp.weixin.qq.com/s/8Jo9SXRN8TxULeAiTJf5tA