積木Sketch插件進階開發指南

The fewer sources of truth we have for a design system, the more efficient we are.——Jon Goldhtml

設計系統的真理來源越少,效率就越高。——Jon Gold,知名全棧設計師前端

背景

1. 積木工具鏈體系

前段時間咱們在美團技術團隊公衆號上發表了《積木Sketch Plugin:設計同窗的貼心搭檔》一文,未曾想到,這個僅在美團外賣C端使用的插件受到了更多的關注,美團多個業務團隊紛紛向咱們拋出「橄欖枝」,表示想要接入以及並表達了願意共同開發的意向,其它互聯網同行也紛紛詢問相關的技術,一時讓咱們有些「受寵若驚」。回想起寫第一篇文章的時候,咱們的心裏仍是有些不安的。做爲UI同窗的一個設計工具,有些RD甚至沒有據說過Sketch這個名字,咱們很認真地修改過上一篇文章的每一句措辭,爭取讓內容更豐富有趣,當時還很擔憂不會被讀者接受。react

積木Sketch插件的「意外走紅」,確實有些出乎咱們的意料,但正是如此,才讓咱們知道UI一致性是絕大部分開發團隊面臨的共性問題,你們對落地設計規範,提升UI中臺能力,提高產研效率都有着強烈的訴求。爲了幫助更多團隊提高產研效率,咱們成立了袋鼠UI共建項目組,將門戶建設、工具鏈建設以及組件建設統一管理統一規劃,並將工具鏈的品牌肯定爲「積木」,而積木Sketch插件即是其中重要的一環。git

積木品牌Logo

咱們經過創建包含相同設計元素的統一物料市場,PM經過Axure插件拾取物料市場中的組件產出原型稿;UI/UE經過Sketch插件落地物料市場中的設計規範,產出符合要求的設計稿;而物料市場中的組件又與RD代碼倉庫中的組件一一對應,從而造成了一個閉環。將來,咱們但願經過高保真原型輸出,能夠給中後臺項目、非依賴體驗項目提供更好的服務體驗,賦予產品同窗直接向技術側輸出原型稿的能力。程序員

袋鼠UI工具鏈體系

2. 積木插件平臺化

伴隨着「積木」品牌的確立,愈來愈多的團隊但願能夠接入積木Sketch插件,其中部分團隊也在和咱們探討技術合做的可能性。UI設計語言與自身業務關聯性很強,不一樣業務的色彩系統、圖形、柵格系統、投影系統、圖文關係千差萬別,其中任意一環的缺失都會致使一致性被破壞,業務方都但願經過積木插件實現設計規範的落地。爲了幫助更多團隊的UI同窗提高設計效率,節約RD同窗頁面調整的時間,同時也讓App界面具備一致性,從而更好地傳達品牌主張和設計理念,咱們決定對積木插件進行平臺化改造。平臺化是指積木插件能夠接入各個業務團隊的整套設計規範,經過平臺化改造,可使積木插件提供的設計元素與業務強關聯,知足不一樣業務團隊的設計需求。github

積木Skecth Plugin平臺化示意

積木插件本來只是外賣提高UI/RD協做效率的一次嘗試,最初的目標僅是UI一致性,可是如今已經做爲全面提高產研效率的媒介,承載了愈來愈多的功能。圍繞設計平常工做,提供高效的設計元素獲取方式,讓工做變得更輕鬆,是積木的核心使命。如何推進設計規範落地,而且輸出到各個業務系統靈活使用,是咱們持續追尋的答案。而探尋研發和設計更爲高效的協做模式也是咱們一直努力的方向。web

經過一段時間的平臺化建設,目前美團已經有7個設計團隊接入了積木插件,覆蓋了美團到家事業部大部分設計同窗,將來咱們會持續推動積木插件的平臺化建設,不斷完善功能,指望能將積木插件打形成業界一流的品牌。json

3. Sketch插件開發進階

第一篇文章多是爲數很少的入門教程,而本篇多是你能找到的惟一一篇進階開發文章。進階開發主要涉及如何切換業務方數據,即選擇所屬業務方後,對應的組件、顏色等設計素材切換爲當前業務方在物料市場中上傳的元素;將承載組件庫的Library文件轉化爲插件能夠識別的格式,並在插件上展現,以供設計師在繪製設計稿時選擇使用;一些優化運行效率,提高用戶體驗的方法。redux

