積木Sketch Plugin:設計同學的貼心搭檔

多年來,美團外賣一直在高速增長,但整個客戶端的UI組件一直沒有得到很好的統一。而在開發過程中因UI缺乏同一的標準導致各種問題凸顯,積木插件Sketch Plugin應運而生。外賣技術團隊將其打造成爲UI一致性的抓手,最終幫助團隊減少開發成本,提升了交付的質量,併爲美團多個業務團隊提供了很好的支持服務。

本文主要介紹了Sketch Plugin項目的背景,並由淺入深地帶領大家認識了Sketch Plugin項目,文中還詳細解讀了該技術的各種優缺點,同時還提供了詳細的實踐步驟和踩坑總結。希望本文對大家打造一致性體驗能夠有所幫助。

A consistent experience is a better experience.——Mark Eberman

| 一致的體驗是更好的體驗。——Mark Eberman,知名設計師

背景

1. UI一致性項目

積木(Tangram)Sketch插件源於美團外賣UI的一致性項目,該項目自2019年5月份被提出,是UI設計團隊與研發團隊共建的項目,目的是改善用戶端體驗的一致性,提升多技術方案間組件的通用性和複用率,整體降低視覺改版的研發成本。

一直以來,外賣業務都處於高速發展階段,人員規模在不斷擴大,項目複雜度在持續增加。目前平臺承載了美團餐飲、商超、閃購、跑腿、藥品等多個業務品類,用戶入口也覆蓋了美團App外賣頻道、外賣App、大衆點評等多個獨立應用。因爲客戶端一直比較側重業務開發,爲了滿足業務快速上線的需求,UI組件並沒有統一的實現,而是分散到各個業務場景中,在開發過程中因UI缺乏同一的標準而導致以下問題不斷凸顯:

UI/UE層面

① UI缺乏標準化的設計規範,在不同App及不同語言平臺上設計風格不統一,用戶體驗不一致。

② 設計資源與代碼均缺乏統一的管理手段,無法實現積累沉澱,無法適應新業務的開發需求。

RD層面

① 組件代碼實現碎片化,存在多次開發的情況,質量難以得到保證。

② 各端代碼API不統一,維護拓展成本較高,變更主題、適配Dark Mode等需求難以實現。

QA層面

重複走查,頻繁迴歸,每次發版均需驗證組件質量。

PM層面

版本迭代效率低,版本需求吞吐量低,不能滿足業務的快速拓展能力。

基於上述開發工作中的切實痛點,以及未來可預見的對客戶端能力的開發需求,我們迫切需要一套統一的UI設計規範,以此沉澱出設計風格,建立統一的UI設計標準,從而抽離成熟的業務場景,提供高質量、可擴展、可統一配置的同時能基於Android/iOS/MRN/Mach組件開發的代碼庫,且具備支持多業務高層次的代碼複用能力,提高UI業務的中臺能力,使項目具有高度一致性。

我們通過積木Sketch插件來落地設計規範,可以保證設計元素均從既定設計標準中獲取,產出符合業務設計語言的設計稿,而各平臺UI組件庫中也有對應實現,從而使積木插件成爲UI一致性的抓手,最終可以減少開發成本,提升交付質量,服務好我們美團的多個業務團隊。

外賣UI一致性項目

2. Sketch & Sketch Plugin

要想保持UI一致性,就不能打破規則。從設計階段顏色的選擇、字體的規範、控件的樣式到RD開發階段代碼的統一管理、API的制定、多端的實現方式,都必須遵守一套規則,而Sketch Plugin建設則是讓規範落地執行的解決方案。

在討論其重要性之前,我們首先簡單介紹一下Sketch:Sketch是一個設計工具包,由總部位於荷蘭海牙的BohemianCoding團隊開發,該團隊成員目前不足百人,來自全球多個國家,通過互聯網遠程協作開發,屬於典型的高效開發團隊。

Sketch容易理解且上手簡單;可與團隊中的每個人創建、更新和共享所有Symbol組件,實現設計資源的共享和版本管理,從此告別“final-final-final-1”;其版本迭代速度非常快,且能不斷添加新功能,滿足用戶的需求,更符合互聯網時代;Sketch可以使用真實數據進行設計。目前,我們設計團隊已經全面使用Sketch進行設計。

設計語言包括Iconfont、色板、文字規範、話術、插畫、動畫、組件等。其實它並不是一個抽象的概念,比如大家提到“美團”就會想起“美團黃”,想到可愛的“袋鼠”,想到那些騎着摩托車、穿着印有“美團外賣”亮黃色衣服的騎手小哥。通過設計語言,我們可以更好地傳達品牌主張和設計理念。UI團隊逐步將設計語言沉澱爲設計規範,並將其量化內置於積木Sketch Plugin中,使產出的設計稿和RD代碼庫中的組件一一對應,從而形成一個完整的閉環,進而可加速整個業務的交付流程。

使用Sketch Plugin可以快速設計出標準頁面

積木Sketch Plugin功能演示

3. 積木Sketch 插件項目

其實,市面上已存在類似插件,爲什麼我們還要自己動手開發呢?因爲UI設計語言與自身業務關聯性很強,不同業務的色彩系統、圖形、柵格系統、投影系統、圖文關係千差萬別,其中任意一環的缺失都會導致一致性被破壞。現有插件所提供的通用設計元素無法滿足外賣設計團隊的需求,開發一款可以與業務強關聯且功能可定製的插件,顯得尤爲重要。

此外,統一的品牌符號、品牌特徵,也有助於加深產品在用戶心中的印象,統一的顏色和交互形式能幫助用戶加深對產品的熟悉感和信任感,一個好的設計語言本身可以在體驗上爲產品加分,也能夠更好創造一致性的體驗。

積木Sketch插件經過一段時間的建設,目前已具備Iconfont、標準色板、組件庫、數據填充、文字模板等功能。

我們通過Iconfont可以從美團的圖標庫中拉取設計團隊上傳的SVG圖標,並直接應用於設計稿;標準色板可以限定設計師的顏色使用範圍,確保設計稿中的顏色均符合設計規範;組件庫中包含從外賣業務中抽離的基本控件與通用組件,具有可複用和標準化的特點,並與不同語言平臺組件庫中的代碼一一對應,使用組件庫中的組件進行設計,可以提升UI的設計效率、開發效率以及走查效率;數據填充庫可以實現圖片填充和文本填充,圖片包含了商品及商家素材,文字則包含了菜品、商鋪名等信息,通過數據填充可以使設計師採用真實數據進行填充,讓設計稿更爲直觀,也更貼近線上環境;文字模板中內置了Head、SubTitle、Body、Caption的使用規範,根據設計稿中文字的位置,點擊文字圖層即可直接應用字體、行高、字距等屬性。

此外,我們還根據設計同學的使用反饋,不斷增添新功能。同時也在拓展插件的使用場景,增加業務線切換功能,使積木插件可以爲更多的團隊服務,並期待它能成爲更多設計師的“貼心搭檔”。

積木Sketch Plugin已支持功能

4. 爲什麼要寫這篇文章?

相信你讀完上面的內容,肯定迫不及待的想了解一下Sketch插件,以此迅速提升自己團隊開發效率了吧?

其實在開始之前,我們可先了解一些不利的條件。第一點,由於Sketch更新速度極快,但是官方文檔卻十分簡單且陳舊,因此很多知名的Sketch Plugin因每次API的變更過大紛紛放棄維護;第二點,由於開發技術棧混亂,成熟項目一般還未開源,而開源的項目基本上沒有什麼參考價值,絕大多數都是“update 3 years ago”;最後一點,macOS開發資料更是少的可憐。

我們閱讀了大量的文檔卻沒有理清頭緒,彷彿很多Wiki講到關鍵地方,比如某個非常期待的功能是怎麼實現的時候,作者竟然一筆帶過,讓人摸不到頭腦。知乎上一篇Sketch Plugin的科普文,很多網友會評論“求教學視頻,我可以花錢買的”。經過一步步踩坑,我們就總結了一些開發經驗,爲了避免大家“重複踩坑”,晚上可以早點下班陪陪家人,我們決定寫一篇文章記錄下開發的過程。雖然比起那些已經更新多版的成熟項目,但還有不少的差距,至少可以讓大家不再那麼迷茫。

當然,即使你覺得自己是個“跟Sketch八竿子打不着”的開發同學,我們也覺得這篇文章同樣也值得閱讀,因爲你會通過本文接觸到前端、移動端、桌面端、服務端的各種開發知識。我們都知道,越來越多的公司開始喜歡招全棧工程師,像Facebook基本上只招全棧工程師。你心裏是不是在想:“是不是在搞笑啊?不過一個插件而已?”先別輕易下結論。

準備好了嗎?盤它!

準備放手Coding之前

好,先彆着急敲擊鍵盤。畢竟我們連使用哪種語言去開發都沒決定,這曾經也是困惱我們許久的一個問題。目前Sketch Plugin開發大概有兩種方式:

① 使用JavaScript + CocoaScript的混合開發模式,Sketch團隊官方維護了一套JS API,並在開發者官網寫了一句非常振奮人心的話:“ Take advantage of ES6, access macOS frameworks and use the Sketch APIs without learning Objective-C or Swift.”

理想很美滿,但現實很骨感。這個API目前還不算完善,很多功能無法實現,因此我們需要搭配CocoaScript訪問更豐富的內部API。

② 直接採用Objective-C 或Swift,並搭配macOS的UI框架AppKit進行開發,簡單粗暴,並且可以利用OC運行時直接調用Sketch內部API。但這裏要特別提醒一下,你要承擔的風險是:隨着Sketch的不斷更新,內部API的命名和使用方式可能會發生較大變化,很多知名插件都因此放棄更新

本文采用了“混合開發模式”進行講解,希望能夠給你一些小啓發。

Sketch 開發原理

1. Sketch Plugin開發流派

2. 環境配置

Skpm(Sketch Plugin Manager)是Sketch提供的用於Plugin創建、Build以及發佈的官方工具。Skpm採用Webpack作爲打包工具,當然如果你對前端知識足夠熟悉,也可以採用Rollup或者roadhog。但是,爲了防止遇到各種各樣的報錯,這裏並不建議你這麼做。

Skpm提供了一系列幫助快速入門的模板,最有用的莫過於skpm/with-webview,它可以幫助我們創建一個基於WebView展示的Demo示例,而且Skpm會在構建完成後,自動創建一個Symbolic Link將插件添加到Sketch的安裝目錄,使Plugin立即可用。

//基於webpack的Sketch官方打包工具skpm
npm install -g skpm
//創建示例工程
skpm create my-plugin --template=skpm/with-webview
//Install the dependencies
npm install
//構建插件
npm run build

3. 項目結構

Plugin Bundle

按照上面的步驟操作完成後,我們會得到如下插件目錄,它以標準化的分層結構存儲了源碼文件以及構建生成的Sketch插件安裝包。這裏沒有使用官方文檔中最簡單的Demo,而是使用目前開發中最爲常用的With-Webview模板進行分析,以免出現學完“1+1”後遇到的全是“微積分”問題,並且大部分插件均是在此基礎上進行拓展。

目錄中的參數,相信你在看完註釋後馬上就能明白。可是如果此前沒有前端開發經驗,可能不瞭解在經過Webpack打包後,腳本文件的文件名會發生變更,比如resources中的webview.js經過打包後會儲存在插件的Resources文件夾中,而文件名則變更爲resources_webview.js,因此在進行代碼編寫時,如果需要在html中引用此文件,也要使用打包後的文件名,即:<script src="../resources_webview.js"></script>。這裏有個小技巧,如果你不知道腳本文件打包後的文件名及路徑,建議先使用Webpack進行編譯,然後查看其在打包後的Plugin中的位置和名稱,然後再進行引用。

├── assets //資源文件夾,如需更改需在package.json中的skpm.assets中設置 
├── my-plugin.sketchplugin   //skpm構建過程生成的插件包
│   └── Contents
│       ├── Resources
│       │   └── _webpack_resources
│       │   └── resources_webview.js
│       │   └── resources_webview.js.map
│       └── Sketch
│           ├── manifest.json
│           ├── __my-command.js
│           └── __my-command.js.map
├── package.json
├── webpack.skpm.config.js
├── resources //資源文件
│  ├── style.css
│  ├── webview.html
│  └── webview.js
└── src //需要被webpack打包的腳本文件以及manifest清單文件
    ├── manifest.json
    └── my-command.js

Manifest

你沒有看錯!plugin中也有manifest.json,它與其它平臺比如Android開發中的清單文件意義相同。清單文件記錄了作者信息、描述、圖標以及獲取更新的途徑等等。想想看,每天熬夜加班寫代碼,總得有個地方把你的名字記錄下來吧。但manifest最重要的作用其實是告訴Sketch如何運行插件,以及如何將插件集成進Sketch的菜單欄中。

commands使用一個數組,記錄了插件所提供的所有命令。比如下面的例子,當用戶從菜單欄點擊 “顯示工具欄”這個條目時,就會執行script.js中的function showPlugin() 。menu則提供了插件在Sketch菜單欄中的佈局信息,Sketch會在插件被加載時初始化菜單。

{
  "commands": [
   {
      "name": "顯示工具欄",
      "identifier": "roo-sketch-plugin.toolbar",
      "script": "./script.js",
      "handlers": {
        "run": "showPlugin"
      }
    }
  ],
  "menu": {
    "title": "????外賣積木SketchPlugin工具欄",
    "items": ["roo-sketch-plugin.toolbar"]
  }
}

package.json

簡單來說,只要你的項目中用到了NPM,根目錄下就會自動生成package.json文件。Node.js項目遵循模塊化的架構,package.json定義了這個項目所需要的各種模塊以及配置信息。使用npm install命令會根據這個配置文件,自動下載所需的模塊,也就是配置項目所需的運行和開發環境。

非常值得稱讚的是,Plugin開發中對於網絡請求、 I/O 操作以及其它功能,可以使用與Node.js兼容的polyfill,其中許多常用modules已經預裝到了Sketch中,比如consolefetchprocessquerystringstreamutil等。

這裏你只需要知道以下幾點:

  • 需要參與Webpack打包的腳本文件必須在resources目錄下聲明,否則不會參與編譯(重點!考試要考!)。

  • assets目錄需要配置在skpm.assets下。

  • 常用的命令可以定義在scripts中方便直接調用。

  • dependencies字段指定了項目運行所依賴的模塊,devDependencies指定項目開發所需要的模塊。

{
  "name": "roo-sketch-plugin",
  "author": "hanyang",
  "description": "外賣積木Sketch plugin,UI同學好喜歡~",
  "version": "0.1.0",
  "skpm": {
    "manifest": "src/manifest.json",
    "main": "roo-sketch-plugin.sketchplugin",
    "assets": ["assets/**/*"]
  },
  "resources": [
    "src/webview/template/webview.js"
  ],
  "scripts": {
    "build": "rm -rf roo-sketch-plugin.sketchplugin && NODE_ENV=development skpm-build",
  },
  "dependencies": {},
  "devDependencies": {}
}

4. API Reference

JavaScript API

由於使用了與Safari相同的JS引擎,Plugin腳本可以獲得完整ES6支持。官方的JavaScript API由Sketch團隊維護,並允許訪問和修改Sketch文檔,通過API可以向Sketch用戶提供數據並提供一些基本的用戶界面集成。

//訪問、修改和創建文檔從color到layer再到symbol等方方面面
var sketchDom = require('sketch/dom')
//對於異步操作,JavaScript API提供了fibers延長contex的lifeTime
var async = require('sketch/async')
//直接在Sketch中提供圖像或文本數據,DataSupplier直接與Sketch用戶界面集成。
var DataSupplier = require('sketch/data-supplier')
//無需重新build的情況下顯示通知以及獲取用戶輸入
var UI = require('sketch/ui')
//保存圖層或文檔的自定義數據,並存儲插件的用戶設置。
var Settings = require('sketch/settings')

CocoaScript Syntax

CocoaScript通過賦予了JavaScript調用Sketch內部API以及macOS Cocoa frameworks的能力,這意味着除了標準的JavaScript庫外,還可以使用許多很棒的類與函數。CocoaScript建立在蘋果的JavaScriptCore之上,而JavaScriptCore是爲Safari提供支持的JavaScript引擎。

因此,當你使用CocoaScript編寫代碼的時候,你就是在寫JavaScript。CocoaScript中的Mocha實現JS到Objective-C的Bridge,雖然Mocha包含在CocoaScript中,但文檔仍保留在原始Github中。因此,你在CocoaScript的Readme中看不到任何語法教程。這裏一個訣竅是,如果你想了解Mocha將原生的Sketch Objects通過bridge,從Objective-C傳遞到JavaScript層的屬性、類或者實例方法的信息,可以將其通過console打印出來:

let mocha = context.document.class().mocha()
console.log(mocha.properties())
//OC
[executeOperation:withObject:error:]
//CocoaScript
executeOperation_withObject_error()

通過CocoaScript 提供的Bridge使用JavaScript調用Objective-C的基本語法如下:

  • Objective-C的方括號語法“[  ]”轉換爲JavaScript中的點“ . ”語法。

  • Objective-C的屬性導出到JavaScript時Getter爲object.name()  而Setter爲object.name = 'Sketch'。

  • Objective-C的selectors被暴露爲JavaScript 的代理方法。

  • “:” 冒號被轉換爲下劃線“ _”, 最後一個下劃線是可選的。

  • 調用帶有一個下劃線的方法需要加倍爲兩個下劃線: sketch_method變爲sketch__method。

  • selector的每個component被連接成不帶有分隔符的單個字符串。

5. Actions

行爲定義

Action指的是由於用戶交互而在應用程序中發生的事件,比如“打開文檔”、“關閉文檔”、“保存”等。Sketch所提供的了Action API可以使插件對應用程序中的事件做出反應,有點類似Android開發中的的BroadCast或者Job Scheduler。官方文檔列舉了數百個可供監聽的Action,但最常用到的只有下面幾個:

監聽回調

我們只需在插件的manifest.json文件中添加一個handler即可。比如下面的例子添加了對於“OpenDocument”的監聽,也就是告訴插件在新文檔被打開時要去執行onOpenDocument這個function。

 {
      "script": "action.js",
      "identifier": "my-action-listener-identifier",
      "handlers": {
        "actions": {
          "OpenDocument": "onOpenDocument"
        }
      }
}

當一個Action被觸發時,會回調JS中的監聽方法,與此同時Sketch可以向目標函數發送Action Context,其中包含動作本身的一些信息。在下面例子中,每次打開文檔時都會彈出一個Toast。

function onOpenDocument(context) {
    context.actionContext.document.showMessage('Document Opened')
}

6. Bridge雙向通信

在常規的插件開發中,UI層一般採用Webview實現,因此你可以使用各種前端開發框架,比如React或者Vue等;而插件的邏輯層(負責調用Skecth API)顯然不在WebView中,因此需要通過Bridge進行通信。邏輯層將從服務器獲取到的數據傳遞給UI層展示,而UI層則將用戶的操作反饋傳遞給邏輯層,使其調用Sketch API更新Layers。

Sketch 通信原理

插件發送消息到WebView

//On the plugin:
browserWindow.webContents
  .executeJavaScript('someGlobalFunctionDefinedInTheWebview("hello")')
  .then(res => {
    // do something with the result
  })

//On the WebView:
window.someGlobalFunctionDefinedInTheWebview = function(arg) {
  console.log(arg)
}

WebView發送消息給插件

//On the webview:
window.postMessage('nativeLog', 'Called from the webview')
//On the plugin:
var sketch = require('sketch')
browserWindow.webContents.on('nativeLog', function(s) {
  sketch.UI.message(s)
})

經過了以上步驟,我們就得到了一個基礎插件,它以WebView作爲內容載體,並具有雙向通信功能。打開插件時,Webview會將頁面加載完成的事件傳遞給邏輯層,邏輯層調用Sketch API彈出Toast;點擊Get a random number可以從邏輯層獲取一個隨機數。

skpm/with-webview 運行效果

快來正式加入開發隊伍

相信閱讀完上面的部分,製作一個簡單的插件對於你來說,已經有點“遊刃有餘”了。但這個時候,疑惑也隨之而來,爲什麼Demo和我們常用插件的UI差別如此之大?

沒錯,官方文檔只教給我們最基礎的插件開發流程,一個成熟的商業項目絕不僅僅是以上這些。一個功能完善的插件應該包括以下三部分:工具欄、WebView容器以及業務數據。下面,我們會一步步爲你展示如何開發一個商業化插件UI,同時也會演示美團外賣“填充功能”的實現(注:篇幅原因文檔中僅保留關鍵代碼。)

常規Sketch插件結構

