当前位置: 首页 > 后端技术 > Node.js

makenode也支持从url加载模块

时间:2023-04-03 14:16:49 Node.js

博客原文:https://zhangzhao.name/posts/how-commonjs-load-url-module/这两天ry神的deno火了.作为node项目的发起人,现在他基于go重写了一个node类项目deno,引起了大家的强烈关注。在deno项目的readme开头,列出了这个项目的优势和需要解决的问题。最吸引我注意的是模块原生支持ts,同时模块也可以从url加载,这是与现有CommonJS最大的区别。仔细想想,deno的模块化比CommonJS更多的是一种运行时能力。现有的CommonJS底层实现过程并不是一成不变的,考虑了很多动态配置,所以基于现有的CommonJS进行改造相对容易,支持url加载或者ts模块也不复杂。主要难点在于与系统调用的连接上的耦合。所以周六我打算在家里开始一个小项目。从上层开始,可以看作是模仿了deno的这些特性,这样一个模仿nativenode的CommonJS模块语法也可以支持这些特性。CommonJS的执行流程如果想让CommonJS支持url访问或者原生加载ts模块,就必须从CommonJS的执行流程开始,在中间阶段注入模块。CommonJS的执行过程其实总结起来很简单,大致分为以下几点:处理路径依赖处理路径依赖也应该是所有模块化加载规范的第一步,换句话说就是根据文件找到文件的位置路径。无论是CommonJS的require还是ESModule的导入,无论是相对路径还是绝对路径,内部都要先对路径进行处理,找到合适的文件地址。模块路径可以是绝对路径,也可以是相对路径,可以省略后缀(js、node、json),可以省略文件名(index),甚至可以是动态路径(运行时根据变量动态拼接)等.首先是遵守约定,同时按照一定的策略找到这个文件的真实位置。中间过程就是把上面模块化省略的东西补上。一般按照CommonJS的这个流程图加载文件。在确认路径并确保文件存在后,加载文件的步骤就简单粗暴了很多。最简单的方法是直接读取硬盘上的文件,将明文模块源代码读入内存。拼接函数在上一步得到的只是代码文本形式的源文件,不具备执行能力。下一步需要将其转化为可执行代码段。如果有同学看过webpack打包的结果,会发现有这样一种现象,所有模块化的内容都在一个函数的闭包中,所有模块内部的加载函数都被webpack变量里面的__webpack_require__代替了。还有一个问题。在CommonJS模块化规范中,我们或多或少会在每个文件中写上module、require等“词”。module和require不能称为关键字。在JS中模块加载方面ESModule中唯一的关键字就是import和export等相关内容。在日常的模块编写过程中,模块对象和require函数完全由node在解析包时注入(类似于上面的__webpack_require__),这给了我们很大的想象空间,我们也可以得到上面的模块是包装并注入到我们传递的每个变量中。简单示例://纯文本代码无法执行varstr=1;console.log(str);将函数拼接在一起,结果仍然是纯文本代码。但是已经可以在这个文件中注入requiremodule等变量,你只需要将其变成一个可执行文件,后面再执行,就可以把module取出来了。function(require,module,exports,__dirname,__filename){//纯文本代码varstr=1;console.log(str);}转换为可执行代码拼接完成后,我们得到的仍然是纯字符串代码,下一步就是将这个字符串变成真正的代码,即把字符串变成可执行代码片段。这个操作在JS的历史上一直是危险的代名词。。。一直有很多方法可以使用,eval,newFunction(str)等等。在node环境下可以直接使用原生提供的vm模块,内部沙箱环境支持我们手动注入一些变量,相对安全。vartxt="function(require,module,exports,__dirname,__filename){module.exports=1;}"varvm=require('vm');var脚本=新虚拟机。脚本(txt);varfunc=script.runInThisContext();在上面的例子中,func已经是一个字符串通过vm转换成可执行代码段的结果。我们的txt是给定了一个函数,所以我们需要调用这个函数来完成此时模块的导出。varm={出口:{}};func(null,m,m.exports);这样,内部导出的内容就会被外部的全局对象m拦截,每个模块导出的结果都会缓存在全局m对象上来。对于require函数,我们在注入的时候需要考虑的就是要经过上面的步骤。require接受一个字符串变量路径,然后依次通过路径找到文件,获取文件,拼接函数,成为可执行代码段并执行,然后仍然给全局缓存对象,这就是“require”所需要的去做。aspect在这个过程中的最终形态是什么?对于最终形状,我们基本上需要提供一个require函数。它的目标是在运行时从远程url加载js模块,ts模块,甚至提供像babel这样的preset来加载各种模块。但是我们的require不能注入到node的bootstrap阶段,所以最后的结果肯定是bootsrap文件是使用CommonJS模块加载的,所有通过我们自定义的require加载的文件都能实现功能。生命周期的设计如上文第二部分所述。对于require函数,我们需要依次做这些事情。我们可以把每个阶段看成一个方面。任何阶段只关注输入和输出,而不关注前一阶段。它是如何产生的。经过深思熟虑,最终设置了两个核心流程,包装模块内容和编译文件结果。package模块内容是对字符串的文件结果进行包装的功能,重点处理字符串结果,对普通文件的文本进行包装。编译文件结果这一步是将代码结果编译成node可以直接识别的js,以便接下来的沙箱环境执行,每次在内存中动态编译文件结果,以便下一步可以执行js。同步还是异步?这个问题其实困扰了我很久。最大的问题是涉及到一些异步加载的问题。按照传统的前端方式,这里一般使用callback或者promise(async/await),但这会带来一个很大的问题。如果是回调方法,意味着我的require最终可能会这样调用:varr=require("nedo");varmoduleA=r("./moduleA");varmoduleB=r("./moduleB");functionlog(module){//所有的执行过程都作为回调//这里我们获取模块的结果console.log(module);}moduleA(log);//传入回调,moduleA加载完成,执行回调moduleB(log);//传入callback,moduleB加载后执行callback,看起来很傻,就算换成AMD的callback调用,也有倒退历史的感觉。如果是像promise(async/await)这样的异步方法,那就意味着我的require最终可能会这样调用:varr=require("nedo");varmoduleA=r("./moduleA");moduleA。then(module=>{//在这里获取模块结果});(asyncfunction(){varmoduleB=awaitr("./moduleB");//在这里获取模块结果})();这种方式也显得很愚蠢。不过中间想到了一个办法。包装函数的时候,多包一层,包装一个IIFE,然后执行一个asyncwrapper,但是这种情况下,bootstrap文件必须手动包装在async函数中,子函数的问题就解决了。但是上层还没有解决,还不完善。其实仔细想想,这个问题的根本原因是请求是async的,导致后面的代码肯定是async出现的。如果我们想从硬盘读取一个文件,那么我们可以使用promise包裹的fs.readFile,当然我们也可以使用fs.readFileSync。前一种方法将使所有后续调用异步,而后一种代码仍然是同步的。虽然表现不佳,但完全符合直觉。所以需要找一个syncrequestform来让最后调用完美。最终思路应该是这样的:varr=require("nedo");varmoduleA=r("./moduleA");//moduleAresultvarmoduleB=r("https://baidu.com");//moduleB结果,同步阻塞想了半天不知道syncrequest怎么写,只好求助于npmjs大神,终于找到了一个sync-request包,仔细研究了一下代码,我发现核心是sync-rpc包。虽然这个包在github上只有5星,但是下载量并不大。但是感觉很厉害。它可以将任何异步代码转换成同步调用形式。是战略之星,未来可能会有很大的不同。。。运行时编译解决了requestasync的问题后,其他问题就变得很简单了,ts使用babel+tspreset在内存中完成编译。如果要添加任何文件支持,只需要在lib/compile下添加相应的文件后缀即可。只要能在内存中完成编译,代码就可以保证结果。在前面toplevelawait的过程中,我们只是包裹了一层注入参数的函数。当然我们也可以在上层再包一层async函数,这样我们就可以直接在使用nedorequire的包内部使用最顶层的await,而不用再使用async。最后的wrapping结果,经过几个小时的不懈努力,终于可以运行helloworld了,代码还处于pre-pre-pre-prototype阶段。仓库地址nedo,希望大家帮忙审核,多提建设性意见...