Sketch插件代碼因爲和業務強相關,且實現方式較爲複雜,可能存在部分敏感信息,因此基本沒有成熟的插件開源。在進行一些複雜功能開發時,咱們也經常「丈二和尚摸不到頭腦」,「要不這個功能算了吧」的想法也不止一次出現,但是每當開會看到旁邊的設計同窗在使用「積木」插件認真做圖時,又一次次堅決了咱們的信念,要不加班再試試吧,沒準就能實現了呢?一次「委曲求全」,後面可能致使整個項目慢慢崩塌,因此咱們一直以將積木插件打形成爲業界領先的插件爲信念。若是說看過了第一篇文章你已經知道了如何開發一款插件,那麼經過本篇文章的學習你就能夠真正實現一款能夠與業務強關聯且功能可定製的成熟工具,與其說是介紹如何開發一個進階版的Sketch插件,不如說是分享給你們完成一個商業化項目的經驗。數組

支持多業務切換

爲了當面對「咱們能夠接入積木插件嗎」這種靈魂拷問時再也不手足無措,平臺化進程迅速啓動。平臺化的核心其實就是當發生業務線變動時,物料市場中的素材總體同步切換,所以咱們須要進行以下幾個操做:首先創建全局變量,存儲當前用戶所述業務方信息及鑑權信息;用戶選擇功能模塊後,根據用戶所述業務方,拉取對應素材;處理Library等素材並渲染頁面展現;根據素材信息變動畫板中的相關Layer。這部分主要介紹如何依靠持久化存儲來實現業務切換的功能,就像在第一篇啓蒙文檔中說的那樣,這裏不會貼大段的代碼,只會幫你梳理最核心的流程,相信你親自實踐一次以後,之後的困難均可以輕鬆解決。

1. 定義通用變量

功能模塊展現的素材與當前選擇的業務相關,所以須要在每一個功能模塊的Redux初始化狀態中增長一些全局狀態變量。好比全部模塊都須要使用businessType來肯定當前選擇的業務,使用theme進行主題切換,使用commonRequestParams獲取用戶鑑權信息等。

export const ACTION_TYPE = 'roo/colorData/index';
const intialState = {
  id: 'color',
  title: '顏色庫',
  theme: 'black',
  businessType: 'waimai-c',
  commonRequestParams: {
    userInfo: '',
  },
};
export default reducerCreator(intialState, ACTION_TYPE);
複製代碼

2. 實現數據交換

第一步:WebView側獲取用戶選擇,將所選的業務方數據經過window.postMessage方法傳遞至插件側。

window.postMessage('onBusinessSelected', item);
複製代碼

第二步:Plugin側經過webViewContents.on( )方法接收從WebView側傳遞過來的數據。Sketch官方經過Settings API提供了一些類的方法來處理用戶的參數設置,這些設置在Sketch關閉後依然會保存,除了存儲一段JSON數據外,Layer、Document甚至是Session variable都是支持的。

webViewContents.on('onBusinessSelected', item => {
    Settings.setSettingForKey('onBusinessSelected', JSON.stringify(item));
  });
​
// 除此以外,插件側也能夠經過localStorage向WebView注入數據
browserWindow.webContents.executeJavaScript(
      `localStorage.setItem("${key}",'${data}')`
);
複製代碼

第三步:當用戶經過工具欄選擇某一功能模塊(例如「插畫庫」)時,會回調NSButton的點擊事件監聽,此時除了須要要讓WebView展現(Show)以及獲取焦點(Focus)外,還須要將第二步存儲的業務方信息傳入,並以此加載當前業務方的物料數據。

//用戶打開的功能模塊
const button = NSButton.alloc().initWithFrame(rect) 
  button.setCOSJSTargetFunction(() => {
    initWebviewData(browserWindow);
    browserWindow.focus();
    browserWindow.show();
  });
​
// 注入全局初始化信息
function initWebviewData(browserWindow) {
  const businessItem = Settings.settingForKey('onBusinessSelected');
  browserWindow.webContents.executeJavaScript(`initBusinessData(${businessItem})`);
}
複製代碼

WebView側功能模塊收到初始化信息,開始進行頁面渲染前的數據準備。Object.keys()方法會返回一個由給定對象的自身可枚舉屬性組成的數組,遍歷這個數組便可拿到全部被注入的初始化數據,以後經過redux的store.dispatch方法更新state便可。至此實現業務切換功能的流程就所有結束了,是否是以爲很是簡單,忍不住想親自動手試一下呢?

ReactDOM.render(<Provider store={store}><App /></Provider>,
  document.getElementById('root')
);
​
window.initBusinessData = data => {
  const businessItem = {};
  Object.keys(data).forEach(key => {
    businessItem[key] = { $set: initialData[key] };
  });
  store.dispatch(update(businessItem));
};
​
const update = payload => ({
  type: ACTION_TYPE,
  payload,
});
複製代碼

3. 小結

