
这是有关创建工具的文章的第二部分,该工具可以导出放置在草图文件中的所有图标:以不同的格式,在不同的平台上使用,并且可以对每个图标进行A / B测试。
您可以
从链接中阅读第一部分。

上次,我们准备了Sketch文件,其中包含所有样式正确且名称正确的图标。 轮到写代码了。
只需说我们经历了反复试验就足够了。 在奠定脚本基础的团队负责人
Nihil Verma开发了关键源代码之后,我开始了一个过程,该过程至少需要三个阶段的重构和许多修改。 因此,我将不讨论脚本的最终创建形式,而是着眼于今天的工作方式。
构建脚本
用Node.js编写的构建脚本在工作中非常简单:通过导入依赖项,声明要处理的Sketch文件列表(这是品牌列表,每个品牌都有与之相关的文件列表),并确保在客户端上安装了Sketch ,脚本依次处理品牌,并对每个品牌执行一系列操作。
- 获取适合品牌的设计令牌(我们需要颜色值)。
- 克隆与品牌相关的Sketch文件,将其解压缩,提取内部JSON文件,并处理其一些内部值(稍后会对此进行更多介绍)。
- 从这些JSON文件( document.json , meta.json和pages / pageUniqueID.json )中读取必要的元数据。 我们对文件中包含的常用样式和资源/图标的列表感兴趣。
- 使用JSON文件再进行几次操作后,它会重新创建档案,并使用Sketch文件(克隆和更新)导出并创建用于三个平台(iOS,Android,Mobile Web)的最终输出文件。
可以在这里找到构建脚本的相关部分:
// ... modules imports here const SKETCH_FILES = { badoo: ['icons_common'], blendr: ['icons_common', 'icons_blendr'], fiesta: ['icons_common', 'icons_fiesta'], hotornot: ['icons_common', 'icons_hotornot'], }; const SKETCH_FOLDER_PATH = path.resolve(__dirname, '../src/'); const SKETCH_TEMP_PATH = path.resolve(SKETCH_FOLDER_PATH, 'tmp'); const DESTINATION_PATH = path.resolve(__dirname, '../dist'); console.log('Build started...'); if (sketchtool.check()) { console.log(`Processing Sketch file via ${sketchtool.version()}`); build(); } else { console.info('You need Sketch installed to run this script'); process.exit(1); } // ---------------------------------------- function build() { // be sure to start with a blank slate del.sync([SKETCH_TEMP_PATH, DESTINATION_PATH]); // process all the brands declared in the list of Sketch files Object.keys(SKETCH_FILES).forEach(async (brand) => { // get the design tokens for the brand const brandTokens = getDesignTokens(brand); // prepare the Sketch files (unzipped) and get a list of them const sketchUnzipFolders = await prepareSketchFiles({ brand, sketchFileNames: SKETCH_FILES[brand], sketchFolder: SKETCH_FOLDER_PATH, sketchTempFolder: SKETCH_TEMP_PATH }); // get the Sketch metadata const sketchMetadata = getSketchMetadata(sketchUnzipFolders); const sketchDataSharedStyles = sketchMetadata.sharedStyles; const sketchDataAssets = sketchMetadata.assetsMetadata; generateAssetsPDF({ platform: 'ios', brand, brandTokens, sketchDataSharedStyles, sketchDataAssets }); generateAssetsSVGDynamicMobileWeb({ platform: 'mw', brand, brandTokens, sketchDataSharedStyles, sketchDataAssets }); generateAssetsVectorDrawableDynamicAndroid({ platform: 'android', brand, brandTokens, sketchDataSharedStyles, sketchDataAssets }); }); }
实际上,管道代码要复杂得多。 这种复杂性的原因在于
prepareSketchFiles
,
getSketchMetadata
和
generateAssets[format][platform]
函数。 下面,我将尝试更详细地描述它们。
准备草图文件
组装过程的第一步是准备Sketch文件,此文件随后将用于导出各种平台的资源。
与特定品牌关联的文件(例如,对于Blendr而言,它们是
icons_common.sketch和
icons_blendr.sketch文件 )被克隆到一个临时文件夹(更确切地说,是在正在处理的品牌命名的子文件夹中)并解压缩。
然后处理JSON文件。 将前缀添加到要进行A / B测试的资源的名称-因此,在导出过程中,它们将以预定义的名称(对应于实验的唯一名称)保存在子文件夹中。 您可以通过存储资源的页面名称来了解该资源是否要接受A / B测试:如果是,该名称将包含前缀“
XP_ ”。

