[譯]如何在 Web 上構建一個插件系統

原文:www.figma.com/blog/how-we…git

在 Figma,咱們最近解決了迄今爲止最大的工程挑戰之一:支持插件。 咱們的插件 API 使第三方開發人員能夠直接在基於瀏覽器的設計工具中運行代碼,所以團隊可使 Figma 適應本身的工做流程。他們能夠用可訪問性檢查器測量對比度,用翻譯應用程序轉換語言,進口商能夠用內容填充設計,以及其餘需求。
程序員

咱們必須仔細設計該插件的功能。在整個軟件歷史中,有不少第三方擴展對平臺產生負面影響的例子。在某些狀況下,他們拖慢了工具的運行速度,在其餘狀況下,每當平臺有新版本發佈時,插件就會中斷。咱們但願在可控範圍內,用戶對 Figma 有更好的插件體驗。github

此外,咱們但願確保插件對用戶而言是安全的,所以不能簡單地使用 eval(PLUGIN_CODE)——不安全的典型定義! 可是,本質上運行插件能夠歸結爲 eval。算法

更具挑戰性的是,Figma 創建在一個很是規的堆棧上,有一些其餘工具沒有的限制。其中,設計編輯器基於 WebGL 和 WebAssembly,部分用戶界面用 Typescript&React 實現,能夠多人同時編輯一個文件。咱們依賴於瀏覽器技術的支持,同時也受到它們的限制。編程

這篇博客將引導你實現一個完美的插件解決方案。最終,咱們的工做歸結爲一個問題:如何安全地、穩定地、高性能地運行插件? api

咱們考慮了不少不一樣路線的方法,進行了數週的討論、原型製做和頭腦風暴。這篇博客僅關注其中構成核心路徑的三種嘗試。跨域


嘗試1:<inline-iframe>沙箱

在最初幾周的研究中,咱們發現了許多有趣的嘗試,如 code-to-code 的轉換,可是,大多數未經生產環境應用程序驗證,存在必定的風險。瀏覽器

最後咱們嘗試了最接近標準沙箱的方法:<inline-iframe> 標籤,運行第三方代碼的應用中有用到,如 CodePen。安全

<inline-iframe> 不是普通的 HTML 標籤,要了解爲何它是安全的,有必要考慮一下須要保證哪些特性。<inline-iframe> 一般用於將一個網站嵌入另外一個網站,例如yelp.com 中嵌入的 Google Map。bash

在這裏,你不會但願 Yelp 僅經過嵌入就能讀取 Google 網站中的內容(可能有私人的用戶信息),一樣地,也不但願 Google 讀取 Yelp 網站中的內容。

這意味着與 <inline-iframe> 的通訊受到瀏覽器的嚴格限制。 若是 <inline-iframe> 的 origin 與容器不一樣(例如 yelp.com 與 google.com),則它們是徹底隔離的,與 <inline-iframe> 通訊的惟一方法是消息傳遞。這些消息都是純字符串,收到消息後,網站能夠自行處理或者忽略。HTML 規範容許瀏覽器將 <inline-iframe> 做爲單獨的進程實現。

瞭解了<inline-iframe>的工做原理後,咱們能夠在每次插件運行時建立一個新的<inline-iframe>,將代碼嵌入<inline-iframe>中來實現插件,插件能夠在<inline-iframe>內執行任何所需的操做。可是隻有經過明確的白名單消息,它才能與 Figma document 交互,而且 <inline-iframe> 的 origin 爲 null,任何往 figma.com 發出的請求都會被瀏覽器的跨域資源共享策略拒絕。

實際上,<inline-iframe>充當了插件的沙箱,沙箱的安全性由瀏覽器供應商保證,他們花了多年時間尋找並修復沙箱中的漏洞。

採用沙箱模型的插件將使用咱們添加到沙箱中的 API,大體以下所示:

const scene = await figma.loadScene() // gets data from the main thread
scene.selection[0].width *= 2
scene.createNode({
  type: 'RECTANGLE',
  x: 10, y: 20,
  ...
})
await figma.updateScene() // flush changes back, to the main thread複製代碼

關鍵在於插件經過調用 loadScene(發送消息給 Figma 獲取 document 的副本)進行初始化,並以調用 updateScene(將插件所作的更改發回給 Figma)結束。 注意:

  1. 咱們獲取 document 的副本,而不是每次讀寫屬性都使用消息傳遞。消息傳遞的開銷約爲每一個往返0.1ms,這樣每秒只能處理1000條左右的消息。
  2. 不直接使用 postMessage,由於使用起來很麻煩。