有小夥伴會問,爲何WebView與Plugin側須要數據傳遞呢,它們不都屬於插件的一部分麼?根本緣由是咱們的界面是經過WebView展現的,可是對Layer的各類操做是經過Sketch的API實現的,WebView只是一個網頁,自己與Sketch並沒有關係,所以必須使用bridge在二者之間進行數據傳遞。別擔憂,這裏再帶你把整個流程梳理一遍:①在插件啓動後會從服務端拉取業務方列表;②用戶在WebView中選擇本身所屬的業務方;③將業務方數據經過bridge傳遞至Plugin側,並經過Sketch的Settings API進行持久化存儲,這樣就能夠保證每次啓動Sketch的時候無需再次選擇所屬業務方;④用戶點擊插件工具欄的按鈕選擇所需功能(例如色板庫、組件庫等),從持久化數據中讀取當前所屬業務方,並通知WebView側拉取當前業務方數據。至此,整個流程結束。

Library庫文件自動化處理

這部分將介紹如何將Library庫文件轉化爲插件能夠識別的JSON格式,並在插件上展現。

若是要問Sketch插件最重要的功能是什麼,組件庫絕對是無可爭議的C位。在長期的版本迭代中,隨着功能的不斷增長以及UI的持續改版,新舊樣式混雜,維護極爲困難。設計師經過將頁面走查結果概括梳理,制定設計規範,從而選取複用性高的組件進行組件庫搭建。經過搭建組件庫能夠進行規範控制,避免控件的隨意組合,減小頁面差別;組件庫中組件知足業務特點,同時具備雲端動態調整能力,能夠在規範更新時進行統一調整。

目前,咱們將組件集成進Sketch供UI使用大體分爲兩個流派:一個是基於Sketch官方的Library庫文件,設計師經過將業務中複用性高的Symbol組件概括整理生成庫文件(後綴.sketch),並上傳至雲端,插件拉取庫文件轉化爲JSON並在操做面板展現供選取使用;另外一個則是採用相似Airbnb開源的React-Sketchapp這樣的框架,它可讓你使用React代碼來製做和管理視覺稿及相關設計資源,官方把它稱做「用代碼來繪畫」,這種方案的實施難度較大,由於本質上設計是感性和理性的結合,設計師使用Sketch是畫,而非帶有邏輯和層級關係的寫,他們對於頁面的樹形結構很難理解,上手成本較高,並且代碼維護成本相對較大。咱們不去評價哪一種方案的好壞,只是第一種方案能夠更好地知足咱們的核心訴求。

Sketch組件庫處理效果示意

1. 訂閱遠程組件庫

Library庫文件其實是一個包含components的文檔,components包括了Symbols、Text Styles以及Layer Styles三類,將Library存儲在雲端就能夠在不一樣文檔甚至不一樣團隊間共享這些components。因爲組件庫實時指向最新,所以當其維護者更新庫中的components時,使用了這些components的文檔將會收到通知,這能夠保證設計稿永遠指向最新的設計規範。

訂閱雲端組件庫的方式很簡單,首先建立一個雲端組件庫,具體能夠參照上一篇文章,若是須要服務多個設計部門,則須要建立多個庫,每一個庫有惟一的RSS地址;在插件中獲取到這些RSS地址後,能夠經過Library.getRemoteLibraryWithRSS方法對其進行訂閱。

// 啓動插件時添加遠程組件庫
export const addRemoteLibrary = context => {
  fetch(LibraryListURL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
  })
    .then(res => res.json())
    .then(response => response.data)
    .then(json => {
      const { remoteLibraryList } = json;
      _.forEach(remoteLibraryList, fileName => {
        Library.getRemoteLibraryWithRSS(fileName, (err, library) => {
        });
      });
      return list;
    });
};
複製代碼

2. Library庫文件轉換JSON數據

將Sketch的Library文件轉換爲JSON的過程,實際上就是轉換爲WebView能夠識別格式的過程。由於積木插件是將組件按照必定分組展現在面板中供設計師選取,所以須要根據組件分類組織其結構。Sketch原生支持採用 "/" 符號對其進行分組:Group-name/Symbol-name,好比命名爲Button/Normal和Button/Pressed的兩個Symbols會成爲Button Group的一部分。

Symbol分組結構

實際中能夠根據業務須要採用三級以上分組命名的方式,經過split方法將Symbol名稱經過 "/" 符號拆分爲數組,第一級名稱、第二級名稱等各級名稱做爲JSON結構的不一樣層級便可,具體操做能夠參照以下示例代碼:

const document = library.getDocument();
    const symbols = [];
    _.forEach(document.pages, page => {
      _.forEach(page.layers, l => {
        if (l.type && l.type === 'SymbolMaster') {
          symbols.push(l);
        }
      });
    });
