<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 7462字,需瀏覽 15分鐘

           ·

          2020-10-20 19:34


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

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

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

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

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

          實(shí)現(xiàn)就是這么簡(jiǎn)單,后面我們會(huì)回顧一下 _compile 函數(shù)的原理,但是需求可不是這么簡(jiǎn)單,我們?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 方法來同步加載所要依賴的其他模塊,然后通過 exportsmodule.exports 來導(dǎo)出需要暴露的接口。其主要是為了解決 JavaScript 的作用域問題而定義的模塊形式,可以使每個(gè)模塊它自身的命名空間中執(zhí)行。

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

          其實(shí)上面整個(gè)模塊系統(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;
          };

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

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

          第一步即調(diào)用 Module 內(nèi)部的 wrapper 函數(shù)對(duì)模塊的原始內(nèi)容進(jìn)行封裝,我們先來看看 wrapper 函數(shù)的實(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 的作用域問題,可以使每個(gè)模塊它自身的命名空間中執(zhí)行。在沒有模塊化方案的時(shí)候,我們一般會(huì)創(chuàng)建一個(gè)自執(zhí)行函數(shù)來避免變量污染:

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

          所以這一步至關(guān)重要,首先 wrapper 函數(shù)就將模塊本身的代碼片段包裹在一個(gè)函數(shù)作用域內(nèi),并且將我們需要用到的對(duì)象作為參數(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 對(duì)象的上下文中編譯并執(zhí)行 code,最后返回結(jié)果。運(yùn)行中的代碼無法獲取本地作用域,但可以獲取當(dāng)前的 global 對(duì)象。

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

          所以以上代碼執(zhí)行后,就將代碼片段字符串編譯成了一個(gè)真正的可執(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ù),并傳入對(duì)應(yīng)的對(duì)象。

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

          所以看到這里你應(yīng)該會(huì)明白,我們?cè)谀K中拿到的 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)引入的邏輯,我們可以抽象出下面的代碼:

          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);
          ??}
          }

          這里有個(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í)行一段代碼片段,我們首先想到的可能就是 evaleval 函數(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') 對(duì)傳入的 'funcBody' 字符串進(jìn)行求值,并返回執(zhí)行這段代碼的函數(shù)。

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

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

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

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

          with 關(guān)鍵詞

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

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

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

          Proxy

          ES6 中提供了一個(gè) Proxy 函數(shù),它是訪問對(duì)象前的一個(gè)攔截器,我們可以利用 ?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 是一個(gè)著名的標(biāo)記。一個(gè)著名的標(biāo)記即是一個(gè)內(nèi)置的 JavaScript Symbol,它可以用來代表內(nèi)部語言行為。

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

          所以我們需要對(duì) 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);
          ??}
          }

          全局變量白名單

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

          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é)上面的代碼,我們就完成了一個(gè)簡(jiǎn)易的 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);
          ??}
          }

          測(cè)試執(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

          關(guān)于本文,你還有什么想問的?歡迎留言討論,如果文章對(duì)你有幫助,希望你能不吝嗇的獻(xiàn)出【在看、點(diǎn)贊、分享】三連。

          瀏覽 68
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  男女趴趴网站 | 水蜜桃在线观看视频 | 吸咬奶头狂揉60分钟视频 | 激情 小说 图片 亚洲 伦 | 九色PORNY原创自拍 |