Play2 ou l'architecture web réactive

1,299 views

Published on

Technical presentation about Zaptravel.
Confoo 2013 - February - Montreal - Canada

By Nicolas Martignole, Principal Engineer at Zaptravel.

Published in: Travel

Play2 ou l'architecture web réactive

  1. 1. @nmartignole Nicolas Martignole Février 2013 ZapTravelvendredi 22 mars 13
  2. 2. L’objectif de cette présentation est de... Découvrir Play2/Scalavendredi 22 mars 13
  3. 3. ZapTravelvendredi 22 mars 13
  4. 4. ZapTravelvendredi 22 mars 13
  5. 5. vendredi 22 mars 13
  6. 6. vendredi 22 mars 13
  7. 7. ZapTravelvendredi 22 mars 13
  8. 8. Framework Web ZapTravelvendredi 22 mars 13
  9. 9. Java et Scala ZapTravelvendredi 22 mars 13
  10. 10. Créé par Guillaume Bort @guillaumebort ZapTravelvendredi 22 mars 13
  11. 11. Rails pour Java/Scala ZapTravelvendredi 22 mars 13
  12. 12. Simple, productif Sauvegardez, rechargez, c’est tout ZapTravelvendredi 22 mars 13
  13. 13. Communauté Java ZapTravelvendredi 22 mars 13
  14. 14. vendredi 22 mars 13
  15. 15. 2 Livres en préparationvendredi 22 mars 13
  16. 16. Démonstration ZapTravelvendredi 22 mars 13
  17. 17. Routes GET / Application.index ZapTravelvendredi 22 mars 13
  18. 18. Scala def index() = Action { val name ="Nicolas" Ok(views.html.Application.index(name)) } ZapTravelvendredi 22 mars 13
  19. 19. Scala - 2 @(name: String) @myTemplate() { <h1>Hello @name</h1> ... } ZapTravelvendredi 22 mars 13
  20. 20. Play 2 en bref ZapTravelvendredi 22 mars 13
  21. 21. Play 2 en bref • RESTful • Sécurité (XSS,CSRF) • Compilateur • Java NIO CoffeScript, Less • Driver asynchrone • JSON pour MongoDB • Websocket, Server • Require.js Sent Event, Comet • NoSQL et BigData ZapTravelvendredi 22 mars 13
  22. 22. Quelques exemples ZapTravelvendredi 22 mars 13
  23. 23. Charger une donnée venant de Redis ZapTravelvendredi 22 mars 13
  24. 24. Architecture Redis Air/Hotel/Cars/Ac Redis Web Resa/Users Web Redis HTTP Web Content HTTPS LB Web ZapTravelvendredi 22 mars 13
  25. 25. Architecture Redis Air/Hotel/Cars/Ac Web Redis Web Resa/Users Web Redis HTTP HTTPS redis Web Content LB Web ZapTravelvendredi 22 mars 13
  26. 26. Cas d’usage Donne moi le label qui correspond à originId =380 ZapTravelvendredi 22 mars 13
  27. 27. Cas d’usage Donne moi le label qui correspond à originId =380 def getSlug(originId: Long): Option[String] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)) } ZapTravelvendredi 22 mars 13
  28. 28. Cas d’usage Donne moi le label qui correspond à originId =380 def getSlug(originId: Long): Option[String] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)) } ZapTravelvendredi 22 mars 13
  29. 29. Cas d’usage Donne moi le label qui correspond à originId =380 def getSlug(originId: Long): Option[String] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)) } Driver Sedis https://github.com/pk11/sedis ZapTravelvendredi 22 mars 13
  30. 30. Un mot sur les Testsvendredi 22 mars 13
  31. 31. package models   import org.specs2.mutable._   import play.api.test._ import play.api.test.Helpers._   class OriginSpecs extends Specification { "An Origin" should { "returns the slug for a valid origin" in { running(FakeApplication()) { Origin.getSlug(380) mustEqual Some("from-london") Origin.getSlug(1) mustEqual Some("from-paris") Origin.getSlug(-9999) mustEqual None } } } } https://gist.github.com/nicmarti/5064048vendredi 22 mars 13
  32. 32. Charger un objet Charge moi un Objet «Londres» ZapTravelvendredi 22 mars 13
  33. 33. Charger un objet Origine 1) charger from-london def getOrigin(originId: Long): Option[Origin] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)).map{ slug=> Option(client.hget("Places:Place:"+originId, "display").map { .... ... } } } ZapTravelvendredi 22 mars 13
  34. 34. Cas d’usage 2) charger display... def getOrigin(originId: Long): Option[Origin] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)).map{ slug=> Option(client.hget("Places:Place:"+originId, "display").map { .... ... } } } ZapTravelvendredi 22 mars 13
  35. 35. Cas d’usage 2) charger display... def getOrigin(originId: Long): Option[Origin] = Redis.pool.withClient { client => for(slug<-Option(client.hget("Url:From:Rev", originId.toString)); display<-Option(client.hget("Places:Place:"+originId, "display") )) yield Origin(originId,display,slug) } } for-comprehension https://gist.github.com/nicmarti/5064066 ZapTravelvendredi 22 mars 13
  36. 36. La Tour Eiffel 1. Charger du JSON à partir de Redis 2. Interpréter et retourner un objet PointOfInterest ZapTravelvendredi 22 mars 13
  37. 37. HGET Pois:PoisHash 52511 {"name":"Eiffel Tower","address":"","latitude":"48.8582493546","longitude":"2.2945117950","website":"www.tour- eiffel.fr","rank":3,"photo":{"r":"eiffel-tower-paris-france","k":"6b56","e":"jpg","w":2406,"h": 1600,"a":"Mirari Erdoiza","l":"http://www.fotopedia.com/items/anboto-RiKxAA3gE6I"},"sentences": {"gbs":[{"d":"The Eiffel Tower is one of the most famous monuments in the world (324 metres, 10,100 tonnes).","a":"Paris","l":"http://www.paris.com/paris_landmarks/monuments/ eiffel_tower_paris"},{"d":"This is without doubt one of the most recognizable structures in the world.","a":"Frommers","l":"http://www.frommers.com/destinations/paris/A25288.html"},{"d":"If the Statue of Liberty is emblematic of New York, Big Ben is London, and the Kremlin is Moscow, then the Eiffel Tower is the symbol of Paris.","a":"Fodors","l":"http://www.fodors.com/world/europe/ france/paris/review-97417.html"},{"d":"When it was built for the 1889 Exposition Universelle (World Fair), marking the centenary of the Revolution, the Tour Eiffel faced massive opposition from Paris artistic and literary elite.","a":"Lonely Planet","l":"http://www.lonelyplanet.com/france/paris /sights/famous-landmark/eiffel-tower"}],"tips":[{"d":"Its pretty high!.","a":"annawelford","l":"http://www.lonelyplanet.com/france/paris/sights/famous- landmark/eiffel-tower","s":"Lonely Planet"},{"d":"Bigger than you think.","a":"anomolly","l":"http:/ /www.lonelyplanet.com/france/paris/sights/famous-landmark/eiffel-tower","s":"Lonely Planet"},{"d":"Overcrowded.","a":"anshjain","l":"http://www.lonelyplanet.com/france/paris/ sights/famous-landmark/eiffel-tower","s":"Lonely Planet"},{"d":"The restaurant on the first floor is an amazing experience!.","a":"ansofie","l":"http://www.lonelyplanet.com/france/paris/sights /famous-landmark/eiffel-tower","s":"Lonely Planet"}]},"tags":["Landmark","Memorials/ Monuments","Sights","Famous landmark"]}vendredi 22 mars 13
  38. 38. Play 2.0 • Définir une case class POI • Définir un Format[POI] • Ecrire la fonction pour lire et parser le JSON Note : Play 2.1 apporte un nouveau parser JSON plus simplevendredi 22 mars 13
  39. 39. Play 2.0 case class POI(name: String, address: String, latitude: String, longitude: String, website: Option[String], photo: Option[SightPhoto] = None, sentences: Sentences, tags: Option[List[String]]) POI = Point of Interest = notre Tour Eiffelvendredi 22 mars 13
  40. 40. Play 2.0vendredi 22 mars 13
  41. 41. Appel Redis et interprétation JSONvendredi 22 mars 13
  42. 42. Afficher une liste ZapTravelvendredi 22 mars 13
  43. 43. Afficher une liste ZapTravelvendredi 22 mars 13
  44. 44. Aller sur Redis def allOrigins: List[Origin] = Redis.pool.withClient { client => // ... // ... } Modèlevendredi 22 mars 13
  45. 45. Préparer une liste def allUrlOrigins: Seq[(String, String)] = { Origin.allOrigins.map{ origin => (origin.slug, origin.label) }.sortBy(_._2) } Contrôleurvendredi 22 mars 13
  46. 46. Envoyer la liste au template Code dans la page HTML <label for="location">Your travel origin is :</label> @select( userForm("originCity"), FolioCriteria.allUrlOrigins , _label -> "Travel from origin", _showConstraints -> false ) Vuevendredi 22 mars 13
  47. 47. Afficher une liste ZapTravelvendredi 22 mars 13
  48. 48. Gérer l’authentificationvendredi 22 mars 13
  49. 49. Comment protéger l’accès à une ressource ? My Infovendredi 22 mars 13
  50. 50. Comment protéger l’accès à une ressource ? My Infovendredi 22 mars 13
  51. 51. Dans le Controller object Application extends Controller { def index = Action { implicit request => val username="test" Ok(html.index(username)) } }vendredi 22 mars 13
  52. 52. Dans le Controller object Application extends Controller with Secured { def index = ActionSecure { username => implicit request => Ok(html.index(username)) } }vendredi 22 mars 13
  53. 53. trait Secured { def username(request: RequestHeader) = request.session.get(Security.username) def onUnauthorized(request: RequestHeader) = Results.Redirect(routes.Auth.login) def ActionSecure(f: => String => Request[AnyContent] => Result) = { Security.Authenticated(username, onUnauthorized) { user => Action{ request => f(user)(request) } } } } Result String Request[AnyContent] HTMLvendredi 22 mars 13
  54. 54. Play2 et Sécurité • Simple • Composable • Facile à testervendredi 22 mars 13
  55. 55. Optimiser l’indexation et le référencementvendredi 22 mars 13
  56. 56. Indexation et référencement • URLs propres et pondérées • Mots clés • Liens et Sitemap • Microformat (Hotel, Avion, Lieux) • Contenu non répétévendredi 22 mars 13
  57. 57. vendredi 22 mars 13
  58. 58. routes Compilé et validévendredi 22 mars 13
  59. 59. URL /from-boston/quality GET /$origin<from-(.*)>/:classifier controllers.Frontoffice.home(origin:String, classifier: String) http://www.zaptravel.com/romance/weekend-deals/from-paris/to-athens/ 12-Apr-2013-to-14-Apr-2013/elite-athens-greecevendredi 22 mars 13
  60. 60. Play2 • La séparation entre la partie routage et la partie contrôleur permet de créer des URLs «propres»vendredi 22 mars 13
  61. 61. Sitemap • Déclarer la table des matières de son site • Optimise le référencement • Permet de mettre en cache les pages curl http://www.zaptravel.com/sitemap.xmlvendredi 22 mars 13
  62. 62. vendredi 22 mars 13
  63. 63. Problème : construire le sitemap de façon asynchronevendredi 22 mars 13
  64. 64. Solution : Async Akka / Play2vendredi 22 mars 13
  65. 65. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }vendredi 22 mars 13
  66. 66. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ... // some other code val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards) ).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }vendredi 22 mars 13
  67. 67. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }vendredi 22 mars 13
  68. 68. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }vendredi 22 mars 13
  69. 69. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, Bref... seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } curl http://www.zaptravel.com/sitemap.xml Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }vendredi 22 mars 13
  70. 70. Gestion du cachevendredi 22 mars 13
  71. 71. Comment améliorer les performances ?vendredi 22 mars 13
  72. 72. Eviter de recharger la même page, utilisez code 304 NotModified Note: @rosstuck a fait une session sur HTTP à Confoo mercredi derniervendredi 22 mars 13
  73. 73. Exemple sur /from-paris/quality Navigateur Play2 GET /from-paris/qualityvendredi 22 mars 13
  74. 74. Exemple sur /from-paris/quality Navigateur Play2 OK HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 ETag: 11299930771 Cache-Control: max-age=600, s-maxage=600, must-revalidate Content-Length: 103586 ... ... ce n’est pas une erreurvendredi 22 mars 13
  75. 75. Recharge /from-paris/quality GET /from-paris/quality If-None-Match: 112999307771 Navigateur Play2 304 Not Modified Content-Length: 0vendredi 22 mars 13
  76. 76. Optimisation 1 • Evitez de faire travailler votre serveur pour rien • Déterminez des ETags «métiers» • Attention à la gestion du cache et des serveurs mandataires.vendredi 22 mars 13
  77. 77. Optimisation 2 Faire de la gestion de cache applicativevendredi 22 mars 13
  78. 78. Cache applicatif ?vendredi 22 mars 13
  79. 79. 2 types de cache Cache technique type Cache de Play2 ou Redis Varnish - Code applicatif - Process à part - utilise la mémoire - Cache HTTP de Play2 ou Redisvendredi 22 mars 13
  80. 80. 2 types de cache Cache technique type Varnish • Facile à installer • Evite de solliciter Play2 • Scalable • Configurablevendredi 22 mars 13
  81. 81. 2 types de cache Cache applicatif Play2/ Redis • Prend en compte le métier • Permet de garder les pages «authentifiées» • Pas aussi performant que la solution Varnishvendredi 22 mars 13
  82. 82. Sur Zaptravel • Page d’accueil optimisé avec Cache de Play2 • Page Folio, section top Deal avec cache Play2 • Page Deal, cache avec Redisvendredi 22 mars 13
  83. 83. Et pour terminer Play2 Architecture Web Apprentissage Tests unitaires Asynchrone (Enumeratee, Iteratee)vendredi 22 mars 13
  84. 84. Merci https://joind.in/7951 @nmartignolevendredi 22 mars 13

×