1. 創建吸附工具欄

所謂吸附式工具欄,就是展示在Skecth右側Inspector Panel旁邊的工具欄,它以吸附的方式與Sketch操作界面融爲一體,這也是絕大多數插件的視覺呈現方式。工具欄中展示了當前插件可以提供的大部分功能,方便我們在操作Document時快速選取使用。

開發工具欄主要使用NSStackView、NSButton、NSImage以及NSFont這幾個類,如果沒有開發過macOS應用的同學可能對這些類有些陌生,可以類比iOS開發中以UI作爲前綴的控件類,NS前綴主要是AppKit以及Foundation的相關類,MS前綴則是Skecth的相關類,CA、CF前綴爲核心動畫庫和核心基礎類。

下面的代碼記錄了創建工具欄的關鍵步驟,更爲詳細的操作可以參考一些Github倉庫,比如sketch-plugin-boilerplate等。

const contentView = context.document.documentWindow().contentView();
const stageView = contentView.subviews().objectAtIndex(0);

//1.創建toolbar
const toolbar = NSStackView.alloc().initWithFrame(NSMakeRect(0, 0, 27, 420));
toolbar.setBackgroundColor(NSColor.windowBackgroundColor());
toolbar.orientation = 1;

//2.創建Button
const button =  NSButton.alloc().initWithFrame(rect)
const Image = NSImage.alloc().initWithContentsOfURL(imageURL)
button.setImage(image)
button.setTitle("數據填充")
button.setFont(NSFont.fontWithName_size('Arial',11))

//3.將Button加入toolbar
toolbar.addView_inGravity(button, gravityType);

