积木Sketch插件进阶开发指南

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

设计系统的真理来源越少,效率就越高。——Jon Gold,知名全栈设计师html

背景

1. 积木工具链体系

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

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

积木品牌Logo

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

袋鼠UI工具链体系

2. 积木插件平台化

伴随着“积木”品牌的确立,愈来愈多的团队但愿能够接入积木Sketch插件,其中部分团队也在和咱们探讨技术合做的可能性。UI设计语言与自身业务关联性很强,不一样业务的色彩系统、图形、栅格系统、投影系统、图文关系千差万别,其中任意一环的缺失都会致使一致性被破坏,业务方都但愿经过积木插件实现设计规范的落地。为了帮助更多团队的UI同窗提高设计效率,节约RD同窗页面调整的时间,同时也让App界面具备一致性,从而更好地传达品牌主张和设计理念,咱们决定对积木插件进行平台化改造。平台化是指积木插件能够接入各个业务团队的整套设计规范,经过平台化改造,可使积木插件提供的设计元素与业务强关联,知足不一样业务团队的设计需求。程序员

积木Skecth Plugin平台化示意

积木插件本来只是外卖提高UI/RD协做效率的一次尝试,最初的目标仅是UI一致性,可是如今已经做为全面提高产研效率的媒介,承载了愈来愈多的功能。围绕设计平常工做,提供高效的设计元素获取方式,让工做变得更轻松,是积木的核心使命。如何推进设计规范落地,而且输出到各个业务系统灵活使用,是咱们持续追寻的答案。而探寻研发和设计更为高效的协做模式也是咱们一直努力的方向。github

经过一段时间的平台化建设,目前美团已经有7个设计团队接入了积木插件,覆盖了美团到家事业部大部分设计同窗,将来咱们会持续推动积木插件的平台化建设,不断完善功能,指望能将积木插件打形成业界一流的品牌。web

3. Sketch插件开发进阶

第一篇文章多是为数很少的入门教程,而本篇多是你能找到的惟一一篇进阶开发文章。进阶开发主要涉及如何切换业务方数据,即选择所属业务方后,对应的组件、颜色等设计素材切换为当前业务方在物料市场中上传的元素;将承载组件库的Library文件转化为插件能够识别的格式,并在插件上展现,以供设计师在绘制设计稿时选择使用;一些优化运行效率,提高用户体验的方法。json

Sketch插件代码因为和业务强相关,且实现方式较为复杂,可能存在部分敏感信息,因此基本没有成熟的插件开源。在进行一些复杂功能开发时,咱们也经常“丈二和尚摸不到头脑”,“要不这个功能算了吧”的想法也不止一次出现,但是每当开会看到旁边的设计同窗在使用“积木”插件认真做图时,又一次次坚决了咱们的信念,要不加班再试试吧,没准就能实现了呢?一次“委曲求全”,后面可能致使整个项目慢慢崩塌,因此咱们一直以将积木插件打形成为业界领先的插件为信念。若是说看过了第一篇文章你已经知道了如何开发一款插件,那么经过本篇文章的学习你就能够真正实现一款能够与业务强关联且功能可定制的成熟工具,与其说是介绍如何开发一个进阶版的Sketch插件,不如说是分享给你们完成一个商业化项目的经验。redux

支持多业务切换

为了当面对“咱们能够接入积木插件吗”这种灵魂拷问时再也不手足无措,平台化进程迅速启动。平台化的核心其实就是当发生业务线变动时,物料市场中的素材总体同步切换,所以咱们须要进行以下几个操做:首先创建全局变量,存储当前用户所述业务方信息及鉴权信息;用户选择功能模块后,根据用户所述业务方,拉取对应素材;处理Library等素材并渲染页面展现;根据素材信息变动画板中的相关Layer。这部分主要介绍如何依靠持久化存储来实现业务切换的功能,就像在第一篇启蒙文档中说的那样,这里不会贴大段的代码,只会帮你梳理最核心的流程,相信你亲自实践一次以后,之后的困难均可以轻松解决。segmentfault

1. 定义通用变量

功能模块展现的素材与当前选择的业务相关,所以须要在每一个功能模块的Redux初始化状态中增长一些全局状态变量。好比全部模块都须要使用businessType来肯定当前选择的业务,使用theme进行主题切换,使用commonRequestParams获取用户鉴权信息等。

export const ACTION_TYPE = 'roo/colorData/index';
const intialState = {
  id: 'color',
  title: '颜色库',
  theme: 'black',
  businessType: 'waimai-c',
  commonRequestParams: {
    userInfo: '',
  },
};
export default reducerCreator(intialState, ACTION_TYPE);

2. 实现数据交换

第一步:WebView侧获取用户选择,将所选的业务方数据经过window.postMessage方法传递至插件侧。

window.postMessage('onBusinessSelected', item);

第二步:Plugin侧经过webViewContents.on( )方法接收从WebView侧传递过来的数据。Sketch官方经过Settings API提供了一些类的方法来处理用户的参数设置,这些设置在Sketch关闭后依然会保存,除了存储一段JSON数据外,Layer、Document甚至是Session variable都是支持的。

webViewContents.on('onBusinessSelected', item => {
    Settings.setSettingForKey('onBusinessSelected', JSON.stringify(item));
  });
​
// 除此以外,插件侧也能够经过localStorage向WebView注入数据
browserWindow.webContents.executeJavaScript(
      `localStorage.setItem("${key}",'${data}')`
);

第三步:当用户经过工具栏选择某一功能模块(例如“插画库”)时,会回调NSButton的点击事件监听,此时除了须要要让WebView展现(Show)以及获取焦点(Focus)外,还须要将第二步存储的业务方信息传入,并以此加载当前业务方的物料数据。

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

WebView侧功能模块收到初始化信息,开始进行页面渲染前的数据准备。Object.keys()方法会返回一个由给定对象的自身可枚举属性组成的数组,遍历这个数组便可拿到全部被注入的初始化数据,以后经过redux的store.dispatch方法更新state便可。至此实现业务切换功能的流程就所有结束了,是否是以为很是简单,忍不住想亲自动手试一下呢?

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

3. 小结

有小伙伴会问,为何WebView与Plugin侧须要数据传递呢,它们不都属于插件的一部分么?根本缘由是咱们的界面是经过WebView展现的,可是对Layer的各类操做是经过Sketch的API实现的,WebView只是一个网页,自己与Sketch并没有关系,所以必须使用bridge在二者之间进行数据传递。别担忧,这里再带你把整个流程梳理一遍:①在插件启动后会从服务端拉取业务方列表;②用户在WebView中选择本身所属的业务方;③将业务方数据经过bridge传递至Plugin侧,并经过Sketch的Settings API进行持久化存储,这样就能够保证每次启动Sketch的时候无需再次选择所属业务方;④用户点击插件工具栏的按钮选择所需功能(例如色板库、组件库等),从持久化数据中读取当前所属业务方,并通知WebView侧拉取当前业务方数据。至此,整个流程结束。

Library库文件自动化处理

这部分将介绍如何将Library库文件转化为插件能够识别的JSON格式,并在插件上展现。

若是要问Sketch插件最重要的功能是什么,组件库绝对是无可争议的C位。在长期的版本迭代中,随着功能的不断增长以及UI的持续改版,新旧样式混杂,维护极为困难。设计师经过将页面走查结果概括梳理,制定设计规范,从而选取复用性高的组件进行组件库搭建。经过搭建组件库能够进行规范控制,避免控件的随意组合,减小页面差别;组件库中组件知足业务特点,同时具备云端动态调整能力,能够在规范更新时进行统一调整。

目前,咱们将组件集成进Sketch供UI使用大体分为两个流派:一个是基于Sketch官方的Library库文件,设计师经过将业务中复用性高的Symbol组件概括整理生成库文件(后缀.sketch),并上传至云端,插件拉取库文件转化为JSON并在操做面板展现供选取使用;另外一个则是采用相似Airbnb开源的React-Sketchapp这样的框架,它可让你使用React代码来制做和管理视觉稿及相关设计资源,官方把它称做“用代码来绘画”,这种方案的实施难度较大,由于本质上设计是感性和理性的结合,设计师使用Sketch是画,而非带有逻辑和层级关系的写,他们对于页面的树形结构很难理解,上手成本较高,并且代码维护成本相对较大。咱们不去评价哪一种方案的好坏,只是第一种方案能够更好地知足咱们的核心诉求。