咱們花了大概一個月時間構建起來,還邀請了一些 Alpha 測試人員,很快就發現了兩個主要缺陷:

1. async/await 對用戶不夠友好

咱們獲得的第一個反饋是,用戶在使用 async/await 時遇到了麻煩。在這種方法中,這是不可避免的。消息傳遞從根本上講是一種異步操做,JavaScript 沒法對異步操做進行同步的阻塞調用,至少須要使用 await 關鍵字將全部調用函數標記爲異步。總的來講,async/await 仍然是一個至關新的 JavaScript 功能,而且須要對併發性有所解。這是個問題,由於咱們預計許多插件開發人員都對 JavaScript 熟悉,但可能沒有接受過正規的 CS 教育。

若是隻須要在插件開始時使用一次 await,結束時使用一次 await,那還不錯。咱們只是告訴開發人員,即便不太瞭解它的功能,也要始終在 loadScene 和 updateScene 使用 await。

問題是某些 API 調用須要大量複雜的邏輯計算,更改一個 layer 上的屬性有時會致使多個 layer 更新,例如調整 frame 的大小將遞歸地應用於子元素上。

這些行爲一般是精細複雜的算法,爲插件從新實現它們是個壞主意。咱們編譯好的 WebAssembly 也存在一樣的邏輯,所以不太好重用。並且,若是不在插件的沙箱中運行這些邏輯,插件將讀取過期的數據。

雖然下面這樣是可控的:

await figma.loadScene()
... do stuff ...
await figma.updateScene()複製代碼

即使是有經驗的工程師,也可能很快變得難以處理:

await figma.loadScene()
... do stuff ...
await figma.updateScene()
await figma.loadScene()
... do stuff ...
await figma.updateScene()
await figma.loadScene()
... do stuff ...
await figma.updateScene()複製代碼

2. 複製 scene 代價很昂貴

<inline-iframe>方法的第二個問題是,在發送給插件前須要序列化大部分document。事實證實,用戶可能在 Figma 中建立很是大的文檔,以致於達到內存限制。例如,Microsoft 的設計系統文件,須要花費14秒才能對 document 進行序列化。鑑於大多數插件都涉及諸如「在個人選擇中交換兩個項目」之類的快速操做,這將使插件沒法使用。

增量或者延遲加載數據也不現實,由於:

  1. 可能須要數月時間重構核心產品
  2. 任何須要等待數據到達的 API 都將是異步的。

總而言之,因爲 Figma 文檔可能包含大量互相依賴的數據,<inline-iframe>方案不適合咱們。


簡單的方案行不通,咱們從新開始,花了兩週時間認真考慮更多奇特的想法。可是大多數方法都有一個或多個主要缺陷:

  1.  API 太難用(如使用 REST API 或相似 GraphQL 的方法訪問 document)
  2. 依賴瀏覽器供應商已刪除或試驗中的功能(如同步 xhr + service worker, shared buffers)
  3. 須要大量的研究或重構應用,可能要花費數月時間,甚至沒法驗證可否正常工做 (例如,在 iframe 中加載 Figma 的副本,而後經過 CRDTs 進行同步,經過交叉編譯的生成器在 JavaScript 中侵入綠色線程?)

最終咱們得出的結論是,須要找到一種能夠直接操做 document 的方法。編寫插件應該像設計師在自動化動做,所以應該容許插件運行在主線程上。

在第二次嘗試以前,咱們須要從新審視容許插件運行在主線程上的含義,咱們起初沒有考慮它,由於知道可能很危險,在主線程上運行聽起來很像 eval(UNSAFE_CODE)。

在主線程上運行的好處是插件能夠:

  1. 直接修改 document 而不是副本,消除了加載時間的問題。
  2. 運行復雜的組件更新和約束邏輯,無需兩份代碼。
  3. 進行同步 API 調用,加載或刷新不會形成混淆。
  4. 用更直觀的方式編寫:插件只是自動執行用戶本來可使用 UI 手動執行的操做。

