积木Sketch Plugin:设计同学的贴心搭档

2020 年 10 月 16 日

积木Sketch Plugin:设计同学的贴心搭档

| A consistent experience is a better experience.——Mark Eberman | 一致的体验是更好的体验。——Mark Eberman 《摘自设计师的 16 句名言》

背景

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 可以快速设计出标准页面

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="./23 积木 Sketch Plugin:设计同学的贴心搭档 _files/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 中,比如 console fetch process querystring stream util 等。

这里你只需要知道以下几点:

  • 需要参与 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:设计同学的贴心搭档

2020 年 10 月 16 日 14:00 6

评论

发布
暂无评论
发现更多内容

《程序员的数学》笔记

Rex

读书笔记

系统服务化构建-两方OAuth

图南日晟

微服务 软件工程 身份认证 架构设计

游戏夜读 | vim,vim,vim

game1night

原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (十)在项目中准备测试环境

编程道与术

Java 编程 软件测试 TDD 单元测试

Vol.6 几个数据库相关的词

Lanpeng2020

数据库 大数据 新手指南

tput命令介绍

唯爱

深入浅出Mysql索引的那些事儿

猿人谷

MySQL 性能优化 索引

代码简洁之路 [持续更新]

吴昊泉

JavaScript 前端 编程习惯

计算机的时间

伴鱼技术团队

分布式 服务器 技术交流

AB 测试平台的设计与实现

伴鱼技术团队

架构 系统设计 后端 A/B

完美兼容老项目!Dataway 4.1.6 返回结构的全面控制

哈库纳

spring Spring Boot Dataway Hasor

Dataway 配置数据接口时和前端进行参数对接

哈库纳

Spring Boot DataQL Dataway Hasor

使用SpreadJS 开发在线问卷系统,构筑CCP(云数据采集)平台

Geek_Willie

数据挖掘 大数据 SpreadJS CCP

免费领课的活动你错过了么?

池建强

极客时间

无需代码!通过 Dataway 配置一个带有分页查询的接口

哈库纳

spring springboot Dataway Hasor

Dataway 整合 Swagger2,让 API 管理更顺畅

哈库纳

Spring Boot DataQL Dataway Hasor

服务化架构-状态码设计要点

图南日晟

微服务 RESTful 架构设计

从 0 到 1 搭建技术中台之技术文化篇

伴鱼技术团队

企业文化 技术管理

Wi-Fi p2p & ap 共存

贾献华

wifi p2p ap

Dataway 4.1.5 以上版本升级指南

哈库纳

string StringBoot Dataway Hasor

小谈校招offer选择

dongh11

职场 职业规划 应届毕业 心态 招聘

自己常用的一些快捷键 windows10

halapano

Windows技巧

说到做到

Yukun

拖延症

Vol.5 Go初探,新手必看!

Lanpeng2020

编程语言 新手指南

ARTS-WEEK01

lee

ARTS 打卡计划

艺术生,我劝你Mac

zhoo299

Mac CG 艺术

Vol.4 了解一下渗透测试

Lanpeng2020

黑客 网络安全

【快点查查】微信小程序使用流程

tomatocc

绝了!Dataway让Spring Boot不再需要Controller、Service、DAO、Mapper

哈库纳

StringBoot DataQL

那些会阻碍程序员成长的细节[1]

码闻强

程序员 程序员人生 职业规划

服务化构建-多维度的认识中台

图南日晟

软件工程 分层架构 架构设计

订阅 每周精要 查看样刊

你将获得:资深编辑编译的全球 IT 要闻,InfoQ 出品的课程和线下活动报名通道,一线技术专家撰写的实操技术案例

微信扫码关注,即刻订阅

积木Sketch Plugin:设计同学的贴心搭档-InfoQ