Web flows w grails

Znowu lektura książki Definitive guide to grails przyśpieszyła. Tym razem bardzo długi rozdział, który przeczytałem bardzo szybko, o web flows w grails. Nie miałem wcześniej praktycznego do czynienia z przepływami (obieg i przepływ będę używał jako tłumaczenia flow). Wiedza jaką wyniosłem z tego rozdziału jest ogromna i żałuję, że wcześniej nie wziąłem się za grails. Jak Jacek Laskowski wielokrotnie wspominał nauka grails to także nauka wielu innych narzędzi i technologi. Ale do rzeczy.

Grails zapewnia wsparcie dla web flows opartego na spring web flow. Przepływy w grails są to serie stanów od rozpoczynające się od stanu początkowego a kończące na ostatnim, kolejno po sobie następujące. Niemożliwe jest wywołanie stanów nie po kolei. Spring Web Flow jest zaawansowaną maszyną stanów, flowExecutionKey i Id zdarzenia są przekazywane pomiędzy klientem a serwerem jako parametry w requescie, umożliwiając przejścia pomiędzy kolejnymi stanami.

W przeciwieństwie do Spring Web Flow, w grails nie jest wymagana żadna konfiguracja xml. Aby utworzyć obieg należy w kontrolerze utworzyć metodę, która kończy się na Flow (konwencja)

def shoppingCartFlow = {
...
}

Każdy przepływ ma swoje własne id, które ma nazwę taką jak ta metoda tylko, że bez słowa Flow (konwencja ponownie), czyli dla powyższego to będzie shoppingCart.

W przeciwieństwie do zwykłych akcji, akcja przepływu w ciele domknięcia nie definiują logiki a sekwencję stanów. Początkowym stanem, jest zawsze pierwszy stan zdefiniowany w przepływie.

def shoppingCartFlow = {
  showCart {
    on("checkout").to "enterPersonalDetails"
    on("continueShopping").to "displayCatalogue"
  }
...
}

Jeśli nastąpi zdarzenie checkout to wtedy przejdzie się do stanu enterPersonalDetails

Widok dla danego stanu znajduje się w grails-app/views/controllerName/flowId/state.gsp np
grails-app/views/store/shoppingCart/showCart.gsp

Stan końcowy to stan który nie przyjmuje parametrów lub jeden (w którym następuje przekierowanie do akcji lub innego obiegu).

Można zmienić nazwę widoku poprzez metodę render w stanie

showCart {
  render(view:"basket")
...
}

Istnieją stany akcji i stany widoku.
Stan widoku wstrzymuje wykonywanie obiegu w celu wyrenderowaniu widoku i interakcji z użytkownikiem. (np showCart wyżej ten z render jak i z on).
Stan akcji wykonuje blok kodu po, którym następuje przejście dalej. np

listAlbums {
  action {
    [ albumList:Album.list(max:10,sort:'dateCreated', order:'desc') ]
  }
  on("success").to "showCatalogue"
  on(Exception).to "handleError"
}

Obiegi posiadają swoje własne zasięgi i obiekty w nich muszą implementować java.io.Serializable:

  • flash – przechowuje obiekty dla obecnego i następnego żądania.
  • flow – przez cały czas przepływu, usuwając obieg kiedy zostanie osiągnięty ostatni stan.
  • conversation – dla danego obiegu i jego podobiegów.

Istnieją dwie opcje wywołania stanu z linku i z wysłania formy (submitt).

Z linka, gdzie action to id przepływu

<g:link controller="store" action="shoppingCart">My Cart</g:link>

tutaj zawsze zostanie utworzony nowy przepływ, a jeśli chcemy wywołać jakieś zdarzenie to zostaje dodany atrybut event

<g:link controller="store" action="shoppingCart" event="checkout">Checkout</g:link>

Użycie zdarzenia w formie wygląda trochę inaczej, zdarzenie jest w atrybucie name submitButton

<g:form name="shoppingForm" url="[controller:'store', action:'shoppingCart']">
...
  <g:submitButton name="checkout" value="Checkout" />
  <g:submitButton name="continueShopping" value="Continue Shopping" />
</g:form>

Aby zwalidować formularz w przepływie, to można przekazać go do stanu akcji. Jednak zalecane jest wywołanie akcji przejścia (transition action). Jeśli transition action się nie powiedzie to przejście jest zatrzymywane a stan przywracany.

