Psaní Angularjs the Elms Way

Nedávno, když jsem se připojil k Headspinu, měl jsem možnost pracovat výhradně v Javascriptu a AngularJS, s oběma jsem měl málo zkušeností. V Headspin se snažíme vyřešit jedinečný problém pro vývojáře aplikací – ladění mobilních aplikací přes globální sítě v reálném čase. Webové uživatelské rozhraní a datový panel jsou velmi důležitou součástí toho, co děláme.

V rámci učení se JavaScriptu a Angularjs mi však trvalo déle, než jsem chtěl, abych se zamyslel nad všemi rozsahy a stavy aplikace Angular, které byly posety všude v kódu a lze je zmutovat téměř odkudkoli. . Nakonec jsem napsal ošklivý JavaScript, na který jsem nebyl hrdý, a bylo méně zábavné ho číst. Začarovaný kruh se pro mě stále točil černou dírou jako jeho digest protějšek, jak jsem se více zapojil.

Konečně jsem cítil, že je čas přestat s tím, co dělám, než spadnu hlouběji do propasti a zamyslím se nad tím, co se pokazilo.

Začal jsem tím, že jsem určil některé z věcí, které mi překážely při učení a porozumění frameworku a také samotnému jazyku JavaScript. Přišel jsem s hrubým seznamem prádla:

  • neomezený povrch proměnlivosti
  • obousměrný tok dat
  • chybějící jasné hranice mezi ovladači a službami

O složitosti

Pro lidi je přirozené zjednodušovat, aby pochopili. Obecně špatně držíme krok se složitostí, natož s multitaskingem.

Když se člověk potýká se složitostí, je správnou věcí minimalizovat „plochu povrchu“, na které je nucen komunikovat s věcmi po ruce. Například ve filmu 300 , král Leonidas takticky zavedl svou malou skupinu tří set válečníků do úzké mezery mezi útesy a podařilo se mu zadržet miliony(?) perských vojáků. Bez ohledu na to, zda jde o skutečnost nebo fikci, je tato taktika minimalizace plochy útoku brilantní, ale zřejmá tváří v tvář složitosti nebo v našem případě řadě pohyblivých částí v kódu, které se snaží změnit stav aplikace.

Javascript, který je tak nahodilým funkčním jazykem, nedělá velkou práci při omezování mutací. Tento výsledek je to, co lze a často lze vidět v Angularjs nebo jakémkoli kódu Javascript:


class FooService {
  constructor() {
    this.state = "foo";
  }
  addBaz() {
    this.state = this.state + " baz";
  }
  addBar() {
    this.state = this.state + " bar";
  }
  _addBaz() {
    this.addBaz();
  }
  // this goes on ...
}

angular.module("Foo").service("FooService", FooService);


Je zřejmé, že je to velmi těžkopádné, ale nestydatě jsem to tak často dělal jen proto, abych věci dokončil a snil o refaktoru později, protože je tak snadné přidat další „zkratkovou“ metodu k dosažení toho, co chci.

Věci se mnohem zhorší, když vložíte službu do řadiče a dostanete se do nepříjemné situace, kdy musíte rozhodnout, kdo má na starosti správu stavu aplikace.


function FooController ($scope, FooService) {
  $scope.FooService = FooService;
  $scope.addBaz = () => {
    FooService.addBaz();

    // or you can do this
    // $scope.FooService.addBaz();

  }
}

angular.module("Foo").controller("FooController", FooController);


Později jsem se dozvěděl, že kontrolér by měl fungovat jako „dispečer“, zatímco službu lze považovat za trvalou vrstvu. To se však v AngularJS dostatečně neodráží ani nepodporuje. Je velmi snadné vytvořit tlustou službu, která dělá práci kontrolora, a vložit ji do kontroléru, který funguje pouze jako loutka.

Kde je například čára mezi ovladačem a službou? Kdy je vhodné vložit službu do řadiče a používat funkce řadiče jako API a kdy přímo použít instanci služby připojenou k rozsahu řadiče k volání jeho vlastních vnitřních metod? Jinými slovy, co nám brání udělat:


<div ng-controller="FooController">

  <!-- Using controller's service instance as API to state -->
  <button ng-click="FooService.addBaz()">Add Baz from Svc</button>

  <!-- INSTEAD OF-->

  <!-- Using controller's method as API to state -->
  <button ng-click="addBaz()">Add Baz from Ctrl</button>

</div>

nebo toto:


<div ng-controller="FooController">

  <!-- Using controller as a state container -->
  <p>{{state}}</p>

  <!-- INSTEAD OF -->

  <!-- Using the controller's service instance as container -->
  <p>{{FooService.state}}</p>

</div>

Začněte používat komponentu nyní

Od Angularjs 1.5 dále rámec zaváděl komponenty a podporoval jejich použití před směrnicemi. Komponenty mají méně funkcí a byly navrženy s izolovaným rozsahem a podporují jednosměrné datové vazby. Rozsah komponenty je vždy izolován od vnějšího světa a „vstupy“ jsou ovládány výhradně pomocí vazeb:


function FreeChildController () {
  this.inTheMood = false;
}
let FreeChildComponent = {
  controller: FreeChildController,
  bindings: {
   inlet: "<"
  },
  template: "<h1>{{$ctrl.inTheMood ? $ctrl.inlet : 'nanana'}}</h1>"
}

S tím je uzavřen rozsah ParentController může interagovat pouze jednosměrně prostřednictvím FreeChildComponent vázaný atribut inlet zatímco komponenta nemá žádné obchodní zasahování do vnějšího rozsahu.


<div ng-controller="ParentController as parent">
  <free-child inlet="parent.complaint"></free-child>  
</div>

Jilmova cesta

Jak jsem již zmínil, než jsem skočil do AngularJS, měl jsem možnost kódovat v Elm, reaktivním jazyce podobném ML, který se kompiluje do Javascriptu. Nejpozoruhodnější na něm je jeho architektura, která podporuje jednosměrný tok dat a velmi rozumný stavový cyklus. Tato architektura sama o sobě inspirovala Redux, státní kontejnerový doplněk dobře známý v komunitě React.

Elmova architektura se skládá ze tří částí – Model, Aktualizace a Zobrazení.

Model

Model je jediným zdrojem pravdy nebo stavu existující aplikace. V Elm je model často definován jako instance záznamu (podobně jako objekt v Javascriptu). Vzhledem k tomu, že Elm je čistě funkční jazyk, model se nikdy nezmutuje na místě. Každá aktualizace modelu vrátí novou instanci upraveného modelu a předá ji běhovému prostředí Elm (podobně jako cyklus digestu AngularJS).

Aktualizovat

Aktualizace je možná nejzajímavější částí aplikace Elm. Je to jediná funkce akceptující Msg typ a model jako argumenty, které odpovídají vzoru přijaté zprávy těm předdefinovaným v Msg Typ Unie a vrátit upravený model. Toto je jediná část, kterou se změní stav modelu.

Zobrazit

V Elmu nepíšete HTML značky. Elmovy pohledy jsou také jen čisté funkce, které přijímají model a vrací instanci Html a Msg , které se za běhu vykreslí do HTML DOM. Níže je uveden základní úryvek jednoduché aplikace počítadla v Elm.


main =
  beginnerProgram { model = 0, view = view, update = update }

view model =
  div []
    [ button [ onClick Decrement ] [ text “-” ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text “+” ]
    ]

type Msg = Increment | Decrement
update msg model =
  case msg of
    Increment -> model + 1
    Decrement -> model – 1


Je téměř čitelná bez jakékoli znalosti jilmu.
Existují i ​​jiné přístupy k dosažení podobného chování v JavaScriptu, ale Elm uspěl velmi elegantně díky designu samotného jazyka.

Restrukturalizace AngularJS

Než budu pokračovat, rád bych objasnil, že jde o názorový vzor. Tento vzor nemá být rámcem, modulem nebo dokonce pravidlem. To se může javascriptovým a Angular programátorům jevit jako nekonvenční, ale vzhledem k nové mysli, jako je ta moje, nemám nic jiného než silné nutkání zlepšit svůj vztah s Angular.

S tím, co bylo řečeno, zde je několik věcí, které bych s AngularJS udělal:

Model

  • Služba by se měla chovat jako velmi tenký obchod nebo stavový kontejner a měla by být vložena do řadiče, který funguje jako správce obchodu a poskytuje státu API.
  • Služba by měla vrátit uzavření konstruktoru úložiště namísto implicitního nastavení svého vnitřního stavu, aby bylo možné vložit možnost počátečního stavu a zpráv z řadiče nebo testu jednotky.
  • Stav služby by měl být aktualizován pouze pomocí update funkce v řadiči, které pošlou řetězec zpráv, který se má porovnat v objektu zpráv služby, a spustí příslušnou čistou funkci. To znamená, že ovladač obchodu obsahuje pouze jednu funkci.
  • Model by měl být jeden objekt – zdroj pravdy – seskupující všechny vlastnosti a aktualizován a vrácen jako celek.

// ES6 class
class StoreSvc {
  constructor () {
    return (initState, messageOpts) => {
      this.model = initState;
      this.messages = MessageOpts;
      return this;
    }
  }
}

app.module("myModule").service("StoreSvc", MyStore);


Kromě toho, že je snazší otestovat službu, zjistil jsem, že tento přístup také podporuje delegování úkolu iniciovat stát na nějakou jinou entitu. Nejdůležitější věcí, kterou je třeba poznamenat, je tento vzor, ​​díky kterému se služba stává velmi generickou vrstvou trvalého stavu s nulovou funkčností . To, co definuje každou službu, je objekt zpráv předaný během vytváření instance, o kterém rozhoduje kontrolér, který službu řídí. To znamená, jak aplikace interaguje se stavem, je na řadiči, který poskytuje popisné messages mapa. To se tak stane API pro aplikační model, které je drženo službou a řízeno kontrolérem.

Toto je příklad „připojení“ řadiče ke službě obchodu a poskytování API modelu:


function StoreController (StoreSvc) {

  // provide a starting model state 
  let model = { 
    name: "", 
    age: 0 
  };

  // provide a messages object aka API to the model
  let messages = {
    SetName : ((model, name) => Object.assign(model, {name: name})),
    SetAge  : ((model, age) => Object.assign(model, {age: age}))
  };

  // initiate a store
  this.store = StoreSvc(model, messages);
}


V messages objektu jsou klíče psány velkými písmeny záměrně, aby se odlišily od jiných klíčů objektů. Zde Object.assign se používá ke sloučení existujícího modelu s objektem obsahujícím vlastnost, která potřebuje aktualizaci, a vrácení klonu, což je funkční přístup oproti tradiční mutaci modelu.

Aktualizovat

  • Ovladač obsahuje pouze jednu funkci, a to update (může to být jakýkoli název), který odešle příslušnou zprávu ke spuštění čisté funkce v messageOpts , objekt mapující klávesy zpráv na funkce. Číslo update funkce je jediným místem v aplikaci zmutovat model služby .

  • Řadič zahájí počáteční stav modelu a mapování zpráv (nebo použije jinou službu k načtení dat, možná přes $http ) jejich vložením do konstruktoru služby.

  • V ideálním případě by se správce obchodu měl starat pouze o aktualizaci služby úložiště a neměl by se starat o správu DOM/komponenty. To by měl být úkol ovladače součásti.

Zde je základní update funkce může vypadat takto:


this.update = (message, model, ...args) => {
  if (message in this.store.messages) {
    this.store.model = this.store.messages[message](model, ...args);
  }
}

Zobrazit

  • Součásti jsou výrazně preferovány před příkazy.
  • V komponentě by akce řízená uživatelským rozhraním měla vždy volat vhodnou funkci vázanou na funkci aktualizace ovladače obchodu se správnou zprávou a argumenty.
  • Komponenta může interpolovat data v modelu z vazby správce obchodu.
  • Používejte pouze jednosměrné vazby (< ) pro vpuštění dat z rozsahu působnosti správce obchodu. Komponenta nemá za úkol měnit nic mimo sebe.
  • Obousměrné vazby, například ngModel by měl být používán s opatrností. V ukázkovém kódu je opuštěno ve prospěch sady ngKeydown , ngKeyup a $event.key .

Zde je návod, jak může komponenta vypadat:


let storeDashboard = {
  controller: myStoreController,
  bindings: {
    title: "<"
  },
  template: `
    <h4>{{$ctrl.title}}</h4>
    <ul>
      <li>
        {{$ctrl.store.model.name}}
        <input ng-model="$ctrl.store.model.name">
      </li>
      <li>
        {{$ctrl.store.model.age}}
        <button ng-click="$ctrl.update('SetAge', $ctrl.store.model, 0)">Reset</button>
      </li>
    </ul>
  `
}


Je také užitečné refaktorovat update funkce pro vrácení instance ovladače.


this.update = (msg, model, ...args) => {
  if (msg in this.store.messages) {
      let newModel = this.store.messages[msg](model, ...args);

      // model mutation happens here
      this.store.model = newModel;
    }
    return this;
  }
}

Nyní je možné zřetězit akce aktualizace v jediném volání direktivy v DOM:


<button type="button" 
        ng-click="$ctrl
                    .update('Decrement', $ctrl.store.model)
                    .update('Attach', $ctrl.store.model)">
  -
</button>


Zjednodušený kód =Předvídatelný stav

S tímto vzorem je mnohem snazší vysledovat, jak se model mutuje jako skupina stavů. Ovladač se stává velmi štíhlým, protože všechny místní funkce jsou refaktorovány a seskupeny do objektu zpráv jako čisté funkce a nechávají aktualizaci fungovat jako jediný neměnný povrch, takže se velmi snadno ladí. Maso aplikace je zhuštěno do messages objekt, mapa řetězců zpráv a pokud možno malé, samostatné čisté funkce, které vracejí nový objekt modelu.

Abychom to shrnuli, zde je jednoduchá aplikace čítače zobrazující tři části jako Model-View-Update. Snažil jsem se vyhnout ngModel místo toho pro jiné klíčové události, což sice zaostává, ale cítil jsem, že jsem pochopil, jak se vyhnout obousměrným vazbám).

Aplikace Counter

Tento ukazuje úplný vzor služby úložiště s řadičem poskytujícím rozhraní API, které ohraničuje rozsah řadiče komponenty a odesílá omezené hodnoty a funkce prostřednictvím vstupních vazeb komponenty.

Uživatelský panel

Závěr

Stojí za to znovu říci, že tento vzorec je pouze osobním průzkumem vyplývajícím z mých vlastních výhrad při práci s JavaScriptem a Angularjs a pokusu jej překonat.

Můžete získat kód z úložiště github (ještě však není dokončeno).

Původně publikováno zde.