​
 // 對symbol進行分組處理,並生成json數據
 for (let i = 0; i < symbols.length; i++) {
      const name = symbols[i].name;
      const subNames = name.split('/');
      // 去掉全部空格
      const groupName = subNames[0].replace(/\s/g, '');
      const typeName = subNames[1].replace(/\s/g, '');
      const symbolName = subNames.join('/').replace(/\s/g, '');
      result[groupName] = result[groupName] || {};
      result[groupName][typeName] = result[groupName][typeName] || [];
      result[groupName][typeName].push({
        symbolID:symbolID,
        name: symbolName,
      });
 }
複製代碼

通過以上操做後,一個簡化版的JSON文件以下方所示:

{
    "美團外賣C端組件庫": {
        "icon": [{
            "symbolID": "E35D2CE8-4276-45A1-972D-E14A06B0CD23",
            "name": "28/問號"
        },{
            "symbolID": "E57D2CE8-4276-45A1-962D-E14A06B0CD61",
            "name": "27/花朵"
        }]
    }
}
複製代碼

3. Symbol縮略圖處理

WebView默認是不支持直接顯示Symbol供用戶拖拽使用的,解決該問題的方案有兩種:(1)經過dump分析Sketch的頭文件發現,能夠採用MSSymbolPreviewGenerator的imageForSymbolAncestry方法導出縮略圖,該方法支持圖片大小、顏色空間等多種屬性設置,優點是較爲靈活,能夠根據須要進行任意配置,不過要承擔後期API變動的風險;(2)直接採用sketchDOM提供的export方法,將Symbol組件導出爲縮略圖,以後在WebView中顯示縮略圖,當拖拽縮略圖至畫板時,再將其替換爲Library中對應的Symbol便可。

import sketchDOM from 'sketch/dom';
​
sketchDOM.export(symbolMaster, {
     overwriting: true,
     'use-id-for-name': true,
     output: path,
     scales: '1',
     formats: 'png',
     compression: 1,
});
複製代碼

4. 小結

以上就是實現平臺化的一個基本流程了,不知道此時你有沒有聽得「雲裏霧裏」。在這裏,再把核心點帶你們複習一下。本節主要講了兩件事情:第一,插件如何才能支持多個業務方,即在插件的業務方列表中選擇相關業務方,就能夠切換對應的設計資源;第二,如何處理Library文件,將其轉換爲JSON供WebView展現使用。具體流程以下:

  1. 不一樣設計組的UI同窗製做完成包含各類components的Library後,經過後臺上傳至雲端。
  2. RD同窗根據當前使用者所屬的設計團隊拉取對應的包括Library在內的設計素材,顏色、圖片,iconFont等設計素材能夠直接展現,但是Library文件不支持在WebView中直接顯示,須要進行處理。
  3. 根據和UI同窗約定組件的命名規則,經過使用「/」分割,將第一級名稱、第二級名稱等各級名稱做爲JSON結構的不一樣層級,再經過sketchDOM提供的export方法將Symbol轉換爲png格式的縮略圖便可在插件中顯示。
  4. 將選中的縮略圖拖拽至Sketch的畫板時,再將縮略圖替換爲Library中的真實Symbol便可。

Library庫文件處理小結

操做體驗優化

完成了上述步驟後,就能夠完成一款支持多業務方的插件了。可是隨着積木Sketch插件接入的業務方愈來愈多,除了聽到能夠顯著提高效率的褒獎外,對插件的吐槽聲也經常傳入咱們的耳邊。市面上成熟的插件也有不少,咱們沒法限制別人的選擇,因此只能讓積木變得更好用,本節主要介紹插件的優化方法。爲了更好地傾聽你們意見,積木插件經過各類措施瞭解用戶的真實想法。首先積木插件接入美團內部的TT(Trouble Tracker)系統,相比公司不少專業系統,TT不帶任何專業流程和定製化,只作純流轉,是一套適用於公司內部的、通用的問題發起、響應和追蹤系統,用戶反饋的問題自動建立工單並與對應RD關聯,Bug能夠最快速修復;插件內部增長反饋渠道,用戶反饋及時發送給相關PM,做爲下次功能排期的權重指標;插件內部增長多維度埋點統計,從設計滲透到高頻使用兩個方面瞭解UI同窗的核心訴求。如下介紹了根據反饋整理的部分高優先級問題的解決方案。

1. 操做界面優化

不少RD在開發過程當中,對界面美化每每嗤之以鼻,「這個功能能用就能夠了」經常被掛在嘴邊。難道UI的需求真的是中看不中用?一個產品設計師說過,最先的產品僅依靠功能就能夠在競品中脫穎而出,能不能用就成爲了一個產品是否合格的標準。後來在愈來愈成熟的互聯網環境中,易用性成了一個新的且更重要的標準,這時同類產品間的功能已經很是接近,沒法經過不斷堆疊功能產生明顯差別。而當同類產品的易用性也趨於相近時,如何解決產品競爭力的問題就再一次擺在面前,這時就要賦予產品情感,好的產品關注功能,優秀的產品關注情感,可讓用戶在使用中感覺到溫暖。

WebView優化

當咱們通過了仔細的功能驗證,興致勃勃的把初版積木插件給用戶體驗時,原本滿心歡喜準備迎接誇獎,沒想到卻獲得了不少交互體驗很差的反饋,「僅僅實現功能就及格了」這個理論在一像素都不願放過的設計師眼中確定行不通。原生WebView給用戶的體驗每每不夠優秀,其實只要一些很簡單的設置就能夠解決,可是這並不表明它不重要。

WebView視圖優化點舉例

禁止觸摸板拖拽形成頁面總體「橡皮筋」效果,禁止用戶選擇(user-select),溢出隱藏等操做,使WebView具備「類原生」效果,都會提高用戶的實際使用體驗。

html,
body,
#root {
  height: 100%;
  overflow: hidden;
  user-select: none;
  background: transparent;
  -webkit-user-select: none;
}
複製代碼

除此以外在正式環境中(NODE_ENV爲production),咱們並不但願當前界面響應右鍵菜單,須要經過給document添加EventListener監聽將相關事件處理方法屏蔽。

document.addEventListener('contextmenu', e => {
  if (process.env.NODE_ENV === 'production') {
    e.preventDefault();
  }
});
複製代碼

工具欄優化

Sketch對於設計師的意義,就像代碼編輯器對於程序員同樣,工做中幾乎無時無刻也離不開。在積木Sketch插件走出美團外賣,被愈來愈多的設計團隊採用後,爲了讓它更加賞心悅目,UI同窗決定對工具條進行一次全新的視覺升級。原生界面開發指的是經過macOS的AppKit進行用戶界面開發,在插件開發中一些須要嵌入Sketch面板的UI模塊就須要進行原生界面開發,好比吸附式工具條就屬於經過macOS原生API開發的界面。

原生開發既可使用Objective-C語言,也可使用CocoaScript經過寫JavaScript的方式進行開發。CocoaScript 經過Mocha實現JS到Objective-C的映射,可讓咱們經過JS調用Sketch內部API以及macOS的Framework。在經過CocoaScript原生開發前須要瞭解一些基礎知識:

  1. 在使用相關框架前須要經過framework()方法進行引入,而Foundation以及CoreGraphics是默認內置的,無需再單獨操做。
  2. 一些Objective-C的selectors選擇器須要指針參數,因爲JavaScript不支持經過引用傳遞對象,所以CocoaScript提供了MOPointer做爲變量引用的代理對象。

UI調整通常分爲三個部分:佈局調整、動效調整、圖片替換。下面的章節會進行逐一介紹。

新版積木工具欄效果圖

佈局調整

這裏UI的需求是NSButton的寬度填充滿整個NSStackView,高度自定義。因爲此功能看起來過於簡單,當時認爲估時0.5天綽綽有餘,但是沒想到搭進去了1個工做日加上2天週末的時間,由於不管如何設置NSStackView中子View尺寸都沒法生效。

在頂住了周圍人「UI問題不影響功能使用,之後有時間再優化吧」的「輿論壓力」後,終於在官方文檔裏面發現了線索:「NSStackView A stack view employs Auto Layout (the system’s constraint-based layout feature) to arrange and align an array of views according to your specification. To use a stack view effectively, you need to understand the basics of Auto Layout constraints as described in Auto Layout Guide.」簡而言之,NSStackView使用constraints的方式進行自動佈局(能夠類比Android中的ConstraintLayout),在進行尺寸修改時,是須要添加錨點的,所以須要經過Anchor的方式進行尺寸修改。

// 建立工具條
const toolbar = NSStackView.alloc().initWithFrame(NSMakeRect(0, 0, 45, 400));
toolbar.setSpacing(7);
// 建立NSButton
const button = NSButton.alloc().initWithFrame(rect)
// 設置NSButton寬高
button
    .widthAnchor()
    .constraintEqualToConstant(rect.size.width)
    .setActive(1);
button
    .heightAnchor()
    .constraintEqualToConstant(rect.size.height)
    .setActive(1);
button.setBordered(false);
// 設置回調點擊事件
button.setCOSJSTargetFunction(onClickListener);
button.setAction('onClickListener:');
// 添加NSButton至NSStackView中
toolbar.addView_inGravity(button, inGravityType);
複製代碼

動效調整

NSButton內置的點擊效果大約15種,能夠經過NSBezelStyle進行設置。積木插件工具欄並無採用點擊後icon反色的通用處理方式,而是點擊後將背景色置爲淺灰。若是想要自定義一些點擊效果,只需在NSButton點擊事件的回調中設置便可。

onClickListener:sender => {
  const threadDictionary = NSThread.mainThread().threadDictionary();
  const currentButton = threadDictionary[identifier];
   if (currentButton.state() === NSOnState) {
      currentButton.setBackgroundColor(NSColor.colorWithHex('#E3E3E3'));
   } else {
      currentButton.setBackgroundColor(NSColor.windowBackgroundColor());
   }
}
複製代碼

圖片加載

Sketch插件既支持加載本地圖片,也支持加載網絡圖片。加載本地圖片時,能夠經過context.plugin的方法獲取一個MSPluginBundle對象,即當前插件bundle文件,它的url()方法會返回當前插件的路徑信息,進而幫助咱們找到存儲在插件中的本地文件;而加載網絡圖片則更加簡單,經過NSURL.URLWithString( )能夠得到一個使用圖片網址初始化獲得的NSURL對象,這裏要格外注意的是,對於網絡圖片請使用https域名。

//本地圖片加載
const localImageUrl = 
       context.plugin.url()
      .URLByAppendingPathComponent('Contents')
      .URLByAppendingPathComponent('Resources')
      .URLByAppendingPathComponent(`${imageurl}.png`);
​
//網絡圖片加載
const remoteImageUrl = NSURL.URLWithString(imageUrl);
​
//根據ImageUrl獲取NSImage對象
const nsImage = NSImage.alloc().initWithContentsOfURL(imageURL);
nsImage.setSize(size);
nsImage.setScalesWhenResized(true);
複製代碼

2. 執行效率優化

只有在設計稿中儘量多地使用組件進行設計,而且將已有頁面中的內容經過設計師的走查梳理逐漸替換成組件,才能真正經過建設組件庫來進行提效。隨着設計團隊逐步將設計語言沉澱爲設計規範,並將其量化內置於積木插件中,組件的數量愈來愈多,積木插件組件庫做爲UI同窗使用最頻繁的功能,須要格外關注其運行效率。

前置組件庫加載

將組件庫的加載邏輯前置,在打開文檔時對遠程組件庫進行訂閱操做。Sketch所提供的了Action API可使插件對應用程序中的事件作出反應,監聽回調只需在插件的manifest.json文件中添加一個handler便可,添加了對於「OpenDocument」的監聽,也就是告訴插件在新文檔被打開時要去執行addRemoteLibrary這個function。

{
      "script": "./libraryProcessor.js",
      "identifier": "libraryProcessor",
      "handlers": {
        "actions": {
          "OpenDocument": "addRemoteLibrary"
        }
      }
}
複製代碼

增長緩存邏輯

組件庫的處理須要將Library文件轉換爲帶有層級信息的JSON文件,而且須要將Symbol導出爲縮略圖顯示。因爲這個步驟較爲耗時,所以能夠將通過處理的Library信息緩存起來,並經過持久化存儲記錄已緩存的Library版本。若已緩存的版本與最新版本一致,且縮略圖與JSON文件均完整,則能夠直接使用緩存信息,極大的提升Library的加載速度。如下非完整代碼,僅做示例:

verifyLibraryCache(businessType, libraryVersion) {
    const temp = Settings.settingForKey('libraryJsonCache');
    const libraryJsonCache = temp ? JSON.parse(temp) : null;
​
    // 1.驗證緩存版本信息
    if (libraryJsonCache.version.businessType !== libraryVersion) {
      return null;
    }
​
    // 2.驗證縮略圖完整性
    const home = getAssertURL(this.mContext, 'libraryImage');
    const path = join(home, businessType);
    if (!fs.existsSync(path) || !fs.readdirSync(path)) {
      return null;
    }
​
    // 3.驗證業務庫Json文件完整性
    if (libraryJsonCache[businessType]) {
      console.info(`當前${businessType}命中緩存`);
      return libraryJsonCache;
    } else {
      return null;
    }
  }
}
複製代碼

3. 自定義Inspector屬性面板

與Objective-C工程混合開發

隨着各個設計組的組件庫建設不斷完善,抽離的組件數量不斷增多,很多UI同窗反饋Sketch原生組件樣式修改面板操做不夠便捷,沒法約束選擇範圍,但願能夠提供一種更有效的組件overrides修改方式,而且當修改「圖片」、「圖標」、「文字」等圖層時,能夠和積木插件的這些功能模塊進行聯動選擇。實現自定義Inspector面板功能既可使操做更便捷,又能夠對修改項進行約束。

自定義屬性面板功能的基本思想,是將組件從組件庫拖至Sketch畫板中時,組件的可修改屬性能夠顯示在Sketch自己的屬性面板上。咱們引入了Objective-C原生開發以實現對Sketch界面的修改,爲何要使用原生開發?雖然官方提供了JS API並承諾持續維護,但這項工做一直處於Doing狀態,並且官方文檔更新緩慢,沒有明確的時間節點,所以對於自定義Native Inspector Panel這種須要Hook API的功能,使用原生開發較爲便捷,並且對於iOS開發者也更加友好,無需再學習前端界面開發知識。

Sketch Inspector面板操做區優化

Xcode工程配置

經過Xcode工程構建自定義屬性面板,最終生成一個能夠供JS側調用的Framework。能夠參考上一篇文章介紹的方法建立Xcode工程,該工程在每次構建後會自動生成測試Sketch插件並放入對應的文件夾中。須要注意的一點是,這裏生成的插件只是爲了方便開發和調試,後面會介紹如何將XCode工程構建的Framework集成至JS主工程中。

Xcode工程配置示意

積木插件的主體功能使用JS代碼實現,可是自定義屬性選擇面板使用Objective-C代碼實現。爲了實現積木插件的JS側功能模塊與OC側模塊之間的通訊和橋接,這裏藉助了Mocha框架來實現相關的功能,Mocha框架也被Sketch官方所使用,將原生側的方法封裝爲官方API後暴露給JS側。

Sketch與插件Framework通訊原理

組件選中時,Sketch軟件會回調onSelectionChanged方法給JS側,JS側藉助Mocha框架能夠實現對OC側的調用,同時將參數以OC對象的方式傳遞。JS側傳遞給OC側的Context內容很豐富,包含了選中的組件、相關圖層還有Sketch軟件自己的信息。雖然Sketch沒有提供API,可是Objective-C語言自己具有KVO監聽對象屬性的能力,咱們經過讀取對應的屬性值,就能夠獲取須要的對象數據。

+ (instancetype)onSelectionChanged:(id)context {
 
    [self setSharedCommand:[context valueForKeyPath:@"command"]]; 
   
    NSString *key = [NSString stringWithFormat:@"%@-RooSketchPluginNativeUI", [document description]];
    __block RooSketchPluginNativeUI *instance = [[Mocha sharedRuntime] valueForKey:key];
​
    NSArray *selection = [context valueForKeyPath:@"actionContext.document.selectedLayers"];
    [instance onSelectionChange:selection];
    return instance;
}
複製代碼

Sketch官方沒有將屬性面板的修改能力暴露給插件側,經過查詢Sketch頭文件發現經過reloadWithViewControllers:方法能夠實現屬性面板刷新,可是在實際開發過程當中發如今某些版本的Sketch上會出現面板閃動的問題,這裏藉助Objective-C的Method Swizzle特性,直接修改reloadWithViewControllers:的運行時行爲解決。

[NSClassFromString(@"MSInspectorStackView") swizzleMethod:@selector(reloadWithViewControllers:)                                        withMethod:@selector(roo_reloadWithViewControllers:)                                                        error:nil];
複製代碼

​ Swizzle方法會修改原始方法的行爲,實際操做中只有在知足特定條件的狀況下才應觸發Swizzle後的方法。

Swizzle方法觸發條件

組件屬性修改與替換原理

經過自定義面板能夠修改組件的可覆蓋項(即override),目前能夠應用可覆蓋項的affectedLayer有Text/Image/Symbol Instance三種。設計師與開發者在此前對圖層的格式進行了約定,保證咱們能夠按照統一的方式讀取並替換圖層的屬性值。

替換文本

基於class-dump,咱們能夠找出Sketch中聲明的全部類的屬性和方法,文本處理的策略是,找到圖層中的全部MSAvailableOverride對象,這些對象即表示可用的覆蓋項,對文本信息的修改其實是經過修改MSAvailableOverride對象的overridePoint來實現的。

id overridePoint = [availableOverride valueForKeyPath:@"overridePoint"];
[symbolInstance setValue:text forOverridePoint:overridePoint];
複製代碼

更改樣式

樣式設置的策略,是找到當前選中組件對應的Library中相關樣式的組件。因爲全部的組件都遵循統一的命名格式,所以只要根據組件命名就能篩選出符合要求的組件。

// 命名方式:一級分類/二級分類/組件名稱,基於圖層獲取對應library
id library = [self getLibraryBySymbol:layer];
// 讀取組件名稱
NSString *layerName = [symbol valueForKeyPath:@"name"];
// 配置符合當前業務的Predicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH [cd] %@", prefix];
// 篩選符合Predicate的全部組件
NSArray *filterResult = [allSybmols filteredArrayUsingPredicate:predicate];
複製代碼

當使用者選中某一個樣式後,插件會將設計稿上的組件替換爲選中的組件,這裏須要使用MSSymbolInstance中的changeInstanceToSymbol方法來實現。須要注意的是,changeInstanceToSymbol僅僅替換了圖層中的組件,可是並無修改圖層上組件的屬性,對於位置和大小等信息須要單獨進行處理。

// 在更新圖層上的組件以前,咱們須要把組件導入到當前document對象中
id foreignSymbol = [libraryController importShareableObjectReference:sharedObject intoDocument:documentData];
​
//更新圖層上的組件 
[symbolInstance changeInstanceToSymbol:localSymbol];
複製代碼

調試技巧

OC側開發的最大問題,在於沒有官方API的支持。所以調試器就顯得很是重要,單步調試可讓咱們很是方便地深刻到Sketch內部瞭解Document內部的數據結構。調試環境須要配置,但足夠簡單,而且對於開發效率的提高是指數級的。

1.對構建Scheme的配置。

2.Attach到Sketch軟件上,這樣就能夠實現斷點調試。

與當前JS工程混合編譯

1.經過skpm中內置的@skpm/xcodeproj-loader編譯XCode工程,並將產物framework拷貝至插件文件夾。

const framework = require('../../RooSketchPluginXCodeProject/RooSketchPluginXCodeProject.xcworkspace/contents.xcworkspacedata');
複製代碼

2.經過Mocha提供的loadFrameworkWithName_inDirectory方法,設置Framework的名稱及路徑便可進行加載。

function() {
    var mocha = Mocha.sharedRuntime();
    var frameworkName = 'RooSketchPluginXCodeProject';
    var directory = frameworkPath;
​
    if (mocha.valueForKey(frameworkName)) {
      console.info('JSloadFramework: `' + frameworkName + '` has loaded.');
      return true;
    } else if (mocha.loadFrameworkWithName_inDirectory(frameworkName, directory)) {
      console.info('JSloadFramework: `' + frameworkName + '` success!');
      mocha.setValue_forKey_(true, frameworkName);
      return true;
    } else {
      console.error('JSloadFramework load failed');
      return false;
    }
  }
複製代碼

3.調用framework中的方法。

// 找到已經被加載的framework
 const frameworkClass = NSClassFromString('RooSketchPluginNativeUI');
// 調用暴露的方法
frameworkClass.onSelectionChanged(context);
複製代碼

一塊兒拼積木

目前,積木插件已經在美團到家事業部遍地開花,咱們但願將來積木品牌產品能夠在更大範圍內獲得應用,幫助更多團隊落地設計規範,提高產研效率,也歡迎更多團隊接入積木工具鏈。「不忘初心,方得始終」,就像第一篇啓蒙文章中說的那樣,咱們除了但願製做一流的產品,也但願積木插件可讓你們在繁忙的工做中得以喘息。咱們會繼續以設計語言爲依託,以積木工具鏈爲抓手,不斷完善優化,拓展插件的使用場景,讓設計與開發變得更輕鬆。

總有人在問,積木插件如今好用嗎?我想說,還不夠好用。可是每次評審需求時看到旁邊的設計師在認真地使用咱們的插件做圖,看到積木插件愛好者爲咱們製做表情包幫助咱們推廣,咱們深知惟有交付最棒的產品,才能不辜負你們的期待。

平臺化二期的需求剛剛肯定完畢,人力分配排期結束,咱們又想了一大波令你拍手稱讚的功能,立刻就要踏上新的征程。夜深了,看着窗外人家的燈,一個個熄滅,夜空也變得愈來愈明亮。咱們的目標,是星辰大海。

使用積木插件插畫庫製做的表情包 Design by 雪美

致謝

感謝外賣技術部曉飛、彥平、瑤哥、雲鵬、冰冰對項目的大力支持。 感謝到家事業部優秀的設計師冉冉、昱翰、淼林、雪美、田園、璟琦。 感謝閃購技術團隊章琦、CRM團隊的怡婷、CI王鵬協助技術開發。

參考文獻

招聘信息

美團外賣長期招聘Android、iOS、FE高級/資深工程師和技術專家,歡迎加入外賣App你們庭。感興趣的同窗可投遞簡歷至:tech@meituan.com(郵件主題請註明:美團外賣前端)。

想閱讀更多技術文章,請關注美團技術團隊(meituantech)官方微信公衆號。

相關文章
相關標籤/搜索