formations/developpement/framework_web/presentation/slides.md

1283 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
marp: true
---
<style>
pre { font-size: 0.5em !important; }
table { font-size: 0.6em !important; }
</style>
# React + Redux
## William Petit - S.C.O.P. Cadoles
---
## Amorçage d'un projet
---
<!-- page_number: true -->
## Méthode simplifiée: `create-react-app`
```bash
npm install -g create-react-app # Installation du générateur d'application
create-react-app my-app # Génération de l'application
cd my-app # Se placer dans le répertoire
npm start # Lancer le serveur de développement
```
https://github.com/facebook/create-react-app
---
## Méthode manuelle
### Installation des dépendances
```bash
mkdir my-app # Création du répertoire de l'application
cd my-app # Se placer dans le répertoire
npm init # Initialiser le projet NPM
# Installation des dépendances NPM
npm install --save \
react react-dom react-hot-loader \
webpack webpack-dev-server \
webpack-cli \
babel-core babel-loader \
babel-preset-env babel-preset-react \
babel-preset-stage-2 \
style-loader file-loader css-loader \
html-webpack-plugin clean-webpack-plugin \
uglifyjs-webpack-plugin webpack-merge
```
---
### Configuration de Webpack
```js
// my-app/webpack.common.js
const webpack = require('webpack')
const HTMLPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [
{ test: /\.(js|jsx)$/, exclude: /node_modules/, use: ['babel-loader'] },
{ test: /\.(scss|sass)$/, use: ['style-loader', 'css-loader'] },
{ test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?.*$|$)/, use: ['file-loader'] }
]
},
resolve: {
extensions: ['*', '.js', '.jsx']
},
plugins: [
new webpack.ProvidePlugin({
'React': 'react'
}),
new HTMLPlugin({
title: 'MyApp',
hash: true,
template: 'src/index.html'
})
]
}
```
---
```js
// my-app/webpack.dev.js
const webpack = require('webpack')
const common = require('./webpack.common.js')
const merge = require('webpack-merge')
const path = require('path')
module.exports = merge(common, {
devtool: 'inline-source-map',
mode: 'development',
plugins: [
new webpack.HotModuleReplacementPlugin()
],
output: {
path: path.join(__dirname, 'public'),
publicPath: '/',
filename: '[name].[hash].js'
},
devServer: {
contentBase: './public',
hot: true
}
})
```
---
```
// my-app/webpack.prod.js
const common = require('./webpack.common.js')
const merge = require('webpack-merge')
const path = require('path')
const webpack = require('webpack')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].[chunkhash].js'
},
devtool: 'source-map',
plugins: [
new CleanWebpackPlugin(['dist']),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new UglifyJSPlugin({
sourceMap: true
})
]
})
```
---
### Compléter le fichier `package.json`
```js
// my-app/package.json
{
"scripts": {
"start": "webpack-dev-server --progress --colors --config webpack.dev.js",
"build": "webpack --progress --colors --config webpack.prod.js"
},
// ...
"babel": {
"presets": [
"env",
"react",
"stage-2"
],
"plugins": ["react-hot-loader/babel"]
}
}
```
---
### Création des fichiers source de base
```html
<!-- my-app/src/index.html -->
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MyApp</title>
<% for (var css in htmlWebpackPlugin.files.css) { %>
<link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
<% } %>
<body>
<div id="app"></div>
</body>
</html>
```
https://github.com/jantimon/html-webpack-plugin
---
```js
// my-app/src/index.js
import ReactDOM from 'react-dom'
import App from './app'
ReactDOM.render(<App />, document.getElementById('app'))
```
```js
// my-app/src/app.js
import { Component } from 'react'
import { hot } from 'react-hot-loader'
class App extends Component {
render () {
return <h1>Hello World !</h1>
}
}
export default hot(module)(App)
```
---
### Lancer le serveur de développement
```bash
npm start # Puis ouvrir http://localhost:8080 dans votre navigateur
```
### Générer une version pour la production
```bash
npm run build # Les fichiers générés seront dans le répertoire my-app/dist
```
---
## Les composants
---
## Différentes notations
### Classe ES6
```jsx
// my-app/src/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
```
---
## Notation fonctionnelle
```jsx
// my-app/src/components/hello-world.js
const HelloWorld = () => {
return (
<div className='hello-world'>
<h1>Hello World</h1>
<p>Welcome to React</p>
</div>
)
}
export default HelloWorld
```
---
## La syntaxe JSX
C'est une représentation proche du XML d'une arborescence de composants. Il ne faut pas faire d'amalgame, **ce n'est pas du HTML**.
La définition des attributs propres au DOM se fait via le nom de la propriété Javascript, pas de l'attribut HTML. **Par exemple l'attribut`class` en HTML devient `className` en JSX**.
Il en est de même pour la **définition des styles**.
---
## `props`
Elles permettent de passer des données aux composants.
```jsx
// my-app/src/components/props-example.js
import React from 'react'
class PropsExample extends React.Component {
render() {
return (
<span>{ this.props.text }</span>
)
}
}
export default PropsExample
// Exemple d'utilisation
// <PropsExample text="foo bar" />
```
---
### `props.children`
La propriété `children` permet de récupérer les composants 'enfants' qui pourraient être passés au composant lors de son rendu.
```jsx
// my-app/src/components/my-list.js
import React from 'react'
class MyList extends React.Component {
render() {
return (
<ul>
{ this.props.children }
</ul>
)
}
}
export default PropsExample
// Exemple d'utilisation
<MyList>
<li>Item 1</li>
<li>Item 2</li>
</MyList>
```
---
## `state`
La propriété `state` d'un composant représente l'état (au sens données) de celui ci.
```jsx
// my-app/src/components/clock.js
import React from 'react'
export default class Clock extends React.Component {
constructor(props) {
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() });
}
}
```
---
## Événements du DOM
Les événements du DOM sont interceptés par le passage de "callbacks" sur les propriétés `on<Event>` des composants.
---
```jsx
// my-app/src/components/counter.js
import React from 'react'
export default class Counter extends React.Component {
constructor(props) {
super(props)
// Initialisation du "state" du composant
this.state = {
count: 0
}
// On "lie" les méthodes de la classe à l'instance
this.increment = this.increment.bind(this)
this.decrement = this.decrement.bind(this)
}
// Méthode de rendu du composant
render() {
return (
<div>
Count: <span>{ this.state.count }</span>&nbsp;
<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() décrémente la valeur du compteur de 1
decrement() {
this.setState(prevState => ({ count: prevState.count-1 }))
}
}
```
---
## Gestion des styles
Différentes méthodologies existent pour gérer les styles des composants.
La méthode classique CSS (avec ou sans préprocesseur SCSS/SASS) est tout à fait viable.
Les styles des composants sont également souvent déclarés en Javascript et utilisés via les `props` des composants.
D'autres solutions plus avancées comme [styled-components](https://www.styled-components.com/) ou [CSS Modules](https://github.com/css-modules/css-modules) existent également. Elles mettent l'accent sur la modularité et la réutilisation des règles de style entre les composants.
---
### Déclaration des styles en Javascript
```jsx
// my-app/src/components/my-styled-component.styles.js
export default {
self: {
backgroundColor: 'red',
border: '1px solid black',
padding: '10px'
},
content: {
backgroundColor: 'blue',
border: '1px dotted white'
}
}
```
```jsx
// my-app/src/components/my-styled-component.js
import React from 'react'
import styles from './my-styled-component.styles.js'
export default class MyStyledComponent extends React.Component {
render() {
return (
<div style={styles.self}>
<div style={styles.content}>
Isnt it pretty ?
</div>
</div>
)
}
}
```
---
## Composition des styles en Javascript
En utilisant l'opérateur de décomposition `...`, il est possible
de composer les styles.
```jsx
// my-app/src/components/my-styled-component.js
import React from 'react'
import styles from './my-styled-component.styles.js'
export default class MyStyledComponent extends React.Component {
render() {
return (
<div style={{...styles.self, ...this.props.style}}>
<div style={{...styles.content, ...this.props.contentStyle}}>
Isnt it pretty ?
</div>
</div>
)
}
}
// Exemple d'utilisation
<StyledComponent style={{backgroundColor: 'green'}} contentStyle={{border: 'none'}} />
```
---
## Cycle de vie et méthodes
https://reactjs.org/docs/react-component.html
---
### Séquence de montage
1. `constructor(nextProps, currentState)`
2. `static getDerivedStateFromProps(nextProps, currentState)`
3. `render()`
4. `componentDidMount()`
---
### Séquence de mise à jour
1. `static getDerivedStateFromProps(nextProps, currentState)`
2. `shouldComponentUpdate(nextProps, nextState)`
3. `render()`
4. `getSnapshotBeforeUpdate(prevProps, prevState)`
5. `componentDidUpdate(prevProps, prevState, snapshot)`
---
### Séquence de démontage
1. `componentWillUnmount()`
---
### Gestion des erreurs
`componentDidCatch(error, info)`
Permet d'intercepter les erreurs qui pourraient être émises dans l'arbre des sous composants.
---
## Composants "contrôlés" (formulaires)
```jsx
// my-app/src/components/my-form.js
import React from 'react'
export default class MyForm extends React.Component {
constructor(props) {
super(props);
this.state = {name: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(evt) {
this.setState({value: evt.target.value});
}
handleSubmit(evt) {
console.log(`Votre nom est ${this.state.name}`);
evt.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Nom:
<input type="text" value={this.state.name} onChange={this.handleChange} />
</label>
<input type="submit" value="Soumettre" />
</form>
);
}
}
```
---
## Intégrer React avec d'autres librairies
https://reactjs.org/docs/integrating-with-other-libraries.html
---
## Tester ses composants
https://facebook.github.io/jest/docs/en/tutorial-react.html
### Installer Jest
```bash
npm install --save-dev jest react-test-renderer babel-jest
```
### Configurer le script NPM
```js
// package.json
{
// ...
"scripts" : {
"test": "jest"
}
//...
}
```
---
## Créer un script de test
```
// my-app/src/components/my-styled-component.test.js
/* globals test, expect */
import MyStyledComponent from './my-styled-component'
import renderer from 'react-test-renderer'
test('Navbar snapshot', () => {
const component = renderer.create(<MyStyledComponent />)
let tree = component.toJSON()
// Vérifier que le composant n'a pas changé depuis le dernier
// snapshot.
// Voir https://facebook.github.io/jest/docs/en/snapshot-testing.html
// pour plus d'informations
expect(tree).toMatchSnapshot()
// L'API expect() de Jest est disponible à l'adresse
// https://facebook.github.io/jest/docs/en/expect.html
// Il est possible d'effectuer des vérifications plus avancées
// grâce au projet Enzyme (vérification du DOM, etc)
// Voir http://airbnb.io/enzyme/ et
// https://facebook.github.io/jest/docs/en/tutorial-react.html#dom-testing
})
```
---
### Lancer les tests
```bash
npm test
```
---
## Gestion des routes
https://reacttraining.com/react-router/web/
### Installation du module
```bash
npm install --save react-router react-router-dom
```
---
### Déclaration basique des routes
```
// my-app/src/app.js
import React from 'react'
import { HashRouter } from 'react-router-dom' // ou BrowserRouter
import { Route, Switch, Redirect } from 'react-router'
import { Clock, MyStyledComponent, My404Page } from './components'
export default class App extends React.Component {
render() {
<HashRouter>
<Switch>
<Route path='/clock' exact component={Clock} />
<Route path='/styled' exact component={MyStyledComponent} />
<Redirect from='/old-style' to='/styled' />
<Route component={My404Page} />
</Switch>
</HashRouter>
}
}
```
---
## Liens de navigation
```
// my-app/src/components/my-linked-component.js
import React from 'react'
import { Link } from 'react-router'
export default class App extends React.Component {
render() {
return (
<div>
<Link to='/styled' />
</div>
)
}
}
```
---
## Paramètres de route
```
// my-app/src/app.js
import React from 'react'
import { HashRouter } from 'react-router-dom' // ou BrowserRouter
import { Route } from 'react-router'
import { MyRouteParameter } from './components'
export default class App extends React.Component {
render() {
<HashRouter>
<Route path='/:myParam' component={MyRouteParameter} />
</HashRouter>
}
}
```
```
// my-app/src/components/my-route-parameter.js
import React from 'react'
import { HashRouter } from 'react-router-dom' // ou BrowserRouter
import { Route } from 'react-router'
import { MyRouteParameter } from './components'
export default class App extends React.Component {
render() {
const { match } = this.props
<span>Paramètre: { match.params.myParam }</span>
}
}
```
---
### Sous routes
```
// my-app/src/app.js
import React from 'react'
import { HashRouter } from 'react-router-dom'
import { Route, Switch } from 'react-router'
import { ProductPage } from './components'
export default class App extends React.Component {
render() {
<HashRouter>
<Switch>
<Route path='/products/:productId' component={ProductPage} />
</Switch>
</HashRouter>
}
}
```
```
// my-app/src/component/product.js
import React from 'react'
import { Route, Switch } from 'react-router'
import ProductView from './product-view'
import ProductEdit from './product-edit'
export default class ProductPage extends React.Component {
render() {
const { match } = this.props
return (
<Switch>
<Route path={`${match.url}/view`} component={ProductView} />
<Route path={`${match.url}/edit`} component={ProductEdit} />
</Switch>
)
}
}
```
---
## Et plus encore...
- [Transitions](https://reacttraining.com/react-router/web/example/animated-transitions)
- [Liens personnalisés](https://reacttraining.com/react-router/web/example/custom-link)
- [Chemins récursifs...](https://reacttraining.com/react-router/web/example/recursive-paths)
---
## État de l'application
---
## Architecture Flux
![center 100%](img/flux-simple-f8-diagram-with-client-action-1300w.png)
---
## Redux
https://redux.js.org/
---
![bg right:75% contain](./img/redux_flow.png?1)
## Flot des données
---
## `store`, `actions` et `reducers`
### Générateurs d'actions
```js
// actions.js
export const ADD_PRODUCT = 'ADD_PRODUCT'
export function addProduct (name, price) {
return {type: ADD_PRODUCT, product: {name, price}}
}
export const REMOVE_PRODUCT = 'REMOVE_PRODUCT'
export function removeProduct (name) {
return {type: REMOVE_PRODUCT, productName: name}
}
```
---
### "Reducer" racine
```js
// reducer.js
export default const rootReducer = (state, action) => {
console.log(`Action: ${JSON.stringify(action)}`)
switch (action.type) {
case ADD_PRODUCT:
// L'action est de type ADD_PRODUCT
// On ajoute le produit dans la liste et
// on retourne un nouvel état modifié
return {
products: [...state.products, action.product]
}
case REMOVE_PRODUCT:
// L'action est de type REMOVE_PRODUCT
// On filtre la liste des produits et on
// retourne un nouvel état modifié
return {
products: state.products.filter(p => p.name !== action.productName)
}
}
// Si l'action n'est pas gérée, on retourne l'état
// sans le modifier
return state
}
```
---
## Création du `store`
```javascript
// store.js
import { createStore } from 'redux'
import rootReducer from './reducer'
export function configureStore(initialState = { products: [] }) {
return createStore(
rootReducer,
initialState
)
}
```
---
## Tester
```js
// store.test.js
/* globals test, expect, jest */
import { addProduct, removeProduct } from './actions'
import { configureStore } from './store'
test('Ajout/suppression des produits', () => {
// On crée une instance de notre store
// avec le state par défaut
const store = configureStore()
// On crée un "faux" subscriber
// pour vérifier que l'état du store
// a bien été modifié le nombre de fois voulu
const subscriber = jest.fn()
// On attache notre faux subscriber
// au store
store.subscribe(subscriber)
// On "dispatch" nos actions
store.dispatch(addProduct('pomme', 5))
store.dispatch(addProduct('orange', 7))
store.dispatch(removeProduct('pomme'))
// On s'assure que notre subscriber a bien été
// appelé
expect(subscriber).toHaveBeenCalledTimes(3)
const state = store.getState()
// On s'assure que l'état du store correspond
// à ce qu'on attend
expect(state).toMatchObject({
products: [
{name: 'orange', price: 7}
]
})
})
```
---
## Actions asynchrones
https://github.com/gaearon/redux-thunk
```bash
npm install --save redux-thunk
```
---
### Ajout du middleware au `store`
```
// store.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducer'
export function configureStore(initialState = { products: [] }) {
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk) // On ajoute le middleware 'thunk'
)
}
```
---
## Générateur d'actions asynchrones
```
// actions.js
export const SAVE_PRODUCT = 'SAVE_PRODUCT'
export const SAVE_PRODUCT_SUCCESS = 'SAVE_PRODUCT_SUCCESS'
export const SAVE_PRODUCT_FAILURE = 'SAVE_PRODUCT_FAILURE'
export function saveProduct (product) {
return (dispatch, getState) => {
dispatch({type: SAVE_PRODUCT, product})
return fetch('http://my-api/products', {
method: 'POST',
body: JSON.stringify(product)
})
.then(res => res.json())
.then(res => dispatch({ type: SAVE_PRODUCT_SUCCESS, product: res.data }))
.catch(error => dispatch({ type: SAVE_PRODUCT_FAILURE, error }))
}
}
```
---
## Utilisation avec React
### Installer les dépendances
```bash
npm install --save react-redux
```
---
### Déclaration de l'usage du `store` dans l'application
```
// my-app/src/index.js
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './app'
import { configureStore } from './store'
const store = configureStore()
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
)
```
---
```
// my-app/src/app.js
import React from 'react'
import { addProduct } from './actions'
import { connect } from 'react-redux'
class App extends React.Component {
componentDidMount() {
// Toutes les 2 secondes, on "dispatch" une nouvelle
// action qui va ajouter un produit dans le store
setInterval(() => {
this.props.dispatch(addProduct(`My product #${Date.now()}`, Math.random()*10|0))
}, 2000)
}
render() {
// On crée une liste à partir des éléments présents
// dans le tableaux de produits
const items = this.props.products.map((p, i) => {
return <li key={i}>{p.name}</li>
})
return (
<ul>
{items}
</ul>
)
}
}
// Cette fonction permet de "sélectionner" les éléments
// qui nous intéressent dans le state et de les exposer
// en tant que "props" sur notre composant
const mapStateToProps = state => {
return {
products: state.products
}
}
// On "emballe" notre composant dans un "higher order component" (ou H.O.C.)
// ce qui nous permet de récupérer les données à partir du store
export default connect(mapStateToProps)(App)
```
---
## Redux-Saga
ou comment gérer les **"effets de bord"**
---
![bg right:75% contain](./img/reduxsaga_flow.png?1)
## Flot des données
---
### Cas d'usage
React-Redux permet d'intégrer au sein du "flux de travail" Redux la gestion des opérations asynchrones.
**Exemple** _Appels à des API distantes, opérations liées au passage du temps, messages websockets..._
Notamment, il offre une gestion de la **synchronisation d'opérations asynchrones** i.e. je dois attendre que l'opération A et l'opération B soient terminées pour effectuer l'opération C.
---
### Installation de la dépendance
```bash
npm install --save redux-saga
```
---
### Ajout du middleware
```js
// src/store/store.js
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'
import myReducer from '../reducers/my'
import rootSaga from '../sagas/root'
import createSagaMiddleware from 'redux-saga'
const sagaMiddleware = createSagaMiddleware()
const rootReducer = combineReducers({
my: myReducer,
});
export function configureStore(initialState = {}) {
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(sagaMiddleware)
)
)
sagaMiddleware.run(rootSaga);
return store;
}
```
---
## Création de la saga "racine"
_Avec une saga "factice" d'authentification_
```js
// src/sagas/root.js
import { all, takeLatest } from 'redux-saga/effects';
import { LOGIN_REQUEST } from '../actions/auth';
import { loginSaga } from './auth';
export default function* rootSaga() {
yield all([
takeLatest(LOGIN_REQUEST, loginSaga),
]);
}
```
---
## Définition des actions
```js
// src/actions/auth.js
export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
export function login(username, password) {
return { type: LOGIN_REQUEST, username, password }
}
export function loginFailure(username, error) {
return { type: LOGIN_FAILURE, username, error }
}
export function loginSuccess(username) {
return { type: LOGIN_SUCCESS, username }
}
```
---
## Création de ma saga d'authentification
```js
// src/sagas/auth.js
import { call, put } from 'redux-saga/effects';
import { loginFailure, loginSuccess } from '../actions/auth';
export default function* loginSaga(action) {
let result;
try {
result = yield call(doLogin, action.username, action.password);
} catch(err) {
yield put(loginFailure(action.username, err));
}
if ('error' in result) {
yield put(loginFailure(action.username, result.error));
return
}
yield put(loginSuccess(action.username));
}
function doLogin(username, password) {
return fetch('http://my-backend.org/login', {
method: 'POST',
body: JSON.stringify({
Username: username,
Password: password
}),
mode: 'cors',
credentials: 'include'
}).then(res => res.json())
}
```
---
## Mise en application
---
### Contexte
Implémenter une application de type "catalogue" comprenant les fonctionnalités suivantes:
- Créer/modifier/supprimer des fiches "produit"
- Trouver des produits via un système de recherche multicritères
- Un mécanisme d'authentification par mot de passe
- Un système de rôles basiques:
- Un rôle "administrateur" pouvant effectuer toutes les actions
- Un rôle "éditeur" pouvant créer/modifier/supprimer des fiches existantes
- Un rôle "consultant" permettant de lire/rechercher des fiches existantes
---
### Contraintes
Vous pouvez sélectionner n'importe quel type de produit à implémenter (album de musique, collection de pins, bestiaire de jeu de rôle...). Voici cependant les différents types d'attributs qui devront être présents sur une fiche produit:
- Un attribut de type numérique
- Un attribut de type "liste de mots clés"
- Un attribut de type "texte libre"
- Un attribut de type "fichier"
La partie "backend" est laissée à votre libre choix.
---
# Licence
## CC BY-NC-SA 3.0 FR
[Creative Commons - Attribution - Pas dUtilisation Commerciale - Partage dans les Mêmes Conditions 3.0 France](https://creativecommons.org/licenses/by-nc-sa/3.0/fr/)