您好,登錄后才能下訂單哦!
小編給大家分享一下怎么使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 的這張流程圖
加載文件
確認了路徑并且確保了文件存在之后,加載文件這一步就簡單粗暴的多。最簡單的方式就是直接讀取硬盤上的文件,將純文本的模塊源代碼讀取至內(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,日后可能大有所為…
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è)資訊頻道!
免責聲明:本站發(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)容。