[pl]O wielkości kodu

English version

Dobry kod to krótki kod. To znaczy, jeśli możemy zaimplementować tą samą funkcjonalność w 100 liniach kodu, albo w 500, to wersja 100 linii będzie lepsza. Głównie dlatego, że krótsza = zawierająca mniej elementów = mniej złożona = prostsza do zrozumienia. A sytuacja, gdy programista nie wie co się dzieje w kodzie nie jest fajna. No i mniej kodu to mniejsza szansa na buga.

Ale niektórzy stosują podejście, które tylko pozornie zmniejsza ilość kodu.

Biblioteki

Jeśli zamiast napisać fragment kodu samodzielnie użyjesz biblioteki, masz mniej swojego kodu. Jednak kod wewnątrz biblioteki, to też jest kod, i uruchamia się on tak samo, jak kod napisany przez ciebie osobiście. Co więcej, tworząc publicznie dostępną bibliotekę autor musi dostosować ją do wielu różnych przypadków użycia i zaimplementować wiele funkcjonalności, których ty nie potrzebujesz.

Nie zrozumcie mnie źle, nie twierdzę, że używanie w kodzie zewnętrznych jest złe. Bardziej chodzi mi o sytuację, gdy mam działającą funkcję o długości 20-30 linijek, a inny developer każe mi ją skasować, a zamiast niej załadować zewnętrzną bibliotekę.

Ale biblioteka z dużym community ma mniejsze szanse, że w środku będzie bug.

Tak, ale za to pojawia się ryzyko, że biblioteka będzie robić coś, czego nie jesteś świadomy i to może doprowadzić do błędu.

Brałem udział kiedyś w jednym projekcie z użyciem JQuery. Czasami zdążała się taka sytuacja, że kliknięcie w jakiś przycisk wywoływało Exception wewnątrz samego jQuery, ale po odświeżeniu F5 problem znikał. Dużo czasu poświęciłem na znalezienie przyczyny.

Okazało się, że w innym widoku dodawałem event listener, ale zamiast metody handlera ustawiałem undefined. JQuery nie robił problemów z ustawieniem takiego listenera, ale rzucał exceptiona przy próbie obsłużenia, nawet jeśli byłem już na innym widoku i tamten event nie był mi potrzebny.

Ale biblioteka będzie lepiej napisana

Może tak, może nie. Trzeba pamiętać, że nie tylko senior ninja developerzy wrzucają projekty na githuba, więc to, że tam coś jest, nie znaczy od razu, że będzie lepsze.

Druga sprawa, że głównym celem tworzenia software jest by działał i robił to, co ma robić. To czy jest dobrze napisane jest tylko środkiem do osiągniecia celu, a nie celem samym w sobie.

Mikroserwisy

Osobiście jestem zdania, że mikroserwisy to architektura tylko dla najbardziej doświadczonych. Ja, mając ponad 5 lat doświadczenia komercyjnego, ciągle czuję się za mały do mikroserwisów.

Pokusą do użycia mikroserwisów może być, że zamiast dużego monolitu w którym jest dużo kodu, masz małe projekciki, które są krótkie i proste do zrozumienia. Problem polega na tym, że jeden mikroserwis to nie jest cały program. Co z tego, że będą oddzielone, na różnych serwerach, może w różnych językach programowania. Dopiero zbiór różnych serwisów tworzy coś użytecznego.

Za to dochodzi ci logika związana z komunikacją. Wysłanie requestu do innego serwisu będzie zawsze bardziej skomplikowane, niż zwykłe wywołanie metody w innej klasie.

Nie jestem przeciwnikiem mikroserwisów, ale jeśli chcesz je zastosować, powinieneś mieć lepszy powód. Jeśli chcesz tylko zachować porządek w kodzie, to podziel go sobie na klasy, namespace, może wydziel do osobnych bibliotek, ale uruchamiaj w ramach jednego procesu.

[pl] HTML vs XHTML – czym to się różni

English version

Dużo ludzi pisze kod w HTML, ale niewielu zgłębia ten temat. Jeśli nie wiesz, czym różni się <br> od <br/>, to ten artykuł jest dla ciebie.

Co było przed internetem?

SGML (Standard Generalized Markup Language) jest to format zapisu dokumentów tekstowych, w których oprócz samego tekstu są jeszcze dodatkowe informacje (tagi zwane też znacznikami). SGML stał się standardem ISO w 1986. Dokument SGML wygląda na przykład tak:

