Ние в Compass сме големи поддръжници на използването на NPM и semver (семантична версия), когато разпространяваме нашите споделени компоненти като пакети. NPM ни предоставя стандартна за индустрията платформа за публикуване на нашите вътрешни зависимости. Инструментите и технологиите, които някой научава, докато работи върху пакет в Compass, са същите, които ще използват в проекти в общността с отворен код. Междувременно самото семантично създаване на версии играе огромна роля за осигуряване на спокойствие. Потребителите на споделени компоненти знаят кога актуализациите са достатъчно безопасни за надграждане, а авторите на компоненти могат да правят големи актуализации, без да се страхуват тихомълком да нарушат договорите, които са сключили с потребителите си.

Ние сме достатъчно доволни от тази система, че започнахме да я използваме за нещо повече от нашите JavaScript библиотеки. Cx, реализацията на CSS на нашата система за проектиране, сама по себе си е NPM пакет. Източникът на CSS се поддържа като собствен пакет с обхват и се публикува, като се използват същите правила на semver, които се прилагат за библиотеките на JavaScript. Екипът на Design System, натоварен с поддържането на библиотеката, беше особено развълнуван от тази настройка, тъй като можеше да направи големи промени в дизайна на библиотеката и да ги маркира като прекъсващи промени, позволявайки на екипите за приложения на Compass, които използваха библиотеката, свободата да надграждат в свободното си време просто чрез актуализиране на зависимостите на тяхното приложение:

{
  "name": "compass-search-app",
  "dependencies": {
    "@uc/cx": "1.2.3",
    …
  }
}

Адът на зависимостта

След няколко месеца поддържане на библиотеката започнахме да чуваме за някои болкови екипи, които изпитват, когато Cx е семантично версиран и инсталиран като зависимост.

От страна на екипа на Design System те бяха разочаровани, че техните актуализации за коригиране на грешки се разпространяват бавно в приложенията в производство. Всеки екип посочи Cx като зависимост с точен номер на версия, което беше страхотно за стабилност и последователни внедрявания, но означаваше, че трябва изрично да се включат за всякакви нови промени в системата за проектиране. Освен това множество приложения в множество екипи може да сочат към различна конкретна семантична версия на библиотеката, което води до леки разлики между приложенията и множество зареждания на библиотеката.

От страна на собствениците на приложения, тъй като Cx беше посочен като зависимост, от авторите на приложения се изискваше да използват процес на изграждане, само за да използват библиотеката. Самият CSS не поддържа автоматично зареждане на стилове от package.json, така че дори изключително простите случаи на употреба налагаха използването на webpack или postcss като процес на изграждане, за да се интегрира библиотеката.

Трябваше да има по-добър начин.

Внедряване на Cx

За да разрешим болката, която изпитваха нашите екипи, решихме, че най-добрият начин на действие е да хостваме Cx на нашия базиран на AWS CDN, подкрепен от S3 и Cloudfront.

Хостирането на библиотеката от разстояние, вместо да се инсталира като зависимост във всяко приложение, води до няколко предимства. На първо място, интеграцията става много по-лесна. Вместо да се налага да инсталират библиотеката в приложение и да я интегрират в процес на изграждане, собствениците на приложения могат просто да използват HTML <link> таг и да препратят към библиотеката като всеки друг CSS файл. Като допълнителен бонус, след като библиотеката бъде изтеглена от браузъра, тя може да бъде кеширана, което означава по-малко CSS за изтегляне в множество приложения. Тъй като всички наши приложения са стандартизирани на нашата основна CSS библиотека, това може да доведе до спестяване на много допълнителни заявки.

Внедряването на това в нашата пакетна екосистема, поддържана от NPM, в крайна сметка се оказа относително лесно. NPM дефинира „набор от скриптове“ за package.json файлове, които се изпълняват автоматично в различни точки от жизнения цикъл на пакета. След преглед на събитията от жизнения цикъл, куката за жизнения цикъл postpublish изглеждаше като най-очевидното време за внедряване на нашата библиотека. Създадохме инструмент за команден ред за внедряване на статични активи в нашия CDN и на postpublish активите са качени:

{
 “name”: “@uc/cx”,
 “scripts”: {
 “prepublish”: “postcss src/index.css --output dist/cx.min.css”,
 “postpublish”: “uc-deploy-assets dist/cx.min.css”
 }
}

