如何使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 left,right,mid函数
Jun 10 Javascript
JavaScript 替换Html标签实现代码
Oct 14 Javascript
jquery 弹出登录窗口实现代码
Dec 24 Javascript
javascript获取隐藏dom的宽高 具体实现
Jul 14 Javascript
全面理解面向对象的 JavaScript(来自ibm)
Nov 10 Javascript
创建、调用JavaScript对象的方法集锦
Dec 24 Javascript
学习Javascript闭包(Closure)知识
Aug 07 Javascript
HTML5基于Tomcat 7.0实现WebSocket连接并实现简单的实时聊天
Oct 31 Javascript
Angularjs之filter过滤器(推荐)
Nov 27 Javascript
基于vue-ssr的静态网站生成器VuePress 初体验
Apr 17 Javascript
node中modules.exports与exports导出的区别
Jun 08 Javascript
原生js实现日历效果
Mar 02 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
需要使用php模板的朋友必看的很多个顶级PHP模板引擎比较分析
2008/05/26 PHP
帝国cms常用标签汇总
2015/07/06 PHP
jquery下json数组的操作实现代码
2010/08/09 Javascript
Jquery跨域获得Json时invalid label错误的解决办法
2011/01/11 Javascript
jQuery EasyUI API 中文文档 DateTimeBox日期时间框
2011/10/16 Javascript
推荐40款强大的 jQuery 导航插件和教程(上篇)
2012/09/14 Javascript
IE下使用cloneNode注意事项分享
2012/11/22 Javascript
JavaScript中的Primitive对象封装介绍
2014/12/31 Javascript
常用的JavaScript模板引擎介绍
2015/02/28 Javascript
JavaScript函数使用的基本教程
2015/06/04 Javascript
jQuery与Ajax以及序列化
2016/02/01 Javascript
Document.body.scrollTop的值总为零的快速解决办法
2016/06/09 Javascript
浅谈JavaScript中数组的增删改查
2016/06/20 Javascript
JavaScript数据类型学习笔记分享
2016/09/01 Javascript
node.js请求HTTPS报错:UNABLE_TO_VERIFY_LEAF_SIGNATURE\的解决方法
2016/12/18 Javascript
JS写XSS cookie stealer来窃取密码的步骤详解
2017/11/20 Javascript
简述JS控制台的使用
2018/07/15 Javascript
基于js判断浏览器是否支持webGL
2020/04/18 Javascript
[56:18]DOTA2上海特级锦标赛主赛事日 - 4 败者组第四轮#2 MVP.Phx VS Fnatic第二局
2016/03/05 DOTA
[07:39]第一届亚洲邀请赛回顾视频
2017/02/14 DOTA
[45:25]OG vs EG 2019国际邀请赛淘汰赛 胜者组 BO3 第一场 8.22
2019/09/05 DOTA
详解Python打包分发工具setuptools
2019/08/05 Python
CSS3中颜色线性渐变实战
2015/07/18 HTML / CSS
英国评分最高的女性剃须刀订阅盒:FFS Beauty
2018/01/25 全球购物
美国最大的高尔夫发球时间预订网站:TeeOff.com
2018/03/28 全球购物
Charles & Keith欧盟:新加坡时尚品牌
2019/08/01 全球购物
荷兰睡眠专家:Beter Bed
2020/11/23 全球购物
C#的几个面试问题
2016/05/22 面试题
会计与审计专业大专生求职信
2013/10/03 职场文书
企业文化标语大全
2014/06/10 职场文书
绿色环保标语
2014/06/12 职场文书
教师批评与自我批评总结
2014/10/16 职场文书
2014年旅游局法制宣传日活动总结
2014/11/01 职场文书
民事诉讼代理词
2015/05/25 职场文书
领导欢送会主持词
2015/07/06 职场文书
Python实现排序方法常见的四种
2021/07/15 Python