├── .github └── ISSUE_TEMPLATE ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── how-to-translate.md ├── img ├── bootstrap-padded-90.png ├── bootstrap-padded.png ├── chai.png ├── eslint-padded-90.png ├── eslint-padded.png ├── eslint.png ├── flow-padded-90.png ├── flow-padded.png ├── flow.png ├── jest-padded-90.png ├── jest-padded.png ├── jest.png ├── js-padded.png ├── js.png ├── mocha.png ├── npm.png ├── pm2-padded-90.png ├── pm2-padded.png ├── pm2.png ├── react-padded-90.png ├── react-padded.png ├── react-router-padded-90.png ├── react-router-padded.png ├── react-router.png ├── react.png ├── redux-padded-90.png ├── redux-padded.png ├── redux.png ├── webpack-padded-90.png ├── webpack-padded.png ├── webpack.png ├── yarn-padded-90.png ├── yarn-padded.png └── yarn.png ├── mdlint.js ├── package.json ├── tutorial ├── 01-node-yarn-package-json.md ├── 02-babel-es6-eslint-flow-jest-husky.md ├── 03-express-nodemon-pm2.md ├── 04-webpack-react-hmr.md ├── 05-redux-immutable-fetch.md ├── 06-react-router-ssr-helmet.md ├── 07-socket-io.md ├── 08-bootstrap-jss.md └── 09-travis-coveralls-heroku.md └── yarn.lock /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | ### Type of issue: (feature suggestion, bug?) 2 | 3 | ### Chapter: 4 | 5 | ### If it's a bug: 6 | 7 | Please try using the code provided instead of your own to see if that solves the issue. If it does, compare the content of relevant files to see if anything is missing in your version. Every chapter is automatically tested, so issues are likely coming from missing an instruction in the tutorial or a typo. Feel free to open an issue if there is a problem with instructions though. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v2.4.5 4 | 5 | - Add `babel-plugin-flow-react-proptypes`. 6 | - Add `eslint-plugin-compat`. 7 | - Add JSS `composes` example. 8 | 9 | ## v2.4.4 10 | 11 | - Update Immutable to remove the `import * as Immutable from 'immutable'` syntax. 12 | - Declare Flow types outside of function params for React components. 13 | - Improve Webpack `publicPath`. 14 | 15 | ## V2, up to v2.4.3 16 | 17 | - Gulp is gone, replaced by NPM (Yarn) scripts. 18 | - Express has been added, with template strings for static HTML. Gzip compression enabled. 19 | - Support for development environment with Nodemon and production environment with PM2. 20 | - Minification or sourcemaps depending on the environment via Webpack. 21 | - Add Webpack Dev Server, with Hot Module Replacement and `react-hot-loader`. 22 | - Add an asynchronous call example with `redux-thunk`. 23 | - Linting / type checking / testing is not launched at every file change anymore, but triggered by Git Hooks via Husky. 24 | - Some chapters have been combined to make it easier to maintain the tutorial. 25 | - Replace Chai and Mocha by Jest. 26 | - Add React-Router, Server-Side rendering, `react-helmet`. 27 | - Rename all "dog" things and replaced it by "hello" things. It's a Hello World app after all. 28 | - Add Twitter Bootstrap, JSS, and `react-jss` for styling. 29 | - Add a Websocket example with Socket.IO. 30 | - Add optional Heroku, Travis, and Coveralls integrations. 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2017 Jonathan Verrecchia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Stack from Scratch :ok_hand: 2 | 3 | [![Build Status](https://travis-ci.org/verekia/js-stack-from-scratch.svg?branch=master)](https://travis-ci.org/verekia/js-stack-from-scratch) 4 | [![Release](https://img.shields.io/github/release/verekia/js-stack-from-scratch.svg?style=flat-square)](https://github.com/verekia/js-stack-from-scratch/releases) 5 | [![Dependencies](https://img.shields.io/david/verekia/js-stack-boilerplate.svg?style=flat-square)](https://david-dm.org/verekia/js-stack-boilerplate) 6 | [![Dev Dependencies](https://img.shields.io/david/dev/verekia/js-stack-boilerplate.svg?style=flat-square)](https://david-dm.org/verekia/js-stack-boilerplate?type=dev) 7 | [![Gitter](https://img.shields.io/gitter/room/js-stack-from-scratch/Lobpar.svg?style=flat-square)](https://gitter.im/js-stack-from-scratch/) 8 | 9 | [![React](/img/react-padded-90.png)](https://facebook.github.io/react/) 10 | [![Redux](/img/redux-padded-90.png)](http://redux.js.org/) 11 | [![React Router](/img/react-router-padded-90.png)](https://github.com/ReactTraining/react-router) 12 | [![Flow](/img/flow-padded-90.png)](https://flowtype.org/) 13 | [![ESLint](/img/eslint-padded-90.png)](http://eslint.org/) 14 | [![Jest](/img/jest-padded-90.png)](https://facebook.github.io/jest/) 15 | [![Yarn](/img/yarn-padded-90.png)](https://yarnpkg.com/) 16 | [![Webpack](/img/webpack-padded-90.png)](https://webpack.github.io/) 17 | [![Bootstrap](/img/bootstrap-padded-90.png)](http://getbootstrap.com/) 18 | 19 | Bienvenue sur ce tutoriel de prise en main d'une *stack* (ensemble d'outils :fr:) Javascript moderne : **JavaScript Stack from Scratch**. 20 | 21 | > :exclamation: **Il s'agit de la V2 du tutoriel, des changements majeurs sont apparus depuis la première version en 2016. Jetez un oeil au [Change Log](/CHANGELOG.md) !** 22 | 23 | Il s'agit d'un guide allant droit au but sur comment mettre en place toute une *stack* JavaScript. Pour le suivre, il faut avoir un minimum de connaissances en programmation ainsi que des bases de JavaScript. **Ce guide se concentre sur comment relier les outils ensemble** et vous donne les **exemples les plus simples possible** pour chaque outil. Vous pouvez voir ce tutoriel comme *un moyen d'écrire votre boilerplate (*modèle* :fr: *) depuis zéro*. Le but de ce tutoriel étant d'assembler divers outils, on ne va pas explorer en détails comment ces outils fonctionnent individuellement. Pour cela, référez-vous à leur documentation ou essayez de trouver d'autres tutoriels si vous voulez en acquérir une plus grande connaissance. 24 | 25 | Vous n'avez pas besoin d'utiliser la *stack* entière si vous construisez une simple page web avec quelques interactions en JS (une combinaison Browserify/Webpack + Babel + jQuery est suffisant pour écrire en ES6 dans différents fichiers), mais si vous voulez construire une web app évolutive et avez besoin d'aide pour mettre tous les différents outils en place, alors ce tutoriel est parfait pour vous ! :+1: 26 | 27 | Une bonne partie de la *stack* décrite dans ce tutoriel utilise React. Si vous débutez et avez juste envie d'apprendre React, [create-react-app](https://github.com/facebookincubator/create-react-app) (:uk:) vous permettra de mettre en place un environnement React très rapidement avec une config déjà préparée. Nous vous recommandons cette approche si par exemple vous arrivez dans une équipe qui utilise React et que vous avez besoin d'une petite mise à niveau avec un environnement d'apprentissage. Dans ce tutoriel, nous n'utiliserons pas de configuration toute prête car nous souhaitons que vous compreniez tout ce qui se passe sous le capot. 28 | 29 | Des exemples de code sont disponibles pour chaque chapitre. Vous pouvez les lancer avec `yarn && yarn start`. Cependant, nous vous recommandons d'écrire tout par vous-même en suivant les instructions étape par étape. :wink: 30 | 31 | 32 | Le code final est disponible dans le [repo JS-Stack-Boilerplate](https://github.com/verekia/js-stack-boilerplate), et dans les [releases](https://github.com/verekia/js-stack-from-scratch/releases). Il y a aussi une [démo live](https://js-stack.herokuapp.com/). 33 | 34 | Ce tutoriel fonctionne sur les plateformes suivantes: Linux, OSX, Windows. 35 | 36 | ## Sommaire 37 | 38 | [01 - Node, Yarn, `package.json`](/tutorial/01-node-yarn-package-json.md#readme) 39 | 40 | [02 - Babel, ES6, ESLint, Flow, Jest, Husky](/tutorial/02-babel-es6-eslint-flow-jest-husky.md#readme) 41 | 42 | [03 - Express, Nodemon, PM2](/tutorial/03-express-nodemon-pm2.md#readme) 43 | 44 | [04 - Webpack, React, HMR](/tutorial/04-webpack-react-hmr.md#readme) 45 | 46 | [05 - Redux, Immutable, Fetch](/tutorial/05-redux-immutable-fetch.md#readme) 47 | 48 | [06 - React Router, Server-Side Rendering, Helmet](/tutorial/06-react-router-ssr-helmet.md#readme) 49 | 50 | [07 - Socket.IO](/tutorial/07-socket-io.md#readme) 51 | 52 | [08 - Bootstrap, JSS](/tutorial/08-bootstrap-jss.md#readme) 53 | 54 | [09 - Travis, Coveralls, Heroku](/tutorial/09-travis-coveralls-heroku.md#readme) 55 | 56 | ## A venir :fire: 57 | 58 | Configurer votre éditeur (Atom dans un premier temps), MongoDB, Progressive Web App, test E2E. 59 | 60 | ## Traductions :uk: :fr: :de: :cn: :jp: :ru: 61 | 62 | Si vous souhaitez ajouter votre traduction, merci de lire les [recommandations de traduction](/how-to-translate.md) pour vous lancer ! 63 | 64 | ### V2 65 | 66 | - [Bulgare](https://github.com/mihailgaberov/js-stack-from-scratch) par [mihailgaberov](http://github.com/mihailgaberov) 67 | - [Italien](https://github.com/fbertone/guida-javascript-moderno) par [Fabrizio Bertone](https://github.com/fbertone) - [fbertone.it](http://fbertone.it) 68 | - [Chinois simplifié](https://github.com/yepbug/js-stack-from-scratch/) par [@yepbug](https://github.com/yepbug) 69 | - [Français](https://github.com/naomihauret/js-stack-from-scratch/) par [Naomi Hauret](https://twitter.com/naomihauret) 70 | 71 | Vous pouvez jeter un oeil aux autres [traductions en cours](https://github.com/verekia/js-stack-from-scratch/issues/147). 72 | 73 | ### V1 :baby: 74 | 75 | - [中文](https://github.com/pd4d10/js-stack-from-scratch) par [@pd4d10](http://github.com/pd4d10) 76 | - [Italien](https://github.com/fbertone/js-stack-from-scratch) par [Fabrizio Bertone](https://github.com/fbertone) 77 | - [日本語](https://github.com/takahashim/js-stack-from-scratch) par [@takahashim](https://github.com/takahashim) 78 | - [Русский](https://github.com/UsulPro/js-stack-from-scratch) par [React Theming](https://github.com/sm-react/react-theming) 79 | - [ไทย](https://github.com/MicroBenz/js-stack-from-scratch) par [MicroBenz](https://github.com/MicroBenz) 80 | 81 | ## Crédits 82 | 83 | Créé par [@verekia](https://twitter.com/verekia) – [verekia.com](http://verekia.com/). 84 | Traduit par [@naomihauret](https://twitter.com/naomihauret) 85 | 86 | Licence: MIT 87 | -------------------------------------------------------------------------------- /how-to-translate.md: -------------------------------------------------------------------------------- 1 | # Comment traduire ce tutoriel 2 | 3 | Merci pour l'intérêt que vous portez à ce tutoriel! Voici quelques recommandations avant de vous lancez. 4 | 5 | Ce tutoriel est en constante évolution afin de proposer aux lecteurs la meilleure expérience d'apprentissage possible. Le code et le fichier `README.md` changerons tous les deux au fil du temps. C'est génial si vous parvenez à faire une traduction en un seul coup, mais ce serait encore mieux si vous pouviez l'adapter en même temps que la version originale en anglais change ! 6 | 7 | Voici ce que nous pensons être un bon *workflow* : 8 | 9 | - Vérifiez s'il y a déjà une [traduction en cours](https://github.com/verekia/js-stack-from-scratch/issues/147) pour votre langue. Si c'est le cas, mettez vous en contact avec les gens qui l'ont ouvert et essayez de travailler ensemble. Tous les mainteneurs seront mentionnés dans le repo en anglais. Le travail de groupe est donc encouragé! Vous pouvez ouvrir des issues sur leur traduction, forker leur projet pour offrir votre aide sur certains chapitres par exemple. 10 | 11 | - Envie de bavarder ? Rejoignez la [room Gitter traduction](https://gitter.im/js-stack-from-scratch/Translations). 12 | 13 | - Forkez le [repo anglais](https://github.com/verekia/js-stack-from-scratch). 14 | 15 | - Postez dans [cette issue](https://github.com/verekia/js-stack-from-scratch/issues/147) la langue et l'URL de votre repo. 16 | 17 | - Traduisez les fichiers `README.md`. 18 | 19 | - Laissez un mot quelque part sur le `README.md` principal précisant qu'il s'agit d'une traduction, avec un lien vers le repo anglais. Si vous ne prévoyez pas de faire évoluer votre traduction au fil du temps, essayez peut-être de faire une référence au repo anglais pour avoir la version la plus à jour de ce tutoriel. C'est à vous de décider ! :wink: 20 | 21 | - Faites une pull Request au repo anglais pour ajouter le lien de votre repo forké dans la rubrique 'Translations' du `README.md` principal. Ca donnerait quelque chose comme ça : 22 | 23 | ```md 24 | ## Translations 25 | 26 | - [Language](http://github.com/yourprofile/your-fork) by [You](http://yourwebsite.com) 27 | ou 28 | - [Language](http://github.com/yourprofile/your-fork) by [@You](http://twitter.com/yourprofile) 29 | ou 30 | - [Language](http://github.com/yourprofile/your-fork) by [@You](http://github.com/yourprofile) 31 | ``` 32 | 33 | Puisque nous voulons vous remerciez pour votre travail autant que possible, vous pouvez mettre le lien que vous voulez sur votre nom (twitter, github, site perso...) 34 | 35 | - Après avoir *one-shot* votre traduction, si vous voulez modifier votre repo avec les derniers changements du repo anglais, [synchronisez votre fork](https://help.github.com/articles/syncing-a-fork/) avec le repo original. Afin qu'il soit plus aisé de voir ce qui a changé depuis votre traduction, vous pouvez utiliser la fonctionnalité de Github pour [comparer les commits](https://help.github.com/articles/comparing-commits-across-time/#comparing-commits). Configurez la **base** au dernier commit du repo anglais, et comparez le à la branche **master** comme ceci : 36 | 37 | 38 | https://github.com/verekia/js-stack-from-scratch/compare/c65dfa65d02c21063d94f0955de90947ba5273ad...master 39 | 40 | 41 | Ca devrait vous donnez une diff facile à lire pour voir exactment ce qui a changé dans les fichiers `README.md` depuis votre traduction ! 42 | -------------------------------------------------------------------------------- /img/bootstrap-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/bootstrap-padded-90.png -------------------------------------------------------------------------------- /img/bootstrap-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/bootstrap-padded.png -------------------------------------------------------------------------------- /img/chai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/chai.png -------------------------------------------------------------------------------- /img/eslint-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/eslint-padded-90.png -------------------------------------------------------------------------------- /img/eslint-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/eslint-padded.png -------------------------------------------------------------------------------- /img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/eslint.png -------------------------------------------------------------------------------- /img/flow-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/flow-padded-90.png -------------------------------------------------------------------------------- /img/flow-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/flow-padded.png -------------------------------------------------------------------------------- /img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/flow.png -------------------------------------------------------------------------------- /img/jest-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/jest-padded-90.png -------------------------------------------------------------------------------- /img/jest-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/jest-padded.png -------------------------------------------------------------------------------- /img/jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/jest.png -------------------------------------------------------------------------------- /img/js-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/js-padded.png -------------------------------------------------------------------------------- /img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/js.png -------------------------------------------------------------------------------- /img/mocha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/mocha.png -------------------------------------------------------------------------------- /img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/npm.png -------------------------------------------------------------------------------- /img/pm2-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/pm2-padded-90.png -------------------------------------------------------------------------------- /img/pm2-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/pm2-padded.png -------------------------------------------------------------------------------- /img/pm2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/pm2.png -------------------------------------------------------------------------------- /img/react-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/react-padded-90.png -------------------------------------------------------------------------------- /img/react-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/react-padded.png -------------------------------------------------------------------------------- /img/react-router-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/react-router-padded-90.png -------------------------------------------------------------------------------- /img/react-router-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/react-router-padded.png -------------------------------------------------------------------------------- /img/react-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/react-router.png -------------------------------------------------------------------------------- /img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/react.png -------------------------------------------------------------------------------- /img/redux-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/redux-padded-90.png -------------------------------------------------------------------------------- /img/redux-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/redux-padded.png -------------------------------------------------------------------------------- /img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/redux.png -------------------------------------------------------------------------------- /img/webpack-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/webpack-padded-90.png -------------------------------------------------------------------------------- /img/webpack-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/webpack-padded.png -------------------------------------------------------------------------------- /img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/webpack.png -------------------------------------------------------------------------------- /img/yarn-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/yarn-padded-90.png -------------------------------------------------------------------------------- /img/yarn-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/yarn-padded.png -------------------------------------------------------------------------------- /img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naomiHauret/js-stack-from-scratch/2022bd35bf2cb7953473411a74e9e3587ecff645/img/yarn.png -------------------------------------------------------------------------------- /mdlint.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob') 2 | const markdownlint = require('markdownlint') 3 | 4 | const config = { 5 | 'default': true, 6 | 'line_length': false, 7 | 'no-emphasis-as-header': false, 8 | } 9 | 10 | const files = glob.sync('**/*.md', { ignore: '**/node_modules/**' }) 11 | 12 | markdownlint({ files, config }, (err, result) => { 13 | if (!err) { 14 | const resultString = result.toString() 15 | console.log('== Linting Markdown Files...') 16 | if (resultString) { 17 | console.log(resultString) 18 | process.exit(1) 19 | } else { 20 | console.log('== OK!') 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "2.4.5", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "test": "node mdlint.js" 7 | }, 8 | "devDependencies": { 9 | "glob": "^7.1.1", 10 | "markdownlint": "^0.4.0" 11 | }, 12 | "repository": "verekia/js-stack-from-scratch", 13 | "author": "Jonathan Verrecchia - @verekia", 14 | "license": "MIT" 15 | } 16 | -------------------------------------------------------------------------------- /tutorial/01-node-yarn-package-json.md: -------------------------------------------------------------------------------- 1 | # 01 - Node, Yarn et `package.json` 2 | 3 | Le code de ce chapitre est disponible [ici](https://github.com/verekia/js-stack-walkthrough/tree/master/01-node-yarn-package-json). 4 | 5 | Dans cette section, nous allons configurer Node, Yarn, un fichier `package.json` basique, et tester un *package* (paquet :fr:). 6 | 7 | ## Node :computer: 8 | 9 | > :bulb: **[Node.js](https://nodejs.org/)** est un environnement d'exécution JavaScript. On l'utilise principalement pour du développement back-end, mais aussi pour du scripting de façon générale. En développement front-end, Node peut être utilisé pour exécuter une série de tâches comme le linting, les tests ou encore la concaténation de fichiers. 10 | 11 | Tout au long de ce tutoriel, nous allons utiliser Node pour pratiquement **tout**, vous allez donc en avoir besoin ! Rendez-vous sur la [page de téléchargement](https://nodejs.org/en/download/current/) pour les installeurs **macOS/OSX** ou **Windows**, ou la [page d'installation via gestionnaire de paquets](https://nodejs.org/en/download/package-manager/) pour les distributions Linux. 12 | 13 | Par exemple, sur **Ubuntu / Debian**, vous exécuterez les commandes suivantes pour lancer Node : 14 | 15 | ```sh 16 | curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash - 17 | sudo apt-get install -y nodejs 18 | ``` 19 | 20 | Vous allez avoir besoin de n'importe quelle version de Node qui soit supérieure à la 6.5.0. 21 | 22 | ## Node Version Management Tools :wrench: 23 | 24 | Si vous avez besoin de flexibilité et de pouvoir utiliser plusieurs versions de Node différentes, jetez un oeil à [NVM](https://github.com/creationix/nvm) ou [tj/n](https://github.com/tj/n). 25 | 26 | ## NPM :bear: 27 | 28 | NPM est le package manager (*gestionnaire de paquets* :fr:) par défaut pour Node. Il est installé automatiquement, en même temps que Node. Les gestionnaires de packages sont utilisés pour installer et gérer des packages (des modules de code que vous ou quelqu'un d'autre a écrit). Nous allons utiliser beaucoup de packages dans ce tutoriel mais, au lieu de Node, nous utiliserons un autre package manager: Yarn. 29 | 30 | ## Yarn :cat: 31 | 32 | > :bulb: **[Yarn](https://yarnpkg.com/)** est un package manager beaucoup plus rapide que NPM qui offre le support hors-ligne et récupère les dépendances [de façon plus prédictible](https://yarnpkg.com/en/docs/yarn-lock). 33 | 34 | Depuis qu'il est [sorti](https://code.facebook.com/posts/1840075619545360) en octobre 2016, Yarn a rapidement été adopté. Il pourrait d'ailleurs bientôt devenir le package manager plébiscité par la communauté JavaScript. Si vous voulez rester avec NPM, vous pouvez tout simplement remplacer toutes les commandes `yarn add` et `yarn add --dev` de ce tutoriel par `npm install --save` et `npm install --save-dev`. 35 | 36 | Installez Yarn en suivant les [instructions](https://yarnpkg.com/en/docs/install) pour votre système d'exploitation. Nous vous recommandons d'utiliser le **script d'installation** de l'onglet *Alternatives* si vous êtes sous macOS ou Unix, pour [éviter](https://github.com/yarnpkg/yarn/issues/1505) d'avoir à recourir à d'autres gestionnaires de packages : 37 | 38 | ``` 39 | sh 40 | curl -o- -L https://yarnpkg.com/install.sh | bash 41 | ``` 42 | 43 | ## `package.json` :package: 44 | 45 | > :bulb: **[package.json](https://yarnpkg.com/en/docs/package-json)** est le fichier utilisé pour décrire et configurer votre projet JavaScript. Il contient toutes les informations générales (le nom de votre projet, sa version, les différents contributeurs, sa licence, etc), les configuration d'options pour les outils que vous utilisez, et même une section pour lancer des *tâches*. 46 | 47 | - Créez un nouveau dossier dans lequel vous allez travailler et rendez-vous dedans (`cd`). 48 | - Lancez `yarn init` et répondez aux questions (utilisez `yarn init -y` pour passer les questions). Cela générera un fichier `package.json` automatiquement. 49 | 50 | Voici le `package.json` basique que nous utiliserons dans ce tutoriel : 51 | 52 | ```json 53 | { 54 | "name": "votre-projet", 55 | "version": "1.0.0", 56 | "license": "MIT" 57 | } 58 | ``` 59 | 60 | ## Hello World :wave: 61 | 62 | - Créez un fichier `index.js` qui contient `console.log('Hello world')` 63 | 64 | :checkered_flag: Lancez `node .` dans ce dossier (`index.js` est le fichier par défaut que va regarder Node dans ce dossier Node). Votre console devrait afficher "Hello world". 65 | 66 | **Note**: Vous avez remarqué l'emoji drapeau de course :checkered_flag: ? Nous l'utiliserons chaque fois que vous attendrez un **checkpoint**. Nous allons parfois effectuer plusieurs gros changement d'un coup, et votre code pourrait ne pas fonctionner jusqu'à ce que vous atteignez le checkpoint suivant. 67 | 68 | ## Script `start` :rocket: 69 | 70 | Utiliser `node .` pour exécuter notre programme est un peu bas-niveau. À la place, nous allons utiliser un script NPM/Yarn pour déclencher l'exécution de ce code. Cela nous donnera un bon niveau d'abstraction pour pouvoir toujours utiliser `yarn start`, même quand notre programme deviendra plus complexe. 71 | 72 | - Dans le fichier `package.json`, ajoutez un objet `scripts` comme ceci : 73 | 74 | ```json 75 | { 76 | "name": "your-project", 77 | "version": "1.0.0", 78 | "license": "MIT", 79 | "scripts": { 80 | "start": "node ." 81 | } 82 | } 83 | ``` 84 | 85 | `start` est le nom que nous allons donner à la *tâche* qui va lancer notre programme. Nous allons créer de nombreuses tâches différentes dans cet objet `scripts` tout au long de ce tutoriel. `start` est le nom classique qu'on donne à la tâche par défaut d'une application. Parmi les autres noms de tâches standard, on retrouve aussi `stop` et `test`. 86 | 87 | `package.json` doit être un fichier JSON valide: il ne doit donc pas avoir de virgule en trop. Faites donc bien attention quand vous éditez votre fichier `package.json` manuellement. 88 | 89 | :checkered_flag: Lancez `yarn start`. Votre console devrait afficher `Hello world`. 90 | 91 | ## Git et `.gitignore` :octocat: 92 | 93 | - Initialisez un repo (*repository*, ou dépôt :fr:) Git avec `git init` 94 | 95 | - Créez un fichier `.gitignore` et ajoutez-y les lignes suivantes : 96 | 97 | ```gitignore 98 | .DS_Store 99 | /*.log 100 | ``` 101 | 102 | Les fichiers `.DS_Store` sont auto-générés par macOS. Vous ne devez jamais les avoir dans votre repo. 103 | 104 | `npm-debug.log` et `yarn-error.log` sont des fichiers qui sont créés quand votre package manager rencontre une erreur. On ne veut pas non plus versionner cela dans notre repo. 105 | 106 | ## Installer et utiliser un package :wrench: 107 | 108 | Dans cette section, nous allons installer et utiliser un package. Un "package", c'est un morceau de code que quelqu'un d'autre a écrit qu'on peut utiliser dans notre propre code. Ici, nous allons essayer un package qui va nous aider à manipuler les couleurs. 109 | 110 | - Installez le package créé par la communauté qui s'appelle `color` en lançant `yarn add color` 111 | 112 | Ouvrez votre `package.json`: Yarn a automatiquement ajouté `color` dans `dependencies`! 113 | 114 | Un dossier `node_modules` a aussi été créé: c'est ici qu'est placé le package qu'on vient d'installer. 115 | 116 | - ajouter le dossier `node_modules/` à votre `.gitignore` 117 | 118 | Vous avez remarqué ? Le fichier `yarn.lock` a été généré par Yarn. Vous devez *commit* ce fichier dans votre repo: il permet de s'assurer que tout le monde dans votre équipe va utiliser les mêmes versions de packages. Si vous utilisez NPM au lieu de Yarn, l'équivalent de ce fichier est le *shrinkwrap*. 119 | 120 | - Ecrivez les lignes suivantes dans votre fichier `index.js` : 121 | 122 | ```js 123 | const color = require('color') 124 | 125 | const redHexa = color({ r: 255, g: 0, b: 0 }).hex() 126 | 127 | console.log(redHexa) 128 | ``` 129 | 130 | :checkered_flag: Lancez `yarn start`. Votre console devrait afficher `#FF0000`. 131 | 132 | Félicitations, vous avez installé et utilisé un package ! :tada: 133 | 134 | `color` a juste été utilisé dans cette section pour vous apprendre comment utiliser un package simple. Nous n'en avons plus besoin, vous pouvez donc le désinstaller: 135 | 136 | - Lancez `yarn remove color` 137 | 138 | ## Deux types de dépendances 139 | 140 | Il y a deux types de *package dependencies* (dépendances de paquets :fr:), `"dependencies"` et `"devDependencies"`: 141 | 142 | **Les Dependencies** (dépendances :fr:) sont des bibliothèques dont vous avez besoin pour que votre application fonctionne (ex: React, Redux, Lodash, jQuery, Vue etc). On les installe avec `yarn add [package]`. 143 | 144 | **Les Dev Dependencies** (dépendances de développement :fr:) sont des bibliothèques utilisées pendant le développement de l'application ou pendant son *build* (ex: Webpack, SASS, linters, frameworks de test, etc). On les installe avec `yarn add --dev [package]`. 145 | 146 | Section suivante: [02 - Babel, ES6, ESLint, Flow, Jest, Husky](02-babel-es6-eslint-flow-jest-husky.md#readme) 147 | 148 | Retour au [sommaire](https://github.com/naomihauret/js-stack-from-scratch#table-of-contents). 149 | -------------------------------------------------------------------------------- /tutorial/02-babel-es6-eslint-flow-jest-husky.md: -------------------------------------------------------------------------------- 1 | # 02 - Babel, ES6, ESLint, Flow, Jest et Husky 2 | 3 | Le code de ce chapitre est disponible [ici](https://github.com/verekia/js-stack-walkthrough/tree/master/02-babel-es6-eslint-flow-jest-husky). 4 | 5 | Maintenant nous allons utiliser un peu de syntaxe ES6, qui est une grande amélioration pour la "vieille" syntaxe ES5. Tous les navigateurs et environnements JS comprennent l'ES5, **mais pas l'ES6**. C'est là qu'un certain outil appelé Babel vient à notre secours ! 6 | 7 | ## Babel 8 | 9 | > :bulb: **[Babel](https://babeljs.io/)** est un compilateur qui transforme le code ES6 (et d'autres trucs comme la syntaxe JSX de React par exemple) en code ES5. Babel est très modulaire et peut être utilisé dans une tonne d'[environnements différents](https://babeljs.io/docs/setup/). C'est de loin le compilateur ES5 préféré de la communauté React. 10 | 11 | - Déplacez votre fichier `index.js` dans un nouveau dossier appelé `src` (src pour *source*). C'est ici que nous allons écrire tout notre code ES6. Retirez tout le code précédent relatif à `color` et remplacez le par un simple : 12 | 13 | ```js 14 | const str = 'ES6' 15 | console.log(`Hello ${str}`) 16 | ``` 17 | 18 | Ici, nous utilisons une *template string* (chaîne de template :fr:), une fonctionnalité ES6 qui nous laisse injecter des variables directement dans une chaîne de caractères sans concaténation en utilisant `${}`. Notez que les template strings sont créées en utilisant des **backquotes** (`...`). 19 | 20 | - Lancez `yarn add --dev babel-cli` pour installer la CLI (*Command Line Interface*, interface en ligne de commande :fr:) de Babel. 21 | 22 | La CLI Babel est "livrée" avec [deux exécutables](https://babeljs.io/docs/usage/cli/): `babel`, qui compile nos fichiers ES6 en nouveaux, et `babel-node`, que vous pouvez utiliser pour remplacer la commande `node` et exécuter vos fichiers ES6 directement. `babel-node` est super pour le développement mais reste très lourd et ne doit pas être utilisé en production. Dans ce chapitre, nous allons utiliser `babel-node` pour mettre en place notre environnement de développement, et dans le prochain, nous utiliserons `babel` pour *build* nos fichiers ES5 pour la production. 23 | 24 | - Dans votre `package.json`, dans le script `start`, remplacez `node .` par `babel-node src` (`index.js` est le fichier par défaut que va chercher Node, c'est pourquoi on peut omettre de préciser `index.js`). 25 | 26 | Maintenant, si vous essayez de lancer `yarn start`, la console devrait afficher ce qu'il faut mais en fait, Babel ne fait rien du tout. C'est parce qu'on a pas indiqué quelles informations on souhaite appliquer. La seule raison pour laquelle les bonnes informations sont affichées... c'est parce que Node comprend nativement ES6, sans l'aide de Babel ! Pour autant, certains navigateurs ou des versions plus anciennes de Node ne réussiraient pas à en faire de même. 27 | 28 | - Lancez `yarn add --dev babel-preset-env` pour installer un package préconfiguré qui s'appelle `env`. Il contient des configurations pour les fonctionnalités ECMAScript les plus récentes supportées par Babel. 29 | 30 | - Créez un fichier `.babelrc` à la racine de votre projet. Il s'agit d'un fichier JSON pour votre configuration Babel. Ecrivez les lignes suivantes pour que Babel utilise la préconfiguration `env` : 31 | 32 | ```json 33 | { 34 | "presets": [ 35 | "env" 36 | ] 37 | } 38 | ``` 39 | 40 | :checkered_flag: `yarn start` devrait continuer à fonctionner, mais maintenant il fait réellement quelque chose. Mais à vrai dire on ne peut pas vraiment savoir, étant donné qu'on utilise `babel-node` pour interpréter de l'ES6 directement. Bientôt, vous aurez la preuve que notre code ES6 est bien transformé quand vous atteindrez la section [syntaxe des modules ES6](#the-es6-modules-syntax) de ce chapitre. 41 | 42 | ## ES6 43 | 44 | > :bulb: **[ES6](http://es6-features.org/)**: L'amélioration la plus significative du langage JavaScript. Il y a trop de fonctionnalités ES6 pour qu'on les liste toutes ici, mais du code ES6 typique utilise des classes avec `class`, `const` et `let`, des template strings, et des *arrow functions* (fonctions flèches :fr:) (`(text) => { console.log(text) }`). 45 | 46 | ### Créer une classe en ES6 47 | 48 | - Créez un nouveau fichier, `src/dog.js`, contenant la classe ES6 suivante : 49 | 50 | ```js 51 | class Dog { 52 | constructor(name) { 53 | this.name = name 54 | } 55 | 56 | bark() { 57 | return `Wah wah, I am ${this.name}` 58 | } 59 | } 60 | 61 | module.exports = Dog 62 | ``` 63 | 64 | Si vous avez déjà fait de la programmation orientée objet avant, cela ne devrait pas vous choquer. Pourtant, c'est quelque chose d'assez récent en JavaScript. La classe est offerte au monde extérieur via l'affectation `module.exports`. 65 | 66 | Dans `src/index.js`, on écrit les lignes suivantes : 67 | 68 | ```js 69 | const Dog = require('./dog') 70 | 71 | const toby = new Dog('Toby') 72 | 73 | console.log(toby.bark()) 74 | ``` 75 | 76 | Comme vous pouvez le voir, à la différence du package `color` utilisé précédemment, lorsqu'on *require* (appelle :fr:) l'un de nos fichiers, on utilise `./` dans le `require()`. 77 | 78 | :checkered_flag: Lancez `yarn start`. Vous devriez lire "Wah wah, I am Toby" dans votre console. :dog: 79 | 80 | ### La syntaxe des modules ES6 81 | 82 | Ici, on remplace tout simplement `const Dog = require('./dog')` par `import Dog from './dog'`, qui est la nouvelle syntaxe de modules ES6 (opposé à la syntaxe des modules "CommonJS"). Actuellement, elle n'est pas supportée nativement par NodeJS : ça sera donc une preuve que notre Babel transforme nos fichiers ES6 correctement. 83 | Dans `dog.js`, on remplace aussi `module.exports = Dog` par `export default Dog` 84 | 85 | :checkered_flag: `yarn start` devrait toujours afficher "Wah wah, I am Toby". :dog: 86 | 87 | ## ESLint 88 | 89 | > **Un linter** est un outil visant à améliorer la qualité et la consistance du code. 90 | 91 | > :bulb: **[ESLint](http://eslint.org)** est un linter de choix pour du code ES6. Un linter va nous donner des informations à propos du format du code, qui va renforcer sa consistance et sa cohérence. C'est aussi un bon moyen d'apprendre des choses sur JavaScript en faisant des erreurs que ESLint va détecter. 92 | 93 | ESLint travaille avec des *règles*, et il y en a [beaucoup](http://eslint.org/docs/rules/) (:uk:). Au lieu de configurer les règles que nous voulons suivre pour notre code par nous-même, Nous allons utiliser une configuration conçue par Airbnb. Cette configuration utilise quelques plugins que nous allons aussi avoir besoin d'installer. 94 | 95 | Jetez un oeil aux [instructions](https://www.npmjs.com/package/eslint-config-airbnb) les plus récentes d'Airbnb pour installer le package de configuration et toutes ses dépendances. Le 03/02/2017, il est recommandé d'utiliser la commande suivante dans votre terminal : 96 | 97 | ```sh 98 | npm info eslint-config-airbnb@latest peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs yarn add --dev eslint-config-airbnb@latest 99 | ``` 100 | 101 | Ceci devrait installer tout ce dont vous avez besoin et ajouter automatiquement `eslint-config-airbnb`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, et `eslint-plugin-react` à votre fichier `package.json`. 102 | 103 | **Remarque**: Nous avons remplacé `npm install` par `yarn add` dans cette commande. De plus, **elle ne fonctionnera pas sous Windows**, alors regardez le fichier `package.json` de ce repo et installez juste les dépendances liées à ESLint manuellement via `yarn add --dev packagename@^#.#.#`; `#.#.#` étant la version donnée dans le `package.json` pour chaque package. 104 | 105 | - Créez un fichier `.eslintrc.json` à la racine de votre projet, comme nous l'avons fait pour Babel. Ecrivez-y les lignes suivantes : 106 | 107 | ```json 108 | { 109 | "extends": "airbnb" 110 | } 111 | ``` 112 | 113 | Nous allons créer un nouveau script NPM/Yarn pour lancer ESLint. Maintenant, installons le package `eslint` pour pouvoir utiliser la commande `eslint` : 114 | 115 | - Lancez `yarn add --dev eslint` 116 | 117 | Modifiez l'objet `scripts` de votre `package.json` pour inclure la nouvelle tâche `test` : 118 | 119 | ```json 120 | "scripts": { 121 | "start": "babel-node src", 122 | "test": "eslint src" 123 | }, 124 | ``` 125 | 126 | Ici, on indique à ESLint qu'on veut qu'il *lint* tous nos fichiers JavaScript situés sous le dossier `src`. 127 | 128 | On utilisera cette tâche `test` standard pour lancer une série de commandes qui validerons notre code, que ce soit du *linting*, du *type checking*, ou des tests unitaires. 129 | 130 | - Lancez `yarn test`. Vous devriez voir une série d'erreurs pour des point-virgule manquants et un warning pour avoir utilisé `console.log()` dans `index.js`. Ajoutez `/* eslint-disable no-console */` en haut de votre fichier `index.js`: cela va nous permettre d'utiliser `console` dans ce fichier. 131 | 132 | **Note**: Si vous êtes sous Windows, soyez sûr de configurer votre éditeur de texte et Git pour qu'ils utilisent les fins de ligne LF Unix et non des CRLF Windows. Si votre projet est seulement utilisé dans des environnements Windows, vous pouvez ajouter `"linebreak-style": [2, "windows"]` dans le tableau de règles ESLint (voir l'exemple ci-dessous) pour imposer CRLF à la place. 133 | 134 | ### Point-virgule 135 | 136 | Ok, c'est probablement le débat le plus chaud dans la communauté JavaScript mais parlons-en un peu. JavaScript a ce truc qui s'appelle l'insertion automatique de point-virgule, qui nous permet d'écrire notre code avec ou sans point-virgule. Sur ce sujet, rien de bon ou mauvais: il s'agit juste d'une préférence personnelle. Si vous aimez la syntaxe de Python, Ruby ou Scala, alors il est très probable que vous préfériez ne pas mettre de point virgule. Si vous préférez la syntaxe de Java, C#, ou PHP, c'est l'inverse. 137 | 138 | La plupart des gens écrivent du JavaScript avec des point-virgules par habitude. C'était mon cas jusqu'à ce que j'y aille en mode no point-virgule après avoir lu quelques exemples de code de la documentation Redux. Dans un premier temps c'était un peu bizarre, mais uniquement parce que je n'y était pas habitué. Mais après une journée à écrire mon code de cette manière, je ne me voyais pas recommencer à utiliser des point-virgules dans mon code. Ils semblaient lourds, non-nécessaires et pas à leur place. Un code sans point-virgule est plus facile à lire à mon avis, et aussi plus facile à taper. 139 | 140 | Je vous recommande de lire [la documentation ESLint sur les point-virgules](http://eslint.org/docs/rules/semi) (:uk:). Comme mentionné dans cette page, si vous décidez de ne pas utiliser de point-virgule, il y a quelques cas, assez rares, où ceux-ci sont toutefois nécessaire. ESLint peut vous protéger de tels cas avec la règle `no-unexpected-multiline`. Configurons ESLint pour pouvoir ne plus mettre de point-virgule en toute sécurité dans `.eslintrc.json`: 141 | 142 | ```json 143 | { 144 | "extends": "airbnb", 145 | "rules": { 146 | "semi": [2, "never"], 147 | "no-unexpected-multiline": 2 148 | } 149 | } 150 | ``` 151 | 152 | :checkered_flag: Lancez `yarn test`, et tout devrait se passer tranquillement. Essayez d'ajouter des point-virgules n'importe où dans le code pour vérifier que le linter fonctionne. 153 | 154 | Nous sommes bien conscient que certains d'entre vous vont vouloir garder les point-virgules, ce qui rend le code de ce tutoriel gênant. Si vous utilisez ce tutoriel juste pour apprendre, nous sommes sûr qu'il restera supportable pour apprendre, même sans point-virgules, jusqu'à pouvoir les utiliser à nouveau dans vos projets. Si vous voulez utiliser le code fourni dans ce tutoriel comme *boilerplate*, cela demandera un peu de réécriture, ce qui devrait être rapide avec ESLint configuré pour imposer les point-virgules, celui-ci vous guidant durant tout le processus. Nous nous excusons si tel est votre cas ! 155 | 156 | ### Compat 157 | 158 | [Compat](https://github.com/amilajack/eslint-plugin-compat) est un plugin ESLint qui vous prévient si vous utilisez des API Javascript ne sont pas disponibles dans les navigateurs que vous supportez. Il utilise [Browserslist](https://github.com/ai/browserslist), qui s'appuie sur [Can I Use](http://caniuse.com/). 159 | 160 | - Lancez `yarn add --dev eslint-plugin-compat` 161 | 162 | - Ajoutez les lignes suivantes à votre fichier `package.json`. Elles indiquent que nous souhaitons supporter les navigateurs représentant plus de 1% du trafic. 163 | 164 | ```json 165 | "browserslist": ["> 1%"], 166 | ``` 167 | 168 | - Modifiez votre fichier `.eslintrc.json` comme suit : 169 | 170 | ```json 171 | { 172 | "extends": "airbnb", 173 | "plugins": [ 174 | "compat" 175 | ], 176 | "rules": { 177 | "semi": [2, "never"], 178 | "no-unexpected-multiline": 2, 179 | "compat/compat": 2 180 | } 181 | } 182 | ``` 183 | 184 | Vous pouvez essayer le plugin en utilisant `navigator.serviceWorker` ou `fetch` dans votre code par exemple, ce qui devrait lancer un warning ESLint. 185 | 186 | ### ESLint dans votre éditeur 187 | 188 | Ce chapitre vous a permis d'installer ESLint dans votre terminal, ce qui est plutôt cool pour détecter les erreurs pendant le *build*/avant de *push*, mais vous voulez aussi probablement l'intégrer dans votre IDE pour un retour immédiat. N'UTILISEZ PAS le linter ES6 natif de votre IDE. Configurez le pour que l'exécutable qu'il utilise pour le *linting* soit celui dans votre dossier `node_modules`. De cette façon, il pourra utiliser toute la configuration de votre projet, la préconfiguration Airbnb etc. Sinon, vous aurez juste un linting ES6 générique. 189 | 190 | ## Flow 191 | 192 | > :bulb: **[Flow](https://flowtype.org/)**: Un type checker statique par Facebook. Il détecte les types incohérents dans le code. Par exemple, il vous donnera une erreur si vous utilisez une chaîne de caractère là où il faudrait utiliser un nombre. 193 | 194 | Maintenant, notre code JavaScript est de l'ES6 valide. Flow peut analyser du JavaScript pur pour nous donner quelques idées, mais pour utiliser toute sa puissance, nous allons avoir besoin d'ajouter des annotations de type dans notre code, ce qui le rendra non standard. On va avoir besoin d'apprendre à Babel et à ESLint ce que sont ces annotations de types pour qu'ils ne paniquent pas quand ils parcourront nos fichiers. 195 | 196 | - Lancez `yarn add --dev flow-bin babel-preset-flow babel-eslint eslint-plugin-flowtype` 197 | 198 | `flow-bin` est l'exécutable pour lancer Flow dans notre tâche `scripts`, `babel-preset-flow` est une préconfiguration pour que Babel puisse comprendre les annotations de type, `babel-eslint` est un package qui permet à ESLint de *s'appuyer sur le parseur de Babel* au lieu du sien, et `eslint-plugin-flowtype` est un plugin ESLint pour *linter* les annotations Flow. Pfiou. 199 | 200 | - Modifiez votre fichier `.babelrc` comme ceci : 201 | 202 | ```json 203 | { 204 | "presets": [ 205 | "env", 206 | "flow" 207 | ] 208 | } 209 | ``` 210 | 211 | - Modifiez aussi votre fichier `.eslintrc.json` : 212 | 213 | ```json 214 | { 215 | "extends": [ 216 | "airbnb", 217 | "plugin:flowtype/recommended" 218 | ], 219 | "plugins": [ 220 | "flowtype", 221 | "compat" 222 | ], 223 | "rules": { 224 | "semi": [2, "never"], 225 | "no-unexpected-multiline": 2, 226 | "compat/compat": 2 227 | } 228 | } 229 | ``` 230 | 231 | **Remarque**: `plugin:flowtype/recommended` contient les instructions pour dire à ESLint d'utiliser le parseur de Babel. Si vous voulez être plus explicite, libre à vous d'ajouter `"parser": "babel-eslint"` dans `.eslintrc.json`. 232 | 233 | Ça fait beaucoup d'un coup, essayez de prendre un instant pour y penser. C'est assez impressionnant de voir qu'il est possible pour ESLint d'utiliser le parseur de Babel pour comprendre les annotations Flow. Ces deux outils sont formidables pour être aussi modulaires ! 234 | 235 | - Chaînez `flow` à votre tâche `test` : 236 | 237 | ```json 238 | "scripts": { 239 | "start": "babel-node src", 240 | "test": "eslint src && flow" 241 | }, 242 | ``` 243 | 244 | - Créez un fichier `.flowconfig` à la racine de votre projet contenant les lignes suivantes : 245 | 246 | ```flowconfig 247 | [options] 248 | suppress_comment= \\(.\\|\n\\)*\\flow-disable-next-line 249 | ``` 250 | Il s'agit d'un petit utilitaire que nous configurons pour que Flow ignore les warning détectés sur la ligne suivante. Vous pourrez l'utiliser de cette façon (qui rappelle `eslint-disable`) : 251 | 252 | ```js 253 | // flow-disable-next-line 254 | something.flow(doesnt.like).for.instance() 255 | ``` 256 | 257 | Bien, on devrait en avoir fini pour ce qui est de la configuration. 258 | 259 | - Ajoutez une annotation Flow au fichier `src/dog.js` comme ceci : 260 | 261 | ```js 262 | // @flow 263 | 264 | class Dog { 265 | name: string 266 | 267 | constructor(name: string) { 268 | this.name = name 269 | } 270 | 271 | bark() { 272 | return `Wah wah, I am ${this.name}` 273 | } 274 | } 275 | 276 | export default Dog 277 | ``` 278 | 279 | Le commentaire `// @flow` dit à Flow que nous voulons que ce fichier soit soumis aux vérifications de types. Pour le reste, les annotations Flow sont typiquement deux-point après le paramètre d'une fonction ou le nom d'une fonction. Voici la [documentation](https://flowtype.org/docs/quick-reference.html) pour plus de détail. 280 | 281 | - Ajoutez également`// @flow` en haut de votre fichier `index.js` 282 | 283 | `yarn test` devrait maintenant *linter* et vérifier les types de votre code comme il faut. 284 | 285 | Il y a deux choses que l'on veut essayer : 286 | 287 | - Dans `dog.js`, remplacez `constructor(name: string)` par `constructor(name: number)`, et lancez `yarn test`. Vous devriez obtenir une erreur **Flow** qui dit que ces deux types sont incompatibles. **Ce qui signifie que Flow est correctement configuré :+1: !** 288 | 289 | - Maintenant, remplacez `constructor(name: string)` par `constructor(name:string)`, et lancez `yarn test`. Vous devriez obtenir une erreur **ESLint** qui dit que vos annotations Flow devraient avoir un espace après les deux points. **Le pluglin Flow pour ESLint est correctement configuré :+1: !** 290 | 291 | :checkered_flag: Si vous avez obtenu ces 2 erreurs, vous avez réussi à configurer Flow et ESLint ! Prenez garde à bien remettre en place les espaces dans vos annotations Flow :wink: 292 | 293 | ### Flow dans votre éditeur de texte 294 | 295 | Comme avec ESLint, vous devriez consacrer un peu de votre temps à configurer votre IDE/éditeur de texte pour qu'il vous donne un feedback immédiat quand Flow détecte un problème dans votre code. 296 | 297 | ## Jest 298 | 299 | > :bulb: **[Jest](https://facebook.github.io/jest/)**: Une bibliothèque de test JavaScript créée par Facebook. Elle est très simple à configurer et fournit tout ce qu'il faut pour faire nos tests unitaires. Jest permet aussi de tester les composants React. 300 | 301 | - Lancez `yarn add --dev jest babel-jest` pour installer Jest et le package pour l'utiliser avec Babel. 302 | 303 | - Dans `.eslintrc.json` ajoutez les lignes suivantes à la racine de l'objet pour permettre l'utilisation des fonctions Jest sans avoir à les importer dans chaque fichier de test : 304 | 305 | ```json 306 | "env": { 307 | "jest": true 308 | } 309 | ``` 310 | 311 | - Créez un fichier `src/dog.test.js` qui contient les lignes suivantes : 312 | 313 | ```js 314 | import Dog from './dog' 315 | 316 | test('Dog.bark', () => { 317 | const testDog = new Dog('Test') 318 | expect(testDog.bark()).toBe('Wah wah, I am Test') 319 | }) 320 | ``` 321 | 322 | - Ajoutez `jest` à votre script `test` : 323 | 324 | ```json 325 | "scripts": { 326 | "start": "babel-node src", 327 | "test": "eslint src && flow && jest --coverage" 328 | }, 329 | ``` 330 | 331 | Le drapeau `--coverage` dit à Jest de générer automatiquement des données de couverture pour nos tests. C'est utile pour voir quelles parties de votre code manquent de test. Il écrit ces données dans un dossier nommé `coverage`. 332 | 333 | - Ajoutez `/coverage/` à votre fichier `.gitignore` 334 | 335 | :checkered_flag: Lancez `yarn test`. Après avoir *linté* et vérifié les types, nos tests Jest devraient se lancer et afficher un tableau de données. Tout devrait être vert ! 336 | 337 | ## Git Hooks avec Husky 338 | 339 | > :bulb: **[Git Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)**: Des scripts qui sont lancés quand certaines actions ont lieu, comme un commit ou un push 340 | 341 | Ok, maintenant on a cette superbe tâche `test` qui nous dit si notre code est bon ou pas. Maintenant, on va configurer des Git Hooks pour lancer automatiquement cette tâche avant chaque `git commit` et `git push`, ce qui nous évitera de pusher du mauvais code dans notre repo si la tâche `test` ne réussit pas. 342 | 343 | [Husky](https://github.com/typicode/husky) est un package qui permet de créer facilement des Git Hooks. 344 | 345 | - Lancez `yarn add --dev husky` 346 | 347 | Tout ce que nous avons à faire est de créer deux tâche dans `scripts`, `precommit` et `prepush` : 348 | 349 | ```json 350 | "scripts": { 351 | "start": "babel-node src", 352 | "test": "eslint src && flow && jest --coverage", 353 | "precommit": "yarn test", 354 | "prepush": "yarn test" 355 | }, 356 | ``` 357 | 358 | :checkered_flag: Maintenant, si vous essayez de faire un `git commit` ou un `git push`, la tâche `test` devrait se lancer automatiquement. 359 | 360 | Si ça ne marche pas, il est possible que `yarn add --dev husky` n'ait pas correctement installé les Git Hooks. Ce problème peut arriver à certaines personnes. Si c'est votre cas, lancez `yarn add --dev husky --force`, et laissez peut-être un mot décrivant votre situation sur [cette issue](https://github.com/typicode/husky/issues/84). 361 | 362 | **Remarque**: Si vous pushez directement après votre commit, vous pouvez utiliser `git push --no-verify` pour éviter d'avoir à lancer à nouveau les tests. 363 | 364 | Prochaine section: [03 - Express, Nodemon, PM2](03-express-nodemon-pm2.md#readme) 365 | 366 | Retour à la [section précédente](01-node-yarn-package-json.md#readme) ou au [sommaire](https://github.com/naomihauret/js-stack-from-scratch#table-of-contents). 367 | -------------------------------------------------------------------------------- /tutorial/03-express-nodemon-pm2.md: -------------------------------------------------------------------------------- 1 | # 03 - Express, Nodemon et PM2 2 | 3 | Le code pour ce chapitre est disponible [ici](https://github.com/verekia/js-stack-walkthrough/tree/master/03-express-nodemon-pm2). 4 | 5 | Dans cette section, nous allons créer un serveur qui affichera notre web app. Nous configurerons également un mode développment et un mode production pour ce serveur. 6 | 7 | ## Express 8 | 9 | > :bulb: **[Express](http://expressjs.com/)** est de loin le framework le plus populaire pour Node. Il fournit une API minimaliste très simple, et ses fonctionnalités peuvent être étendues avec *middleware*. 10 | 11 | Mettons en place un serveur Express minimal qui servira à afficher notre page HTML avec un peu de CSS. 12 | 13 | - Supprimez tous les fichiers dans `src` 14 | 15 | Créez les fichiers et dossiers suivants : 16 | 17 | - un fichier `public/css/style.css` qui contient : 18 | 19 | ```css 20 | body { 21 | width: 960px; 22 | margin: auto; 23 | font-family: sans-serif; 24 | } 25 | 26 | h1 { 27 | color: limegreen; 28 | } 29 | ``` 30 | 31 | - un dossier `src/client/` 32 | 33 | - un dossier `src/shared/` 34 | 35 | C'est dans ce dossier que nous allons mettre le code JavaScript *universel/isomorphique* (des fichiers qui vont être utilisés par le client et par le serveur). Les routes sont un bon cas d'usage de code partagé, comme vous le constaterez plus tard dans ce tutoriel quand nous ferons des appel asynchrones. Ici nous avons simplement quelques constantes de configuration comme exemple. 36 | 37 | - Créez un fichier `src/shared/config.js` qui contient : 38 | 39 | ```js 40 | // @flow 41 | 42 | export const WEB_PORT = process.env.PORT || 8000 43 | export const STATIC_PATH = '/static' 44 | export const APP_NAME = 'Hello App' 45 | ``` 46 | 47 | Si le processus Node utilisé pour faire fonctionner votre app a une variable d'environnement `process.env.PORT` de configurée (c'est le cas quand on va déployer sur Heroku par exemple), il l'utilisera comme port. S'il n'y en a pas, le port par défaut sera `8000`. 48 | 49 | - Créez un fichier `src/shared/util.js` qui contient : 50 | 51 | ```js 52 | // @flow 53 | 54 | // eslint-disable-next-line import/prefer-default-export 55 | export const isProd = process.env.NODE_ENV === 'production' 56 | ``` 57 | 58 | C'est un simple utilitaire pour tester si nous sommes en production ou non. Le commentaire `// eslint-disable-next-line import/prefer-default-export` est présent car nous n'avons qu'un seul export nommé ici. Si vous avez plusieurs exports dans ce fichier, vous pouvez le retirer. 59 | 60 | - Lancez `yarn add express compression` 61 | 62 | `compression` est un middleware qui va activer la compression Gzip sur le serveur. 63 | 64 | - Créez un fichier `src/server/index.js` qui contient : 65 | 66 | ```js 67 | // @flow 68 | 69 | import compression from 'compression' 70 | import express from 'express' 71 | 72 | import { APP_NAME, STATIC_PATH, WEB_PORT } from '../shared/config' 73 | import { isProd } from '../shared/util' 74 | import renderApp from './render-app' 75 | 76 | const app = express() 77 | 78 | app.use(compression()) 79 | app.use(STATIC_PATH, express.static('dist')) 80 | app.use(STATIC_PATH, express.static('public')) 81 | 82 | app.get('/', (req, res) => { 83 | res.send(renderApp(APP_NAME)) 84 | }) 85 | 86 | app.listen(WEB_PORT, () => { 87 | // eslint-disable-next-line no-console 88 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' : '(development)'}.`) 89 | }) 90 | ``` 91 | 92 | Rien de bien méchant ici, c'est presque le 'Hello world' du tutoriel de Express avec quelques imports en plus. Nous utilisons deux dossiers statiques différents ici : `dist` pour les fichiers générés, `public` pour ceux déclarés. 93 | 94 | - Créez un fichier `src/server/render-app.js` qui contient : 95 | 96 | ```js 97 | // @flow 98 | 99 | import { STATIC_PATH } from '../shared/config' 100 | 101 | const renderApp = (title: string) => 102 | ` 103 | 104 | 105 | ${title} 106 | 107 | 108 | 109 |

