如何使node也支持從url加載一個(gè)module詳解
前言
最近兩天 ry 大神的 deno 火了一把。作為 node 項(xiàng)目的發(fā)起人,現(xiàn)在又基于 go 重新寫了一個(gè)類似 node 的項(xiàng)目命名為 deno,引發(fā)了大家的強(qiáng)烈關(guān)注。
在 deno 項(xiàng)目 readme 的開始就列舉出了這個(gè)項(xiàng)目的優(yōu)勢(shì)和需要解決的問(wèn)題,里面最讓我矚目的就是模塊原生支持 ts ,同時(shí)也能也必須從 url 加載模塊,這也是與現(xiàn)有的 CommonJS 最大的不同。
仔細(xì)思考一下,deno 的模塊化與 CommonJS 相比,更多的是一些 runtime 的能力?,F(xiàn)有的 CommonJS 底層實(shí)現(xiàn)過(guò)程并不是靜態(tài)化,考慮了很多的動(dòng)態(tài)配置,所以基于現(xiàn)有到 CommonJS 改造起來(lái)還是比較容易的,支持 url 加載或者 ts 模塊也并不復(fù)雜,主要難點(diǎn)在于與系統(tǒng)調(diào)用的耦合度上。所以周六在家準(zhǔn)備擼個(gè)小項(xiàng)目,從上層入手,算是仿照 deno 的這幾個(gè)特性使得一個(gè)仿原生 node 的 CommonJS 模塊語(yǔ)法也能支持這些特性。
CommonJS 的執(zhí)行過(guò)程
想要讓 CommonJS 支持 url 訪問(wèn)或者原生加載 ts 模塊,必須從 CommonJS 的執(zhí)行過(guò)程中入手,在中間階段將模塊注入進(jìn)去。而 CommonJS 的執(zhí)行過(guò)程其實(shí)總結(jié)起來(lái)很簡(jiǎn)單,大概分為以下幾點(diǎn):
- 處理路徑依賴
處理路徑依賴應(yīng)該也是所有模塊化加載規(guī)范的第一步,換言之就是根據(jù)路徑找到文件的位置。無(wú)論是 CommonJS 的 require 還是 ESModule 的 import,無(wú)論是相對(duì)路徑還是絕對(duì)路徑,都必須首先在內(nèi)部對(duì)這個(gè)路徑進(jìn)行處理,找到合適的文件地址。
模塊路徑有可能是絕對(duì)路徑,有可能是相對(duì)路徑,有可能省略了后綴(js、node、json),有可能省略了文件名(index),甚至是動(dòng)態(tài)路徑(運(yùn)行時(shí)基于變量的動(dòng)態(tài)拼接)等等。
首先就是遵守約定,同時(shí)按照一定的策略找到這個(gè)文件的真實(shí)位置,中間的過(guò)程就是補(bǔ)齊上面模塊化省略的東西。一般都是根據(jù) CommonJS 的這張流程圖
- 加載文件
確認(rèn)了路徑并且確保了文件存在之后,加載文件這一步就簡(jiǎn)單粗暴的多。最簡(jiǎn)單的方式就是直接讀取硬盤上的文件,將純文本的模塊源代碼讀取至內(nèi)存。
- 拼接函數(shù)
在上一步中獲取到的只是代碼的文本形式源文件,并不具有執(zhí)行能力。在接下來(lái)的步驟中需要將它變?yōu)橐粋€(gè)可執(zhí)行的代碼段。
如果有同學(xué)看過(guò) webpack 打包出來(lái)的結(jié)果,可以發(fā)現(xiàn)有這么一個(gè)現(xiàn)象,所有模塊化的內(nèi)容都處在一個(gè)函數(shù)的閉包中,內(nèi)部所有的模塊加載函數(shù)都替換成了 __webpack_require__ 這類的 webpack 內(nèi)部變量。
還有一個(gè)問(wèn)題,在 CommonJS 模塊化規(guī)范中我們或多或少在每個(gè)文件中會(huì)寫 module, module.exports require 等等這樣的「字眼」,因?yàn)檫@里的 module 和 require 講道理并不能稱為關(guān)鍵字,JS 中關(guān)于模塊加載方面的關(guān)鍵字只有 ESModule 中 import 和 export 等等相關(guān)的內(nèi)容,他們是真真正正的關(guān)鍵字。而這里 CommonJS 里面帶來(lái)的 module 和 require 則完全算是自己實(shí)現(xiàn)的一種 hack,在日常的 CommonJS 模塊書寫過(guò)程中,module 對(duì)象和 require 函數(shù)完全是 node 在包解析時(shí)注入進(jìn)去的(類似上面的 __webpack_require__)
這也就給了我們極大的想象空間,我們也完全可以將上面拿到的 module 進(jìn)行包裹然后注入我們傳遞的每一個(gè)變量。簡(jiǎn)單的例子:
// 純文本代碼 無(wú)法執(zhí)行 var str = 1; console.log(str);
將函數(shù)進(jìn)行拼接,結(jié)果依舊是一個(gè)純文本代碼。但是已經(jīng)可以給這個(gè)文件內(nèi)部注入 require module 等變量,只需后續(xù)將它變?yōu)榭蓤?zhí)行文件并執(zhí)行,就能把模塊取出來(lái)。
function(require, module, exports, __dirname, __filename) { // 純文本代碼 var str = 1; console.log(str); }
- 轉(zhuǎn)化為可執(zhí)行代碼
拼接完成之后我們拿到的是還是純字符串的代碼,接下來(lái)就需要將這個(gè)字符串變成真正的代碼,也就是將字符串變?yōu)榭蓤?zhí)行代碼片段,這種操作在 JS 的歷史上一直是危險(xiǎn)的代名詞…一直以來(lái)也有多種方法可以使用,eval、new Function(str) 等等。而在 node 環(huán)境中可以直接使用原生提供的 vm 模塊,內(nèi)部的沙盒環(huán)境支持我們手動(dòng)注入一些變量,相對(duì)來(lái)說(shuō)安全性還有所保證。
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();
上面這個(gè)示例中,func 就已經(jīng)是經(jīng)過(guò) vm 從字符串變?yōu)榭蓤?zhí)行代碼段的結(jié)果,我們的 txt 給定的是一個(gè)函數(shù),所以此時(shí)我們需要調(diào)用這個(gè)函數(shù)來(lái)最后完成模塊的導(dǎo)出。
var m = { exports: {} }; func(null, m, m.exports);
這樣的話,內(nèi)部導(dǎo)出的內(nèi)容就會(huì)被外面全局對(duì)象 m 所截獲,將每一個(gè)模塊導(dǎo)出的結(jié)果緩存到全局的 m 對(duì)象上面來(lái)。
而對(duì)于 require 函數(shù)來(lái)講,注入時(shí)我們需要考慮的就是走完上面的幾個(gè)步驟,require 接受一個(gè)字符串變量路徑,然后依次通過(guò)路徑找到文件,獲取文件,拼接函數(shù),變?yōu)榭蓤?zhí)行代碼段并執(zhí)行,之后仍給全局的緩存對(duì)象,這就是 「require」需要做的內(nèi)容。
過(guò)程中的切面
- 最終形態(tài)是什么
對(duì)于最終的形態(tài),本質(zhì)上我們是要提供一個(gè) require 函數(shù),它的目標(biāo)就是在 runtime 能夠從遠(yuǎn)端 url 加載 js 模塊,能夠加載 ts 模塊甚至類似 babel 提供 preset 加載各種各樣的模塊。
但是我們的 require 無(wú)法注入到 node bootstrap 階段,所以最終結(jié)果一定得是 bootsrap 文件使用 CommonJS 模塊加載,通過(guò)我們自定義的 require 加載的所有文件都能實(shí)現(xiàn)功能。
- 生命周期的設(shè)計(jì)
就如上面的第二部分介紹的那樣,對(duì)于 require 函數(shù)我們要依次做這些事情,完全可以把每個(gè)階段看做一個(gè)切面,任何一個(gè)階段只關(guān)注輸入和輸出而不關(guān)注上個(gè)階段是如何產(chǎn)出的。
經(jīng)過(guò)仔細(xì)的思考,最終設(shè)置了兩個(gè)核心的過(guò)程,包裹模塊內(nèi)容 和 編譯文件結(jié)果。
包裹模塊內(nèi)容就是將字符串的文件結(jié)果包裹一下函數(shù),專注于處理字符串結(jié)果,將普通文件的文本進(jìn)行包裹。
編譯文件結(jié)果這一步就是將代碼結(jié)果編譯成 node 能夠直接識(shí)別的 js 而使得下一步沙盒環(huán)境進(jìn)行執(zhí)行,每次通過(guò)文件結(jié)果動(dòng)態(tài)在內(nèi)存進(jìn)行編譯,從而使得下一步 js 的執(zhí)行。
- 同步還是異步?
這個(gè)問(wèn)題其實(shí)困擾了很久。最大的問(wèn)題就是里面涉及了部分異步加載的問(wèn)題,按照傳統(tǒng)前端的做法,這里一般都是使用 callback 或者 promise(async/await) 的方式,但這樣就會(huì)帶來(lái)一個(gè)很大的問(wèn)題。
如果是 callback 的方式,那么意味著最終我的 require 可能得這樣調(diào)用:
var r = require("nedo"); var moduleA = r("./moduleA"); var moduleB = r("./moduleB"); function log(module) { // 所有執(zhí)行過(guò)程作為 callback // 這里拿到 module 的結(jié)果 console.log(module); } moduleA(log); // 傳入 callback,moduleA 加載結(jié)束執(zhí)行回調(diào) moduleB(log); // 傳入 callback,moduleB 加載結(jié)束執(zhí)行回調(diào)
這樣就顯得很愚蠢,即使改成 AMD 那樣的 callback 調(diào)用也感覺是在開歷史的倒車。
如果是 promise(async/await) 這樣的異步方式,那么意味著最終我的 require 可能得這樣調(diào)用:
var r = require("nedo"); var moduleA = r("./moduleA"); moduleA.then(module => { // 這里拿到 module 結(jié)果 }); (async function() { var moduleB = await r("./moduleB"); // 這里拿到 module 的結(jié)果 })();
說(shuō)實(shí)話這種方式也顯得很愚蠢。不過(guò)中間我想了個(gè)方法,包裹函數(shù)時(shí)多包一層,包一個(gè) IIFE 然后自執(zhí)行一個(gè) async 的 wrapper,不過(guò)這樣的話 bootstrap 文件就必須還得手動(dòng)包裹在 async 的函數(shù)中,子函數(shù)的問(wèn)題解決了但是上層沒有解決,不夠完美。
其實(shí)后來(lái)仔細(xì)的思考了一下,造成這樣的問(wèn)題的原因究其根本是因?yàn)?request 是 async 的,這就導(dǎo)致了后續(xù)的代碼必須以 async 的方式出現(xiàn)。如果我們想要從硬盤讀取一個(gè)文件,那么我們可以使用 promise 包裹的 fs.readFile,當(dāng)然我們也可以使用 fs.readFileSync 。前者的方法會(huì)讓后續(xù)的所有調(diào)用都變成異步,而后者的代碼還是同步,雖然性能很差但是完全符合直覺。
所以就必須找到一個(gè) sync 的 request 的形式,才能讓最終調(diào)用變的完美,最終的想法結(jié)果應(yīng)該如下:
var r = require("nedo"); var moduleA = r("./moduleA"); // moduleA 結(jié)果 var moduleB = r("https://baidu.com"); // moduleB 結(jié)果,同步阻塞
思考了半天不知道 sync 的 request 應(yīng)該怎么寫,后來(lái)只得求助萬(wàn)能的 npmjs,結(jié)果真的發(fā)現(xiàn)了一個(gè) sync-request 的包,仔細(xì)研究了一下代碼發(fā)現(xiàn)核心是借助了 sync-rpc 這個(gè)包,雖然這個(gè)包 github 只有 5 個(gè) star,下載量也不大。但是感覺卻是非常的厲害,能夠?qū)⑷魏萎惒降拇a轉(zhuǎn)化為同步調(diào)用的形式,戰(zhàn)略性 star,日后可能大有所為…
- runtime 編譯
解決了 request async 的問(wèn)題之后其他問(wèn)題都變的非常簡(jiǎn)單,ts 使用 babel + ts preset 在內(nèi)存中完成了編譯,如果想要增加任何文件的支持,只需要在 lib/compile 下加入對(duì)應(yīng)的文件后綴即可,在內(nèi)存中只要能夠完成編譯就能夠最終保證代碼結(jié)果。
- top level await
在之前的過(guò)程中我們只是包了一層注入?yún)?shù)的函數(shù)進(jìn)去,當(dāng)然也可以上層包裹一層 async 函數(shù),這樣就可以在使用 nedo require 的包內(nèi)部直接使用頂層 await,不需要再使用 async 進(jìn)行包裹
最終結(jié)果
最后經(jīng)過(guò)幾個(gè)小時(shí)的不懈努力,最終能夠?qū)?hello world 跑起來(lái)了,代碼還處于 pre-pre-pre-prototype 的階段。倉(cāng)庫(kù)地址 nedo ,希望大家多幫忙 review,提供更多建設(shè)性的意見…
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
express中創(chuàng)建 websocket 接口及問(wèn)題解答
本文主要介紹了express中創(chuàng)建 websocket 接口及問(wèn)題解答,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05Node?文件查找優(yōu)先級(jí)及?Require?方法文件查找策略
這篇文章主要介紹了Node文件查找優(yōu)先級(jí)及Require方法文件查找策略。文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09node.js [superAgent] 請(qǐng)求使用示例
這篇文章主要介紹了node.js [superAgent] 請(qǐng)求使用示例,分別給大家匯總了post請(qǐng)求、get請(qǐng)求、delete請(qǐng)求和put請(qǐng)求的示例,推薦給大家,希望大家能夠喜歡。2015-03-03nodejs和npm版本不匹配報(bào)錯(cuò)的解決方法
當(dāng)公司要求使用固定nodejs的版本時(shí),自己不小心更新了npm,就會(huì)導(dǎo)致npm和nodejs不匹配,下面這篇文章主要給大家介紹了關(guān)于nodejs和npm版本不匹配報(bào)錯(cuò)的解決方法,需要的朋友可以參考下2023-04-04node通過(guò)express搭建自己的服務(wù)器
本篇文章主要介紹了node通過(guò)express搭建自己的服務(wù)器 ,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09利用yarn代替npm管理前端項(xiàng)目模塊依賴的方法詳解
這篇文章主要給大家介紹了關(guān)于利用yarn代替npm管理前端項(xiàng)目模塊依賴的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09