如何使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 相关文章推荐
javascript 简练的几个函数
Aug 29 Javascript
XMLHTTPRequest的属性和方法简介
Nov 23 Javascript
jQuery+CSS3实现树叶飘落特效
Feb 01 Javascript
jquery带动画效果幻灯片特效代码
Aug 27 Javascript
node.js从数据库获取数据
May 08 Javascript
javascript运算符——位运算符全面介绍
Jul 14 Javascript
简单实现JS倒计时效果
Dec 23 Javascript
jQuery中弹出iframe内嵌页面元素到父页面并全屏化的实例代码
Dec 27 Javascript
webpack2.0搭建前端项目的教程详解
Apr 05 Javascript
纯js实现页面返回顶部的动画(超简单)
Aug 10 Javascript
深入浅析Vue.js计算属性和侦听器
May 05 Javascript
JavaScript实现淘宝商品图切换效果
Apr 29 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
dedecms中显示数字验证码的修改方法
2007/03/21 PHP
WordPress中登陆后关闭登陆页面及设置用户不可见栏目
2015/12/31 PHP
asp 的 分词实现代码
2007/05/24 Javascript
Display SQL Server Login Mode
2007/06/21 Javascript
UserData用法总结 lanyu出品
2010/07/01 Javascript
jq选项卡鼠标延迟的插件实例
2013/05/13 Javascript
jQuery 无刷新分页实例代码
2013/11/12 Javascript
jquery制作搜狐快站页面效果示例分享
2014/02/21 Javascript
jquery+css实现绚丽的横向二级下拉菜单-附源码下载
2015/08/23 Javascript
jquery实现顶部向右伸缩的导航区域代码
2015/09/02 Javascript
jquery遍历json对象集合详解
2016/05/18 Javascript
JS button按钮实现submit按钮提交效果
2016/11/01 Javascript
JavaScript实现简单的星星评分效果
2017/05/18 Javascript
JavaScript函数中的this四种绑定形式
2017/08/15 Javascript
Vue的百度地图插件尝试使用
2017/09/06 Javascript
浅谈React高阶组件
2018/03/28 Javascript
axios对请求各种异常情况处理的封装方法
2018/09/25 Javascript
CentOS7中源码编译安装NodeJS的完整步骤
2018/10/13 NodeJs
Javascript中绑定click事件的四种方式介绍
2018/10/26 Javascript
微信小程序功能之全屏滚动效果的实现代码
2018/11/22 Javascript
vue项目每30秒刷新1次接口的实现方法
2018/12/04 Javascript
新手如何快速理解js异步编程
2019/06/24 Javascript
jQuery pager.js 插件动态分页功能实例分析
2019/08/02 jQuery
解决vue 表格table列求和的问题
2019/11/06 Javascript
js实现tab栏切换效果
2020/08/02 Javascript
对python中GUI,Label和Button的实例详解
2019/06/27 Python
挪威户外活动服装和装备购物网站:Bergfreunde挪威
2016/10/20 全球购物
BIBLOO捷克:购买女装、男装、童装、鞋和配件
2017/01/27 全球购物
《鱼游到了纸上》教学反思
2014/02/20 职场文书
《回乡偶书》教学反思
2014/04/12 职场文书
治安消防安全责任书
2014/07/23 职场文书
2015小学教师年度考核工作总结
2015/05/12 职场文书
青春雷锋观后感
2015/06/10 职场文书
骆驼祥子读书笔记
2015/06/26 职场文书
MongoDB数据库常用的10条操作命令
2021/06/18 MongoDB
python playwright之元素定位示例详解
2022/07/23 Python