enterPersonalDetails {
  on("submit") {
    flow.person = new Person(params)
    flow.person.validate() ? success() : error()
  }.to "enterShipping"
  on("return").to "showCart"
}

Aby utworzyć podobieg należy użyć subflow(nazwaObieguFlow)

wrappingOptions {
  subflow(chooseGiftWrapFlow)
  on('giftWrapChosen') {
    flow.giftWrap = conversation.giftWrap
  }
  on('cancelGiftWrap'). to 'enterShippingAddress'
}

Zdarzeniami tutaj są wszystkie końcowe stany przepływu chooseGiftWrapFlow.

Istnieje w requescie właściwość xhr która określa czy żądanie jest ajaxowe.

W przepływach można zastosować CommandObject.

W obiegu można dynamicznie tworzyć przejścia pomiędzy stanami:

on('back').to {
  def view
  if(flow.genreRecommendations || flow.userRecomendations)
    view = "showRecommendations"
  else if(flow.lastAlbum.shippingAddress) {
    view = 'enterShipping'
  }
  else {
    view = 'requireHardCopy'
  }
return view
}

W różny sposób można zapisać przejścia między stanami

on('back').to 'enterShipping' // static String name
on('back').to { 'enterShipping' } // Groovy optional return
on('back').to { return 'enterShipping' } // Groovy explicit return

wszystkie powyżej są równoważne.

Poprzez assert można sprawdzić czy stan jest w odpowiednim stanie (masło maślane – ale chodzi o to czy nie pojawił się jakiś błąd np przy zapisie do bazy, pola są zwalidowane itp).

Testowanie przepływów jest wykonywane za pomocą klasy grails.test.WebFlowTestCase i są to testy integracyjne. Przy wygenerowaniu testu należy pamiętać o podmianie klasy z której nasza klasa dziedziczy (z GroovyTestCase na WebFlowTestCase).
Następnie należy zaimplementować abstrakcyjną metodę getFlow, która zwraca domknięcie, które reprezentuje web flow np

def controller = new StoreController()
def getFlow() {
  controller.buyFlow
}

Przyjazne linki i grails

Dziś trochę krótszy rozdział książki Definitive guide to grails ale jednak wbrew pozorom z dużą ilością ciekawych informacji. Nie tylko programiście mają łatwiej z grails ale także seo i pozycjonowanie może mieć pewne korzyści przy mapowaniu url i tworzeniu przyjaznych linków.

Domyślne mapowanie url w grails jest kolejnym przykładem konwencji ponad konfigurację. Można zastosować własne mapowanie, które posiada wiele opcji ale nie wymaga żadnych xml tylko troszkę kodu w groovy.

Domyślne mapowanie składa się z obowiązkowego kontrolera, opcjonalnej akcji (jak jej nie ma to jest wywoływana domyślna) i opcjonalnego id obiektu.
/controller/action/id

Definicja mapowania znajduje się w grails-app/conf/UrlMappings.groovy
Do mapowania można dodawać kolejne opcjonalne elementy, które muszą się pojawić na końcu wzorca. W domyślnym mapowaniu każdy element jest zmienną, która zawiera prefix – $. Można dodać statyczny tekst, który będzie częścią url:

static mappings = {
  "/showAlbum/$controller/$action?/$id?" {
    constraints {
      // apply constraints here
    }
  }
}

W tym przypadku url mapuje /showAlbum/album/list. Bez /showAlbum to będzie niepoprawny adres.

Z url można usunąć nazwę kontrolera i akcji mapując je na specyficzną nazwę:

static mappings = {
  "/showAlbum/$id" {
    controller = 'album'
    action = 'show'
  }
}

Teraz pokazywanie danego albumu będzie posiadało url: /showAlbum/jakieśId

Grails dostarcza także inną składnię tego samego mapowania

static mappings = {
  "/showAlbum/$id"(controller:'album', action:'show')
}

Grails zapewnia obsługę standardowych parametrów http, np url /showArtist?artistName=Rush może zostać obsłużony jeśli mapowanie będzie zdefiniowane

static mappings = {
"/showArtist"(controller:'artist', action:'show')
}

Nie trzeba nigdzie tutaj dodawać dodatkowych parametrów.

