diff --git a/developpement/framework_web/presentation/img/flux-simple-f8-diagram-with-client-action-1300w.png b/developpement/framework_web/presentation/img/flux-simple-f8-diagram-with-client-action-1300w.png new file mode 100644 index 0000000..9286f70 Binary files /dev/null and b/developpement/framework_web/presentation/img/flux-simple-f8-diagram-with-client-action-1300w.png differ diff --git a/developpement/framework_web/presentation/img/redux_flow.epgz b/developpement/framework_web/presentation/img/redux_flow.epgz new file mode 100644 index 0000000..1820dbd Binary files /dev/null and b/developpement/framework_web/presentation/img/redux_flow.epgz differ diff --git a/developpement/framework_web/presentation/img/redux_flow.png b/developpement/framework_web/presentation/img/redux_flow.png new file mode 100644 index 0000000..0943c4a Binary files /dev/null and b/developpement/framework_web/presentation/img/redux_flow.png differ diff --git a/developpement/framework_web/presentation/slides.md b/developpement/framework_web/presentation/slides.md new file mode 100644 index 0000000..92eff17 --- /dev/null +++ b/developpement/framework_web/presentation/slides.md @@ -0,0 +1,1137 @@ + + +# React + Redux +## William Petit - S.C.O.P. Cadoles + +--- + +## Amorçage d'un projet + +--- + + + +## 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 + + + + + + + MyApp + <% for (var css in htmlWebpackPlugin.files.css) { %> + + <% } %> + +
+ + +``` +https://github.com/jantimon/html-webpack-plugin + +--- + +```js +// my-app/src/index.js + +import ReactDOM from 'react-dom' +import App from './app' + +ReactDOM.render(, 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

Hello World !

+ } +} + +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 ( +
+

Hello World

+

Welcome to React

+
+ ) + } + +} + +export default HelloWorld +``` +--- + +## Notation fonctionnelle + +```jsx +// my-app/src/components/hello-world.js + +const HelloWorld = () => { + return ( +
+

Hello World

+

Welcome to React

+
+ ) +} + +export default HelloWorld +``` +--- + +## La syntaxe JSX + +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` + +Elle 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 ( + { this.props.text } + ) + } + +} + +export default PropsExample + +// Exemple d'utilisation + +// + +``` +--- + +### `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 ( + + ) + } + +} + +export default PropsExample + + +// Exemple d'utilisation + + +
  • Item 1
  • +
  • Item 2
  • +
    + +``` + +--- + +## `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 ( +
    Time: { this.state.time.toString() }
    + ) + } + + // 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` 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 ( +
    + Count: { this.state.count }  + + +
    + ) + } + // 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 })) + } +} +``` + +--- + +## Gestion des styles + +Différentes méthodologies existes 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 ( +
    +
    + Isnt it pretty ? +
    +
    + ) + } + +} +``` +--- +## 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 ( +
    +
    + Isnt it pretty ? +
    +
    + ) + } + +} + +// Exemple d'utilisation + + + +``` +--- + +## Cycle de vie et méthodes + +https://reactjs.org/docs/react-component.html + +--- + +### Séquence de montage + +1. `constructor(props)` +2. `componentWillMount()` +3. `render()` +4. `componentDidMount()` + +--- + +### Séquence de mise à jour + +1. `componentWillReceiveProps(nextProps)` +2. `shouldComponentUpdate(nextProps, nextState)` +3. `componentWillUpdate(nextProps, nextState)` +4. `render()` +5. `componentDidUpdate(prevProps, prevState)` + +--- + +### 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 ( +
    + + +
    + ); + } +} +``` +--- + +## 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() + + 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() { + + + + + + + + + + + + + } + +} +``` + +--- + +## 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 ( +
    + +
    + ) + } +} +``` +--- + +## 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() { + + + + } +} +``` +``` +// 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 + Paramètre: { match.params.myParam } + } +} +``` + +--- + +### 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() { + + + + + + } +} +``` +``` +// 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 ( + + + + + ) + } +} +``` +--- + +## 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/ + +--- + +## Flot des données + +![center 100%](./img/redux_flow.png?1) + +--- + +## `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 retoure l'état + // sans le modifier + return state + +} +``` + +--- + +## Création du `store` +``` +// store.js + +import { createStore } from 'redux' +import rootReducer from './reducer' + +export function configureStore(initialState = { products: [] }) { + return createStore( + rootReducer, + initialState + ) +} +``` +--- +## Tester + +``` +// store.test.js + +/* globals test, expect, jest */ +import { addProduct, removeProduct } from './actions' +import { configureStore } from './store' + +test('Ajout/suppression des produits', () => { + // On créait une instance de notre store + // avec le state par défaut + const store = configureStore() + + // On créait un "faux" subscriber + // pour vérifier que l'état du store + // à 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( + + + , + 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éait une liste à partir des éléments présents + // dans le tableaux de produits + const items = this.props.products.map((p, i) => { + return
  • {p.name}
  • + }) + return ( +
      + {items} +
    + ) + } + +} + +// 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) +``` + +--- + +## 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 d’Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 France](https://creativecommons.org/licenses/by-nc-sa/3.0/fr/) \ No newline at end of file