├── .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 | [](https://travis-ci.org/verekia/js-stack-from-scratch)
4 | [](https://github.com/verekia/js-stack-from-scratch/releases)
5 | [](https://david-dm.org/verekia/js-stack-boilerplate)
6 | [](https://david-dm.org/verekia/js-stack-boilerplate?type=dev)
7 | [](https://gitter.im/js-stack-from-scratch/)
8 |
9 | [](https://facebook.github.io/react/)
10 | [](http://redux.js.org/)
11 | [](https://github.com/ReactTraining/react-router)
12 | [](https://flowtype.org/)
13 | [](http://eslint.org/)
14 | [](https://facebook.github.io/jest/)
15 | [](https://yarnpkg.com/)
16 | [](https://webpack.github.io/)
17 | [](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 = () =>
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 |
580 |
581 | export default NotFoundPage
582 | ```
583 |
584 | En fait, le composant `` ne fait rien apparaître du tout, il injecte juste du contenu dans le `head` de notre document et montre les mêmes données au serveur.
585 |
586 | :checkered_flag: Lancez `yarn start`, `yarn dev:wds` et naviguez entre les différentes pages. Le titre de votre onglet devrait changer selon la page sur laquelle vous vous trouvez et devrait rester le même quand vous rafraîchissez la page. Affichez le code source de la page dans votre navigateur pour voir comment React Helmet initialise les balises `title` et `meta` tags même pour du rendu côté serveur.
587 |
588 | Prochaine section: [07 - Socket.IO](07-socket-io.md#readme)
589 |
590 | Retour à la [section précédente](05-redux-immutable-fetch.md#readme) ou au [sommaire](https://github.com/naomihauret/js-stack-from-scratch#table-of-contents).
591 |
--------------------------------------------------------------------------------
/tutorial/07-socket-io.md:
--------------------------------------------------------------------------------
1 | # 07 - Socket.IO
2 |
3 | Le code pour ce chapitre est disponible [ici](https://github.com/verekia/js-stack-walkthrough/tree/master/07-socket-io).
4 |
5 | > :bulb: **[Socket.IO](https://github.com/socketio/socket.io)** est une bibliothèque qui nous permet de travailler facilement avec les Websockets. Socket.IO fournit une API pratique et des fallbacks (solutions de rechange :fr: ) pour les navigateurs qui ne supportent pas les Websockets.
6 |
7 | Dans ce chapitre, nous allons créer un échange de message basique entre le client et le serveur. Afin de ne pas ajouter plus de page et de composant (ce qui ne serait pas en lien avec la fonctionnalité qui nous intéresse dans ce chapitre), cet échange aura lieu dans la console du navigateur. Pas de truc en rapport avec l'UI (User Interface - Interface utilisateur :fr: ) dans ce chapitre :wink: !
8 |
9 |
10 | - Lancez `yarn add socket.io socket.io-client`
11 |
12 | ## Côté serveur
13 |
14 | - Éditez votre fichier `src/server/index.js` de la manière suivante :
15 |
16 | ```js
17 | // @flow
18 |
19 | import compression from 'compression'
20 | import express from 'express'
21 | import { Server } from 'http'
22 | import socketIO from 'socket.io'
23 |
24 | import routing from './routing'
25 | import { WEB_PORT, STATIC_PATH } from '../shared/config'
26 | import { isProd } from '../shared/util'
27 | import setUpSocket from './socket'
28 |
29 | const app = express()
30 | // flow-disable-next-line
31 | const http = Server(app)
32 | const io = socketIO(http)
33 | setUpSocket(io)
34 |
35 | app.use(compression())
36 | app.use(STATIC_PATH, express.static('dist'))
37 | app.use(STATIC_PATH, express.static('public'))
38 |
39 | routing(app)
40 |
41 | http.listen(WEB_PORT, () => {
42 | // eslint-disable-next-line no-console
43 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' :
44 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`)
45 | })
46 | ```
47 |
48 | Notez que pour que Socket.IO fonctionne, vous avez besoin d'utiliser `Server` de `http` pour écouter (`listen`) en cours et pas du `app` de Express. Heureusement, ça ne change pas grand chose à notre code. Tous les détails pour Websocket sont externalisés dans un fichier différent appelé avec `setUpSocket`.
49 |
50 | - Ajoutez les constantes suivantes à votre fichier `src/shared/config.js` :
51 |
52 | ```js
53 | export const IO_CONNECT = 'connect'
54 | export const IO_DISCONNECT = 'disconnect'
55 | export const IO_CLIENT_HELLO = 'IO_CLIENT_HELLO'
56 | export const IO_CLIENT_JOIN_ROOM = 'IO_CLIENT_JOIN_ROOM'
57 | export const IO_SERVER_HELLO = 'IO_SERVER_HELLO'
58 | ```
59 |
60 | Il y a le *type de messages* que votre client et votre serveur vont échanger. Nous vous suggérons de les préfixer avec soit `IO_CLIENT`, soit `IO_SERVER` afin de savoir clairement *qui* envoie le message. Sinon, les choses pourraient devenir assez confuses une fois que vous aurez beaucoup de messages.
61 |
62 | Comme vous pouvez le voir, nous avons un `IO_CLIENT_JOIN_ROOM`, ('client IO vient de rejoindre la salle/room/channel' :fr:) puisque pour le bien de la démo, nos clients rejoindrons une room (comme une chatroom). Les rooms sont utiles pour diffuser des messages à des groupes d'utilisateurs spécifiques.
63 |
64 | - Créez un fichier `src/server/socket.js` contenant :
65 |
66 | ```js
67 | // @flow
68 |
69 | import {
70 | IO_CONNECT,
71 | IO_DISCONNECT,
72 | IO_CLIENT_JOIN_ROOM,
73 | IO_CLIENT_HELLO,
74 | IO_SERVER_HELLO,
75 | } from '../shared/config'
76 |
77 | /* eslint-disable no-console */
78 | const setUpSocket = (io: Object) => {
79 | io.on(IO_CONNECT, (socket) => {
80 | console.log('[socket.io] A client connected.')
81 |
82 | socket.on(IO_CLIENT_JOIN_ROOM, (room) => {
83 | socket.join(room)
84 | console.log(`[socket.io] A client joined room ${room}.`)
85 |
86 | io.emit(IO_SERVER_HELLO, 'Hello everyone!')
87 | io.to(room).emit(IO_SERVER_HELLO, `Hello clients of room ${room}!`)
88 | socket.emit(IO_SERVER_HELLO, 'Hello you!')
89 | })
90 |
91 | socket.on(IO_CLIENT_HELLO, (clientMessage) => {
92 | console.log(`[socket.io] Client: ${clientMessage}`)
93 | })
94 |
95 | socket.on(IO_DISCONNECT, () => {
96 | console.log('[socket.io] A client disconnected.')
97 | })
98 | })
99 | }
100 | /* eslint-enable no-console */
101 |
102 | export default setUpSocket
103 | ```
104 |
105 | Okay, donc dans ce fichier, nous implémentons *comment notre serveur devrait réagir quand des clients se connectent et lui envoient des messages* :
106 |
107 | - Quand le client se connecte, on l'affiche dans la console du serveur et gagnons accès à l'objet `socket`, que nous pouvons utiliser pour répondre au client.
108 | - Quand un client envoie `IO_CLIENT_JOIN_ROOM`, on le fait rejoindre la `room` qu'il souhaite. Une fois qu'il l'a rejoint, on envoie 3 messages de démo : 1 à chaque utilisateur, 1 à chaque utilisateurs de cette room et enfin, 1 message à ce client seulement.
109 | - Quand le client envoie `IO_CLIENT_HELLO`, on affiche son message dans la console du serveur.
110 | - Quand le client se déconnecte, on l'affiche aussi.
111 |
112 | ## Côté client
113 |
114 | Côté client, nous allons faire quelque chose de très similaire :
115 |
116 | - Éditez votre fichier `src/client/index.jsx` comme ceci :
117 |
118 | ```js
119 | // [...]
120 | import setUpSocket from './socket'
121 |
122 | // [À la toute fin du fichier]
123 | setUpSocket(store)
124 | ```
125 | Comme vous pouvez le voir, on passe un store Redux à `setUpSocket`. De cette façon, dès qu'un message Websocket venant serveur et qui altère le state Redux du client, on peut `dispatch` (expédier/envoyer/distribuer :fr:) les actions. Mais on ne va pas `dispatch` n'importe quoi dans cet exemple.
126 |
127 | - Créez un fichier `src/client/socket.js` contenant :
128 |
129 | ```js
130 | // @flow
131 |
132 | import socketIOClient from 'socket.io-client'
133 |
134 | import {
135 | IO_CONNECT,
136 | IO_DISCONNECT,
137 | IO_CLIENT_HELLO,
138 | IO_CLIENT_JOIN_ROOM,
139 | IO_SERVER_HELLO,
140 | } from '../shared/config'
141 |
142 | const socket = socketIOClient(window.location.host)
143 |
144 | /* eslint-disable no-console */
145 | // eslint-disable-next-line no-unused-vars
146 | const setUpSocket = (store: Object) => {
147 | socket.on(IO_CONNECT, () => {
148 | console.log('[socket.io] Connected.')
149 | socket.emit(IO_CLIENT_JOIN_ROOM, 'hello-1234')
150 | socket.emit(IO_CLIENT_HELLO, 'Hello!')
151 | })
152 |
153 | socket.on(IO_SERVER_HELLO, (serverMessage) => {
154 | console.log(`[socket.io] Server: ${serverMessage}`)
155 | })
156 |
157 | socket.on(IO_DISCONNECT, () => {
158 | console.log('[socket.io] Disconnected.')
159 | })
160 | }
161 | /* eslint-enable no-console */
162 |
163 | export default setUpSocket
164 | ```
165 | Ce qui se passe ici ne devrait pas trop vous surprendre si vous avez bien compris ce que nous avons fait sur le serveur :
166 |
167 | - Dès que le client est connecté, on l'affiche dans la console du navigateur et rejoignons la room `hello-1234` avec un message `IO_CLIENT_JOIN_ROOM`.
168 | - On envoie un `Hello!` avec un message `IO_CLIENT_HELLO`.
169 | - Si le serveur nous envoie un message `IO_SERVER_HELLO`, on l'affiche dans la console du navigateur.
170 | - On affiche aussi n'importe quelle déconnexion.
171 |
172 | :checkered_flag: Lancez `yarn start` et `yarn dev:wds` Rendez-vous sur `http://localhost:8000`. Ouvrez la console de votre navigateur et regardez le terminal où tourne votre serveur Express. Vous devriez voir la communication Websocket entre votre serveur et votre client :+1: !
173 |
174 | Prochaine section : [08 - Bootstrap, JSS](08-bootstrap-jss.md#readme)
175 |
176 | Retour à la [section précédente](06-react-router-ssr-helmet.md#readme) ou au [sommaire](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
177 |
--------------------------------------------------------------------------------
/tutorial/08-bootstrap-jss.md:
--------------------------------------------------------------------------------
1 | # 08 - Bootstrap et JSS
2 |
3 | Le code de ce chapitre est disponible dans la branche [`master-no-services`](https://github.com/verekia/js-stack-boilerplate/tree/master-no-services) du [repo JS-Stack-Boilerplate](https://github.com/verekia/js-stack-boilerplate).
4 |
5 | Bien ! Il est temps de faire une beauté à notre app toute moche :lipstick:. Nous allons utiliser Twitter Bootstrap pour lui appliquer des styles basiques. On ajoutera ensuite une bibliothèque CSS-dans-le-JS pour ajouter du style personnalisé.
6 |
7 | ## Twitter Bootstrap
8 |
9 | > :bulb: **[Twitter Bootstrap](http://getbootstrap.com/)** est une bibliothèque de composants d'interface utilisateur.
10 |
11 | Il y a 2 options pour intégrer Bootstrap dans une app React. Les 2 ont leurs avantages et leurs inconvénients :
12 |
13 | - la première consiste à utiliser la distribution officielle, **qui utilise jQuery et Tether** pour le comportement de ses composants
14 | - la deuxième repose sur une bibliothèque qui ré-implémente tous les composants Boostrap en React, comme [React-Bootstrap](https://react-bootstrap.github.io/) or [Reactstrap](https://reactstrap.github.io/).
15 |
16 | Les bibliothèques tierces fournissent des composants React très pratique qui réduisent drastiquement le code trop long des composants HTML officiels, et s'intègre vraiment bien avec votre base React. Cela étant dit, nous devons admettre que nous sommes assez réticent à utiliser ces bibliothèques, car elles sont toujours *derrière* par rapport aux distributions officiels (et parfois, potentiellement très loin derrière). Aussi, elles ne marchent pas avec les thèmes Bootstrap qui implémentent leur propre JS. C'est un gros pas en arrière quand on sait que l'une des forces principales de Bootstrap est son énorme communauté de designers capable de créer des thèmes magnifiques.
17 |
18 | Pour cette raison, nous allons plutôt intégrer la distribution officielle, avec jQuery et Tether. Un des problèmes de cette approche est bien sûr, la taille de notre bundle. Pour votre information, un bundle fait environ 200Ko (zippé), jQuery, Tether, et le JS de Bootstrap' inclus. Nous pensons que c'est assez raisonnable, mais si c'est trop pour vous, vous devriez considérez le fait d'utiliser une autre option pour Bootstrap, voire de ne pas utiliser Bootstrap du tout.
19 |
20 | ### Le CSS de Bootstrap
21 |
22 | - Supprimez le fichier `public/css/style.css`
23 |
24 | - Lancez `yarn add bootstrap@4.0.0-alpha.6`
25 |
26 | - Copiez le fichier `bootstrap.min.css` et `bootstrap.min.css.map` de `node_modules/bootstrap/dist/css` dans votre dossier `public/css`.
27 |
28 | - Éditez `src/server/render-app.jsx` comme ceci :
29 |
30 | ```html
31 |
32 | ```
33 |
34 | ### Le JS de Bootstrap avec jQuery et Tether
35 |
36 | Maintenant que le style de Bootstrap est chargé sur notre page, nous avons besoin du comportement Javascript pour les composants.
37 |
38 | - Lancez `yarn add jquery tether`
39 |
40 | - Éditez votre fichier `src/client/index.jsx` comme ceci :
41 |
42 | ```js
43 | import $ from 'jquery'
44 | import Tether from 'tether'
45 |
46 | // [Juste après tous vos imports]
47 |
48 | window.jQuery = $
49 | window.Tether = Tether
50 | require('bootstrap')
51 | ```
52 | Ça chargera le code Javascript de Bootstrap.
53 |
54 | ### Les composants Bootstrap
55 |
56 | Bien, c'est l'heure de copier/coller un paquet de fichiers ! :smile:
57 |
58 | - Éditez votre fichier `src/shared/component/page/hello-async.jsx` comme ceci :
59 |
60 | ```js
61 | // @flow
62 |
63 | import React from 'react'
64 | import Helmet from 'react-helmet'
65 |
66 | import MessageAsync from '../../container/message-async'
67 | import HelloAsyncButton from '../../container/hello-async-button'
68 |
69 | const title = 'Async Hello Page'
70 |
71 | const HelloAsyncPage = () =>
72 |
264 | ```
265 |
266 | Voici un exemple de *style inline React* (style en ligne React :fr:).
267 |
268 | Cela se traduira en `
` dans votre DOM. Nous avons besoin de ce code pour pousser le contenu sous la barre de navigation, mais ce n'est pas ce qui est important ici. [Les styles inline React](https://speakerdeck.com/vjeux/react-css-in-js) sont un super moyen d'isoler le style de votre composant du CSS global, mais tout cela a un coût: vous ne pouvez pas utiliser les fonctionnalités CSS natives comme `:hover`, les media queries, les animations, ou `font-face`. C'est [l'une des raisons](https://github.com/cssinjs/jss/blob/master/docs/benefits.md#compared-to-inline-styles) pour laquelle nous allons intégrer une bibliothèque CSS-dans-le-JS, JSS, qu'on verra plus tard dans ce chapitre.
269 |
270 | - Éditez votre fichier `src/shared/component/nav.jsx` comme ceci :
271 |
272 | ```js
273 | // @flow
274 |
275 | import $ from 'jquery'
276 | import React from 'react'
277 | import { Link, NavLink } from 'react-router-dom'
278 | import { APP_NAME } from '../config'
279 | import {
280 | HOME_PAGE_ROUTE,
281 | HELLO_PAGE_ROUTE,
282 | HELLO_ASYNC_PAGE_ROUTE,
283 | NOT_FOUND_DEMO_PAGE_ROUTE,
284 | } from '../routes'
285 |
286 | const handleNavLinkClick = () => {
287 | $('body').scrollTop(0)
288 | $('.js-navbar-collapse').collapse('hide')
289 | }
290 |
291 | const Nav = () =>
292 |
312 |
313 | export default Nav
314 | ```
315 | Il y a un truc nouveau ici, `handleNavLinkClick`. Un problème qu'on a recontré en utilisant la `navbar` de Bootstrap dans une SPA (SPA = Single Page Application = Application à Page Unique :fr:), c'est que cliquer sur un lien sur mobile ne cache pas le menu, et ne scroll pas en haut de page. Voilà une super opportunité pour vous montrer un exemple de comment intégrer un peu de jQuery / du code spécifique à Bootstrap dans votre app :
316 |
317 | ```js
318 | import $ from 'jquery'
319 | // [...]
320 |
321 | const handleNavLinkClick = () => {
322 | $('body').scrollTop(0)
323 | $('.js-navbar-collapse').collapse('hide')
324 | }
325 |
326 |
327 | ```
328 |
329 | **Remarque**: Ici on a retiré tous les attributs liés à l'accessibilité (comme les `aria`) afin de rendre le code plus lisible *dans le contexte de ce tutoriel*. **Vous devez absolument les remettre**. Référez vous à la documentation et aux exemples de code pour voir comment les utiliser.
330 |
331 | :checkered_flag: Votre application devrait maintenant être totalement stylisée avec Bootstrap :sparkles: .
332 |
333 | ## L'état actuel de CSS
334 |
335 | En 2016, la stack moderne typique de JavaScript s'est intallée. Les différentes bibliothèques et outils que ce tutoriel vous a fait installer sont pratiquement les *tous derniers standards de l'industrie* (*tousse - même si tout va être totalement obsolète d'ici un an - tousse* ). Oui, c'est une stack assez complexe à mettre en place, mais au moins, la plupart des développeurs front-end sont d'accord pour dire que c'est vers React-Redux-Webpack que l'on doit se diriger. Maintenant, en ce qui concerne le CSS, on a d'assez mauvaises nouvelles. Rien n'est défini, il n'y a pas de façon standard de faire les choses.
336 |
337 | SASS, BEM, SMACSS, SUIT, Bass CSS, React Inline Styles, LESS, Styled Components, CSSX, JSS, Radium, Web Components, CSS Modules, OOCSS, Tachyons, Stylus, Atomic CSS, PostCSS, Aphrodite, React Native for Web, [MaintainableCSS](https://github.com/naomiHauret/maintainablecss.com), [Trowel](https://github.com/Trowel/Trowel) et beaucoup d'autres qu'on ne cite pas, sont toutes des approches différentes ou des outils sur comment faire le travail. Et ils le font tous très bien ! Et c'est ça le problème : il n'y a pas de gagnant qui ressort du lot, c'est un bazar énorme.
338 |
339 | Les cool kids React ont tendance à favoriser les approches CSS-in-JS, React inline styles et les CSS modules, étant donné qu'ils s'intégrent très facilement avec React en résolvant plusieurs [problèmes](https://speakerdeck.com/vjeux/react-css-in-js) avec lesquelles les approches CSS normales ont du mal.
340 |
341 | Les CSS Modules fonctionnent bien, mais ils ne tirent pas avantages de la puissance de JavaScript et de toutes ses fonctionnalités sur le CSS. Ils fournissent juste l'encapsulation, ce qui est correct, mais à notre avis, les React inline styles et CSS-in-JS emmènent CSS à un tout autre niveau. Notre suggestion est d'utiliser les React inline styles pour les styles communs (c'est aussi ce que vous pouvez utiliser dans React Native), et utiliser une bibliothèque CSS-in-JS pour les trucs comme `:hover` et les media queries.
342 |
343 | Il y a des [tonnes de bibliothèques CSS-in-JS](https://github.com/MicheleBertoli/css-in-js). JSS en est une très complète et [performante](https://github.com/cssinjs/jss/blob/master/docs/performance.md).
344 |
345 | ## JSS
346 |
347 | > :bulb: **[JSS](http://cssinjs.org/)** bibliothèque CSS-in-JS qui permet d'écrire du style en Javascript et de l'injecter dans votre app.
348 |
349 | Maintenant qu'on a un template de base avec Bootstrap, écrivons un peu de CSS personnalisé. On a mentionné plus tôt que les React inline styles ne pouvait pas gérer les `:hover` et les media queries. Nous allons donc vous en montrer un exemple simple sur la homepage en utilisant JSS. JSS peut être utilisé via `react-jss`, une bibliothèque facile à utiliser avec les composants React.
350 |
351 | - Lancez `yarn add react-jss`
352 |
353 | Ajoutez les lignes suivantes à votre fichier `.flowconfig`, étant donné que Flow a un [problème](https://github.com/cssinjs/jss/issues/411) avec JSS:
354 |
355 | ```flowconfig
356 | [ignore]
357 | .*/node_modules/jss/.*
358 | ```
359 |
360 | ### Côté serveur
361 |
362 | JSS peut rendre du style sur le serveur pour le rendu initial de la page.
363 |
364 | - Ajoutez les constantes suivantes à votre fichier `src/shared/config.js` :
365 |
366 | ```js
367 | export const JSS_SSR_CLASS = 'jss-ssr'
368 | export const JSS_SSR_SELECTOR = `.${JSS_SSR_CLASS}`
369 | ```
370 |
371 | - Éditez votre fichier `src/server/render-app.jsx` comme ceci :
372 |
373 | ```js
374 | import { SheetsRegistry, SheetsRegistryProvider } from 'react-jss'
375 | // [...]
376 | import { APP_CONTAINER_CLASS, JSS_SSR_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config'
377 | // [...]
378 | const renderApp = (location: string, plainPartialState: ?Object, routerContext: ?Object = {}) => {
379 | const store = initStore(plainPartialState)
380 | const sheets = new SheetsRegistry()
381 | const appHtml = ReactDOMServer.renderToString(
382 |
383 |
384 |
385 |
386 |
387 |
388 | )
389 | // [...]
390 |
391 |
392 | // [...]
393 | ```
394 |
395 | ## Côté client
396 |
397 | La première chose que le client doit faire après avoir affiché l'app côté client, c'est de se débarasser de tout le style généré côté serveur par JSS.
398 |
399 | - Ajoutez les lignes suivantes au fichier `src/client/index.jsx` après les appels `ReactDOM.render` (par exemple, avant `setUpSocket(store)`) :
400 |
401 | ```js
402 | import { APP_CONTAINER_SELECTOR, JSS_SSR_SELECTOR } from '../shared/config'
403 | // [...]
404 |
405 | const jssServerSide = document.querySelector(JSS_SSR_SELECTOR)
406 | // flow-disable-next-line
407 | jssServerSide.parentNode.removeChild(jssServerSide)
408 |
409 | setUpSocket(store)
410 | ```
411 |
412 | Éditez le fichier `src/shared/component/page/home.jsx` comme ceci :
413 |
414 | ```js
415 | import injectSheet from 'react-jss'
416 | // [...]
417 | const styles = {
418 | hoverMe: {
419 | '&:hover': {
420 | color: 'red',
421 | },
422 | },
423 | '@media (max-width: 800px)': {
424 | resizeMe: {
425 | color: 'red',
426 | },
427 | },
428 | specialButton: {
429 | composes: ['btn', 'btn-primary'],
430 | backgroundColor: 'limegreen',
431 | },
432 | }
433 |
434 | const HomePage = ({ classes }: { classes: Object }) =>
435 | // [...]
436 |
437 |
JSS
438 |
Hover me.
439 |
Resize the window.
440 |
441 |
442 | // [...]
443 |
444 | export default injectSheet(styles)(HomePage)
445 | ```
446 | A la différence des React inline styles, JSS utilise des classes. Vous passez vos styles à `injectSheet` et les classes CSS atterrissent dans les props de votre composant.
447 |
448 | :checkered_flag: Lancez `yarn start` et `yarn dev:wds`. Ouvrez la homepage. Affichez le code source de la page (pas dans l'inspecteur) pour voir que les styles JSS sont présents dans le DOM au rendu initial dans l'élément `