W taki sposób można dodać wiele parametrów do url ale przy większej ilości staje się on coraz bardziej brzydki i nieczytelny. Grails zapewnia, że zamiast takiego mapowania url z parametrami /showArtist?artistName=Rush można wartość dodać jako część url /showArtist/Rush, wystarczy tylko zmapować ten parametr:

static mappings = {
  "/showArtist/$artistName"(controller:'artist', action:'show')
}

Taki parametr można przekazać w kontrolerze za pomocą params.parametrName

def artist = Artist.findByName(params.artistName)

Można dodawać dodatkowe parametry, które nie będą pokazane w url:

static mappings = {
  "/showArtist/$artistName"(controller:'artist', action:'show') {
    format = 'simple'
  }
}

Tutaj istnieje dodatkowy parametr format o wartości simple.

W url można zmapować wzorzec do określonego widoku.

static mappings = {
  "/"(view:'/welcome')
}

Tutaj jest mapowany root aplikacji (/) do widoku grails-app/views/welcome.gsp

Także można zmapować do widoku w konkretnym kontrolerze

static mappings = {
  "/find"(view:'query', controller:'search')
}

Tutaj url /find jest zmapowany do widoku query w kontrolerze search. W takim przypadku nie jest wywoływana żadna akcja kontrolera, tylko zostaje określona lokalizacja odpowiedniej strony gsp.

W mapowaniu url można używać ograniczeń podobnych a właściwe prawie takich samych (drobna różnica o czym za chwilę) jak ograniczenia w klasach domenowych.

static mappings = {
  "/grailsblogs/$year/$month/$day/$entry_name?" {
    controller = 'blog'
    action = 'display'
    constraints {
      year matches: /[0-9]{4}/
      month matches: /[0-9]{2}/
      day matches: /[0-9]{2}/
    }
  }
}

Parametr year musi być czterocyfrowym numerem, month i day dwu. Ograniczenia muszą się pojawić w takiej kolejności jak przekazane parametry.

Różnica między ograniczeniem w klasie domenowej a w mapowaniu url jest taka, że w klasie domenowej jest to pole którego przypisaną wartością jest domknięcie a w mapowaniu jest to metoda, która jako parametr przekazuje domknięcie. Czyli w constraincie w mapowaniu url nie jest potrzebny znak równości (właściwie przypisania =) a przy klasie domenowej on musi być.

W mapowaniu url w grails dozwolone są wildcardy, ich symbolem jest gwiazdka(*).

static mappings = {
  "/images/*.jpg"(controller:'image')
}

To mapowanie odnosi się do dowolnego pliku w katalogu image, który kończy się na jpg. Jeśli jest potrzeba aby odnosiło się do dowolnej ilości podkatalogów należy użyć podwójnej gwiazdki (**)

static mappings = {
  "/images/**.jpg"(controller:'image')
}

Można mapować do akcji HTTP

static mappings = {
  "/artist/$artistName" {
    controller = 'artist'
    action = [GET: 'show',
                PUT: 'update',
                POST: 'save',
                DELETE: 'delete']
  }
}

A także do kodów odpowiedzi HTTP

static mappings = {
  "404"(controller:'store')
}

Tag również potrafi obsługiwać mapowanie url. O ile nic nie zostanie zmienione w pliku UrlMappings.groovy to jego użycie w formie:

<g:link action='show'
  controller='artist'
  id="${artist.id}">${artist.name}</g:link>

utworzy link /artist/show/jakieśId
A jeśli chcemy aby ten tag obsługiwał przyjazne linki, które zdefiniujemy w pliku np w ten sposób:

static mappings = {
"/showArtist/$artistName"(controller:'artist', action:'show')
}

to wywołanie tagu nie wiele się różni od poprzedniego wywołania:

<g:link action='show'
  controller='artist'
  params="[artistName:${artist.name.replaceAll(' ', '_')}">${artist.name}
</g:link>

i tutaj do mapy parametrów jest przekazywany parametr artistName, na którego wartości została dokonana operacja zamiany stringów.

Można tworzyć własne klasy mapujące, które znajdują się w grails-app/conf/ i kończą się UrlMappings (konwencja). Ich struktura jest taka sama jak domyślnego pliku mapowania.

Chyba to nie będzie niespodzianką ale grails pozwala testować mapowanie url. Podstawową klasą jest grails.test.GrailsUrlMappingsTestCase a testy są integracyjne a nie jednostkowe jak dotychczas.

%d blogerów lubi to: