927 lines
16 KiB
Markdown
927 lines
16 KiB
Markdown
<style>
|
||
pre { font-size: 0.5em !important; }
|
||
table { font-size: 0.6em !important; }
|
||
</style>
|
||
|
||
# Frameworks Javascript - Tour d'horizon
|
||
## William Petit - S.C.O.P. Cadoles
|
||
|
||
---
|
||
|
||
<!-- page_number: true -->
|
||
## Les frameworks
|
||
|
||
## RiotJS
|
||
## ReactJS
|
||
## EmberJS
|
||
|
||
---
|
||
|
||
## RiotJS
|
||
|
||
### Présentation générale
|
||
### Concepts généraux
|
||
### Exercice
|
||
|
||
---
|
||
|
||
## Présentation générale (1)
|
||
|
||
- Publication: Septembre 2013
|
||
- Mainteneurs principaux: [@GianlucaGuarini](https://github.com/GianlucaGuarini) et [@tipiirai](https://github.com/tipiirai) (projet communautaire)
|
||
- License: MIT
|
||
|
||
---
|
||
|
||
## Présentation générale (2)
|
||
|
||
|Avantages|Inconvénients|
|
||
|:-|:-|
|
||
|Une librairie et pas un framework| Une librairie et pas un framework|
|
||
| Projet communautaire | Projet communautaire |
|
||
| Orientée "composant" | |
|
||
| API minimaliste |
|
||
| Prêt à l'emploi (pas de transpilage nécessaire en développement) |
|
||
| Routeur "frontend" disponible |
|
||
|
||
---
|
||
|
||
## Concepts généraux
|
||
|
||
### Composants ("tags")
|
||
### Observable
|
||
|
||
---
|
||
|
||
## Composants
|
||
|
||
### Structure et utilisation d'un composant
|
||
### Templating
|
||
### Styles de composant
|
||
### Évènements du DOM
|
||
### Cycle de vie du composant
|
||
|
||
---
|
||
|
||
## Structure d'un composant
|
||
|
||
```html
|
||
<!-- components/my-tag.tag -->
|
||
|
||
<!-- Racine du Tag -->
|
||
<my-tag>
|
||
|
||
<!-- Définition du DOM -->
|
||
<span>{ opts.foo }</span>
|
||
|
||
<!-- Style du composant -->
|
||
<style>
|
||
:scope {
|
||
display: block
|
||
}
|
||
</style>
|
||
|
||
<!-- Script du composant -->
|
||
<script>
|
||
this.on('mount', () => {
|
||
this.opts.foo = "bar";
|
||
});
|
||
</script>
|
||
|
||
</my-tag>
|
||
```
|
||
|
||
---
|
||
|
||
## Utilisation du composant
|
||
|
||
### Compilation dans le navigateur
|
||
|
||
```html
|
||
<html>
|
||
<body>
|
||
|
||
<!-- Utilisation du composant dans notre page HTML -->
|
||
<my-tag></my-tag>
|
||
|
||
<!-- Inclusion de notre "tag" -->
|
||
<script type="riot/tag" src="components/my-tag.tag"></script>
|
||
|
||
<!-- Inclusion de la librairie Riot + compilateur -->
|
||
<script type="text/javascript" src="js/riot+compiler.js"></script>
|
||
|
||
<!-- Montage des composants Riot -->
|
||
<script type="text/javascript">
|
||
var opts = {}; // Options passées à tous les tags
|
||
riot.mount('*', opts)
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
---
|
||
|
||
## Templating
|
||
|
||
```html
|
||
<my-tag>
|
||
|
||
<!-- Interpolation simple -->
|
||
<span>{ foo }</span> <!-- <span>bar</span> -->
|
||
|
||
<!-- Boucles -->
|
||
<ul>
|
||
<li each={ item in myItems }>{ item.text }</li>
|
||
</ul>
|
||
|
||
<!-- Condition avec appel de méthode -->
|
||
|
||
<span if={ isFooBar() }>foo est égal à "bar" !</span>
|
||
|
||
<script>
|
||
|
||
this.foo = "bar"
|
||
|
||
this.myItems = [
|
||
{ text: "Hello World" },
|
||
{ text: "Foo Bar" }
|
||
];
|
||
|
||
isFooBar() {
|
||
return this.foo == "bar";
|
||
}
|
||
|
||
</script>
|
||
|
||
</my-tag>
|
||
```
|
||
|
||
---
|
||
|
||
## Styles de composant
|
||
|
||
### Feuille de style
|
||
### Styles et classes dynamiques
|
||
|
||
---
|
||
|
||
## Feuille de style
|
||
|
||
```html
|
||
<my-tag>
|
||
|
||
<!-- Définition du DOM -->
|
||
<span id="foo">Foo</span>
|
||
<div class="bar">Bar</div>
|
||
|
||
<!-- Style du composant -->
|
||
<style>
|
||
:scope {
|
||
display: block
|
||
}
|
||
#foo {
|
||
color: red;
|
||
}
|
||
.bar {
|
||
background: blue;
|
||
}
|
||
</style>
|
||
|
||
</my-tag>
|
||
```
|
||
|
||
---
|
||
|
||
## Styles et classes dynamiques
|
||
|
||
```html
|
||
<my-tag>
|
||
|
||
<!-- Définition du DOM -->
|
||
<span style={ fooStyles }>Foo</span>
|
||
<div class={ barClasses }>Bar</div>
|
||
|
||
|
||
<script>
|
||
|
||
this.fooStyles = {
|
||
color: "red"
|
||
}
|
||
|
||
this.barClasses = {
|
||
"bar": true, // Classes conditionnelles
|
||
"foo": false
|
||
}
|
||
|
||
</script>
|
||
|
||
</my-tag>
|
||
```
|
||
|
||
---
|
||
|
||
## Évènements du DOM
|
||
|
||
|
||
```html
|
||
<my-tag>
|
||
|
||
<button onclick={ onButtonClick }>Click me !</button>
|
||
|
||
<script>
|
||
|
||
onButtonClick(evt) {
|
||
|
||
// Si l'élément à l'origine se trouve dans une boucle each={ ...},
|
||
// evt.item pointera vers l'élément courant
|
||
|
||
console.log(evt);
|
||
|
||
}
|
||
|
||
</script>
|
||
|
||
</my-tag>
|
||
```
|
||
|
||
---
|
||
|
||
## Cycle de vie du composant
|
||
|
||
```html
|
||
<my-tag>
|
||
|
||
<script>
|
||
this.on('before-mount', () => console.log('before-mount'));
|
||
this.on('mount', () => console.log('mount'));
|
||
this.on('update', () => console.log('update'));
|
||
this.on('updated', () => console.log('updated'));
|
||
this.on('before-unmount', () => console.log('before-unmount'));
|
||
this.on('unmount', () => console.log('unmount'));
|
||
</script>
|
||
|
||
</my-tag>
|
||
```
|
||
|
||
---
|
||
|
||
## Observable (1)
|
||
|
||
### Déclaration d'un "service"
|
||
```javascript
|
||
// services/auth.js
|
||
class MyAuthService {
|
||
|
||
constructor() {
|
||
riot.observable(this)
|
||
this.user = null;
|
||
}
|
||
|
||
login(user, password) {
|
||
|
||
var form = new FormData();
|
||
form.append('login', login);
|
||
form.append('password', password);
|
||
|
||
return fetch('/login', { method: 'POST', form: form })
|
||
.then(res => res.json())
|
||
.then(user => {
|
||
this.user = user;
|
||
this.trigger('loggedIn', user);
|
||
})
|
||
;
|
||
}
|
||
|
||
isLoggedIn() {
|
||
return this.user != null;
|
||
}
|
||
|
||
}
|
||
|
||
const myAuth = new MyAuthService();
|
||
```
|
||
|
||
---
|
||
## Observable (2)
|
||
|
||
### Utilisation du service
|
||
|
||
```html
|
||
<my-app></my-app>
|
||
|
||
<!-- Tag "inline" -->
|
||
<script type="riot/tag">
|
||
|
||
<my-app>
|
||
<button if={ !user }
|
||
onclick={ login }>
|
||
Login
|
||
</button>
|
||
<span if={ user }>User: { user.name }</span>
|
||
</my-app>
|
||
|
||
<script>
|
||
this.user = null;
|
||
|
||
this.on('mount', () => {
|
||
this.opts.auth.on('loggedIn', user => {
|
||
this.user = user;
|
||
this.update();
|
||
});
|
||
});
|
||
|
||
login() {
|
||
this.opts.auth.login();
|
||
}
|
||
|
||
</script>
|
||
|
||
</script>
|
||
|
||
<script type="text/javascript" src="services/auth.js"></script>
|
||
<script type="text/javascript">riot.mount('*', { auth: myAuth })</script>
|
||
```
|
||
|
||
---
|
||
|
||
## Exercice
|
||
|
||
Implémenter une application minimaliste de gestion de tâches avec Riot. Cette application doit comprendre les fonctionnalités suivantes:
|
||
|
||
- Ajouter une nouvelle tâche (champs texte) avec une priorité parmi les suivantes: "haute", "moyenne", "basse" et un statut: "en cours" ou "terminé"
|
||
- Marquer une tâche comme "terminée".
|
||
- Afficher la liste des tâches en attente, triées par priorité décroissante + tâches terminées en dernier.
|
||
- Les tâches doivent être persistées dans le "Local Storage" du navigateur.
|
||
|
||
---
|
||
|
||
## ReactJS
|
||
|
||
### Présentation générale
|
||
### Concepts généraux
|
||
### Prise en main
|
||
|
||
---
|
||
|
||
## Présentation générale (1)
|
||
|
||
|
||
- Publication: Mars 2013
|
||
- Mainteneur principal: Facebook
|
||
- License: MIT
|
||
|
||
---
|
||
|
||
## Présentation générale (2)
|
||
|
||
|
||
|Avantages|Inconvénients|
|
||
|:-|:-|
|
||
|Grande communauté, très active | Changement de license puis rétropédalage en 2017 |
|
||
| Maintenu et utilisé par Facebook | Nécessité de mettre en place un pipeline de transpilage |
|
||
| Architecture Flux | Architecture Flux |
|
||
| Possibilité de faire des applications "isomorphiques" avec NodeJS |
|
||
| Projet "React Native" |
|
||
| API stable |
|
||
| Virtual DOM ([voir cet article](https://auth0.com/blog/face-off-virtual-dom-vs-incremental-dom-vs-glimmer/)) |
|
||
| Moteurs "lightweight" compatibles |
|
||
|
||
---
|
||
|
||
## Concepts généraux
|
||
|
||
### Composants
|
||
### L'architecture Flux
|
||
|
||
---
|
||
|
||
## Composants
|
||
|
||
### Structure d'un composant
|
||
### Props
|
||
### State
|
||
### Évènements du DOM
|
||
### Cycle de vie d'un composant
|
||
|
||
---
|
||
|
||
## Structure d'un composant
|
||
|
||
```jsx
|
||
// components/hello-world.js
|
||
|
||
import React from 'react'
|
||
|
||
class HelloWorld extends React.Component {
|
||
|
||
render() {
|
||
return (
|
||
<div className='hello-world'>
|
||
<h1>Hello World</h1>
|
||
<p>Welcome to React</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
}
|
||
|
||
export default HelloWorld
|
||
|
||
```
|
||
|
||
---
|
||
|
||
## Props
|
||
|
||
```jsx
|
||
// components/props-example.js
|
||
|
||
import React from 'react'
|
||
|
||
class PropsExample extends React.Component {
|
||
|
||
render() {
|
||
return (
|
||
<span>{ this.props.text }</span>
|
||
)
|
||
}
|
||
|
||
}
|
||
|
||
export default PropsExample
|
||
|
||
// app.js
|
||
|
||
import React from 'react'
|
||
import ReactDOM from 'react-dom'
|
||
import PropsExample from './components/props-example'
|
||
|
||
const mountpoint = document.getElementById('props-example');
|
||
|
||
ReactDOM.render(<PropsExample text="foo bar" />, mountpoint)
|
||
|
||
```
|
||
|
||
---
|
||
|
||
## State
|
||
|
||
```jsx
|
||
// components/clock.js
|
||
|
||
import React from 'react'
|
||
|
||
class Clock extends React.Component {
|
||
|
||
constructor(props) {
|
||
// On fait appel au constructeur de la classe
|
||
// parente
|
||
super(props)
|
||
|
||
// Initialisation du "state" du composant
|
||
this.state = {
|
||
time: new Date()
|
||
}
|
||
// On appelle la méthode tick() du composant
|
||
// toutes les secondes
|
||
setInterval(this.tick.bind(this), 1000);
|
||
}
|
||
|
||
// Méthode de rendu du composant
|
||
render() {
|
||
return (
|
||
<div>Time: { this.state.time.toString() }</div>
|
||
)
|
||
}
|
||
|
||
// La méthode tick() met à jour le state du composant avec
|
||
// la date courante
|
||
tick() {
|
||
this.setState({ time: new Date() });
|
||
}
|
||
|
||
}
|
||
|
||
|
||
export default Clock
|
||
|
||
```
|
||
|
||
---
|
||
|
||
### Évènements du DOM
|
||
|
||
```jsx
|
||
// components/counter.js
|
||
import React from 'react'
|
||
|
||
class Counter extends React.Component {
|
||
|
||
constructor(props) {
|
||
// On fait appel au constructeur de la classe
|
||
// parente
|
||
super(props)
|
||
|
||
// Initialisation du "state" du composant
|
||
this.state = {
|
||
count: 0
|
||
}
|
||
}
|
||
|
||
// Méthode de rendu du composant
|
||
render() {
|
||
return (
|
||
<div>
|
||
Count: <span>{ this.state.count }</span>
|
||
<button onClick={ () => this.increment() }>+1</button>
|
||
<button onClick={ () => this.decrement() }>-1</button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// La méthode increment() incrémente la valeur du compteur de 1
|
||
increment() {
|
||
this.setState(prevState => ({ count: prevState.count+1 }))
|
||
}
|
||
|
||
// La méthode decrement() incrémente la valeur du compteur de 1
|
||
decrement() {
|
||
this.setState(prevState => ({ count: prevState.count-1 }))
|
||
}
|
||
|
||
}
|
||
|
||
export default Counter
|
||
```
|
||
---
|
||
|
||
## Cycle de vie d'un composant
|
||
|
||
```
|
||
// components/clock-lifecycle.js
|
||
import React from 'react'
|
||
|
||
class Clock extends React.Component {
|
||
|
||
constructor(props) {
|
||
super(props)
|
||
this.state = {
|
||
time: new Date()
|
||
}
|
||
}
|
||
|
||
componentDidMount() {
|
||
console.log('mount')
|
||
this.intervalId = setInterval(this.tick.bind(this), 1000);
|
||
}
|
||
|
||
componentWillUnmount() {
|
||
console.log('unmount')
|
||
clearInterval(this.intervalId);
|
||
}
|
||
|
||
render() {
|
||
return (
|
||
<div>Time: { this.state.time.toString() }</div>
|
||
)
|
||
}
|
||
|
||
tick() {
|
||
this.setState({ time: new Date() });
|
||
}
|
||
|
||
}
|
||
|
||
export default Clock
|
||
```
|
||
---
|
||
|
||
## L'architecture Flux
|
||
|
||
![center](./img/flux-simple-f8-diagram-with-client-action-1300w.png)
|
||
|
||
[Voir une description des concepts](https://github.com/facebook/flux/tree/master/examples/flux-concepts)
|
||
|
||
---
|
||
|
||
## Exercice
|
||
|
||
Implémenter une application minimaliste de gestion de tâches avec React (sans l'architecture Flux). Cette application doit comprendre les fonctionnalités suivantes:
|
||
|
||
- Ajouter une nouvelle tâche (champs texte) avec une priorité parmi les suivantes: "haute", "moyenne", "basse" et un statut: "en cours" ou "terminé"
|
||
- Marquer une tâche comme "terminée".
|
||
- Afficher la liste des tâches en attente, triées par priorité décroissante + tâches terminées en dernier.
|
||
- Les tâches doivent être persistées dans le "Local Storage" du navigateur.
|
||
|
||
---
|
||
|
||
## EmberJS
|
||
|
||
### Présentation générale
|
||
### Concepts généraux
|
||
### Initialisation d'un projet
|
||
### Exercice
|
||
|
||
---
|
||
|
||
## Présentation générale (1)
|
||
|
||
|
||
- Publication: Décembre 2011
|
||
- Mainteneur principal: [Ember Core Team](https://emberjs.com/team/) (projet communautaire)
|
||
- License: MIT
|
||
|
||
---
|
||
|
||
## Présentation générale (2)
|
||
|
||
|Avantages|Inconvénients|
|
||
|:-|:-|
|
||
|"Clés en main"|"Opiniatre"|
|
||
|API stable|Structure MVC classique|
|
||
|Long-term support (1 an)|
|
||
|Documentation exhaustive|
|
||
|Ember Data|
|
||
|Ember Inspector|
|
||
|Ember CLI|
|
||
|
||
---
|
||
|
||
## Concepts généraux
|
||
|
||
### Ember CLI
|
||
### Routes
|
||
### Templates
|
||
### Modèles
|
||
### Contrôleurs
|
||
|
||
---
|
||
|
||
## Ember CLI
|
||
|
||
### Générer une nouvelle application
|
||
|
||
```shell
|
||
npm install ember-cli -g
|
||
ember new my-app
|
||
cd my-app
|
||
ember serve
|
||
```
|
||
|
||
---
|
||
|
||
## Routes (1)
|
||
|
||
### Générer une nouvelle route
|
||
|
||
```shell
|
||
ember generate route posts
|
||
```
|
||
|
||
---
|
||
|
||
## Routes (2)
|
||
|
||
### Attacher un/plusieurs modèles à une route
|
||
|
||
```javascript
|
||
// app/routes/posts.js
|
||
import Route from '@ember/routing/route';
|
||
|
||
export default Route.extend({
|
||
model() {
|
||
return this.get('store').findAll('post');
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Routes (3)
|
||
|
||
### Effectuer une redirection
|
||
|
||
```javascript
|
||
// app/routes/index.js
|
||
import Route from '@ember/routing/route';
|
||
|
||
export default Route.extend({
|
||
beforeModel() {
|
||
this.transitionTo('posts');
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Templates (1)
|
||
|
||
### Syntaxe générale
|
||
```handlebars
|
||
{{!-- Interpolation de variable --}}
|
||
{{ myVar }}
|
||
|
||
{{!-- Utilisation d'un "helper" --}}
|
||
{{helper params... }}
|
||
|
||
{{!-- Définition d'un "block" --}}
|
||
{{#block params... }}
|
||
...
|
||
{{/block}}
|
||
|
||
{{!-- Helpers imbriqués --}}
|
||
{{sum (multiply 2 4) 2}}
|
||
```
|
||
|
||
---
|
||
|
||
## Templates (2)
|
||
|
||
### "Blocks" de base
|
||
```handlebars
|
||
{{!-- Condition --}}
|
||
{{if myVar}}
|
||
Maybe...
|
||
{{else}}
|
||
Or not.
|
||
{{/if}}
|
||
|
||
{{!-- Boucle --}}
|
||
{{#each people as |person|}}
|
||
<li>Hello, {{person.name}}!</li>
|
||
{{/each}}
|
||
```
|
||
|
||
---
|
||
|
||
## Templates (3)
|
||
|
||
### "Helpers" spécifiques
|
||
```handlebars
|
||
{{!-- Affiche myVar[key] (accès dynamique aux variables exposées dans le controleur) --}}
|
||
{{get myVar "key"}}
|
||
|
||
{{!-- Affiche le contenu de la (sous) route --}}
|
||
{{outlet}}
|
||
|
||
{{!-- Modèle exposé par la route --}}
|
||
{{model}}
|
||
|
||
{{!-- Affiche un lien vers la route donnée --}}
|
||
{{link-to routeName}}
|
||
|
||
{{!-- Attacher une action à un élément (onclick) --}}
|
||
<button {{action "myAction"}}>Do it !</button>
|
||
```
|
||
---
|
||
|
||
## Contrôleurs (1)
|
||
|
||
### Générer un contrôleur
|
||
|
||
```shell
|
||
ember generate controller posts
|
||
```
|
||
---
|
||
|
||
## Contrôleurs (2)
|
||
|
||
### Exposer des variables pour le template
|
||
|
||
```shell
|
||
import Controller from '@ember/controller';
|
||
|
||
export default Controller.extend({
|
||
foo: "bar" // {{ foo }} dans le template associé
|
||
});
|
||
```
|
||
---
|
||
|
||
## Contrôleurs (3)
|
||
|
||
### Actions
|
||
|
||
```shell
|
||
import Controller from '@ember/controller';
|
||
|
||
export default Controller.extend({
|
||
|
||
foo: "bar"
|
||
|
||
actions: {
|
||
myAction: function() {
|
||
// On modifie la valeur de "foo"
|
||
// Si "foo" est utilisé dans le template, il sera automatiquement mis à jour
|
||
this.set("foo", "hello world");
|
||
}
|
||
}
|
||
});
|
||
|
||
```
|
||
---
|
||
|
||
## Modèles (1)
|
||
|
||
### Générer un nouveau modèle
|
||
|
||
```shell
|
||
ember generate model post
|
||
```
|
||
---
|
||
|
||
## Modèles (2)
|
||
|
||
### Définition d'un modèle
|
||
|
||
```javascript
|
||
// app/models/comment.js
|
||
import DS from 'ember-data';
|
||
|
||
export default DS.Model.extend({
|
||
|
||
// Simple attributs
|
||
text: DS.attr('string'),
|
||
|
||
// Relations
|
||
post: DS.belongsTo('post')
|
||
|
||
});
|
||
|
||
// app/models/post.js
|
||
import DS from 'ember-data';
|
||
|
||
export default DS.Model.extend({
|
||
|
||
// Simples attributs
|
||
title: DS.attr('string'),
|
||
content: DS.attr('string'),
|
||
|
||
// Relations
|
||
comments: DS.hasMany('comment')
|
||
|
||
});
|
||
```
|
||
---
|
||
|
||
## Modèles (3)
|
||
|
||
### Manipuler une instance de modèle
|
||
|
||
```javascript
|
||
// app/controllers/posts.js
|
||
|
||
// Créer une nouvelle instance
|
||
var post = store.createRecord('post', {
|
||
title: 'Mon article',
|
||
content: 'Lorem ipsum'
|
||
});
|
||
|
||
// Modifier l'instance
|
||
post.set('title', 'Foo bar');
|
||
|
||
// Sauvegarde de l'instance
|
||
post.save().then(() => console.log("Post saved !"));
|
||
|
||
// Supprimer l'instance
|
||
post.destroyRecord().then(() => console.log("Post deleted !"));
|
||
|
||
```
|
||
---
|
||
|
||
## Modèles (4)
|
||
|
||
### Effectuer des requêtes sur le "store"
|
||
|
||
```javascript
|
||
// app/controllers/posts.js
|
||
|
||
// Récupérer toutes les instances de "post"
|
||
var posts = this.get('store').findAll('post');
|
||
|
||
// Requête avec filtre
|
||
var posts = this.get('store').query('post', {
|
||
filter: {
|
||
title: "Foo Bar"
|
||
}
|
||
});
|
||
|
||
```
|
||
---
|
||
|
||
## Exercice
|
||
|
||
Implémenter une application minimaliste de gestion de tâches avec EmberJS. Cette application doit comprendre les fonctionnalités suivantes:
|
||
|
||
- Ajouter une nouvelle tâche (champs texte) avec une priorité parmi les suivantes: "haute", "moyenne", "basse" et un statut: "en cours" ou "terminé"
|
||
- Marquer une tâche comme "terminée".
|
||
- Afficher la liste des tâches en attente, triées par priorité décroissante + tâches terminées en dernier.
|
||
- Les tâches doivent être persistées dans le "Local Storage" du navigateur.
|
||
|
||
---
|
||
|
||
# Licence
|
||
|
||
## CC BY-NC-SA 3.0 FR
|
||
|
||
[Creative Commons - Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 France](https://creativecommons.org/licenses/by-nc-sa/3.0/fr/) |