如何使node也支持从url加载一个module详解


Posted in Javascript onJune 05, 2018

前言

最近两天 ry 大神的 deno 火了一把。作为 node 项目的发起人,现在又基于 go 重新写了一个类似 node 的项目命名为 deno,引发了大家的强烈关注。

在 deno 项目 readme 的开始就列举出了这个项目的优势和需要解决的问题,里面最让我瞩目的就是模块原生支持 ts ,同时也能也必须从 url 加载模块,这也是与现有的 CommonJS 最大的不同。

仔细思考一下,deno 的模块化与 CommonJS 相比,更多的是一些 runtime 的能力。现有的 CommonJS 底层实现过程并不是静态化,考虑了很多的动态配置,所以基于现有到 CommonJS 改造起来还是比较容易的,支持 url 加载或者 ts 模块也并不复杂,主要难点在于与系统调用的耦合度上。所以周六在家准备撸个小项目,从上层入手,算是仿照 deno 的这几个特性使得一个仿原生 node 的 CommonJS 模块语法也能支持这些特性。

CommonJS 的执行过程

想要让 CommonJS 支持 url 访问或者原生加载 ts 模块,必须从 CommonJS 的执行过程中入手,在中间阶段将模块注入进去。而 CommonJS 的执行过程其实总结起来很简单,大概分为以下几点:

  • 处理路径依赖

处理路径依赖应该也是所有模块化加载规范的第一步,换言之就是根据路径找到文件的位置。无论是 CommonJS 的 require 还是 ESModule 的 import,无论是相对路径还是绝对路径,都必须首先在内部对这个路径进行处理,找到合适的文件地址。

模块路径有可能是绝对路径,有可能是相对路径,有可能省略了后缀(js、node、json),有可能省略了文件名(index),甚至是动态路径(运行时基于变量的动态拼接)等等。

首先就是遵守约定,同时按照一定的策略找到这个文件的真实位置,中间的过程就是补齐上面模块化省略的东西。一般都是根据 CommonJS 的这张流程图

如何使node也支持从url加载一个module详解

  • 加载文件

确认了路径并且确保了文件存在之后,加载文件这一步就简单粗暴的多。最简单的方式就是直接读取硬盘上的文件,将纯文本的模块源代码读取至内存。

  • 拼接函数

在上一步中获取到的只是代码的文本形式源文件,并不具有执行能力。在接下来的步骤中需要将它变为一个可执行的代码段。

如果有同学看过 webpack 打包出来的结果,可以发现有这么一个现象,所有模块化的内容都处在一个函数的闭包中,内部所有的模块加载函数都替换成了 __webpack_require__ 这类的 webpack 内部变量。

还有一个问题,在 CommonJS 模块化规范中我们或多或少在每个文件中会写 module, module.exports require 等等这样的「字眼」,因为这里的 module 和 require 讲道理并不能称为关键字,JS 中关于模块加载方面的关键字只有 ESModule 中 import 和 export 等等相关的内容,他们是真真正正的关键字。而这里 CommonJS 里面带来的 module 和 require 则完全算是自己实现的一种 hack,在日常的 CommonJS 模块书写过程中,module 对象和 require 函数完全是 node 在包解析时注入进去的(类似上面的 __webpack_require__)

这也就给了我们极大的想象空间,我们也完全可以将上面拿到的 module 进行包裹然后注入我们传递的每一个变量。简单的例子:

// 纯文本代码 无法执行
var str = 1;
console.log(str);

将函数进行拼接,结果依旧是一个纯文本代码。但是已经可以给这个文件内部注入 require module 等变量,只需后续将它变为可执行文件并执行,就能把模块取出来。

function(require, module, exports, __dirname, __filename) {
 // 纯文本代码
 var str = 1;
 console.log(str);
}
  • 转化为可执行代码

拼接完成之后我们拿到的是还是纯字符串的代码,接下来就需要将这个字符串变成真正的代码,也就是将字符串变为可执行代码片段,这种操作在 JS 的历史上一直是危险的代名词…一直以来也有多种方法可以使用,eval、new Function(str) 等等。而在 node 环境中可以直接使用原生提供的 vm 模块,内部的沙盒环境支持我们手动注入一些变量,相对来说安全性还有所保证。

var txt = "function(require, module, exports, __dirname, __filename) {
 module.exports = 1;
}"

var vm = require('vm');
var script = new vm.Script(txt);
var func = script.runInThisContext();

上面这个示例中,func 就已经是经过 vm 从字符串变为可执行代码段的结果,我们的 txt 给定的是一个函数,所以此时我们需要调用这个函数来最后完成模块的导出。

var m = {
 exports: {}
};
func(null, m, m.exports);

这样的话,内部导出的内容就会被外面全局对象 m 所截获,将每一个模块导出的结果缓存到全局的 m 对象上面来。

而对于 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,日后可能大有所为…

如何使node也支持从url加载一个module详解

  • runtime 编译

解决了 request async 的问题之后其他问题都变的非常简单,ts 使用 babel + ts preset 在内存中完成了编译,如果想要增加任何文件的支持,只需要在 lib/compile 下加入对应的文件后缀即可,在内存中只要能够完成编译就能够最终保证代码结果。

  • top level await

在之前的过程中我们只是包了一层注入参数的函数进去,当然也可以上层包裹一层 async 函数,这样就可以在使用 nedo require 的包内部直接使用顶层 await,不需要再使用 async 进行包裹

最终结果

最后经过几个小时的不懈努力,最终能够将 hello world 跑起来了,代码还处于 pre-pre-pre-prototype 的阶段。仓库地址 nedo ,希望大家多帮忙 review,提供更多建设性的意见…

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
Js日期选择器并自动加入到输入框中示例代码
Aug 02 Javascript
javascript中如何处理引号编码"
Aug 15 Javascript
深入解析JavaScript中的变量作用域
Dec 06 Javascript
悬浮数字的实现案例
Feb 19 Javascript
javaScript中Math()函数注意事项
Jun 18 Javascript
jQuery基于ajax实现星星评论代码
Aug 07 Javascript
安装使用Mongoose配合Node.js操作MongoDB的基础教程
Mar 01 Javascript
JavaScript微信定位功能实现方法
Nov 29 Javascript
JS设计模式之单例模式(一)
Sep 29 Javascript
JavaScript实现浅拷贝与深拷贝的方法分析
Jul 05 Javascript
javascript和php使用ajax通信传递JSON的实例
Aug 21 Javascript
vue实现点击按钮“查看详情”弹窗展示详情列表操作
Sep 09 Javascript
Js中将Long转换成日期格式的实现方法
Jun 05 #Javascript
JS非行间样式获取函数的实例代码
Jun 05 #Javascript
JavaScript实现读取与输出XML文件数据的方法示例
Jun 05 #Javascript
Node错误处理笔记之挖坑系列教程
Jun 05 #Javascript
Vue项目中跨域问题解决方案
Jun 05 #Javascript
Vue多系统切换实现方案
Jun 05 #Javascript
jQuery实现的简单对话框拖动功能示例
Jun 05 #jQuery
You might like
星际争霸任务指南——神族
2020/03/04 星际争霸
PHP检测用户语言的方法
2015/06/15 PHP
py文件转exe时包含paramiko模块出错解决方法
2016/08/12 PHP
详解php用curl调用接口方法,get和post两种方式
2017/01/13 PHP
phpMyAdmin通过密码漏洞留后门文件
2018/11/20 PHP
Javascript之文件操作
2007/03/07 Javascript
鼠标滑过出现预览的大图提示效果
2014/02/26 Javascript
javascript实现复选框选中属性
2015/03/25 Javascript
JS产生随机数的几个用法详解
2016/06/22 Javascript
JS产生随机数的用法小结
2016/12/10 Javascript
jQuery设置和获取select、checkbox、radio的选中值方法
2017/01/01 Javascript
深究AngularJS如何获取input的焦点(自定义指令)
2017/06/12 Javascript
Three.js入门之hello world以及如何绘制线
2017/09/25 Javascript
js解决软键盘遮挡输入框的问题分享
2017/12/19 Javascript
利用adb shell和node.js实现抖音自动抢红包功能(推荐)
2018/02/22 Javascript
Webpack之tree-starking 解析
2018/09/11 Javascript
Vue组件之单向数据流的解决方法
2018/11/10 Javascript
JavaScript Date对象功能与用法学习记录
2020/04/28 Javascript
Vue proxyTable配置多个接口地址,解决跨域的问题
2020/09/11 Javascript
[00:37]2016完美“圣”典风云人物:rOtk宣传片
2016/12/09 DOTA
Python实例分享:快速查找出被挂马的文件
2014/06/08 Python
Python实现图片拼接的代码
2018/07/02 Python
Python3模拟curl发送post请求操作示例
2019/05/03 Python
Python3批量生成带logo的二维码方法
2019/06/24 Python
python 一维二维插值实例
2020/04/22 Python
Python爬虫实现百度翻译功能过程详解
2020/05/29 Python
python递归函数用法详解
2020/10/26 Python
Python创建简单的神经网络实例讲解
2021/01/04 Python
美国值得信赖的婚恋交友网站:eHarmony
2018/10/04 全球购物
机电专业个人自荐信格式模板
2013/09/23 职场文书
计算机开发个人求职信范文
2013/09/26 职场文书
会计自荐信范文
2014/03/09 职场文书
预备党员表决心书
2014/03/11 职场文书
保研推荐信格式
2015/03/25 职场文书
2015年保卫科工作总结
2015/05/14 职场文书
python周期任务调度工具Schedule使用详解
2021/11/23 Python