Это вторая часть статьи о создании инструмента, способного экспортировать все помещённые в Sketch-файл иконки: в разных форматах, для разных платформ, с возможностью A/B-тестирования каждой из иконок.
Первую часть вы можете прочесть по ссылке.
В прошлый раз мы подготовили Sketch-файлы, содержащие все иконки в нужных стилях и с правильными названиями. Пришёл черёд написания кода.
Достаточно сказать, что мы шли путём проб и ошибок. После того как наш тимлид Нихил Верма, заложивший основы скрипта, разработал ключевой исходный код, я запустил процесс, потребовавший не менее трёх фаз рефакторинга и множества модификаций. По этой причине я не буду вдаваться в подробности создания скрипта и сосредоточусь на том, как он работает сегодня, в финальном виде.
Скрипт сборки
Написанный на 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-файлов
Первый этап в процессе сборки — подготовка Sketch-файлов, которые позже будут использованы для экспорта ресурсов для различных платформ.
Файлы, ассоциированные с конкретным брендом (например, в случае с Blendr это файлы icons_common.sketch и icons_blendr.sketch) клонируются во временную папку (точнее в подпапку, названную по имени обрабатываемого бренда) и разархивируются.
Затем обрабатываются JSON-файлы. К названию ресурсов, подлежащих A/B-тестированию, добавляется префикс — таким образом, при экспорте они будут сохранены в подпапке с заранее указанным названием (соответствующим уникальному имени эксперимента). Понять, подлежит ли ресурс A/B-тестированию, можно по названию страницы, на которой он хранится: если подлежит, в названии будет содержаться префикс "XP_".
В примере выше экспортируемые ресурсы будут сохраняться в подпапке «this__is_an_experiment» с названием файла вида «icon-name[variant-name].ext».
Считывание метаданных Sketch
Второй важный этап — извлечение всех необходимых метаданных из 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 (свойство do_objectID
в Sketch):
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-файлы в папке pages (повторю, что названия файлов — вида [pageUniqueId].json) и изучаем хранящиеся на этой странице ресурсы (они выглядят как слои). Таким образом, мы получаем имя, ширину/высоту каждой иконки, метаданные Sketch по иконке данного слоя, а если мы имеем дело со страницей эксперимента, то ещё и названия A/B-теста и варианта данной иконки.
Примечание: объект page.json имеет очень сложное устройство, поэтому я не буду на нём останавливаться. Если вам интересно, что внутри, советую создать новый пустой Sketch-файл, добавить в него какой-нибудь контент и сохранить; затем переименовать его расширение в ZIP, разархивировать его и изучить один из файлов в папке pages.
В процессе обработки артбордов мы также создадим список экспериментов (и соответствующих ресурсов). Он нам понадобится, чтобы определить, какие варианты иконки использованы в каком эксперименте, — названия вариантов иконки привязаны к «базовому» объекту.
Для каждого обрабатываемого 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-тестированию) в разных файлах ассоциируются с одним и тем же брендом:
Создание готовых файлов в разных форматах для разных платформ
Заключительный этап нашего процесса — непосредственно создание файлов иконок в разных форматах для разных платформ (PDF для iOS, SVG/JSX для Web и VectorDrawable для Android).
Как можно понять из количества передаваемых функциям generateAssets[format][platform]
параметров, эта часть конвейера самая сложная. Именно здесь процесс начинает дробиться и меняться в зависимости от платформы. Ниже вы увидите логический ход скрипта целиком и то, как относящаяся к генерированию ресурсов часть разделяется на три похожих, но отличающихся друг от друга процесса:
Для создания готовых ресурсов с корректными цветами, соответствующими обрабатываемому бренду, нам понадобится провести ещё несколько манипуляций с JSON-файлами. Мы проходим по всем слоям, к которым применён общий стиль, и заменяем цветовые значения цветами из дизайн-токена бренда.
Для генерирования файлов для Android необходимо выполнить дополнительное действие (о нём чуть позже): мы меняем свойство 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-файлов), при экспорте мы сливаем множество файлов в один каталог (относящийся к бренду/платформе). Это означает, что, если ресурс — идентифицируемый по названию слоя — уже существовал в предыдущем Sketch-файле, он будет переписан при следующем экспорте. Опять же, это не более чем обычная операция слияния.
Впрочем, в этом примере некоторые ресурсы могут оказаться «призраками». Такое происходит, когда иконка в файле подвергается A/B-тестированию, но в последующем файле перезаписывается. Тогда файлы вариантов экспортируются в папку назначения, имеют соответствующую ресурсу ссылку в объекте assetsMetadata
(со своими ключом и свойствами), но не ассоциируются ни с одним базовым ресурсом (из-за глубокого слияния объектов assetsMetadata
). Такие файлы будут удалены позже, перед завершением процесса.
Как уже было сказано, для разных платформ требуются разные итоговые форматы. iOS подходят PDF-файлы, и мы можем напрямую экспортировать их с помощью команды SketchTool. Для Mobile Web необходимы JSX-файлы, а для Android — VectorDrawable. По этой причине мы экспортируем ресурсы в формате SVG во временную папку и уже после этого подвергаем обработке.
PDF-файлы для iOS
Как ни странно, PDF — единственный (?) формат, который поддерживается Xcode и OS/iOS для импорта и визуализации векторных ресурсов (вот короткое объяснение данного выбора Apple).
Поскольку мы можем напрямую экспортировать в PDF через SketchTool, никаких дополнительных действий не потребуется: просто сохраните файлы непосредственно в папке назначения, и всё.
Файлы в формате React/JSX для Web
В случае с Web мы используем SVGR— библиотеку Node, позволяющую конвертировать SVG в компоненты React. Однако мы хотим делать кое-что покруче: «динамически раскрашивать» иконку во время исполнения (цвета при этом берутся из токенов). Для этого перед конвертированием мы меняем значения 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>
то итоговый ресурс/иконка 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
динамическим, берущим значения из токенов (их можно сделать доступными для компонента React <Icon/>
через Context API, но это отдельная история).
Эта замена возможна благодаря метаданным Sketch для ресурсов объекта assetsMetadata
: рекурсивно пройдясь по слоям, вы можете создать селектор DOM (для примера выше это #Icons
#badge-feature-like #circle
) и использовать его для поиска узла в древе SVG и замены значения его атрибута fill
(для этого нам нужна библиотека cheerio).
Файлы VectorDrawable для Android
Android поддерживает векторную графику с помощью кастомного векторного формата VectorDrawable. Обычно конвертирование из SVG в VectorDrawable производится прямо в Android Studio. Однако нам хотелось полностью автоматизировать процесс, поэтому мы искали способ конвертирования при помощи кода.
Изучив различные инструменты и библиотеки, мы остановились на svg2vectordrawable. Она не только активно поддерживается (во всяком случае активнее, чем все остальные), но и более функциональна, чем остальные.
Реалии таковы, что VectorDrawable и SVG не одинаковы в своей функциональности: некоторые функции SVG (к примеру, радиальные градиенты и комплексное выделение) не поддерживаются VectorDrawable, а другие начали поддерживаться совсем недавно (начиная с версии Android API 24). Одна из вытекающих из этого проблем — то, что в более старых версиях (до 24) не поддерживается значение even-odd атрибута fill-rule. Однако нам в Badoo необходима поддержка версии Android 5 и выше. Вот почему на одном из более ранних этапов мы привели fill
каждого вектора в Sketch-файлах к значению non-zero
.
В принципе, дизайнеры могут выполнить это действие вручную:
Но об этом легко забыть и допустить ошибку. Поэтому мы решили добавить в процесс для Android дополнительный этап, на котором все векторы в JSON автоматически конвертируются в non-zero
. Это делается для того, чтобы при экспорте иконок в SVG они уже были в необходимом формате, а каждый создаваемый объект VectorDrawable поддерживался устройствами на Android 5.
Готовый файл 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
мы проходимся по нему, проверяя каждую из них:
- обычный ли это ресурс (например,
tabbar-livestream
); если да, то просто оставляем его; - если это вариант для A/B-теста (например, experiment/tabbar-livestream[variant]), мы ассоциируем его название, путь, имена A/B-теста и варианта со свойством
abtests
базового ресурса (в нашем случае это tabbar-livestream), после чего удаляем запись о варианте из списка/объекта (имеет значение лишь «базовый» элемент); - если это «призрак», то удаляем файл и убираем запись из списка/объекта.
После завершения данного процесса словарь будет содержать список всех базовых иконок (и их A/B-тестов, если таковые имеются), и только их. Информация о каждой из них включает в себя название, размер, путь и, если иконка подлежит A/B-тестированию, информацию о различных её вариантах.
Словарь сохраняется в формате JSON в папке назначения для бренда и платформы. Вот, например, файл assets.json, сгенерированный для приложения Blendr под Mobile Web:
{
"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"
}
}
},
...
}
}
Теперь остаётся лишь упаковать все папки assets в ZIP-архивы для более удобного скачивания.
Итог
Описанный в статье процесс — от клонирования и манипуляций со Sketch-файлами до экспорта и конвертирования ресурсов в поддерживаемые платформами форматы и сохранения собранной метаинформации в библиотеке ресурсов — повторяется с каждым объявленным в скрипте сборки брендом.
Вот скриншот, демонстрирующий внешний вид папок src и dist после завершения процесса:
На данном этапе с помощью одной простой команды вы можете загрузить все ресурсы (JSON, ZIP и файлы ресурсов) в удалённое хранилище и сделать их доступными для всех платформ для скачивания и использования в кодовой базе.
То, как именно платформы получают и обрабатывают ресурсы (с помощью кастомных скриптов, созданных специально для этих целей), не выходит за рамки статьи. И этот вопрос наверняка будет освещён в одном из следующих постов кем-то из моих коллег.
Заключение (и усвоенные уроки)
Я всегда любил Sketch. Программа долгие годы была инструментом «по умолчанию» для разработчиков и дизайнеров. Поэтому мне было очень любопытно изучить средства интеграции вроде html-sketchapp и другие подобные инструменты, которые мы могли бы использовать в рабочем процессе и своих конвейерах.
Я, как и многие другие, всегда стремился к такому (идеальному) процессу:
Однако должен признаться, что я начал сомневаться в том, что Sketch — подходящий инструмент, особенно с учётом дизайн-системы. Поэтому я стал присматриваться к другим сервисам, таким как Figma с его открытыми API и Framer X с удобной интеграцией с React, поскольку не чувствовал у Sketch движения к интеграции с кодом (каким бы он ни был).
Так вот, этот проект меня переубедил. Не полностью, но во многом.
Пусть Sketch и не открывает свои API, но само устройство внутренней структуры его файлов служит своего рода «неофициальным» API. Создатели могли бы использовать зашифрованные названия или скрывать ключи в JSON-объектах, но вместо этого они придерживаются понятного, читабельного и концептуального соглашения о наименовании. Не думаю, что это случайность.
Тот факт, что Sketch-файлами можно управлять подобным образом, открыл мне дорогу ко множеству будущих разработок и улучшений: от плагинов для проверки наименования, стилизации и структуры слоёв для иконок до интеграции с нашей Wiki и документацией нашей дизайн-системы (обоюдной). Создав Node-приложения на Electron или Carlo, мы можем облегчить для дизайнеров процесс выполнения множества рутинных действий.
Одним из неожиданных (по крайней мере, для меня) бонусов стало то, что Sketch-файлы с иконками Cosmos стали «источником истины» — нечто похожее произошло и с дизайн-системой Cosmos. Если иконки нет, то её не существует в кодовой базе (вернее не должно существовать; но теперь мы знаем, что такие случаи — исключение). Теперь это кажется очевидным, но так было не всегда — опять же, по крайней мере, для меня.
Когда к нам пришло осознание того, что Sketch-файлами можно манипулировать, то, что начиналось как MVP-проект, обернулось глубоким погружением в их внутреннее устройство. Мы ещё не знаем, куда нас приведёт эта тропа, но пока нам сопутствует удача. Дизайнеры, разработчики, продакт-менеджеры, руководство — все сходятся в том, что нововведение позволит упростить работу каждому из нас и предотвратить возникновение многих ошибок. Кроме того, перед нами открылись двери к новым способам использования иконок.
И последнее: описанный мной конвейер был построен для наших нужд, а потому он адаптирован под наши реалии. Имейте в виду, что он может не отвечать требованиям вашей компании.
Главное, чем я хотел поделиться, — что всё возможно. Может быть, другим способом, с другими подходами и другими файловыми форматами, с меньшей сложностью (например, вам может не понадобиться поддержка нескольких брендов и A/B-тестирования), но теперь вы можете автоматизировать процесс доставки иконок с кастомным скриптом Node.js и Sketch.
Ищите свой способ! Это весело и довольно просто.
Благодарности
Этот огромный проект был разработан совместно с Нихилом Вермой (Mobile Web), создавшим первую версию скрипта сборки, Артёмом Рудым (Android) и Игорем Савельевым (iOS), которые разработали скрипты для импорта и приёма ресурсов для своих платформ.
Спасибо, ребята! Работать с вами над проектом и претворять его в жизнь было настоящим удовольствием.
Автор: alxgutnikov