而对于 require 函数来讲,注入时我们需要考虑的就是走完上面的几个步骤,require 接受一个字符串变量路径,然后依次通过路径找到文件,获取文件,拼接函数,变为可执行代码段并执行,之后仍给全局的缓存对象,这就是 「require」需要做的内容。
过程中的切面
最终形态是什么
对于最终的形态,本质上我们是要提供一个 require 函数,它的目标就是在 runtime 能够从远端 url 加载 js 模块,能够加载 ts 模块甚至类似 babel 提供 preset 加载各种各样的模块。
但是我们的 require 无法注入到 node bootstrap 阶段,所以最终结果一定得是 bootsrap 文件使用 CommonJS 模块加载,通过我们自定义的 require 加载的所有文件都能实现功能。
生命周期的设计
就如上面的第二部分介绍的那样,对于 require 函数我们要依次做这些事情,完全可以把每个阶段看做一个切面,任何一个阶段只关注输入和输出而不关注上个阶段是如何产出的。
经过仔细的思考,最终设置了两个核心的过程,包裹模块内容 和 编译文件结果。
包裹模块内容就是将字符串的文件结果包裹一下函数,专注于处理字符串结果,将普通文件的文本进行包裹。
编译文件结果这一步就是将代码结果编译成 node 能够直接识别的 js 而使得下一步沙盒环境进行执行,每次通过文件结果动态在内存进行编译,从而使得下一步 js 的执行。
同步还是异步?
这个问题其实困扰了很久。最大的问题就是里面涉及了部分异步加载的问题,按照传统前端的做法,这里一般都是使用 callback 或者 promise(async/await) 的方式,但这样就会带来一个很大的问题。
如果是 callback 的方式,那么意味着最终我的 require 可能得这样调用:
var r = require("nedo"); var moduleA = r("./moduleA"); var moduleB = r("./moduleB"); function log(module) { // 所有执行过程作为 callback // 这里拿到 module 的结果 console.log(module); } moduleA(log); // 传入 callback,moduleA 加载结束执行回调 moduleB(log); // 传入 callback,moduleB 加载结束执行回调
这样就显得很愚蠢,即使改成 AMD 那样的 callback 调用也感觉是在开历史的倒车。
如果是 promise(async/await) 这样的异步方式,那么意味着最终我的 require 可能得这样调用:
var r = require("nedo"); var moduleA = r("./moduleA"); moduleA.then(module => { // 这里拿到 module 结果 }); (async function() { var moduleB = await r("./moduleB"); // 这里拿到 module 的结果 })();
说实话这种方式也显得很愚蠢。不过中间我想了个方法,包裹函数时多包一层,包一个 IIFE 然后自执行一个 async 的 wrapper,不过这样的话 bootstrap 文件就必须还得手动包裹在 async 的函数中,子函数的问题解决了但是上层没有解决,不够完美。
其实后来仔细的思考了一下,造成这样的问题的原因究其根本是因为 request 是 async 的,这就导致了后续的代码必须以 async 的方式出现。如果我们想要从硬盘读取一个文件,那么我们可以使用 promise 包裹的 fs.readFile,当然我们也可以使用 fs.readFileSync 。前者的方法会让后续的所有调用都变成异步,而后者的代码还是同步,虽然性能很差但是完全符合直觉。
所以就必须找到一个 sync 的 request 的形式,才能让最终调用变的完美,最终的想法结果应该如下:
var r = require("nedo"); var moduleA = r("./moduleA"); // moduleA 结果 var moduleB = r("https://baidu.com"); // moduleB 结果,同步阻塞
思考了半天不知道 sync 的 request 应该怎么写,后来只得求助万能的 npmjs,结果真的发现了一个 sync-request 的包,仔细研究了一下代码发现核心是借助了 sync-rpc 这个包,虽然这个包 github 只有 5 个 star,下载量也不大。但是感觉却是非常的厉害,能够将任何异步的代码转化为同步调用的形式,战略性 star,日后可能大有所为…
runtime 编译
解决了 request async 的问题之后其他问题都变的非常简单,ts 使用 babel + ts preset 在内存中完成了编译,如果想要增加任何文件的支持,只需要在 lib/compile 下加入对应的文件后缀即可,在内存中只要能够完成编译就能够最终保证代码结果。
top level await
在之前的过程中我们只是包了一层注入参数的函数进去,当然也可以上层包裹一层 async 函数,这样就可以在使用 nedo require 的包内部直接使用顶层 await,不需要再使用 async 进行包裹
最终结果