Date post: | 29-Jan-2018 |
Category: |
Technology |
Upload: | andrzej-krzywda |
View: | 1,712 times |
Download: | 0 times |
Single Page Applications
Andrzej Krzywda
Use cases with CoffeeScript, DCI, AOP, TDD
Kto lubi pisać w JavaScript?
Agenda
• Frontend - Zmiana sposobu myślenia
• Single Page Application
• Frontend - architektura
Zmiana sposobu myślenia
Dumny programista backendów
“JavaScript? My zajmujemy się
poważnymi projektami”
(2007 - 2008)
Rails consulting since 2007
“OK, zrobimy autocompletion”
“Znajdźmy jakiś plugin railsowy, który to
generuje i nie patrzmy do środka”
“Czy ceny mogą się zmieniać bez przeładowania?”
“Renderujmy JavaScript po stronie serwera”
“Pokazywanie słówek bez przeładowania”
Backendy - RubyFrontendy - Coffee
Social games for brands
(gierki i konkursy na FB i strony firmowe)
“Gra w kółko i krzyżyk, potem wybór
nagrody”
Hybryda
Pierwsza wersja naszej platformy tak działała.
Platforma GameBoxed==
jeden backend, wiele frontendów
market
Gdzie renderujemy html?
Cały html na frontendzie, serwer tylko zwraca JSON.
HTML na frontendzie nie różni się od HTML
na backendzie
<p>W tej sesji gry niesamowite umiejętności strzeleckie pozwolily Ci osiągnąć nowy rekord:<br> <b>{{playerMaxScore}} punktów.</b></p>
<a class="button ok_button">DALEJ</a>
handlebar
Rewolucja
Frontend to osobna aplikacja
Market 2.0
Pusher
web sockets
Pusher
Zmiana sposobu myślenia
• Faza 1: No JavaScript
• Faza 2: JQuery explosion
• Faza 3: Page/Widget object
• Faza 4: Single Page Application
W której fazie jest Twój projekt?
Single Page Application
Gmail, Twitter, Facebook, Trello
CoffeeScriptbetter JS
Class oriented language
Same zalety
JS == assembler
Coffee > Ruby
underscore.js
GameBoxed używa tylko Coffee do
frontend’ów
Polecamy!
Single Page Application
Architektura
Trygve
MVC, DCI
Zapomnijmy o MVC z backendu!
Nie ma MVC na backendzie
Nie ma View na backendzie
Prawdziwe MVC(w uproszczeniu - zmiana w modelu
powoduje automatyczną zmianę w GUI)
(Rails, Struts, Spring - nie są MVC!)
GUI - model
Modele• Game
• Player
• GameSession
• Round
• Prize
• Friend (Invitation)
• Life (LifeRequest...)
• Team
Views
• PrizeComponent
• FriendsComponent
• GameArea
• Popups
Pierwsze podejście
Popupy
• Wyświetl popup z zespołem gracza
• Po naciśnięciu OK, wyświetl popup z nagrodami
Callbacks
triggerActionsAfterMove: (move, callback) => @getBonusWhenStartCrossed(move) @getBonusWhenLandedOnCellWithFriends(@board.currentCell()) @pickCardAndNotifyIfAny(@board.currentCell(), callback)
pickCardAndNotifyIfAny: (cell, callback) => console.debug "pickCardAndNotifyIfAny #{cell.position}" card = @drawCard(cell) if not card console.debug "no card picked" callback?() return console.debug "card found: #{card.identifier}" card.onPicked(@) if card.onPicked? @eventBroker.trigger("player:picked_card:#{card.identifier}", card, callback) @eventBroker.trigger("player:picked_card", card)
class engine.monopoly.controllers.CardItemBargainContoller constructor: (@services, @game) -> @helper = new CardHelperForUsecases(@services)
setup: => @services.eventBroker.bind('player:picked_card:CardItemBargain', @execute)
execute: (card, callback) => @popup = @helper.showCardGenericPopupAndBindOnOK( (=> @applyFormDataToBargain(card, callback)), null) @popup.bind("popup:opened", => @popup.find('input').focus())
applyFormDataToBargain: (card, callback) => offer = @popup.find('input').val() new CardItemBargain(@services, @game).execute(card, offer, callback)
Eventy
class engine.shooter.components.StageResultWon
constructor: (@eventBroker) -> _.extend(@, Backbone.Events) super() @templateId = "stage_result_won"
addMeToScreen: (root, me) => $("#gameArea").append(me)
configureElement: (me) => me.find('.okButton').mousedown (event) => @hide() @eventBroker.trigger('stage:result:shown')
class engine.shooter.models.Game
constructor: (@serverSide, @eventBroker) -> super(@eventBroker) @levels = [] @guns = []
registerEvents: (eventBroker) => eventBroker.bind('game:start:requested', @start)
eventBroker.bind("player:clicked:inside-target", @playerTriggeredShotInsideTarget) eventBroker.bind("player:clicked:magazine:reload", @playerWantsToReload)
eventBroker.bind("stage:start:clicked", @startStageClicked) eventBroker.bind('countdown:stage:finish', @finishCurrentStage)
eventBroker.bind("stage:result:shown", @loadNextStageOrFinishGame)
Wymagana duża dyscyplina
Gdzie jest główne flow?
Use casesUsecaseController
DCI
Game Designer
Piotrek
Tomek - programista
(praca zdalna w praktyce)
class engine.invite_and_win.GameUseCase constructor: (@game, @player) -> ObjectHelper.addRole(@player, engine.shared.models.PlayerWithFriends)
@facebookHQ = new engine.invite_and_win.FacebookHQ()
tryToEnterGameArea: () => if @amIEnteringGameFirstTime() if @amICommingFromInvitation() @tellPlayerHeIsPartOfTeam(@facebookHQ.friendsInviting)
@teachPlayerHowToPlay()
else #n-th time... if @amICommingFromInvitation() @tellPlayerHeIsPartOfTeam(@facebookHQ.friendsInviting)
if not @playerLikesFanpage() @askPlayerToLikeFanpage()
if @haveNotYetPickedFavPizzaCountry() @askPlayerToDeclareHisFavCountry()
Use case używa dziedzinę
Ani use case, ani dziedzina, nie wiedzą
nic o GUI
Ani use case, ani dziedzina, nie wiedzą
nic o persistence
Use case’y mogą działać z innym GUI i innym
persistence
Gdzie jest DCI?
Data Context Interaction
Trygve
Dane pozostają w obiektach
Data
Obiekty są dosyć “cienkie”
class engine.shared.models.Player constructor: () -> @rank = null @maxScore = 0
Use case to zachowanieContext
Obiekty mają wstrzykiwane role
Interaction
Role są dodawane runtime!
class engine.invite_and_win.GameUseCase constructor: (@game, @player) -> ObjectHelper.addRole(@player, engine.shared.models.PlayerWithFriends)
tryToEnterGameArea: () => if @amIEnteringGameFirstTime() if @amICommingFromInvitation() @tellPlayerHeIsPartOfTeam(@facebookHQ.friendsInviting)
@teachPlayerHowToPlay()
else #n-th time... if @amICommingFromInvitation() @tellPlayerHeIsPartOfTeam(@facebookHQ.friendsInviting)
if not @playerLikesFanpage() @askPlayerToLikeFanpage()
if @haveNotYetPickedFavPizzaCountry() @askPlayerToDeclareHisFavCountry()
rola
OOP != COP
class engine.shared.models.PlayerWithFriends extends Mixin setup: => @friends = [] @invitedFriends = [] @acceptedFriends = []
setInvitedFriends: (facebookUids) => for facebookUid in facebookUids friend = new Friend({facebookUid: facebookUid}) @invitedFriends.push(friend)
setFriends: (friends) => @friends = friends
addFriend: (friend) => existing = @getFriendByFacebookUid(friend?.facebookUid) if not existing? @friends.push(friend)
class engine.invite_and_win.GameGuiConfiguration constructor: (@gameUseCase, @game, @gui, @services, @sharedComponents) -> execute: () => Around(@gameUseCase, 'tryToEnterGameArea', @checkFbInvitation) After (@gameUseCase, 'tryToEnterGameArea', @showTeamArea) After (@gameUseCase, 'tryToEnterGameArea', @showButtonInviteOrPostPicture) Around(@gameUseCase, 'tellPlayerHeIsPartOfTeam', @showTeamPopup) Around(@gameUseCase, 'askPlayerToLikeFanpage', @showLikePopup) Around(@gameUseCase, 'teachPlayerHowToPlay', @showTutorialPopup) Around(@gameUseCase, 'playerWantsToKnowWinnersWithPrize', @showWinnersPopup) Around(@gameUseCase, 'playerWantsToKnowPrizes', @showPrizesPopup) Around(@gameUseCase, 'askPlayerToDeclareHisFavCountry', @showDeclareCountryPopup) Around(@gameUseCase, 'iAcceptMyFriendInvitationToATeam', @onIAcceptMyFriendInvitationToATeam)
Around(@gameUseCase, 'tellPlayerHeIsPartOfTeam', @showTeamPopup)
showTeamPopup: (proceed, friendsInviting) => data = {inviting_friends: friendsInviting} popup = @popupsComponent.showPopup('team_popup', data) popup.bind('popup:closed', => proceed(friendsInviting))
Persistence
ServerSide
class engine.shared.server.ServerSide constructor: (@gameBasicDetails) -> @gameEngineUrl = "/engine/games/#{@gameBasicDetails.id}" @gameUrl = "/games/#{@gameBasicDetails.id}" @errors = []
gameDetailsLoaded: (gameDetails, callback) => callback(gameDetails)
fetchGameDetails: (callback, errback) => $.ajax( type: "GET" url: "#{@gameEngineUrl}.json" success: (gameDetails) => @gameDetailsLoaded(gameDetails, callback) error: errback )
My ładujemy dane na starcie
Można ładować w trakcie
Testowanie
scenario "player enters and has no friends", -> @player.enterGame(@playerWithNoFriends) @player.shouldSeeMainAreaWithInviteButton() @player.shouldSeeRemainingFriendsToCompleteTeam(4)
scenario "player enters and has collected part team", -> @player.enterGame(@playerWith3AcceptedFriends) @player.shouldSeeMainAreaWithInviteButton() @player.shouldSeeRemainingFriendsToCompleteTeam(1)
scenario "player enters and has collected whole team", -> @player.enterGame(@playerWith4AcceptedFriends) @player.shouldSeeMainAreaWithPostToWallButton()
TDD
Acceptance tests
with test.ServerSide
Reużycie?
Jak reużyć kod po obu stronach?
Nie wiem.
Czy Google spozycjonuje SPA?
Tak.Ale trzeba renderować
html po stronie serwera
Frameworks
My nie potrzebujemy
Ty prawdopodobnie też nie potrzebujesz.
Krytyka Backbone.js
powiązanie danych z widokami
a gdzie logika biznesowa?
Backbone Models
Przydatne do API, ale nie używajmy ich jako
dziedziny
Relations/objects mismatch
Resources/objects mismatch
Libraries - yesFrameworks - no
Bardzo ważne pytanie
A może Twój projekt powinien być
SinglePageApp?
Co jest lepsze dla użytkowników?
tl;dr
• Frontend to osobna aplikacja
• Używaj CoffeeScript
• Nie używaj frameworków
• Pisz use case’y w kodzie
• Poczytaj o DCI
• Twórz fajne frontendy
Dziękuję!@andrzejkrzywda
http://andrzejkrzywda.com
Pytania?
(ostatnio sporo bloguję o frontendach)