Single Page Applications with CoffeeScript [Polish]

Post on 29-Jan-2018

1,712 views 0 download

transcript

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)