//4.將toolbar加入SketchWindow
const views = stageView.subviews()
const finalViews = []
for (let i = 0; i < views.count(); i++) {
 finalViews.push(view)
 if(view[i].identifier() === 'view_canvas'){
   finalViews.push(toolbar)
}
 stageView.subviews = finalViews
 stageView.adjustSubviews()

2. 創建WebView容器

除了通過CocoaScript創建原生NSPanel外,這裏推薦使用官方的sketch-module-web-view快速創建WebView容器,它提供了豐富的API對窗口的展示樣式和行爲進行定製,包括Frameless Window、Drag等,同時還封裝了WebView與插件層的通信的Bridge,使你可以輕鬆在"frontend" (the WebView)和"backend" (the plugin running in Sketch)之間發送消息。

//(1)方法一:原生方式加入webview
const panel = NSPanel.alloc().init();
panel.setFrame_display(NSMakeRect(0, 0, panelWidth, panelHeight), true);
const wkwebviewConfig = WKWebViewConfiguration.alloc().init()
const webView = WKWebView.alloc().initWithFrame_configuration(
  CGRectMake(0, 0, panelWidth, panelWidth),
  wkwebviewConfig
)
panel.contentView().addSubview(webView);
webview.loadFileURL_allowingReadAccessToURL(
  NSURL.URLWithString(url),
  NSURL.URLWithString('file:///')
)
//(2)方法二:使用官方的BrowserWindow
import BrowserWindow from "sketch-module-web-view";
const browserWindow = new BrowserWindow(options);
const webViewContents = browserWindow.webContents;

 webViewContents
    .executeJavaScript(`someGlobalFunctionDefinedInTheWebview(${JSON.stringify(someObject)})`)
    .then(res => {
      // do something with the result
    })
 browserWindow.loadURL(require('./webview.html'))

3. 創建內容頁面

歷盡千辛萬苦,我們終於拿到了WebView,這下就可以發揮你“天馬行空”的想象力了。不管是React還是Vue,亦或只是一些簡單的靜態頁面對於你而言應該都不在話下。在完成界面開發後,只需通過Window向插件發送指令即可。下面的例子演示了積木插件的“數據填充”功能。

UI側

import React from 'react';
import ReactDOM from 'react-dom';

//使用react搭建用戶頁面
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

//傳遞用戶點擊填充類目給插件層,這裏以填充文字爲例
export const PostMessage = (name, fillData) => {
  try {
    window.postMessage("fill-text-layer", fillData);
  } catch (e) {
    console.error(name, "出現異常!!!" + fillData);
  }
}; 

插件側

  browserWindow.webContents.on('fill-text-layer', function(s) {
   //找到當前頁面document
  const document = context.document;
   //獲取用戶選擇的layers
     const selection = Document.fromNative(document).selectedLayers;
        layers.forEach(item => {
          //判斷layer類型是否爲文字
          if (item.type === 'Text') {
            //更新textlayer
            item.text = value;
          }
   }); 
})

4. 還想加點出彩的功能

如果你還不滿足於此,說明你真的是個很愛學習,也很有潛力的開發同學。一個完善的插件需要包括交互層、API層、業務層、調試層以及發佈層,每層各司其職,它們都在默默幹好自己的工作。

前面的步驟,通過構件菜單欄、創建Webiew完成了交互層的開發;通過Webview的Bridge傳遞用戶操作到插件側代碼,之後調用Sketch API對圖層進行操作,這是API層的工作;而根據自身需求並依託交互層與API層的實現去編寫業務代碼,則是業務層的工作;至此,你應該就擁有了一個可運行的插件了。

但除此之外,在代碼編寫過程中還需要Lint組件輔助開發,發現問題需要使用各類Dev工具進行調試,通過QA驗證後,需要Cli工具打包併發布插件更新。這一小節,我們將簡單介紹一些基本的調試層和發佈層知識。

積木Sketch Plugin結構

Webpack配置

Skpm默認採用Webpack作爲打包工具。Webpack是一個現代JavaScript應用程序的靜態模塊打包器(Module Bundler)。當Webpack處理應用程序時,它會遞歸地構建一個依賴關係圖(Dependency Graph),其中包含應用程序需要的每個模塊,然後將所有這些模塊打包成一個或多個Bundle,需要在webpack.config.js進行配置,類似於Android中的Gradle,同樣支持各種插件。

Webpack處理流程示意

由於插件的開發者未必是前端同學,可能之前並沒有接觸過Webpack,因此我們在這裏介紹它的一些常用配置,讓你有更多的時間關注業務代碼。第一次接觸Webpack是在去年一次公司內部的技術培訓上(美團技術學院提供了很多技術培訓課程,加入我們就可以盡情地在知識的海洋中遨遊了),美團MRN項目的打包方案就是Webpack。

在前端圈有各種各樣的打包工具,比如Webpack、Rollup、Gulp、Grunt 等等。RN打包用的是Facebok實現的一套叫做Metro的工具,而美團MRN打包工具的選型是Webpack,因爲Webpack具有強大的插件機制和豐富的社區生態,可以完成複雜的流水線打包工作,Webpack在Plugin開發中同樣發揮了非常重要的作用。Webpack有五個核心概念:

在插件開發中需要處理html、css、sass、jpg、style等各種文件,只有在Webpack中配置相應的Loader後,這些文件才能被處理。而且我們很可能遇到某些文件需要使用特定的插件,而其它文件又無需處理的情況。下面的示例中列舉了添加插件、對文件單獨處理以及參數配置這三個常用的基本操作。

module.exports = function (config, entry) { 
  //常用功能1:增加插件
    config.module.rules.push({
    test: /\.(svg)([\?]?.*)$/,
    use: [
      {
        loader: "file-loader",
        options: {
          outputPath: url => path.join(WEBPACK_DIRECTORY, url),
          publicPath: url => {return url;}}}
    ]
  });}

//常用功能2:對文件單獨處理
if (entry.script === "src/script.js") {
    config.plugins.push(
      new htmlWebpackPlugin({ })
    );
}

//常用功能3:定製js處理
  config.module.rules.push({
    test: /\.jsx?$/,
    use: [
      { loader: "babel-loader",
        options: {
          presets: [
            "@babel/preset-react",
            "@babel/preset-env"
          ],
          plugins: [
            //引入antd組件庫
            ["import",{libraryName: "antd",libraryDirectory: "es",style: "css"}]
      ]}}]
  });

ESLint配置

JavaScript是一門非常靈活的語言,很多錯誤往往運行時才爆出,通過配置前端代碼檢查方案,在編寫代碼過程中可直接得到錯誤反饋,也可以進行代碼風格檢查,不僅提升了開發效率,同時對不良代碼編寫習慣也能起到糾正作用。在ESLint中需要配置基礎語法規則、React 規則、JSX規則等,由於Sketch插件的CocoaScript語法較爲特殊,需要配置全局變量以此忽略AppKit中無法識別的類。

雖然,我們曾在部門組會中被多次“安利”ESLint的強大作用(這裏給大家推薦一篇技術文章:ESLint 在中大型團隊的應用實踐),但如果不是做前端或者RN開發的同學,可能對於ESLint的複雜配置並不熟悉。可以直接使用Skpm提供的ESlint Config,裏面配置了包含Sketch和macOS的頭文件的全局變量,而代碼格式化則推薦使用Prettier。

npm install --save-dev eslint-config-sketch
//或者直接使用帶prettier以eslint的skpm template工程
$ skpm create my-plugin --template=skpm/with-prettier

內容服務端化

Sketch推出的庫(Library)功能對於維護設計系統或風格指南,起到非常重要的作用,可以給團隊帶來高效工作體驗,甚至改變設計團隊工作方式和流程。我們通過組件庫可以在整個設計團隊中共享組件(Symbol),Library可以實現“一處更改,處處生效”,即使是關聯了遠程組件庫歷史的設計稿檢測到更新時,也會收到Sketch通知,確保工作中使用的是最新組件。

庫功能對美團外賣UI一致性起着至關重要的作用,這主要體現在兩方面:首先是實現設計風格沉澱,目前袋鼠UI已經形成了自己的獨特風格,外賣設計團隊根據設計規範,對符合UI一致性外賣業務場景的組件不斷進行抽象及建設,沉澱出越來越多的通用業務組件,這些組件需要及時擴充到Library中,供團隊成員使用;另外一個作用,則是保持團隊使用的均爲最新組件,由於各種原因,組件的設計元素(色彩、字體、圓角等屬性)可能會發生變更,需要及時提醒團隊成員更新組件,保持所有頁面的一致性。

Sketch內置的iOS遠程組件庫

Library中的Symbol提示更新

庫組件自動更新,其實就是 “庫列表” - “庫 ID” - “外部組件原始 ID” 這三者的關聯。Sketch內部是靠UUID進行對象識別的,通過庫組件的庫ID,從庫面板的列表中,按照添加的時間從新到舊依次檢索所有未被禁用的、鏈接完好的庫,直到匹配到庫的ID ,然後查找該庫文件內是否有與庫組件SymbolID匹配的組件,如果包含且內容有差異就提醒更新,更新的過程實際上是內容替換。

我們通過以下步驟使用RSS技術共享Library供整個UI設計團隊使用:

  • 將Library Document 託管到公司內網服務器上。

  • 創建一個XML文件記錄版本信息和更新地址。

  • 最後使用Meyerweb URL編碼器之類的工具(或直接encodeURIComponent)對XML feed URL進行編碼並將其添加到以下內容:sketch://add-library?url=https://***.xml。

  • 將此URI在瀏覽器中打開即可。

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
  <channel>
    <title>My Sketch Library</title>
    <description>My Sketch Library</description>
    <image>
      <url></url>
    </image>
    <item>
      <title>My Sketch Library</title>
      <pubDate>Wed, 23 Jun 2019 11:19:04 +0000</pubDate>
      <enclosure url="mysketchlibrary.sketch" type="application/octet-stream" sparkle:version="1"/>
    </item>
  </channel>
</rss>

5. 開發流程小結

前面一口氣講述了很多內容,可能你一時無法消化。別急,慢慢來。我們這裏對插件的開發流程作個簡要的總結:

  • 首先利用JavaScript 或CocoaScript開發操作面板。

  • 使用NPM安裝所需依賴。

  • 通過Bridge傳遞用戶操作到插件邏輯側,通過調用Skecth API對文檔進行處理。
    使用Webpack進行打包。

  • 通過測試後發佈插件更新。

Sketch Plugin開發流程

別人可能沒告訴你的事兒

這部分主要記錄了積木Sketch Plugin開發過程中的踩坑經歷,但是這裏,我們沒有貼大段的代碼,沒有直接告訴你答案,而是把分析問題的過程記錄下來。“授人以魚不如授人以漁”,相信只要你瞭解了這些分析技巧,即使之後遇到更多的問題,也可以輕(jia)鬆(ban)解決。

1. 與Xcode工程混合編譯

首先,我們要明確一個問題,爲什麼要使用XCode工程?

雖然官方提供了JS API並承諾持續維護,但這項工作一直處於Doing狀態,而且官方文檔更新緩慢,沒有明確的時間節點。因此,對於某些功能,比如我們想建一個具有Native Inspector Panel的插件,就不得不使用XCode進行開發。使用Xcode開發對於iOS開發者也更加友好,無需再學習前端界面開發知識。

這裏推薦Invison的開發成員James Tang分享的博客文章《Sketch Plugin Xcode Template》,裏面詳細描述了構建插件XCode工程的步驟,這也成爲很多插件開發者遵循的範本。當然隨着Sketch的不斷升級,某些API已經不受支持,但作者講述的開發流程和思路依然沒有改變,具有很高的學習價值。

JavaScript
//利用 Mocha加載framework
var mocha = Mocha.sharedRuntime();
[mocha loadFrameworkWithName:frameworkName inDirectory:pluginRootPath]

除此之外,Skpm中已經內置了@skpm/xcodeproj-loader,也可在JS中直接加載Framework。

JavaScript
//加載framework
const framework = require('../xcode-project-name/project-name.xcodeproj/project.pbxproj');
const nativeClass = framework.getClass('NativeClassName');
//獲取nib文件
const ui = framework.getNib('NativeNibFile');
//也可以直接加載xib文件
const NibUI = require('../xcode-project-name/view-name.xib')
var nib = NibUI()
let dialog = NSAlert.alloc().init()
dialog.setAccessoryView(nib.getRoot())
dialog.runModal()

當然你也可以直接使用Github上一些知名的開源項目,有些會直接提供Framework供你使用,比如更改原生的toolbar:

2. 瞭解Electron

爲什麼在講述Sketch Plugin的時候,忽然會提到Electron?這裏有一個小故事,某天上班打開大象(美團內部溝通軟件)。

MacOS版大象截圖

看到一條公衆號推送,是公司成立了Electron技術俱樂部(美團技術團隊內部自發成立了很多技術俱樂部),經過了解發現Electron基於Chromium和Node.js,可以使用HTML、CSS和JavaScript構建桌面應用程序,Electron負責其中比較複雜的部分,而開發者只需關心應用的核心需求即可。大象的Mac端就大量使用了Electron技術,用Web框架去開發桌面應用,可以直接複用Web現有的開發成果並獲得出色的運行效率。

我們就進行了簡單的學習,在之後的一段時間並沒有再去關注這項技術,直到某天在插件開發的過程中忽然遇到一個問題:在插件WebView顯示的情況下,在桌面空白處點擊使Sketch軟件失去焦點,整個App就會被隱藏。試了幾個流行的插件,發現大部分均有此問題,這給設計師的工作造成了諸多不便。試想,我只是去打開Finder找一個文件,你爲什麼要把我的軟件最小化?在Github上留言後,很快得到了項目開發者Mathieu Dutour的官方回覆,原來只需要設置一個hidesOnDeactivate屬性即可。

等等!這不是Electron中的屬性麼?仔細查看Readme才發現作者寫道“The API is mimicking the BrowserWindow API of Electron.”這下可方便多了!你想自定義窗口的表現,只需按照Electron的API設置即可,想想看其實Electron的工作方式是不是和Sketch Plugin如出一轍?

3. 更新原生屬性面板

爲了更好地提升積木Sketch Plugin的使用體驗,UI同學通過建立公共Wiki記錄我們設計團隊在插件使用過程中的反饋建議,其中有一條很奇怪:“通過插件面板更新Layer屬性後,右側面板不刷新。”和上一個問題一樣,經測試其它插件大部分也有此問題,但是如何去更新右側屬性面板呢?翻閱了Sketch的API文檔還是“丈二和尚,摸不着頭腦”。這個時候想起了macOS開發的一個神器Interface Inspector,它可以在運行時分析正在運行的Mac應用程序的界面結構和屬性,非常強大。

開心的下載下來後,發現這個軟件上次的更新時間是6年前,忽然有了一種不祥的預感。果然Attach任何App時都會提示無法Attach,在macOS Catalina版本已經無法運行。可是這怎麼能難倒“萬能”的程序員呢?我們查看系統報錯,發現是mach_inject_bundle_stub錯誤,查閱發現mach_inject_bundle_stub是Github上的一個開源庫,所以自己下載源碼重新編譯個Bundle包就可以了。

Attach成功後,就可以對Sketch的面板進行屬性分析了,是不是忽然感覺打開了新世界的大門?經過查閱發現右側面板在MSInspectorController中。如下圖所示:

Interface Inspector對Sketch進行運行時分析

下一步需要用Class-Dump工具來提取Sketch的頭文件,查看可以對inspector面板進行操作的所有方法:

通過class-dump得到的頭文件

不出所料,我們發現了reload(),猜測調用這個方法可以刷新面板,測試一下發現問題被修復了。如果你使用Sketch的JavaScript API的話,名稱不一定能完全對應,但是基本差不多,稍加分析也可以找到。這裏只是教大家一個思路,這樣即使遇到其它問題,按照上面的步驟試試看,沒準就可以解決。

JavaScript
// reload the inspector to see the changes 
var sketch = require('sketch')
var document = sketch.getSelectedDocument()
document.sketchObject.inspectorController().reload()

歡迎你的加入

如你所見,積木Sketch Plugin可以幫助設計團隊提升設計效率、沉澱設計語言以及減少走查負擔;讓RD同學面對新項目時,可以專注於業務需求而無需把時間耗費在組件的編寫上;減少QA工作量,保證控件質量無需頻繁迴歸測試;幫助PM提高版本迭代效率及版本需求吞吐量,提供業務的快速拓展能力。

當然,我們除了希望製作一流的產品,也希望積木插件可以讓你在繁忙的工作中得以喘息。我們會繼續以設計語言爲依託,以Skecth Plugin爲抓手持續進行UI一致性建設,提高客戶端UI業務中臺能力。

可能對於一個前端工程師來說,對React、Webpack等配置可以信手拈來;對於一個iOS工程師來說,XCode調試、Objective-C語法是開發前的基礎;對於一個桌面工程師來說,對Electron、Hook分析已司空見慣。可Sketch Plugin開發就是這麼有趣,雖然只是一個小小的插件,但它會讓你接觸各個端的技術,提升技術視野,但同樣會讓你在開發過程中遇到很多困難,曾經困擾了我好幾天的一個Webpack問題,部門同事幫我們聯繫了一個開發經驗豐富的前端妹子去諮詢,對方一行代碼竟然就解決了。做你害怕做的事,然後你會發現,不過如此。

目前,積木插件開發還處於較爲初級的階段,包括Mach(外賣自研動態化框架)實時預覽、模板代碼自動生成、自建插畫庫等功能已經在路上。除此之外,我們還規劃了很多激動人心的功能,需要製作更多精美的前端頁面,需要更完善的後臺管理。

這裏加個廣告吧!不管你是FE、Android、iOS、後端,只要你對Bug毫不手軟,精益求精,都歡迎你加入我們外賣技術團隊,跟我們一起完善Sketch插件生態,讓積木插件可以爲更多業務場景提供服務,爲用戶提供卓越的體驗。讓我們一起用“積木”拼出萬千世界!

嗯,就先寫到這裏吧!UI團隊同學說我們的實現和設計稿竟然差了一個像素,我們要回去改Bug了。

致謝

特別感謝優秀的設計師昱翰、沛東、淼林、雪美,他們在插件開發過程中給予的幫助。

特別感謝技術團隊的雲鵬、曉飛在技術上給予的指導。

“前人栽樹,後人乘涼。”我們向優秀開源項目開發者致敬。

參考文獻

| Sketch Plugin開發官方文檔
| 深入理解Sketch庫
| 凹凸實驗室高大師Sketch插件開發實踐
| Sketch Plugin Xcode Template
| Beginning Sketch Plugins Development in Xcode
| 攜程機票Sketch插件開發實踐

----------  END  ----------

招聘信息

美團外賣長期招聘 Android、iOS、FE 高級/資深工程師和技術專家,歡迎加入外賣App大家庭。歡迎感興趣的同學發送簡歷至:[email protected](郵件標題註明:美團外賣技術團隊)

也許你還想看

微前端在美團外賣的實踐

美團外賣前端容器化演進實踐

Android視頻技術探索之旅:美團外賣商家端的實踐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章