2017: Aplikacja webowa

Praca ksiegowych zbyt często polega na żmudnym przepisywaniu kwot, numerków, dziesiątek tabelek z liczbami. Mamy zamiar to zmienić. Tworzymy arbitrue — wirtualnego asystenta księgowego, nowy produkt skierowany na rynek w Wielkiej Brytanii.

Jako zespół infakt.pl — od lat pomagamy małym przedsiębiorcom z całej Polski przy fakturowaniu i księgowości. Na nasz sukces składa się wiele czynników, ale wiemy, że jednym z nich jest dobrze zaprojektowana, intuicyjna i przejrzysta aplikacja webowa.

Stojąc przed nowym projektem, postawiliśmy sobie poprzeczkę jeszcze wyżej. W 2007 roku, gdy startowaliśmy z inFakt, postawiliśmy na Ruby on Rails. Dziś wiemy, że podjęliśmy dobrą decyzję. 10 lat później, w 2017 tworzymy nową aplikację i stajemy przed kolejnym wyborem. Chcemy móc za 10 lat powiedzieć to samo.

alt text

Nowa aplikacja webowa w 2017,

Nie trzeba być bacznym obserwatorem, aby stwierdzić, że przez ostatnie lata wiele się dzieje w świecie frontendu. Kiedyś wystarczyło generowanie widoków stron na serwerze. Potem, zaczęto modyfikować stronę (DOM) po stronie przeglądarki co spowodowało pojawienie się jQuery, następnie wprowadziliśmy się asynchroniczność za sprawą AJAX… pewnie znacie tą historię. W dalszej kolejności, zapragnęliśmy informować użytkownika na żywo o nowych zdarzeniach, np. wiadomościach, aktualizacjach, czy też zaksięgowanych plikach, np przy użyciu Web Socketów. Naturalnie wykształciła się koncepcja Single Page Application — SPA.

a więc Progressive Web Application,

W związku z rozwojem urządzeń mobilnych, sieci społecznościowych, chęci ujednolicenia wyglądu na różnych platformach, ukuto nową listę zasad: Progressive Web Application. Dziś, czy nam się to podoba czy nie, najlepsi tworzą aplikację które są:

  • Progresywne — Działają dla każdego, niezależnie od przeglądarki, ciągle dodając wsparcie dla nowych technologii, ponieważ jest tworzona z myślą o ciągłym rozwoju.
  • Responsywne — Działają na wszystkim. Komputerach, telefonach, tabletach, a nawet lodówkach, jeśli tylko jest dla nich zastosowanie.
  • Niezależne od jakości łącza — Dzięki serwisom działającym w tle, które potrafią wstrzymać wysyłanie danych aż do przywrócenia łączności, pozwalają na pracę offline i użytkownik nie odczuje, że pracuje na wolnym łączu.
  • ‘Aplikacyjne/natywne’ — Wyglądają i działają jak aplikacja, dzięki separacji funkcjonalności, od zawartości.
  • Aktualne — Zawsze zaktualizowane do najnowszej wersji, np. podczas cichych aktualizacji wykonywanych przez ‘service worker’.
  • Bezpieczne — Zawsze dostępne przez https.
  • Wykrywalne — Rozpoznawalne jako ‘aplikacja’, dzięki obecności W3C manifest i rejestracji w tle, przez co są obecne w wynikach wyszukiwania.
  • Ciągle zachęcające — Wysyłają np. wiadomości PUSH czy maile.
  • Instalowalne — Pozwalają na dodawawanie do pulpitu, bez ściągnięcia ze sklepu czy strony.
  • Pozwalające na linkowanie — Są dostępne pod linkiem nie wymagającym instalacji, który można łatwo udostępnić.

Tworzenie takich aplikacji przenosi dużą ilość logiki, ciężar tworzenia intefrejsu użytkownika z serwera na przeglądarkę, która z silnika do interpretacji wyglądu stron staje się pełną platformą uruchomieniową. Taka zmiana stwarza wiele wyzwań, dla projektantów, developerów jak i samych przeglądarek. Aby ułatwić tworzenie takich aplikacji, jak grzyby po deszczu wyrosło kilka rozwiązań w postaci frameworków. Doświadczenie nas nauczyło, że jeżeli chcemy być konkurencyjni, musimy wybierać zawsze najnowszą technologię. Biorąc pod uwagę trendy oraz to, że z punktu widzenia użytkownika aplikacje typu SPA mają niezaprzeczalnie lepszą użyteczność oraz szybkość — a to wartości, do których zawsze dążymy. Chcemy aby arbiture powtórzył sukces inFakt. Wiemy, że design i wybór technologii frontowej są kluczowe, aby zaskarbić sobie serca użytkowników. Ale jak ją wybrać?

działająca z REST API

W inFakt mamy doświadczenie w tworzeniu backendu oraz REST API w Ruby on Rails. Jesteśmy zadowoleni z tej technologii, pracują z nami eksperci i jesteśmy zaangażowani w działanie lokalnej społeczności Ruby. Postanowiliśmy pozostać przy niej, dlatego podstawowym warunkiem, który musi spełniać technologia w której stworzymy aplikację frontową jest wygodna wpółpraca z REST API i Ruby on Rails.

i WebSocketami

Aby zapewnić użytkownikowi ciągłe aktualizacje o nowych wiadomościach, powiadomieniach zdecydowaliśmy, że arbitrue będzie korzystać z WebSocketów. Ruby wspiera tą technologię, w oparciu o gem ActionCable. Znamy tą technologię i chcielibyśmy mieć możliwość jej użycia w naszej aplikacji frontowej.

oparta o bibliotekę…

Samobójstwem dla małego zespołu wydaje się być pisanie takiej aplikacji od zera, bez wsparcia żadnej biblioteki. Odpowiednie dobranie i wykorzystane frameworka, umożliwia szybkie tworzenie nowoczesnych aplikacji i daje możliwość efektywnej realizacji projektu. Mechanizmy, komponenty, pluginy, które można wykorzystać w aplikacji poprawiają przezjrzystość kodu, umożliwiają jego łatwe wielokrotne wykorzystanie. Działając zgodnie z konwencją proponowaną przez bibliotekę, piszemy kod który jest bardziej uporządkowany, przyjazny. Z drugiej strony, często frameworki mocno narzucają architekturę i sposób pisania aplikacji, która nie zawsze może wpisywać się w naszą wizję produktu. Wybierając bibliotekę przywiązujemy się nie tylko do technologii, ale także do ekosystemu, składającego się z wtyczek, tutoriali, przykładów i wsparcia. Aby ten wybór był możliwie świadomy, postanowiliśmy porównać trzy najpopularniejsze obecnie frameworki frontowe. Pierwszym działaniem było zawężenie listy kandydatów. W tym celu skorzystaliśmy ze strony stateofjs.com:

alt text

Niebieskia część słupka odpowiada za zainteresowanie technologią, czerwona doświadczeniami. Mniej nasycone kolory oznaczają brak zainteresowania, bądź złe doświadczenia, a nasycone żywe zainteresowanie bądź pozytywne doświadczenia. Wg danych, największe zainteresowanie wzbudzał Angular 2, Vue oraz React. Najwięcej satysfakcji przyniósł React (53/5) oraz Vue (10/1). Jako, że Angularjs(1) został zastąpiony Angularem 2, którego rozwinięciem jest Angular 4, pozostaniemy przy porównaniu z najnowszą wersją. Takie wyniki pozwoliły nam na szybkie zawężenie kandydatów do: React.js, Vue.js oraz Angular 2 (4)

Już niedługo opublikujemy bardziej techniczny artykuł porównujący te technologie.

Przekazywanie zmiennych w playbookach Ansible

Czasami problemem w playbookach Ansible staje się przekazywanie zmiennych między częściami tego playbooka wykonywanymi na różnych hostach. Rozważmy taki przypadek, w którym wykonamy następujące kroki:

  • Na lokalnej maszynie wykonamy przykładowe polecenie i zarejestrujemy jego wynik jako zmienną testvar
  • Połączymy się z innym hostem i spróbujemy odwołać się na nim do zmiennej testvar za pomocą modułu debug
  • Spróbujemy zmodyfikować przykładowy playbook tak, żeby zawartość zmiennej prawidłowo uzyskać.

Prosty playbook, który pozwoli nam na przetestowanie dostępności zmiennych wygląda np. tak:

---
- hosts: localhost
  become: no
  connection: local

  tasks:
  - shell: echo "Foo bar"
    register: testvar

  - debug: var=testvar

- hosts: vagrant.local
  become: no

  tasks:
  - debug: var=testvar

W efekcie wywołania takiego playbooka przekonamy się, że zmienna testvar, prawidłowo zarejestrowana na maszynie lokalnej, na hoście vagrant.local nie jest dostępna:

$ ansible-playbook -i 'vagrant.local,' test.local

PLAY [localhost] ***************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] => {
    "testvar": {
        "changed": true,
        "cmd": "echo \"Foo bar\"",
        "delta": "0:00:00.001634",
        "end": "2017-03-17 16:58:20.821949",
        "rc": 0,
        "start": "2017-03-17 16:58:20.820315",
        "stderr": "",
        "stdout": "Foo bar",
        "stdout_lines": [
            "Foo bar"
        ],
        "warnings": []
    }
}

PLAY [vagrant.local] ***********************************************************

TASK [setup] *******************************************************************
ok: [vagrant.local]

TASK [debug] *******************************************************************
ok: [vagrant.local] => {
    "testvar": "VARIABLE IS NOT DEFINED!"
}

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0
vagrant.local              : ok=2    changed=0    unreachable=0    failed=0

Okazuje się jednak, że wystarczy drobna zmiana, żeby “dobrać się” do naszej zmiennej. Dla czytelności prezentuję tylko drugą część playbooka:

- hosts: vagrant.local
  become: no

  tasks:
  - debug: var=hostvars['localhost']['testvar']

Po wywołaniu zmodyfikowanego playbooka otrzymujemy w tym zadaniu wartość naszej zmiennej:

TASK [debug] *******************************************************************
ok: [vagrant.local] => {
    "hostvars['localhost']['testvar']": {
        "changed": true,
        "cmd": "echo \"Foo bar\"",
        "delta": "0:00:00.001697",
        "end": "2017-03-17 17:03:01.279967",
        "rc": 0,
        "start": "2017-03-17 17:03:01.278270",
        "stderr": "",
        "stdout": "Foo bar",
        "stdout_lines": [
            "Foo bar"
        ],
        "warnings": []
    }
}

Migracja z rvm do rbenv na mac OS

Na początek usuwamy wszystko co dotyczy RVM:

rvm implode

# ręcznie usuwamy (jeśli istnieje) plik:
rm ~/.rvmrc 

# oraz wpisy dotyczące RVM z:
.profile
.bash_profile
.bashrc
.zshrc

Następnie instalujemy rbenv przy pomocy brew:

brew install rbenv ruby-build

Lub ściągając z git hub’a

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

Dodajemy rbenv do PATH:

# do .bash_profile lub .zshrc dodajemy:
export PATH="$HOME/.rbenv/bin:$PATH"

Oraz uruchamiamy:

rbenv init 

I uzupełniamy wskazany plik odpowiednim wpisem.

Aby zainstalować odpowiednią wersję ruby:

rbenv install 2.4.0

Możemy nadal używać plików .ruby-version wpisując w nie wersję ruby której chcemy używać w danym projekcie.

Synchroniczne uruchamianie workerów Sidekiq

Sidekiq pozwala na wykonywnie określonych zadań asynchronicznie, “w tle”. Najczęściej realizowane jest to za pomocą specjalnie stworzonych klas (workerów), które dzięki użyciu modułu Sidekiq::Worker zyskują metody z których możemy skorzystać aby zlecić wykonanie kodu w tle.

class FooWorker
  include Sidekiq::Worker

  def perform(foo_id)
    "Wartość foo_id wynosi: #{foo_id}"
  end
end

# wykonanie w tle
FooWorker.perform_async(1)
=> "d26cca4c2786f52e48da9520"

# wykonanie po wskazanym czasie
FooWorker.perform_in(10.seconds, 1)
=> "369aee857301ef89d7669821"

Czasem przydatne może być wykonanie kodu workera w ramach aktualnego wątku, bez planowania zadania w tle. Wbrew pozorom jest to bardzo łatwe - wystarczy pamiętać, że worker jest przecież regularną klasą Ruby, w związku z tym możemy po prostu powołać jego instancję i wykonać metodę perform:

FooWorker.new.perform(1)
=> "Wartość foo_id wynosi: 1"

Oczywiście, przy takim wywołaniu całkowicie pomijamy strukturę Sidekiq, tracąc tym samym kolejkowanie, ponawianie zadań itd. Może to być jednak bardzo przydatne w momencie, kiedy chcemy łatwo sprawdzić działanie kodu zamkniętego w workerze, np. w trakcie debugowania aplikacji.

Sortowanie NULL w PostgreSQL

W przypadku kiedy kolumna według której sortujemy dane pobierane z bazy PostgreSQL przybiera wartość NULL, kolejność wyników jest następująca:

  • dla sortowania w porządku rosnącym (ASC), wartości NULL zostaną umieszczone jako ostatnie,
  • dla sortowania w porządku malejącym (DESC), wartości NULL zostaną umieszczone jako pierwsze.

Nie zawsze jednak jest to preferowane zachowanie - łatwo wyobrazić sobie sytuację, kiedy puste dane chcemy zawsze prezentować jako ostatnie (lub jako pierwsze) - niezależnie od przyjętego porządku sortowania.

Można to łatwo wymusić jawnie wskazując w momencie sortowania w jakim porządku mają być zwracane wartości NULL. Aby to osiągnąć, należy do warunku sortowania - ORDER BY column_name ASC | DESC - dodać:

  • NULLS LAST - jeżeli wartość NULL ma być umieszczona na końcu wyników,
  • NULLS FIRST - jeżeli wartość NULL ma być umieszczona na początku wyników.

Przykładowo, dla danych:

-------------------
| id | name       |
-------------------
| 1  | Zofia      |
| 2  | NULL       |
| 3  | Alicja     |
-------------------

możemy sortować w następujący sposób:

# NULLe zawsze ostatnie
SELECT id, name FROM table_name ORDER BY name ASC NULLS LAST;
=> [3, Alicja], [1, Zofia], [2, NULL]

SELECT id, name FROM table_name ORDER BY name DESC NULLS LAST;
=> [1, Zofia], [3, Alicja], [2, NULL]

# NULLe zawsze pierwsze
SELECT id, name FROM table_name ORDER BY name ASC NULLS LAST;
=> [2, NULL], [3, Alicja], [1, Zofia]

SELECT id, name FROM table_name ORDER BY name DESC NULLS LAST;
=> [2, NULL], [1, Zofia], [3, Alicja]

Więcej informacji dostępne jest w dokumentacji PostgreSQL.

Docker - przydatne polecenia

Kilka użytecznych poleceń przy pracy z docker’em.

Lista kontenerów z informacją o zajmowanym miejscu na dysku:

  docker ps -s

Podgląd używanych zasobów przez poszczególne kontenery:

  docker stats

Usuwanie nieużywanych kontenerów:

  docker rmi -f $(docker images | grep "<none>" | awk "{print \$3}")

watch unicorn - sprawdzenie kiedy się unicorn przeładuje

Aby nie wpisywać co kilka sekund polecenia sprawdzającego czy np. workery unicorna się przeładowały, możemy użyć polecenia:

  watch 'ps aux | grep master | grep unicorn | grep -v grep'

Po słowie kluczowym watch podejemy polecenie jakie ma zostać wykonane. Polecenie będzie powtarzane co 2 sekundy, aby zmienić interwał używamy opcji -n lub --interva.

Sprawdzanie istnienia rekordów

Istnienie rekordów w bazie danych można sprawdzić używając ActiveRecord na kilka sposobów. Poniżej zestawienie możliwych konstrukcji, wraz z zapytaniem SQL jakie generują. Większość zapytań generuje prawidłowe zapytanie, warto jednak zwrócić uwagę na te, które pobierają wszystkie rekordy z bazy i dopiero na pobranej kolekcji wykonują metody z modułu Enumerable.

Zapytania generujące prawidłowy SQL:

User.scoped.any?
SELECT COUNT(*) FROM "users"

User.scoped.empty?
SELECT COUNT(*) FROM "users"

User.scoped.blank?
SELECT COUNT(*) FROM "users"

User.scoped.exists?
SELECT 1 AS one FROM "users" LIMIT 1

User.scoped.present?
SELECT COUNT(*) FROM "users"

User.scoped.many?
SELECT COUNT(*) FROM "users"

Zapytania pobierające wszystkie rekordy:

User.scoped.none?
SELECT "users".* FROM "users"

User.scoped.one?
SELECT "users".* FROM "users"

URI.join - łatwa modyfikacja ścieżki w adresie URL

Często spotykamy się z sytuacją, kiedy do adresu URL chcemy dołączyć odpowiednią ścieżkę, tworząc tym sposobem kompletny URI. Wydaje się to trywialne, jednak bardzo łatwo może rodzić błędy.

