Résumé
Découvrez comment nous avons utilisé Polymer pour créer un WebGL hautes performances, contrôlé par les appareils mobiles Sabre laser modulaire et configurable. Nous examinons certains détails clés de notre projet https://lightsaber.withgoogle.com/ pour vous faire gagner du temps lorsque vous créerez le vôtre la prochaine fois que vous rencontrerez des Stormtroopers en colère.
Présentation
Si vous vous demandez ce que sont les composants Polymer ou WebComponents, il serait préférable de commencer par partager un extrait d'un projet opérationnel réel. Voici un exemple tiré de la page d’accueil de notre projet https://lightsaber.withgoogle.com. Il est un fichier HTML standard, mais qui contient un peu de magie:
<!-- Element-->
<dom-module id="sw-page-landing">
<!-- Template-->
<template>
<style>
<!-- include elements/sw/pages/sw-page-landing/styles/sw-page-landing.css-->
</style>
<div class="centered content">
<sw-ui-logo></sw-ui-logo>
<div class="connection-url-wrapper">
<sw-t key="landing.type" class="type"></sw-t>
<div id="url" class="connection-url">.</div>
<sw-ui-toast></sw-ui-toast>
</div>
</div>
<div class="disclaimer epilepsy">
<sw-t key="disclaimer.epilepsy" class="type"></sw-t>
</div>
<sw-ui-footer state="extended"></sw-ui-footer>
</template>
<!-- Polymer element script-->
<script src="scripts/sw-page-landing.js"></script>
</dom-module>
Beaucoup de possibilités s'offrent à vous aujourd'hui pour créer une application HTML5. API, frameworks, bibliothèques, moteurs de jeu, etc. Malgré tous les choix, il est difficile de trouver une configuration qui soit un bon mix entre contrôle des hautes performances des graphismes et modulaire épurée la structure et l'évolutivité. Nous avons constaté que Polymer pouvait nous aider à organiser le projet tout en permettant des optimisations de performances de bas niveau. Nous avons donc soigneusement conçu la façon dont nous avons décomposé notre projet en composants afin de tirer le meilleur parti des fonctionnalités de Polymer.
Modularité avec Polymer
Polymer est une bibliothèque qui offre de nombreuses possibilités de contrôle de la façon dont votre projet est créé à partir d'éléments personnalisés réutilisables. Il vous permet d'utiliser des modules autonomes et entièrement fonctionnels contenus dans un seul fichier HTML. Ils contiennent non seulement la structure (balisage HTML), mais aussi la logique et les styles intégrés.
Examinez l'exemple ci-dessous:
<link rel="import" href="bower_components/polymer/polymer.html">
<dom-module id="picture-frame">
<template>
<!-- scoped CSS for this element -->
<style>
div {
display: inline-block;
background-color: #ccc;
border-radius: 8px;
padding: 4px;
}
</style>
<div>
<!-- any children are rendered here -->
<content></content>
</div>
</template>
<script>
Polymer({
is: "picture-frame",
});
</script>
</dom-module>
Mais pour un projet plus vaste, il peut être utile de séparer ces trois (HTML, CSS, JS) et ne les fusionner qu'au moment de la compilation. Nous avons donc attribué un dossier distinct à chaque élément du projet :
src/elements/
|-- elements.jade
`-- sw
|-- debug
| |-- sw-debug
| |-- sw-debug-performance
| |-- sw-debug-version
| `-- sw-debug-webgl
|-- experience
| |-- effects
| |-- sw-experience
| |-- sw-experience-controller
| |-- sw-experience-engine
| |-- sw-experience-input
| |-- sw-experience-model
| |-- sw-experience-postprocessor
| |-- sw-experience-renderer
| |-- sw-experience-state
| `-- sw-timer
|-- input
| |-- sw-input-keyboard
| `-- sw-input-remote
|-- pages
| |-- sw-page-calibration
| |-- sw-page-connection
| |-- sw-page-connection-error
| |-- sw-page-error
| |-- sw-page-experience
| `-- sw-page-landing
|-- sw-app
| |-- bower.json
| |-- scripts
| |-- styles
| `-- sw-app.jade
|-- system
| |-- sw-routing
| |-- sw-system
| |-- sw-system-audio
| |-- sw-system-config
| |-- sw-system-environment
| |-- sw-system-events
| |-- sw-system-remote
| |-- sw-system-social
| |-- sw-system-tracking
| |-- sw-system-version
| |-- sw-system-webrtc
| `-- sw-system-websocket
|-- ui
| |-- experience
| |-- sw-preloader
| |-- sw-sound
| |-- sw-ui-button
| |-- sw-ui-calibration
| |-- sw-ui-disconnected
| |-- sw-ui-final
| |-- sw-ui-footer
| |-- sw-ui-help
| |-- sw-ui-language
| |-- sw-ui-logo
| |-- sw-ui-mask
| |-- sw-ui-menu
| |-- sw-ui-overlay
| |-- sw-ui-quality
| |-- sw-ui-select
| |-- sw-ui-toast
| |-- sw-ui-toggle-screen
| `-- sw-ui-volume
`-- utils
`-- sw-t
Et le dossier de chaque élément a la même structure interne avec des des répertoires et des fichiers pour la logique (fichiers Coffee), les styles (fichiers Scss) et (fichier Jade).
Voici un exemple d'élément sw-ui-logo
:
sw-ui-logo/
|-- bower.json
|-- scripts
| `-- sw-ui-logo.coffee
|-- styles
| `-- sw-ui-logo.scss
`-- sw-ui-logo.jade
Et si vous examinez le fichier .jade
:
// Element
dom-module(id='sw-ui-logo')
// Template
template
style
include elements/sw/ui/sw-ui-logo/styles/sw-ui-logo.css
img(src='[[url]]')
// Polymer element script
script(src='scripts/sw-ui-logo.js')
Vous pouvez voir comment les éléments sont organisés de manière claire en incluant des styles et une logique provenant de fichiers distincts. Pour inclure nos styles dans nos éléments Polymer, nous utilisons l'instruction include
de Jade. Nous obtenons ainsi le contenu réel du fichier CSS intégré après compilation. L'élément de script sw-ui-logo.js
va
au moment de l'exécution.
Dépendances modulaires avec Bower
Normalement, nous conservons les bibliothèques et autres dépendances au niveau du projet.
Cependant, dans la configuration ci-dessus, vous remarquerez un bower.json
qui se trouve dans le
dossier de l'élément: dépendances au niveau de l'élément. L'idée derrière cette approche
est que dans une situation où vous avez
de nombreux éléments avec différentes
dépendances, nous pouvons faire en sorte de
charger uniquement les dépendances qui sont
réellement utilisé. Si vous supprimez un élément, vous n'avez pas besoin de vous souvenir de supprimer sa dépendance, car vous aurez également supprimé le fichier bower.json
qui déclare ces dépendances. Chaque élément charge indépendamment
les dépendances qui s’y rapportent.
Toutefois, pour éviter la duplication des dépendances, nous incluons un fichier .bowerrc
.
dans le dossier de chaque élément. Cela indique à
bower où stocker
afin de s'assurer qu'il n'y en a qu'une à la fin
répertoire:
{
"directory" : "../../../../../bower_components"
}
De cette façon, si plusieurs éléments déclarent THREE.js
comme dépendance, une fois
Bower l'installe pour le premier
élément et commence à analyser le second,
il détecte que cette dépendance est déjà installée et n'effectue pas
téléchargez-le à nouveau ou dupliquez-le. De même, il maintient cette dépendance
à condition qu'au moins un élément le définisse
son bower.json
.
Un script bash recherche tous les fichiers bower.json
dans la structure des éléments imbriqués.
Ensuite, il entre dans ces répertoires un par un et exécute bower install
dans
chacun d'entre eux:
echo installing bower components...
modules=$(find /vagrant/app -type f -name "bower.json" -not -path "*node_modules*" -not -path "*bower_components*")
for module in $modules; do
pushd $(dirname $module)
bower install --allow-root -q
popd
done
Modèle "Nouvel élément rapide"
Cela prend un peu de temps chaque fois que vous souhaitez créer un élément : générer le dossier et la structure de fichiers de base avec les noms appropriés. Nous utilisons donc Slush pour écrire un générateur d'éléments simple.
Vous pouvez appeler le script à partir de la ligne de commande:
$ slush element path/to/your/element-name
Le nouvel élément est créé, y compris l'ensemble de la structure et du contenu des fichiers.
Nous avons défini des modèles pour les fichiers d'éléments, par exemple le modèle de fichier .jade
se présente comme suit:
// Element
dom-module(id='<%= name %>')
// Template
template
style
include elements/<%= path %>/styles/<%= name %>.css
span This is a '<%= name %>' element.
// Polymer element script
script(src='scripts/<%= name %>.js')
Le générateur Slush remplace les variables par les chemins et les noms réels des éléments.
Utiliser Gulp pour créer des éléments
Gulp garde le contrôle du processus de compilation. Et dans notre structure, pour construire les éléments dont nous avons besoin de Gulp pour suivre les étapes suivantes:
- Compiler les fichiers
.coffee
des éléments en.js
- Compiler les éléments
.scss
fichiers dans.css
- Compiler les éléments
.jade
dans.html
, intégrant les fichiers.css
.
Plus en détail:
La compilation des éléments .coffee
fichiers dans .js
gulp.task('elements-coffee', function () {
return gulp.src(abs(config.paths.app + '/elements/**/*.coffee'))
.pipe($.replaceTask({
patterns: [{json: getVersionData()}]
}))
.pipe($.changed(abs(config.paths.static + '/elements'), {extension: '.js'}))
.pipe($.coffeelint())
.pipe($.coffeelint.reporter())
.pipe($.sourcemaps.init())
.pipe($.coffee({
}))
.on('error', gutil.log)
.pipe($.sourcemaps.write())
.pipe(gulp.dest(abs(config.paths.static + '/elements')));
});
Pour les étapes 2 et 3, nous utilisons gulp et un plug-in Compass pour compiler scss
en .css
et .jade
en .html
, selon une approche similaire à l'étape 2 ci-dessus.
Avec des éléments en polymère
Pour inclure les éléments Polymer, nous utilisons des importations HTML.
<link rel="import" href="elements.html">
<!-- Polymer -->
<link rel="import" href="../bower_components/polymer/polymer.html">
<!-- Custom elements -->
<link rel="import" href="sw/sw-app/sw-app.html">
<link rel="import" href="sw/system/sw-system/sw-system.html">
<link rel="import" href="sw/system/sw-routing/sw-routing.html">
<link rel="import" href="sw/system/sw-system-version/sw-system-version.html">
<link rel="import" href="sw/system/sw-system-environment/sw-system-environment.html">
<link rel="import" href="sw/pages/sw-page-landing/sw-page-landing.html">
<link rel="import" href="sw/pages/sw-page-connection/sw-page-connection.html">
<link rel="import" href="sw/pages/sw-page-calibration/sw-page-calibration.html">
<link rel="import" href="sw/pages/sw-page-experience/sw-page-experience.html">
<link rel="import" href="sw/ui/sw-preloader/sw-preloader.html">
<link rel="import" href="sw/ui/sw-ui-overlay/sw-ui-overlay.html">
<link rel="import" href="sw/ui/sw-ui-button/sw-ui-button.html">
<link rel="import" href="sw/ui/sw-ui-menu/sw-ui-menu.html">
Optimiser les éléments Polymer pour la production
Un grand projet peut finir par comporter de nombreux éléments Polymer. Dans notre
nous en avons plus de cinquante. Si vous considérez que chaque
élément a une
fichier .js
distinct et dont certaines ont des bibliothèques référencées, il devient supérieur à
100 fichiers distincts. Cela signifie que le navigateur doit effectuer
beaucoup de requêtes,
avec une perte de performances. Comme pour un processus de concatenaison et de minification que nous appliquerions à un build Angular, nous "vulcanisons" le projet Polymer à la fin pour la production.
Vulcanize est un outil Polymer qui aplatit l'arborescence des dépendances en un seul fichier HTML, ce qui réduit le nombre de requêtes. Cela est particulièrement utile pour les navigateurs prend en charge les composants Web de manière native.
CSP (Content Security Policy) et Polymer
Lorsque vous développez des applications Web sécurisées, vous devez implémenter CSP. CSP est un ensemble de règles qui empêchent les attaques par script intersites (XSS) : l'exécution de scripts provenant de sources non sécurisées ou l'exécution de scripts intégrés ; à partir de fichiers HTML.
Le fichier .html
unique, optimisé, concaténé et réduit généré par Vulcanize contient désormais tout le code JavaScript intégré dans un format non conforme au CSP. Pour résoudre ce problème, nous utilisons un outil appelé
Crisper :
Crisper divise les scripts intégrés d'un fichier HTML et les place dans un seul fichier JavaScript externe pour se conformer au CSP. Nous transmettons donc le modèle vulcanisé
HTML dans Crisper et vous obtenez deux fichiers: elements.html
et
elements.js
Dans elements.html
, il se charge également de charger le
a généré elements.js
.
Structure logique de l'application
Dans Polymer, les éléments peuvent être de tout type, d'une utilitaire non visuelle à de petits éléments d'interface utilisateur autonomes et réutilisables (comme des boutons), en passant par des modules plus importants tels que des "pages" et même la composition d'applications complètes.
Post-traitement avec Polymer et une architecture parent-enfant
Dans n'importe quel pipeline graphique 3D, il existe toujours une dernière étape au cours de laquelle des effets sont ajoutés sur l'ensemble de l'image en tant que superposition. Il s'agit de la l'étape de post-traitement, qui implique des effets tels que des éclats, des rayons divins, la profondeur de champ, le bokeh, le floutage, etc. Les effets sont combinés et appliqués différents éléments en fonction de la construction de la scène. Dans THREE.js, nous vous pouvez créer un nuanceur personnalisé pour le post-traitement en JavaScript c'est possible avec Polymer grâce à sa structure parent-enfant.
Si vous examinez le code HTML de notre élément de post-traitement:
<dom-module id="sw-experience-postprocessor">
<!-- Template-->
<template>
<sw-experience-effect-bloom class="effect"></sw-experience-effect-bloom>
<sw-experience-effect-dof class="effect"></sw-experience-effect-dof>
<sw-experience-effect-vignette class="effect"></sw-experience-effect-vignette>
</template>
<!-- Polymer element script-->
<script src="scripts/sw-experience-postprocessor.js"></script>
</dom-module>
Nous spécifions les effets en tant qu'éléments Polymer imbriqués dans une classe commune. Ensuite,
Dans sw-experience-postprocessor.js
, nous procédons ainsi:
effects = @querySelectorAll '.effect'
@composer.addPass effect.getPass() for effect in effects
Nous utilisons la fonctionnalité HTML et querySelectorAll
de JavaScript pour trouver tous les effets imbriqués en tant qu'éléments HTML dans le post-traitement, dans l'ordre dans lequel ils ont été spécifiés. Nous effectuons ensuite une itération sur eux et les ajoutons au compositeur.
Maintenant, supposons que nous voulons supprimer l'effet DOF (Profondeur de champ) et modifier l'ordre des effets de fleur et de vignetage. Tout ce que nous avons à faire est de modifier la définition du post-traitement par quelque chose comme:
<dom-module id="sw-experience-postprocessor">
<!-- Template-->
<template>
<sw-experience-effect-vignette class="effect"></sw-experience-effect-vignette>
<sw-experience-effect-bloom class="effect"></sw-experience-effect-bloom>
</template>
<!-- Polymer element script-->
<script src="scripts/sw-experience-postprocessor.js"></script>
</dom-module>
et la scène va s'exécuter, sans modifier une seule ligne de code.
Boucle de rendu et de mise à jour dans Polymer
Avec Polymer, nous pouvons aussi aborder le rendu et les mises à jour du moteur avec élégance.
Nous avons créé un élément timer
qui utilise requestAnimationFrame
et calcule
telles que l'heure actuelle (t
) et le temps différentiel (temps écoulé depuis le
dernière image (dt
):
Polymer
is: 'sw-timer'
properties:
t:
type: Number
value: 0
readOnly: true
notify: true
dt:
type: Number
value: 0
readOnly: true
notify: true
_isRunning: false
_lastFrameTime: 0
ready: ->
@_isRunning = true
@_update()
_update: ->
if !@_isRunning then return
requestAnimationFrame => @_update()
currentTime = @_getCurrentTime()
@_setT currentTime
@_setDt currentTime - @_lastFrameTime
@_lastFrameTime = @_getCurrentTime()
_getCurrentTime: ->
if window.performance then performance.now() else new Date().getTime()
Nous utilisons ensuite la liaison de données pour lier les propriétés t
et dt
à notre
moteur (experience.jade
):
sw-timer(
t='{ % templatetag openvariable % }t}}',
dt='{ % templatetag openvariable % }dt}}'
)
sw-experience-engine(
t='[t]',
dt='[dt]'
)
Nous écoutons aussi les modifications de t
et de dt
dans le moteur et chaque fois que
les valeurs changent, la fonction _update
est appelée:
Polymer
is: 'sw-experience-engine'
properties:
t:
type: Number
dt:
type: Number
observers: [
'_update(t)'
]
_update: (t) ->
dt = @dt
@_physics.update dt, t
@_renderer.render dt, t
Toutefois, si vous avez besoin d'augmenter le nombre d'images par seconde, vous pouvez supprimer la liaison de données de Polymer dans la boucle de rendu pour économiser quelques millisecondes nécessaires pour informer les éléments des modifications. Nous avons implémenté les observateurs personnalisés comme suit:
sw-timer.coffee
:
addUpdateListener: (listener) ->
if @_updateListeners.indexOf(listener) == -1
@_updateListeners.push listener
return
removeUpdateListener: (listener) ->
index = @_updateListeners.indexOf listener
if index != -1
@_updateListeners.splice index, 1
return
_update: ->
# ...
for listener in @_updateListeners
listener @dt, @t
# ...
La fonction addUpdateListener
accepte un rappel et l'enregistre dans son
de rappels. Ensuite, dans la boucle de mise à jour, nous effectuons une itération sur chaque rappel.
nous l'exécutons directement avec les arguments dt
et t
, en contournant la liaison de données ou
le déclenchement d'événements. Une fois qu'un rappel n'est plus censé être actif, nous avons ajouté une
removeUpdateListener
, qui vous permet de supprimer un rappel ajouté précédemment.
Sabre laser dans THREE.js
THREE.js élimine les détails de bas niveau de WebGL et nous permet de nous concentrer sur le problème. Notre problème est d'affronter les Stormtroopers, arme. Faisons donc un sabre laser.
La lame brillante différencie un sabre laser des anciens. arme à deux mains. Il se compose principalement de deux parties: la poutre et la traînée. que l'on voit lorsqu'on le déplace. Nous l'avons créée avec une forme cylindrique lumineuse et une traînée dynamique qui la suit lorsque le joueur se déplace.
The Blade
La lame se compose de deux sous-pales. Un intérieur et un extérieur. Il s'agit tous deux de maillages THREE.js avec leurs matériaux respectifs.
La lame intérieure
Pour la lame intérieure, nous avons utilisé un matériau personnalisé avec un nuanceur personnalisé. Mer à partir d'une droite créée par deux points et projeter la ligne entre ces deux dans un plan. C'est ce que vous contrôlez avec votre mobile, cela donne une impression de profondeur et d'orientation au sabre.
Pour donner l'impression qu'il s'agit d'un objet rond et lumineux, nous observons les distance orthogonale de n'importe quel point du plan par rapport au point principal reliant les deux points A et B comme ci-dessous. Plus un point est proche de l'axe principal, plus il est lumineux.
La source ci-dessous montre comment nous calculons un vFactor
pour contrôler l'intensité.
dans le nuanceur de sommets pour qu'il se fonde dans la scène
nuanceur de fragments.
THREE.LaserShader = {
uniforms: {
"uPointA": {type: "v3", value: new THREE.Vector3(0, -1, 0)},
"uPointB": {type: "v3", value: new THREE.Vector3(0, 1, 0)},
"uColor": {type: "c", value: new THREE.Color(1, 0, 0)},
"uMultiplier": {type: "f", value: 3.0},
"uCoreColor": {type: "c", value: new THREE.Color(1, 1, 1)},
"uCoreOpacity": {type: "f", value: 0.8},
"uLowerBound": {type: "f", value: 0.4},
"uUpperBound": {type: "f", value: 0.8},
"uTransitionPower": {type: "f", value: 2},
"uNearPlaneValue": {type: "f", value: -0.01}
},
vertexShader: [
"uniform vec3 uPointA;",
"uniform vec3 uPointB;",
"uniform float uMultiplier;",
"uniform float uNearPlaneValue;",
"varying float vFactor;",
"float getDistanceFromAB(vec2 a, vec2 b, vec2 p) {",
"vec2 l = b - a;",
"float l2 = dot( l, l );",
"float t = dot( p - a, l ) / l2;",
"if( t < 0.0 ) return distance( p, a );",
"if( t > 1.0 ) return distance( p, b );",
"vec2 projection = a + (l * t);",
"return distance( p, projection );",
"}",
"vec3 getIntersection(vec4 a, vec4 b) {",
"vec3 p = a.xyz;",
"vec3 q = b.xyz;",
"vec3 v = normalize( q - p );",
"float t = ( uNearPlaneValue - p.z ) / v.z;",
"return p + (v * t);",
"}",
"void main() {",
"vec4 a = modelViewMatrix * vec4(uPointA, 1.0);",
"vec4 b = modelViewMatrix * vec4(uPointB, 1.0);",
"if(a.z > uNearPlaneValue) a.xyz = getIntersection(a, b);",
"if(b.z > uNearPlaneValue) b.xyz = getIntersection(a, b);",
"a = projectionMatrix * a; a /= a.w;",
"b = projectionMatrix * b; b /= b.w;",
"vec4 p = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
"gl_Position = p;",
"p /= p.w;",
"float d = getDistanceFromAB(a.xy, b.xy, p.xy) * gl_Position.z;",
"vFactor = 1.0 - clamp(uMultiplier * d, 0.0, 1.0);",
"}"
].join( "\n" ),
fragmentShader: [
"uniform vec3 uColor;",
"uniform vec3 uCoreColor;",
"uniform float uCoreOpacity;",
"uniform float uLowerBound;",
"uniform float uUpperBound;",
"uniform float uTransitionPower;",
"varying float vFactor;",
"void main() {",
"vec4 col = vec4(uColor, vFactor);",
"float factor = smoothstep(uLowerBound, uUpperBound, vFactor);",
"factor = pow(factor, uTransitionPower);",
"vec4 coreCol = vec4(uCoreColor, uCoreOpacity);",
"vec4 finalCol = mix(col, coreCol, factor);",
"gl_FragColor = finalCol;",
"}"
].join( "\n" )
};
L'éclairage de la lame extérieure
Pour le halo externe, nous effectuons le rendu dans un tampon de rendu distinct et utilisons un et se fondre dans l'image finale pour obtenir l'éclat souhaité. L'image ci-dessous montre les trois régions différentes si vous voulez un sabre décent. à savoir le cœur blanc, le milieu une teinte bleue et un halo extérieur.
Sentier du sabre laser
La traînée du sabre laser est la clé de l'effet complet, comme le montre l'original. de la série Star Wars. Nous avons créé la piste avec un fan de triangles générés de manière dynamique en fonction du mouvement du sabre laser. Ces fans sont alors transmis au post-processeur pour une amélioration visuelle supplémentaire. Pour créer le une forme de ventilateur, nous avons un segment de droite basé sur sa transformation précédente et "current transform", nous générons un nouveau triangle dans le maillage, de la queue après une certaine longueur.
Une fois que nous avons un maillage, nous lui attribuons un matériau simple et nous le transmettons pour créer un effet fluide. Nous utilisons le même effet de floraison que celui que nous avons appliqué à l'éclat extérieur de la lame et obtenons une traînée fluide, comme vous pouvez le voir :
Lueur autour de la piste
Pour que la pièce finale soit complète, nous avons dû gérer l'aura autour du tracé réel, qui peut être créée de plusieurs façons. La solution que nous nous n'allons pas y entrer en détail. Pour des raisons de performances, c'est de créer pour ce tampon, qui crée un bord lisse autour d'une pince RenderBuffer. Nous combinons ensuite cette sortie dans le rendu final. Ici, vous pouvez voir l'éclat qui entoure la piste:
Conclusion
Polymer est un concept et une bibliothèque puissants (tout comme les WebComponents sont général). Ce que vous créez ne dépend que de vous. Il peut s’agir de n’importe quoi, d'un bouton d'interface utilisateur à une application WebGL en taille réelle. Dans les chapitres précédents, voici quelques conseils et astuces pour utiliser efficacement Polymer en production et comment structurer des modules plus complexes bien. Nous vous avons également montré comment créer un sabre laser d'aspect esthétique avec WebGL. Si vous combinez tout cela, n'oubliez pas de Vulcaniser vos éléments Polymer avant le déploiement sur le serveur de production et, si vous n'oubliez pas d'utiliser Crisper, Si vous souhaitez rester en conformité avec les CSP, vous pouvez être obligé de faire appel à vous !