Sketch组件库处理效果示意

1. 订阅远程组件库

Library库文件其实是一个包含components的文档,components包括了Symbols、Text Styles以及Layer Styles三类,将Library存储在云端就能够在不一样文档甚至不一样团队间共享这些components。因为组件库实时指向最新,所以当其维护者更新库中的components时,使用了这些components的文档将会收到通知,这能够保证设计稿永远指向最新的设计规范。

订阅云端组件库的方式很简单,首先建立一个云端组件库,具体能够参照上一篇文章,若是须要服务多个设计部门,则须要建立多个库,每一个库有惟一的RSS地址;在插件中获取到这些RSS地址后,能够经过Library.getRemoteLibraryWithRSS方法对其进行订阅。

// 启动插件时添加远程组件库
export const addRemoteLibrary = context => {
  fetch(LibraryListURL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
  })
    .then(res => res.json())
    .then(response => response.data)
    .then(json => {
      const { remoteLibraryList } = json;
      _.forEach(remoteLibraryList, fileName => {
        Library.getRemoteLibraryWithRSS(fileName, (err, library) => {
        });
      });
      return list;
    });
};

2. Library库文件转换JSON数据

将Sketch的Library文件转换为JSON的过程,实际上就是转换为WebView能够识别格式的过程。由于积木插件是将组件按照必定分组展现在面板中供设计师选取,所以须要根据组件分类组织其结构。Sketch原生支持采用 "/" 符号对其进行分组:Group-name/Symbol-name,好比命名为Button/Normal和Button/Pressed的两个Symbols会成为Button Group的一部分。

Symbol分组结构

实际中能够根据业务须要采用三级以上分组命名的方式,经过split方法将Symbol名称经过 "/" 符号拆分为数组,第一级名称、第二级名称等各级名称做为JSON结构的不一样层级便可,具体操做能够参照以下示例代码:

const document = library.getDocument();
    const symbols = [];
    _.forEach(document.pages, page => {
      _.forEach(page.layers, l => {
        if (l.type && l.type === 'SymbolMaster') {
          symbols.push(l);
        }
      });
    });
​
 // 对symbol进行分组处理,并生成json数据
 for (let i = 0; i < symbols.length; i++) {
      const name = symbols[i].name;
      const subNames = name.split('/');
      // 去掉全部空格
      const groupName = subNames[0].replace(/\s/g, '');
      const typeName = subNames[1].replace(/\s/g, '');
      const symbolName = subNames.join('/').replace(/\s/g, '');
      result[groupName] = result[groupName] || {};
      result[groupName][typeName] = result[groupName][typeName] || [];
      result[groupName][typeName].push({
        symbolID:symbolID,
        name: symbolName,
      });
 }

通过以上操做后,一个简化版的JSON文件以下方所示:

{
    "美团外卖C端组件库": {
        "icon": [{
            "symbolID": "E35D2CE8-4276-45A1-972D-E14A06B0CD23",
            "name": "28/问号"
        },{
            "symbolID": "E57D2CE8-4276-45A1-962D-E14A06B0CD61",
            "name": "27/花朵"
        }]
    }
}

3. Symbol缩略图处理

WebView默认是不支持直接显示Symbol供用户拖拽使用的,解决该问题的方案有两种:(1)经过dump分析Sketch的头文件发现,能够采用MSSymbolPreviewGenerator的imageForSymbolAncestry方法导出缩略图,该方法支持图片大小、颜色空间等多种属性设置,优点是较为灵活,能够根据须要进行任意配置,不过要承担后期API变动的风险;(2)直接采用sketchDOM提供的export方法,将Symbol组件导出为缩略图,以后在WebView中显示缩略图,当拖拽缩略图至画板时,再将其替换为Library中对应的Symbol便可。

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

