国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

如何使node也支持從url加載一個module詳解

2019-11-19 13:43:03
字體:
來源:轉載
供稿:網友

前言

最近兩天 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 的這張流程圖

  • 加載文件

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

  • 拼接函數

在上一步中獲取到的只是代碼的文本形式源文件,并不具有執行能力。在接下來的步驟中需要將它變為一個可執行的代碼段。

如果有同學看過 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,日后可能大有所為…

  • 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,提供更多建設性的意見…

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 金川县| 旺苍县| 洞口县| 滕州市| 天祝| 柘荣县| 平利县| 大关县| 上犹县| 石河子市| 丰都县| 彭阳县| 新闻| 威海市| 栾城县| 常山县| 饶阳县| 应用必备| 开封市| 盐池县| 中阳县| 彩票| 遂平县| 奈曼旗| 无锡市| 永登县| 拉萨市| 南京市| 社会| 阿鲁科尔沁旗| 沐川县| 大渡口区| 文登市| 邯郸市| 陇川县| 绩溪县| 报价| 延寿县| 丰都县| 阳东县| 舟山市|