<tag1>
    Some Text
    <tag2 attribute otherattribute="value">
</tag1>

Standard ten definiuje składnię (ang. syntax) to jak dany dokument sparsować. Parser SGMLa wie, że mam tag o nazwie tag1 i że ma on dwoje dzieci: fragment tekstu i kolejny tag.

Ale standard nie definiuje co oznacza tag1 (tak zwana semantyka). Jest to język ogólnego przeznaczenia, stworzony jako baza dla innych języków jak na przykład

HTML

Upubliczniony w 1991 roku, HTML definiuje na przykład że <a> to link, a jego atrybut href zawiera adres url.

Poza HTMLem, sam SGML nie zdobył wielkiej popularności, dlatego niewiele ludzi nawet zna tą nazwę, ale osobiście uważam, że ważne jest by znać różnicę między składnią a semantyką.

XML i XHTML

W 1998 roku ukńczono pracę nad nowym standardem: Extensible Markup Language (XML). Jest to następca SGML’a, im tak samo jak on, zajmuje się tylko składnią dokumentu.

W 2000 roku W3C (organizacja standaryzująca) opublikowała specyfikację XHTML 1.0. Był to HTML 4.01, w którym składnia została podmieniona na XML. Oznacza to, że dokument XHTML jest 100% poprawnym dokumentem XML.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
 <head>
   <title>XHTML 1.0 Example</title>
 </head>
 <body>
   <p>
     This is an example
     <br/>
     of XHTML
   </p>
   <div id="empty"/>
 </body>
</html>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 strictl//EN"
  "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
 <head>
   <title>XHTML 1.0 Example</title>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
 </head>
 <body>
   <p>
     This is an example
     <br>
     of HTML
   </p>
   <div id="empty"></div>
 </body>
</html>

Oba dokumenty wyglądają dosyć podobnie, dlatego też wielu programistów nie zwraca na te szczegóły uwagi.

Pierwsza duża różnica jest taka, że XML nie toleruje błędów składni. SGML w sytuacji niepoprawnego zapisu spróbuje sparsować tyle ile jest w stanie, XML po prostu zwróci syntax error.

Każdy dokument XHTML zaczyna się od deklaracji XML, w której jest zapisana wersja samego XML’a oraz kodowanie znaków.

XML posiada mechanizm przestrzeni nazw (namespace), dzięki temu można mieszać różne formaty w jednym pliku. Na przykład możesz umieścić fragment XHTML’a wewnątrz pliku SVG albo RSS

Samozamykające się tagi

HTML posiada mechanizm automatycznego zamykania tagów. Na przykład tag <br> zawsze sam się zamyka i nie może mieć żanej treści. Inny przykład: tag <p> nie może zawierać w środku innego tagu <p>, więc gdy w kodzie pojawi się

<p>first
<p>second
<p>last</p>

paser wie, że otwarcie nowego <p> powinno automatycznie zamknąć wcześniejsze. Przy czym jak łatwo zauważyć, wymaga to,  by parser znał indywidualne zachowanie każdego tagu. W przypadku XHTML musisz zamykać wszystkie tagi ręcznie, tak aby każdy parser XML powinien przetworzyć plik poprawnie bez znajomości tych tagów. jeśli czegoś nie domkniesz, dostaniesz syntax error.

Ale żeby kod był bardziej czytelny, w XML istnieje skrócony zapis <br/>, który jest równoznaczny <br></br>. Ale ważne, by pamiętać, że w zwykłym HTML znak / na końcu tagu jest ignorowany (jak cześniej wspomniałem, HTML stara się ignorować błędy) i nic nie robi.

Więc jeśli w kodzie napiszesz:

<body>
  <div/>
  <p>Some text</p>
</body>

to jeżeli test to fragment XHTML’a, zostanie to potraktowane jako:

<body>
  <div></div>
  <p>Some text</p>
</body>

a wewnątrz HTML’a jako:

<body>
  <div>
    <p>Some text</p>
  </div>
</body>

Porzucony XHTML 2.0

Na samym początku XHTML 1.0 i 1.1 miały 2 duże problemy. Pierwszy: nie był wspierany przez Internet Explorera aż do wersji 9 (czyli roku 2011, 11 po wprowadzeniu standardu), a trzeba pamiętać, że w tamtych czasach od wydania nowej wersji, do momentu, gdy znaczna część użytkowników zaktualizowała przeglądarki potrafiły minąć kolejne lata. W takich warunkach nawet pisząc XHTML, pliki trzeba było serwować z content-type: text/html, tak aby IE sparsowalo to jako HTML, tym samym tracąc wszystkie zalety XML’a.

Drugi problem: programistom nie spodobała się idea, że syntax error spowoduje, że cała strona/aplikacja przestanie działać, a użytkownik zobaczy wielki komunikat o błędzie.

W3C pracowało nad specyfikacją XHTML 2.0, który miał zrywać z wsteczną kompatybilnością. Ten pomysł niezbyt podobał się developerom, więc w 2006 roku pomysł został porzucony. Ale wbrew temu, co niektórzy myślą, nie jest to koniec historii XHTML’a.

Teraźniejszość – HTML5

Po tym, jak W3C porzuciło XHTML 2.0, stworzono nowy standard – HTML 5. Jednym z głównych założeń było to, że HTML i XHTML będą rozwijane równolegle w ramach jednej specyfikacji. programista może wybrać, którą składnię woli, ale od momentu sparsowania przez przeglądarkę reszta jest już identyczna.

W3C zdecydowało, że HTML nie będzie już dalej pełni kompatybilne z SGML (bo komu to potrzebne). Wprowadzono też ficzery dotychczas znane tylko z XHTML’a, np. osadzanie MathML (wciąż nie wspierane przez Chrome 😡) czy SVG wewnątrz HTML’a.

Warto zauważyć, że nawet jeśli nie wpisujesz przestrzeni nazw (namespace) w kodzie, przeglądarka sama doda je w trakcie parsowania. Łatwo to sprawdzić z poziomu JavaScriptu:

var div = document.createElement('div');
div.innerHTML = '<p></p><svg></svg>';
console.log(div.children[0].namespaceURI);// -> "http://www.w3.org/1999/xhtml"
console.log(div.children[1].namespaceURI);// -> "http://www.w3.org/2000/svg"

Trzeba pamiętać, że gdy chcemy dynamicznie tworzyć elementy przez obiekty DOM musimy samemu podać przestrzenie nazw, inaczej tag nie zostanie potraktowany jako SVG, ale jako nieznany tag HTML, gdyż jak wspomniałem wyżej, po sparsowaniu przeglądarkarka traktuje HTML i XHTML identycznie (z wyjątkiem pola innerHTML, ale to wyjątek).

var badSvg=document.createElement("svg");
console.log(badSvg.namespaceURI);// -> "http://www.w3.org/1999/xhtml"
var goodSvg=document.createElementNS("http://www.w3.org/2000/svg","svg");
console.log(goodSvg.namespaceURI);// -> "http://www.w3.org/2000/svg"

[pl][video] Kurs technologii WebGL – #1 co to jest WebGL

Cześć, witam cię na kanale The Bugger, ja jestem Mateusz a to jest 1. odcinek kursu programowania w technologii WebGL. Jest to kurs przeznaczony dla programistów, więc wymagam od ciebie na starcie umiejętności programowania wmiarę sprawnie w języku Java Script i znajomości technologii webowych, ale nie wymagam od ciebie znajomości rzeczy związanych z grafiką, bo tego właśnie będziemy się tutaj uczyć.

Co będzie w tym kursie: będą przede wszystkim:

  • podstawowe zagadnienia związane z samą grafiką 3D, jak np. co to są siatki, co to są tekstury, jak się tworzy materiały itd.
  • podstawy matematyki i geometrii które są niestety niezbędne takie jak np. co to są wektory, operacje na wektorach
  • implementacja tego wszystkiego w Javascripcie

A więc co to jest WebGL? Jest to API dostępne z poziomu przeglądarki. Jest dostępne w języku Java Script. Wszyscy myślą, że umożliwia ono wyświetlanie grafiki 3D, co nie do końca jest prawdą, bo tak naprawdę daje nam tylko dostęp do GPU (karty graficznej), przy czym aby wyświetlić grafikę 3D musimy sami zadbać o obliczenia geometryczne itd.

