溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么使node也支持從url加載一個module詳解

發(fā)布時間:2021-02-08 10:59:25 來源:億速云 閱讀:175 作者:小新 欄目:web開發(fā)

小編給大家分享一下怎么使node也支持從url加載一個module詳解,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

前言

最近兩天 ry 大神的 deno 火了一把。作為 node 項目的發(fā)起人,現(xiàn)在又基于 go 重新寫了一個類似 node 的項目命名為 deno,引發(fā)了大家的強烈關(guān)注。

在 deno 項目 readme 的開始就列舉出了這個項目的優(yōu)勢和需要解決的問題,里面最讓我矚目的就是模塊原生支持 ts ,同時也能也必須從 url 加載模塊,這也是與現(xiàn)有的 CommonJS 最大的不同。

仔細思考一下,deno 的模塊化與 CommonJS 相比,更多的是一些 runtime 的能力?,F(xiàn)有的 CommonJS 底層實現(xiàn)過程并不是靜態(tài)化,考慮了很多的動態(tài)配置,所以基于現(xiàn)有到 CommonJS 改造起來還是比較容易的,支持 url 加載或者 ts 模塊也并不復雜,主要難點在于與系統(tǒng)調(diào)用的耦合度上。所以周六在家準備擼個小項目,從上層入手,算是仿照 deno 的這幾個特性使得一個仿原生 node 的 CommonJS 模塊語法也能支持這些特性。

CommonJS 的執(zhí)行過程

想要讓 CommonJS 支持 url 訪問或者原生加載 ts 模塊,必須從 CommonJS 的執(zhí)行過程中入手,在中間階段將模塊注入進去。而 CommonJS 的執(zhí)行過程其實總結(jié)起來很簡單,大概分為以下幾點:

  • 處理路徑依賴

處理路徑依賴應該也是所有模塊化加載規(guī)范的第一步,換言之就是根據(jù)路徑找到文件的位置。無論是 CommonJS 的 require 還是 ESModule 的 import,無論是相對路徑還是絕對路徑,都必須首先在內(nèi)部對這個路徑進行處理,找到合適的文件地址。

模塊路徑有可能是絕對路徑,有可能是相對路徑,有可能省略了后綴(js、node、json),有可能省略了文件名(index),甚至是動態(tài)路徑(運行時基于變量的動態(tài)拼接)等等。

首先就是遵守約定,同時按照一定的策略找到這個文件的真實位置,中間的過程就是補齊上面模塊化省略的東西。一般都是根據(jù) CommonJS 的這張流程圖

怎么使node也支持從url加載一個module詳解

  • 加載文件

確認了路徑并且確保了文件存在之后,加載文件這一步就簡單粗暴的多。最簡單的方式就是直接讀取硬盤上的文件,將純文本的模塊源代碼讀取至內(nèi)存。

  • 拼接函數(shù)

在上一步中獲取到的只是代碼的文本形式源文件,并不具有執(zhí)行能力。在接下來的步驟中需要將它變?yōu)橐粋€可執(zhí)行的代碼段。

如果有同學看過 webpack 打包出來的結(jié)果,可以發(fā)現(xiàn)有這么一個現(xiàn)象,所有模塊化的內(nèi)容都處在一個函數(shù)的閉包中,內(nèi)部所有的模塊加載函數(shù)都替換成了 __webpack_require__ 這類的 webpack 內(nèi)部變量。

還有一個問題,在 CommonJS 模塊化規(guī)范中我們或多或少在每個文件中會寫 module, module.exports require 等等這樣的「字眼」,因為這里的 module 和 require 講道理并不能稱為關(guān)鍵字,JS 中關(guān)于模塊加載方面的關(guān)鍵字只有 ESModule 中 import 和 export 等等相關(guān)的內(nèi)容,他們是真真正正的關(guān)鍵字。而這里 CommonJS 里面帶來的 module 和 require 則完全算是自己實現(xiàn)的一種 hack,在日常的 CommonJS 模塊書寫過程中,module 對象和 require 函數(shù)完全是 node 在包解析時注入進去的(類似上面的 __webpack_require__)

這也就給了我們極大的想象空間,我們也完全可以將上面拿到的 module 進行包裹然后注入我們傳遞的每一個變量。簡單的例子:

// 純文本代碼 無法執(zhí)行
var str = 1;
console.log(str);

將函數(shù)進行拼接,結(jié)果依舊是一個純文本代碼。但是已經(jīng)可以給這個文件內(nèi)部注入 require module 等變量,只需后續(xù)將它變?yōu)榭蓤?zhí)行文件并執(zhí)行,就能把模塊取出來。

function(require, module, exports, __dirname, __filename) {
 // 純文本代碼
 var str = 1;
 console.log(str);
}
  • 轉(zhuǎn)化為可執(zhí)行代碼

拼接完成之后我們拿到的是還是純字符串的代碼,接下來就需要將這個字符串變成真正的代碼,也就是將字符串變?yōu)榭蓤?zhí)行代碼片段,這種操作在 JS 的歷史上一直是危險的代名詞…一直以來也有多種方法可以使用,eval、new Function(str) 等等。而在 node 環(huán)境中可以直接使用原生提供的 vm 模塊,內(nèi)部的沙盒環(huán)境支持我們手動注入一些變量,相對來說安全性還有所保證。

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 就已經(jīng)是經(jīng)過 vm 從字符串變?yōu)榭蓤?zhí)行代碼段的結(jié)果,我們的 txt 給定的是一個函數(shù),所以此時我們需要調(diào)用這個函數(shù)來最后完成模塊的導出。

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