可是,如今咱們遇到了如下問題:

  1. 插件可能會掛起,且沒法中斷。
  2. 插件能夠向 figma.com 發送網絡請求。
  3. 插件能夠訪問和修改全局狀態。包括修改 UI,在 API 外部創建對內部應用狀態的依賴,或進行徹頭徹尾的惡意操做,例如更改 ({}).__proto__ 的值,這會使全部 JavaScript 對象都中毒。

咱們決定放棄對(1)的要求,當插件凍結時,會影響 Figma 被感知的穩定性。可是,咱們的插件模型在明確的用戶操做下能夠正常運行。在插件運行時更改 UI,凍結老是會歸因於插件。這也意味着插件不能 「破壞」 document。

eval 很危險意味着什麼?

爲了解決插件可以發送網絡請求並訪問全局狀態的問題,首先須要正確理解 「隨意的eval JavaScript 代碼是危險的」 的含義。

若是 JavaScript 變量只能進行相似 7 * 24 * 60 * 60的算術運算(簡稱SimpleScript),執行 eval 是很安全的。向 SimpleScript 添加一些功能,例如變量賦值和if 語句,使其更像一種編程語言,仍然是很是安全的。添加函數求值,就有了 lambda 演算和圖靈完整性。

換句話說,JavaScript 不必定是危險的。在最簡單的狀況下,它只是算術運算的一種擴展方式,當它訪問輸入和輸出時比較危險,包括網絡、DOM 等,危險的是這些瀏覽器 API。

API 都是全局變量,因此隱藏全局變量!

從理論上講,隱藏全局變量聽起來不錯,可是僅經過「隱藏」它們來保證安全是困難的。 好比,你可能考慮刪除 window 對象上的全部屬性,或將其設置爲 null,可是代碼仍然能夠訪問諸如 ({}).constructor 之類的全局變量。尋找全部可能泄漏全局變量的方式很是具備挑戰性。

相反,咱們須要一種更強大的沙箱,在這些沙箱裏,全局變量首先就不存在。

說到先前僅支持算術運算的 SimpleScript 示例,編寫算術求值程序是 CS 101的一個簡單練習,在該程序的任何合理實現中,SimpleScript 都不能執行算術運算以外的任何操做。

如今,擴展 SimpleScript 支持更多的語言功能,直到它變成 JavaScript ,這樣的程序稱爲解釋器,這是運行 JavaScript 這種動態解釋語言的方式。


嘗試2:將 JavaScript 解釋器編譯爲 WebAssembly

對於像咱們這樣的小型創業公司來講,實現 JavaScript 太繁重了,爲了驗證這種方法,咱們使用 Duktape(一種 C++ 編寫的輕量級 JavaScript 解釋器),將其編譯爲 WebAssembly。

咱們在上面運行了標準 JavaScript 測試套件 test262,它經過了全部 ES5 測試,一些不重要的測試除外。使用 Duktape 運行插件代碼,須要調用已編譯解釋器的 eval 函數。

這種方法的特性以下:

  1. 解釋器運行在主線程中,意味着能夠建立基於主線程的 API。
  2. 容易推理出是安全的。Duktape 不支持任何瀏覽器 API,此外,它做爲 WebAssembly 運行,而 WebAssembly 自己是一個沙箱環境,沒法訪問瀏覽器 API。換句話說,默認狀況下,插件代碼只能經過明確列入白名單的 API 與外界通訊。
  3. 比常規 JavaScript 慢,由於該解釋器不是 JIT 的,但這不要緊。
  4. 須要瀏覽器編譯一箇中等大小的 WASM 二進制文件,須要必定的成本。
  5. 瀏覽器調試工具默認狀況下不可用,咱們花了一天時間爲解釋器實現一個控制檯,說明至少能夠調試插件。
  6.  Duktape 僅支持 ES5,可是使用 Babel 這樣的工具交叉編譯較新的 JavaScript 版本已成爲網絡社區的常規操做。

(注:幾個月後,Fabrice Bellard 發佈了 QuickJS,它自己就支持 ES6。)

如今,編譯一個 JavaScript 解釋器! 做爲程序員你可能會想到:

太讚了!

或者

真的嗎?已有 JavaScript 引擎的瀏覽器中的 JavaScript 引擎?接下來是什麼,瀏覽器中的操做系統嗎?

有些懷疑是對的!除非必要,最好避免從新實現瀏覽器。咱們已經花費了不少精力實現整個渲染系統,作到了必不可少的性能和跨瀏覽器支持,可是咱們仍然儘可能不從新發明輪子。

這不是咱們最終採用的方法,有一個更好的方法。可是,覆蓋這一點很重要,由於這是理解咱們最終的沙箱模型的一個步驟,該模型更爲複雜。


嘗試3:Realms

儘管咱們有一種編譯 JS 解釋器的好方法,但還有另一種工具,由 Agoric 創造的稱爲 Realms shim 的技術。該技術將建立沙箱和支持插件做爲潛在用例,Realms API 大大體以下:

let g = window; // outer global
let r = new Realm(); // realm object

let f = r.evaluate("(function() { return 17 })");

f() === 17 // true

Reflect.getPrototypeOf(f) === g.Function.prototype // false
Reflect.getPrototypeOf(f) === r.global.Function.prototype // true複製代碼

 實際上,可使用已有的(儘管不爲人知的)JavaScript 功能來實現該技術,沙箱能夠隱藏全局變量,shim 起做用的核心大體以下:

function simplifiedEval(scopeProxy, userCode) {
  'use strict'
  with (scopeProxy) {
    eval(userCode)
  }
}複製代碼

這是用於演示的簡化版本,實際版本中還有一些細微差別,可是,它展現了難題的關鍵部分:with 語句 和 Proxy 對象。

with(obj) 建立了一個新的做用域,在該做用域內可使用 obj 的屬性來解析變量。在下例中,咱們能夠從 Math 對象的屬性中解析出變量 PI,cos 和 sin ,而 console 是從全局做用域解析的,它不是 Math 的屬性。

with (Math) {
  a = PI * r * r
  x = r * cos(PI)
  y = r * sin(PI)
  console.log(x,  y)
}複製代碼

Proxy 對象是 JavaScript 對象最動態的形式。

  • 最基本的 JavaScript 對象經過屬性訪問 obj.x 返回一個值。
  • 更高級的 JavaScript 對象能夠有 getter 屬性。
  • Proxy 經過執行 get 方法來攔截屬性的訪問。

嘗試訪問如下 proxy 上的任何屬性(白名單中的除外),將返回 undefined。

