乐趣区

关于前端:积木Sketch插件进阶开发指南

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

设计零碎的真谛起源越少,效率就越高。——Jon Gold,出名全栈设计师

背景

1. 积木工具链体系

前段时间咱们在美团技术团队公众号上发表了《积木 Sketch Plugin:设计同学的贴心搭档》一文,未曾想到,这个仅在美团外卖 C 端应用的插件受到了更多的关注,美团多个业务团队纷纷向咱们抛出“橄榄枝”,示意想要接入以及并表白了违心共同开发的动向,其它互联网同行也纷纷询问相干的技术,一时让咱们有些“受宠若惊”。回想起写第一篇文章的时候,咱们的心田还是有些不安的。作为 UI 同学的一个设计工具,有些 RD 甚至没有据说过 Sketch 这个名字,咱们很认真地批改过上一篇文章的每一句措辞,争取让内容更丰盛乏味,过后还很放心不会被读者承受。

积木 Sketch 插件的“意外走红”,的确有些出乎咱们的预料,但正是如此,才让咱们晓得 UI 一致性是绝大部分开发团队面临的共性问题,大家对落地设计规范,进步 UI 中台能力,晋升产研效率都有着强烈的诉求。为了帮忙更多团队晋升产研效率,咱们成立了袋鼠 UI 共建项目组,将门户建设、工具链建设以及组件建设对立治理统一规划,并将工具链的品牌确定为“积木”,而积木 Sketch 插件便是其中重要的一环。

咱们通过建设蕴含雷同设计元素的对立物料市场,PM 通过 Axure 插件拾取物料市场中的组件产出原型稿;UI/UE 通过 Sketch 插件落地物料市场中的设计规范,产出符合要求的设计稿;而物料市场中的组件又与 RD 代码仓库中的组件一一对应,从而造成了一个闭环。将来,咱们心愿通过高保真原型输入,能够给中后盾我的项目、非依赖体验我的项目提供更好的服务体验,赋予产品同学间接向技术侧输入原型稿的能力。

2. 积木插件平台化

随同着“积木”品牌的确立,越来越多的团队心愿能够接入积木 Sketch 插件,其中局部团队也在和咱们探讨技术单干的可能性。UI 设计语言与本身业务关联性很强,不同业务的色调零碎、图形、栅格零碎、投影零碎、图文关系千差万别,其中任意一环的缺失都会导致一致性被毁坏,业务方都心愿通过积木插件实现设计规范的落地。为了帮忙更多团队的 UI 同学晋升设计效率,节约 RD 同学页面调整的工夫,同时也让 App 界面具备一致性,从而更好地传播品牌主张和设计理念,咱们决定对积木插件进行平台化革新。平台化是指积木插件能够接入各个业务团队的整套设计规范,通过平台化革新,能够使积木插件提供的设计元素与业务强关联,满足不同业务团队的设计需要。

积木插件本来只是外卖晋升 UI/RD 合作效率的一次尝试,最后的指标仅是 UI 一致性,然而当初曾经作为全面晋升产研效率的媒介,承载了越来越多的性能。围绕设计日常工作,提供高效的设计元素获取形式,让工作变得更轻松,是积木的外围使命。如何推动设计规范落地,并且输入到各个业务零碎灵便应用,是咱们继续追寻的答案。而探寻研发和设计更为高效的合作模式也是咱们始终致力的方向。

通过一段时间的平台化建设,目前美团曾经有 7 个设计团队接入了积木插件,笼罩了美团到家事业部大部分设计同学,将来咱们会继续推动积木插件的平台化建设,不断完善性能,冀望能将积木插件打造成业界一流的品牌。

3. Sketch 插件开发进阶

第一篇文章可能是为数不多的入门教程,而本篇可能是你能找到的惟一一篇进阶开发文章。进阶开发次要波及如何切换业务方数据,即抉择所属业务方后,对应的组件、色彩等设计素材切换为以后业务方在物料市场中上传的元素;将承载组件库的 Library 文件转化为插件能够辨认的格局,并在插件上展现,以供设计师在绘制设计稿时抉择应用;一些优化运行效率,晋升用户体验的办法。

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 是画,而非带有逻辑和层级关系的写,他们对于页面的树形构造很难了解,上手老本较高,而且代码保护老本绝对较大。咱们不去评估哪种计划的好坏,只是第一种计划能够更好地满足咱们的外围诉求。

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 的一部分。

理论中能够依据业务须要采纳三级以上分组命名的形式,通过 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 即可。

操作体验优化