4. 小结

以上就是实现平台化的一个基本流程了,不知道此时你有没有听得“云里雾里”。在这里,再把核心点带你们复习一下。本节主要讲了两件事情:第一,插件如何才能支持多个业务方,即在插件的业务方列表中选择相关业务方,就能够切换对应的设计资源;第二,如何处理Library文件,将其转换为JSON供WebView展现使用。具体流程以下:

  1. 不一样设计组的UI同窗制做完成包含各类components的Library后,经过后台上传至云端。
  2. RD同窗根据当前使用者所属的设计团队拉取对应的包括Library在内的设计素材,颜色、图片,iconFont等设计素材能够直接展现,但是Library文件不支持在WebView中直接显示,须要进行处理。
  3. 根据和UI同窗约定组件的命名规则,经过使用“/”分割,将第一级名称、第二级名称等各级名称做为JSON结构的不一样层级,再经过sketchDOM提供的export方法将Symbol转换为png格式的缩略图便可在插件中显示。
  4. 将选中的缩略图拖拽至Sketch的画板时,再将缩略图替换为Library中的真实Symbol便可。

Library库文件处理小结

操做体验优化

完成了上述步骤后,就能够完成一款支持多业务方的插件了。可是随着积木Sketch插件接入的业务方愈来愈多,除了听到能够显著提高效率的褒奖外,对插件的吐槽声也经常传入咱们的耳边。市面上成熟的插件也有不少,咱们没法限制别人的选择,因此只能让积木变得更好用,本节主要介绍插件的优化方法。为了更好地倾听你们意见,积木插件经过各类措施了解用户的真实想法。首先积木插件接入美团内部的TT(Trouble Tracker)系统,相比公司不少专业系统,TT不带任何专业流程和定制化,只作纯流转,是一套适用于公司内部的、通用的问题发起、响应和追踪系统,用户反馈的问题自动建立工单并与对应RD关联,Bug能够最快速修复;插件内部增长反馈渠道,用户反馈及时发送给相关PM,做为下次功能排期的权重指标;插件内部增长多维度埋点统计,从设计渗透到高频使用两个方面了解UI同窗的核心诉求。如下介绍了根据反馈整理的部分高优先级问题的解决方案。

1. 操做界面优化

不少RD在开发过程当中,对界面美化每每嗤之以鼻,“这个功能能用就能够了”经常被挂在嘴边。难道UI的需求真的是中看不中用?一个产品设计师说过,最先的产品仅依靠功能就能够在竞品中脱颖而出,能不能用就成为了一个产品是否合格的标准。后来在愈来愈成熟的互联网环境中,易用性成了一个新的且更重要的标准,这时同类产品间的功能已经很是接近,没法经过不断堆叠功能产生明显差别。而当同类产品的易用性也趋于相近时,如何解决产品竞争力的问题就再一次摆在面前,这时就要赋予产品情感,好的产品关注功能,优秀的产品关注情感,可让用户在使用中感觉到温暖。

WebView优化

当咱们通过了仔细的功能验证,兴致勃勃的把初版积木插件给用户体验时,原本满心欢喜准备迎接夸奖,没想到却获得了不少交互体验很差的反馈,“仅仅实现功能就及格了”这个理论在一像素都不愿放过的设计师眼中确定行不通。原生WebView给用户的体验每每不够优秀,其实只要一些很简单的设置就能够解决,可是这并不表明它不重要。

WebView视图优化点举例

禁止触摸板拖拽形成页面总体“橡皮筋”效果,禁止用户选择(user-select),溢出隐藏等操做,使WebView具备“类原生”效果,都会提高用户的实际使用体验。

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

除此以外在正式环境中(NODE_ENV为production),咱们并不但愿当前界面响应右键菜单,须要经过给document添加EventListener监听将相关事件处理方法屏蔽。

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

工具栏优化

