środa, 31 maja 2017

Dogevents - wyszukiwanie wydarzeń wg lokalizacji

Jedną z głównych funkcjonalności w projekcie Dogevents jest wyszukiwanie wydarzeń wg lokalizacji. MongoDB natywnie udostępnia taką funkcjonalność, należy tylko posiadać dane geolokalizacyjne w odpowiednim formacie, przygotować odpowiedni indeks i wywołać odpowiednie zapytanie.

Format danych

MongoDB wspiera wiele typów GeoJSON takich jak punkt, linia, wielokąt. W moim przypadku ten pierwszy ma znaczenie gdyż miejsce wydarzenia to nic innego jak wskazanie na koordynaty (współrzędne) w postaci szerokości i długości geograficznej, np. [54.4760932,18.5446327]. 
W bazie muszą one zostać zapisane w postaci:
  • tablicy: [54.4760932,18.5446327]
  • dokumentu, np.: { lat: 54.4760932, lng: 18.5446327 }
U mnie właśnie ten wymóg okazał się barierą i musiałem dokonać zmiany w modelu. Dane na temat miejsca pobrane z Facebook graphApi zwracane są w postaci:

"place": {
    "name": "Psia Kość - szkolenie psów w Skoczowie",
    "location": {
      "city": "Skoczów",
      "country": "Poland",
      "latitude": 49.80972,
      "longitude": 18.79726,
      "street": "Wiejska 2",
      "zip": "43-430"
    },
    "id": "519318824878019"
  }

i przy próbie założenia wymaganego indeksu otrzymywałem błąd związany z brakiem możliwości rozpoznania danych lokalizacyjnych.

Transformacja danych

Aby rozwiązać powyższy problem zmieniłem mój model domenowy dodając nową klasę zgodną z formatem MongoDB i zmodyfikowałem strukturę location: 

public class Coordinates
{
    public double lng { get; set; }
    public double lat { get; set; }
}


public class Location
{
    public string City { get; set; }
    public string Country { get; set; }

    //For MongoDB geospatial search
    public Coordinates Coordinates { get; set; }

    public float Latitude { get; set; }
    public float Longitude { get; set; }
    public string Street { get; set; }
    public string Zip { get; set; }

    public Location()
    {
        this.Coordinates = new Coordinates();
    }
}

Aby dane zostały wprowadzone do nowej właściwości Coordinates dodałem metodę, która jest wołana w momencie deserializacji:

[OnDeserialized]
public void OnDeserialized(StreamingContext context)
{
    if (this.Place?.Location != null)
    {
        this.Place.Location.Coordinates.lat = this.Place.Location.Latitude;
        this.Place.Location.Coordinates.lng = this.Place.Location.Longitude;
    }
}

Jest to trochę mało eleganckie wyjście i powoduje duplikowanie danych. Ale chciałem na szybko uzyskać działający efekt. Na pewno jest to miejsce do optymalizacji, transformacji modelu. Końcowy dokument przyjął następującą formę:

"Place" : {
                "Name" : "Ośrodek Wypoczynkowy Omega",
                "Location" : {
                        "City" : "Przywidz",
                        "Country" : "Poland",
                        "Coordinates" : {
                                "lng" : 18.326669692993164,
                                "lat" : 54.193641662597656
                        },
                        "Latitude" : 54.193641662597656,
                        "Longitude" : 18.326669692993164,
                        "Street" : "Wczasowa 11",
                        "Zip" : "83-047"
                }
        },

Geospatial indeks

Kolejnym krokiem było utworzenie indeksów, który umożliwią tworzenie zapytań wykorzystujących dane lokalizacyjne. W swojej funkcjonalności będę wykorzystywał funkcję $near która wymaga tylko jednego indeksu typu 2d.

db.Events.createIndex({ "Place.Location.Coordinates":"2d" })

Przykładowe zapytanie