Najprostsze rozwiązanie to zwykła konkatenacja stringów:

base_url = 'https://www.infakt.pl/'
path = 'sample_path'

"#{base_url}#{sample_path}"
=> 'https://www.infakt.pl/sample_path'

Na pierwszy rzut oka wszystko jest w porządku. Jeżeli jednak wartość base_url nie będzie oczywista (np. będzie pochodzić z ustawień zdefiniownych w innym miejscu aplikacji) bardzo łatwo o wygenerowanie błednego adresu. Również w przypadku wartości określającej ścieżkę, możemy w prosty sposób doprowadzić do błędu:

# brak backslasha między hostem a ścieżką
base_url = 'https://www.infakt.pl'
path = 'sample_path'

"#{base_url}#{sample_path}"
=> 'https://www.infakt.plsample_path'

# zdublowany backslash między hostem a ścieżką
base_url = 'https://www.infakt.pl/'
path = '/sample_path'

"#{base_url}#{sample_path}"
=> 'https://www.infakt.pl//sample_path'

Kolejny sposób, to parsowanie adresu URL i dodanie ścieżki, co nie wydaje się być zbyt eleganckie, a dodatkowo obarczone jest ryzykiem wygenerowania wyjątku, w przypadku kiedy ścieżka nie jest ścieżką absolutną.

URI.parse('https://www.infakt.pl/').tap { |uri| uri.path = '/sample_path' }.to_s
 => "https://www.infakt.pl/sample_path"

URI.parse('https://www.infakt.pl').tap { |uri| uri.path = '/sample_path' }.to_s
=> 'https://www.infakt.pl/sample_path'

URI.parse('https://www.infakt.pl/').tap { |uri| uri.path = 'sample_path' }.to_s
URI::InvalidComponentError: bad component(expected absolute path component): sample_path

Na szczęście, równie łatwo jak wygenerować błąd, możemy się przed nim zabezpieczyć, używając URI.join, które łączy domenę ze ścieżką, dbając jednocześnie o poprawność wygenerowego adresu:

URI.join('https://www.infakt.pl/', 'sample_path')
=> 'https://www.infakt.pl/sample_path'

URI.join('https://www.infakt.pl', 'sample_path')
=> 'https://www.infakt.pl/sample_path'

URI.join('https://www.infakt.pl/', '/sample_path')
=> 'https://www.infakt.pl/sample_path'

URI.join w dokumentacji Ruby

$LOAD_PATH - łatwiejsze ładowanie kodu

Przyzwyczajenie do znanego z Rails automatycznego ładowania całego kodu aplikacji przy jej starcie powoduje, że przy pracy z mikroserwisami, gdzie często cały kod aplikacji składa się z klikunastu plików może pojawić się problem z ładowaniem potrzebnych plików.

Pierwszym rozwiązaniem jakie przychodzi do głowy jest instrukcja require_relative umożliwiająca wskazanie ścieżki względnej w stosunku do pliku w którym ta instrukcja się znajduje. Fakt - wygodne, ale trzeba pilnować ilości ../ w ścieżce, co sprawia że bardzo łatwo o pomyłkę.

Z pomocą przychodzi globalna zmienna predefiniowana w Ruby - $LOAD_PATH - której wartość to tablica zawierająca ścieżki znane aplikacji, z których możemy ładować pliki. Wystarczy dodać do tej ścieżki główny katalog naszej aplikacji, i potem możemy już ładować pliki zawsze względem katalogu głównego. To znacznie ułatwia ustalenie właściwej ścieżki, i wydaje się też być bardziej naturalne.

boot.rb
app
  services
    user
      finder.rb
    
lib
  mapper.rb

Dla podanej struktury aplikacji, aby załadować plik lib/mapper.rb wewnątrz pliku app/servives/user/finder.rb należałoby użyć instrukcji:

 
# app/servives/user/finder.rb
require_relative '../../../lib/mapper.rb'

Po odpowiednim ustawieniu $LOAD_PATH w pliku startowym aplikacji (boot.rb) instrukcja ta staje się dużo prostsza:

 
# boot.rb
$LOAD_PATH.unshift File.expand_path('../', __FILE__)

# app/servives/user/finder.rb
require 'lib/mapper.rb'

Warto wiedzieć, że $LOAD_PATH jest tylko zdecydowanie czytelniejszym aliasem do innej globalnej zmiennej predefiniowanej - $:.