自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

微前端代碼隔離方案,手把手實(shí)現(xiàn)一個 JS 沙箱隔離!

開發(fā) 前端
js沙箱大量應(yīng)用在微前端框架執(zhí)行子應(yīng)用代碼的場景中。我們這里采用最簡單的方式來模擬一下這個場景。

今天我們一起來探究一下前端 js 沙箱的核心實(shí)現(xiàn)邏輯,我們將從以下幾個方面來展開討論:

  1. 準(zhǔn)備調(diào)試環(huán)境,探究沙箱需要解決的問題。
  2. 創(chuàng)建沙箱環(huán)境。
  3. 通過 with 語句改變沙箱變量作用域鏈。
  4. 通過 proxy 攔截 with 上下文的get,set操作。

這幾個方面一步一步實(shí)現(xiàn)一個簡易的js沙箱。

準(zhǔn)備調(diào)試環(huán)境,探究沙箱需要解決的問題:

js沙箱大量應(yīng)用在微前端框架執(zhí)行子應(yīng)用代碼的場景中。我們這里采用最簡單的方式來模擬一下這個場景。

我們準(zhǔn)備一個js文件,這個js文件的內(nèi)容就模擬微前端應(yīng)用基座應(yīng)用的執(zhí)行環(huán)境,在里面通過 var 定義一個全局變量:

圖片圖片

我們準(zhǔn)備兩段字符串,字符串的內(nèi)容就是js代碼,我們假設(shè)這兩段代碼就分別代表了兩個子應(yīng)用的代碼:

圖片圖片

假如現(xiàn)在微前端框架需要執(zhí)行子應(yīng)用1的代碼,那么就會 fetch 子應(yīng)用1的靜態(tài)資源服務(wù)器,獲取到類似于 subCode1 這樣的一個代碼字符串。執(zhí)行子應(yīng)用2也是同理。那么現(xiàn)在第一個問題來了,怎樣自動執(zhí)行字符串內(nèi)部的js代碼?在js中有兩種比較常見的方式:

  1. Function:
  2. eval

Function 是js提供的一個內(nèi)置構(gòu)造函數(shù),基于它,可以通過字符串,創(chuàng)建出一個js函數(shù),因此我們可以這樣嘗試:

圖片圖片

因?yàn)?Function(...)本身就是一個表達(dá)式,所以我們可以直接進(jìn)行調(diào)用:

圖片圖片

我們查看一下控制臺:

圖片圖片

可以看到子應(yīng)用1的代碼就被成功執(zhí)行了,我們嘗試子應(yīng)用2的代碼也是同理。

eval是js內(nèi)置的一個函數(shù),這個函數(shù)接收一個字符串,eval函數(shù)會將這個字符串作為標(biāo)準(zhǔn)的js代碼進(jìn)行執(zhí)行:

圖片圖片

我們查看輸出:

圖片圖片

可以看到子應(yīng)用代碼同樣被正常執(zhí)行了。 這樣很方便呀,實(shí)際上微前端框架內(nèi)部也是使用類似的方式來執(zhí)行子應(yīng)用的js代碼的,但是這樣會有什么問題呢?

  1. 目前子應(yīng)用中的變量定義會污染全局環(huán)境:

目前我們在子應(yīng)用中定義的變量在全局環(huán)境中可以直接訪問,所以我們需要調(diào)整一下 subCode1 以及 subCode2 兩個子應(yīng)用的測試代碼:

圖片圖片

我們將兩端子應(yīng)用的測試代碼包裹到了一個立即執(zhí)行函數(shù)中,這樣子應(yīng)用內(nèi)部定義的變量就不會污染全局了。大部分構(gòu)建工具構(gòu)建的產(chǎn)物也是這種方式。

  1. 在子應(yīng)用內(nèi)部可以赤裸裸的訪問 window 對象,并且可以直接對它們進(jìn)行操作:

圖片圖片

圖片圖片

我們可以直接在子應(yīng)用2中通過訪問 window 對象來改變 window 對象的內(nèi)容。如何攔截子應(yīng)用直接污染瀏覽器宿主環(huán)境的 window 對象是實(shí)現(xiàn)js沙箱最核心的問題。

創(chuàng)建沙箱環(huán)境

我們先編寫一個沙箱構(gòu)造器函數(shù):

var a = 1

const sandbox = () => {
  return (subCode1) => {
     
  }
}

我們編寫了一個高階函數(shù),這個高階函數(shù)返回了一個可以接收并且執(zhí)行子應(yīng)用代碼的函數(shù)。我們期望如果需要執(zhí)行子應(yīng)用的結(jié)果代碼,只需要這樣進(jìn)行調(diào)用:

const box = sandbox()

box(子應(yīng)用1代碼)
box(子應(yīng)用2代碼)
box(子應(yīng)用3代碼)

接下來就是要在高階函數(shù)中去編寫執(zhí)行子應(yīng)用的代碼了,我們沿著沙箱的需求來思考解決方案。 我們的需求就是屏蔽瀏覽器window對象,為了實(shí)現(xiàn)這個需求,我們可以在返回的高階函數(shù)中編寫如下的邏輯:

const boxCtx = {}

return (subCode1) => {
  const boxFn = `(function (window) {
    ${code}
  })(boxCtx)`
  // 通過eval函數(shù)執(zhí)行子應(yīng)用代碼
  eval(boxFn)
}

實(shí)際上就是我們在父函數(shù)中創(chuàng)建了一個沙箱的上下文環(huán)境,在高階函數(shù)中,將子應(yīng)用代碼放到一個立即執(zhí)行函數(shù)中去進(jìn)行執(zhí)行,立即執(zhí)行函數(shù)的參數(shù)就是一個window變量,這樣,當(dāng)我們在子應(yīng)用代碼內(nèi)部訪問 window 的時(shí)候,因?yàn)樽饔糜蜴湹脑?,就只會訪問到我們在沙箱環(huán)境中設(shè)置的 window 參數(shù),而無法訪問到瀏覽器全局的 window 對象了。我們可以測試一下:

sandbox()('(function() { window.testVal1 = "testVal1"; window.a = 2; })()')
console.log(' window.testVal1',  window.testVal1)

圖片圖片

這樣做了之后我們就已經(jīng)可以攔截子應(yīng)用中對于 window 對象屬性的 set 操作了。 但是如果我們在子應(yīng)用中嘗試直接通過變量通過 a 來訪問剛剛設(shè)置的值的時(shí)候,卻是這樣的結(jié)果:

sandbox('(function() { window.a = 2; console.log("ssss", a); })()')

圖片圖片

依然訪問的是全局變量a,原因其實(shí)很簡單,因?yàn)槲覀兡壳皹?gòu)建的沙箱函數(shù)內(nèi)部只有一個 window 變量,并沒有 a 變量。所以js在執(zhí)行這里的時(shí)候只能沿著作用域鏈訪問到全局作用域中的a變量了。要解決這個問題,我們必須確保,我們設(shè)置的 window 對象上的所有的屬性全部掛載到沙箱的作用域中。

通過 with語句改變沙箱變量作用域鏈:

with 語句的能力可以這樣理解:在js運(yùn)行時(shí)將指定代碼段的執(zhí)行上下文設(shè)置為指定的對象,從而調(diào)整該代碼端的作用域鏈。 使用with,我們可以這樣改動并且測試沙箱代碼:

const boxCtx = { b: "b" }

return (subCode1) => {
  const boxFn = `(function boxFn (window) {
    with(window) { ${code} }
  })(boxCtx)`
  // 通過eval函數(shù)執(zhí)行子應(yīng)用代碼
  eval(boxFn)
}
sandbox('(function sub1() { window.a = 2; console.log("b", b) })()')

圖片圖片

此時(shí) with 語句使得沙箱部分的作用域鏈變成了類似于這樣的結(jié)構(gòu):

sub1(子應(yīng)用函數(shù)上下文) ---> window(with語句設(shè)置的代碼上下文) ---> boxFn 函數(shù)執(zhí)行上下文 ---> 全局執(zhí)行上下文

因此當(dāng)我們直接在子應(yīng)用內(nèi)部訪問 a 變量的時(shí)候:

圖片圖片

就可以正常訪問子應(yīng)用內(nèi)部設(shè)置的全局變量了。

但是依然有問題沒有解決,比如我們嘗試執(zhí)行以下的函數(shù):

sandbox('(function() { window.a = 2; window.console.log("ssss", a); })()')

圖片圖片

原因其實(shí)很簡單,因?yàn)榇藭r(shí)的沙箱的window對象并沒有 console 屬性,要解決這個問題,我們可以嘗試擴(kuò)展以下沙箱的上下文的內(nèi)容,一個很暴力的解法就是:

const boxCtx = {
    console
  }

我們可能會想到一個更加簡單的擴(kuò)展沙箱上下文的方法:

const boxCtx = {
    ...window
  }

這樣做了之后我們再利用 window.console 來輸出 a 的值:

圖片圖片

為什么會是1呢?如果我們此時(shí)在控制臺中訪問一下window.a的值會看到一個更加詭異的現(xiàn)象:

圖片圖片

全局的window的a屬性被修改了。

全局window的a屬性被修改的原因其實(shí)很簡單,因?yàn)闉g覽器宿主的 window 上嵌套了一個 window 屬性,并且這個屬性指向了全局window對象,因此window.window是一個無限嵌套的循環(huán)引用:

圖片圖片

正是因?yàn)檫@樣,所以當(dāng)我們直接使用 ... 淺拷貝一個 window 對象的時(shí)候,實(shí)際上 嵌套的 window 屬性也就被拷貝過來了,因此此時(shí)沙箱上下文是一個類似于這樣的對象:

{
  ...,
  window: {
    ...,
    window
  }
}

因此,當(dāng)沙箱中查找 window 變量的時(shí)候是可以直接在 boxCtx 上查找到,而查找的結(jié)果就是全局 window 對象的淺拷貝。因此就導(dǎo)致了我們在沙箱內(nèi)部對于 window 的所有操作實(shí)際上操作在了全局window對象上。因此直接拷貝 window 對象來擴(kuò)展沙箱上下文的方式是不行的。我們得找其他更加簡單的方案。

通過 proxy 攔截 with 上下文的get,set操作:

我們可以提出這樣的設(shè)想,我們在沙箱內(nèi)部通過 window.xxx 進(jìn)行 get 的時(shí)候,如果 xxx 在沙箱內(nèi)部的 window 上存在,那么就直接使用該值,如果不存在,就前往 window 對象上去查找 xxx 的值,這樣是不是就可以解決這個問題了呢?而做到這一步的關(guān)鍵就是攔截沙箱上下文的 get 操作,因此首先我們將傳遞給 with 語句的上下文對象變成一個 Proxy 代理對象:

const createSandboxCtxProxy = () => {
  return new Proxy({}, {
    get(target, key, receiver) {
      
    },
    set(target, key, value, receiver) {

    }
  })
}

const boxCtx = createSandboxCtxProxy()

緊接著可以這樣去攔截 get 和 set 操作:

get(target, key, receiver) {
      console.log('get', key)
      //優(yōu)先從代理對象上取值
      if(Reflect.has(target,key)){
         return Reflect.get(target,key);
      }

      //如果找不到,就直接從window對象上取值
      const rawValue = Reflect.get(window,key);
      //其他情況直接返回
      return rawValue
    },
    set(target, key, value, receiver) {
      return Reflect.set(target, key, value)
    },

這樣操作之后我們再次來進(jìn)行測試:

sandbox('(function() { window.a = 2; window.b = "b"; })()')

圖片圖片

這樣就達(dá)到了和改造之前一樣的效果了,緊接著我們這樣測試:

sandbox('(function() { window.a = 2; window.b = "b"; window.console.log("a,b", a, b) })()')

圖片圖片

可以看到 console 對象已經(jīng)可以正常的訪問到了。那么是不是就大功告成了呢?我們再來試一下 window.alert 這類方法:

sandbox('(function() { window.a = 2; window.b = "b"; window.alert(`a, b: ${a}, $`) })()')

圖片圖片

這個錯誤是因?yàn)闉g覽器 window 上的方法在調(diào)用的時(shí)候函數(shù)內(nèi)部的 this 一定要指向?yàn)g覽器 window 對象。比如我們使用一個更好理解的方式來復(fù)原這個錯誤:

圖片圖片

定位到了錯誤的原因,我們就很容易可以解決了,只需要調(diào)整 get 方法:

get(target, key, receiver) {
      console.log('get', key)
    //優(yōu)先從代理對象上取值
    if(Reflect.has(target,key)){
      return Reflect.get(target,key);
    }

    //如果找不到,就直接從window對象上取值
    const rawValue = Reflect.get(window,key);

    //如果兜底的是一個函數(shù),需要綁定window對象,比如window.addEventListener
    if(typeof rawValue === 'function'){
      const valueStr = rawValue.toString();
      if(!/^function\s+[A-Z]/.test(valueStr) && !/^class\s+/.test(valueStr)){
        return rawValue.bind(window); // 所有 window 上非構(gòu)造函數(shù)調(diào)用時(shí)候的 this 綁定window對象
      }
    }

    //其他情況直接返回
    return rawValue
    },

再次測試:

圖片圖片

問題已經(jīng)解決了。至此我們就一步步最簡化的實(shí)現(xiàn)了一個js沙箱。

責(zé)任編輯:武曉燕 來源: 程序員Sunday
相關(guān)推薦

2020-05-19 10:45:31

沙箱前端原生對象

2022-06-28 15:29:56

Python編程語言計(jì)時(shí)器

2022-09-22 12:38:46

antd form組件代碼

2022-11-07 18:36:03

組件RPC框架

2021-06-22 10:43:03

Webpack loader plugin

2020-08-12 09:07:53

Python開發(fā)爬蟲

2024-02-06 10:04:49

Express框架repo

2021-12-29 11:38:59

JS前端沙箱

2019-08-26 09:25:23

RedisJavaLinux

2020-12-02 12:29:24

Vue無限級聯(lián)樹形

2021-11-10 11:40:42

數(shù)據(jù)加解密算法

2023-11-24 16:57:53

2022-10-19 14:16:18

樣式隔離前綴css

2023-04-26 12:46:43

DockerSpringKubernetes

2014-01-22 09:19:57

JavaScript引擎

2017-07-19 13:27:44

前端Javascript模板引擎

2022-08-26 08:01:38

DashWebJavaScrip

2016-11-01 09:46:04

2020-12-15 08:58:07

Vue編輯器vue-cli

2021-09-26 16:08:23

CC++clang_forma
點(diǎn)贊
收藏

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