Tak przygotowana baza danych umożliwia rozpoczęcie wyszukiwania wydarzeń wg lokalizacji. Możliwości jest naprawdę wiele, mój przykład użycia jest jednym z prostszych! Poniższy przykład pokazuję zapytanie zwracające wydarzenia w odległości 50km od wskazanego miejsca:


db.Events.find({
    "Place.Location.Coordinates": {
        $near: {
            $geometry: {
                type: "Point",
                coordinates: [18.3737986, 54.5792772]
            },
            $maxDistance: 50 * 1000
        }
    }
}).pretty()

Koordynaty muszą zostać podane w postaci [długość_geograficzna (lng), szerokość_geograficzna (lat)]. Możemy określić maksymalny dystans (w metrach) jak również minimalny poprzez wskazane $minDistance.

Podsumowując. W bardzo prosty sposób możemy dodać do aplikacji funkcjonalność, która w połączeniu z bieżącą lokalizacją użytkownika umożliwi dostarczanie interesujących wydarzeń w w jego okolicy. Nie spodziewałem się tak szybkiego rozwiązania tego problemu. Jedynym z jakim się spotkałem to w dużej mierze brak danych lokalizacyjnych lub wpisanie samego miasta jako lokalizacji wydarzenia. Także wynik końcowy jest w dużej mierze zależny od osoby tworzącej wydarzenie.

piątek, 26 maja 2017

Relacja z CodeEurope 2017 Warszawa

Wczoraj wczesnym rankiem (pobudka 03:15 😪) wybrałem się do Warszawy na konferencję Code Europe. Trochę niedospany dotarłem na miejsce przed godz. 09:00. Kolejek do rejestracji nie było - tutaj organizatorzy w porównaniu do Wrocławia znaczącą się poprawili. Zanim opiszę po kolei prelekcje, w których uczestniczyłem kilka rzeczy, które spodobały mi się bądź nie.

✅ Cena biletu - była naprawdę niska. Przełożyło się to na mocne lokowanie produktów/firm w niektórych prezentacjach ale coś za coś.
✅ Ilość prelekcji - szeroki wybór tematyczny
✅ Miejsce - stadion, sale konferencyjne, strefy do posiedzenia, muzyczka na prawdę na plus. Może na głównym holu było momentami za ciasno ale było tam sporo stoisk firm, wystawców itp.
❌Panele ledowe (zamiast ekranów z rzutnikiem) dostępne na niektórych salach były usytuowane za nisko! Od połowy sali nie było już widać ich dolnych części.
Ostatnia prezentacja - już mało ludzi ale i tak nie widać dolnej części ekranu

❌Brak gastronomii - dostępne było tylko bistro na terenie stadionu, przydało by się kilka foodtruckow, jakiś dodatkowy wózek z kawą.

Prelekcje

A Fundamental Formula for Microservices using Docker - Ian Philpot 

Ewangelista Microsoftu w bardzo przystępny sposób przedstawił ABC związane mikroserwisami. Poparł to przykładami jak pewne rzeczy rozwiązywał w projektach, w których uczestniczył, własnym doświadczeniem. Komunikatywny, otwarty - prezentacja na plus!

Real world IoT solutions using Azure IoT & Azure Functions - Joe Raio

Na tą prezentację trafiłem bo: pierwsza "Pamiętaj o pamięci" nie odbyła się z powodu choroby prelegenta a druga "Scalling the data infrastructure @ Spotify" była wypchana po brzegi i zostały tylko miejsca na holu. 
Kolejny "przedstawiciel" Microsoftu, których przedstawił integrację czujników Ph (na platformie Arduino) z chmurą Azure, której celem było zbieranie danych i odpowiednie reagowanie na przekroczenie dopuszczalnych norm. Pokazane zostały "składniki", które zostały użyte w Azure i prostota (no może poza czujnikiem z arduino) w jaki sposób można to zrealizować. Mam już podstawowe doświadczenie z Azure ale była to okazja to poznania nowych rzeczy jak Azure IoT Hub, Azure Functions. Tutaj również wystawiam pozytywną ocenę.

Object Calisthenics - 9 steps to better OO code - Paweł Lewtak

Ta prezentacja najbardziej mnie rozczarowała. Przez 30min nie wiedziałem kto bardziej się męczy - ja czy prowadzący, którego wyraźnie wybiło z rytmu to, iż musiał poprowadzić ją po angielsku (a było nie pytać ludzi). Rzucane były kolejno przykłady złego kodu i jego dobrej wersji - taka kawa na ławę w 30min. Jak dla mnie nieprzystępnie - ocena negatywna.

One AI program to rule them all - Stanford's General Game Playing - Maciej Świechowski

Bardzo "akademicka" prezentacja z elementami matematyki ale ciekawa, coś z czym nie miałem do czynienia. Na plus prowadzącego język angielski - słychać było kilka lat spędzonych w Australii 😏
Ocena pozytywna.

The Things Git Can Do - Enrico Campidoglio

Jeden z bardziej charyzmatycznych prelegentów. Było wesoło, z żartami - w sam raz aby przebudzić słuchaczy. Enrico przedstawił podstawy Git'a - ale nie komend, tylko to co pod spodem się dzieję i dlaczego nie powinniśmy się tego bać. To nie Git jest trudny tylko komendy Gita. A to dlatego, że zostały napisane przez programistów dla programistów 😀 Jak najbardziej pozytywna ocena tej prelekcji.

Następnie wybrałem się na przerwę. Coś zjadłem, wypiłem kawę ale byłem tak zmęczony, że odpuściłem kolejną prelekcję i pokręciłem się trochę po stadionie. Niestety tak wczesna pobudka odbiła się negatywnie i przyswajanie informacji nie było dobre. Już wiem, że nie ma sensu zrywać się i jechać na cały dzień. Lepiej szukać czegoś na miejscu albo wybrać się dzień wcześniej tak aby być świeżym na prelekcjach.

Od Rest do GraphQL - Bartosz Sypytkowski

Dosyć świeża technologia od Facebooka ale będąca wartą rozważenia alternatywą dla Resta. Przystępnie pokazane plusy i minusy, wyzwania, z którymi trzeba będzie się zmierzyć. Bez fajerwerków ale solidna dawka wiedzy. Ocena pozytywna.

Exploring WebVR - Martin Splitt

Wybrałem coś lekkiego i dodatkowo otrzymaliśmy sporą dawkę humoru ze strony prowadzącego. Sytuacja była komiczna - pojawia się intro, że wystąpi Martin Splitt i nagle na rzutniku zaczynają pojawiać się nie jego slajdy. Techniczny podbiega do jego laptopa, odpina kabel a slajdy dalej lecą. Martin wybrnął widowiskowo. Zaczął po prostu opowiadać nam o tym co widać na rzutniku 😂 Kupa śmiechu na sali. Po kilku minutach techniczni ogarnęli sprzęt i przeszliśmy do wprowadzenia w świat wirtualnych technologii. Od sprzętu po możliwości przeglądarek w tym zakresie. Bardzo pozytywny prowadzący, lekka prezentacja i dodatkowo mały stand up.

It's all about the state, czyli co skrywa async/await w C#? - Dariusz Pawlukiewicz

Mięcho na sam koniec! Miałem obywa czy dam radę po całym dniu to ogarnąć ale już po kilku minutach byłem na TAK! Przystępnie zaprezentowane co dzieje się w IL'u gdy używamy async/await. Był mały problem z panelem ledowym i niewidocznym kodem. A szkoda bo live demo szło jak po sznurku bez żadnych zacięć. Brawo!

Podsumowanie

Udział w konferencji oceniam raczej pozytywnie. Zmęczenie dawało o sobie znak więc następnym razem inaczej będę planował takie wyjazdy. Dodatkowo raczej odpuszczę już sobie takie konferencję, które w dużej mierze promują produkty, zbierają masę HR'ow i firm, które chcą pozyskać deweloperów. Na tym etapie widzę większy potencjal w warsztatach, udziale w spotkaniach lokalnych grup lub bardziej technicznych konferencjach. 

środa, 24 maja 2017

DSP2017 etap 1 - czas podsumowań i gorzkich żali

Zbliżamy się do końca pierwszego etapu DSP2017. Nastał czas pierwszych podsumowań, przemyśleń i gorzkich żali. Zacznę od tego ostatniego 😕

Samotność

Po początkowej marcowej euforii przyszedł czas zniechęcenia, taki moment zawahania czy start w DSP miał jakikolwiek sens. Postępy w projekcie były znikome do tego to pisanie postów. W tym czasie wolałbym kodować! I tak nagle poczułem się samotny - nie życiowo ale taki niezauważony, niepotrzebny, nieodczuwający efektu DSP. Jedną z pierwszych myśli jaka mi w tedy przyszła do głowy to gdzie się podział organizator? zapomniał o nas? Ten konkurs jeszcze trwa? Zdaję sobie sprawę, że Maciej ma bardzo dużo na głowie ale jakaś mała forma cotygodniowej e-mobilizacji (może nawet sponsorzy mogliby się w to włączyć) byłaby fajnym wsparciem. Na szczęście było to u mnie chwilowe i dość szybko się z tym uporałem 😊

Walka z samym sobą

Ten konkurs to w dużej mierze walka z samym sobą. Ty jesteś właścicielem produktu, kierownikiem, bawisz się w marketing, projektujesz i realizujesz. Można się tutaj sprawdzić na kilku polach. Ja osobiście nie jestem dobry w marketingu i mój udział w DSP nie bardzo rozgłaszałem. Publikacja postów była rzucana na kilka kanałów społecznościowych. Myślę, że gdybym poświęcił większą uwagę temu efekt zauważenia byłby większy.

Czy konkurs DSP jest potrzebny?

Czy DSP jest mi potrzebne? Blogowanie, rozwijanie projektu - to wszystko przecież mogłem już dawno robić. No tak tylko tak to już jest, że jest potrzebny kopniak, mobilizacja, cel a może nawet ten element rywalizacji. Mam trochę żal do siebie, że tyle lat zwlekałem z tym. Inną sprawą jest, iż nie widziałem w tym żadnego celu. Dziś wiem, że po części było to także podyktowane miejscami w których pracowałem. Jeśli nie masz dobrego team leader'a, w zespole jest niemoc to twoje próby wybicia się czy różnych propozycji zmian, rozwoju są olewane.

To wszystko jest drugorzędne

Te wszystkie negatywne odczucia są drugorzędne. Ilość wiedzy i postęp jaki dotąd poczyniłem jest ogromny. Rozwijanie własnego projektu jest bardziej cenne niż takie suche przyswajanie wiedzy poprzez podcasty czy udział w konferencjach. Nie neguje tych drugich bo odgrywają swoją rolę w zdobywaniu informacji ale praktyka, pisanie kodu to jest to co daje dużego kopa! W krótkim czasie zapoznałem się m.in. z asp.net core, Vue.js, Webpack, Node, MongoDB, Azure. Wiadomo, że nie osiągnąłem poziomu specjalisty w tym zakresie ale jest to cenna wiedza i doświadczenie umożliwiające wejście do projektu i dalszy rozwój w tym obszarze.
Aspekt blogowania okazał się równie cenny. Pisząc posta za każdym razem weryfikujesz swoją wiedzę, uzupełniasz ją. Blog stanowi część twojego portfolio, jest twoją wizytówką. Dla mnie osobiście jest możliwością dzielenia się wiedzą. Być może po drugiej stronie łącza jest tam jakaś osoba, która z niej korzysta. Pamiętam jak zaczynałem przygodę z programowaniem sam miałem takich wirtualnych mentorów. Kilku nawet udało się spotkać na konferencjach 😁

środa, 17 maja 2017

MongoDB - ViewModel

Po pierwszych testach projektu zauważyłem, że wywołania REST API zwracają za dużo danych. Było to spowodowane zwracaniem modelu domenowego, który nie jest odpowiedni do tego typu operacji. Dlatego częstym zabiegiem jest wprowadzenie tzw. view model, który odpowiada danym zwracanym do interfejsu użytkownika czy wywołań API. Zazwyczaj jest on "szczuplejszy" i prostszy od domenowego.
W tym poście chciałbym pokazać rozwiązanie oparte o bazę danych MongoDB i metody generyczne.


Dogevents - domain model
IEvent - główny model domenowy. Ten interfejs zawiera wszystkie informacje o wydarzeniu. Jest złożoną strukturą, która odzwierciedla wydarzenia pobrane z Facebook przy użyciu Graph Api.

public class Event
{
    [BsonId]
    public long Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    [JsonProperty("start_time")]
    public DateTime StartTime { get; set; }

    [JsonProperty("end_time")]
    public DateTime EndTime { get; set; }

    [BsonIgnore]
    public string Url { get => $"https://www.facebook.com/events/{Id}/"; }

    public string CoverUrl { get => Cover.source; }

    [JsonProperty("is_canceled")]
    public bool IsCanceled { get; set; }

    public Cover Cover { get; set; }
    public Owner Owner { get; set; }
    public Place Place { get; set; }
    public IEnumerable<string> Categories { get; set; }
}


IViewEventModel - Podstawowy interfejs na potrzeby widoków. Stworzony do oznaczenia metod generycznych - tak aby móc wykorzystywać w nich pewne właściwości niezbędne do zapytań bazodanowych.

public interface IViewEventModel
{
    long Id { get; set; }
    string Name { get; set; }
    DateTime StartTime { get; set; }
}

View service

Aby móc wykorzystywać serwis pobierający wydarzenia z bazy MongoDB z różnymi modelami zadeklarowałem metody generyczne z ograniczeniem, że zwracany model musi implementować IViewEventModel. Jest to podyktowane tym, iż implementacja niektórych metod musi posiadać dostęp do specyficznych właściwości modelu, np. metoda GetIncoming<T>() wykorzystuje pole StartTime do zdefiniowania filtru na tym polu. Pominięcie takiego "wskazania" (z ang. constraint) uniemożliwiło by to.


public interface IViewEventsService
{
    Task<List<T>> GetPopular<T>() where T : IViewEventModel;

    Task<List<T>> GetIncoming<T>() where T : IViewEventModel;

    Task<List<T>> GetJustAdded<T>() where T : IViewEventModel;
}

View models

Następnie zdefiniowałem kilka klas, które będą zwracały tylko potrzebne w danym momencie dane. Dla przykładu jedna z klas, która zawiera tylko dane potrzebne do wyświetlania nagłówka wydarzenia, bez jego szczegółów.


public class EventCardHeaderViewModel : IViewEventModel
{
    public long Id { get; set; }
    public string Name { get; set; }
    public DateTime StartTime { get; set; }

    public string Url { get => $"https://www.facebook.com/events/{Id}/"; }
}

Przykład użycia



[HttpGet]
[Route("GetPopular")]
public async Task<IEnumerable<EventCardViewModel>> GetPopular()
{
    return await _viewEventService.GetPopular<EventCardViewModel>();
}

Powyżej przykład wywołania metody serwisowej ze wskazaniem na jaki model mają zostać zmapowane dane. Jest tylko jedno ale. Nie została wykonana pewna konfiguracja czy też zastosowana konwencja jak ma się zachowywać MongoDB podczas deserializacji danych na nasz model. Domyślnie, jeśli zabraknie definicji któregoś z pól otrzymamy wyjątek typu FormatException:



MongoDB konwencje

Aby rozwiązać ten problem należy odpowiednio skonfigurować mechanizm serializacji w MongoDB używając mechanizmu konwencji. API MongoDB udostępnia szereg wbudowanych "pakietów zachowań". Pełną listę można znaleźć tutaj: MongoDB Serialization Convetions Do rozwiązania tego problemu użyłem IgnoreExtraElementsConvention. Sama rejestracja odbywa się z użyciem klasy ConventionRegistry:



private static void RegisterConventions()
{
    ConventionRegistry.Register("DogeventsConvetion", new MongoConvention(), x => true);
}

private class MongoConvention : IConventionPack
{
    public IEnumerable<IConvention> Conventions => new List<IConvention>
    {
        new IgnoreExtraElementsConvention(true),
    };
}

niedziela, 7 maja 2017

PWA - Progressive Web Application

Czy jesteście w stanie wyobrazić sobie aplikacje na telefony bez potrzeby ich instalacji ze sklepu play store, app store? Korzystania z nich nawet w przypadku braku dostępu do internetu? Witam w świecie PWA - Progressive Web Application - nowoczesnych aplikacji internetowych.
Po raz pierwszy trafiłem na ten termin słuchając podcasta  Making a Web App Progressive with Christian Heilmann. W bardzo przystępny sposób Christian Heilmann opowiedział o idei jaka stoi za PWA, jak wyglądają bieżące możliwości przeglądarek w zakresie obsługi funkcjonalności, które mają dostarczać nowoczesne aplikacje internetowe i do czego to wszystko zmierza. Zrobiło to na mnie duże wrażenie. Nie tylko od strony technologicznej ale także praktycznej jako użytkownika takiej aplikacji. Samo odejście od braku potrzeby instalacji ze sklepu aby jej użyć już jest dla mnie bardzo wygodne. Ile to razy instalowałem aplikację aby użyć jej raz.
Co istotne aby aplikacja internetowa stała się "nowoczesną" nie trzeba pisać jej od nowa. Można dokonać pewnych modyfikacji, nowych implementacji, który przyczynią się do zmiany jej w PWA. Poniżej przedstawiam bardzo ogólne założenia, które mam nadzieję wprowadzą Cię w świat PWA.

Główne założenia

Niezawodność

Aplikacja ma działać zawsze, w każdych warunkach. Niezależnie czy użytkownik ma dostęp do sieci czy nie. W tym celu przydatne jest buforowanie danych po stronie klienta tak aby przy braku dostępu do internetu miał do nich dostęp. Tutaj główną rolę odgrywa service-worker, który pełni rolę usługi po stronie klienta odpowiadającej za obsługę przychodzących żądań oraz zarządzanie buforowaniem danych (z ang. cache).
źródło: https://auth0.com/blog/creating-offline-first-web-apps-with-service-workers/


Szybkość


Jedna z najbardziej frustrujących rzeczy - oczekiwanie na załadowanie się strony internetowej. Aplikacja może oferować nie wiadomo jakie fajerwerki ale jeśli jest wolna, użytkownik ciągli dostaje komunikaty typu "trwa ładowanie danych, proszę czekać ...", interfejs zawiesza się lub trzeba długo czekać na jej uruchomienie to z pewnością przeważy to na jej niekorzyść i możesz być pewien że już z niej nie skorzysta.

Atrakcyjność

Pod tym pojęciem nie kryje się tylko wizualna część aplikacji ale także możliwość uruchomienia jej z ekranu startowego, obsługa na pełnym ekranie czy wysyłanie powiadomień typu push. Użytkownik nie ma odczuwać że ma do czynienia z atrapą aplikacji. Ma ona dostarczać takich samych odczuć jak użytkowanie natywnych aplikacji. Zapewnić to mogą Web App Manifest oraz wspomnianie już Web Push Notification

Przykładowe aplikacje

Istnieje już kilka rozwiązań, które całkowicie implementują PWA są to m.in.:
Moją uwagę przykuła ostatnia pozycja ze względu na udostępnienie przez autora kodu gdzie można zobaczyć cały stos technologiczny i rozwiązanie wspomnianych idei PWA.

Przydatne narzędzia

Na sam koniec wspomnę o kilku narzędziach, które mogą pomóc w pracy przy tworzeniu bądź konwersji aplikacji internetowej do PWA. Są to:
  • Lighthouse - narzędzie do badania w jakim stopniu nasza aplikacja spełnia założenia PWA.
  • Konsole deweloperskie dostępne w przeglądarkach, które w całości dostarczają nam wszystkich informacji oraz narzędzi. Polecam zapoznanie się z artykułem Tools for PWA Developers

Podsumowanie

Moim zdaniem PWA wytycza nowy kierunek aplikacji internetowych. Nie jest to może rewolucja ale ewolucja, która wprowadza wygodne dla użytkownika rozwiązania. Odchodzi od modelu instalacji aplikacji ze sklepu, pozwala na jej użytkowanie również przy braku dostępu do sieci, przeszukiwanie jej zawartości przez wyszukiwarki co zwiększa szansę na trafienie do docelowego odbiorcy. Minusem są przeglądarki internetowe, które na swój sposób realizują obsługę mechanizmów PWA i poszczególna implementacja na każdej z nich może się różnić i być na innym etapie. Oby nie tak bardzo jak w przypadku arkuszy CSS kilka lat temu 😉

środa, 3 maja 2017

Azure - Deploy Node.js + Vue + Webpack

Nie mogłem doczekać się aż napiszę tego posta. Minęło kilka dni, cisza nastała na blogu ale była spowodowana spadkiem mocy i frustracją spowodowaną wieloma bezowocnymi próbami zbudowania aplikacji node.js na serwerze Azure. Ale w końcu udało się. Można powiedzieć, że mam już prawie pełne CI 😏

Scenariusz wdrożenia

Zacznę od opisu jaki efekt chciałem uzyskać.
Azure deploy - node.js, vue.js, webpack
Lokalne zmiany są zatwierdzane i "wypychane" na serwer githuba, Następnie Azure poprzez skonfigurowany automatyczny deploy rozpoczyna całą procedurę wdrożenia:
  • Pobiera kod źródłowy z githuba do folderu \home\site\repository
  • Kopiuje pliki do folderu \home\site\wwwroot
  • Wykonuje instalację node_modules
Tak to wygląda bardzo poglądowo, w tle dzieje się wiele więcej rzeczy. 
W tym całym procesie brakowało mi aby to Azure był odpowiedzialny za uruchomienie webpacka i wykonanie całej tej procedury transformacji plików .js, .vue itd. tak aby pozbyć się z repozytorium "/dist".

Potencjalne problemy

Jak wspomniałem na początku posta, droga nie była łatwa ale też w dużej mierze przez brak doświadczenia jak działa deploy Azure i node.js w środowisku produkcyjnym. Oto kilka wskazówek:
  • Node.js na Azure działa w trybie "production". Weź to pod uwagę gdy chcesz uruchamiać skrypty które coś budują. Tutaj nie masz skonfigurowanego środowiska deweloperskiego adhoc.
  • Zwróć uwagę na użytą wersję node.js oraz npm. Większość moich problemów wynikała z dość starej wersji npm 1.4. Można to zmienić dodając ustawienie WEBSITE_NODE_DEFAULT_VERSION. Listą dostępnych runtime jest duża i można ją zobaczyć pod adresem: https://your_site_name.scm.azurewebsites.net/api/diagnostics/runtime
  • Zanim doszedłem do zmiany wersji node.js przez długi czas borykałem się ze znanym z wcześniejszych wersji windows problemem maksymalnego rozmiaru nazwy ścieżki ograniczonego do 260 znaków. Jest dostępne obejście tego problemu: #max_path-explanation-and-workarounds
  • Pamiętaj aby nie kopiować nie potrzebnych rzeczy do folderu docelowego, np. logi z builda, node_modules typu devDependencies


#1 Własny skrypt deploy.cmd

Aby ruszyć z tematem wpierw należy wykonać modyfikację automatycznego deploy poprzez utworzenie własnego skryptu. Służy do tego narzędzie azure-cli https://www.npmjs.com/package/azure-cli. Tworzy ona dwa pliki .deployment oraz deploy.cmd. Obydwa należy umieścić w katalogu root aplikacji. Do ich utworzenia służy komenda:
azure site deploymentscript --node
deploy.cmd jest skryptem batch, w którym widać kilka komend ustawiających zmienne do poleceń jak i ich wywołania. Głównie są to operacje kopiowania plików pomiędzy folderem %DEPLOYMENT_SOURCE% a %DEPLOYMENT_TARGET%
Na uwagę zasługuje zmienna %DEPLOYMENT_SOURCE%. Wskazuje ona na ścieżkę do folderu repozytorium z projektem. W moim przypadku mój projekt nie leży w głównym katalogu repozytorium, więc musiałem dokonać zmiany w ustawieniach podając prawidłowy katalog.


Dodatkowo dodałem ustawienie "command" wskazujące na mój skrypt deploy.cmd. Nie musisz tego wykonywać jeśli ten skrypt będzie umieszczony w głównym katalogu repozytorium:
Azure - deploy.cmd path

#2 Webpack build

Kolejnym krokiem jest dodanie do skryptu deploy.cmd wywołania webpack aby wykonał przetwarzanie naszego projektu. Co istotne chcę aby ten krok został wykonany jeszcze w lokalnym folderze repozytorium a nie w docelowym site/wwwroot gdzie mają znaleźć się tylko wymagane moduły produkcyjne node'a, statyczne pliki html oraz przygotowany przez webpack /dist. Pozostały kod źródłowy nie powinien w tym miejscu się znaleźć.


Całość zamyka się w takim oto wywołaniu:
deploy.cmd
:Deployment
echo Handling node.js deployment.

:: 1. Webpack Build
IF EXIST "%DEPLOYMENT_SOURCE%\webpack.config.js" (
  echo Installing node modules dev/prod
  pushd "%DEPLOYMENT_SOURCE%"
  call npm install --only=prod
  call npm install --only=dev
  echo Running webpack build
  call npm run build
  popd
  IF !ERRORLEVEL! NEQ 0 goto error
  echo Finished webpack build
)

  • Sprawdzam czy w folderze projektu znajduje się plik konfiguracyjny webpack.config.js
  • Następnie przystępuje do instalacji paczek node'a - produkcyjnych i deweloperskich. Związane jest to z tym iż package.json dzieli moduły, które są potrzebne do uruchomienia aplikacji (dependencies) od tych wymaganych tylko w środowisku deweloperskim (devDependencies). My potrzebujemy obydwie zależności.
  • następnie uruchamiam skonfigurowany w package.json "build" skrypt. Komenda npm run build
package.json
"scripts": {
    "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
  },

Tak wygląda log z wywołania:
Azure - deploy.cmd output
Widać, że został wykorzystany niestandardowy katalog źródłowy, własny skrypt oraz wywołanie instalacji paczek node'a i webpack.

#3 KuduSync

Kolejnym krokiem jest kopiowanie plików z folderu źródłowego do docelowego site/wwwroot. Na tym etapie należy wykluczyć niepotrzebne pliki. Komenda %KUDU_SYNC_CMD% zawiera argument "-i", który umożliwia wskazanie takich plików/folderów. Domyślnie jest dodany foldery .git oraz skrypty deploy. Resztę dopisujemy wg uznania. Na pewno nie chcę kopiować zainstalowanych na potrzeby webpack paczek node'a oraz plików źródłowych .vue. Możemy także wykluczyć pliki konfiguracyjne takie jak webpack.config.js


:: 2. KuduSync
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd;node_modules;.vue;webpack.config.js"
  IF !ERRORLEVEL! NEQ 0 goto error
)

Podsumowanie

Dzięki własnej implementacji a raczej rozszerzeniu deploy.cmd o wywołanie webpack udało się uzyskać bardzo wygodne rozwiązanie gdzie nie muszę umieszczać w repozytorium wygenerowanych plików przerzucając to na serwer Azure. Głównym problemem, który najbardziej może doskwierać to to, iż teraz musimy niejako utrzymywać dwa środowiska deweloperskie tak aby zarówno build lokalny jak i ten na Azure wykonywały się poprawnie.