Инструментът за команден ред uc-deploy-assets управлява качването на активи в нашия S3 контейнер, поддържан от CloudFront, и автоматично поставя тези активи в папка въз основа на името на пакета. След това този актив е достъпен от публичен URL адрес със стабилна, дефинирана URL схема, която може да бъде свързана от HTML:

<link rel=”stylesheet” type=”text/css” href=”/cx/1.2.3/cx.min.css”>

Адът на зависимостта, този път в облака

Тук веднага се натъкнахме на проблем.

Част от структурата на папките и URL схемата е групиране на внедряванията по номер на публикувана версия. Това е страхотно за запазване на всички предимства на semver, но тогава собствениците на приложения ще трябва да актуализират маркера <link> в своя HTML всеки път, когато корекция на грешка или нова функция се пуснат от екипа на Design System. От друга страна, ние също не искахме да елиминираме всички предимства на семантичните версии, на които се радвахме, когато преминавахме към това базирано на внедряване използване на Cx.

Качването на Cx на едно и също място и URL адрес всеки път, когато се актуализира, ще опрости интеграцията за приложенията, като им позволи да се свържат към един URL адрес, но това ни връща в онзи свят преди semver. Това, от което се нуждаехме, беше лекотата на използване на свързването към хоствани CSS файлове с допълнителната стабилност и безопасност на библиотека, поддържана от версия.

Въведете семантичното маршрутизиране

След известно мислене и проучване осъзнахме, че самите NPM вече са решили този проблем. Когато едно приложение декларира множество зависимости с общи зависимости между тях, би било разточително да имате 3 версии на една и съща зависимост в приложението. „Синтаксисът на диапазона на semver“ е създаден, за да реши точно този проблем. Приложенията и пакетите могат да декларират диапазон от версии, с които решат, че са съвместими, и докато всичките 3 зависимости имат припокриващи се диапазони, можете да разрешите това до една съвместима версия.

Бихме могли да използваме подобна концепция за нашите хоствани CSS файлове (или всякакви статични активи, наистина), като използваме URL пренасочвания. Вместо приложение, свързващо се с точна версия на библиотеката, те биха могли да изберат да се свържат с URL адрес за пренасочване въз основа на правила за диапазон на семантични версии:

  • /cx/1.2/cx.min.css
  • /cx/1/cx.min.css
  • /cx/latest/cx.min.css

Ако приемем, че версия 1.2.3 е най-новата версия на Cx, тогава всичките 3 URL адреса ще се преобразуват в абсолютно същия актив: версия 1.2.3. Въпреки това, като се имат предвид още няколко итерационни цикъла на Cx, където е публикувана корекция (1.2.4), добавена е нова функция (1.3.0) и след това е направена критична промяна (2.0.0), тогава 3-те URL адреса ще се разрешат така:

  • /cx/1.2/cx.min.css -> /cx/1.2.3/cx.min.css
  • /cx/1/cx.min.css -> /cx/1.3.0/cx.min.css
  • /cx/latest/cx.min.css -> /cx/2.0.0/cx.min.css

Това позволява на приложенията сами да решават какъв компромис са готови да направят между автоматичното получаване на актуализации и стабилността на собственото си приложение.

Внедряване на Semver Routing

Първоначално разгледахме прилагането на тези правила чрез простия синтаксис на S3 за „пренасочване към уебсайт“. Вече пишехме наш собствен S3 инструмент за команден ред за качване и така добавянето на правила за пренасочване към качването беше само въпрос на използване на техния API:

const Redirect = { 
  Protocol: protocol,
  HostName: hostname, 
  ReplaceKeyPrefixWith: `${name}/${version}/`
};
s3Client.putBucketWebsite({
  Bucket: bucket,
  WebsiteConfiguration: {
    RoutingRules: {
      Condition: {KeyPrefixEquals: `${name}/latest/`},
      Redirect,
    }, {
      Condition: {KeyPrefixEquals: `${name}/${semver.major(version)}/`},
      Redirect,
   }, {
      Condition: {KeyPrefixEquals: `${name}/${semver.major(version)}.${semver.minor(version)}/`},
      Redirect,
    }
  },
});

Дори успяхме да използваме собствения пакет semver на NPM, за да вземем номерата на основната и второстепенната версия!

