成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

手動(dòng)實(shí)現(xiàn)一個(gè) JavaScript 模塊執(zhí)行器

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

 [[346623]]

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

 

  1. module.exports = {  
  2.   name : 'ConardLi'
  3.   action : function(){ 
  4.     console.log(this.name); 
  5.   } 
  6. }; 

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

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

 

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

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

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

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

node Module 模塊加載原理

 

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

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

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

 

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

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

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

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

 

  1. Module.wrap = function(script) { 
  2.   return Module.wrapper[0] + script + Module.wrapper[1]; 
  3. }; 
  4.  
  5. Module.wrapper = [ 
  6.   '(function (exports, require, module, __filename, __dirname) { '
  7.   '\n});' 
  8. ]; 

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

 

  1. (function(global){ 
  2.   // 執(zhí)行代碼。。 
  3. })(window) 

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

 

  1. (function (exports, require, module, __filename, __dirname) { 
  2.   module.exports = {  
  3.     name : 'ConardLi'
  4.     action : function(){ 
  5.      console.log(this.name); 
  6.    } 
  7.   }; 
  8. }); 

編譯封裝函數(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 對象。

 

  1. var compiledWrapper = vm.runInThisContext(wrapper, {  
  2.   filename: filename, 
  3.   lineOffset: 0, 
  4.   displayErrors: true 
  5. }); 

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

 

  1. (function (exports, require, module, __filename, __dirname) { 
  2.   module.exports = {  
  3.     name : 'ConardLi'
  4.     action : function(){ 
  5.      console.log(this.name); 
  6.    } 
  7.   }; 
  8. }); 

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

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

 

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

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

實(shí)現(xiàn) Module 模塊

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

 

  1. export default class Module { 
  2.   exports = {} 
  3.   wrapper = [ 
  4.     'return (function (exports, module) { '
  5.     '\n});' 
  6.   ]; 
  7.  
  8.   wrap(script) { 
  9.     return `${this.wrapper[0]} ${script} ${this.wrapper[1]}`; 
  10.   }; 
  11.  
  12.   compile(content) { 
  13.     const wrapper = this.wrap(content); 
  14.     const compiledWrapper = vm.runInContext(wrapper); 
  15.     compiledWrapper.call(this.exports, this.exports, this); 
  16.   } 

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

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

 

eval

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

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

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

new Function()

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

 

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

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

  • fn 是一段編譯好的代碼,可以直接執(zhí)行,而 eval 需要編譯一次
  • fn 沒有對所在閉包的作用域訪問權(quán)限,不過它依然能夠訪問全局作用域

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

with 關(guān)鍵詞

 

 

 

 

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

 

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

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

Proxy

 

 

 

 

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

 

  1. function compileCode(code) { 
  2.   code = 'with (sandbox) {' + code + '}'
  3.   const fn = new Function('sandbox', code); 
  4.   return (sandbox) => { 
  5.     const proxy = new Proxy(sandbox, { 
  6.       has() { 
  7.         return true;  
  8.       } 
  9.     }); 
  10.     return fn(proxy); 
  11.   } 

Symbol.unscopables

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

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

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

 

  1. function compileCode(code) { 
  2.   code = 'with (sandbox) {' + code + '}'
  3.   const fn = new Function('sandbox', code); 
  4.   return (sandbox) => { 
  5.     const proxy = new Proxy(sandbox, { 
  6.       has() { 
  7.         return true;  
  8.       }, 
  9.       get(target, key, receiver) { 
  10.         if (key === Symbol.unscopables) { 
  11.           return undefined;  
  12.         } 
  13.         Reflect.get(target, key, receiver); 
  14.       } 
  15.     }); 
  16.     return fn(proxy); 
  17.   } 

全局變量白名單

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

 

  1. const ALLOW_LIST = ['console']; 
  2.  
  3. function compileCode(code) { 
  4.   code = 'with (sandbox) {' + code + '}'
  5.   const fn = new Function('sandbox', code); 
  6.   return (sandbox) => { 
  7.     const proxy = new Proxy(sandbox, { 
  8.       has() { 
  9.         if (!ALLOW_LIST.includes(key)) { 
  10.             return true
  11.         } 
  12.       }, 
  13.       get(target, key, receiver) { 
  14.         if (key === Symbol.unscopables) { 
  15.           return undefined;  
  16.         } 
  17.         Reflect.get(target, key, receiver); 
  18.       } 
  19.     }); 
  20.     return fn(proxy); 
  21.   } 

最終代碼:

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

 

  1. const ALLOW_LIST = ['console']; 
  2.  
  3. export default class Module { 
  4.  
  5.   exports = {} 
  6.   wrapper = [ 
  7.     'return (function (exports, module) { '
  8.     '\n});' 
  9.   ]; 
  10.  
  11.   wrap(script) { 
  12.     return `${this.wrapper[0]} ${script} ${this.wrapper[1]}`; 
  13.   }; 
  14.  
  15.   runInContext(code) { 
  16.     code = `with (sandbox) { ${code}  }`; 
  17.     const fn = new Function('sandbox', code); 
  18.     return (sandbox) => { 
  19.       const proxy = new Proxy(sandbox, { 
  20.         has(target, key) { 
  21.           if (!ALLOW_LIST.includes(key)) { 
  22.             return true
  23.           } 
  24.         }, 
  25.         get(target, key, receiver) { 
  26.           if (key === Symbol.unscopables) { 
  27.             return undefined; 
  28.           } 
  29.           Reflect.get(target, key, receiver); 
  30.         } 
  31.       }); 
  32.       return fn(proxy); 
  33.     } 
  34.   } 
  35.  
  36.   compile(content) { 
  37.     const wrapper = this.wrap(content); 
  38.     const compiledWrapper = this.runInContext(wrapper)({}); 
  39.     compiledWrapper.call(this.exports, this.exports, this); 
  40.   } 

測試執(zhí)行效果:

 

  1. function getModuleFromString(code) { 
  2.   const scanModule = new Module(); 
  3.   scanModule.compile(code); 
  4.   return scanModule.exports; 
  5.  
  6. const module = getModuleFromString(` 
  7. module.exports = {  
  8.   name : 'ConardLi'
  9.   action : function(){ 
  10.     console.log(this.name); 
  11.   } 
  12. }; 
  13. `); 
  14.  
  15. module.action(); // ConardLi 

 

責(zé)任編輯:華軒 來源: code秘密花園
相關(guān)推薦

2022-05-05 08:43:22

SQL執(zhí)行器參數(shù)

2021-06-25 10:38:05

JavaScript編譯器前端開發(fā)

2022-04-29 08:41:40

開發(fā)應(yīng)用程序執(zhí)行器

2020-06-11 08:48:49

JavaScript開發(fā)技術(shù)

2025-07-03 00:28:41

2018-09-18 10:11:21

前端vue.jsjavascript

2023-08-24 10:24:54

GitLabPodman

2017-07-13 17:00:17

內(nèi)置執(zhí)行器開發(fā)

2014-04-14 15:54:00

print()Web服務(wù)器

2024-08-09 08:55:43

if執(zhí)行器版本

2017-03-15 08:43:29

JavaScript模板引擎

2017-03-20 17:59:19

JavaScript模板引擎

2020-11-30 06:20:13

javascript

2023-04-17 09:08:27

CSS計(jì)時(shí)器

2016-11-08 18:53:08

編譯器

2022-10-20 11:00:52

SQL解析器

2015-04-16 09:38:23

2009-06-09 21:50:55

Javascript函數(shù)getStyle

2012-07-18 11:31:02

ibmdw

2016-11-29 13:31:52

JavaScriptsetTimeout定時(shí)執(zhí)行
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 精品久久久久久 | 亚洲国产二区 | 粉嫩一区二区三区性色av | 夜久久| 日韩最新网站 | 一区二区三区中文字幕 | 中文字幕乱码一区二区三区 | 国产精品久久久久久久久久 | 中文字幕成人网 | 六月成人网 | 97狠狠干| 久久久久久久久久久成人 | 国产三级 | 国产在线精品一区二区三区 | 国产精品一区在线观看 | 久久国产精品偷 | 国产精品久久一区二区三区 | 国产美女永久免费无遮挡 | 一级片aaa| 在线色网站 | 九九热免费视频在线观看 | 精品久久久网站 | 亚洲欧美在线视频 | 国产免费一区二区三区网站免费 | 精品免费国产一区二区三区四区 | av电影一区 | 91在线精品秘密一区二区 | 免费人成在线观看网站 | 国产精品一区二区三级 | 国产一区免费 | 国产午夜精品一区二区三区四区 | www.国产精| 久久精品小视频 | 久久国内精品 | 国产福利91精品一区二区三区 | 日韩一区二区在线观看视频 | 伊人伊人 | 欧美一级欧美三级在线观看 | 精品欧美一区二区在线观看视频 | 亚洲免费网址 | 午夜免费电影 |