溫馨提示×

溫馨提示×

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

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

如何手動實現(xiàn)一個 JavaScript 模塊執(zhí)行器

發(fā)布時間:2021-09-30 13:58:11 來源:億速云 閱讀:121 作者:柒染 欄目:web開發(fā)

今天就跟大家聊聊有關(guān)如何手動實現(xiàn)一個 JavaScript 模塊執(zhí)行器,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

如果給你下面這樣一個代碼片段(動態(tài)獲取的代碼字符串),讓你在前端動態(tài)引入這個模塊并執(zhí)行里面的函數(shù),你會如何處理呢?

module.exports = {    name : 'ConardLi',   action : function(){     console.log(this.name);   } };

node 環(huán)境的執(zhí)行

如果在 node 環(huán)境,我們可能會很快的想到使用 Module 模塊, Module 模塊中有一個私有函數(shù)  _compile,可以動態(tài)的加載一個模塊:

export function getRuleFromString(code) {   const myModule = new Module('my-module');   myModule._compile(code,'my-module');   return myModule.exports; }

實現(xiàn)就是這么簡單,后面我們會回顧一下 _compile 函數(shù)的原理,但是需求可不是這么簡單,我們?nèi)绻谇岸谁h(huán)境動態(tài)引入這段代碼呢?

嗯,你沒聽錯,最近正好碰到了這樣的需求,需要在前端和 Node 端抹平動態(tài)引入模塊的邏輯,好,下面我們來模仿 Module 模塊實現(xiàn)一個前端環(huán)境的  JavaScript 模塊執(zhí)行器。

首先我們先來回顧一下 node 中的模塊加載原理。

node Module 模塊加載原理

Node.js 遵循 CommonJS 規(guī)范,該規(guī)范的核心思想是允許模塊通過 require 方法來同步加載所要依賴的其他模塊,然后通過 exports  或 module.exports 來導(dǎo)出需要暴露的接口。其主要是為了解決 JavaScript  的作用域問題而定義的模塊形式,可以使每個模塊它自身的命名空間中執(zhí)行。

再在每個 NodeJs 模塊中,我們都能取到 module、exports、__dirname、__filename 和 require  這些模塊。并且每個模塊的執(zhí)行作用域都是相互隔離的,互不影響。

其實上面整個模塊系統(tǒng)的核心就是 Module 類的 _compile 方法,我們直接來看 _compile 的源碼:

Module.prototype._compile = function(content, filename) {   // 去除 Shebang 代碼   content = internalModule.stripShebang(content);    // 1.創(chuàng)建封裝函數(shù)   var wrapper = Module.wrap(content);     // 2.在當(dāng)前上下文編譯模塊的封裝函數(shù)代碼   var compiledWrapper = vm.runInThisContext(wrapper, {      filename: filename,     lineOffset: 0,     displayErrors: true   });    var dirname = path.dirname(filename);   var require = internalModule.makeRequireFunction(this);    var depth = internalModule.requireDepth;      // 3.運(yùn)行模塊的封裝函數(shù)并傳入 module、exports、__dirname、__filename、require    var result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);   return result; };

整個執(zhí)行過程我將其分為三步:

創(chuàng)建封裝函數(shù)

第一步即調(diào)用 Module 內(nèi)部的 wrapper 函數(shù)對模塊的原始內(nèi)容進(jìn)行封裝,我們先來看看 wrapper 函數(shù)的實現(xiàn):

Module.wrap = function(script) {   return Module.wrapper[0] + script + Module.wrapper[1]; };  Module.wrapper = [   '(function (exports, require, module, __filename, __dirname) { ',   '\n});' ];

CommonJS 的主要目的就是解決 JavaScript  的作用域問題,可以使每個模塊它自身的命名空間中執(zhí)行。在沒有模塊化方案的時候,我們一般會創(chuàng)建一個自執(zhí)行函數(shù)來避免變量污染:

(function(global){   // 執(zhí)行代碼。。 })(window)

所以這一步至關(guān)重要,首先 wrapper  函數(shù)就將模塊本身的代碼片段包裹在一個函數(shù)作用域內(nèi),并且將我們需要用到的對象作為參數(shù)引入。所以上面的代碼塊被包裹后就變成了:

(function (exports, require, module, __filename, __dirname) {   module.exports = {      name : 'ConardLi',     action : function(){      console.log(this.name);    }   }; });

編譯封裝函數(shù)代碼

NodeJs 中的 vm 模塊提供了一系列 API 用于在 V8 虛擬機(jī)環(huán)境中編譯和運(yùn)行代碼。JavaScript  代碼可以被編譯并立即運(yùn)行,或編譯、保存然后再運(yùn)行。

vm.runInThisContext() 在當(dāng)前的 global 對象的上下文中編譯并執(zhí)行  code,最后返回結(jié)果。運(yùn)行中的代碼無法獲取本地作用域,但可以獲取當(dāng)前的 global 對象。

var compiledWrapper = vm.runInThisContext(wrapper, {    filename: filename,   lineOffset: 0,   displayErrors: true });

所以以上代碼執(zhí)行后,就將代碼片段字符串編譯成了一個真正的可執(zhí)行函數(shù):

(function (exports, require, module, __filename, __dirname) {   module.exports = {      name : 'ConardLi',     action : function(){      console.log(this.name);    }   }; });

運(yùn)行封裝函數(shù)

最后通過 call 來執(zhí)行編譯得到的可執(zhí)行函數(shù),并傳入對應(yīng)的對象。

var result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);

所以看到這里你應(yīng)該會明白,我們在模塊中拿到的 module,就是 Module 模塊的實例本身,我們直接調(diào)用的 exports 實際上是  module.exports 的引用,所以我們既可以使用 module.exports 也可以使用 exports 來導(dǎo)出一個模塊。

實現(xiàn) Module 模塊

如果我們想在前端環(huán)境執(zhí)行一個 CommonJS 模塊,那么我們只需要手動實現(xiàn)一個 Module  模塊就好了,重新梳理上面的流程,如果只考慮模塊代碼塊動態(tài)引入的邏輯,我們可以抽象出下面的代碼:

export default class Module {   exports = {}   wrapper = [     'return (function (exports, module) { ',     '\n});'   ];    wrap(script) {     return `${this.wrapper[0]} ${script} ${this.wrapper[1]}`;   };    compile(content) {     const wrapper = this.wrap(content);     const compiledWrapper = vm.runInContext(wrapper);     compiledWrapper.call(this.exports, this.exports, this);   } }

這里有個問題,在瀏覽器環(huán)境是沒有 VM 這個模塊的,VM  會將代碼加載到一個上下文環(huán)境中,置入沙箱(sandbox),讓代碼的整個操作執(zhí)行都在封閉的上下文環(huán)境中進(jìn)行,我們需要自己實現(xiàn)一個瀏覽器環(huán)境的沙箱。

實現(xiàn)瀏覽器沙箱

eval

在瀏覽器執(zhí)行一段代碼片段,我們首先想到的可能就是 eval, eval 函數(shù)可以將一個 Javascript 字符串視作代碼片段執(zhí)行。

但是,由 eval() 執(zhí)行的代碼能夠訪問閉包和全局作用域,這會導(dǎo)致被稱為代碼注入 code injection 的安全隱患, eval  雖然好用,但是經(jīng)常被濫用,是 JavaScript 最臭名昭著的功能之一。

所以,后來又出現(xiàn)了很多在沙箱而非全局作用域中的執(zhí)行字符串代碼的值的替代方案。

new Function()

Function 構(gòu)造器是 eval() 的一個替代方案。new Function(...args, 'funcBody') 對傳入的  'funcBody' 字符串進(jìn)行求值,并返回執(zhí)行這段代碼的函數(shù)。

fn = new Function(...args, 'functionBody');

返回的 fn 是一個定義好的函數(shù),最后一個參數(shù)為函數(shù)體。它和 eval 有兩點(diǎn)區(qū)別:

  • fn 是一段編譯好的代碼,可以直接執(zhí)行,而 eval 需要編譯一次

  • fn 沒有對所在閉包的作用域訪問權(quán)限,不過它依然能夠訪問全局作用域

但是這仍然不能解決訪問全局作用域的問題。

with 關(guān)鍵詞

如何手動實現(xiàn)一個 JavaScript 模塊執(zhí)行器

with 是 JavaScript 一個冷門的關(guān)鍵字。它允許一個半沙箱的運(yùn)行環(huán)境。with  代碼塊中的代碼會首先試圖從傳入的沙箱對象獲得變量,但是如果沒找到,則會在閉包和全局作用域中尋找。閉包作用域的訪問可以用new Function()  來避免,所以我們只需要處理全局作用域。with 內(nèi)部使用 in 運(yùn)算符。在塊中訪問每個變量,都會使用 variable in sandbox  條件進(jìn)行判斷。若條件為真,則從沙箱對象中讀取變量。否則,它會在全局作用域中尋找變量。

function compileCode(src) {   src = 'with (sandbox) {' + src + '}'   return new Function('sandbox', src) }

試想,如果 variable in sandbox  條件永遠(yuǎn)為真,沙箱環(huán)境不就永遠(yuǎn)也讀取不到環(huán)境變量了嗎?所以我們需要劫持沙箱對象的屬性,讓所有的屬性永遠(yuǎn)都能讀取到。

Proxy

如何手動實現(xiàn)一個 JavaScript 模塊執(zhí)行器

ES6 中提供了一個 Proxy 函數(shù),它是訪問對象前的一個攔截器,我們可以利用 Proxy 來攔截 sandbox  的屬性,讓所有的屬性都可以讀取到:

function compileCode(code) {   code = 'with (sandbox) {' + code + '}';   const fn = new Function('sandbox', code);   return (sandbox) => {     const proxy = new Proxy(sandbox, {       has() {         return true;        }     });     return fn(proxy);   } }

Symbol.unscopables

Symbol.unscopables 是一個著名的標(biāo)記。一個著名的標(biāo)記即是一個內(nèi)置的 JavaScript  Symbol,它可以用來代表內(nèi)部語言行為。

Symbol.unscopables 定義了一個對象的 unscopable(不可限定)屬性。在 with 語句中,不能從 Sandbox 對象中檢索  Unscopable 屬性,而是直接從閉包或全局作用域檢索屬性。

所以我們需要對 Symbol.unscopables 這種情況做一次加固,

function compileCode(code) {   code = 'with (sandbox) {' + code + '}';   const fn = new Function('sandbox', code);   return (sandbox) => {     const proxy = new Proxy(sandbox, {       has() {         return true;        },       get(target, key, receiver) {         if (key === Symbol.unscopables) {           return undefined;          }         Reflect.get(target, key, receiver);       }     });     return fn(proxy);   } }

全局變量白名單

但是,這時沙箱里是執(zhí)行不了瀏覽器默認(rèn)為我們提供的各種工具類和函數(shù)的,它只能作為一個沒有任何副作用的純函數(shù),當(dāng)我們想要使用某些全局變量或類時,可以自定義一個白名單:

const ALLOW_LIST = ['console'];  function compileCode(code) {   code = 'with (sandbox) {' + code + '}';   const fn = new Function('sandbox', code);   return (sandbox) => {     const proxy = new Proxy(sandbox, {       has() {         if (!ALLOW_LIST.includes(key)) {             return true;         }       },       get(target, key, receiver) {         if (key === Symbol.unscopables) {           return undefined;          }         Reflect.get(target, key, receiver);       }     });     return fn(proxy);   } }

最終代碼:

好了,總結(jié)上面的代碼,我們就完成了一個簡易的 JavaScript 模塊執(zhí)行器:

const ALLOW_LIST = ['console'];  export default class Module {    exports = {}   wrapper = [     'return (function (exports, module) { ',     '\n});'   ];    wrap(script) {     return `${this.wrapper[0]} ${script} ${this.wrapper[1]}`;   };    runInContext(code) {     code = `with (sandbox) { ${code}  }`;     const fn = new Function('sandbox', code);     return (sandbox) => {       const proxy = new Proxy(sandbox, {         has(target, key) {           if (!ALLOW_LIST.includes(key)) {             return true;           }         },         get(target, key, receiver) {           if (key === Symbol.unscopables) {             return undefined;           }           Reflect.get(target, key, receiver);         }       });       return fn(proxy);     }   }    compile(content) {     const wrapper = this.wrap(content);     const compiledWrapper = this.runInContext(wrapper)({});     compiledWrapper.call(this.exports, this.exports, this);   } }

測試執(zhí)行效果:

function getModuleFromString(code) {   const scanModule = new Module();   scanModule.compile(code);   return scanModule.exports; }  const module = getModuleFromString(` module.exports = {    name : 'ConardLi',   action : function(){     console.log(this.name);   } }; `);  module.action(); // ConardLi

看完上述內(nèi)容,你們對如何手動實現(xiàn)一個 JavaScript 模塊執(zhí)行器有進(jìn)一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問一下細(xì)節(jié)

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

AI