За съжаление, тази реализация се оказа твърде наивна за мащаба, от който се нуждаехме. В рамките на няколко седмици след въвеждането на тази система в повече вътрешни пакети, които трябва да разположат статични активи, се натъкнахме на не особено добре документиран лимит от страна на S3 с пренасочвания: те са ограничени до 50 общо пренасочва на кофа S3. Това очевидно няма да проработи, имахме нужда от нов план.

Маршрутизиране @ Edge

Свързахме се с поддръжката на AWS за потенциално повишаване на лимита. Те ни казаха, че лимитът е за цялата система и не може да бъде променен, но за да разгледаме евентуално решаването на нашия проблем с помощта на нов продукт, който пускат, наречен „Lambda@Edge“.

Lambda@Edge ви позволява да изпълнявате код на крайните възли, използвани от Cloudfront. Можем да използваме нашата логика, за да разберем как да пренасочваме към конкретни версии на активи, без да губим предимствата на напълно кеширана и бързо зареждаща се CDN.

Lambda semver-пренасочване

Ламбда функцията се свежда до следния поток, който разделихме на отделни, индивидуално тествани (!!!) функции:

1. Получете събитието Lambda@Edge в нашия манипулатор на събития. Събитието има много информация и контекст за искания актив, но най-вече се интересуваме от няколко полета в полето request.

{
  "Records": [{
    "cf": {
      "request": {
        "method": "GET",
        "origin": {
          "custom": {
            "domainName": "bucket-name.s3.amazonaws.com",
            "port": 443,
            "protocol": "https"
          }
        },
        "uri": "/deploy-assets/latest/compass-tech.png"
      }
    }
  }]
}

2. От полето за заявка можем да анализираме 3 важни части от информацията: името на кофата S3, пътя до актива и обхвата на semver.

const {bucket, path, range} = parseRequest(request);

3. С името на кофата и пътя на актива в ръка, можем да направим заявка до кофата, за да изброим всички възможни версии, които са внедрени. Можем да направим това, защото стандартизирахме качването на файлове в папка, наречена на версията на semver. Получаването на списъка с обекти в дадена директория на S3 ще ни даде тези версии. Всички имена на папки, които не са валидни semver, могат безопасно да бъдат игнорирани и филтрирани.

// Returns an array of version numbers: ['1.2.0', '1.2.1', '2.0.0']
const versions = await listAssetVersions(bucket, path);

4. Разрешете точната версия, от която се нуждаем, от диапазона, който искаме. Това всъщност е супер лесно, защото библиотеката semver NPM има функция, наречена maxSatisfying. Това е допълнително страхотно, защото приема всякакъв вид синтаксис на диапазон, което означава, че нашият url за пренасочване ще поддържа всеки валиден синтаксис на диапазон, който пакетът semver поддържа, дори лудост като >=1.2.7 <1.3.0 в нашите URL адреси.

const exactRange = semver.maxSatisfying(versions, range);

5. Пренапишете uri полето, което получихме от Lambda@Edge, за да сочи към точния актив (ако всички стъпки по-горе са успешни).

const exactPath = getExactPath(request.uri, exactRange);
event.Records[0].cf.request.uri = exactPath;

Ако някой от тях се провали, оставете обекта на събитието да продължи непроменен и поискайте оригиналния URL път.

Свързване на всичко заедно

Сега, когато ламбда логиката е написана (и напълно тествана!!), трябва да свържем ламбда към Cloudfront. В нашата Cloudfront дистрибуция добавяме нова асоциация на Cloudfront Event като поведение. В него казваме, че всеки път, когато се случи заявка за произход (което означава, че заявеният актив не е вече кеширан от Cloudfront, така че трябва да го поиска от източника), тя трябва да задейства изпълнението на нашата функция Lambda. Всеки път, когато се поиска URL адрес в тази дистрибуция на Cloudfront, тя ще изпълни нашата функция Lambda (ако вече не е кеширала отговора) и нашата функция може или да го игнорира и да го остави непроменен към S3, или да го насочи към точен актив !

Изпълняваме този код в производствена среда от няколко месеца и той работи чудесно. Препоръчваме на всички наши приложения да използват статични активи по този начин и по подразбиране да сочат към major.minor URL адреси за пренасочване, така че автоматично да получават корекции на грешки, когато бъдат пуснати. Ние сме развълнувани от възможностите, които това отключва, и очакваме с нетърпение да го използваме допълнително в бъдеще.