在上面的示例中,导出的资源将存储在“
this__is_an_experiment ”子文件夹中,文件名的形式为“
icon-name [variant-name] .ext ”。
读取草图元数据
第二个重要步骤是从Sketch文件或内部JSON文件中提取所有必要的元数据。 正如我们在上面看到的,这是两个主要文件(
document.json和
meta.json )和页面文件(
pages / pageUniqueId.json )。
document.json文件用于获取出现在
layerStyles对象的属性下的常见样式的列表:
{ "_class": "document", "do_objectID": "45D2DA82-B3F4-49D1-A886-9530678D71DC", "colorSpace": 1, ... "layerStyles": { "_class": "sharedStyleContainer", "objects": [ { "_class": "sharedStyle", "do_objectID": "9BC39AAD-CDE6-4698-8EA5-689C3C942DB4", "name": "features/feature-like", "value": { "_class": "style", "fills": [ { "_class": "fill", "isEnabled": true, "color": { "_class": "color", "alpha": 1, "blue": 0.10588235408067703, "green": 0.4000000059604645, "red": 1 }, "fillType": 0, "noiseIndex": 0, "noiseIntensity": 0, "patternFillType": 1, "patternTileScale": 1 } ], "blur": {...}, "startMarkerType": 0, "endMarkerType": 0, "miterLimit": 10, "windingRule": 1 } }, ...
我们将有关每种样式的基本信息存储在键值格式对象中。 稍后将在我们需要基于唯一ID(Sketch中的
do_objectID
属性)提取样式名称时使用:
const parsedSharedStyles = {}; parsedDocument.layerStyles.objects.forEach((object) => { parsedSharedStyles[object.do_objectID] = { name: object.name, isFill: _.get(object, 'value.fills[0].color') !== undefined, isBorder: _.get(object, 'value.borders[0].color') !== undefined, }; });
现在,我们转到
meta.json文件并获取页面列表。 我们对它们的
unique-id
和
name
感兴趣:
{ "commit": "623a23f2c4848acdbb1a38c2689e571eb73eb823", "pagesAndArtboards": { "EE6BE8D9-9FAD-4976-B0D8-AB33D2B5DBB7": { "name": "Icons", "artboards": { "3275987C-CE1B-4369-B789-06366EDA4C98": { "name": "badge-feature-like" }, "C6992142-8439-45E7-A346-FC35FA01440F": { "name": "badge-feature-crush" }, ... "7F58A1C4-D624-40E3-A8C6-6AF15FD0C32D": { "name": "tabbar-livestream" } ... } }, "ACF82F4E-4B92-4BE1-A31C-DDEB2E54D761": { "name": "XP_this__is_an_experiment", "artboards": { "31A812E8-D960-499F-A10F-C2006DDAEB65": { "name": "this__is_an_experiment/tabbar-livestream[variant1]" }, "20F03053-ED77-486B-9770-32E6BA73A0B8": { "name": "this__is_an_experiment/tabbar-livestream[variant2]" }, "801E65A4-3CC6-411B-B097-B1DBD33EC6CC": { "name": "this__is_an_experiment/tabbar-livestream[control]" } } },
然后,我们读取与
页面文件夹中每个页面相对应的JSON文件(我重复说,文件名的格式为
[pageUniqueId] .json ),并
检查存储在该页面上的资源(它们看起来像图层)。 因此,我们得到每个图标的
名称 ,
宽度/高度,该层图标的Sketch
元数据 。如果要处理
实验页面,则需要获得
A / B测试的名称以及该图标的
变体 。
注意 :page.json对象具有非常复杂的设备,因此我不再赘述。 如果您对其中的内容感兴趣,建议您创建一个新的空Sketch文件,向其中添加一些内容并保存; 然后将其扩展名重命名为ZIP,解压缩并检查pages文件夹中的文件之一。
在处理画板的过程中,我们还将创建
实验列表 (和相关资源)。 我们将需要它来确定在哪个实验中使用了哪个图标变体-图标变体的名称附加到“基础”对象上。
对于每个要处理的品牌
assetsMetadata
Sketch文件,我们创建一个
assetsMetadata
对象:
{ "navigation-bar-edit": { "do_objectID": "86321895-37CE-4B3B-9AA6-6838BEDB0977", ...sketch_artboard_properties, "name": "navigation-bar-edit", "assetname": "navigation-bar-edit", "source": "icons_common", "width": 48, "height": 48 "layers": [ { "do_objectID": "A15FA03C-DEA6-4732-9F85-CA0412A57DF4", "name": "Path", ...sketch_layer_properties, "sharedStyleID": "6A3C0FEE-C8A3-4629-AC48-4FC6005796F5", "style": { ... "fills": [ { "_class": "fill", "isEnabled": true, "color": { "_class": "color", "alpha": 1, "blue": 0.8784313725490196, "green": 0.8784313725490196, "red": 0.8784313725490196 }, } ], "miterLimit": 10, "startMarkerType": 0, "windingRule": 1 }, }, ], ... }, "experiment-name/navigation-bar-edit[variant]": { "do_objectID": "00C0A829-D8ED-4E62-8346-E7EFBC04A7C7", ...sketch_artboard_properties, "name": "experiment-name/navigation-bar-edit[variant]", "assetname": "navigation-bar-edit", "source": "icons_common", "width": 48, "height": 48 ...
如您所见,在实验中,一个图标(在本例中为
Navigation-bar-edit )可以对应许多资源。 同时,相同的图标可能会在与该品牌相关联的另一个Sketch文件中以相同的名称出现。
这非常有用 :我们使用此技巧来编译一组通用的图标,然后根据品牌标识特定的选项。 这就是为什么我们将与特定品牌关联的Sketch文件声明为数组的原因:
const SKETCH_FILES = { badoo: ['icons_common'], blendr: ['icons_common', 'icons_blendr'], fiesta: ['icons_common', 'icons_fiesta'], hotornot: ['icons_common', 'icons_hotornot'], };
在这种情况下,顺序至关重要。 实际上,在脚本调用的
getSketchMetadata
函数中,我们一次不会作为列表文件返回一个
assetsMetadata
对象。 相反,我们执行对象的深层合并-我们将它们合并并返回单个
assetsMetadata
对象。
通常,这无非是将Sketch文件及其资源“逻辑”合并到单个文件中。 但是,逻辑并不像看起来那样简单。 这是我们创建的图表,试图弄清楚当不同文件中具有相同名称(并可能经过A / B测试)的图标与同一品牌相关联时会发生什么:

为不同平台创建不同格式的现成文件
我们流程的最后阶段是直接为不同平台创建不同格式的图标文件(适用于iOS的PDF,适用于Web的SVG / JSX和适用于Android的VectorDrawable)。
从传递给
generateAssets[format][platform]
函数的参数数量中可以看到,管道的这一部分最复杂。 这是该过程开始分解并根据平台而变化的地方。 在下面,您将看到脚本的整体逻辑过程,以及与资源生成相关的部分如何分为三个相似但不同的过程:

要创建具有与所处理品牌相对应的正确颜色的现成资源,我们将需要对JSON文件进行更多操作。 我们遍历应用通用样式的所有层,然后用品牌设计标记中的颜色替换颜色值。
要为Android生成文件,您需要执行其他操作(稍后
windingRule
):我们将每一层的
fill-rule
属性从
even-odd
更改
non-zero
(这由JSON对象的
windingRule
属性控制,其中1表示“奇/偶” ,而0为“不等于零”)。
完成这些操作后,我们将JSON文件重新打包到标准Sketch文件中,以处理和导出具有更新属性的资源(克隆和更新的文件是普通的Sketch文件,可以打开,查看,编辑,保存等)。 )
之后,我们使用SketchTool(包裹
在Node下 )以适合平台的格式自动导出所有资源。 对于与品牌相关的每个文件(或更确切地说,它们的克隆和更新版本),我们运行以下命令:
sketchtool.run(`export slices ${cloneSketchFile} --formats=svg --scales=1 --output=${destinationFolder} --overwriting`);
您可能会猜到,此命令以特定格式将资源导出到目标文件夹,可以选择使用缩放(我们暂时保留原始缩放)。 这里的关键是
-overwriting
选项:就像我们对
assetsMetadata
对象进行深层合并(对应于“逻辑” Sketch文件)一样,在导出时,我们
assetsMetadata
许多文件合并到一个目录中(与品牌/平台有关)。 这意味着如果资源(由图层名称标识)在先前的Sketch文件中已经存在,则它将在下一次导出期间被覆盖。 同样,这仅是正常的合并操作。
但是,在此示例中,某些资源可能变成了“鬼”。 当文件中的图标经过A / B测试,但在后续文件中被覆盖时,会发生这种情况。 然后,将变体文件导出到目标文件夹,并具有一个与
assetsMetadata
对象中的资源相对应的链接(及其键和属性),但不与任何基础资源关联(由于
assetsMetadata
对象的深度合并)。 在完成此过程之前,稍后将删除此类文件。
如前所述,不同的平台需要不同的输出格式。 iOS文件适合PDF,我们可以使用SketchTool命令直接将其导出。 JSX文件对于Mobile Web是必需的,而VectorDrawable对于Android是必需的。 因此,我们将SVG格式的资源导出到一个临时文件夹中,然后我们对其进行处理。
适用于iOS的PDF
奇怪的是,PDF是Xcode和OS / iOS支持导入和呈现矢量资源的唯一(?)格式(以下
是对 Apple选择
的简短说明 )。
由于我们可以通过SketchTool直接导出为PDF,因此无需其他步骤:只需将文件直接保存到目标文件夹即可。
React / JSX Web文件
对于Web,我们使用SVGR库Node,它允许您将SVG转换为React组件。 但是,我们想要突然做一些事情:在运行时对图标“动态着色”(颜色取自标记)。 为此,在转换之前,我们将矢量的
fill
值更改为以前将通用样式应用于矢量的
fill
值,以将其替换为与该样式对应的标记中的值。
因此,如果从Sketch导出的
badge-feature-like.svg文件看起来像这样:
<?xml version="1.0" encoding="UTF-8"?> <svg width="128px" height="128px" viewBox="0 0 128 128" version="1.1" xmlns="<a href="http://www.w3.org/2000/svg">http://www.w3.org/2000/svg</a>" xmlns:xlink="<a href="http://www.w3.org/1999/xlink">http://www.w3.org/1999/xlink</a>"> <!-- Generator: sketchtool 52.2 (67145) -<a href="http://www.bohemiancoding.com/sketch"> http://www.bohemiancoding.com/sketch</a> --> <title>badge-feature-like</title> <desc>Created with sketchtool.</desc> <g id="Icons" fill="none" fill-rule="evenodd"> <g id="badge-feature-like"> <circle id="circle" fill="#E71032" cx="64" cy="64" r="64"> <path id="Shape" fill="#FFFFFF" d="M80.4061668,..."></path> </g> </g> </svg>
那么最终的resource /
badge-feature-like.js图标将如下所示:
/* This file is generated automatically - DO NOT EDIT */ /* eslint-disable max-lines,max-len,camelcase */ const React = require('react'); module.exports = function badge_feature_like({ tokens }) { return ( <svg data-origin="pipeline" viewBox="0 0 128 128"> <g fill="none" fillRule="evenodd"> <circle fill={tokens.TOKEN_COLOR_FEATURE_LIKED_YOU} cx={64} cy={64} r={64} /> <path fill="#FFF" d="M80.4061668,..." /> </g> </svg> ); };
如您所见,我们用从标记中获取值的动态
fill
色替换了静态
fill
色(可以通过Context API将其用于React
<Icon/>
组件,但这是另一回事了)。
assetsMetadata
资产元数据对象的资产的Sketch元数据,可以进行替换:递归地遍历图层,您可以创建一个DOM选择器(例如上
#Icons
#badge-feature-like #circle
#Icons
#badge-feature-like #circle
),并使用它来搜索SVG树中的节点并替换其值
fill
属性(为此,我们需要
cheerio库)。
Android的VectorDrawable文件
Android支持使用自定义
VectorDrawable矢量格式的矢量图形。 通常从SVG到VectorDrawable的转换
直接在Android Studio中完成 。 但是,我们希望完全自动化该过程,因此我们正在寻找一种使用代码进行转换的方法。
研究了各种工具和库之后,我们决定使用
svg2vectordrawable 。 它不仅得到了积极的支持(无论如何,比其他任何人都更加活跃),而且比其他功能更强大。
现实情况是VectorDrawable和SVG在功能上并不相同:VectorDrawable不支持某些SVG函数(例如,径向渐变和复杂的高亮显示),而最近(从Android API 24开始)就开始支持其他功能。 由此引起的问题之一是较早的版本(最多24个)
不支持fill-rule属性的奇偶值 。 但是,Badoo我们需要支持Android 5及更高版本。 这就是为什么在较早的阶段之一中,我们将Sketch文件
fill
每个矢量的
fill
都设为
non-zero
值的原因。
原则上,设计人员可以手动执行此操作:

但这很容易忘记并犯错。 因此,我们决定在Android流程中增加一个步骤,在该步骤中,JSON中的所有向量都将自动转换为
non-zero
。 这样做是为了将图标导出到SVG时,它们已经是必需的格式,并且Android 5上的设备支持每个创建的VectorDrawable对象。
完成的
badge-feature-like.xml文件如下所示:
<!-- This file is generated automatically - DO NOT EDIT --> <vector xmlns:android="<a href="http://schemas.android.com/apk/res/android">http://schemas.android.com/apk/res/android</a>" android:width="128dp" android:height="128dp" android:viewportWidth="128" android:viewportHeight="128"> <path android:fillColor="?color_feature_liked_you" android:pathData="M64 1a63 63 0 1 0 0 126A63 63 0 1 0 64 1z" /> <path android:fillColor="#FFFFFF" android:pathData="M80.406 ..." /> </vector>
在VectorDrawable文件中,我们插入
fill
颜色的变量名,这些变量名通过Android应用程序中的通用样式与设计标记相关联。

值得注意的是,Android Studio对组织资源有严格的要求:名称中没有子文件夹和大写字母! 因此,我们不得不为图标名称提出一种新格式:对于要测试的资源,它们看起来像这样:
ic_icon-name__experiment-name__variant-name
。
JSON字典作为资源库
将资源文件保存为最终格式后,仅保留收集在汇编过程中获得的所有元信息并将其保存到“字典”中,以供各种平台的代码库导入和使用资源时使用。
从
assetsMetadata
对象中提取图标的平面列表之后
assetsMetadata
我们遍历它,并检查每个图标:
- 这是常规资源
tabbar-livestream
(例如tabbar-livestream
)? 如果是这样,那就离开吧;
- 如果这是A / B测试的选项(例如, Experiment / tabbar-livestream [variant] ),我们将其名称,路径,A / B测试的名称和变量与
abtests
属性相关联
基本资源(在我们的示例中是tabbar-livestream ),此后,我们从列表/对象中删除有关变体的记录(仅“ base”元素很重要);
- 如果是“ ghost”,则删除文件,然后从列表/对象中删除条目。
完成此过程后,词典将包含所有基本图标(以及它们的A / B测试,如果有)的列表,并且仅包含它们。 有关每个选项的信息包括名称,大小,路径,如果该图标经过A / B测试,则还提供有关其各种选项的信息。
该词典以JSON格式保存在
品牌和
平台的目标文件夹中。 例如,这是为Mobile Web的Blendr应用程序生成的
asset.json文件:
{ "platform": "mw", "brand": "blendr", "assets": { "badge-feature-like": { "assetname": "badge-feature-like", "path": "assets/badge-feature-like.jsx", "width": 64, "height": 64, "source": "icons_common" }, "navigation-bar-edit": { "assetname": "navigation-bar-edit", "path": "assets/navigation-bar-edit.jsx", "width": 48, "height": 48, "source": "icons_common" }, "tabbar-livestream": { "assetname": "tabbar-livestream", "path": "assets/tabbar-livestream.jsx", "width": 128, "height": 128, "source": "icons_blendr", "abtest": { "this__is_an_experiment": { "control": "assets/this__is_an_experiment/tabbar-livestream__control.jsx", "variant1": "assets/this__is_an_experiment/tabbar-livestream__variant1.jsx", "variant2": "assets/this__is_an_experiment/tabbar-livestream__variant2.jsx" }, "a_second-experiment": { "control": "assets/a_second-experiment/tabbar-livestream__control.jsx", "variantA": "assets/a_second-experiment/tabbar-livestream__variantA.jsx" } } }, ... } }
现在剩下的就是将所有
资产文件夹打包到ZIP存档中,以便于下载。
总结
本文中描述的过程,从克隆和处理Sketch文件到将资源导出和转换为平台支持的格式,以及将收集的元信息保存在资源库中,对于构建脚本中宣布的每个品牌都重复进行。
下面的屏幕快照显示了完成该过程后
src和
dist文件夹的外观:

在此阶段,您可以使用一个简单的命令将所有资源(JSON,ZIP和资源文件)上载到远程存储,并使它们可用于所有平台,以供下载和在代码库中使用。
平台如何准确地接收和处理资源(使用专门为此目的创建的自定义脚本)并不超出本文的范围。 我的一位同事可能在以下其中一篇文章中讨论了这个问题。
结论(和经验教训)
我一直很喜欢Sketch。 多年来,该程序一直是开发人员和设计人员的默认工具。 因此,我很好奇学习诸如
html-sketchapp之类的集成工具以及我们可以在工作流和管道中使用的其他类似工具。
我
和其他许多人一样 ,
一直在为这样(理想)的过程而
努力 :

但是,我必须承认,我开始怀疑Sketch是否适合用作工具,尤其是考虑到设计系统时。 因此,我开始研究其他服务,例如带有开放API的Figma和具有与React方便集成的Framer X的服务,因为我不觉得Sketch倾向于与代码集成(无论它是什么)。
因此,这个项目说服了我。 不完全,但是在很多方面。
尽管Sketch未打开其API,但其文件内部结构的设备本身却是一种“非官方” API。 创建者可以使用加密的名称,也可以在JSON对象中隐藏密钥,但是他们遵循清晰,可读和概念性的命名约定。 我不认为这是偶然的。
Sketch文件可以通过这种方式进行管理,这一事实为我打开了许多未来的发展和改进的道路:从插件检查图标的名称,样式和层结构到与Wiki集成以及与我们的设计系统文档(相互)。 通过在
Electron或
Carlo上创建Node应用程序,我们可以使设计人员更轻松地完成许多例行任务。
( , ) , Sketch- Cosmos « » — - Cosmos. , ( ; , — ). , — , , .
, Sketch- , , MVP-, . , , . , , -, — , . , .
:
,
. ,
.
, , — . , , , (, A/B-), Node.js Sketch.
! .
(Mobile Web), ,
(Android)
(iOS), .
, ! .