Dlatego uprościmy sobie sprawę i skorzystamy z biblioteki. Taką biblioteką jest Three.js. Jest to biblioteka open source, dostępna na GitHubie. Ma 63 tys gwiazdek na GitHubie, więc patrząc na wszystkie biblioteki Javascriptowe, które są na GitHubie, jest na 8. miejscu, zaraz za Node.js, przed Material-UI. Jest to biblioteka dostępna na licencji MIT, więc można ją spokojnie wykorzystywać do celów komercyjnych.

Na swoim GitHubie przygotowałem demko. Można sobie ściągnąć to repozytorium. Jest to standardowy projekt na Webpacku, więc z konsoli wykonujemy najpierw yarn install, nastepnie yarn start, i na http://localhost:8080/ mamy kręcący się sześcian.

[pl] Jak poprawnie wysyłać obrazki w mailach

English version

Czy zdarzyło ci się kiedyś otworzyć maila, w którym tekst wyświetla się poprawnie, ale nie załadowały się obrazki? Wtedy twój program pocztowy wyświetla ci komunikat o tym, że obrazki zostały zablokowane i musisz kliknąć by je odblokować.

To częsty problem. Ba, ciężko znaleźć maile tego problemu pozbawione, w których obrazki ładują się poprawnie (no chyba że wcześniej dodałeś nadawcę do zaufanych). Jest to problem tak powszechny, że wiele ludzi, z którymi na ten temat rozmawiałem już go nie zauważają, albo uważają, że z tym się nie da nic zrobić i to jest normalne.

Ale odpowiedź na pytania czemu tak się dzieje i jak temu zaradzić jest stosunkowo prosta.

Powód

Maile są wysyłane w formie HTML, dlatego też programiści podchodzą do tworzenia szablonów maili jak do tworzenia zwykłych stron www – ale maile stronami www nie są.

Na normalnej stronie internetowej, aby dodać obrazek wstawiasz link do obrazka wewnątrz tabu <img>. Dla przeglądarki nie ma znaczenia czy obrazek jest na tym samym, czy na innym serwerze. Dlatego przy tworzeniu szablonów maili najprostsze wydaje się wrzucić obrazki na jakiś serwer i wstawić pełny url do <img> w mailu.

Tylko, że takie działanie jest świetną okazją dla spamerów. Jeśli ktoś rozsyła spam, chciałby wiedzieć, kto to w ogóle odczytał. Wtedy będzie wiedział, na które maile warto spam wysyłać, a które są np. przez nikogo nieużywane. W takiej sytuacji spamer może dodać do każdego maila <img> z unikalnym url-em, ale kierującym do jego serwera. Kiedy ofiara otworzy maila, aby wyświetlić obrazek program pocztowy musi połączyć się z serwerem spamera. Z tego właśnie powodu programy pocztowe blokują obrazki.

Rozwiązanie

Aby wysłać maila z obrazkiem musisz dodać go jako załącznik. To rozwiązuje problem, gdyż taki obrazek jest przesyłany razem z treścią maila i nie ma potrzeby odpytywania zewnętrznych serwerów.

Dodając załącznik należy nadać mu ContentID (cid), czyli identyfikator, po którym będzie można się odwołać wewnątrz html-a.

<img src="cid:your_content_id" alt="">

Nie zapomnij też zaznaczyć załącznika jako inline, dzięki czemu nie wyświetli się jako standardowy załącznik. Jak dokładnie to zrobić zależy od sposobu w jaki wysyłasz maile.

Alternatywnie mógłbyś użyć obrazka zakodowanego w base64 i url-a typu data:, ale nie jest to wspierane we wszystkich programach pocztowych, szczególnie w Microsoft Outlook. Jak wspomniałem wcześniej, mail nie jest stroną www i możesz wykorzystać w nim tylko ułamek możliwości przeglądarek internetowych, jeśli chcesz mieć pewność, że wszyscy odczytają twoje wiadomości poprawnie.

[pl] Javascript tricks #1 – rozszerzanie prototypów

English version

Javascriptowe tablice mają kilka metod, które pozwalają operować na danych wg. podejścia funkcyjnego (np. filtermapreduce). 

const input=[
    {name: "John", points: 5},
    {name: "Bob", points: 0},
    {name: "Alice", points: 4}];
const result = input.filter(x=>x.points>0).map(x=>x.name);
console.log(result);// => ["John", "Alice"]

Przy czym są to tylko podstawowe metody, nie mamy tak dużych możliwości jak np. biblioteka LINQ w C#.

Istnieją biblioteki do JS o większych możliwościach. Przykładem niech będzie Lodash:

var _ = require('lodash');

const input=[
    {name: "John", points: 5},
    {name: "Bob", points: 0},
    {name: "Alice", points: 4}];
const result = _.map(_.filter(input, x=>x.points>0),x=>x.name); 
console.log(result);// => ["John", "Alice"]

Lodash daje nam dość dużo metod do wykorzystania, jednak tych metod nie wykonujemy na tablicy, tylko tablicę przekazujemy jako parametr metody. Różnica może niewielka, ale gdy chcemy wywołać metody jedna pod drugiej (jak w przykładach powyżej) kod robi się nieczytelny: nie wykonuje się od lewej do prawej, tylko niejako od środka na zewnątrz.

Extensions

W C# jest coś takiego jak „extension method” (pozwolę sobie nie tłumaczyć na siłę na „metody rozszerzające” jak robi to oficjalna dokumentacja tylko zostanę przy oryginalnej nazwie). Jest to sposób na dodanie metody do klasy bez modyfikacji tej klasy.

public static class A{
    public static string MyMethod(this List<string> a){
        return "Hello, world!";
    }
}

//...

var animals = new List<string>{"cat", "dog", "lion"};
Console.WriteLine(animals.MyMethod()); // => "Hello, world!"

W JS można zrobić coś bardzo podobnego (ale nie identycznego, o czym później). Javascript wykorzystuje dziedziczenie oparte na prototypach, więc wszystkie tablice to obiekty dziedziczące z obiektu Array.prototype. Jest to obiekt wbudowany w przeglądarkę, ale możesz do niego przypisywać nowe property i metody jak do każdego zwykłego obiektu, a zmiany będą widoczny we wszystkich obiektach które po nim dziedziczą (nawet tych utworzonych wcześniej).

const animals = ["cat", "dog", "lion"];
Array.prototype.myMethod=function(){return "Hello, world!";}
console.log(animals.myMethod());// => "Hello, world!"

I w tym momencie doświadczeni programiści Javascriptu prawdopodobnie uznają mnie za jakiegoś heretyka. W tym momencie warto wspomnieć czym różni się to podejście od extensionów w C#. Otóż extension w C# znajduje się w jakimś namespace i jest widoczny tylko wtedy, gdy ten namespace w danym pliku załadujemy. Za to edycja prototypów w JS działa globalnie (na wszystkie tablice wewnątrz danego window, niezależnie w jakim pliku jesteśmy). Ma to swoją nazwę: zanieczyszczenie prototypów (ang. prototype pollution).

Dlatego stosuj tą technikę ostrożnie. Jeśli piszesz projekt w kilka osób: uzgodnij z całym zespołem jeśli chcesz namieszać w globalnych prototypach. Odradzam też robienie tego gdy piszesz bibliotekę dla kogoś.

Jakie metody można sobie dopisać?

Zaprezentuję tutaj kilka użytecznych metod które warto dopisać, wraz z implementacją i przykładami użycia. Stworzyłem bibliotekę npm-ową która je zawiera (ale zachęcam cię do pisania tego typu metod samodzielnie)

npm i prototype-extensions
# LUB
yarn add prototype-extensions

sum

Pierwsza z metod po prostu dodaje wszystkie elementy tablicy do siebie, zwracając liczbę.

const array=[10, 2, 5, 0.5];
console.log(array.sum());// => 17.5

Często jednak nie mamy gołych liczb, ale całe obiekty. Dlatego wygodnie jest móc przekazać funkcję, która wyłuska nam z obiektu liczbę, którą chcemy.

const work=[
{worket: 'Alice', workedHours:8, pricePerHour:20},
{worket: 'Bob', workedHours:10, pricePerHour:15},
{worket: 'John', workedHours:2, pricePerHour:30},
];
const totalCost = work.sum(x=>x.workedHours * pricePerHour);
console.log(totalCost);// => 370

implementacja

if (!Array.prototype.sum) {
    Array.prototype.sum = function (fun = x => x) {
        return this.reduce((sum, item) => sum + Number(fun(item)), 0);
    }
}

Pierwszy if jest żeby się upewnić, że nie nadpisujemy jakiejś już istniejącej metody, którą mógł dodać ktoś inny (lub jeśli pojawi się nowa metoda w samej przeglądarce)

W drugiej linii tworzymy nową funkcję, którą przypisujemy do prototypu tablic (Array.prototype). Jako parametr funkcji przekazujemy funkcję (wiem jak to brzmi, ale trochę na tym polega programowanie funkcyjne) która zamieni nam element tablicy na liczbę. Domyślnym parametrem jest funkcja która zwraca to co przyjęła (bo chcemy, żeby w sytuacji, gdy nie podamy żadnego parametru, były brane elementy tablicy tak jak są).

Jeśli ten zapis jest dla ciebie niezrozumiały oto linia nr 2 w wersji bardziej rozpisanej:

Array.prototype.sum = function (fun) {
    if(!fun){
        fun = function(input){
            return input;
        }
    }
//...

sortBy

Sortowanie danych to dosyć podstawowa operacja. Jednak wbudowana w JS metoda sort wymaga podania mody, która porównuje dwie zmienne ze sobą i zwraca, która z nich jest mniejsza. Dużo wygodniejsze byłoby sortowanie po konkretnej wartości (lub kilku wartościach), która jest liczbą.

const people=[
    {name:'Anna', age:18},
    {name:'Bob', age:25},
    {name:'John', age:17},
];
people.sortBy(x=>x.age);
console.log(people);// => [
                    // =>     {name:'John', age:17},
                    // =>     {name:'Anna', age:18},
                    // =>     {name:'Bob', age:25},
                    // => ];

Implementacja

if (!Array.prototype.sortBy) {
    Array.prototype.sortBy = function (...args) {
        let orders = args.map(x => typeof x == 'function' ? x : y => y[x]);
        let compareFunction = (a, b) => {
            for (let order of orders) {
                let valueA = order(a);
                let valueB = order(b);
                if (valueA > valueB)
                    return 1;
                else if (valueA < valueB)
                    return -1;
            }
            return 0;
        };
        return this.sort(compareFunction);
    }
}

Zakładam, że być może programista chciały posortować po kilku wartościach (jeśli pierwsza wartość jest identyczna posortuj po kolejnej), dlatego użyłem zapisu …args (z parametrów funkcji robi nam tablicę) a następnie pętli for..of.

Dodałem też możliwość, aby nie tylko przekazać parametrem funkcję, ale też nazwę property w formie stringa (dlatego użyłem metody map)

min i max

Jak znaleźć największy lub najmniejszy element tablicy? Można tablicę posortować, a potem wziąć z niej pierwszy/ostatni element, ale to podejście jest dosyć słabe z punktu widzenia wydajności. Dużo wydajniej jest mieć metodę która znajduje jeden element.

const array1 = [5,10,2.2,-30];
console.log(array1.min()); // => -30
console.log(array1.max()); // => 10

W programowaniu obiektowym rzadko spotykamy tablice zawierające same liczby. Raczej mamy całe obiekty, które mają w sobie jakieś wartości liczbowe:

const people=[
    {name:'Anna', age:18},
    {name:'Bob', age:25},
    {name:'John', age:17},
];
console.log(people.min(x=>x.age));// => {name:'John', age:17}

Implementacja

if (!Array.prototype.max) {
    Array.prototype.max = function (fun = x => x) {
        let value = null;
        let object = null;
        for (let item of this) {
            const itemValue = fun(item);
            if (typeof itemValue === 'number' && !isNaN(itemValue) && (value == null || itemValue > value)) {
                value = itemValue;
                object = item;
            }
        }
        return object;
    }
}

groupBy

Czasem przydaje się pogrupować obiekty po jakiejś wspólnej wartości:

const people=[
    {name:'Alice', type:'teacher'},
    {name:'Bob', type:'student'},
    {name:'John', type:'student'}
];
const grouped = people.groupBy(x => x.type);
console.log(grouped); // => Map(2){
                      // =>  "teacher" => [{name:'Alice', type:'teacher'}],
                      // =>  "student" => [{name:'Bob', type:'student'}, {name:'John', type:'student'}]
                      // => }

Można spytać, dlaczego użyłem Map zamiast zwykłego Object? Dlatego że w Map kluczem może być wszystko, co daje nam większą elastyczność, gdzie przy zastosowaniu Object kluczem może być tylko String i Symbol.

Implementacja

if (!Array.prototype.groupBy) {
    Array.prototype.groupBy = function (fun = x => x) {
        const ret = new Map();
        for (const value of this) {
            const key = fun(value);
            if (ret.has(key))
                ret.get(key).push(value);
            else
                ret.set(key, [value]);
        }
        return ret;
    }
}

Przyszłość

W specyfikacji ECMAScript Next zaproponowano nowy operator :: (https://github.com/tc39/proposal-bind-operator), który byłby dużo lepszym rozwiązaniem niż rozszerzanie prototypów, ale w tym momencie jest określany jako stage 0, czyli nie jest nawet pewne czy i kiedy zostanie dodany do specyfikacji JavaScriptu, a nawet jeśli, minie dużo czasu zanim przeglądarki go zaimplementują.

Linki

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

[pl] Promisy w Vue – jak poprawnie komunikować się z api

English version

W tym artykule zakładam, że wiesz jak działają promisy w JS. Jeśli nie, zachęcam do przeczytania artykułu na MDN.

Spójrz na poniższy component Vue i spróbuj znaleźć w nim buga:

<template>
    <div>
        <button @click="load(1)" :class="{active:active===1}">Show first article</button>
        <button @click="load(2)" :class="{active:active===2}">Show second article</button>
        <p v-if="loader">Loading...</p>
        <p v-else>{{content}}</p>
    </div>
</template>
<script>
export default {
    data(){
        return {content:null, active:null, loader:false}
    },
    methods:{
        load(id){
            this.active=id;
            this.loader=true;
            fetch(`/article/${id}`)
                .then(response=>response.json())
                .then(json=>{
                    this.content=json;
                    this.loader=false;
                });
        }
    }
};
</script>
<style>
    button.active{
        color:red;
    }
</style>

Dosyć prosty komponent, zawiera 2 przyciski. Klikając na jeden z nich włączamy metodę load, która ładuje treść danego artykułu z api restowego. Na czas ładowania wyświetlany jest <p> z napisem „Loading…”. Zaznacza też kliknięty przycisk na czerwono (klasa active) żeby było widać który artykuł jest wyświetlany. Tak prosty komponent – czy może coś pójść nie tak?

Pomyślmy chwilkę, co się stanie, gdy klikniesz jeden z przycisków, a zaraz potem drugi? Albo w ogóle zaczniesz klikać w te dwa przyciski na zmianę tak szybko jak potrafisz? Nic szczególnego. Nie znaczy to że wszystko jest w porządku – najprawdopodobniej twoje restapi działa na localhoście, więc odpowiedź z serwera przyjdzie szybciej niż zdążysz nacisnąć kolejnego buttona.

Ale nawet jeśli spowolnisz sobie sieć (np. w devtoolsach Chrome jest taka możliwość) to i tak nic się nie wydarzy – restapi odpowie wolniej, ale i tak odpowiedzi zejdą w tej samej kolejności w jakiej zostały kliknięte.

Ale jest możliwe, szczególnie przy słabym internecie lub przy bardzo obciążonym serwerze, że odpowiedzi z serwera przyjdą w innej kolejnośc. W tym momencie zmienna content, która została przypisana wewnątrz then nie będzie się zgadzać z active, która była przypisana poza then. W tej sytuacji wyświetlona treść artykułu nie będzie się zgadzać z tytułem (podświetlonym buttonem)

Możesz pomyśleć: skoro zazwyczaj działa, to wszystko jest w porządku, nie ma co się przejmować. Ale to bardzo złe podejście. W pewnym momencie ten bug przydarzy się twojemu klientowi, więc on odezwie się do ciebie, że trzeba to naprawić. Wtedy ty uruchamiasz aplikację i odpowiadasz najpopularniejszym zdaniem ludzi z branży IT: „dziwne, u mnie działa”.

Najgorszy rodzaj buga to taki, który przydarza się tylko czasem i nie jesteś w stanie go powtórzyć.

Mój sposób jak sprawdzić czy nasza aplikacja nie ma bugów związanych z asynchronicznością, to na czas testów po stronie backendu przy każdym wywołaniu requesta dodać sleep na losową liczbą sekund, dzięki czemu odpowiedzi z serwera będą zawsze losowe.

Jak to naprawić

Jest wiele sposobów jak się zabezpieczyć. Najprostszym jest zablokować przyciski na czas ładowania, tak żeby nie można było wykonywać dwóch requestów jednocześnie. Jednak jest to mało user-friendly i jak dla mnie to bardziej maskowanie problemu niż jego naprawianie.

Drugie podejście to sprawdzić, czy dany request jest wciąż aktualny:

        load(id){
            this.active=id;
            this.loader=true;
            fetch(`/article/${id}`)
                .then(response=>response.json())
                .then(json=>{
                    if(this.active!=id) return;//id changed, so ignore
                    this.content=json;
                    this.loader=false;
                });
        }

W tym przykładzie sprawdzamy, czy id się zmieniło. Możemy tak zrobić, jeśli zakładamy, że gdy odpytamy API kilka razy o ten sam id, to zawsze otrzymamy tą samą odpowiedź. Jeśli nie możemy tak założyć, użyjmy typu Symbol:

        load(id){
            const loadSymbol = Symbol();
            this.loadSymbol = loadSymbol;
            this.active=id;
            this.loader=true;
            fetch(`/article/${id}`)
                .then(response=>response.json())
                .then(json=>{
                    if(this.loadSymbol != loadSymbol) return;//loadSymbol changed, so ignore
                    this.content=json;
                    this.loader=false;
                });
        }

Jednak ciągle nie byłem zadowolony z tego rozwiązania, więc wymyśliłem coś lepszego.

PromiseStatus

Popatrz na tą klasę:

import {Enum} from 'enumify-fork';

export default class PromiseStatus {
    constructor(promise = null) {
        this.promise = promise;
    }

    set promise(promise) {
        this.data = null;
        this.error = null;
        this.status = PromiseStatus.Status.noPromise;
        this._promise = null;
        if (promise) {
            this._promise = promise;
            this.status = PromiseStatus.Status.pending;
            promise.then(data => {
                if (this._promise === promise) {
                    this.data = data;
                    this.status = PromiseStatus.Status.resolved;
                }
            }, error => {
                if (this._promise === promise) {
                    this.error = error;
                    this.status = PromiseStatus.Status.rejected;
                }
            })
        }
    }

    get promise() {
        return this._promise;
    }
}

PromiseStatus.Status = class extends Enum {
};
PromiseStatus.Status.initEnum(['noPromise', 'pending', 'resolved', 'rejected']);

Tworzysz obiekt tej klasy przekazując jej promise. W zamian za to klasa dostarcza ci property o nazwie status. Jest to enum, który przyjmuje wartość pending (gdy dane są wciąż ładowane), resolved (gdy dane zostały załadowane poprawnie) lub rejected gdy wystąpił błąd. Dodatkowo property data i error zawierają wartości zwrócone odpowiednio przez resolve i reject.

Ale zapytasz, to czym to się różni od normalnego .then().catch() lub await (użycie async/await zmienia mocno strukturę kodu, ale działanie jest takie samo jak then)? Odpowiedź brzmi: tych property możesz użyć bezpośrednio w <template>, bez użycia dodatkowych zmiennych (co upraszcza kod, ale też zabezpiecza przed tym, że się rozjadą).

Jak tego użyć?

<template>
    <div>
        <button @click="load(1)" :class="{active:active===1}">Show first article</button>
        <button @click="load(2)" :class="{active:active===2}">Show second article</button>
        <p v-if="content.status === Status.pending">Loading...</p>
        <p v-else-if="content.status === Status.resolved">{{content.data}}</p>
        <p v-else-if="content.status === Status.rejected">Error while loading article</p>
    </div>
</template>
<script>
import PromiseStatus from 'reactive-promise-status';

export default {
    data(){
        return {content:new PromiseStatus(), active:null, Status:PromiseStatus.Status}
    },
    methods:{
        load(id){
            this.active=id;
            this.content.promise = fetch(`/article/${id}`).then(response=>response.json());
        }
    }
};
</script>
<style>
    button.active{
        color:red;
    }
</style>

Z poziomu szablonu możemy sprawdzić czy dane się już załadowały w v-if. Samą treść wyciągniemy z content.data. W momencie gdy dane zostaną załadowane widok odświeży się sam.

Warto zauważyć, że zmienna content jest przypisywana w momencie kliknięcia, a nie w momencie załadowania danych, przez to uodparniamy się na problemy z kolejnością odpowiedzi z serwera.

Linki

Jeśli chciałbyś użyć w swoim projekcie ta klasa jest dostępna w npm/yarn. Link do githuba: https://github.com/matrix0123456789/reactive-promise-status

yarn add reactive-promise-status
LUB
npm install reactive-promise-status