這樣的話,內(nèi)部導出的內(nèi)容就會被外面全局對象 m 所截獲,將每一個模塊導出的結(jié)果緩存到全局的 m 對象上面來。

而對于 require 函數(shù)來講,注入時我們需要考慮的就是走完上面的幾個步驟,require 接受一個字符串變量路徑,然后依次通過路徑找到文件,獲取文件,拼接函數(shù),變?yōu)榭蓤?zhí)行代碼段并執(zhí)行,之后仍給全局的緩存對象,這就是 「require」需要做的內(nèi)容。

過程中的切面

  • 最終形態(tài)是什么

對于最終的形態(tài),本質(zhì)上我們是要提供一個 require 函數(shù),它的目標就是在 runtime 能夠從遠端 url 加載 js 模塊,能夠加載 ts 模塊甚至類似 babel 提供 preset 加載各種各樣的模塊。

但是我們的 require 無法注入到 node bootstrap 階段,所以最終結(jié)果一定得是 bootsrap 文件使用 CommonJS 模塊加載,通過我們自定義的 require 加載的所有文件都能實現(xiàn)功能。

  • 生命周期的設計

就如上面的第二部分介紹的那樣,對于 require 函數(shù)我們要依次做這些事情,完全可以把每個階段看做一個切面,任何一個階段只關(guān)注輸入和輸出而不關(guān)注上個階段是如何產(chǎn)出的。

經(jīng)過仔細的思考,最終設置了兩個核心的過程,包裹模塊內(nèi)容 和 編譯文件結(jié)果。

包裹模塊內(nèi)容就是將字符串的文件結(jié)果包裹一下函數(shù),專注于處理字符串結(jié)果,將普通文件的文本進行包裹。

編譯文件結(jié)果這一步就是將代碼結(jié)果編譯成 node 能夠直接識別的 js 而使得下一步沙盒環(huán)境進行執(zhí)行,每次通過文件結(jié)果動態(tài)在內(nèi)存進行編譯,從而使得下一步 js 的執(zhí)行。

  • 同步還是異步?

這個問題其實困擾了很久。最大的問題就是里面涉及了部分異步加載的問題,按照傳統(tǒng)前端的做法,這里一般都是使用 callback 或者 promise(async/await) 的方式,但這樣就會帶來一個很大的問題。

如果是 callback 的方式,那么意味著最終我的 require 可能得這樣調(diào)用:

var r = require("nedo");
var moduleA = r("./moduleA");
var moduleB = r("./moduleB");

function log(module) {
 // 所有執(zhí)行過程作為 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é)果
})();

說實話這種方式也顯得很愚蠢。不過中間我想了個方法,包裹函數(shù)時多包一層,包一個 IIFE 然后自執(zhí)行一個 async 的 wrapper,不過這樣的話 bootstrap 文件就必須還得手動包裹在 async 的函數(shù)中,子函數(shù)的問題解決了但是上層沒有解決,不夠完美。

其實后來仔細的思考了一下,造成這樣的問題的原因究其根本是因為 request 是 async 的,這就導致了后續(xù)的代碼必須以 async 的方式出現(xiàn)。如果我們想要從硬盤讀取一個文件,那么我們可以使用 promise 包裹的 fs.readFile,當然我們也可以使用 fs.readFileSync 。前者的方法會讓后續(xù)的所有調(diào)用都變成異步,而后者的代碼還是同步,雖然性能很差但是完全符合直覺。

所以就必須找到一個 sync 的 request 的形式,才能讓最終調(diào)用變的完美,最終的想法結(jié)果應該如下:

var r = require("nedo");
var moduleA = r("./moduleA");
// moduleA 結(jié)果

var moduleB = r("https://baidu.com");
// moduleB 結(jié)果,同步阻塞

思考了半天不知道 sync 的 request 應該怎么寫,后來只得求助萬能的 npmjs,結(jié)果真的發(fā)現(xiàn)了一個 sync-request 的包,仔細研究了一下代碼發(fā)現(xiàn)核心是借助了 sync-rpc 這個包,雖然這個包 github 只有 5 個 star,下載量也不大。但是感覺卻是非常的厲害,能夠?qū)⑷魏萎惒降拇a轉(zhuǎn)化為同步調(diào)用的形式,戰(zhàn)略性 star,日后可能大有所為…

怎么使node也支持從url加載一個module詳解

  • runtime 編譯

解決了 request async 的問題之后其他問題都變的非常簡單,ts 使用 babel + ts preset 在內(nèi)存中完成了編譯,如果想要增加任何文件的支持,只需要在 lib/compile 下加入對應的文件后綴即可,在內(nèi)存中只要能夠完成編譯就能夠最終保證代碼結(jié)果。

  • top level await

在之前的過程中我們只是包了一層注入?yún)?shù)的函數(shù)進去,當然也可以上層包裹一層 async 函數(shù),這樣就可以在使用 nedo require 的包內(nèi)部直接使用頂層 await,不需要再使用 async 進行包裹

以上是“怎么使node也支持從url加載一個module詳解”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學習更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI