<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>

          積木Sketch插件進(jìn)階開發(fā)指南

          共 25472字,需瀏覽 51分鐘

           ·

          2021-04-02 10:42


          前段時(shí)間,美團(tuán)外賣技術(shù)團(tuán)隊(duì)積木Sketch插件“意外走紅”。為了幫助更多的設(shè)計(jì)師小哥哥、小姐姐落地設(shè)計(jì)規(guī)范,提升產(chǎn)研效率,積木Sketch團(tuán)隊(duì)開始著手打造一個(gè)平臺化的產(chǎn)品。本文介紹了積木Sketch插件進(jìn)階開發(fā)指南。希望通過本篇內(nèi)容的學(xué)習(xí),大家可以知道如何真正實(shí)現(xiàn)一款可以與業(yè)務(wù)強(qiáng)關(guān)聯(lián)且功能可定制的成熟工具。

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


          設(shè)計(jì)系統(tǒng)的真理來源越少,效率就越高?!狫on Gold,知名全棧設(shè)計(jì)師

          背景

          1. 積木工具鏈體系

          前段時(shí)間我們在美團(tuán)技術(shù)團(tuán)隊(duì)公眾號上發(fā)表了《積木Sketch Plugin:設(shè)計(jì)同學(xué)的貼心搭檔》一文,不曾想到,這個(gè)僅在美團(tuán)外賣C端使用的插件受到了更多的關(guān)注,美團(tuán)多個(gè)業(yè)務(wù)團(tuán)隊(duì)紛紛向我們拋出“橄欖枝”,表示想要接入以及并表達(dá)了愿意共同開發(fā)的意向,其它互聯(lián)網(wǎng)同行也紛紛詢問相關(guān)的技術(shù),一時(shí)讓我們有些“受寵若驚”?;叵肫饘懙谝黄恼碌臅r(shí)候,我們的內(nèi)心還是有些不安的。作為UI同學(xué)的一個(gè)設(shè)計(jì)工具,有些RD甚至沒有聽說過Sketch這個(gè)名字,我們很認(rèn)真地修改過上一篇文章的每一句措辭,爭取讓內(nèi)容更豐富有趣,當(dāng)時(shí)還很擔(dān)心不會被讀者接受。

          積木Sketch插件的“意外走紅”,確實(shí)有些出乎我們的意料,但正是如此,才讓我們知道UI一致性是絕大部分開發(fā)團(tuán)隊(duì)面臨的共性問題,大家對落地設(shè)計(jì)規(guī)范,提高UI中臺能力,提升產(chǎn)研效率都有著強(qiáng)烈的訴求。為了幫助更多團(tuán)隊(duì)提升產(chǎn)研效率,我們成立了袋鼠UI共建項(xiàng)目組,將門戶建設(shè)、工具鏈建設(shè)以及組件建設(shè)統(tǒng)一管理統(tǒng)一規(guī)劃,并將工具鏈的品牌確定為“積木”,而積木Sketch插件便是其中重要的一環(huán)。

          積木品牌Logo

          我們通過建立包含相同設(shè)計(jì)元素的統(tǒng)一物料市場,PM通過Axure插件拾取物料市場中的組件產(chǎn)出原型稿;UI/UE通過Sketch插件落地物料市場中的設(shè)計(jì)規(guī)范,產(chǎn)出符合要求的設(shè)計(jì)稿;而物料市場中的組件又與RD代碼倉庫中的組件一一對應(yīng),從而形成了一個(gè)閉環(huán)。未來,我們希望通過高保真原型輸出,可以給中后臺項(xiàng)目、非依賴體驗(yàn)項(xiàng)目提供更好的服務(wù)體驗(yàn),賦予產(chǎn)品同學(xué)直接向技術(shù)側(cè)輸出原型稿的能力。

          袋鼠UI工具鏈體系

          2. 積木插件平臺化

          伴隨著“積木”品牌的確立,越來越多的團(tuán)隊(duì)希望可以接入積木Sketch插件,其中部分團(tuán)隊(duì)也在和我們探討技術(shù)合作的可能性。UI設(shè)計(jì)語言與自身業(yè)務(wù)關(guān)聯(lián)性很強(qiáng),不同業(yè)務(wù)的色彩系統(tǒng)、圖形、柵格系統(tǒng)、投影系統(tǒng)、圖文關(guān)系千差萬別,其中任意一環(huán)的缺失都會導(dǎo)致一致性被破壞,業(yè)務(wù)方都希望通過積木插件實(shí)現(xiàn)設(shè)計(jì)規(guī)范的落地。為了幫助更多團(tuán)隊(duì)的UI同學(xué)提升設(shè)計(jì)效率,節(jié)約RD同學(xué)頁面調(diào)整的時(shí)間,同時(shí)也讓App界面具有一致性,從而更好地傳達(dá)品牌主張和設(shè)計(jì)理念,我們決定對積木插件進(jìn)行平臺化改造。平臺化是指積木插件可以接入各個(gè)業(yè)務(wù)團(tuán)隊(duì)的整套設(shè)計(jì)規(guī)范,通過平臺化改造,可以使積木插件提供的設(shè)計(jì)元素與業(yè)務(wù)強(qiáng)關(guān)聯(lián),滿足不同業(yè)務(wù)團(tuán)隊(duì)的設(shè)計(jì)需求。

          積木Skecth Plugin平臺化示意

          積木插件原本只是外賣提升UI/RD協(xié)作效率的一次嘗試,最初的目標(biāo)僅是UI一致性,但是現(xiàn)在已經(jīng)作為全面提升產(chǎn)研效率的媒介,承載了越來越多的功能。圍繞設(shè)計(jì)日常工作,提供高效的設(shè)計(jì)元素獲取方式,讓工作變得更輕松,是積木的核心使命。如何推動設(shè)計(jì)規(guī)范落地,并且輸出到各個(gè)業(yè)務(wù)系統(tǒng)靈活使用,是我們持續(xù)追尋的答案。而探尋研發(fā)和設(shè)計(jì)更為高效的協(xié)作模式也是我們一直努力的方向。

          通過一段時(shí)間的平臺化建設(shè),目前美團(tuán)已經(jīng)有7個(gè)設(shè)計(jì)團(tuán)隊(duì)接入了積木插件,覆蓋了美團(tuán)到家事業(yè)部大部分設(shè)計(jì)同學(xué),未來我們會持續(xù)推進(jìn)積木插件的平臺化建設(shè),不斷完善功能,期望能將積木插件打造成業(yè)界一流的品牌。

          3. Sketch插件開發(fā)進(jìn)階

          第一篇文章可能是為數(shù)不多的入門教程,而本篇可能是你能找到的唯一一篇進(jìn)階開發(fā)文章。進(jìn)階開發(fā)主要涉及如何切換業(yè)務(wù)方數(shù)據(jù),即選擇所屬業(yè)務(wù)方后,對應(yīng)的組件、顏色等設(shè)計(jì)素材切換為當(dāng)前業(yè)務(wù)方在物料市場中上傳的元素;將承載組件庫的Library文件轉(zhuǎn)化為插件可以識別的格式,并在插件上展示,以供設(shè)計(jì)師在繪制設(shè)計(jì)稿時(shí)選擇使用;一些優(yōu)化運(yùn)行效率,提升用戶體驗(yàn)的方法。

          Sketch插件代碼由于和業(yè)務(wù)強(qiáng)相關(guān),且實(shí)現(xiàn)方式較為復(fù)雜,可能存在部分敏感信息,所以基本沒有成熟的插件開源。在進(jìn)行一些復(fù)雜功能開發(fā)時(shí),我們也常?!罢啥蜕忻坏筋^腦”,“要不這個(gè)功能算了吧”的想法也不止一次出現(xiàn),可是每當(dāng)開會看到旁邊的設(shè)計(jì)同學(xué)在使用“積木”插件認(rèn)真作圖時(shí),又一次次堅(jiān)定了我們的信念,要不加班再試試吧,沒準(zhǔn)就能實(shí)現(xiàn)了呢?一次“委曲求全”,后面可能導(dǎo)致整個(gè)項(xiàng)目慢慢崩塌,所以我們一直以將積木插件打造成為業(yè)界領(lǐng)先的插件為信念。

          如果說看過了第一篇文章你已經(jīng)知道了如何開發(fā)一款插件,那么通過本篇文章的學(xué)習(xí)你就可以真正實(shí)現(xiàn)一款可以與業(yè)務(wù)強(qiáng)關(guān)聯(lián)且功能可定制的成熟工具,與其說是介紹如何開發(fā)一個(gè)進(jìn)階版的Sketch插件,不如說是分享給大家完成一個(gè)商業(yè)化項(xiàng)目的經(jīng)驗(yàn)。

          支持多業(yè)務(wù)切換

          為了當(dāng)面對“我們可以接入積木插件嗎”這種靈魂拷問時(shí)不再手足無措,平臺化進(jìn)程迅速啟動。平臺化的核心其實(shí)就是當(dāng)發(fā)生業(yè)務(wù)線變更時(shí),物料市場中的素材整體同步切換,因此我們需要進(jìn)行如下幾個(gè)操作:首先建立全局變量,存儲當(dāng)前用戶所述業(yè)務(wù)方信息及鑒權(quán)信息;用戶選擇功能模塊后,根據(jù)用戶所述業(yè)務(wù)方,拉取對應(yīng)素材;處理Library等素材并渲染頁面展示;根據(jù)素材信息變更畫板中的相關(guān)Layer。這部分主要介紹如何依靠持久化存儲來實(shí)現(xiàn)業(yè)務(wù)切換的功能,就像在第一篇啟蒙文檔中說的那樣,這里不會貼大段的代碼,只會幫你梳理最核心的流程,相信你親自實(shí)踐一次之后,以后的困難都可以輕松解決。

          1. 定義通用變量

          功能模塊展示的素材與當(dāng)前選擇的業(yè)務(wù)相關(guān),因此需要在每個(gè)功能模塊的Redux初始化狀態(tài)中增加一些全局狀態(tài)變量。比如所有模塊都需要使用businessType來確定當(dāng)前選擇的業(yè)務(wù),使用theme進(jìn)行主題切換,使用commonRequestParams獲取用戶鑒權(quán)信息等。

          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. 實(shí)現(xiàn)數(shù)據(jù)交換

          第一步:WebView側(cè)獲取用戶選擇,將所選的業(yè)務(wù)方數(shù)據(jù)通過window.postMessage方法傳遞至插件側(cè)。

          window.postMessage('onBusinessSelected', item);

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

           webViewContents.on('onBusinessSelected', item => {
              Settings.setSettingForKey('onBusinessSelected'JSON.stringify(item));
            });

          // 除此之外,插件側(cè)也可以通過localStorage向WebView注入數(shù)據(jù)
          browserWindow.webContents.executeJavaScript(
                `localStorage.setItem("${key}",'${data}')`
          );

          第三步:當(dāng)用戶通過工具欄選擇某一功能模塊(例如“插畫庫”)時(shí),會回調(diào)NSButton的點(diǎn)擊事件監(jiān)聽,此時(shí)除了需要要讓W(xué)ebView展示(Show)以及獲取焦點(diǎn)(Focus)外,還需要將第二步存儲的業(yè)務(wù)方信息傳入,并以此加載當(dāng)前業(yè)務(wù)方的物料數(shù)據(jù)。

          //用戶打開的功能模塊
          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側(cè)功能模塊收到初始化信息,開始進(jìn)行頁面渲染前的數(shù)據(jù)準(zhǔn)備。Object.keys( )方法會返回一個(gè)由給定對象的自身可枚舉屬性組成的數(shù)組,遍歷這個(gè)數(shù)組即可拿到所有被注入的初始化數(shù)據(jù),之后通過redux的store.dispatch方法更新state即可。至此實(shí)現(xiàn)業(yè)務(wù)切換功能的流程就全部結(jié)束了,是不是覺得非常簡單,忍不住想親自動手試一下呢?

          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. 小結(jié)

          有小伙伴會問,為什么WebView與Plugin側(cè)需要數(shù)據(jù)傳遞呢,它們不都屬于插件的一部分么?根本原因是我們的界面是通過WebView展示的,但是對Layer的各種操作是通過Sketch的API實(shí)現(xiàn)的,WebView只是一個(gè)網(wǎng)頁,本身與Sketch并無關(guān)系,因此必須使用bridge在兩者之間進(jìn)行數(shù)據(jù)傳遞。別擔(dān)心,這里再帶你把整個(gè)流程梳理一遍:

          1. 在插件啟動后會從服務(wù)端拉取業(yè)務(wù)方列表。

          2. 用戶在WebView中選擇自己所屬的業(yè)務(wù)方。

          3. 將業(yè)務(wù)方數(shù)據(jù)通過bridge傳遞至Plugin側(cè),并通過Sketch的Settings API進(jìn)行持久化存儲,這樣就可以保證每次啟動Sketch的時(shí)候無需再次選擇所屬業(yè)務(wù)方。

          4. 用戶點(diǎn)擊插件工具欄的按鈕選擇所需功能(例如色板庫、組件庫等),從持久化數(shù)據(jù)中讀取當(dāng)前所屬業(yè)務(wù)方,并通知WebView側(cè)拉取當(dāng)前業(yè)務(wù)方數(shù)據(jù)。至此,整個(gè)流程結(jié)束。

          Library庫文件自動化處理

          這部分將介紹如何將Library庫文件轉(zhuǎn)化為插件可以識別的JSON格式,并在插件上展示。

          如果要問Sketch插件最重要的功能是什么,組件庫絕對是無可爭議的C位。在長期的版本迭代中,隨著功能的不斷增加以及UI的持續(xù)改版,新舊樣式混雜,維護(hù)極為困難。設(shè)計(jì)師通過將頁面走查結(jié)果歸納梳理,制定設(shè)計(jì)規(guī)范,從而選取復(fù)用性高的組件進(jìn)行組件庫搭建。通過搭建組件庫可以進(jìn)行規(guī)范控制,避免控件的隨意組合,減少頁面差異;組件庫中組件滿足業(yè)務(wù)特色,同時(shí)具有云端動態(tài)調(diào)整能力,可以在規(guī)范更新時(shí)進(jìn)行統(tǒng)一調(diào)整。

          目前,我們將組件集成進(jìn)Sketch供UI使用大致分為兩個(gè)流派:一個(gè)是基于Sketch官方的Library庫文件,設(shè)計(jì)師通過將業(yè)務(wù)中復(fù)用性高的Symbol組件歸納整理生成庫文件(后綴.sketch),并上傳至云端,插件拉取庫文件轉(zhuǎn)化為JSON并在操作面板展示供選取使用;另一個(gè)則是采用類似Airbnb開源的React-Sketchapp這樣的框架,它可以讓你使用React代碼來制作和管理視覺稿及相關(guān)設(shè)計(jì)資源,官方把它稱作“用代碼來繪畫”,這種方案的實(shí)施難度較大,因?yàn)楸举|(zhì)上設(shè)計(jì)是感性和理性的結(jié)合,設(shè)計(jì)師使用Sketch是畫,而非帶有邏輯和層級關(guān)系的寫,他們對于頁面的樹形結(jié)構(gòu)很難理解,上手成本較高,而且代碼維護(hù)成本相對較大。我們不去評價(jià)哪種方案的好壞,只是第一種方案可以更好地滿足我們的核心訴求。

          Sketch組件庫處理效果示意

          1. 訂閱遠(yuǎn)程組件庫

          Library庫文件實(shí)際上是一個(gè)包含components的文檔,components包括了Symbols、Text Styles以及Layer Styles三類,將Library存儲在云端就可以在不同文檔甚至不同團(tuán)隊(duì)間共享這些components。由于組件庫實(shí)時(shí)指向最新,因此當(dāng)其維護(hù)者更新庫中的components時(shí),使用了這些components的文檔將會收到通知,這可以保證設(shè)計(jì)稿永遠(yuǎn)指向最新的設(shè)計(jì)規(guī)范。

          訂閱云端組件庫的方式很簡單,首先創(chuàng)建一個(gè)云端組件庫,具體可以參照上一篇文章,如果需要服務(wù)多個(gè)設(shè)計(jì)部門,則需要創(chuàng)建多個(gè)庫,每個(gè)庫有唯一的RSS地址;在插件中獲取到這些RSS地址后,可以通過Library.getRemoteLibraryWithRSS方法對其進(jìn)行訂閱。

          // 啟動插件時(shí)添加遠(yuǎn)程組件庫
          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庫文件轉(zhuǎn)換JSON數(shù)據(jù)

          將Sketch的Library文件轉(zhuǎn)換為JSON的過程,實(shí)際上就是轉(zhuǎn)換為WebView可以識別格式的過程。因?yàn)榉e木插件是將組件按照一定分組展示在面板中供設(shè)計(jì)師選取,因此需要根據(jù)組件分類組織其結(jié)構(gòu)。Sketch原生支持采用 "/" 符號對其進(jìn)行分組:Group-name/Symbol-name,比如命名為Button/Normal和Button/Pressed的兩個(gè)Symbols會成為Button Group的一部分。

          Symbol分組結(jié)構(gòu)

          實(shí)際中可以根據(jù)業(yè)務(wù)需要采用三級以上分組命名的方式,通過split方法將Symbol名稱通過 "/" 符號拆分為數(shù)組,第一級名稱、第二級名稱等各級名稱作為JSON結(jié)構(gòu)的不同層級即可,具體操作可以參照如下示例代碼:

           const document = library.getDocument();
              const symbols = [];
              _.forEach(document.pages, page => {
                _.forEach(page.layers, l => {
                  if (l.type && l.type === 'SymbolMaster') {
                    symbols.push(l);
                  }
                });
              });

           // 對symbol進(jìn)行分組處理,并生成json數(shù)據(jù)
           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,
                });
           }

          經(jīng)過以上操作后,一個(gè)簡化版的JSON文件如下方所示:

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

          3. Symbol縮略圖處理

          WebView默認(rèn)是不支持直接顯示Symbol供用戶拖拽使用的,解決該問題的方案有兩種:

          1. 通過dump分析Sketch的頭文件發(fā)現(xiàn),可以采用MSSymbolPreviewGenerator的imageForSymbolAncestry方法導(dǎo)出縮略圖,該方法支持圖片大小、顏色空間等多種屬性設(shè)置,優(yōu)勢是較為靈活,可以根據(jù)需要進(jìn)行任意配置,不過要承擔(dān)后期API變更的風(fēng)險(xiǎn)。
          2. 直接采用sketchDOM提供的export方法,將Symbol組件導(dǎo)出為縮略圖,之后在WebView中顯示縮略圖,當(dāng)拖拽縮略圖至畫板時(shí),再將其替換為Library中對應(yīng)的Symbol即可。
          import sketchDOM from 'sketch/dom';

          sketchDOM.export(symbolMaster, {
               overwritingtrue,
               'use-id-for-name'true,
               output: path,
               scales'1',
               formats'png',
               compression1,
          });

          4. 小結(jié)

          以上就是實(shí)現(xiàn)平臺化的一個(gè)基本流程了,不知道此時(shí)你有沒有聽得“云里霧里”。在這里,再把核心點(diǎn)帶大家復(fù)習(xí)一下。本節(jié)主要講了兩件事情:第一,插件如何才能支持多個(gè)業(yè)務(wù)方,即在插件的業(yè)務(wù)方列表中選擇相關(guān)業(yè)務(wù)方,就可以切換對應(yīng)的設(shè)計(jì)資源;第二,如何處理Library文件,將其轉(zhuǎn)換為JSON供WebView展示使用。具體流程如下:

          1. 不同設(shè)計(jì)組的UI同學(xué)制作完成包含各種components的Library后,通過后臺上傳至云端。

          2. RD同學(xué)根據(jù)當(dāng)前使用者所屬的設(shè)計(jì)團(tuán)隊(duì)拉取對應(yīng)的包括Library在內(nèi)的設(shè)計(jì)素材,顏色、圖片,iconFont等設(shè)計(jì)素材可以直接展示,可是Library文件不支持在WebView中直接顯示,需要進(jìn)行處理。

          3. 根據(jù)和UI同學(xué)約定組件的命名規(guī)則,通過使用“/”分割,將第一級名稱、第二級名稱等各級名稱作為JSON結(jié)構(gòu)的不同層級,再通過sketchDOM提供的export方法將Symbol轉(zhuǎn)換為png格式的縮略圖即可在插件中顯示。

          4. 將選中的縮略圖拖拽至Sketch的畫板時(shí),再將縮略圖替換為Library中的真實(shí)Symbol即可。

          Library庫文件處理小結(jié)

          操作體驗(yàn)優(yōu)化

          完成了上述步驟后,就可以完成一款支持多業(yè)務(wù)方的插件了。但是隨著積木Sketch插件接入的業(yè)務(wù)方越來越多,除了聽到可以顯著提升效率的褒獎外,對插件的吐槽聲也常常傳入我們的耳邊。市面上成熟的插件也有很多,我們無法限制別人的選擇,所以只能讓積木變得更好用,本節(jié)主要介紹插件的優(yōu)化方法。

          為了更好地傾聽大家意見,積木插件通過各種措施了解用戶的真實(shí)想法。首先積木插件接入美團(tuán)內(nèi)部的TT(Trouble Tracker)系統(tǒng),相比公司很多專業(yè)系統(tǒng),TT不帶任何專業(yè)流程和定制化,只做純流轉(zhuǎn),是一套適用于公司內(nèi)部的、通用的問題發(fā)起、響應(yīng)和追蹤系統(tǒng),用戶反饋的問題自動創(chuàng)建工單并與對應(yīng)RD關(guān)聯(lián),Bug可以最快速修復(fù);插件內(nèi)部增加反饋渠道,用戶反饋及時(shí)發(fā)送給相關(guān)PM,作為下次功能排期的權(quán)重指標(biāo);插件內(nèi)部增加多維度埋點(diǎn)統(tǒng)計(jì),從設(shè)計(jì)滲透到高頻使用兩個(gè)方面了解UI同學(xué)的核心訴求。以下介紹了根據(jù)反饋整理的部分高優(yōu)先級問題的解決方案。

          1. 操作界面優(yōu)化

          很多RD在開發(fā)過程中,對界面美化往往嗤之以鼻,“這個(gè)功能能用就可以了”常常被掛在嘴邊。難道UI的需求真的是中看不中用?一個(gè)產(chǎn)品設(shè)計(jì)師說過,最早的產(chǎn)品僅依靠功能就可以在競品中脫穎而出,能不能用就成為了一個(gè)產(chǎn)品是否合格的標(biāo)準(zhǔn)。后來在越來越成熟的互聯(lián)網(wǎng)環(huán)境中,易用性成了一個(gè)新的且更重要的標(biāo)準(zhǔn),這時(shí)同類產(chǎn)品間的功能已經(jīng)非常接近,無法通過不斷堆疊功能產(chǎn)生明顯差異。而當(dāng)同類產(chǎn)品的易用性也趨于相近時(shí),如何解決產(chǎn)品競爭力的問題就再一次擺在面前,這時(shí)就要賦予產(chǎn)品情感,好的產(chǎn)品關(guān)注功能,優(yōu)秀的產(chǎn)品關(guān)注情感,可以讓用戶在使用中感受到溫暖。

          WebView優(yōu)化

          當(dāng)我們經(jīng)過了仔細(xì)的功能驗(yàn)證,興致勃勃的把第一版積木插件給用戶體驗(yàn)時(shí),本來滿心歡喜準(zhǔn)備迎接夸獎,沒想到卻得到了很多交互體驗(yàn)不好的反饋,“僅僅實(shí)現(xiàn)功能就及格了”這個(gè)理論在一像素都不肯放過的設(shè)計(jì)師眼中肯定行不通。原生WebView給用戶的體驗(yàn)往往不夠優(yōu)秀,其實(shí)只要一些很簡單的設(shè)置就可以解決,但是這并不代表它不重要。

          WebView視圖優(yōu)化點(diǎn)舉例

          禁止觸摸板拖拽造成頁面整體“橡皮筋”效果,禁止用戶選擇(user-select),溢出隱藏等操作,使WebView具有“類原生”效果,都會提升用戶的實(shí)際使用體驗(yàn)。

          html,
          body,
          #root {
            height100%;
            overflow: hidden;
            user-select: none;
            background: transparent;
            -webkit-user-select: none;
          }

          除此之外在正式環(huán)境中(NODE_ENV為production),我們并不希望當(dāng)前界面響應(yīng)右鍵菜單,需要通過給document添加EventListener監(jiān)聽將相關(guān)事件處理方法屏蔽。

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

          工具欄優(yōu)化

          Sketch對于設(shè)計(jì)師的意義,就像代碼編輯器對于程序員一樣,工作中幾乎無時(shí)無刻也離不開。在積木Sketch插件走出美團(tuán)外賣,被越來越多的設(shè)計(jì)團(tuán)隊(duì)采用后,為了讓它更加賞心悅目,UI同學(xué)決定對工具條進(jìn)行一次全新的視覺升級。原生界面開發(fā)指的是通過macOS的AppKit進(jìn)行用戶界面開發(fā),在插件開發(fā)中一些需要嵌入Sketch面板的UI模塊就需要進(jìn)行原生界面開發(fā),比如吸附式工具條就屬于通過macOS原生API開發(fā)的界面。

          原生開發(fā)既可以使用Objective-C語言,也可以使用CocoaScript通過寫JavaScript的方式進(jìn)行開發(fā)。CocoaScript 通過Mocha實(shí)現(xiàn)JS到Objective-C的映射,可以讓我們通過JS調(diào)用Sketch內(nèi)部API以及macOS的Framework。在通過CocoaScript原生開發(fā)前需要了解一些基礎(chǔ)知識:

          1. 在使用相關(guān)框架前需要通過framework()方法進(jìn)行引入,而Foundation以及CoreGraphics是默認(rèn)內(nèi)置的,無需再單獨(dú)操作。

          2. 一些Objective-C的selectors選擇器需要指針參數(shù),由于JavaScript不支持通過引用傳遞對象,因此CocoaScript提供了MOPointer作為變量引用的代理對象。

          UI調(diào)整一般分為三個(gè)部分:布局調(diào)整、動效調(diào)整、圖片替換。下面的章節(jié)會進(jìn)行逐一介紹。

          新版積木工具欄效果圖

          布局調(diào)整

          這里UI的需求是NSButton的寬度填充滿整個(gè)NSStackView,高度自定義。由于此功能看起來過于簡單,當(dāng)時(shí)認(rèn)為估時(shí)0.5天綽綽有余,可是沒想到搭進(jìn)去了1個(gè)工作日加上2天周末的時(shí)間,因?yàn)闊o論如何設(shè)置NSStackView中子View尺寸都無法生效。

          在頂住了周圍人“UI問題不影響功能使用,以后有時(shí)間再優(yōu)化吧”的“輿論壓力”后,終于在官方文檔里面發(fā)現(xiàn)了線索:“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的方式進(jìn)行自動布局(可以類比Android中的ConstraintLayout),在進(jìn)行尺寸修改時(shí),是需要添加錨點(diǎn)的,因此需要通過Anchor的方式進(jìn)行尺寸修改。

          // 創(chuàng)建工具條
          const toolbar = NSStackView.alloc().initWithFrame(NSMakeRect(0045400));
          toolbar.setSpacing(7);
          // 創(chuàng)建NSButton
          const button = NSButton.alloc().initWithFrame(rect)
          // 設(shè)置NSButton寬高
          button
              .widthAnchor()
              .constraintEqualToConstant(rect.size.width)
              .setActive(1);
          button
              .heightAnchor()
              .constraintEqualToConstant(rect.size.height)
              .setActive(1);
          button.setBordered(false);
          // 設(shè)置回調(diào)點(diǎn)擊事件
          button.setCOSJSTargetFunction(onClickListener);
          button.setAction('onClickListener:');
          // 添加NSButton至NSStackView中
          toolbar.addView_inGravity(button, inGravityType);

          動效調(diào)整

          NSButton內(nèi)置的點(diǎn)擊效果大約15種,可以通過NSBezelStyle進(jìn)行設(shè)置。積木插件工具欄并沒有采用點(diǎn)擊后icon反色的通用處理方式,而是點(diǎn)擊后將背景色置為淺灰。如果想要自定義一些點(diǎn)擊效果,只需在NSButton點(diǎn)擊事件的回調(diào)中設(shè)置即可。

          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插件既支持加載本地圖片,也支持加載網(wǎng)絡(luò)圖片。加載本地圖片時(shí),可以通過context.plugin的方法獲取一個(gè)MSPluginBundle對象,即當(dāng)前插件bundle文件,它的url()方法會返回當(dāng)前插件的路徑信息,進(jìn)而幫助我們找到存儲在插件中的本地文件;而加載網(wǎng)絡(luò)圖片則更加簡單,通過NSURL.URLWithString( )可以獲得一個(gè)使用圖片網(wǎng)址初始化得到的NSURL對象,這里要格外注意的是,對于網(wǎng)絡(luò)圖片請使用https域名。

          //本地圖片加載
          const localImageUrl = 
                 context.plugin.url()
                .URLByAppendingPathComponent('Contents')
                .URLByAppendingPathComponent('Resources')
                .URLByAppendingPathComponent(`${imageurl}.png`);

          //網(wǎng)絡(luò)圖片加載
          const remoteImageUrl = NSURL.URLWithString(imageUrl);

          //根據(jù)ImageUrl獲取NSImage對象
          const nsImage = NSImage.alloc().initWithContentsOfURL(imageURL);
          nsImage.setSize(size);
          nsImage.setScalesWhenResized(true);

          2. 執(zhí)行效率優(yōu)化

          只有在設(shè)計(jì)稿中盡可能多地使用組件進(jìn)行設(shè)計(jì),并且將已有頁面中的內(nèi)容通過設(shè)計(jì)師的走查梳理逐漸替換成組件,才能真正通過建設(shè)組件庫來進(jìn)行提效。隨著設(shè)計(jì)團(tuán)隊(duì)逐步將設(shè)計(jì)語言沉淀為設(shè)計(jì)規(guī)范,并將其量化內(nèi)置于積木插件中,組件的數(shù)量越來越多,積木插件組件庫作為UI同學(xué)使用最頻繁的功能,需要格外關(guān)注其運(yùn)行效率。

          前置組件庫加載

          將組件庫的加載邏輯前置,在打開文檔時(shí)對遠(yuǎn)程組件庫進(jìn)行訂閱操作。Sketch所提供的了Action API可以使插件對應(yīng)用程序中的事件做出反應(yīng),監(jiān)聽回調(diào)只需在插件的manifest.json文件中添加一個(gè)handler即可,添加了對于“OpenDocument”的監(jiān)聽,也就是告訴插件在新文檔被打開時(shí)要去執(zhí)行addRemoteLibrary這個(gè)function。

          {
                "script""./libraryProcessor.js",
                "identifier""libraryProcessor",
                "handlers": {
                  "actions": {
                    "OpenDocument""addRemoteLibrary"
                  }
                }
          }

          增加緩存邏輯

          組件庫的處理需要將Library文件轉(zhuǎn)換為帶有層級信息的JSON文件,并且需要將Symbol導(dǎo)出為縮略圖顯示。由于這個(gè)步驟較為耗時(shí),因此可以將經(jīng)過處理的Library信息緩存起來,并通過持久化存儲記錄已緩存的Library版本。若已緩存的版本與最新版本一致,且縮略圖與JSON文件均完整,則可以直接使用緩存信息,極大的提高Library的加載速度。以下非完整代碼,僅作示例:

          verifyLibraryCache(businessType, libraryVersion) {
              const temp = Settings.settingForKey('libraryJsonCache');
              const libraryJsonCache = temp ? JSON.parse(temp) : null;

              // 1.驗(yàn)證緩存版本信息
              if (libraryJsonCache.version.businessType !== libraryVersion) {
                return null;
              }

              // 2.驗(yàn)證縮略圖完整性
              const home = getAssertURL(this.mContext, 'libraryImage');
              const path = join(home, businessType);
              if (!fs.existsSync(path) || !fs.readdirSync(path)) {
                return null;
              }

              // 3.驗(yàn)證業(yè)務(wù)庫Json文件完整性
              if (libraryJsonCache[businessType]) {
                console.info(`當(dāng)前${businessType}命中緩存`);
                return libraryJsonCache;
              } else {
                return null;
              }
            }
          }

          3. 自定義Inspector屬性面板

          與Objective-C工程混合開發(fā)

          隨著各個(gè)設(shè)計(jì)組的組件庫建設(shè)不斷完善,抽離的組件數(shù)量不斷增多,不少UI同學(xué)反饋Sketch原生組件樣式修改面板操作不夠便捷,無法約束選擇范圍,希望可以提供一種更有效的組件overrides修改方式,并且當(dāng)修改“圖片”、“圖標(biāo)”、“文字”等圖層時(shí),可以和積木插件的這些功能模塊進(jìn)行聯(lián)動選擇。實(shí)現(xiàn)自定義Inspector面板功能既可以使操作更便捷,又可以對修改項(xiàng)進(jìn)行約束。

          自定義屬性面板功能的基本思想,是將組件從組件庫拖至Sketch畫板中時(shí),組件的可修改屬性可以顯示在Sketch本身的屬性面板上。我們引入了Objective-C原生開發(fā)以實(shí)現(xiàn)對Sketch界面的修改,為什么要使用原生開發(fā)?雖然官方提供了JS API并承諾持續(xù)維護(hù),但這項(xiàng)工作一直處于Doing狀態(tài),而且官方文檔更新緩慢,沒有明確的時(shí)間節(jié)點(diǎn),因此對于自定義Native Inspector Panel這種需要Hook API的功能,使用原生開發(fā)較為便捷,而且對于iOS開發(fā)者也更加友好,無需再學(xué)習(xí)前端界面開發(fā)知識。

          Sketch Inspector面板操作區(qū)優(yōu)化

          Xcode工程配置

          通過Xcode工程構(gòu)建自定義屬性面板,最終生成一個(gè)可以供JS側(cè)調(diào)用的Framework。可以參考上一篇文章介紹的方法創(chuàng)建Xcode工程,該工程在每次構(gòu)建后會自動生成測試Sketch插件并放入對應(yīng)的文件夾中。需要注意的一點(diǎn)是,這里生成的插件只是為了方便開發(fā)和調(diào)試,后面會介紹如何將XCode工程構(gòu)建的Framework集成至JS主工程中。

          Xcode工程配置示意

          積木插件的主體功能使用JS代碼實(shí)現(xiàn),但是自定義屬性選擇面板使用Objective-C代碼實(shí)現(xiàn)。為了實(shí)現(xiàn)積木插件的JS側(cè)功能模塊與OC側(cè)模塊之間的通信和橋接,這里借助了Mocha框架來實(shí)現(xiàn)相關(guān)的功能,Mocha框架也被Sketch官方所使用,將原生側(cè)的方法封裝為官方API后暴露給JS側(cè)。

          Sketch與插件Framework通信原理

          組件選中時(shí),Sketch軟件會回調(diào)onSelectionChanged方法給JS側(cè),JS側(cè)借助Mocha框架可以實(shí)現(xiàn)對OC側(cè)的調(diào)用,同時(shí)將參數(shù)以O(shè)C對象的方式傳遞。JS側(cè)傳遞給OC側(cè)的Context內(nèi)容很豐富,包含了選中的組件、相關(guān)圖層還有Sketch軟件本身的信息。雖然Sketch沒有提供API,但是Objective-C語言本身具備KVO監(jiān)聽對象屬性的能力,我們通過讀取對應(yīng)的屬性值,就可以獲取需要的對象數(shù)據(jù)。

          + (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官方?jīng)]有將屬性面板的修改能力暴露給插件側(cè),通過查詢Sketch頭文件發(fā)現(xiàn)通過reloadWithViewControllers:方法可以實(shí)現(xiàn)屬性面板刷新,但是在實(shí)際開發(fā)過程中發(fā)現(xiàn)在某些版本的Sketch上會出現(xiàn)面板閃動的問題,這里借助Objective-C的Method Swizzle特性,直接修改reloadWithViewControllers:的運(yùn)行時(shí)行為解決。

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

          Swizzle方法會修改原始方法的行為,實(shí)際操作中只有在滿足特定條件的情況下才應(yīng)觸發(fā)Swizzle后的方法。

          Swizzle方法觸發(fā)條件

          組件屬性修改與替換原理

          通過自定義面板可以修改組件的可覆蓋項(xiàng)(即override),目前可以應(yīng)用可覆蓋項(xiàng)的affectedLayer有Text/Image/Symbol Instance三種。設(shè)計(jì)師與開發(fā)者在此前對圖層的格式進(jìn)行了約定,保證我們可以按照統(tǒng)一的方式讀取并替換圖層的屬性值。

          替換文本

          基于class-dump,我們可以找出Sketch中聲明的所有類的屬性和方法,文本處理的策略是,找到圖層中的所有MSAvailableOverride對象,這些對象即表示可用的覆蓋項(xiàng),對文本信息的修改實(shí)際上是通過修改MSAvailableOverride對象的overridePoint來實(shí)現(xiàn)的。

          id overridePoint = [availableOverride valueForKeyPath:@"overridePoint"];
          [symbolInstance setValue:text forOverridePoint:overridePoint];

          更改樣式

          樣式設(shè)置的策略,是找到當(dāng)前選中組件對應(yīng)的Library中相關(guān)樣式的組件。由于所有的組件都遵循統(tǒng)一的命名格式,因此只要根據(jù)組件命名就能篩選出符合要求的組件。

          // 命名方式:一級分類/二級分類/組件名稱,基于圖層獲取對應(yīng)library
          id library = [self getLibraryBySymbol:layer];
          // 讀取組件名稱
          NSString *layerName = [symbol valueForKeyPath:@"name"];
          // 配置符合當(dāng)前業(yè)務(wù)的Predicate
          NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH [cd] %@", prefix];
          // 篩選符合Predicate的所有組件
          NSArray *filterResult = [allSybmols filteredArrayUsingPredicate:predicate];

          當(dāng)使用者選中某一個(gè)樣式后,插件會將設(shè)計(jì)稿上的組件替換為選中的組件,這里需要使用MSSymbolInstance中的changeInstanceToSymbol方法來實(shí)現(xiàn)。需要注意的是,changeInstanceToSymbol僅僅替換了圖層中的組件,但是并沒有修改圖層上組件的屬性,對于位置和大小等信息需要單獨(dú)進(jìn)行處理。

          // 在更新圖層上的組件之前,我們需要把組件導(dǎo)入到當(dāng)前document對象中
          id foreignSymbol = [libraryController importShareableObjectReference:sharedObject intoDocument:documentData];

          //更新圖層上的組件 
          [symbolInstance changeInstanceToSymbol:localSymbol];

          調(diào)試技巧

          OC側(cè)開發(fā)的最大問題,在于沒有官方API的支持。因此調(diào)試器就顯得非常重要,單步調(diào)試可以讓我們非常方便地深入到Sketch內(nèi)部了解Document內(nèi)部的數(shù)據(jù)結(jié)構(gòu)。調(diào)試環(huán)境需要配置,但足夠簡單,并且對于開發(fā)效率的提升是指數(shù)級的。

          1. 對構(gòu)建Scheme的配置。

          2. Attach到Sketch軟件上,這樣就可以實(shí)現(xiàn)斷點(diǎn)調(diào)試。

          與當(dāng)前JS工程混合編譯

          1. 通過skpm中內(nèi)置的@skpm/xcodeproj-loader編譯XCode工程,并將產(chǎn)物framework拷貝至插件文件夾。

          const framework = require('../../RooSketchPluginXCodeProject/RooSketchPluginXCodeProject.xcworkspace/contents.xcworkspacedata');

          2通過Mocha提供的loadFrameworkWithName_inDirectory方法,設(shè)置Framework的名稱及路徑即可進(jìn)行加載。

          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調(diào)用framework中的方法

          // 找到已經(jīng)被加載的framework
           const frameworkClass = NSClassFromString('RooSketchPluginNativeUI');
          // 調(diào)用暴露的方法
          frameworkClass.onSelectionChanged(context);

          一起拼積木

          目前,積木插件已經(jīng)在美團(tuán)到家事業(yè)部遍地開花,我們希望未來積木品牌產(chǎn)品可以在更大范圍內(nèi)得到應(yīng)用,幫助更多團(tuán)隊(duì)落地設(shè)計(jì)規(guī)范,提升產(chǎn)研效率,也歡迎更多團(tuán)隊(duì)接入積木工具鏈。

          “不忘初心,方得始終”,就像第一篇啟蒙文章中說的那樣,我們除了希望制作一流的產(chǎn)品,也希望積木插件可以讓大家在繁忙的工作中得以喘息。我們會繼續(xù)以設(shè)計(jì)語言為依托,以積木工具鏈為抓手,不斷完善優(yōu)化,拓展插件的使用場景,讓設(shè)計(jì)與開發(fā)變得更輕松。

          總有人在問,積木插件現(xiàn)在好用嗎?我想說,還不夠好用。但是每次評審需求時(shí)看到旁邊的設(shè)計(jì)師在認(rèn)真地使用我們的插件作圖,看到積木插件愛好者為我們制作表情包幫助我們推廣,我們深知唯有交付最棒的產(chǎn)品,才能不辜負(fù)大家的期待。

          平臺化二期的需求剛剛確定完畢,人力分配排期結(jié)束,我們又想了一大波令你拍手稱贊的功能,馬上就要踏上新的征程。夜深了,看著窗外人家的燈,一個(gè)個(gè)熄滅,夜空也變得越來越明亮。我們的目標(biāo),是星辰大海。

          使用積木插件插畫庫制作的表情包 Design By 雪美

          致謝

          感謝外賣技術(shù)部曉飛、彥平、瑤哥、云鵬、冰冰對項(xiàng)目的大力支持。

          感謝到家事業(yè)部優(yōu)秀的設(shè)計(jì)師冉冉、昱翰、淼林、雪美、田園、璟琦。

          感謝閃購技術(shù)團(tuán)隊(duì)章琦、CRM團(tuán)隊(duì)的怡婷、CI王鵬協(xié)助技術(shù)開發(fā)。

          參考文獻(xiàn)

          • 百度Sketch插件開發(fā)總結(jié)

          • 愛奇藝產(chǎn)品工作流優(yōu)化:搭建組件庫做高ROI

          • 阿里重磅開源中后臺UI解決方案Fusion

          • Painting with Code

          • Sketch Developers Discussion
          推薦閱讀
          1. CSS變量對JS交互組件開發(fā)帶來的提升與變革

          2. Vue scoped與深度選擇器deep的原理

          3. 如何做到在 Markdown 中使用 Vue 語法

          4. 【深入vue】為什么Vue3.0不再使用defineProperty實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽?

          5. 帶你五步學(xué)會Vue SSR

          6. Vue3 新增API

          7. Vue Router history模式的配置方法及其原理

          ??愛心三連擊

          1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的點(diǎn)贊,在看是我創(chuàng)作的動力。

          2.關(guān)注公眾號前端名獅,回復(fù)「1」加入前端交流群,一起學(xué)習(xí)進(jìn)步!

          3.也可添加微信【qq1248351595】,一起成長。


          “在看轉(zhuǎn)發(fā)”是最大的支持

          瀏覽 55
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  波多野结衣视频免费在线观看 | 精品久久久久久久久久 | 影音先锋av资源在线 | 大香蕉在线看 | 国产一级黄|