实现了上述步骤后,就能够实现一款反对多业务方的插件了。然而随着积木 Sketch 插件接入的业务方越来越多,除了听到能够显著晋升效率的贬责外,对插件的吐槽声也经常传入咱们的耳边。市面上成熟的插件也有很多,咱们无奈限度他人的抉择,所以只能让积木变得更好用,本节次要介绍插件的优化办法。为了更好地聆听大家意见,积木插件通过各种措施理解用户的实在想法。首先积木插件接入美团外部的 TT(Trouble Tracker)零碎,相比公司很多业余零碎,TT 不带任何业余流程和定制化,只做纯流转,是一套实用于公司外部的、通用的问题发动、响应和追踪零碎,用户反馈的问题主动创立工单并与对应 RD 关联,Bug 能够最疾速修复;插件外部减少反馈渠道,用户反馈及时发送给相干 PM,作为下次性能排期的权重指标;插件外部减少多维度埋点统计,从设计渗透到高频应用两个方面理解 UI 同学的外围诉求。以下介绍了依据反馈整顿的局部高优先级问题的解决方案。

1. 操作界面优化

很多 RD 在开发过程中,对界面丑化往往不屑一顾,“这个性能能用就能够了”经常被挂在嘴边。难道 UI 的需要真的是中看不中用?一个产品设计师说过,最早的产品仅依附性能就能够在竞品中怀才不遇,能不能用就成为了一个产品是否合格的规范。起初在越来越成熟的互联网环境中,易用性成了一个新的且更重要的规范,这时同类产品间的性能曾经十分靠近,无奈通过一直重叠性能产生显著差别。而当同类产品的易用性也趋于相近时,如何解决产品竞争力的问题就再一次摆在面前,这时就要赋予产品情感,好的产品关注性能,优良的产品关注情感,能够让用户在应用中感触到和煦。

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 开发者也更加敌对,无需再学习前端界面开发常识。

Xcode 工程配置

通过 Xcode 工程构建自定义属性面板,最终生成一个能够供 JS 侧调用的 Framework。能够参考上一篇文章介绍的办法创立 Xcode 工程,该工程在每次构建后会主动生成测试 Sketch 插件并放入对应的文件夹中。须要留神的一点是,这里生成的插件只是为了不便开发和调试,前面会介绍如何将 XCode 工程构建的 Framework 集成至 JS 主工程中。

积木插件的主体性能应用 JS 代码实现,然而自定义属性抉择面板应用 Objective- C 代码实现。为了实现积木插件的 JS 侧功能模块与 OC 侧模块之间的通信和桥接,这里借助了 Mocha 框架来实现相干的性能,Mocha 框架也被 Sketch 官网所应用,将原生侧的办法封装为官网 API 后裸露给 JS 侧。

组件选中时,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 后的办法。

组件属性批改与替换原理

通过自定义面板能够批改组件的可笼罩项(即 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);

一起拼积木

目前,积木插件曾经在美团到家事业部遍地开花,咱们心愿将来积木品牌产品能够在更大范畴内失去利用,帮忙更多团队落地设计规范,晋升产研效率,也欢送更多团队接入积木工具链。“不忘初心,方得始终”,就像第一篇启蒙文章中说的那样,咱们除了心愿制作一流的产品,也心愿积木插件能够让大家在忙碌的工作中得以喘息。咱们会持续以设计语言为依靠,以积木工具链为抓手,不断完善优化,拓展插件的应用场景,让设计与开发变得更轻松。

总有人在问,积木插件当初好用吗?我想说,还不够好用。然而每次评审需要时看到旁边的设计师在认真地应用咱们的插件作图,看到积木插件爱好者为咱们制作表情包帮忙咱们推广,咱们深知唯有交付最棒的产品,能力不辜负大家的期待。

平台化二期的需要刚刚确定结束,人力调配排期完结,咱们又想了一大波令你拍手称誉的性能,马上就要踏上新的征程。夜深了,看着窗外人家的灯,一个个燃烧,夜空也变得越来越亮堂。咱们的指标,是星辰大海。

致谢

感激外卖技术部晓飞、彦平、瑶哥、云鹏、冰冰对我的项目的大力支持。
感激到家事业部优良的设计师徐徐、昱翰、淼林、雪美、田园、璟琦。
感激闪购技术团队章琦、CRM 团队的怡婷、CI 王鹏帮助技术开发。

参考文献

  • 百度 Sketch 插件开发总结
  • 爱奇艺产品工作流优化:搭建组件库做高 ROI
  • 阿里重磅开源中后盾 UI 解决方案 Fusion
  • Painting with Code
  • Sketch Developers Discussion

招聘信息

美团外卖长期招聘 Android、iOS、FE 高级 / 资深工程师和技术专家,欢送退出外卖 App 小家庭。感兴趣的同学可投递简历至:tech@meituan.com(邮件主题请注明:美团外卖前端)。

想浏览更多技术文章,请关注美团技术团队(meituantech)官网微信公众号。

退出移动版