Bouygues Telecom
sbachelier
carlogren
Espace Client COnvergeant (ECCO pour les intimes)
Architecture Client / Serveurs "classique" SOA
PHP / Zend
Web Services SOAP/XML
Méthode Agile
SCRUM - Sprint 2 semaines
Janvier (POC)
Avril
Juin (Shift tech.)
Dream Team
Octobre (MEP)
Backbone.Marionette
Require.js
Workflow avancé avec Grunt
Tests unitaires avec Karma, Mocha, Chai, Sinon
Correspond à l'arrivée de nouvelles ressources
Plus que 3 mois pour la refonte avant MEP
Démo fonctionnelle en fin de sprint
Construction des ressources en // de l'application
Cohabitation avec certaines parties de l'espace client
Congés d'été
Intégration de marketing ciblé (Revive Ad Server)
Sécurité (HTTPS, SSO)
Intégration et interconnexion avec SalesForce
Exploitation de la nouvelle API REST
Cohabitation du nouvel espace client avec l'existant
Monitoring (Kibana)
Analytics (XiTi)
Cinq environnements principaux
Dev, Recette, Intégration, Pre-prod, Prod
Gestion des URLs différentes suivant les environnements
i18n : Une seule langue (FR)
Volonté de séparer tous les champs textes
Gestion effectuée par la MOA
Utilisation du format YAML
Automatisation avec des tâches Grunt
S'exécute avant le lancement de l'application
Un certain nombre de vérifications sont effectuées
Authentification
Test du profil client (redirection)
Participation à la beta
Initialisation de certaines données
Utilisation d'un SSO (Single Sign ON)
OAuth 2
JWT (JSON Web Token)
HTTPS !!!
Token généré et signé par le serveur
Décrypté côté client :
Librairie jsrasign
Contient un certain nombre de claims, dont la date d'expiration
Extraction des informations utilisateur
Récupération du token (URL ou sessionStorage)
Validation du token au lancement de l'application
Redirection vers la mire d'authentification si besoin
token non présent
token invalide
token expiré
Authentification des requêtes API
utilisation de l'access token renvoyé par l'OAuth
envoyé via un header HTTP
Détection des status code 401
redirect vers OAuth
REST ~ HATEOAS : attribut _links
GET /resource
{
"id": 1234,
"firstname": "Jean",
"lastname": "Dupont"
}
GET /resource
{
"id": 1234,
"firstname": "Jean",
"lastname": "Dupont",
"_links": {
"factures": { "href": "...." },
"lignes": { "href": "...." },
"adressesFacturation": { "href": "...." }
}
}
Avantages
API auto-découvrante
Un seul point d'entrée : la ressource personne
Inconvénients
Beaucoup d'appels
Un appel par ressource
GET uniquement
Choix délibéré d'implémentation
POST/PUT/DELETE
Opérations réalisées sur l'espace client
Avantages
Cycle de livraison plus rapide
Impact réduit sur la sécurité
Suit le cycle de livraison des ressources
Inconvénients
Rediriger l'utilisateur vers l'espace client actuel
Trouver une solution pour l'expérience utilisateur
Utilisation de Handlebars
Compilation des templates avec Grunt
Utilisation du runtime dans le navigateur
Nombreux helpers métiers
Génération d'un seul fichier templates.js
Customisation du Marionette.Renderer
Motivation :
utilisation de require.js
découplage vues / templates :
Utilisation d'une référence
// Vue avant
Mn.ItemView.extend({
template: templates['offre-ligne']
});
// Vue avant
Mn.ItemView.extend({
template: templates['offre-ligne']
});
// Vue après
Mn.ItemView.extend({
template: 'offre-ligne'
});
Implémentation simplifiée
var templates = require('templates');
Marionette.Renderer.render = function (name, data) {
return this.renderString(name, data);
};
Marionette.Renderer.renderString = function (name, data) {
if (!templates[name]) {
throw new Error('template [' + name + '] not found.');
}
return templates[name](data);
};
var templates = require('templates');
Marionette.Renderer.render = function (name, data) {
return this.renderString(name, data);
};
Centralisation de la distribution d'évènements
Meilleur découplage des différents composants/vues
Choix de Postal.js
Présence de channel
Wildcard (*, #) sur les topics
Utilisation d'une "enveloppe"
Cas d'utilisation : Backbone.history.navigate
handleLinkClick: function (e) {
var $target = $(e.currentTarget);
bus.publish('navigate', $target.attr('href'));
}
Marionette.AppRouter.extend({
initialize: function () {
this.subscriptions = [
bus.subscribe('navigate', _.bind(this.handleNavigate, this))
];
},
handleNavigate: function (path) {
// business logic ...
this.navigate(path, {trigger: true});
}
});
Génération de log depuis la SPA
Fonctionnel : Affichage des vues
Technique : Récupération des données
Stockage dans un Elastic Search
Visualisation avec Kibana
Intégration d'ECCO dans A360
Sous la forme :
d'un lien vers une autre page
ouverture d'une fenêtre (popup)
redirection vers une route interne
ouverture d'un bloc
Les liens vers l'espace client actuel (ECCO)
Page interne => IFrame
IFRAME
1
2
Injection d'un bloc HTML
=> IFrame
Problématique :
A360 est utilisé par différents profils
client
conseiller de clientèle (CDC)
conseiller de vente (CDV)
Problématique :
A360 est utilisé par différents profils
client
conseiller de clientèle (CDC)
conseiller de vente (CDV)
Chargement :
application web (client)
iframe dans salesforce (CDC)
webview dans application métier (CDV)
Lien vers ECCO dépend du profil de la personne
le lien
des paramètres à passer (query)
comportement d'affichage
iframe
bloc
window
target (blank, self)
ECCO :
~60 parcours utilisateurs à intégrer
Solution retenue
paramétrisation suivant le profil
paramétrisation des comportements
Utilisation de fichiers de configuration
gestion des paramètres
configuration des liens
configuration des routes internes
type:
iframe:
query: display=frame
target: iframe
Définition des types:
- lien interne / externe
- query d'un lien
- target d'un élément A
- attribut DOM injecté
type:
iframe:
query: display=frame
target: iframe
newpage:
target: _blank
dataTarget: a360
type:
iframe:
query: display=frame
target: iframe
newpage:
target: _blank
dataTarget: a360
self:
target: _self
dataTarget: a360
type:
iframe:
query: display=frame
target: iframe
newpage:
target: _blank
dataTarget: a360
self:
target: _self
dataTarget: a360
window:
query: display=frame
dataTarget: window
type:
iframe:
query: display=frame
target: iframe
newpage:
target: _blank
dataTarget: a360
self:
target: _self
dataTarget: a360
window:
query: display=frame
dataTarget: window
widget:
query: display=frame
dataTarget: widget
user:
client:
type: iframe
Comportement par défaut pour chaque profil
user:
client:
type: iframe
cdc:
type: window
query: login=:login
user:
client:
type: iframe
cdc:
type: window
query: login=:login
cdv:
type: iframe
query: login=:login
reinitialisercodeparental:
changementMDP:
reinitialisercodeparental:
query: ctype=p&no_reference=:contractId
reinitialisercodeparental:
query: ctype=p&no_reference=:contractId
user:
client:
changementMDP:
user:
cdc:
query: type=interne
changementMDP:
user:
client:
type: self
query: CallbackURL=${url.a360}
changementMDP:
user:
cdc:
query: type=interne
client:
type: self
query: CallbackURL=${url.a360}
reinitialisercodeparental:
query: ctype=p&no_reference=:contractId
user:
client:
type: widget
~60 liens à mettre en place
interne (router)
externe
3 types de profils principaux
soit ~180 boutons ?
=> il doit y avoir une solution plus simple et plus "propre"
<a {...}
data-link-type="external"
>
Changement mot de passe
</a>
<a {...}
data-link-type="external"
data-link="changementMDP"
>
Changement mot de passe
</a>
Mise en place du lien dans le template
<a {...}
data-link-type="external"
data-link="changementMDP"
>
Changement mot de passe
</a>
Manipulation de la balise A par le behaviour
<a {...}
data-link-type="external"
data-link="changementMDP"
href="<link>?ctype=p&display=frame&ctype=p&no_reference=1234"
target="_self"
>
Changement mot de passe
</a>
Marionette.ItemView.extend({
behaviors: {
},
});
Marionette.ItemView.extend({
behaviors: {
EccoButton: {
behaviorClass: behaviors['ecco-button']
}
},
});
Marionette.ItemView.extend({
behaviors: {
EccoButton: {
behaviorClass: behaviors['ecco-button']
}
},
eccoData: function() {
var contrat = this.options.contract;
return {
contractId: contract.id
};
}
});
Mise en place du behaviour
<a
{...}
data-link-type="external"
>
Réinitialiser code parental
</a>
<a
{...}
data-link-type="external"
data-link="reinitialisercodeparental"
>
Réinitialiser code parental
</a>
<a
{...}
data-link-type="external"
data-link="reinitialisercodeparental"
data-widget-target="reinitcode-widget"
>
Réinitialiser code parental
</a>
<div data-widget="reinitcode-widget"></div>
Injection d'une IFrame au travers d'une Mn.ItemView
Ajout des écouteurs sur les évènements :
onload
onresize
onmessage (et postMessage)
Non supporté par la propriété "events" d'une vue
"Sticky bar"
Menu contextuel commun à tous les sites BouyguesTelecom
=> Réécriture du lien une fois la barre chargée
Injection d'un script pour l'affichage du menu
Nécessité de modifier le lien
Problème
comment s'assurer que l'HTML est bien injecté pour pouvoir modifier le lien
Solution
MutationObserver
Début octobre
Ouverture progressive
CDC / CDV
3% des clients
Fin prévisionnelle de la beta en janvier 2016
Amélioration des performances
attente fortement réduite sur A360
pré-chargement < 0.5s (A360) contre ~2s (ECCO)
moins de données échangées
navigation beaucoup plus fluide