${title}

110 | 111 | 112 | ` 113 | 114 | export default renderApp 115 | ``` 116 | 117 | Vous savez, votre habitude d'avoir un langage de template pour le back-end ? Et bien c'est assez obsolète, maintenant que Javascript supporte les template strings. Ici, nous créons une fonction qui prend un paramètre `title` et l'injecte dans les balises `title` et `h1` de la page, et qui retourne une chaîne de caractère complète en HTML. Nous utilisons aussi une constante `STATIC_PATH` comme chemin de base pour tous nos assets statiques 118 | 119 | ### Coloration syntaxique pour les template strings HTML sur Atom (optionnel) 120 | 121 | Il est possible de faire fonctionner la coloration syntaxique pour du code HTML dans des template strings selon votre éditeur. Dans Atom, si vous préfixez votre template string avec le tag `html` (ou n'importe quel tag qui *fini* par `html`, code `jaimehtml`), il colorera automatiquement le contenu de cette string. 122 | 123 | ```js 124 | import { html } from `common-tags` 125 | 126 | const template = html` 127 |
Wow, colors!
128 | ` 129 | ``` 130 | 131 | Nous n'avons pas inclu ce petit tour dans le *boilerplate* de ce tutoriel puisqu'il semble fonctionner seulement pour Atom, ce qui est loin d'être idéal. Ceux d'entre vous qui utilisent Atom pourraient trouver cela utile. 132 | 133 | Enfin bref, revenous à nos moutons ! 134 | 135 | - Dans le fichier `package.json` changez votre script `start` comme ceci : `"start": "babel-node src/server",` 136 | 137 | :checkered_flag: Lancez `yarn start`, et dans votre navigateur, rendez-vous sur `localhost:8000`. Si tout fonctionne comme prévu, vou devriez avoir une page blanche avec "Hello App" d'écrit dans l'onglet et dans le titre vert de la page. 138 | 139 | **Remarque**: Quelques processus - typiquement, ceux qui attendent que des choses se passent, comme un serveur par exemple - vous éviterons d'entrer des commandes dans votre terminal jusqu'à ce qu'ils aient terminé. Pour interrompre ce genre de processus et avoir à nouveau accès à votre prompt, il faut faire **Ctrl+C**. Comme alternative, vous pouvez ouvrir un nouvel onglet dans votre terminal pour être capable d'entrer d'autres commandes en même temps que votre processus tourne. Vous pouvez aussi faire tourner ces processus en arrière-plan mais cela sort du cadre de notre tutoriel. 140 | 141 | ## Nodemon 142 | 143 | > :bulb: **[Nodemon](https://nodemon.io/)** est un utilitaire qui va automatiquement relancer votre serveur Node dès qu'un fichier est modifié dans le dossier. Nous allons utiliser Nodemon dès que nous sommes en mode **développement**. 144 | 145 | - Lancez `yarn add --dev nodemon` 146 | 147 | - Changez votre `scripts` de la manière suivante : 148 | 149 | ```json 150 | "start": "yarn dev:start", 151 | "dev:start": "nodemon --ignore lib --exec babel-node src/server", 152 | ``` 153 | 154 | `start` est maintenant un pointeur vers une autre tâche, `dev:start`. Cela nous donne une couche d'abstraction pour modifier ce qu'est la tâche par défaut. 155 | 156 | Dans `dev:start`, le drapeau `--ignore lib` indique qu'il ne faut *pas* redémarrer le serveur quand des changements arrivent dans le dossier `lib`. Vous n'avez pas encore ce dossier mais nous allons le générer dans la prochaine section de ce chapitre, donc tout va bientôt faire sens. Normalement, Nodemon utilise l'exécutable `node`. Dans notre cas, puisqu'on utilise Babel, on peut dire à Nodemon d'utiliser l'exécutable `babel-node` à la place. De cette façon, il comprendra notre code ES6/Flow. 157 | 158 | :checkered_flag: Lancez `yarn start` et ouvrez `localhost:8000` dans votre navigateur. Changez la constante `APP_NAME` dans `src/shared/config.js`, qui devrait déclencher le redémarrage de votre serveur dans le terminal. Rafraîchissez la page pour voir le titre modifié. Notez que le redémarrage automatique du serveur est différent du *Hot Module Replacement* (quand les composants d'une page sont mis à jour en temps réel). Ici nous avons toujours besoin de rafraîchir la page manuellement, mais au moins nous n'avons pas à kill le processus et à le redémarrer manuellement pour voir les changements. Le Hot Module Replacement sera introduit dans le prochain chapitre. 159 | 160 | ## PM2 161 | 162 | > :bulb: **[PM2](http://pm2.keymetrics.io/)** est un process manager pour Node. Il garde tous nos processus vivants en production et offre des tonnes de fonctionnalités pour les gérer et suivre leurs performances. 163 | 164 | Nous allons utiliser PM2 dès que nous serons en **production**. 165 | 166 | - Lancez `yarn add --dev pm2` 167 | 168 | En production, vous voulez que votre serveur soit aussi performant que possible. `babel-node` déclenche tout le processus de transpilage de Babel pour vos fichiers à chaque exécution: vous ne voulez pas de ça en production. Nous avons besoin que Babel fasse tout ce travail en amont, et que notre serveur rende nos bons vieux fichiers ES5 précompilés. 169 | 170 | Une des principales fonctionnalités de Babel est de prendre un dossier de code ES6 (habituellement nommé `src`) et le transpiler en un dossier de code ES5 (habituellement nommé `lib`). 171 | 172 | Ce dossier `lib` étant auto-généré, le nettoyer avant un nouveau *build* est une bonne pratique, puisqu'il peut contenir d'anciens fichiers dont on ne veut plus. `rimraf` est un package simple et efficace pour supprimer les fichiers sur différentes plateformes. 173 | 174 | - Lancez `yarn add --dev rimraf` 175 | 176 | Ajoutons la tâche `prod:build` à `scripts`: 177 | 178 | ```json 179 | "prod:build": "rimraf lib && babel src -d lib --ignore .test.js", 180 | ``` 181 | 182 | - Lancez `yarn prod:build`: il devrait générer un dossier `lib` qui contient le code transpilé, sauf pour les fichiers se terminant par `.test.js` (notez que les fichiers `.test.jsx` sont aussi ignorés par ce paramètre). 183 | 184 | - Ajoutez `/lib/` à votre `.gitignore` 185 | 186 | Une dernière chose: nous allons passer une variable d'environnement `NODE_ENV` à notre exécutable PM2. Avec Unix, vous pouvez faire ça en lançant `NODE_ENV=production pm2`, mais Windows utilise une syntaxe différente. Nous allons utiliser un package qui s'appelle `cross-env` pour que cette syntaxe soit également utilisable sous Windows. 187 | 188 | - Lancez `yarn add --dev cross-env` 189 | 190 | Modifions notre fichier `package.json` : 191 | 192 | ```json 193 | "scripts": { 194 | "start": "yarn dev:start", 195 | "dev:start": "nodemon --ignore lib --exec babel-node src/server", 196 | "prod:build": "rimraf lib && babel src -d lib --ignore .test.js", 197 | "prod:start": "cross-env NODE_ENV=production pm2 start lib/server && pm2 logs", 198 | "prod:stop": "pm2 delete server", 199 | "test": "eslint src && flow && jest --coverage", 200 | "precommit": "yarn test", 201 | "prepush": "yarn test" 202 | }, 203 | ``` 204 | 205 | :checkered_flag: Lancez `yarn prod:build`, puis `yarn prod:start`. PM2 devrait montrer un processus actif. Rendez vous sur `http://localhost:8000/`: vous devriez voir votre app. Votre terminal devrait afficher les logs, qui devraient être "Server running on port 8000 (production).". Notez qu'avec PM2, vos processus sont lancés en arrière-plan. Si vous faites Ctrl+C, cela tuera la commande `pm2 logs`, qui était la denière commande de notre chaîne `prod:start`, mais le serveur devrait toujours afficher la page. Si vous voulez arrêter le serveur, lancez `yarn prod:stop`. 206 | 207 | Maintenant que nous avons une tâche `prod:build`, ça serait parfait si on pouvait s'assurer que tout fonctionne correctement avant de *push* du code sur le repo. Puisqu'il n'est pas vraiment nécessaire de le lancer à chaque commit, nous vous suggérons de l'ajouter à la tâche `prepush` : 208 | 209 | ```json 210 | "prepush": "yarn test && yarn prod:build" 211 | ``` 212 | 213 | :checkered_flag: Lancez `yarn prepush` ou *pushez* vos fichiers pour déclencher le processus. 214 | 215 | **Remarque**: Nous n'avons aucun test ici, donc Jest se plaindra un peu. Ignorez-le pour l'instant. 216 | 217 | Prochaine section: [04 - Webpack, React, HMR](04-webpack-react-hmr.md#readme) 218 | 219 | Retour à la [section précédent](02-babel-es6-eslint-flow-jest-husky.md#readme) ou au [sommaire](https://github.com/naomihauret/js-stack-from-scratch#table-of-contents). 220 | -------------------------------------------------------------------------------- /tutorial/04-webpack-react-hmr.md: -------------------------------------------------------------------------------- 1 | # 04 - Webpack, React et Hot Module Replacement 2 | 3 | Le code pour ce chapitre est disponible [ici](https://github.com/verekia/js-stack-walkthrough/tree/master/04-webpack-react-hmr). 4 | 5 | ## Webpack 6 | 7 | > :bulb: **[Webpack](https://webpack.js.org/)** est un *module bundler* (*empaqueteur de module* :fr:). Il prend nos différents fichiers sources, les traitent et les fusionne (habituellement) en un seul fichier JavScript appelé "bundle", qui est le seul fichier qui sera exécuté par le client. 8 | 9 | Créons un *hello world* basique et *empaquetons-le* avec Webpack. 10 | 11 | - Dans le fichier `src/shared/config.js`, ajoutons les constantes suivantes : 12 | 13 | ```js 14 | export const WDS_PORT = 7000 15 | 16 | export const APP_CONTAINER_CLASS = 'js-app' 17 | export const APP_CONTAINER_SELECTOR = `.${APP_CONTAINER_CLASS}` 18 | ``` 19 | 20 | - Créez un fichier `src/client/index.js` contenant : 21 | 22 | ```js 23 | import 'babel-polyfill' 24 | 25 | import { APP_CONTAINER_SELECTOR } from '../shared/config' 26 | 27 | document.querySelector(APP_CONTAINER_SELECTOR).innerHTML = '

Hello Webpack!

' 28 | ``` 29 | 30 | Si vous voulez utiliser les fonctionnalités ES les plus récentes dans votre code client comme les `Promise`, vous allez avoir besoin d'inclure le [Polyfill Babel](https://babeljs.io/docs/usage/polyfill/) avant tout autre chose dans votre bundle. 31 | 32 | - Lancez `yarn add babel-polyfill` 33 | 34 | Si vous lancez ESLint sur ce fichier, il se plaindra que `document` est *undefined* (non-défini). 35 | 36 | - Ajoutez les lignes suivantes à `env` dans votre fichier `.eslintrc.json`. Cela vous permettra d'utiliser `window` et `document`: 37 | 38 | ```json 39 | "env": { 40 | "browser": true, 41 | "jest": true 42 | } 43 | ``` 44 | 45 | Bien, maintenant, nous avons besoin d'empaqueter cette app client ES6 dans un bundle ES5. 46 | 47 | - Créez un fichier`webpack.config.babel.js` contenant : 48 | 49 | ```js 50 | // @flow 51 | 52 | import path from 'path' 53 | 54 | import { WDS_PORT } from './src/shared/config' 55 | import { isProd } from './src/shared/util' 56 | 57 | export default { 58 | entry: [ 59 | './src/client', 60 | ], 61 | output: { 62 | filename: 'js/bundle.js', 63 | path: path.resolve(__dirname, 'dist'), 64 | publicPath: isProd ? '/static/' : `http://localhost:${WDS_PORT}/dist/`, 65 | }, 66 | module: { 67 | rules: [ 68 | { test: /\.(js|jsx)$/, use: 'babel-loader', exclude: /node_modules/ }, 69 | ], 70 | }, 71 | devtool: isProd ? false : 'source-map', 72 | resolve: { 73 | extensions: ['.js', '.jsx'], 74 | }, 75 | devServer: { 76 | port: WDS_PORT, 77 | }, 78 | } 79 | ``` 80 | 81 | Ce fichier est utilisé pour décrire comment notre bundle devrait être assemblé. `entry` est le point d'entrée de notre app, `output.filename` est le nom du bundle à générer, `output.path` et `output.publicPath` décrivent le dossier de destination et l'URL. 82 | 83 | On place ensuite le bundle dans un fichier `dist`, qui va contenir les choses qui seront générées automatiquement (pas comme le CSS déclaratif qu'on a créé plus tôt et qui, lui, est dans `public`). `module.rules` est l'endroit où vous dites à Webpack d'appliquer des traitements à certains types de fichiers. Ici, on lui dit que nous voulons que tous nos fichiers `.js` et `.jsx` (pour React), exceptés ceux dans les `node_modules` doivent passer dans quelque chose qui s'appelle `babel-loader`. On veut aussi que ces deux extensions soient utilisées pour `resolve` les modules quand on les `import`. Pour finir, on déclare un port pour le Webpack Dev Server. 84 | 85 | **Remarque**: L'extension `.babel.js` est une fonctionnalité Webpack pour appliquer nos transformations Babel à ce fichier de configuration. 86 | 87 | `babel-loader` est un plugin pour Webpack qui transpile votre code tout comme nous l'avons fait depuis le début de ce tutoriel. La seule différence est que cette fois, le code finira dans le navigateur et non plus dans votre serveur. 88 | 89 | - Lancez `yarn add --dev webpack webpack-dev-server babel-core babel-loader` 90 | 91 | `babel-core` est une dépendance paire de `babel-loader`, donc nous l'installerons aussi. 92 | 93 | - Ajoutez `/dist/` à votre `.gitignore` 94 | 95 | ### Modifications de tâches 96 | 97 | En mode développement, nous allons utiliser `webpack-dev-server` pour prendre avantage du Hot Module Reloading (qu'on verra plus tard dans ce chapitre), et en production on utilisera simplement `webpack` pour générer nos bundles. Dans les deux cas, le flag `--progress` est utile pour afficher des informations supplémentaires quand Webpack compile vos fichiers. En production, on passera aussi le flag `-p` à Webpack pour qu'il minifie notre code, et qu'il passe la variable d'environnement `NODE_ENV` à `production`. 98 | 99 | Modifions notre `scripts` pour implémenter tout ça et profitons-en pour modifier nos autres tâches aussi : 100 | 101 | ```json 102 | "scripts": { 103 | "start": "yarn dev:start", 104 | "dev:start": "nodemon -e js,jsx --ignore lib --ignore dist --exec babel-node src/server", 105 | "dev:wds": "webpack-dev-server --progress", 106 | "prod:build": "rimraf lib dist && babel src -d lib --ignore .test.js && cross-env NODE_ENV=production webpack -p --progress", 107 | "prod:start": "cross-env NODE_ENV=production pm2 start lib/server && pm2 logs", 108 | "prod:stop": "pm2 delete server", 109 | "lint": "eslint src webpack.config.babel.js --ext .js,.jsx", 110 | "test": "yarn lint && flow && jest --coverage", 111 | "precommit": "yarn test", 112 | "prepush": "yarn test && yarn prod:build" 113 | }, 114 | ``` 115 | 116 | Dans `dev:start` on déclare explicitement les extensions de fichiers à suivre : `.js` et `.jsx`. Ajoutez `dist` dans les dossiers ignorés. 117 | 118 | Nous avons créé une tâche `lint` séparée et avons ajouté `webpack.config.babel.js` aux fichiers à *linter*. 119 | 120 | - Ensuite, créons le conteneur pour notre app dans `src/server/render-app.js`, et incluons le bundle qui sera généré : 121 | 122 | ```js 123 | // @flow 124 | 125 | import { APP_CONTAINER_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config' 126 | import { isProd } from '../shared/util' 127 | 128 | const renderApp = (title: string) => 129 | ` 130 | 131 | 132 | ${title} 133 | 134 | 135 | 136 |
137 | 138 | 139 | 140 | ` 141 | 142 | export default renderApp 143 | ``` 144 | 145 | Selon l'environnement dans lequel nous sommes, nous inclurons soit le bundle Webpack Dev Server, soit celui de production. Notez que le chemin pour le bundle Webpack Dev Server est *virtuel*, `dist/js/bundle.js` n'est pas vraiment lu de votre disque dur en mode développement. Il est également nécessaire de donner à Webpack Dev Server un port différent de votre port web principal. 146 | 147 | - Pour finir, dans `src/server/index.js`, modifiez le message de votre `console.log` comme ceci : 148 | 149 | ```js 150 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' : 151 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`) 152 | ``` 153 | 154 | Cela donnera aux autres développeurs une indication sur ce qu'il faut faire s'ils essaient juste de lancer `yarn start` sans Webpack Dev Server. 155 | 156 | Ok, ça fait beaucoup de changement, voyons si tout marche comme prévu : 157 | 158 | :checkered_flag: Lancez `yarn start` dans votre terminal. Ouvrez en un autre dans une nouvelle fenêtre ou un nouvel onglet, et lancez-y `yarn dev:wds`. Une fois que Webpack Dev Server a fini de générez le bundle et son sourcemaps (les deux devraient être des fichiers d'environ ~600ko) et que les 2 processus sont tranquilles dans votre terminal, ouvrez `http://localhost:8000/` dans votre navigateur. Vous devriez voir "Hello Webpack!". Ouvrez la console de votre navigateur et sous l'onglet Source, vérifiez quels fichiers sont inclus. Vous devriez seulement voir `static/css/style.css` sous `localhost:8000/`, et tous vos fichiers sources ES6 sous `webpack://./src`. Cela signifie que les sourcemaps fonctionnent. Dans votre éditeur de texte, dans `src/client/index.js`, changez `Hello Webpack!` en n'importe quelle autre chaîne de caractère. Dès que vous aurez sauvegardé votre fichier, dans votre terminal, Webpack Dev Server devrait générer un nouveau bundle, et l'onglet de votre navigateur, se rafraîchir automatiquement. 159 | 160 | - Tuez le processus précédent dans votre terminal avec Ctrl+C, puis lancez `yarn prod:build`, et ensuite `yarn prod:start`. Ouvrez `http://localhost:8000/` dans votre navigateur. Vous devriez toujours voir "Hello Webpack!". Dans l'onglet Source de votre navigateur, vous devriez voir `static/js/bundle.js` sous `localhost:8000/`, mais pas de sources `webpack://`. Cliquez sur `bundle.js` pour être sûr que tout est bien minifié. Lancez `yarn prod:stop`. 161 | 162 | Bien joué, c'était assez intense. Vous avez bien mérité une pause :clap: ! La prochaine section est plus simple. 163 | 164 | **Note**: Nous vous recommandons d'avoir au moins 3 terminaux ouverts: un pour le serveur Express, un autre pour Webpack Dev Server, et enfin un pour Git, les tests, et les commandes générales comme l'installation de package avec `yarn`. Idéalement, vous devriez scindez la fenêtre de vos terminaux en plusieurs panels afin de tous les voir d'un coup. 165 | 166 | ## React 167 | 168 | > :bulb: **[React](https://facebook.github.io/react/)** est une bibliothèque créée par Facebook qui permet de mettre en place des interfaces utilisateur. Il utilise la syntaxe **[JSX](https://facebook.github.io/react/docs/jsx-in-depth.html)** pour représenter des éléments HTML et des composants tout en exploitant la puissance de JavaScript. 169 | 170 | Dans cette section nous allons "rendre" du texte en utilisant React et JSX. 171 | 172 | Tout d'abord, installons React et ReactDOM : 173 | 174 | - Lancez `yarn add react react-dom` 175 | 176 | Renommez votre fichier `src/client/index.js` en `src/client/index.jsx` et écrivez-y un peu de React : 177 | 178 | ```js 179 | // @flow 180 | 181 | import 'babel-polyfill' 182 | 183 | import React from 'react' 184 | import ReactDOM from 'react-dom' 185 | 186 | import App from './app' 187 | import { APP_CONTAINER_SELECTOR } from '../shared/config' 188 | 189 | ReactDOM.render(, document.querySelector(APP_CONTAINER_SELECTOR)) 190 | ``` 191 | 192 | - Créez un fichier `src/client/app.jsx` contenant : 193 | 194 | ```js 195 | // @flow 196 | 197 | import React from 'react' 198 | 199 | const App = () =>

Hello React!

200 | 201 | export default App 202 | ``` 203 | 204 | Puisqu'on utilise la syntaxe JSX ici, nous devons expliquer à Babel qu'il a besoin de transformer ce JSX avec la préconfiguration `babel-preset-react`. Et tant qu'on y est, nous allons aussi ajouter le plugin Babel `flow-react-proptypes` qui va automatiquement générer les PropTypes depuis nos annotations Flow pour nos composants React. 205 | 206 | - Lancez `yarn add --dev babel-preset-react babel-plugin-flow-react-proptypes` et éditez votre fichier `.babelrc` de cette façon : 207 | 208 | ```json 209 | { 210 | "presets": [ 211 | "env", 212 | "flow", 213 | "react" 214 | ], 215 | "plugins": [ 216 | "flow-react-proptypes" 217 | ] 218 | } 219 | ``` 220 | 221 | :checkered_flag: Lancez `yarn start` et `yarn dev:wds`. Dans votre navigateur, rendez vous sur `http://localhost:8000`. Vous devriez voir "Hello React!". 222 | 223 | Maintenant essayez de changer le texte du fichier `src/client/app.jsx` en quelque chose d'autre. Webpack Dev Server devrait recharger la page automatiquement, ce qui est plutôt cool, mais on va rendre ça encore mieux. :ok_hand: 224 | 225 | ## Hot Module Replacement 226 | 227 | > :bulb: **[Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/)** (*HMR*) est une fonctionnalité Webpack puissante qui remplace automatiquement un module sans avoir à recharger toute la page. 228 | 229 | Pour que HMR fonctionne avec React, nous allons devoir ajuster quelques petites choses : 230 | 231 | - Lancez `yarn add react-hot-loader@next` 232 | 233 | - Éditez votre fichier `webpack.config.babel.js` de la façon suivante : 234 | 235 | ```js 236 | import webpack from 'webpack' 237 | // [...] 238 | entry: [ 239 | 'react-hot-loader/patch', 240 | './src/client', 241 | ], 242 | // [...] 243 | devServer: { 244 | port: WDS_PORT, 245 | hot: true, 246 | headers: { 247 | 'Access-Control-Allow-Origin': '*', 248 | }, 249 | }, 250 | plugins: [ 251 | new webpack.optimize.OccurrenceOrderPlugin(), 252 | new webpack.HotModuleReplacementPlugin(), 253 | new webpack.NamedModulesPlugin(), 254 | new webpack.NoEmitOnErrorsPlugin(), 255 | ], 256 | ``` 257 | 258 | La partie `headers` autorise le *Cross-Origin Resource Sharing* qui est nécessaire au bon fonctionnement des Hot Module Replacement. 259 | 260 | - Éditez votre fichier `src/client/index.jsx` : 261 | 262 | ```js 263 | // @flow 264 | 265 | import 'babel-polyfill' 266 | 267 | import React from 'react' 268 | import ReactDOM from 'react-dom' 269 | import { AppContainer } from 'react-hot-loader' 270 | 271 | import App from './app' 272 | import { APP_CONTAINER_SELECTOR } from '../shared/config' 273 | 274 | const rootEl = document.querySelector(APP_CONTAINER_SELECTOR) 275 | 276 | const wrapApp = AppComponent => 277 | 278 | 279 | 280 | 281 | ReactDOM.render(wrapApp(App), rootEl) 282 | 283 | if (module.hot) { 284 | // flow-disable-next-line 285 | module.hot.accept('./app', () => { 286 | // eslint-disable-next-line global-require 287 | const NextApp = require('./app').default 288 | ReactDOM.render(wrapApp(NextApp), rootEl) 289 | }) 290 | } 291 | ``` 292 | 293 | Nous avons besoin de créer à notre `App` un enfant de l'`AppContainer` de `react-hot-loader`, et nous allons avoir besoin de `require` la prochaine version de notre `App` lors du *hot reloading*. Pour faire en sorte que ce processus soit propre et respecte bien les principes DRY (don't repeat yourself - ne te répète pas :fr:), nous créons une petite fonction `wrapApp` que nous utilisons aux endroits où il a besoin d'afficher `App`. Vous êtes libre de bouger `eslint-disable global-require` en haut du fichier pour que ce soit plus lisible. 294 | 295 | :checkered_flag: S'il était toujours en train de tourner, redémarrez votre processus `yarn dev:wds`. Ouvrez `localhost:8000` dans votre navigateur. Dans l'onglet console de l'inspecteur du navigateur, vous devriez voir des logs qui concerne HMR. Allez-y, changez quelque chose dans `src/client/app.jsx` et sauvegardez. Vos changements ont lieu dans le navigateur en quelques secondes sans que la page ait à se recharger entièrement ! :+1: 296 | 297 | Prochaine section: [05 - Redux, Immutable, Fetch](05-redux-immutable-fetch.md#readme) 298 | 299 | Retourner à la [section précédente](03-express-nodemon-pm2.md#readme) ou au [sommaire](https://github.com/naomihauret/js-stack-from-scratch#table-of-contents). 300 | -------------------------------------------------------------------------------- /tutorial/05-redux-immutable-fetch.md: -------------------------------------------------------------------------------- 1 | # 05 - Redux, Immutable et Fetch 2 | 3 | Le code pour ce chapitre est disponible [ici](https://github.com/verekia/js-stack-walkthrough/tree/master/05-redux-immutable-fetch). 4 | 5 | Dans ce chapitre, nous allons coupler React et Redux pour faire une app très simple. Cette app contiendra un message et un bouton. Le message changera lorsque l'utilisateur clique sur le bouton. 6 | 7 | Avant de commencer, voici une rapide introduction à ImmutableJS, qui n'a absolument rien à voir avec React et Redux mais que nous utiliserons dans ce chapitre. 8 | 9 | ## ImmutableJS 10 | 11 | > :bulb: **[ImmutableJS](https://facebook.github.io/immutable-js/)** (ou Immutable) est une bibliothèque Facebook qui sert à manipuler des collections immuables (immutable :uk:), comme les listes ou les maps. N'importe quel changement effectué sur une collection immuable retourne un nouvel objet sans transformer l'objet original. 12 | 13 | Par exemple, au lieu de faire : 14 | 15 | ```js 16 | const obj = { a: 1 } 17 | obj.a = 2 // Mute `obj` 18 | ``` 19 | 20 | Vous feriez 21 | 22 | ```js 23 | const obj = Immutable.Map({ a: 1 }) 24 | obj.set('a', 2) // Retourne un nouvel object sans muter `obj` 25 | ``` 26 | 27 | Cette approche suit le paradigme de la **programmation fonctionnelle** qui fonctionne très bien avec Redux. :ok_hand: 28 | 29 | Quand on crée des collections immuables, la méthode `Immutable.fromJS()` s'avère être très pratique : elle prend n'importe quel objet/tableau JS et retourne une version immuable de celui-ci : 30 | 31 | ```js 32 | const immutablePerson = Immutable.fromJS({ 33 | name: 'Stan', 34 | friends: ['Kyle', 'Cartman', 'Kenny'], 35 | }) 36 | 37 | console.log(immutablePerson) 38 | 39 | /* 40 | * Map { 41 | * "name": "Stan", 42 | * "friends": List [ "Kyle", "Cartman", "Kenny" ] 43 | * } 44 | */ 45 | ``` 46 | 47 | - Lancez `yarn add immutable@4.0.0-rc.2` 48 | 49 | ## Redux 50 | 51 | > :bulb: **[Redux](http://redux.js.org/)** est une bibliothèque qui va prendre en charge les cycles de vie de notre application. Redux crée un *store*, qui est la seule source de vérité à propos du *state* (l'état :fr:) de votre app à n'importe quel moment. 52 | 53 | Commençons par la partie simple, déclarons nos actions Redux : 54 | 55 | - Lancez `yarn add redux redux-actions` 56 | 57 | - Créez un fichier `src/client/action/hello.js` qui contient : 58 | 59 | ```js 60 | // @flow 61 | 62 | import { createAction } from 'redux-actions' 63 | 64 | export const SAY_HELLO = 'SAY_HELLO' 65 | 66 | export const sayHello = createAction(SAY_HELLO) 67 | ``` 68 | 69 | Ce fichier expose une *action*, `SAY_HELLO`, et son *action creator* (*créateur d'action* :fr:), `sayHello`, qui est une fonction. Nous utilisons [`redux-actions`](https://github.com/acdlite/redux-actions) pour réduire le *boilerplate* associés aux actions Redux. `redux-actions` implémente le modèle de [standard d'action Flux](https://github.com/acdlite/flux-standard-action) , qui fait que les *action creators* retournent des objets ayant les attributs `type` et `payload`. 70 | 71 | - Créez une fichier `src/client/reducer/hello.js` qui contient : 72 | 73 | ```js 74 | // @flow 75 | 76 | import Immutable from 'immutable' 77 | import type { fromJS as Immut } from 'immutable' 78 | 79 | import { SAY_HELLO } from '../action/hello' 80 | 81 | const initialState = Immutable.fromJS({ 82 | message: 'Initial reducer message', 83 | }) 84 | 85 | const helloReducer = (state: Immut = initialState, action: { type: string, payload: any }) => { 86 | switch (action.type) { 87 | case SAY_HELLO: 88 | return state.set('message', action.payload) 89 | default: 90 | return state 91 | } 92 | } 93 | 94 | export default helloReducer 95 | ``` 96 | 97 | Dans ce fichier, on initialise le *state* (l'état :fr:) de notre *reducer* (réducteur :fr:) avec un map immuable contenant une propriété, `message`, initialisé à `Initial reducer message`. Le `helloReducer` gère l'action `SAY_HELLO` en initialisant le nouveau `message` avec le *payload* (la charge :fr:) de l'action. L'annotation Flow pour `action` la déstructure en un `type` et un `payload`. Le `payload` peut être de n'importe quel type. Ca peut avoir l'air assez funky si c'est la première fois que vous voyez ça, mais ça reste assez compréhensible. Pour le type de `state`, on utilise l'instruction Flow `import type`pour obtenir le type retourné de `fromJS`. On le renomme `Immut` pour plus de clareté, car `state: fromJS` serait assez confus. La ligne `import type` sera retirée comme n'importe quelle autre annotation Flow. Remarquez l'utilisation de `Immutable.fromJS()` et `set()` comme nous avons pu les voir précédemment. 98 | 99 | ## React-Redux 100 | 101 | > :bulb: **[react-redux](https://github.com/reactjs/react-redux)** *connecte* un store Redux avec des composants React. Avec `react-redux`, quand le store Redux change, les composants React sont modifiés automatiquement. Ils déclenchent aussi les actions Redux. 102 | 103 | - Lancez `yarn add react-redux` 104 | 105 | Dans ces sections, nous allons créer des *Components* (composants :fr:) des *Containers* (conteneurs :fr:). 106 | 107 | Les **Components** sont des composants React *stupides*, dans le sens où ils ne savent rien du state Redux (l'état de Redux :fr:). Les **Containers** sont des composants *intelligents* qui connaissent le *state* et que nous allons *connecter* à nos composants stupides. 108 | 109 | - Créez un fichier `src/client/component/button.jsx` contenant: 110 | 111 | ```js 112 | // @flow 113 | 114 | import React from 'react' 115 | 116 | type Props = { 117 | label: string, 118 | handleClick: Function, 119 | } 120 | 121 | const Button = ({ label, handleClick }: Props) => 122 | 123 | 124 | export default Button 125 | ``` 126 | 127 | **Remarque**: Vous pouvez voir un cas de *type alias* (alias de type :fr:) Flow ici. Nous définissons le type de `Props` avant d'annoter les `props` déstructurées de notre composant avec. 128 | 129 | - Créez un fichier `src/client/component/message.jsx` contenant : 130 | 131 | ```js 132 | // @flow 133 | 134 | import React from 'react' 135 | 136 | type Props = { 137 | message: string, 138 | } 139 | 140 | const Message = ({ message }: Props) => 141 |

{message}

142 | 143 | export default Message 144 | ``` 145 | 146 | Voici des exemples de composants *stupides*. Ils n'ont pas de logique et affichent juste ce qu'on leur dit d'afficher via les React **props**. La principale différence entre `button.jsx` et `message.jsx` est que `Button` contient une référence à un *action dispatcher* (expéditeur d'action :fr:) dans ses props, où `Message` contient juste quelques données à afficher. 147 | 148 | Encore une fois, les *components* ne savent rien des **actions** Redux ou sur le **state** de notre app. C'est pourquoi nous allons créer des **containers** intelligents qui alimenterons les bons *action dispatchers* et les bonnes données à ces composants stupides. 149 | 150 | - Créez un fichier `src/client/container/hello-button.js` contenant : 151 | 152 | ```js 153 | // @flow 154 | 155 | import { connect } from 'react-redux' 156 | 157 | import { sayHello } from '../action/hello' 158 | import Button from '../component/button' 159 | 160 | const mapStateToProps = () => ({ 161 | label: 'Say hello', 162 | }) 163 | 164 | const mapDispatchToProps = dispatch => ({ 165 | handleClick: () => { dispatch(sayHello('Hello!')) }, 166 | }) 167 | 168 | export default connect(mapStateToProps, mapDispatchToProps)(Button) 169 | ``` 170 | 171 | Ce container relie le composant `Button` avec l'action `sayHello` et la méthode `dispatch` de Redux. 172 | 173 | - Créez un fichier `src/client/container/message.js` contenant : 174 | 175 | ```js 176 | // @flow 177 | 178 | import { connect } from 'react-redux' 179 | 180 | import Message from '../component/message' 181 | 182 | const mapStateToProps = state => ({ 183 | message: state.hello.get('message'), 184 | }) 185 | 186 | export default connect(mapStateToProps)(Message) 187 | ``` 188 | Ce container relie le state de l'app Redux avec le composant `Message`. Quand le state change, `Message` ré-affichera automatique le bon prop (propriété :fr:) `message` . Ces connexions sont faites via la fonction `connect` de `react-redux`. 189 | 190 | - Modifier votre fichier `src/client/app.jsx` de la manière suivante : 191 | 192 | ```js 193 | // @flow 194 | 195 | import React from 'react' 196 | import HelloButton from './container/hello-button' 197 | import Message from './container/message' 198 | import { APP_NAME } from '../shared/config' 199 | 200 | const App = () => 201 |
202 |

{APP_NAME}

203 | 204 | 205 |
206 | 207 | export default App 208 | ``` 209 | 210 | Nous n'avons toujours pas initialisé le store Redux et n'avons pas encore mis ces deux containers dans notre app : 211 | 212 | - Éditez le fichier `src/client/index.jsx` comme ceci : 213 | 214 | ```js 215 | // @flow 216 | 217 | import 'babel-polyfill' 218 | 219 | import React from 'react' 220 | import ReactDOM from 'react-dom' 221 | import { AppContainer } from 'react-hot-loader' 222 | import { Provider } from 'react-redux' 223 | import { createStore, combineReducers } from 'redux' 224 | 225 | import App from './app' 226 | import helloReducer from './reducer/hello' 227 | import { APP_CONTAINER_SELECTOR } from '../shared/config' 228 | import { isProd } from '../shared/util' 229 | 230 | const store = createStore(combineReducers({ hello: helloReducer }), 231 | // eslint-disable-next-line no-underscore-dangle 232 | isProd ? undefined : window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) 233 | 234 | const rootEl = document.querySelector(APP_CONTAINER_SELECTOR) 235 | 236 | const wrapApp = (AppComponent, reduxStore) => 237 | 238 | 239 | 240 | 241 | 242 | 243 | ReactDOM.render(wrapApp(App, store), rootEl) 244 | 245 | if (module.hot) { 246 | // flow-disable-next-line 247 | module.hot.accept('./app', () => { 248 | // eslint-disable-next-line global-require 249 | const NextApp = require('./app').default 250 | ReactDOM.render(wrapApp(NextApp, store), rootEl) 251 | }) 252 | } 253 | ``` 254 | Prenons un instant pour revoir tout ça. D'abord, nous créons un *store* avec `createStore`. Les stores sont créés en leur passant des reducers. Ici, nous n'avons qu'un seul reducer, mais pour le bien de notre évolutivité future, nous utilisons `combineReducers` pour regrouper tous nos reducers ensemble. Le dernier paramètre bizarre de `createStore` est un truc pour relier Redux aux [outils de développement](https://github.com/zalmoxisus/redux-devtools-extension) (Redux Devtools :uk:) du navigateur, qui sont incroyablement pratiques pour débugger. Puisque ESLint va se plaindre des underscores dans `__REDUX_DEVTOOLS_EXTENSION__`, on désactive cette règle. Ensuite, on *emballe* toute notre app dans le composant `Provider` de `react-redux`' grâce à notre fonction `wrapApp`, et lui passons notre store. 255 | 256 | 257 | :checkered_flag: Maintenant, vous pouvez lancer `yarn start` et `yarn dev:wds` et vous rendre sur `http://localhost:8000`. Vous devriez voir s'afficher "Initial reducer message" et un bouton. Quand vous cliquez sur le bouton, le message devrait changer pour "Hello!". Si vous avez installé les outils de développement Redux dans votre navigateur, vous devriez voir le state de votre app changer au fur et à mesure que vous cliquez sur le bouton. 258 | 259 | Félicitations, nous avons enfin créé une app qui fait quelque chose :tada: :clap: ! Bon d'accord, ce n'est pas super impressionnant de l'extérieur, mais on sait tous que c'est propulsé par une stack hyper badass sous le capot :wink: . 260 | 261 | ## Étendre notre app avec un appel asynchrone 262 | 263 | Nous allons maintenant ajouter un second bouton à notre app. Il déclenchera un appel AJAX pour récupérer un message depuis le serveur. Pour le bien de la démo, cet appel enverra aussi quelques données, le nombre codé en dur `1234`. 264 | 265 | ### L'extrémité du serveur (endpoint :uk:) 266 | 267 | - Créez un fichier `src/shared/routes.js` contenant : 268 | 269 | ```js 270 | // @flow 271 | 272 | // eslint-disable-next-line import/prefer-default-export 273 | export const helloEndpointRoute = (num: ?number) => `/ajax/hello/${num || ':num'}` 274 | ``` 275 | 276 | Cette fonction est une petite aide pour nous aider à produire les lignes suivantes : 277 | 278 | ```js 279 | helloEndpointRoute() // -> '/ajax/hello/:num' (for Express) 280 | helloEndpointRoute(1234) // -> '/ajax/hello/1234' (for the actual call) 281 | ``` 282 | 283 | Maintenant, créons rapidement un fichier de test pour nous assurer que tout fonctionne correctement : 284 | 285 | - Créez un fichier `src/shared/routes.test.js` contenant : 286 | 287 | ```js 288 | import { helloEndpointRoute } from './routes' 289 | 290 | test('helloEndpointRoute', () => { 291 | expect(helloEndpointRoute()).toBe('/ajax/hello/:num') 292 | expect(helloEndpointRoute(123)).toBe('/ajax/hello/123') 293 | }) 294 | ``` 295 | 296 | - Lancez `yarn test` et tous les tests devraient se dérouler avec succès 297 | 298 | - Dans `src/server/index.js`, ajoutez les lignes suivantes : 299 | 300 | ```js 301 | import { helloEndpointRoute } from '../shared/routes' 302 | 303 | // [Au-dessous de app.get('/')...] 304 | 305 | app.get(helloEndpointRoute(), (req, res) => { 306 | res.json({ serverMessage: `Hello from the server! (received ${req.params.num})` }) 307 | }) 308 | ``` 309 | 310 | ### Nouveaux containers 311 | 312 | - Créez un fichier `src/client/container/hello-async-button.js` contenant : 313 | 314 | ```js 315 | // @flow 316 | 317 | import { connect } from 'react-redux' 318 | 319 | import { sayHelloAsync } from '../action/hello' 320 | import Button from '../component/button' 321 | 322 | const mapStateToProps = () => ({ 323 | label: 'Say hello asynchronously and send 1234', 324 | }) 325 | 326 | const mapDispatchToProps = dispatch => ({ 327 | handleClick: () => { dispatch(sayHelloAsync(1234)) }, 328 | }) 329 | 330 | export default connect(mapStateToProps, mapDispatchToProps)(Button) 331 | ``` 332 | 333 | Afin de démontrer comment vous passeriez un paramètre à votre appel asynchrone et pour garder les choses simples, nous allons coder en dur la valeur `1234` ici. Typiquement, cette valeur pourrait venir d'un champ de formulaire rempli par l'utilisateur. 334 | 335 | - Créez un fichier `src/client/container/message-async.js` contenant : 336 | 337 | ```js 338 | // @flow 339 | 340 | import { connect } from 'react-redux' 341 | 342 | import MessageAsync from '../component/message' 343 | 344 | const mapStateToProps = state => ({ 345 | message: state.hello.get('messageAsync'), 346 | }) 347 | 348 | export default connect(mapStateToProps)(MessageAsync) 349 | ``` 350 | 351 | Vous pouvez voir que dans ce container, nous faisons référence à une propriété `messageAsync`, que nous allons bientôt ajouter à notre reducer. 352 | 353 | Ce dont nous avons besoin maintenant, c'est de créer l'action `sayHelloAsync`. 354 | 355 | ### Fetch 356 | 357 | > :bulb: **[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)** est une fonction JavaScript standardisée pour faire des appels asynchrones inspirée par les méthodes AJAX de jQuery. 358 | 359 | Nous allons utiliser `fetch` pour faire des appels au serveur depuis le client. `fetch` n'est pas encore supporté par tous les navigateurs, donc nous allons avoir besoin d'un polyfill (c'est-à-dire un ensemble de fonctions permettant de simuler une ou des fonctionnalités qui ne sont pas nativement disponibles dans le navigateur). 360 | 361 | `isomorphic-fetch` est un polyfill qui fait fonctionner `fetch` de façon cross-browsers et dans Node aussi ! :ok_hand: 362 | 363 | - Lancez `yarn add isomorphic-fetch` 364 | 365 | Puisque nous utilisons `eslint-plugin-compat`, nous allons avoir besoin d'indiquer que nous utilisons un polyfill pour `fetch`, histoire de ne pas recevoir des warning à ce sujet. 366 | 367 | - Ajoutez les lignes suivantes à votre fichier `.eslintrc.json` : 368 | 369 | ```json 370 | "settings": { 371 | "polyfills": ["fetch"] 372 | }, 373 | ``` 374 | 375 | ### 3 actions asynchrones 376 | 377 | `sayHelloAsync` ne va pas être une action normale. Les actions asynchrones sont le plus souvent séparées en 3 actions qui déclenchent 3 states différents: une action *requête* ou *chargement* (request ou loading :uk:), une action *succès* (success :uk:) et une action échec (failure :uk:) 378 | 379 | - Éditez le fichier `src/client/action/hello.js` comme ceci : 380 | 381 | ```js 382 | // @flow 383 | 384 | import 'isomorphic-fetch' 385 | 386 | import { createAction } from 'redux-actions' 387 | import { helloEndpointRoute } from '../../shared/routes' 388 | 389 | export const SAY_HELLO = 'SAY_HELLO' 390 | export const SAY_HELLO_ASYNC_REQUEST = 'SAY_HELLO_ASYNC_REQUEST' 391 | export const SAY_HELLO_ASYNC_SUCCESS = 'SAY_HELLO_ASYNC_SUCCESS' 392 | export const SAY_HELLO_ASYNC_FAILURE = 'SAY_HELLO_ASYNC_FAILURE' 393 | 394 | export const sayHello = createAction(SAY_HELLO) 395 | export const sayHelloAsyncRequest = createAction(SAY_HELLO_ASYNC_REQUEST) 396 | export const sayHelloAsyncSuccess = createAction(SAY_HELLO_ASYNC_SUCCESS) 397 | export const sayHelloAsyncFailure = createAction(SAY_HELLO_ASYNC_FAILURE) 398 | 399 | export const sayHelloAsync = (num: number) => (dispatch: Function) => { 400 | dispatch(sayHelloAsyncRequest()) 401 | return fetch(helloEndpointRoute(num), { method: 'GET' }) 402 | .then((res) => { 403 | if (!res.ok) throw Error(res.statusText) 404 | return res.json() 405 | }) 406 | .then((data) => { 407 | if (!data.serverMessage) throw Error('No message received') 408 | dispatch(sayHelloAsyncSuccess(data.serverMessage)) 409 | }) 410 | .catch(() => { 411 | dispatch(sayHelloAsyncFailure()) 412 | }) 413 | } 414 | ``` 415 | 416 | Au lieu de retourner une action, `sayHelloAsync` retourne une fonction qui lance l'appel `fetch`. `fetch` retourne une `Promise`, que nous utilisons pour *expédier* différentes actions selon l'état actuel de notre appel asynchrone. 417 | 418 | ### 3 gestionnaires d'actions asynchrones 419 | 420 | Gérons ces différentes actions dans `src/client/reducer/hello.js`: 421 | 422 | ```js 423 | // @flow 424 | 425 | import Immutable from 'immutable' 426 | import type { fromJS as Immut } from 'immutable' 427 | 428 | import { 429 | SAY_HELLO, 430 | SAY_HELLO_ASYNC_REQUEST, 431 | SAY_HELLO_ASYNC_SUCCESS, 432 | SAY_HELLO_ASYNC_FAILURE, 433 | } from '../action/hello' 434 | 435 | const initialState = Immutable.fromJS({ 436 | message: 'Initial reducer message', 437 | messageAsync: 'Initial reducer message for async call', 438 | }) 439 | 440 | const helloReducer = (state: Immut = initialState, action: { type: string, payload: any }) => { 441 | switch (action.type) { 442 | case SAY_HELLO: 443 | return state.set('message', action.payload) 444 | case SAY_HELLO_ASYNC_REQUEST: 445 | return state.set('messageAsync', 'Loading...') 446 | case SAY_HELLO_ASYNC_SUCCESS: 447 | return state.set('messageAsync', action.payload) 448 | case SAY_HELLO_ASYNC_FAILURE: 449 | return state.set('messageAsync', 'No message received, please check your connection') 450 | default: 451 | return state 452 | } 453 | } 454 | 455 | export default helloReducer 456 | ``` 457 | 458 | Nous avons ajouté un nouveau champ dans notre store, et nous le modifions avec différents messages selon l'action que nous recevons. Pendant `SAY_HELLO_ASYNC_REQUEST`, nous montrons `Loading...`. `SAY_HELLO_ASYNC_SUCCESS` modifie `messageAsync` d'une façon similaire à comment `SAY_HELLO` modifie `message`. `SAY_HELLO_ASYNC_FAILURE` donne un message d'erreur. 459 | 460 | 461 | ### Redux-thunk 462 | 463 | Dans `src/client/action/hello.js`, nous avons créé `sayHelloAsync`, un créateur d'actions qui retourne une fonction. En fait, ce n'est pas une fonctionnalité qui est nativement supportée par Redux. Afin de pouvoir effectuer ces actions asynchrones nous avons besoin d'étendre la fonctionnalité de Redux avec le *middleware* `redux-thunk`. 464 | 465 | - Lancez `yarn add redux-thunk` 466 | 467 | - Modifier votre fichier `src/client/index.jsx` comme ceci : 468 | 469 | ```js 470 | // @flow 471 | 472 | import 'babel-polyfill' 473 | 474 | import React from 'react' 475 | import ReactDOM from 'react-dom' 476 | import { AppContainer } from 'react-hot-loader' 477 | import { Provider } from 'react-redux' 478 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux' 479 | import thunkMiddleware from 'redux-thunk' 480 | 481 | import App from './app' 482 | import helloReducer from './reducer/hello' 483 | import { APP_CONTAINER_SELECTOR } from '../shared/config' 484 | import { isProd } from '../shared/util' 485 | 486 | // eslint-disable-next-line no-underscore-dangle 487 | const composeEnhancers = (isProd ? null : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose 488 | 489 | const store = createStore(combineReducers({ hello: helloReducer }), 490 | composeEnhancers(applyMiddleware(thunkMiddleware))) 491 | 492 | const rootEl = document.querySelector(APP_CONTAINER_SELECTOR) 493 | 494 | const wrapApp = (AppComponent, reduxStore) => 495 | 496 | 497 | 498 | 499 | 500 | 501 | ReactDOM.render(wrapApp(App, store), rootEl) 502 | 503 | if (module.hot) { 504 | // flow-disable-next-line 505 | module.hot.accept('./app', () => { 506 | // eslint-disable-next-line global-require 507 | const NextApp = require('./app').default 508 | ReactDOM.render(wrapApp(NextApp, store), rootEl) 509 | }) 510 | } 511 | ``` 512 | 513 | Ici, nous passons`redux-thunk` à la fonction `applyMiddleware` de Redux. Afin que les outils de développement Redux continuent de fonctionner, nous avons aussi besoin d'utiliser la fonction `compose` de Redux. Ne vous en faites pas trop à propos de cette partie, retenez juste qu'on améliore Redux avec `redux-thunk`. 514 | 515 | - Modifiez le fichier `src/client/app.jsx` comme ceci : 516 | 517 | ```js 518 | // @flow 519 | 520 | import React from 'react' 521 | import HelloButton from './container/hello-button' 522 | import HelloAsyncButton from './container/hello-async-button' 523 | import Message from './container/message' 524 | import MessageAsync from './container/message-async' 525 | import { APP_NAME } from '../shared/config' 526 | 527 | const App = () => 528 |
529 |

{APP_NAME}

530 | 531 | 532 | 533 | 534 |
535 | 536 | export default App 537 | ``` 538 | 539 | :checkered_flag: Lancez `yarn start` et `yarn dev:wds` et vous devriez maintenant être capable de cliquer sur le bouton "Say hello asynchronously and send 1234" et de récupérer le message correspondant depuis le serveur ! Puisque vous travaillez en local, l'appel est instantané, mais si vous ouvrez les outils de développement Redux, vous remarquerez que chaque clic déclenche à la fois `SAY_HELLO_ASYNC_REQUEST` et `SAY_HELLO_ASYNC_SUCCESS`, ce qui fait que le message passe par le state intermédiaire `Loading...` comme on l'attendait. 540 | 541 | Vous pouvez vous félicitez, c'était une section intense :tada: :clap: ! Mais ajoutons quelques tests :wink: 542 | 543 | ## Tests 544 | 545 | Dans cette section, nous allons tester nos actions et notre reducer. Commençons avec les actions. 546 | 547 | Afin d'isoler la logique spécifique au fichier `action/hello.js`, on va se moquer des choses qui ne le concerne pas, ainsi que cette requête AJAX `fetch` qui ne devrait pas déclencher un véritable appel AJAX dans nos tests. 548 | 549 | - Lancez `yarn add --dev redux-mock-store fetch-mock` 550 | 551 | - Créez un fichier `src/client/action/hello.test.js` contenant : 552 | 553 | ```js 554 | import fetchMock from 'fetch-mock' 555 | import configureMockStore from 'redux-mock-store' 556 | import thunkMiddleware from 'redux-thunk' 557 | 558 | import { 559 | sayHelloAsync, 560 | sayHelloAsyncRequest, 561 | sayHelloAsyncSuccess, 562 | sayHelloAsyncFailure, 563 | } from './hello' 564 | 565 | import { helloEndpointRoute } from '../../shared/routes' 566 | 567 | const mockStore = configureMockStore([thunkMiddleware]) 568 | 569 | afterEach(() => { 570 | fetchMock.restore() 571 | }) 572 | 573 | test('sayHelloAsync success', () => { 574 | fetchMock.get(helloEndpointRoute(666), { serverMessage: 'Async hello success' }) 575 | const store = mockStore() 576 | return store.dispatch(sayHelloAsync(666)) 577 | .then(() => { 578 | expect(store.getActions()).toEqual([ 579 | sayHelloAsyncRequest(), 580 | sayHelloAsyncSuccess('Async hello success'), 581 | ]) 582 | }) 583 | }) 584 | 585 | test('sayHelloAsync 404', () => { 586 | fetchMock.get(helloEndpointRoute(666), 404) 587 | const store = mockStore() 588 | return store.dispatch(sayHelloAsync(666)) 589 | .then(() => { 590 | expect(store.getActions()).toEqual([ 591 | sayHelloAsyncRequest(), 592 | sayHelloAsyncFailure(), 593 | ]) 594 | }) 595 | }) 596 | 597 | test('sayHelloAsync data error', () => { 598 | fetchMock.get(helloEndpointRoute(666), {}) 599 | const store = mockStore() 600 | return store.dispatch(sayHelloAsync(666)) 601 | .then(() => { 602 | expect(store.getActions()).toEqual([ 603 | sayHelloAsyncRequest(), 604 | sayHelloAsyncFailure(), 605 | ]) 606 | }) 607 | }) 608 | ``` 609 | Bien, jetons un oeil à ce qui se passe ici. D'abord, on *mock* (on ignore) le store Redux en utilisant `const mockStore = configureMockStore([thunkMiddleware])`. En faisant cela, on peut expédier des actions sans qu'ils déclenchent la logique du reducer. Pour chaque test, on *mock* `fetch` en utilisant `fetchMock.get()` et le faisons retourner ce que nous voulons. En fait, ce qu'on teste avec `expect()`, c'est quelles séries d'actions ont été expédiées par le store, grâce à la fonction `store.getActions()` de `redux-mock-store`. Après chaque test, on restaure le comportement normal de `fetch` avec `fetchMock.restore()`. 610 | 611 | Et maintenant, testons notre reducer (ce qui est beaucoup plus simple) : 612 | 613 | - Créez un fichier `src/client/reducer/hello.test.js` contenant : 614 | 615 | ```js 616 | import { 617 | sayHello, 618 | sayHelloAsyncRequest, 619 | sayHelloAsyncSuccess, 620 | sayHelloAsyncFailure, 621 | } from '../action/hello' 622 | 623 | import helloReducer from './hello' 624 | 625 | let helloState 626 | 627 | beforeEach(() => { 628 | helloState = helloReducer(undefined, {}) 629 | }) 630 | 631 | test('handle default', () => { 632 | expect(helloState.get('message')).toBe('Initial reducer message') 633 | expect(helloState.get('messageAsync')).toBe('Initial reducer message for async call') 634 | }) 635 | 636 | test('handle SAY_HELLO', () => { 637 | helloState = helloReducer(helloState, sayHello('Test')) 638 | expect(helloState.get('message')).toBe('Test') 639 | }) 640 | 641 | test('handle SAY_HELLO_ASYNC_REQUEST', () => { 642 | helloState = helloReducer(helloState, sayHelloAsyncRequest()) 643 | expect(helloState.get('messageAsync')).toBe('Loading...') 644 | }) 645 | 646 | test('handle SAY_HELLO_ASYNC_SUCCESS', () => { 647 | helloState = helloReducer(helloState, sayHelloAsyncSuccess('Test async')) 648 | expect(helloState.get('messageAsync')).toBe('Test async') 649 | }) 650 | 651 | test('handle SAY_HELLO_ASYNC_FAILURE', () => { 652 | helloState = helloReducer(helloState, sayHelloAsyncFailure()) 653 | expect(helloState.get('messageAsync')).toBe('No message received, please check your connection') 654 | }) 655 | ``` 656 | 657 | Avant chaque test, on initialise `helloState` avec le résultat par défaut de notre reducer (le cas `default` de notre `switch` dans le reducer, qui retourne `initialState`). Les tests deviennent alors très explicites, on s'assure juste que le reducer modifie `message` et `messageAsync` correctement selon l'action qu'il reçoit. 658 | 659 | :checkered_flag: Lancez `yarn test`. Tout devrait être vert ! 660 | 661 | Prochaine: [06 - React Router, Server-Side Rendering, Helmet](06-react-router-ssr-helmet.md#readme) 662 | 663 | Retourner à la [section précédente](04-webpack-react-hmr.md#readme) ou au [sommaire](https://github.com/naomihauret/js-stack-from-scratch#table-of-contents). 664 | -------------------------------------------------------------------------------- /tutorial/06-react-router-ssr-helmet.md: -------------------------------------------------------------------------------- 1 | # 06 - React Router, Server-Side Rendering et Helmet 2 | 3 | Le code pour ce chapitre est disponible [ici](https://github.com/verekia/js-stack-walkthrough/tree/master/06-react-router-ssr-helmet). 4 | 5 | Dans ce chapitre, nos allons créer différentes pages pour notre app et rendre la navigation entre celles-ci possible. C'est parti ! 6 | 7 | ## React Router 8 | 9 | > :bulb: **[React Router](https://reacttraining.com/react-router/)** est une bibliothèque faite pour naviguer entre les différentes pages de votre app React. Elle peut être utilisée à la fois sur le client et aussi sur le serveur. 10 | 11 | React Router a eu de grosses modifications à la sortie de la v4, toujours en beta. Puisqu'on a envie que ce tutoriel soit durable, nous utiliserons la v4 de React Router. 12 | 13 | - Lancez `yarn add react-router@next react-router-dom@next` 14 | 15 | Du côté du client, on a d'abord besoin *d'emballer* notre app dans un composant `BrowserRouter`. 16 | 17 | - Modifier votre fichier `src/client/index.jsx` comme ceci : 18 | 19 | ```js 20 | // [...] 21 | import { BrowserRouter } from 'react-router-dom' 22 | // [...] 23 | const wrapApp = (AppComponent, reduxStore) => 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ``` 32 | 33 | ## Pages 34 | 35 | Notre app aura 4 pages : 36 | 37 | - Une page "Home" 38 | - Une page "Hello" qui montre un bouton et un message pour l'action synchrone 39 | - Une page "Hello Async" qui montre un bouton et un message pour l'action asynchrone 40 | - Une page 404 "Not Found". 41 | 42 | - Créez un fichier `src/client/component/page/home.jsx` contenant : 43 | 44 | ```js 45 | // @flow 46 | 47 | import React from 'react' 48 | 49 | const HomePage = () =>

Home

50 | 51 | export default HomePage 52 | ``` 53 | 54 | - Créez un fichier `src/client/component/page/hello.jsx` contenant : 55 | 56 | ```js 57 | // @flow 58 | 59 | import React from 'react' 60 | 61 | import HelloButton from '../../container/hello-button' 62 | import Message from '../../container/message' 63 | 64 | const HelloPage = () => 65 |
66 | 67 | 68 |
69 | 70 | export default HelloPage 71 | 72 | ``` 73 | 74 | - Créez un fichier `src/client/component/page/hello-async.jsx` contenant : 75 | 76 | ```js 77 | // @flow 78 | 79 | import React from 'react' 80 | 81 | import HelloAsyncButton from '../../container/hello-async-button' 82 | import MessageAsync from '../../container/message-async' 83 | 84 | const HelloAsyncPage = () => 85 |
86 | 87 | 88 |
89 | 90 | export default HelloAsyncPage 91 | ``` 92 | 93 | - Créez un fichier `src/client/component/page/not-found.jsx` contenant : 94 | 95 | ```js 96 | // @flow 97 | 98 | import React from 'react' 99 | 100 | const NotFoundPage = () =>

Page not found

101 | 102 | export default NotFoundPage 103 | ``` 104 | 105 | ## Navigation 106 | 107 | Ajoutons quelques routes dans le fichier de configuration partagé. 108 | 109 | - Éditez votre fichier `src/shared/routes.js` comme ceci : 110 | 111 | ```js 112 | // @flow 113 | 114 | export const HOME_PAGE_ROUTE = '/' 115 | export const HELLO_PAGE_ROUTE = '/hello' 116 | export const HELLO_ASYNC_PAGE_ROUTE = '/hello-async' 117 | export const NOT_FOUND_DEMO_PAGE_ROUTE = '/404' 118 | 119 | export const helloEndpointRoute = (num: ?number) => `/ajax/hello/${num || ':num'}` 120 | ``` 121 | 122 | La route `/404` va juste être utilisée dans un lien de navigation pour bien montrer ce qui se passe quand on clique sur un lien brisé . 123 | 124 | - Créez un fichier `src/client/component/nav.jsx` contenant : 125 | 126 | ```js 127 | // @flow 128 | 129 | import React from 'react' 130 | import { NavLink } from 'react-router-dom' 131 | import { 132 | HOME_PAGE_ROUTE, 133 | HELLO_PAGE_ROUTE, 134 | HELLO_ASYNC_PAGE_ROUTE, 135 | NOT_FOUND_DEMO_PAGE_ROUTE, 136 | } from '../../shared/routes' 137 | 138 | const Nav = () => 139 | 153 | 154 | export default Nav 155 | ``` 156 | 157 | Ici on crée simplement quelques `NavLink` qui utilisent les routes que nous avons déclarées précédemment. 158 | 159 | - Pour finir, éditez `src/client/app.jsx` comme ceci : 160 | 161 | ```js 162 | // @flow 163 | 164 | import React from 'react' 165 | import { Switch } from 'react-router' 166 | import { Route } from 'react-router-dom' 167 | import { APP_NAME } from '../shared/config' 168 | import Nav from './component/nav' 169 | import HomePage from './component/page/home' 170 | import HelloPage from './component/page/hello' 171 | import HelloAsyncPage from './component/page/hello-async' 172 | import NotFoundPage from './component/page/not-found' 173 | import { 174 | HOME_PAGE_ROUTE, 175 | HELLO_PAGE_ROUTE, 176 | HELLO_ASYNC_PAGE_ROUTE, 177 | } from '../shared/routes' 178 | 179 | const App = () => 180 |
181 |

{APP_NAME}

182 |
190 | 191 | export default App 192 | ``` 193 | 194 | :checkered_flag: Lancez `yarn start` et `yarn dev:wds`. Dans votre navigateur, rendez-vous sur `http://localhost:8000`, et cliquez sur les liens pour naviguer entre nos différentes pages. Vous devriez voir que l'URL change dynamiquement. Passez d'une page à l'autre et utilisez le bouton "page précédente" de votre navigateur pour voir que l'historique de votre navigateur fonctionne comme prévu. 195 | 196 | Maintenant, disons que vous êtes allés sur `http://localhost:8000/hello` de cette façon. Rafraîchissez la page. Maintenant, vous obtenez une 404, parce que notre serveur Express ne répond qu'à `/`. En fait, quand vous naviguiez entre les pages, vous le faisiez seulement du côté du client. Maintenant, ajoutons le server-side rendering (rendu côté serveur :fr:) à tout ceci pour obtenir le bon comportement. 197 | 198 | ## Server-Side Rendering 199 | 200 | > :question: Le **Server-Side Rendering**, ou rendu côté serveur, signifie afficher votre app au chargement initial de la page au lieu de s'appuyer sur le JavaScript pour l'afficher dans le navigateur du client. 201 | 202 | Le SSR est essentiel pour le SEO (search engine optimization - référencement naturel :fr: ) et fournit une meilleure expérience utilisateur en montrant tout de suite notre app à l'utilisateur 203 | 204 | La première chose que nous allons faire ici, c'est migrer la plupart de notre code client dans la partie partagée / isomorphique / universelle de notre code, puisque c'est le serveur qui va maintenant afficher notre app React. 205 | 206 | ### La grosse migration vers `shared` 207 | 208 | - Déplacez tous vos fichiers situés dans le dossier `client` vers `shared`, **sauf** `src/client/index.jsx`. 209 | 210 | On doit modifier plusieurs de nos imports. 211 | 212 | - Dans `src/client/index.jsx`,remplacez les 3 occurences de `'./app'` par `'../shared/app'`, et `'./reducer/hello'` par `'../shared/reducer/hello'` 213 | 214 | - Dans `src/shared/app.jsx`, remplacez `'../shared/routes'` par `'./routes'` et `'../shared/config'` par `'./config'` 215 | 216 | - Dans `src/shared/component/nav.jsx`, remplacez `'../../shared/routes'` par `'../routes'` 217 | 218 | ### Changements du serveur 219 | 220 | - Créez un fichier `src/server/routing.js` contenant : 221 | 222 | ```js 223 | // @flow 224 | 225 | import { 226 | homePage, 227 | helloPage, 228 | helloAsyncPage, 229 | helloEndpoint, 230 | } from './controller' 231 | 232 | import { 233 | HOME_PAGE_ROUTE, 234 | HELLO_PAGE_ROUTE, 235 | HELLO_ASYNC_PAGE_ROUTE, 236 | helloEndpointRoute, 237 | } from '../shared/routes' 238 | 239 | import renderApp from './render-app' 240 | 241 | export default (app: Object) => { 242 | app.get(HOME_PAGE_ROUTE, (req, res) => { 243 | res.send(renderApp(req.url, homePage())) 244 | }) 245 | 246 | app.get(HELLO_PAGE_ROUTE, (req, res) => { 247 | res.send(renderApp(req.url, helloPage())) 248 | }) 249 | 250 | app.get(HELLO_ASYNC_PAGE_ROUTE, (req, res) => { 251 | res.send(renderApp(req.url, helloAsyncPage())) 252 | }) 253 | 254 | app.get(helloEndpointRoute(), (req, res) => { 255 | res.json(helloEndpoint(req.params.num)) 256 | }) 257 | 258 | app.get('/500', () => { 259 | throw Error('Fake Internal Server Error') 260 | }) 261 | 262 | app.get('*', (req, res) => { 263 | res.status(404).send(renderApp(req.url)) 264 | }) 265 | 266 | // eslint-disable-next-line no-unused-vars 267 | app.use((err, req, res, next) => { 268 | // eslint-disable-next-line no-console 269 | console.error(err.stack) 270 | res.status(500).send('Something went wrong!') 271 | }) 272 | } 273 | ``` 274 | C'est dans ce fichier qu'on va gérer les requêtes et les réponses. Les appels à la logique métier sont externalisés dans un module `controller` différent. 275 | 276 | **Remarque**: Vous trouverez beaucoup d'exemple de React Router qui utilisent `*` comme route sur le serveur, laissant la gestion de toutes les routes à React Router. Puisque toutes les requêtes passent par la même fonction, ça rend l'implémentation de page style MVC (Model-View-Controller ; Modèle-Vue-Contrôleur :fr:) peu pratique. Au lieu de faire ça, ici on déclare explicitement les routes et leurs réponses afin d'être capable d'aller récupérer les données depuis la base de données et de les passer à une page donnée facilement. 277 | 278 | - Créez un fichier `src/server/controller.js` contenant : 279 | 280 | ```js 281 | // @flow 282 | 283 | export const homePage = () => null 284 | 285 | export const helloPage = () => ({ 286 | hello: { message: 'Server-side preloaded message' }, 287 | }) 288 | 289 | export const helloAsyncPage = () => ({ 290 | hello: { messageAsync: 'Server-side preloaded message for async page' }, 291 | }) 292 | 293 | export const helloEndpoint = (num: number) => ({ 294 | serverMessage: `Hello from the server! (received ${num})`, 295 | }) 296 | ``` 297 | 298 | Voici notre contrôleur. Typiquement, il s'occupe de toute la logique métier et des appels à la base de données mais, dans notre cas, on code juste en dur des résultats. Ces résultats sont repassés au module `routing` afin d'être utilisés pour initialiser notre store Redux SSR. (rappel : SSR = Server-Side Rendering = Rendu Côté Serveur :fr:) 299 | 300 | - Créez un fichier `src/server/init-store.js` contenant : 301 | 302 | ```js 303 | // @flow 304 | 305 | import Immutable from 'immutable' 306 | import { createStore, combineReducers, applyMiddleware } from 'redux' 307 | import thunkMiddleware from 'redux-thunk' 308 | 309 | import helloReducer from '../shared/reducer/hello' 310 | 311 | const initStore = (plainPartialState: ?Object) => { 312 | const preloadedState = plainPartialState ? {} : undefined 313 | 314 | if (plainPartialState && plainPartialState.hello) { 315 | // flow-disable-next-line 316 | preloadedState.hello = helloReducer(undefined, {}) 317 | .merge(Immutable.fromJS(plainPartialState.hello)) 318 | } 319 | 320 | return createStore(combineReducers({ hello: helloReducer }), 321 | preloadedState, applyMiddleware(thunkMiddleware)) 322 | } 323 | 324 | export default initStore 325 | ``` 326 | La seule chose qu'on fait ici, à part appeler `createStore` et appliquer un middleware, c'est de fusionner le pur objet JS qu'on a reçu du `controller` en state Redux par défaut contenant des objets immuables. 327 | 328 | - Éditez `src/server/index.js` de cette façon 329 | 330 | ```js 331 | // @flow 332 | 333 | import compression from 'compression' 334 | import express from 'express' 335 | 336 | import routing from './routing' 337 | import { WEB_PORT, STATIC_PATH } from '../shared/config' 338 | import { isProd } from '../shared/util' 339 | 340 | const app = express() 341 | 342 | app.use(compression()) 343 | app.use(STATIC_PATH, express.static('dist')) 344 | app.use(STATIC_PATH, express.static('public')) 345 | 346 | routing(app) 347 | 348 | app.listen(WEB_PORT, () => { 349 | // eslint-disable-next-line no-console 350 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' : 351 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`) 352 | }) 353 | ``` 354 | 355 | Rien de spécial ici, on appelle juste `routing(app)` au lieu d'implémenter le routage dans ce fichier. 356 | 357 | - Renommez `src/server/render-app.js` en `src/server/render-app.jsx` et éditez-le comme ceci : 358 | 359 | ```js 360 | // @flow 361 | 362 | import React from 'react' 363 | import ReactDOMServer from 'react-dom/server' 364 | import { Provider } from 'react-redux' 365 | import { StaticRouter } from 'react-router' 366 | 367 | import initStore from './init-store' 368 | import App from './../shared/app' 369 | import { APP_CONTAINER_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config' 370 | import { isProd } from '../shared/util' 371 | 372 | const renderApp = (location: string, plainPartialState: ?Object, routerContext: ?Object = {}) => { 373 | const store = initStore(plainPartialState) 374 | const appHtml = ReactDOMServer.renderToString( 375 | 376 | 377 | 378 | 379 | ) 380 | 381 | return ( 382 | ` 383 | 384 | 385 | FIX ME 386 | 387 | 388 | 389 |
${appHtml}
390 | 393 | 394 | 395 | ` 396 | ) 397 | } 398 | 399 | export default renderApp 400 | ``` 401 | 402 | C'est dans `ReactDOMServer.renderToString` que la magie a lieu. React va évaluer toute notre `App` `shared`, et retourner une pure string d'éléments HTML. `Provider` fonctionne de la même façon que sur le client, mais sur le serveur, on *emballe* notre app dans `StaticRouter` au lieu de `BrowserRouter`. Afin de passer le store Redux du au client, on le passe à `window.__PRELOADED_STATE__` qui est juste un nom de variable arbitraire. 403 | 404 | **Remarque**: Les objets immuables implémentent la méthode `toJSON()` ce qui veut dire que vous pouvez utiliser `JSON.stringify` pour les passer en strings JSON pures. 405 | 406 | - Éditez `src/client/index.jsx` pour utiliser ce state préchargé : 407 | 408 | ```js 409 | import Immutable from 'immutable' 410 | // [...] 411 | 412 | /* eslint-disable no-underscore-dangle */ 413 | const composeEnhancers = (isProd ? null : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose 414 | const preloadedState = window.__PRELOADED_STATE__ 415 | /* eslint-enable no-underscore-dangle */ 416 | 417 | const store = createStore(combineReducers( 418 | { hello: helloReducer }), 419 | { hello: Immutable.fromJS(preloadedState.hello) }, 420 | composeEnhancers(applyMiddleware(thunkMiddleware))) 421 | ``` 422 | 423 | Ici, on alimente notre store côté client avec le `preloadedState` qu'on a reçu du serveur. 424 | 425 | :checkered_flag: Maintenant, vous pouvez lancer `yarn start`, `yarn dev:wds` et naviguer entre les pages. Rafraîchissez la page sur `/hello`, `/hello-async`, et `/404` (ou n'importe quelle autre URI). Tout devrait fonctionner correctement. Remarquez comme `message` et `messageAsync` varient selon si vous êtes allés sur cette page depuis le client ou si elle vient du rendu côté serveur. 426 | 427 | ### React Helmet 428 | 429 | > :question: **[React Helmet](https://github.com/nfl/react-helmet)**: Une bibliothèque pour injecter du contenu dans le `head` d'une app React, à la fois sur le client et le serveur. 430 | 431 | Nous avons fait exprès de vous faire écrire `FIX ME` dans le titre pour souligner le fait que même si nous faisons du server-side rendering, on ne remplit pas la balise `title` correctement (ni aucune autre balise dans le `head` qui varie selon la page sur laquelle vous vous trouvez). 432 | 433 | - Lancez `yarn add react-helmet` 434 | 435 | - Éditez `src/server/render-app.jsx` comme ceci : 436 | 437 | ```js 438 | import Helmet from 'react-helmet' 439 | // [...] 440 | const renderApp = (/* [...] */) => { 441 | // [...] 442 | const appHtml = ReactDOMServer.renderToString(/* [...] */) 443 | const head = Helmet.rewind() 444 | 445 | return ( 446 | ` 447 | 448 | 449 | ${head.title} 450 | ${head.meta} 451 | 452 | 453 | [...] 454 | ` 455 | ) 456 | } 457 | ``` 458 | 459 | React Helmet utilise `rewind` de [react-side-effect](https://github.com/gaearon/react-side-effect) pour extraire des données du rendu de notre app, qui contiendra bientôt quelques composants `` components. Ces composants `` sont là où nous initialisons `title` et d'autres détails de `head` pour chaque page. Noteez que `Helmet.rewind()` *doit* arriver après `ReactDOMServer.renderToString()`. 460 | 461 | - Éditez `src/shared/app.jsx` comme ceci : 462 | 463 | ```js 464 | import Helmet from 'react-helmet' 465 | // [...] 466 | const App = () => 467 |
468 | 469 |