const scopeProxy = new Proxy(whitelist, {
  get(target, prop) {
    // here, target === whitelist
    if (prop in target) {
      return target[prop]
    }
    return undefined
  }
}複製代碼

如今,將這個 proxy 做爲 with 的參數,它將截獲全部變量解析,永遠不會使用全局做用域:

with (proxy) {
  document // undefined!
  eval("xhr") // undefined!
}複製代碼

好吧,仍然能夠經過 ({}).constructor 這樣的表達式訪問某些全局變量。此外,沙箱確實須要訪問某些全局變量,如 Object,它常出如今合法的 JavaScript 代碼(如 Object.keys )中。

爲了使插件可以訪問全局變量又不弄亂 window 對象,Realms 沙箱建立了一個同源 iframe 來實例化全部這些全局變量的副本。這個 iframe 與嘗試1中的版本不一樣,同源 iframe 不受 CORS 的限制。

當 <inline-iframe> 與父 document 同源時:

1. 它擁有全部全局變量的副本,如 Object.prototype

2. 能夠從父 document 訪問這些全局變量。


將這些全局變量放入 Proxy 對象的白名單,這樣插件就能夠訪問到。 最後,這個新的 <inline-iframe> 帶有一個 eval 函數的副本,與現有的 eval 函數有一個重要區別:即使是隻能經過 ({}).constructor 這樣的語法訪問的內置值,也會解析爲 iframe 中的副本

這種使用 Realms 的沙箱方法具備許多不錯的特性:

  • 它運行在主線程上。
  • 速度很快,由於仍然使用瀏覽器的 JavaScript JIT 來執行代碼。
  • 可使用瀏覽器開發者工具

可是它安全嗎?

使用 Realms 安全地實現 API

咱們對 Realms 的沙箱功能感到滿意。儘管比 JavaScript 解釋器方法包含更多微妙之處,它仍然能夠做爲白名單,其實現規模較小且易於審覈,而且是由網絡社區中德高望重的成員建立的。

可是,使用 Realms 並非故事的結局,這僅僅是一個沙箱,插件沒法執行任何操做,咱們仍然須要實現提供 API 的插件。這些 API 也要保證安全,由於大多數插件確實須要顯示 UI 併發送網絡請求(例如,使用 Google 表格中的數據填充設計)。

考慮到默認狀況下沙箱是不包含 console 對象的,畢竟 console 是瀏覽器 API,而不是 JavaScript 的功能,能夠將其做爲全局變量傳遞到沙箱。

realm.evaluate(USER_CODE, { log: console.log })複製代碼

 或者將原始值隱藏在函數中,這樣沙箱就沒法修改:

realm.evaluate(USER_CODE, { log: (...args) => { console.log(...args) } })複製代碼

 不幸的是,這是一個安全漏洞。即便在第二個例子中,匿名函數也是在 realm 以外建立的,而後直接提供給了 realm,這意味着插件能夠沿着 log 函數的原型鏈到達沙箱外。

實現 console.log 的正確方法是將其包裝在 realm 內建立的函數中,下面是一個簡化的示例(實際上,也有必要轉換 realms 拋出的全部異常)。

// Create a factory function in the target realm. 
// The factory return a new function holding a closure.
const safeLogFactory = realm.evaluate(`
        (function safeLogFactory(unsafeLog) { 
                return function safeLog(...args) {
                        unsafeLog(...args);
                }
        })
`);

// Create a safe function
const safeLog = safeLogFactory(console.log);

// Test it, abort if unsafe
const outerIntrinsics = safeLog instanceof Function;
const innerIntrinsics = realm.evaluate(`log instanceof Function`, { log: safeLog });
if (outerIntrinsics || !innerIntrinsics) throw new TypeError(); 

// Use it
realm.evaluate(`log("Hello outside world!")`, { log: safeLog });複製代碼

一般,沙箱永遠不能直接訪問在沙箱外部建立的對象,由於它們能夠訪問全局做用域。一樣重要的是,API 必須謹慎對待來自沙箱內部的對象,它們有可能與沙箱外部的對象混在一塊兒。

這帶來了一個問題。儘管能夠建立安全的 API,但讓開發人員每次向 API 添加新功能時,都擔憂難以捉摸的對象源語義是不可行的。 該如何解決這個問題呢?

一個解釋器一個API

問題在於,直接基於 Realms 建立 Figma API 會使每一個 API 端點都須要審覈,包括輸入和輸出值,這範圍太大了。


儘管 Realms 沙箱中的代碼使用相同的 JavaScript 引擎運行(爲咱們提供了便利的工具),仍然能夠假裝成受到 WebAssembly 方法的限制。

考慮一下 Duktape,嘗試2中編譯爲 WebAssembly 的 JavaScript 解釋器。主線程 JavaScript 代碼不可能直接保存沙箱中對象的引用,畢竟在沙箱中,WebAssembly 管理着本身的堆和這些堆中全部的 JavaScript 對象,實際上,Duktape 甚至可能不使用與瀏覽器引擎相同的內存來實現 JavaScript 對象!

結果,只有經過低階操做(例如從虛擬機中複製整數和字符串)才能爲 Duktape 實現API,能夠在解釋器內部保留對象或函數的引用,但只能做爲不透明的控制代碼。

這樣的接口以下所示:

// vm == virtual machine == interpreter
export interface LowLevelJavascriptVm {
  typeof(handle: VmHandle): string

  getNumber(handle: VmHandle): number
  getString(handle: VmHandle): string

  newNumber(value: number): VmHandle
  newString(value: string): VmHandle
  newObject(prototype?: VmHandle): VmHandle
  newFunction(name: string, value: (this: VmHandle, ...args: VmHandle[]) => VmHandle): VmHandle

  // For accessing properties of objects
  getProp(handle: VmHandle, key: string | VmHandle): VmHandle
  setProp(handle: VmHandle, key: string | VmHandle, value: VmHandle): void
  defineProp(handle: VmHandle, key: string | VmHandle, descriptor: VmPropertyDescriptor): void

  callFunction(func: VmHandle, thisVal: VmHandle, ...args: VmHandle[]): VmCallResult
  evalCode(code: string): VmCallResult
}

export interface VmPropertyDescriptor {
  configurable?: boolean
  enumerable?: boolean
  get?: (this: VmHandle) => VmHandle
  set?: (this: VmHandle, value: VmHandle) => void
}複製代碼

請注意,這是實現 API 用到的接口,但它或多或少 1:1 映射到 Duktape 的解釋器 API。畢竟,Duktape(和相似的虛擬機)是專門爲嵌入式設計的,且容許嵌入程序與 Duktape 通訊。

使用此接口,對象 { x: 10,y: 10 } 能夠這樣傳遞給沙箱:

let vm: LowLevelJavascriptVm = createVm()
let jsVector = { x: 10, y: 10 }
let vmVector = vm.createObject()
vm.setProp(vmVector, "x", vm.newNumber(jsVector.x))
vm.setProp(vmVector, "y", vm.newNumber(jsVector.y))複製代碼

 Figma 節點對象 」opacity」 屬性的 API 以下所示:

vm.defineProp(vmNodePrototype, 'opacity', {
  enumerable: true,
  get: function(this: VmHandle) {
    return vm.newNumber(getNode(vm, this).opacity)
  },
  set: function(this: VmHandle, val: VmHandle) {
    getNode(vm, this).opacity = vm.getNumber(val)
    return vm.undefined
  }
})複製代碼

 使用 Realms 沙箱一樣能夠很好地實現這個底層接口,這樣實現的代碼量是相對少的(咱們的例子中大約 500 行代碼)。而後就是仔細審覈代碼,一旦完成,即可以基於這些接口建立新的 API,而不用擔憂沙盒相關的安全性問題。 在文獻中,這稱爲膜模式。


本質上,這是將 JavaScript 解釋器和 Realms 沙箱都視爲 「運行 JavaScript 的某些獨立環境」。

在沙箱上建立底層抽象還有一個關鍵,儘管咱們對 Realms 的安全性充滿信心,但在安全性方面再當心也不爲過。咱們意識到 Realms 可能存在未被發現的漏洞,某天會變成須要處理的問題,這就是接下來咱們討論編譯解釋器(甚至不會用到)的緣由。API 是經過實現可互換接口實現的,因此使用解釋器仍然是備選方案,能夠在不從新實現任何 API 或不破壞任何現有插件的狀況下使用它。

插件豐富的功能

如今,咱們有了能夠安全運行任意插件的沙箱和容許插件操做 Figma document 的 API,這已經開啓了不少可能性。

可是,咱們最初的問題是爲設計工具構建一個插件系統,大部分這樣的插件都有建立 UI 的功能,須要某種形式的網絡訪問。更通常地說,咱們但願插件儘量多地利用瀏覽器和 JavaScript 生態系統。

像前面 console.log 的例子那樣,咱們能夠每次當心地暴露一個安全的受限版本的瀏覽器 API。可是,瀏覽器 API(尤爲是 DOM)的範圍很大,甚至比 JavaScript 自己還要大。這樣的嘗試可能因爲過於嚴格而沒法使用,或者可能存在安全漏洞。

咱們再次引入 origin 爲 null 的<inline-iframe>來解決這個問題。插件能夠建立 <inline-iframe> 並在其中放置任意的 HTML 和 Javascript。

與咱們最初嘗試使用 <inline-iframe> 不一樣的是,如今插件由兩部分組成:

1. Realms 沙箱內,運行在主線程上,能夠訪問 Figma document 的部分。

2. 運行在 <inline-iframe> 內,能夠訪問瀏覽器 API 的部分。

這兩部分能夠經過消息傳遞通訊。這種結構比起在同一個環境中運行兩個部分,會使瀏覽器 API 用起來更加繁瑣。 可是,鑑於當前的瀏覽器技術,這是咱們能作到的最好方法了。咱們發佈測試版兩個月以來,它並無阻止開發人員建立出色的插件。


結論

咱們可能走了一段彎路,但最終找到了在 Figma 中實現插件的可行方案。Realm shim 使咱們可以隔離第三方代碼,同時在相似瀏覽器的環境中運行。

這對咱們來講是最好的解決方案,但可能並不適用於每一個公司或平臺。若是你須要隔離第三方代碼,則值得評估一下是否存在與咱們類似的性能或 API 工程學方面的問題,若是沒有,那麼使用 iframe 隔離代碼就足夠了,簡單老是好的。咱們但願保持簡單!

最後,咱們很是關注最終的用戶體驗——插件的用戶將發現它們穩定可靠,具有基本 Javascript 知識的開發人員也可以建立。

在基於瀏覽器的設計工具的團隊中工做,最讓人激動的事情之一就是,可以遇到不少未知領域,而且創造解決此類技術難題的新方法。若是您喜歡這些工程冒險之旅,請查看咱們博客的其他部分獲取更多信息。

相關文章
相關標籤/搜索