Sketch对于设计师的意义,就像代码编辑器对于程序员同样,工做中几乎无时无刻也离不开。在积木Sketch插件走出美团外卖,被愈来愈多的设计团队采用后,为了让它更加赏心悦目,UI同窗决定对工具条进行一次全新的视觉升级。原生界面开发指的是经过macOS的AppKit进行用户界面开发,在插件开发中一些须要嵌入Sketch面板的UI模块就须要进行原生界面开发,好比吸附式工具条就属于经过macOS原生API开发的界面。

原生开发既可使用Objective-C语言,也可使用CocoaScript经过写JavaScript的方式进行开发。CocoaScript 经过Mocha实现JS到Objective-C的映射,可让咱们经过JS调用Sketch内部API以及macOS的Framework。在经过CocoaScript原生开发前须要了解一些基础知识:

  1. 在使用相关框架前须要经过framework()方法进行引入,而Foundation以及CoreGraphics是默认内置的,无需再单独操做。
  2. 一些Objective-C的selectors选择器须要指针参数,因为JavaScript不支持经过引用传递对象,所以CocoaScript提供了MOPointer做为变量引用的代理对象。

UI调整通常分为三个部分:布局调整、动效调整、图片替换。下面的章节会进行逐一介绍。

新版积木工具栏效果图

布局调整

这里UI的需求是NSButton的宽度填充满整个NSStackView,高度自定义。因为此功能看起来过于简单,当时认为估时0.5天绰绰有余,但是没想到搭进去了1个工做日加上2天周末的时间,由于不管如何设置NSStackView中子View尺寸都没法生效。

在顶住了周围人“UI问题不影响功能使用,之后有时间再优化吧”的“舆论压力”后,终于在官方文档里面发现了线索:“NSStackView A stack view employs Auto Layout (the system’s constraint-based layout feature) to arrange and align an array of views according to your specification. To use a stack view effectively, you need to understand the basics of Auto Layout constraints as described in Auto Layout Guide.”简而言之,NSStackView使用constraints的方式进行自动布局(能够类比Android中的ConstraintLayout),在进行尺寸修改时,是须要添加锚点的,所以须要经过Anchor的方式进行尺寸修改。

// 建立工具条
const toolbar = NSStackView.alloc().initWithFrame(NSMakeRect(0, 0, 45, 400));
toolbar.setSpacing(7);
// 建立NSButton
const button = NSButton.alloc().initWithFrame(rect)
// 设置NSButton宽高
button
    .widthAnchor()
    .constraintEqualToConstant(rect.size.width)
    .setActive(1);
button
    .heightAnchor()
    .constraintEqualToConstant(rect.size.height)
    .setActive(1);
button.setBordered(false);
// 设置回调点击事件
button.setCOSJSTargetFunction(onClickListener);
button.setAction('onClickListener:');
// 添加NSButton至NSStackView中
toolbar.addView_inGravity(button, inGravityType);

动效调整

NSButton内置的点击效果大约15种,能够经过NSBezelStyle进行设置。积木插件工具栏并无采用点击后icon反色的通用处理方式,而是点击后将背景色置为浅灰。若是想要自定义一些点击效果,只需在NSButton点击事件的回调中设置便可。

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

图片加载

Sketch插件既支持加载本地图片,也支持加载网络图片。加载本地图片时,能够经过context.plugin的方法获取一个MSPluginBundle对象,即当前插件bundle文件,它的url()方法会返回当前插件的路径信息,进而帮助咱们找到存储在插件中的本地文件;而加载网络图片则更加简单,经过NSURL.URLWithString( )能够得到一个使用图片网址初始化获得的NSURL对象,这里要格外注意的是,对于网络图片请使用https域名。

//本地图片加载
const localImageUrl = 
       context.plugin.url()
      .URLByAppendingPathComponent('Contents')
      .URLByAppendingPathComponent('Resources')
      .URLByAppendingPathComponent(`${imageurl}.png`);
​
//网络图片加载
const remoteImageUrl = NSURL.URLWithString(imageUrl);
​
//根据ImageUrl获取NSImage对象
const nsImage = NSImage.alloc().initWithContentsOfURL(imageURL);
nsImage.setSize(size);
nsImage.setScalesWhenResized(true);

2. 执行效率优化

只有在设计稿中尽量多地使用组件进行设计,而且将已有页面中的内容经过设计师的走查梳理逐渐替换成组件,才能真正经过建设组件库来进行提效。随着设计团队逐步将设计语言沉淀为设计规范,并将其量化内置于积木插件中,组件的数量愈来愈多,积木插件组件库做为UI同窗使用最频繁的功能,须要格外关注其运行效率。

前置组件库加载

将组件库的加载逻辑前置,在打开文档时对远程组件库进行订阅操做。Sketch所提供的了Action API可使插件对应用程序中的事件作出反应,监听回调只需在插件的manifest.json文件中添加一个handler便可,添加了对于“OpenDocument”的监听,也就是告诉插件在新文档被打开时要去执行addRemoteLibrary这个function。

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

增长缓存逻辑

组件库的处理须要将Library文件转换为带有层级信息的JSON文件,而且须要将Symbol导出为缩略图显示。因为这个步骤较为耗时,所以能够将通过处理的Library信息缓存起来,并经过持久化存储记录已缓存的Library版本。若已缓存的版本与最新版本一致,且缩略图与JSON文件均完整,则能够直接使用缓存信息,极大的提升Library的加载速度。如下非完整代码,仅做示例:

verifyLibraryCache(businessType, libraryVersion) {
    const temp = Settings.settingForKey('libraryJsonCache');
    const libraryJsonCache = temp ? JSON.parse(temp) : null;
​
    // 1.验证缓存版本信息
    if (libraryJsonCache.version.businessType !== libraryVersion) {
      return null;
    }
​
    // 2.验证缩略图完整性
    const home = getAssertURL(this.mContext, 'libraryImage');
    const path = join(home, businessType);
    if (!fs.existsSync(path) || !fs.readdirSync(path)) {
      return null;
    }
​
    // 3.验证业务库Json文件完整性
    if (libraryJsonCache[businessType]) {
      console.info(`当前${businessType}命中缓存`);
      return libraryJsonCache;
    } else {
      return null;
    }
  }
}

3. 自定义Inspector属性面板

与Objective-C工程混合开发

随着各个设计组的组件库建设不断完善,抽离的组件数量不断增多,很多UI同窗反馈Sketch原生组件样式修改面板操做不够便捷,没法约束选择范围,但愿能够提供一种更有效的组件overrides修改方式,而且当修改“图片”、“图标”、“文字”等图层时,能够和积木插件的这些功能模块进行联动选择。实现自定义Inspector面板功能既可使操做更便捷,又能够对修改项进行约束。

自定义属性面板功能的基本思想,是将组件从组件库拖至Sketch画板中时,组件的可修改属性能够显示在Sketch自己的属性面板上。咱们引入了Objective-C原生开发以实现对Sketch界面的修改,为何要使用原生开发?虽然官方提供了JS API并承诺持续维护,但这项工做一直处于Doing状态,并且官方文档更新缓慢,没有明确的时间节点,所以对于自定义Native Inspector Panel这种须要Hook API的功能,使用原生开发较为便捷,并且对于iOS开发者也更加友好,无需再学习前端界面开发知识。

Sketch Inspector面板操做区优化

Xcode工程配置

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

Xcode工程配置示意

积木插件的主体功能使用JS代码实现,可是自定义属性选择面板使用Objective-C代码实现。为了实现积木插件的JS侧功能模块与OC侧模块之间的通讯和桥接,这里借助了Mocha框架来实现相关的功能,Mocha框架也被Sketch官方所使用,将原生侧的方法封装为官方API后暴露给JS侧。

Sketch与插件Framework通讯原理

组件选中时,Sketch软件会回调onSelectionChanged方法给JS侧,JS侧借助Mocha框架能够实现对OC侧的调用,同时将参数以OC对象的方式传递。JS侧传递给OC侧的Context内容很丰富,包含了选中的组件、相关图层还有Sketch软件自己的信息。虽然Sketch没有提供API,可是Objective-C语言自己具有KVO监听对象属性的能力,咱们经过读取对应的属性值,就能够获取须要的对象数据。

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

Sketch官方没有将属性面板的修改能力暴露给插件侧,经过查询Sketch头文件发现经过reloadWithViewControllers:方法能够实现属性面板刷新,可是在实际开发过程当中发如今某些版本的Sketch上会出现面板闪动的问题,这里借助Objective-C的Method Swizzle特性,直接修改reloadWithViewControllers:的运行时行为解决。

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


Swizzle方法会修改原始方法的行为,实际操做中只有在知足特定条件的状况下才应触发Swizzle后的方法。

Swizzle方法触发条件

组件属性修改与替换原理

经过自定义面板能够修改组件的可覆盖项(即override),目前能够应用可覆盖项的affectedLayer有Text/Image/Symbol Instance三种。设计师与开发者在此前对图层的格式进行了约定,保证咱们能够按照统一的方式读取并替换图层的属性值。

替换文本

基于class-dump,咱们能够找出Sketch中声明的全部类的属性和方法,文本处理的策略是,找到图层中的全部MSAvailableOverride对象,这些对象即表示可用的覆盖项,对文本信息的修改其实是经过修改MSAvailableOverride对象的overridePoint来实现的。

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

更改样式

样式设置的策略,是找到当前选中组件对应的Library中相关样式的组件。因为全部的组件都遵循统一的命名格式,所以只要根据组件命名就能筛选出符合要求的组件。

// 命名方式:一级分类/二级分类/组件名称,基于图层获取对应library
id library = [self getLibraryBySymbol:layer];
// 读取组件名称
NSString *layerName = [symbol valueForKeyPath:@"name"];
// 配置符合当前业务的Predicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH [cd] %@", prefix];
// 筛选符合Predicate的全部组件
NSArray *filterResult = [allSybmols filteredArrayUsingPredicate:predicate];

当使用者选中某一个样式后,插件会将设计稿上的组件替换为选中的组件,这里须要使用MSSymbolInstance中的changeInstanceToSymbol方法来实现。须要注意的是,changeInstanceToSymbol仅仅替换了图层中的组件,可是并无修改图层上组件的属性,对于位置和大小等信息须要单独进行处理。

// 在更新图层上的组件以前,咱们须要把组件导入到当前document对象中
id foreignSymbol = [libraryController importShareableObjectReference:sharedObject intoDocument:documentData];
​
//更新图层上的组件 
[symbolInstance changeInstanceToSymbol:localSymbol];

调试技巧

OC侧开发的最大问题,在于没有官方API的支持。所以调试器就显得很是重要,单步调试可让咱们很是方便地深刻到Sketch内部了解Document内部的数据结构。调试环境须要配置,但足够简单,而且对于开发效率的提高是指数级的。

1.对构建Scheme的配置。

2.Attach到Sketch软件上,这样就能够实现断点调试。

与当前JS工程混合编译

1.经过skpm中内置的@skpm/xcodeproj-loader编译XCode工程,并将产物framework拷贝至插件文件夹。

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

2.经过Mocha提供的loadFrameworkWithName_inDirectory方法,设置Framework的名称及路径便可进行加载。

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

3.调用framework中的方法。

// 找到已经被加载的framework
 const frameworkClass = NSClassFromString('RooSketchPluginNativeUI');
// 调用暴露的方法
frameworkClass.onSelectionChanged(context);

一块儿拼积木

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

总有人在问,积木插件如今好用吗?我想说,还不够好用。可是每次评审需求时看到旁边的设计师在认真地使用咱们的插件做图,看到积木插件爱好者为咱们制做表情包帮助咱们推广,咱们深知惟有交付最棒的产品,才能不辜负你们的期待。

平台化二期的需求刚刚肯定完毕,人力分配排期结束,咱们又想了一大波令你拍手称赞的功能,立刻就要踏上新的征程。夜深了,看着窗外人家的灯,一个个熄灭,夜空也变得愈来愈明亮。咱们的目标,是星辰大海。

使用积木插件插画库制做的表情包 Design by 雪美

致谢

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

参考文献

招聘信息

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

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

相关文章
相关标签/搜索