├── .github
└── ISSUE_TEMPLATE
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── Definitions.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
├── 01-node-yarn-package-json_ru.md
├── 02-babel-es6-eslint-flow-jest-husky.md
├── 03-express-nodemon-pm2.md
├── 03-express-nodemon-pm2_ru.md
├── 04-webpack-react-hmr.md
├── 04-webpack-react-hmr_ru.md
├── 05-redux-immutable-fetch.md
├── 05-redux-immutable-fetch_ru.md
├── 06-react-router-ssr-helmet.md
├── 06-react-router-ssr-helmet_ru.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 |
--------------------------------------------------------------------------------
/Definitions.md:
--------------------------------------------------------------------------------
1 | Термины и определения
2 |
3 | package - пакет
4 | https://ru.wikipedia.org/wiki/Package_(Java)
5 |
6 | class - класс
7 |
8 | Type Checking - Типизация
9 |
10 | OOP - объектно-ориентированное программирование
11 |
12 | JavaScript
13 |
14 | template strings - Шаблонные строки
15 | https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/template_strings
16 |
17 | arrow functions - стрелочные функции
18 | https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Functions/Arrow_functions
19 |
20 | CommonJS
21 |
22 | Gulp - сборщик проектов
23 | https://habrahabr.ru/post/208890/
24 |
25 | Task Runner - менеджер задач.
26 | (диспетчер запуска задач)
27 |
28 | Node - сервер Node
29 |
30 | plain JavaScript object - простой JavaScript объект
31 |
32 | Babel
33 |
34 | синтаксический сахар - syntactic sugar
35 |
36 | lint - статический анализ, контроль качества кода
37 | https://ru.wikipedia.org/wiki/Lint
38 |
39 | linter - статический анализатор кода.
40 |
41 | Gulpfile
42 |
43 | linting errors
44 |
45 | bundle - сборка
46 |
47 | polyfill
48 | https://remysharp.com/2010/10/08/what-is-a-polyfill
49 |
50 | TRANSPILING - транспилияция (трансляция)
51 | https://www.stevefenton.co.uk/2012/11/compiling-vs-transpiling/
52 |
53 | tutorial - руководство
54 |
55 | entry point file - файл, указывающий на начальную точку сборки
56 |
57 | back-end - серверная часть
58 |
59 | trailing commas - завершающая запятая (/пробел)
60 | http://www.multitran.ru/c/m.exe?t=6766895_1_2&s1=trailing%20space
61 |
62 | repo/repository - репозиторий
63 |
64 | commit - фиксировать гл.
65 | commit - коммит сущ.
66 |
67 | reducer functions - reducer-функции
68 |
--------------------------------------------------------------------------------
/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
2 |
3 | [](https://travis-ci.org/verekia/js-stack-from-scratch) [](https://gitter.im/js-stack-from-scratch/Lobby)
4 |
5 | [](https://facebook.github.io/react/)
6 | [](http://redux.js.org/)
7 | [](https://github.com/ReactTraining/react-router)
8 | [](https://flowtype.org/)
9 | [](http://eslint.org/)
10 | [](https://facebook.github.io/jest/)
11 | [](https://yarnpkg.com/)
12 | [](https://webpack.github.io/)
13 | [](http://getbootstrap.com/)
14 |
15 | >Это русскоязычная версия руководства Джонатана Верекии ([@verekia](https://twitter.com/verekia)). Оригинальное руководство расположено [здесь](https://github.com/verekia/js-stack-from-scratch). **Начата [работа](https://github.com/UsulPro/js-stack-from-scratch/issues/8) по переводу второй части**. Первая версия находится [тут](https://github.com/UsulPro/js-stack-from-scratch-v1-rus)
16 |
17 | Добро пожаловать в мое современное руководство по стеку технологий JavaScript: **Стек технологий JavaScript с нуля**.
18 |
19 | > 🎉 **Это вторая версия руководства. По сравнению с предыдущм релизом 2016г произведены значительные изменения. См. [Change Log](/CHANGELOG.md)!**
20 |
21 |
22 | Это практико-ориентированное пособие по применению JavaScript технологий. Вам потребуются общие знания по программированию и основы JavaScript. Это пособие **нацелено на интеграцию необходимых инструментов** и предоставляет **максимально простые примеры** для каждого инструмента. Вы можете рассматривать данный документ, как *возможность создать свой собственный шаблонный проект с нуля*. Поскольку целью этого руководства является сборка различных инструментов, я не буду вдаваться в детали по каждому из них. Если вы хотите получить по ним более глубокие знания, изучайте их документацию или другие руководства.
23 |
24 | Конечно, вам не нужны все эти технологии, если вы делаете простую веб страницу с парой JS функций (комбинации Browserify / Webpack + Babel + jQuery достаточно, чтобы написать ES6 код в нескольких файлах), но если вы собираетесь создать масштабируемое веб приложение, и вам нужно все правильно настроить, то это руководство вам отлично подходит.
25 |
26 | В большой части технологий, описываемых здесь, используется React. Если вы только начинаете использовать React и просто хотите изучить его, то [create-react-app](https://github.com/facebookincubator/create-react-app) поможет вам и кратко ознакомит с инфраструктурой React на основе предустановленной конфигурации. Я бы, например, порекомендовал такой подход для тех, кому нужно влиться в команду, использующую React, и на чем-то потренироваться, чтобы подтянуть свои знания. В этом руководстве мы не будем пользоваться предустановленными конфигурациями, поскольку я хочу, чтобы вы полностью понимали все, что происходит "под капотом".
27 |
28 | В каждой части руководства имеются примеры кода, и вы можете запускать их через `yarn && yarn start`. Однако я рекомендую писать все с нуля самостоятельно, следуя **пошаговым инструкциям**.
29 |
30 | Итоговый код данного руководства доступен в отдельном репозитории: [JS-Stack-Boilerplate repository](https://github.com/verekia/js-stack-boilerplate). Он работает под Linux, macOS, и Windows.
31 |
32 | ## Содержание
33 |
34 | [01 - Node, Yarn, `package.json`](/tutorial/01-node-yarn-package-json_ru.md)
35 |
36 | [02 - Babel, ES6, ESLint, Flow, Jest, Husky](/tutorial/02-babel-es6-eslint-flow-jest-husky.md) 
37 |
38 | [03 - Express, Nodemon, PM2](/tutorial/03-express-nodemon-pm2_ru.md)
39 |
40 | [04 - Webpack, React, HMR](/tutorial/04-webpack-react-hmr.md)
41 |
42 | [05 - Redux, Immutable, Fetch](/tutorial/05-redux-immutable-fetch_ru.md)
43 |
44 | [06 - React Router, Server-Side Rendering, Helmet](/tutorial/06-react-router-ssr-helmet_ru.md)
45 |
46 | [07 - Socket.IO](/tutorial/07-socket-io.md) 
47 |
48 | [08 - Bootstrap, JSS](/tutorial/08-bootstrap-jss.md) 
49 |
50 | [09 - Travis, Coveralls, Heroku](/tutorial/09-travis-coveralls-heroku.md) 
51 |
52 | ## Далее планируется
53 |
54 | Настройка вашего редактора (Atom и другие), MongoDB, Прогрессивное веб приложение (Progressive Web App).
55 |
56 | ## Переводы на другие языки
57 |
58 | Если вы хотите добавить перевод на другой язык, пожалуйста читайте [рекомендации по переводу](/how-to-translate.md) чтобы начать!
59 |
60 | ### Версия 2
61 |
62 | - [Русский _в процессе превода_](https://github.com/UsulPro/js-stack-from-scratch) by [React Theming](https://github.com/sm-react/react-theming)
63 |
64 | ### Версия 1
65 |
66 | - [中文](https://github.com/pd4d10/js-stack-from-scratch) by [@pd4d10](http://github.com/pd4d10)
67 | - [Italiano](https://github.com/fbertone/js-stack-from-scratch) by [Fabrizio Bertone](https://github.com/fbertone)
68 | - [日本語](https://github.com/takahashim/js-stack-from-scratch) by [@takahashim](https://github.com/takahashim)
69 | - [Русский](https://github.com/UsulPro/js-stack-from-scratch-v1-rus) by [React Theming](https://github.com/sm-react/react-theming)
70 | - [ไทย](https://github.com/MicroBenz/js-stack-from-scratch) by [MicroBenz](https://github.com/MicroBenz)
71 |
72 | ## Сведения
73 |
74 | Создано [@verekia](https://twitter.com/verekia) – [verekia.com](http://verekia.com/).
75 |
76 | Переведено [@usulpro](https://github.com/UsulPro) - [react-theming](https://github.com/sm-react/react-theming)
77 |
78 | Лицензия: MIT
79 |
--------------------------------------------------------------------------------
/how-to-translate.md:
--------------------------------------------------------------------------------
1 | # How to translate this tutorial
2 |
3 | Thank you for your interest in translating my tutorial! Here are a few recommendations to get started.
4 |
5 | This tutorial is in constant evolution to provide the best learning experience to readers. Both the code and `README.md` files will change over time. It is great if you do a one-shot translation that won't evolve, but it would be even better if you could try to keep up with the original English version as it changes!
6 |
7 | Here is what I think is a good workflow:
8 |
9 | - Check if there is already an [ongoing translation](https://github.com/verekia/js-stack-from-scratch/issues/147) for your language. If that's the case, get in touch with the folks who opened it and consider collaborating. All maintainers will be mentioned on the English repo, so team work is encouraged! You can open issues on their translation fork project to offer your help on certain chapters for instance.
10 |
11 | - Join the [Translations Gitter room](https://gitter.im/js-stack-from-scratch/Translations) if you're feeling chatty.
12 |
13 | - Fork the main [English repository](https://github.com/verekia/js-stack-from-scratch).
14 |
15 | - Post in [this issue](https://github.com/verekia/js-stack-from-scratch/issues/147) the language and URL of your forked repo.
16 |
17 | - Translate the `README.md` files.
18 |
19 | - Add a note somewhere explaining on the main `README.md` that this is a translation, with a link to the English repository. If you don't plan to make the translation evolve over time, you can maybe add a little note saying to refer to the English one for an up-to-date version of the tutorial. I'll leave that up to your preference.
20 |
21 | - Submit a Pull Request to the English repo to add a link to your forked repository under the Translations section of the main `README.md`. It could look like this:
22 |
23 | ```md
24 | ## Translations
25 |
26 | - [Language](http://github.com/yourprofile/your-fork) by [You](http://yourwebsite.com)
27 | or
28 | - [Language](http://github.com/yourprofile/your-fork) by [@You](http://twitter.com/yourprofile)
29 | or
30 | - [Language](http://github.com/yourprofile/your-fork) by [@You](http://github.com/yourprofile)
31 | ```
32 |
33 | Since I want to reward you for your good work as much as possible, you can put any link you like on your name (to your personal website, Twitter profile, or Github profile for instance).
34 |
35 | - After your original one-shot translation, if you want to update your repo with the latest change from the main English repo, [sync your fork](https://help.github.com/articles/syncing-a-fork/) with my repo. To make it easy to see what changed since your initial translation, you can use Github's feature to [compare commits](https://help.github.com/articles/comparing-commits-across-time/#comparing-commits). Set the **base** to the last commit from the English repo you used to translate, and compare it to **master**, like so:
36 |
37 |
38 | https://github.com/verekia/js-stack-from-scratch/compare/c65dfa65d02c21063d94f0955de90947ba5273ad...master
39 |
40 |
41 | That should give you a easy-to-read diff to see exactly what changed in `README.md` files since your translation!
42 |
--------------------------------------------------------------------------------
/img/bootstrap-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/bootstrap-padded-90.png
--------------------------------------------------------------------------------
/img/bootstrap-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/bootstrap-padded.png
--------------------------------------------------------------------------------
/img/chai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/chai.png
--------------------------------------------------------------------------------
/img/eslint-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/eslint-padded-90.png
--------------------------------------------------------------------------------
/img/eslint-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/eslint-padded.png
--------------------------------------------------------------------------------
/img/eslint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/eslint.png
--------------------------------------------------------------------------------
/img/flow-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/flow-padded-90.png
--------------------------------------------------------------------------------
/img/flow-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/flow-padded.png
--------------------------------------------------------------------------------
/img/flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/flow.png
--------------------------------------------------------------------------------
/img/jest-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/jest-padded-90.png
--------------------------------------------------------------------------------
/img/jest-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/jest-padded.png
--------------------------------------------------------------------------------
/img/jest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/jest.png
--------------------------------------------------------------------------------
/img/js-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/js-padded.png
--------------------------------------------------------------------------------
/img/js.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/js.png
--------------------------------------------------------------------------------
/img/mocha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/mocha.png
--------------------------------------------------------------------------------
/img/npm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/npm.png
--------------------------------------------------------------------------------
/img/pm2-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/pm2-padded-90.png
--------------------------------------------------------------------------------
/img/pm2-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/pm2-padded.png
--------------------------------------------------------------------------------
/img/pm2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/pm2.png
--------------------------------------------------------------------------------
/img/react-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/react-padded-90.png
--------------------------------------------------------------------------------
/img/react-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/react-padded.png
--------------------------------------------------------------------------------
/img/react-router-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/react-router-padded-90.png
--------------------------------------------------------------------------------
/img/react-router-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/react-router-padded.png
--------------------------------------------------------------------------------
/img/react-router.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/react-router.png
--------------------------------------------------------------------------------
/img/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/react.png
--------------------------------------------------------------------------------
/img/redux-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/redux-padded-90.png
--------------------------------------------------------------------------------
/img/redux-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/redux-padded.png
--------------------------------------------------------------------------------
/img/redux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/redux.png
--------------------------------------------------------------------------------
/img/webpack-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/webpack-padded-90.png
--------------------------------------------------------------------------------
/img/webpack-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/webpack-padded.png
--------------------------------------------------------------------------------
/img/webpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/webpack.png
--------------------------------------------------------------------------------
/img/yarn-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/yarn-padded-90.png
--------------------------------------------------------------------------------
/img/yarn-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/img/yarn-padded.png
--------------------------------------------------------------------------------
/img/yarn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usulpro/js-stack-from-scratch/849cce974544e658efd8abddc371b4f4d75d088e/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, and `package.json`
2 |
3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/01-node-yarn-package-json).
4 |
5 | In this section we will set up Node, Yarn, a basic `package.json` file, and try a package.
6 |
7 | ## Node
8 |
9 | > 💡 **[Node.js](https://nodejs.org/)** is a JavaScript runtime environment. It is mostly used for Back-End development, but also for general scripting. In the context of Front-End development, it can be used to perform a whole bunch of tasks like linting, testing, and assembling files.
10 |
11 | We will use Node for basically everything in this tutorial, so you're going to need it. Head to the [download page](https://nodejs.org/en/download/current/) for **macOS** or **Windows** binaries, or the [package manager installations page](https://nodejs.org/en/download/package-manager/) for Linux distributions.
12 |
13 | For instance, on **Ubuntu / Debian**, you would run the following commands to install 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 | You want any version of Node > 6.5.0.
21 |
22 | ## Node Version Management Tools
23 |
24 | If you need the flexibility to use multiple versions of Node, check out [NVM](https://github.com/creationix/nvm) or [tj/n](https://github.com/tj/n).
25 |
26 | ## NPM
27 |
28 | NPM is the default package manager for Node. It is automatically installed alongside with Node. Package managers are used to install and manage packages (modules of code that you or someone else wrote). We are going to use a lot of packages in this tutorial, but we'll use Yarn, another package manager.
29 |
30 | ## Yarn
31 |
32 | > 💡 **[Yarn](https://yarnpkg.com/)** is a Node.js package manager which is much faster than NPM, has offline support, and fetches dependencies [more predictably](https://yarnpkg.com/en/docs/yarn-lock).
33 |
34 | Since it [came out](https://code.facebook.com/posts/1840075619545360) in October 2016, it received a very quick adoption and may soon become the package manager of choice of the JavaScript community. If you want to stick to NPM you can simply replace all `yarn add` and `yarn add --dev` commands of this tutorial by `npm install --save` and `npm install --save-dev`.
35 |
36 | Install Yarn by following the [instructions](https://yarnpkg.com/en/docs/install) for your OS. I would recommend using the **Installation Script** from the *Alternatives* tab if you are on macOS or Unix, to [avoid](https://github.com/yarnpkg/yarn/issues/1505) relying on other package managers:
37 |
38 | ```sh
39 | curl -o- -L https://yarnpkg.com/install.sh | bash
40 | ```
41 |
42 | ## `package.json`
43 |
44 | > 💡 **[package.json](https://yarnpkg.com/en/docs/package-json)** is the file used to describe and configure your JavaScript project. It contains general information (your project name, version, contributors, license, etc), configuration options for tools you use, and even a section to run *tasks*.
45 |
46 | - Create a new folder to work in, and `cd` in it.
47 | - Run `yarn init` and answer the questions (`yarn init -y` to skip all questions), to generate a `package.json` file automatically.
48 |
49 | Here is the basic `package.json` I'll use in this tutorial:
50 |
51 | ```json
52 | {
53 | "name": "your-project",
54 | "version": "1.0.0",
55 | "license": "MIT"
56 | }
57 | ```
58 |
59 | ## Hello World
60 |
61 | - Create an `index.js` file containing `console.log('Hello world')`
62 |
63 | 🏁 Run `node .` in this folder (`index.js` is the default file Node looks for in a folder). It should print "Hello world".
64 |
65 | **Note**: See that 🏁 racing flag emoji? I will use it every time you reach a **checkpoint**. We are sometimes going to make a lot of changes in a row, and your code may not work until you reach the next checkpoint.
66 |
67 | ## `start` script
68 |
69 | Running `node .` to execute our program is a bit too low-level. We are going to use an NPM/Yarn script to trigger the execution of that code instead. That will give us a nice abstraction to be able to always use `yarn start`, even when our program gets more complicated.
70 |
71 | - In `package.json`, add a `scripts` object like so:
72 |
73 | ```json
74 | {
75 | "name": "your-project",
76 | "version": "1.0.0",
77 | "license": "MIT",
78 | "scripts": {
79 | "start": "node ."
80 | }
81 | }
82 | ```
83 |
84 | `start` is the name we give to the *task* that will run our program. We are going to create a lot of different tasks in this `scripts` object throughout this tutorial. `start` is typically the name given to the default task of an application. Some other standard task names are `stop` and `test`.
85 |
86 | `package.json` must be a valid JSON file, which means that you cannot have trailing commas. So be careful when editing manually your `package.json` file.
87 |
88 | 🏁 Run `yarn start`. It should print `Hello world`.
89 |
90 | ## Git and `.gitignore`
91 |
92 | - Initialize a Git repository with `git init`
93 |
94 | - Create a `.gitignore` file and add the following to it:
95 |
96 | ```gitignore
97 | .DS_Store
98 | /*.log
99 | ```
100 |
101 | `.DS_Store` files are auto-generated macOS files that you should never have in your repository.
102 |
103 | `npm-debug.log` and `yarn-error.log` are files that are created when your package manager encounters an error, we don't want them versioned in our repository.
104 |
105 | ## Installing and using a package
106 |
107 | In this section we will install and use a package. A "package" is simply a piece of code that someone else wrote, and that you can use in your own code. It can be anything. Here, we're going to try a package that helps you manipulate colors for instance.
108 |
109 | - Install the community-made package called `color` by running `yarn add color`
110 |
111 | Open `package.json` to see how Yarn automatically added `color` in `dependencies`.
112 |
113 | A `node_modules` folder has been created to store the package.
114 |
115 | - Add `node_modules/` to your `.gitignore`
116 |
117 | You will also notice that a `yarn.lock` file got generated by Yarn. You should commit this file to your repository, as it will ensure that everyone in your team uses the same version of your packages. If you're sticking to NPM instead of Yarn, the equivalent of this file is the *shrinkwrap*.
118 |
119 | - Write the following to your `index.js` file:
120 |
121 | ```js
122 | const color = require('color')
123 |
124 | const redHexa = color({ r: 255, g: 0, b: 0 }).hex()
125 |
126 | console.log(redHexa)
127 | ```
128 |
129 | 🏁 Run `yarn start`. It should print `#FF0000`.
130 |
131 | Congratulations, you installed and used a package!
132 |
133 | `color` is just used in this section to teach you how to use a simple package. We won't need it anymore, so you can uninstall it:
134 |
135 | - Run `yarn remove color`
136 |
137 | ## Two kinds of dependencies
138 |
139 | There are two kinds of package dependencies, `"dependencies"` and `"devDependencies"`:
140 |
141 | **Dependencies** are libraries you need for your application to function (React, Redux, Lodash, jQuery, etc). You install them with `yarn add [package]`.
142 |
143 | **Dev Dependencies** are libraries used during development or to build your application (Webpack, SASS, linters, testing frameworks, etc). You install those with `yarn add --dev [package]`.
144 |
145 | Next section: [02 - Babel, ES6, ESLint, Flow, Jest, Husky](02-babel-es6-eslint-flow-jest-husky.md#readme)
146 |
147 | Back to the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
148 |
--------------------------------------------------------------------------------
/tutorial/01-node-yarn-package-json_ru.md:
--------------------------------------------------------------------------------
1 | # 01 - Node, Yarn, и `package.json`
2 |
3 | Код для этой главы доступен [здесь](https://github.com/verekia/js-stack-walkthrough/tree/master/01-node-yarn-package-json).
4 |
5 | В этой части мы настроим Node, Yarn, простой файл `package.json` и протестируем пакет.
6 |
7 | ## Node
8 |
9 | > 💡 **[Node.js](https://nodejs.org/)** - среда исполнения JavaScript, в основном используется для Back-End разработки, но также и для общих целей. В контексте Front-End разработки может применяться для выполнения целого ряда задач, таких как линтинг (linting), тестирование и сборка файлов.
10 |
11 | Мы будем использовать Node буквально везде в этом руководстве, так что вам нужно будет ее установить. Зайдите на [страницу загрузки](https://nodejs.org/en/download/current/) для **macOS** или **Windows** дистрибутивов, или на [страницу установки пакетного менеджера](https://nodejs.org/en/download/package-manager/) для linux.
12 |
13 | Например для **Ubuntu / Debian** выполните следующие команды, чтобы установить 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 | Вам подойдет любая версия Node > 6.5.0.
21 |
22 | ## Средства управления версиями Node.
23 |
24 | Если вам нужна гибкость в использовании различных версий Node, рассмотрите [NVM](https://github.com/creationix/nvm) или [tj/n](https://github.com/tj/n).
25 |
26 | ## NPM
27 |
28 | NPM - менеджер пакетов Node по умолчанию. Он автоматически устанавливается вместе с Node. Менеджеры пакетов используются для установки и управления пакетами (модулями кода, которые написали вы или кто-то другой). Мы будем использовать много пакетов в этом руководстве, но мы установим Yarn - другой пакетный менеджер.
29 |
30 | ## Yarn
31 |
32 | > 💡 **[Yarn](https://yarnpkg.com/)** - менеджер пакетов Node.js гораздо более быстрый чем NPM, поддерживает offline режим и с [более предсказуемой](https://yarnpkg.com/en/docs/yarn-lock) загрузкой пакетов.
33 |
34 | С момента [выхода](https://code.facebook.com/posts/1840075619545360) в Октябре 2016 он очень быстро получил признание, и, возможно, скоро станет основным менеджером пакетов для JavaScript сообщества. Если вы хотите остаться с NPM, вы можете просто заменить в этом руководстве все команды `yarn add` и `yarn add --dev` на `npm install --save` and `npm install --save-dev`.
35 |
36 | Установите Yarn, следую [инструкциям](https://yarnpkg.com/en/docs/install) для вашей ОС. Если вы на macOS или Unix, я бы рекомендовал использовать **установочный Script** из вкладки *Alternatives*, чтобы [избежать](https://github.com/yarnpkg/yarn/issues/1505) взаимодействий с другими пакетными менеджерами:
37 |
38 | ```sh
39 | curl -o- -L https://yarnpkg.com/install.sh | bash
40 | ```
41 |
42 | ## `package.json`
43 |
44 | > 💡 **[package.json](https://yarnpkg.com/en/docs/package-json)** - файл, используемый для описания и конфигурирования вашего JavaScript проекта. Он содержит основную информацию (имя проекта, версия, контрибьюторы, лиценция и т.д.), конфигурационные настройки для инструментов, которые вы используете и даже раздел для запуска *задач*.
45 |
46 | - Создайте новую папку для работы и `cd` в нее.
47 | - Запустите `yarn init` и ответьте на вопросы (`yarn init -y` - пропустить все вопросы), чтобы создать файл `package.json` автоматически.
48 |
49 | Вот простой `package.json` который я буду использовать в этом руководстве:
50 |
51 | ```json
52 | {
53 | "name": "your-project",
54 | "version": "1.0.0",
55 | "license": "MIT"
56 | }
57 | ```
58 |
59 | ## Hello World
60 |
61 | - Создайте файл `index.js` содержащий `console.log('Hello world')`
62 |
63 | 🏁 Запустите `node .` в этой папке (`index.js` - файл по умолчанию, который Node ищет в папке). Должно выйти "Hello world".
64 |
65 | **Примечание**: Видите этот значек - 🏁 - гоночный флаг? Я буду его использовать каждый раз при достижении **чекпоинта**. Иногда мы будем делать много изменения подряд, и ваш код не будет работать до тех пор, пока вы не достигнете следующего чекпоинта.
66 |
67 | ## `start` script
68 |
69 | Использование `node .` для запуска программ - несколько низкоуровневый подход. Вместо этого, мы будем использовать NPM/Yarn скрипты для запуска выполнения этого кода. Это даст нам отличный уровень абстракции, позволяющий всегда использовать `yarn start`, даже когда наша программа станет более сложной.
70 |
71 | - В `package.json`, добавьте такой объект `scripts`:
72 |
73 | ```json
74 | {
75 | "name": "your-project",
76 | "version": "1.0.0",
77 | "license": "MIT",
78 | "scripts": {
79 | "start": "node ."
80 | }
81 | }
82 | ```
83 |
84 | `start` - имя, которое мы дали *задаче*, которая будет запускать нашу программу. Мы создадим много различных задач в этом объекте `scripts` в ходе данного руководства. `start` - это имя по умолчанию одной из типичных задач приложения. Другие стандартные названия задач - это `stop` это `test`.
85 |
86 | `package.json` должен быть валидным JSON файлом, что означает, что вы не можете использовать конечные запятые (trailing commas). Поэтому будьте аккуратны вручную редактируя файл `package.json`.
87 |
88 | 🏁 Запустите `yarn start`. Должно выйти `Hello world`.
89 |
90 | ## Git и `.gitignore`
91 |
92 | - Инициализируйте репозиторий Git с помощью `git init`
93 |
94 | - Создайте файл `.gitignore` и добавьте в него следующее:
95 |
96 | ```gitignore
97 | .DS_Store
98 | /*.log
99 | ```
100 |
101 | `.DS_Store` - автогенерируемые macOS файлы, которые никогда не должны быть в вашем репозитории.
102 |
103 | `npm-debug.log` и `yarn-error.log` - файлы, генерируемые пакетным менеджером при ошибках. Мы не хотим хранить их в репозитории.
104 |
105 | ## Установка и использование пакетов
106 |
107 | В этой части мы установим и воспользуемся пакетом. "Пакет" - это просто кусок кода, который написал кто-то другой и который вы можете использовать в своем собственном коде. Это может быть что угодно. Сейчас, например, мы попробуем пакет, который помогает манипулировать цветами.
108 |
109 | - Установим созданный сообществом пакет, который называется `color`, запустив `yarn add color`.
110 |
111 | Откройте `package.json` чтобы увидеть как Yarn автоматически добавил `color` в `dependencies`.
112 |
113 | Папка `node_modules` была создана для хранения пакетов.
114 |
115 | - Добавьте `node_modules/` в `.gitignore`
116 |
117 | Вы также заметите файл `yarn.lock`, сгенерированный Yarn. Вам нужно добавить коммит с этим файлом в репозиторий, поскольку это даст уверенность, что все в вашей команде используют одни и теже версии пакетов. Если вы предпочитаете NPM а не Yarn, эквивалентом этому файлу будет *shrinkwrap*.
118 |
119 | - Напишите следующее в файл `index.js`:
120 |
121 | ```js
122 | const color = require('color')
123 |
124 | const redHexa = color({ r: 255, g: 0, b: 0 }).hex()
125 |
126 | console.log(redHexa)
127 | ```
128 |
129 | 🏁 Запустите `yarn start`. Должно выйти `#FF0000`.
130 |
131 | Поздравляем! Вы установили и использовали пакет.
132 |
133 | Мы использовали `color` в этом разделе, чтобы продемонстрировать вам как использовать простой пакет. Он больше нам не потребуется, поэтому можно его удалить:
134 |
135 | - Запустите `yarn remove color`
136 |
137 | ## Два вида зависимостей
138 |
139 | Пакеты могут быть двух видов зависимостей `"dependencies"` и `"devDependencies"`
140 |
141 | **Dependencies** - библиотеки, нужные чтобы ваше приложение функционировало (React, Redux, Lodash, jQuery, etc). Вы устанавливаете их с помощью `yarn add [package]`.
142 |
143 | **Dev Dependencies** - библиотеки, используемые во время разработки или для сборки вашего приложения (Webpack, SASS, linters, testing frameworks, etc). Устанавливайте их с помощью `yarn add --dev [package]`.
144 |
145 | Следующий раздел: [02 - Babel, ES6, ESLint, Flow, Jest, Husky](02-babel-es6-eslint-flow-jest-husky.md#readme)
146 |
147 | Назад в [содержание](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
148 |
--------------------------------------------------------------------------------
/tutorial/02-babel-es6-eslint-flow-jest-husky.md:
--------------------------------------------------------------------------------
1 | # 02 - Babel, ES6, ESLint, Flow, Jest, and Husky
2 |
3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/02-babel-es6-eslint-flow-jest-husky).
4 |
5 | We're now going to use some ES6 syntax, which is a great improvement over the "old" ES5 syntax. All browsers and JS environments understand ES5 well, but not ES6. That's where a tool called Babel comes to the rescue!
6 |
7 | ## Babel
8 |
9 | > 💡 **[Babel](https://babeljs.io/)** is a compiler that transforms ES6 code (and other things like React's JSX syntax) into ES5 code. It is very modular and can be used in tons of different [environments](https://babeljs.io/docs/setup/). It is by far the preferred ES5 compiler of the React community.
10 |
11 | - Move your `index.js` into a new `src` folder. This is where you will write your ES6 code. Remove the previous `color`-related code in `index.js`, and replace it with a simple:
12 |
13 | ```js
14 | const str = 'ES6'
15 | console.log(`Hello ${str}`)
16 | ```
17 |
18 | We're using a *template string* here, which is an ES6 feature that lets us inject variables directly inside the string without concatenation using `${}`. Note that template strings are created using **backquotes**.
19 |
20 | - Run `yarn add --dev babel-cli` to install the CLI interface for Babel.
21 |
22 | Babel CLI comes with [two executables](https://babeljs.io/docs/usage/cli/): `babel`, which compiles ES6 files into new ES5 files, and `babel-node`, which you can use to replace your call to the `node` binary and execute ES6 files directly on the fly. `babel-node` is great for development but it is heavy and not meant for production. In this chapter we are going to use `babel-node` to set up the development environment, and in the next one we'll use `babel` to build ES5 files for production.
23 |
24 | - In `package.json`, in your `start` script, replace `node .` by `babel-node src` (`index.js` is the default file Node looks for, which is why we can omit `index.js`).
25 |
26 | If you try to run `yarn start` now, it should print the correct output, but Babel is not actually doing anything. That's because we didn't give it any information about which transformations we want to apply. The only reason it prints the right output is because Node natively understands ES6 without Babel's help. Some browsers or older versions of Node would not be so successful though!
27 |
28 | - Run `yarn add --dev babel-preset-env` to install a Babel preset package called `env`, which contains configurations for the most recent ECMAScript features supported by Babel.
29 |
30 | - Create a `.babelrc` file at the root of your project, which is a JSON file for your Babel configuration. Write the following to it to make Babel use the `env` preset:
31 |
32 | ```json
33 | {
34 | "presets": [
35 | "env"
36 | ]
37 | }
38 | ```
39 |
40 | 🏁 `yarn start` should still work, but it's actually doing something now. We can't really tell if it is though, since we're using `babel-node` to interpret ES6 code on the fly. You'll soon have a proof that your ES6 code is actually transformed when you reach the [ES6 modules syntax](#the-es6-modules-syntax) section of this chapter.
41 |
42 | ## ES6
43 |
44 | > 💡 **[ES6](http://es6-features.org/)**: The most significant improvement of the JavaScript language. There are too many ES6 features to list them here but typical ES6 code uses classes with `class`, `const` and `let`, template strings, and arrow functions (`(text) => { console.log(text) }`).
45 |
46 | ### Creating an ES6 class
47 |
48 | - Create a new file, `src/dog.js`, containing the following ES6 class:
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 | It should not look surprising to you if you've done OOP in the past in any language. It's relatively recent for JavaScript though. The class is exposed to the outside world via the `module.exports` assignment.
65 |
66 | In `src/index.js`, write the following:
67 |
68 | ```js
69 | const Dog = require('./dog')
70 |
71 | const toby = new Dog('Toby')
72 |
73 | console.log(toby.bark())
74 | ```
75 |
76 | As you can see, unlike the community-made package `color` that we used before, when we require one of our files, we use `./` in the `require()`.
77 |
78 | 🏁 Run `yarn start` and it should print "Wah wah, I am Toby".
79 |
80 | ### The ES6 modules syntax
81 |
82 | Here we simply replace `const Dog = require('./dog')` by `import Dog from './dog'`, which is the newer ES6 modules syntax (as opposed to "CommonJS" modules syntax). It is currently not natively supported by NodeJS, so this is your proof that Babel processes those ES6 files correctly.
83 |
84 | In `dog.js`, we also replace `module.exports = Dog` by `export default Dog`
85 |
86 | 🏁 `yarn start` should still print "Wah wah, I am Toby".
87 |
88 | ## ESLint
89 |
90 | > 💡 **[ESLint](http://eslint.org)** is the linter of choice for ES6 code. A linter gives you recommendations about code formatting, which enforces style consistency in your code, and code you share with your team. It's also a great way to learn about JavaScript by making mistakes that ESLint will catch.
91 |
92 | ESLint works with *rules*, and there are [many of them](http://eslint.org/docs/rules/). Instead of configuring the rules we want for our code ourselves, we will use the config created by Airbnb. This config uses a few plugins, so we need to install those as well.
93 |
94 | Check out Airbnb's most recent [instructions](https://www.npmjs.com/package/eslint-config-airbnb) to install the config package and all its dependencies correctly. As of 2017-02-03, they recommend using the following command in your terminal:
95 |
96 | ```sh
97 | npm info eslint-config-airbnb@latest peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs yarn add --dev eslint-config-airbnb@latest
98 | ```
99 |
100 | It should install everything you need and add `eslint-config-airbnb`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, and `eslint-plugin-react` to your `package.json` file automatically.
101 |
102 | **Note**: I've replaced `npm install` by `yarn add` in this command. Also, this won't work on Windows, so take a look at the `package.json` file of this repository and just install all the ESLint-related dependencies manually using `yarn add --dev packagename@^#.#.#` with `#.#.#` being the versions given in `package.json` for each package.
103 |
104 | - Create an `.eslintrc.json` file at the root of your project, just like we did for Babel, and write the following to it:
105 |
106 | ```json
107 | {
108 | "extends": "airbnb"
109 | }
110 | ```
111 |
112 | We'll create an NPM/Yarn script to run ESLint. Let's install the `eslint` package to be able to use the `eslint` CLI:
113 |
114 | - Run `yarn add --dev eslint`
115 |
116 | Update the `scripts` of your `package.json` to include a new `test` task:
117 |
118 | ```json
119 | "scripts": {
120 | "start": "babel-node src",
121 | "test": "eslint src"
122 | },
123 | ```
124 |
125 | Here we just tell ESLint that want to lint all JavaScript files under the `src` folder.
126 |
127 | We will use this standard `test` task to run a chain of all the commands that validate our code, whether it's linting, type checking, or unit testing.
128 |
129 | - Run `yarn test`, and you should see a whole bunch of errors for missing semicolons, and a warning for using `console.log()` in `index.js`. Add `/* eslint-disable no-console */` at the top of our `index.js` file to allow the use of `console` in this file.
130 |
131 | **Note**: If you're on Windows, make sure you configure your editor and Git to use Unix LF line endings and not Windows CRLF. If your project is only used in Windows environments, you can add `"linebreak-style": [2, "windows"]` in ESLint's `rules` array (see the example below) to enforce CRLF instead.
132 |
133 | ### Semicolons
134 |
135 | Alright, this is probably the most heated debate in the JavaScript community, let's talk about it for a minute. JavaScript has this thing called Automatic Semicolon Insertion, which allows you to write your code with or without semicolons. It really comes down to personal preference and there is no right and wrong on this topic. If you like the syntax of Python, Ruby, or Scala, you will probably enjoy omitting semicolons. If you prefer the syntax of Java, C#, or PHP, you will probably prefer using semicolons.
136 |
137 | Most people write JavaScript with semicolons, out of habit. That was my case until I tried going semicolon-less after seeing code samples from the Redux documentation. At first it felt a bit weird, simply because I was not used to it. After just one day of writing code this way I could not see myself going back to using semicolons at all. They felt so cumbersome and unnecessary. A semicolon-less code is easier on the eyes in my opinion, and is faster to type.
138 |
139 | I recommend reading the [ESLint documentation about semicolons](http://eslint.org/docs/rules/semi). As mentioned in this page, if you're going semicolon-less, there are some rather rare cases where semicolons are required. ESLint can protect you from such cases with the `no-unexpected-multiline` rule. Let's set up ESLint to safely go semicolon-less in `.eslintrc.json`:
140 |
141 | ```json
142 | {
143 | "extends": "airbnb",
144 | "rules": {
145 | "semi": [2, "never"],
146 | "no-unexpected-multiline": 2
147 | }
148 | }
149 | ```
150 |
151 | 🏁 Run `yarn test`, and it should now pass successfully. Try adding an unnecessary semicolon somewhere to make sure the rule is set up correctly.
152 |
153 | I am aware that some of you will want to keep using semicolons, which will make the code provided in this tutorial inconvenient. If you are using this tutorial just for learning, I'm sure it will remain bearable to learn without semicolons, until going back to using them on your real projects. If you want to use the code provided in this tutorial as a boilerplate though, it will require a bit of rewriting, which should be pretty quick with ESLint set to enforce semicolons to guide you through the process. I apologize if you're in such case.
154 |
155 | ### Compat
156 |
157 | [Compat](https://github.com/amilajack/eslint-plugin-compat) is a neat ESLint plugin that warns you if you use some JavaScript APIs that are not available in the browsers you need to support. It uses [Browserslist](https://github.com/ai/browserslist), which relies on [Can I Use](http://caniuse.com/).
158 |
159 | - Run `yarn add --dev eslint-plugin-compat`
160 |
161 | - Add the following to your `package.json`, to indicate that we want to support browsers that have more than 1% market share:
162 |
163 | ```json
164 | "browserslist": ["> 1%"],
165 | ```
166 |
167 | - Edit your `.eslintrc.json` file like so:
168 |
169 | ```json
170 | {
171 | "extends": "airbnb",
172 | "plugins": [
173 | "compat"
174 | ],
175 | "rules": {
176 | "semi": [2, "never"],
177 | "no-unexpected-multiline": 2,
178 | "compat/compat": 2
179 | }
180 | }
181 | ```
182 |
183 | You can try the plugin by using `navigator.serviceWorker` or `fetch` in your code for instance, which should raise an ESLint warning.
184 |
185 | ### ESLint in your editor
186 |
187 | This chapter set you up with ESLint in the terminal, which is great for catching errors at build time / before pushing, but you also probably want it integrated to your IDE for immediate feedback. Do NOT use your IDE's native ES6 linting. Configure it so the binary it uses for linting is the one in your `node_modules` folder instead. This way it can use all of your project's config, the Airbnb preset, etc. Otherwise you will just get some generic ES6 linting.
188 |
189 | ## Flow
190 |
191 | > 💡 **[Flow](https://flowtype.org/)**: A static type checker by Facebook. It detects inconsistent types in your code. For instance, it will give you an error if you try to use a string where should be using a number.
192 |
193 | Right now, our JavaScript code is valid ES6 code. Flow can analyze plain JavaScript to give us some insights, but in order to use its full power, we need to add type annotations in our code, which will make it non-standard. We need to teach Babel and ESLint what those type annotations are in order for these tools to not freak out when parsing our files.
194 |
195 | - Run `yarn add --dev flow-bin babel-preset-flow babel-eslint eslint-plugin-flowtype`
196 |
197 | `flow-bin` is the binary to run Flow in our `scripts` tasks, `babel-preset-flow` is the preset for Babel to understand Flow annotations, `babel-eslint` is a package to enable ESLint *to rely on Babel's parser* instead of its own, and `eslint-plugin-flowtype` is an ESLint plugin to lint Flow annotations. Phew.
198 |
199 | - Update your `.babelrc` file like so:
200 |
201 | ```json
202 | {
203 | "presets": [
204 | "env",
205 | "flow"
206 | ]
207 | }
208 | ```
209 |
210 | - And update `.eslintrc.json` as well:
211 |
212 | ```json
213 | {
214 | "extends": [
215 | "airbnb",
216 | "plugin:flowtype/recommended"
217 | ],
218 | "plugins": [
219 | "flowtype",
220 | "compat"
221 | ],
222 | "rules": {
223 | "semi": [2, "never"],
224 | "no-unexpected-multiline": 2,
225 | "compat/compat": 2
226 | }
227 | }
228 | ```
229 |
230 | **Note**: The `plugin:flowtype/recommended` contains the instruction for ESLint to use Babel's parser. If you want to be more explicit, feel free to add `"parser": "babel-eslint"` in `.eslintrc.json`.
231 |
232 | I know this is a lot to take in, so take a minute to think about it. I'm still amazed that it is even possible for ESLint to use Babel's parser to understand Flow annotations. These 2 tools are really incredible for being so modular.
233 |
234 | - Chain `flow` to your `test` task:
235 |
236 | ```json
237 | "scripts": {
238 | "start": "babel-node src",
239 | "test": "eslint src && flow"
240 | },
241 | ```
242 |
243 | - Create a `.flowconfig` file at the root of your project containing:
244 |
245 | ```flowconfig
246 | [options]
247 | suppress_comment= \\(.\\|\n\\)*\\flow-disable-next-line
248 | ```
249 |
250 | This is a little utility that we set up to make Flow ignore any warning detected on the next line. You would use it like this, similarly to `eslint-disable`:
251 |
252 | ```js
253 | // flow-disable-next-line
254 | something.flow(doesnt.like).for.instance()
255 | ```
256 |
257 | Alright, we should be all set for the configuration part.
258 |
259 | - Add Flow annotations to `src/dog.js` like so:
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 | The `// @flow` comment tells Flow that we want this file to be type-checked. For the rest, Flow annotations are typically a colon after a function parameter or a function name. Check out the [documentation](https://flowtype.org/docs/quick-reference.html) for more details.
280 |
281 | - Add `// @flow` at the top of `index.js` as well.
282 |
283 | `yarn test` should now both lint and type-check your code fine.
284 |
285 | There are 2 things that I want you to try:
286 |
287 | - In `dog.js`, replace `constructor(name: string)` by `constructor(name: number)`, and run `yarn test`. You should get a **Flow** error telling you that those types are incompatible. That means Flow is set up correctly.
288 |
289 | - Now replace `constructor(name: string)` by `constructor(name:string)`, and run `yarn test`. You should get an **ESLint** error telling you that Flow annotations should have a space after the colon. That means the Flow plugin for ESLint is set up correctly.
290 |
291 | 🏁 If you got the 2 different errors working, you are all set with Flow and ESLint! Remember to put the missing space back in the Flow annotation.
292 |
293 | ### Flow in your editor
294 |
295 | Just like with ESLint, you should spend some time configuring your editor / IDE to give you immediate feedback when Flow detects issues in your code.
296 |
297 | ## Jest
298 |
299 | > 💡 **[Jest](https://facebook.github.io/jest/)**: A JavaScript testing library by Facebook. It is very simple to set up and provides everything you would need from a testing library right out of the box. It can also test React components.
300 |
301 | - Run `yarn add --dev jest babel-jest` to install Jest and the package to make it use Babel.
302 |
303 | - Add the following to your `.eslintrc.json` at the root of the object to allow the use of Jest's functions without having to import them in every test file:
304 |
305 | ```json
306 | "env": {
307 | "jest": true
308 | }
309 | ```
310 |
311 | - Create a `src/dog.test.js` file containing:
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 | - Add `jest` to your `test` script:
323 |
324 | ```json
325 | "scripts": {
326 | "start": "babel-node src",
327 | "test": "eslint src && flow && jest --coverage"
328 | },
329 | ```
330 |
331 | The `--coverage` flag makes Jest generate coverage data for your tests automatically. This is useful to see which parts of your codebase lack testing. It writes this data into a `coverage` folder.
332 |
333 | - Add `/coverage/` to your `.gitignore`
334 |
335 | 🏁 Run `yarn test`. After linting and type checking, it should run Jest tests and show a coverage table. Everything should be green!
336 |
337 | ## Git Hooks with Husky
338 |
339 | > 💡 **[Git Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)**: Scripts that are run when certain actions like a commit or a push occur.
340 |
341 | Okay so we now have this neat `test` task that tells us if our code looks good or not. We're going to set up Git Hooks to automatically run this task before every `git commit` and `git push`, which will prevent us from pushing bad code to the repository if it doesn't pass the `test` task.
342 |
343 | [Husky](https://github.com/typicode/husky) is a package that makes this very easy to set up Git Hooks.
344 |
345 | - Run `yarn add --dev husky`
346 |
347 | All we have to do is to create two new tasks in `scripts`, `precommit` and `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 | 🏁 If you now try to commit or push your code, it should automatically run the `test` task.
359 |
360 | If it does not work, it is possible that `yarn add --dev husky` did not install the Git Hooks properly. I never encountered this issue but it happens for some people. If that's your case, run `yarn add --dev husky --force`, and maybe post a note describing your situation in [this issue](https://github.com/typicode/husky/issues/84).
361 |
362 | **Note**: If you are pushing right after a commit, you can use `git push --no-verify` to avoid running all the tests again.
363 |
364 | Next section: [03 - Express, Nodemon, PM2](03-express-nodemon-pm2.md#readme)
365 |
366 | Back to the [previous section](01-node-yarn-package-json.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
367 |
--------------------------------------------------------------------------------
/tutorial/03-express-nodemon-pm2.md:
--------------------------------------------------------------------------------
1 | # 03 - Express, Nodemon, and PM2
2 |
3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/03-express-nodemon-pm2).
4 |
5 | In this section we are going to create the server that will render our web app. We will also set up a development mode and a production mode for this server.
6 |
7 | ## Express
8 |
9 | > 💡 **[Express](http://expressjs.com/)** is by far the most popular web application framework for Node. It provides a very simple and minimal API, and its features can be extended with *middleware*.
10 |
11 | Let's set up a minimal Express server to serve an HTML page with some CSS.
12 |
13 | - Delete everything inside `src`
14 |
15 | Create the following files and folders:
16 |
17 | - Create a `public/css/style.css` file containing:
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 | - Create an empty `src/client/` folder.
32 |
33 | - Create an empty `src/shared/` folder.
34 |
35 | This folder is where we put *isomorphic / universal* JavaScript code – files that are used by both the client and the server. A great use case of shared code is *routes*, as you will see later in this tutorial when we'll make an asynchronous call. Here we simply have some configuration constants as an example for now.
36 |
37 | - Create a `src/shared/config.js` file, containing:
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 | If the Node process used to run your app has a `process.env.PORT` environment variable set (that's the case when you deploy to Heroku for instance), it will use this for the port. If there is none, we default to `8000`.
48 |
49 | - Create a `src/shared/util.js` file containing:
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 | That's a simple util to test if we are running in production mode or not. The `// eslint-disable-next-line import/prefer-default-export` comment is because we only have one named export here. You can remove it as you add other exports in this file.
59 |
60 | - Run `yarn add express compression`
61 |
62 | `compression` is an Express middleware to activate Gzip compression on the server.
63 |
64 | - Create a `src/server/index.js` file containing:
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 | Nothing fancy here, it's almost Express' Hello World tutorial with a few additional imports. We're using 2 different static file directories here. `dist` for generated files, `public` for declarative ones.
93 |
94 | - Create a `src/server/render-app.js` file containing:
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 | You know how you typically have *templating engines* on the back-end? Well these are pretty much obsolete now that JavaScript supports template strings. Here we create a function that takes a `title` as a parameter and injects it in both the `title` and `h1` tags of the page, returning the complete HTML string. We also use a `STATIC_PATH` constant as the base path for all our static assets.
118 |
119 | ### HTML template strings syntax highlighting in Atom (optional)
120 |
121 | It might be possible to get syntax highlighting working for HTML code inside template strings depending on your editor. In Atom, if you prefix your template string with an `html` tag (or any tag that *ends* with `html`, like `ilovehtml`), it will automatically highlight the content of that string. I sometimes use the `html` tag of the `common-tags` library to take advantage of this:
122 |
123 | ```js
124 | import { html } from `common-tags`
125 |
126 | const template = html`
127 |
Wow, colors!
128 | `
129 | ```
130 |
131 | I did not include this trick in the boilerplate of this tutorial, since it seems to only work in Atom, and it's less than ideal. Some of you Atom users might find it useful though.
132 |
133 | Anyway, back to business!
134 |
135 | - In `package.json` change your `start` script like so: `"start": "babel-node src/server",`
136 |
137 | 🏁 Run `yarn start`, and hit `localhost:8000` in your browser. If everything works as expected you should see a blank page with "Hello App" written both on the tab title and as a green heading on the page.
138 |
139 | **Note**: Some processes – typically processes that wait for things to happen, like a server for instance – will prevent you from entering commands in your terminal until they're done. To interrupt such processes and get your prompt back, press **Ctrl+C**. You can alternatively open a new terminal tab if you want to keep them running while being able to enter commands. You can also make these processes run in the background but that's out of the scope of this tutorial.
140 |
141 | ## Nodemon
142 |
143 | > 💡 **[Nodemon](https://nodemon.io/)** is a utility to automatically restart your Node server when file changes happen in the directory.
144 |
145 | We are going to use Nodemon whenever we are in **development** mode.
146 |
147 | - Run `yarn add --dev nodemon`
148 |
149 | - Change your `scripts` like so:
150 |
151 | ```json
152 | "start": "yarn dev:start",
153 | "dev:start": "nodemon --ignore lib --exec babel-node src/server",
154 | ```
155 |
156 | `start` is now just a pointer to an other task, `dev:start`. That gives us a layer of abstraction to tweak what the default task is.
157 |
158 | In `dev:start`, the `--ignore lib` flag is to *not* restart the server when changes happen in the `lib` directory. You don't have this directory yet, but we're going to generate it in the next section of this chapter, so it will soon make sense. Nodemon typically runs the `node` binary. In our case, since we're using Babel, we can tell Nodemon to use the `babel-node` binary instead. This way it will understand all the ES6/Flow code.
159 |
160 | 🏁 Run `yarn start` and open `localhost:8000`. Go ahead and change the `APP_NAME` constant in `src/shared/config.js`, which should trigger a restart of your server in the terminal. Refresh the page to see the updated title. Note that this automatic restart of the server is different from *Hot Module Replacement*, which is when components on the page update in real-time. Here we still need a manual refresh, but at least we don't need to kill the process and restart it manually to see changes. Hot Module Replacement will be introduced in the next chapter.
161 |
162 | ## PM2
163 |
164 | > 💡 **[PM2](http://pm2.keymetrics.io/)** is a Process Manager for Node. It keeps your processes alive in production, and offers tons of features to manage them and monitor them.
165 |
166 | We are going to use PM2 whenever we are in **production** mode.
167 |
168 | - Run `yarn add --dev pm2`
169 |
170 | In production, you want your server to be as performant as possible. `babel-node` triggers the whole Babel transpilation process for your files at each execution, which is not something you want in production. We need Babel to do all this work beforehand, and have our server serve plain old pre-compiled ES5 files.
171 |
172 | One of the main features of Babel is to take a folder of ES6 code (usually named `src`) and transpile it into a folder of ES5 code (usually named `lib`).
173 |
174 | This `lib` folder being auto-generated, it's a good practice to clean it up before a new build, since it may contain unwanted old files. A neat simple package to delete files with cross platform support is `rimraf`.
175 |
176 | - Run `yarn add --dev rimraf`
177 |
178 | Let's add the following `prod:build` task to our `package.json`:
179 |
180 | ```json
181 | "prod:build": "rimraf lib && babel src -d lib --ignore .test.js",
182 | ```
183 |
184 | - Run `yarn prod:build`, and it should generate a `lib` folder containing the transpiled code, except for files ending in `.test.js` (note that `.test.jsx` files are also ignored by this parameter).
185 |
186 | - Add `/lib/` to your `.gitignore`
187 |
188 | One last thing: We are going to pass a `NODE_ENV` environment variable to our PM2 binary. With Unix, you would do this by running `NODE_ENV=production pm2`, but Windows uses a different syntax. We're going to use a small package called `cross-env` to make this syntax work on Windows as well.
189 |
190 | - Run `yarn add --dev cross-env`
191 |
192 | Let's update our `package.json` like so:
193 |
194 | ```json
195 | "scripts": {
196 | "start": "yarn dev:start",
197 | "dev:start": "nodemon --ignore lib --exec babel-node src/server",
198 | "prod:build": "rimraf lib && babel src -d lib --ignore .test.js",
199 | "prod:start": "cross-env NODE_ENV=production pm2 start lib/server && pm2 logs",
200 | "prod:stop": "pm2 delete server",
201 | "test": "eslint src && flow && jest --coverage",
202 | "precommit": "yarn test",
203 | "prepush": "yarn test"
204 | },
205 | ```
206 |
207 | 🏁 Run `yarn prod:build`, then run `yarn prod:start`. PM2 should show an active process. Go to `http://localhost:8000/` in your browser and you should see your app. Your terminal should show the logs, which should be "Server running on port 8000 (production).". Note that with PM2, your processes are run in the background. If you press Ctrl+C, it will kill the `pm2 logs` command, which was the last command our our `prod:start` chain, but the server should still render the page. If you want to stop the server, run `yarn prod:stop`
208 |
209 | Now that we have a `prod:build` task, it would be neat to make sure it works fine before pushing code to the repository. Since it is probably unnecessary to run it for every commit, I suggest adding it to the `prepush` task:
210 |
211 | ```json
212 | "prepush": "yarn test && yarn prod:build"
213 | ```
214 |
215 | 🏁 Run `yarn prepush` or just push your files to trigger the process.
216 |
217 | **Note**: We don't have any test here, so Jest will complain a bit. Ignore it for now.
218 |
219 | Next section: [04 - Webpack, React, HMR](04-webpack-react-hmr.md#readme)
220 |
221 | Back to the [previous section](02-babel-es6-eslint-flow-jest-husky.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
222 |
--------------------------------------------------------------------------------
/tutorial/03-express-nodemon-pm2_ru.md:
--------------------------------------------------------------------------------
1 | # 03 - Express, Nodemon и PM2
2 |
3 | Код для этой главы доступен [тут](https://github.com/verekia/js-stack-walkthrough/tree/master/03-express-nodemon-pm2).
4 |
5 | В этом разделе мы создадим сервер, который будет генерировать наше веб приложение. Также мы настроим для этого сервера режимы разработки и production.
6 |
7 | ## Express
8 |
9 | Express определенно наиболее популярный фреймворк для веб приложений под Node. У него очень простой и минимальный API, а его возможности могут быть расширены с помощью *промежуточного ПО* (middleware).
10 |
11 | Давайте настроим минимальный сервер Express, выдающий HTML страницу с минимальным CSS.
12 |
13 | - Удалите все внутри `src`
14 |
15 | Создайте следующие файлы и папки:
16 |
17 | - Создайте файл `public/css/style.css` содержащий:
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 | - Создайте пустую папку `src/client/`.
32 |
33 | - Создайте пустую папку `src/shared/`.
34 |
35 | Эта папка - место в которое мы поместим *изоморфный / универсальный* JavaScript код - файлы которые будут использованы как на клиенте, так и на сервере. Отличный пример использования общего кода - *маршруты* (routes), как вы увидите дальше в этом руководстве, когда мы будем использовать асинхронный вызов. Пока что мы просто разместим тут несколько конфигурационных констант в качестве примера.
36 |
37 | - Создайте файл `src/shared/config.js`, содержащий:
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 | Если процесс Node, запускающий ваше приложение содержит переменную окружения `process.env.PORT` (например, в случае, если вы публикуете на Heroku), она будет задавать порт. В противном случае, по умолчанию будет использоваться `8000`.
48 |
49 | - Создайте файл `src/shared/util.js`, содержащий:
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 | Это простая утилита для проверки запущены ли мы в режиме production или нет. Комментарий `// eslint-disable-next-line import/prefer-default-export` нужен изза того, что у нас только один именованный экспорт в это файле. Вы можете его убрать как только добавите сюда экспорт других переменных.
59 |
60 | - Выполните `yarn add express compression`
61 |
62 | `compression` - это промежуточное ПО для Express активирующее Gzip сжатие на сервере.
63 |
64 | - Создайте файл `src/server/index.js` содержащий:
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 | Здесь ничего особенного, это практически 'Hello World' для Express плюс несколько дополнительных импортов. Мы здесь используем две отдельных директории для статических файлов. `dist` для генерируемых и `public` для декларируемых.
93 |
94 | - Создайте файл `src/server/render-app.js` содержащий:
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 | Возможно, вам привычно использовать *шаблонизаторы* при работе с back-end? Что ж, теперь их можно считать довольно устаревшимы с тех пор, как JavaScript стал поддерживать шаблонные строки. Здесь мы создали функцию, которая принимает в качестве параметра `title` и вставляет это значение в тэги `title` и `h1`, возвращая строку с полноценной HTML страницей. Мы также используем константу `STATIC_PATH` для задания базового пути для всех наших статических элементов.
118 |
119 | ### Подсветка HTML синтаксиса в шаблонных строках для Atom (не обязательное)
120 |
121 | В зависимости от используемого текстового редактора, возможна подсветка синтаксиса HTML кода внутри шаблонных строк. В Атоме, если вы добавите префикс `html` к шаблонной строке (или любой другой префикс, заканчивающийся на `html`, как `ilovehtml`), то содержимое этой строки автоматически будет подсвечиваться. Я иногда использую `html` тэг из библиотеки `common-tags`, чтобы воспользоваться данной возможностью:
122 |
123 | ```js
124 | import { html } from `common-tags`
125 |
126 | const template = html`
127 |
Wow, colors!
128 | `
129 | ```
130 |
131 | Я не стал включать этот трюк в boilerplate этого руководства, поскольку это, похоже работает только в Атоме, и это не идеальный подход. Однако для тех, кто использует Атом, это может быть полезным.
132 |
133 | В любом случае вернемся к нашему проекту.
134 |
135 | - В `package.json` измените скрипт `start` таким образом: `"start": "babel-node src/server",`
136 |
137 | 🏁 Запустите `yarn start` и откройте `localhost:8000` в браузере. Если все заработало как и ожидалось, то вы увидете пустую страницу с надписями "Hello App" в названии вкладки и на зеленом заголовке страницы.
138 |
139 | **Примечание**: Некоторые процессы (обычно процессы, ожидающие своего завершения, как, например, сервер) не позволяют вам вводить команды в терминале пока они не завершатся. Чтобы прервать подобный процесс и получить обратно приглашение к вводу, нажмите **Ctrl+C**. Как вариант, вы можете открыть еще одну вкладку с терминалом, если хотите, чтобы процесс работал, пока вы вводите команды. Вы также можете запустить эти процессы в фоне, но это вне рамок данного руководства.
140 |
141 | ## Nodemon
142 |
143 | > 💡 **[Nodemon](https://nodemon.io/)** - утилита для автоматического перезапуска сервера Node при изменении файлов в директории.
144 |
145 | Мы будем использовать Nodemon в режиме **разработки**
146 |
147 | - Запустите `yarn add --dev nodemon`
148 |
149 | - Измените `scripts` так, чтобы:
150 |
151 | ```json
152 | "start": "yarn dev:start",
153 | "dev:start": "nodemon --ignore lib --exec babel-node src/server",
154 | ```
155 |
156 | Теперь `start` лишь указатель на другую задачу - `dev:start`. Это дает нам уровень абстракции, позволяющий настраивать какая задача будет выполняться по умолчанию.
157 |
158 | В `dev:start`, мы устанавливаем флаг `--ignore lib` для того, чтобы *не* перезапускать сервер, когда изменения происходят в директории `lib`. У вас пока еще нет этой директории, но мы создадим ее в следующем разделе этой главы. Так что скоро это понадобится. Обычно Nodemon запускает бинарники `node`. В нашем случае, поскольку мы используем Babel, мы, вместо этого, указали Nodemon запускать `babel-node`. Таким образом, мы сделали доступным весь наш ES6/Flow код.
159 |
160 | 🏁 Запустите `yarn start` и откройте `localhost:8000`. Двигаемся дальше и изменим константу `APP_NAME` в `src/shared/config.js`, что должно вызвать перезапуск сервера в терминале. Обновите страницу, чтобы увидеть измененный заголовок. Заметьте, что этот автоматический рестарт сервера отличается от *Hot Module Replacement*, при котором компоненты обновляются на странице в реальном времени. Здесь нам по прежнему требуется ручное обновление, но по крайней мере не нужно убивать процесс и вручную перезапускать сервер, чтобы увидеть изменения. Hot Module Replacement будет представлен в следующей главе.
161 |
162 | ## PM2
163 |
164 | > 💡 **[PM2](http://pm2.keymetrics.io/)** - это менеджер процессов для Node, обеспечивающий жизнеспособность вашего приложения и предлагающий тонны возможностей по управлению и мониторингу.
165 |
166 | Мы будем использовать PM2 в режиме **production**
167 |
168 |
169 | - Выполните `yarn add --dev pm2`
170 |
171 | В production вы хотите, чтобы сервер был настолько производительным, насколько это возможно. `babel-node` начинает процесс транспиляции всех файлов при каждом перезапуске, чего вы бы хотели избежать в production. Нам нужно, чтобы Babel выполнил всю эту работу заранее, и сервер выдавал обычные старые предкомпилированные ES5 файлы.
172 |
173 | Одной из основных возможностей Babel является способность взять папку с ES6 кодом (обычно `src`) и транспилировать его в папку с ES5 кодом (обычно `lib`).
174 |
175 | Поскольку папка `lib` автогенерируется, хорошей практикой будет очищать ее перед каждым новым построением, поскольку она может содержать нежелательные старые файлы. `rimraf` – простой лаконичный пакет, для удаления файлов с кроссплатформенной поддержкой.
176 |
177 | - Запустите `yarn add --dev rimraf`
178 |
179 | Давайте добавим следующую задачу `prod:build` в `package.json`:
180 |
181 | ```json
182 | "prod:build": "rimraf lib && babel src -d lib --ignore .test.js",
183 | ```
184 |
185 | - Запустите `yarn prod:build`. Это должно сгенерировать папку `lib`, содержащую транспилированный код, за исключением файлов, заканчивающихся на `.test.js` (заметьте, что файлы `.test.jsx` также игнорируются с помощью этого параметра).
186 |
187 | - Добавьте `/lib/` в `.gitignore`
188 |
189 | Последняя вещь: Мы собираемся передать переменную окружения `NODE_ENV` в исполняемый файл PM2. В Unix, вы бы сделали это через `NODE_ENV=production pm2`, но Windows использует другой синтаксис. Мы воспользуемся небольшим пакетом `cross-env`, чтобы заставить этот синтаксис работать также и для Windows.
190 |
191 | - Запустите `yarn add --dev cross-env`
192 |
193 | Обновим `package.json` так:
194 |
195 | ```json
196 | "scripts": {
197 | "start": "yarn dev:start",
198 | "dev:start": "nodemon --ignore lib --exec babel-node src/server",
199 | "prod:build": "rimraf lib && babel src -d lib --ignore .test.js",
200 | "prod:start": "cross-env NODE_ENV=production pm2 start lib/server && pm2 logs",
201 | "prod:stop": "pm2 delete server",
202 | "test": "eslint src && flow && jest --coverage",
203 | "precommit": "yarn test",
204 | "prepush": "yarn test"
205 | },
206 | ```
207 |
208 | 🏁 Запустите `yarn prod:build`, а затем `yarn prod:start`. PM2 должен показать активный процесс. Зайдите на `http://localhost:8000/` в браузере и вы должны увидеть наше приложение. Терминал должен выдать лог: "Server running on port 8000 (production).". Заметьте, что PM2 запускает процессы в фоне. Если вы нажмете Ctrl+C, это прервет команду `pm2 logs`, которая была последней в цепочке после `prod:start`, но сам сервер по прежнему должен генерировать страницы. Если вам нужно остановить сервер, наберите `yarn prod:stop`.
209 |
210 | Теперь, когда у нас есть задача `prod:build`, было бы здорово проверять все ли работает хорошо перед тем как закачивать код в репозиторий. Поскольку, возможно не требуется запускать его перед каждым коммитом, я предлагаю добавить это в задачу `prepush`:
211 |
212 | ```json
213 | "prepush": "yarn test && yarn prod:build"
214 | ```
215 |
216 | 🏁 Запустите `yarn prepush` или просто начните загружать файлы (push), чтобы запустить этот процесс.
217 |
218 | **Примечание**: У нас пока нет никаких тестов, так что Jest пожалуется на это. Пока что проигнорируйте это.
219 |
220 | Следующий раздел: [04 - Webpack, React, HMR](04-webpack-react-hmr_ru.md)
221 |
222 | Назад в [предыдущий раздел](02-babel-es6-eslint-flow-jest-husky_ru.md) или [содержание](../..).
--------------------------------------------------------------------------------
/tutorial/04-webpack-react-hmr.md:
--------------------------------------------------------------------------------
1 | # 04 - Webpack, React, and Hot Module Replacement
2 |
3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/04-webpack-react-hmr).
4 |
5 | ## Webpack
6 |
7 | > 💡 **[Webpack](https://webpack.js.org/)** is a *module bundler*. It takes a whole bunch of various source files, processes them, and assembles them into one (usually) JavaScript file called a bundle, which is the only file your client will execute.
8 |
9 | Let's create some very basic *hello world* and bundle it with Webpack.
10 |
11 | - In `src/shared/config.js`, add the following constants:
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 | - Create an `src/client/index.js` file containing:
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 | If you want to use some of the most recent ES features in your client code, like `Promise`s, you need to include the [Babel Polyfill](https://babeljs.io/docs/usage/polyfill/) before anything else in your bundle.
31 |
32 | - Run `yarn add babel-polyfill`
33 |
34 | If you run ESLint on this file, it will complain about `document` being undefined.
35 |
36 | - Add the following to `env` in your `.eslintrc.json` to allow the use of `window` and `document`:
37 |
38 | ```json
39 | "env": {
40 | "browser": true,
41 | "jest": true
42 | }
43 | ```
44 |
45 | Alright, we now need to bundle this ES6 client app into an ES5 bundle.
46 |
47 | - Create a `webpack.config.babel.js` file containing:
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 | This file is used to describe how our bundle should be assembled: `entry` is the starting point of our app, `output.filename` is the name of the bundle to generate, `output.path` and `output.publicPath` describe the destination folder and URL. We put the bundle in a `dist` folder, which will contain things that are generated automatically (unlike the declarative CSS we created earlier which lives in `public`). `module.rules` is where you tell Webpack to apply some treatment to some type of files. Here we say that we want all `.js` and `.jsx` (for React) files except the ones in `node_modules` to go through something called `babel-loader`. We also want these two extensions to be used to `resolve` modules when we `import` them. Finally, we declare a port for Webpack Dev Server.
82 |
83 | **Note**: The `.babel.js` extension is a Webpack feature to apply our Babel transformations to this config file.
84 |
85 | `babel-loader` is a plugin for Webpack that transpiles your code just like we've been doing since the beginning of this tutorial. The only difference is that this time, the code will end up running in the browser instead of your server.
86 |
87 | - Run `yarn add --dev webpack webpack-dev-server babel-core babel-loader`
88 |
89 | `babel-core` is a peer-dependency of `babel-loader`, so we installed it as well.
90 |
91 | - Add `/dist/` to your `.gitignore`
92 |
93 | ### Tasks update
94 |
95 | In development mode, we are going to use `webpack-dev-server` to take advantage of Hot Module Reloading (later in this chapter), and in production we'll simply use `webpack` to generate bundles. In both cases, the `--progress` flag is useful to display additional information when Webpack is compiling your files. In production, we'll also pass the `-p` flag to `webpack` to minify our code, and the `NODE_ENV` variable set to `production`.
96 |
97 | Let's update our `scripts` to implement all this, and improve some other tasks as well:
98 |
99 | ```json
100 | "scripts": {
101 | "start": "yarn dev:start",
102 | "dev:start": "nodemon -e js,jsx --ignore lib --ignore dist --exec babel-node src/server",
103 | "dev:wds": "webpack-dev-server --progress",
104 | "prod:build": "rimraf lib dist && babel src -d lib --ignore .test.js && cross-env NODE_ENV=production webpack -p --progress",
105 | "prod:start": "cross-env NODE_ENV=production pm2 start lib/server && pm2 logs",
106 | "prod:stop": "pm2 delete server",
107 | "lint": "eslint src webpack.config.babel.js --ext .js,.jsx",
108 | "test": "yarn lint && flow && jest --coverage",
109 | "precommit": "yarn test",
110 | "prepush": "yarn test && yarn prod:build"
111 | },
112 | ```
113 |
114 | In `dev:start` we explicitly declare file extensions to monitor, `.js` and `.jsx`, and add `dist` in the ignored directories.
115 |
116 | We created a separate `lint` task and added `webpack.config.babel.js` to the files to lint.
117 |
118 | - Next, let's create the container for our app in `src/server/render-app.js`, and include the bundle that will be generated:
119 |
120 | ```js
121 | // @flow
122 |
123 | import { APP_CONTAINER_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config'
124 | import { isProd } from '../shared/util'
125 |
126 | const renderApp = (title: string) =>
127 | `
128 |
129 |
130 | ${title}
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | `
139 |
140 | export default renderApp
141 | ```
142 |
143 | Depending on the environment we're in, we'll include either the Webpack Dev Server bundle, or the production bundle. Note that the path to Webpack Dev Server's bundle is *virtual*, `dist/js/bundle.js` is not actually read from your hard drive in development mode. It's also necessary to give Webpack Dev Server a different port than your main web port.
144 |
145 | - Finally, in `src/server/index.js`, tweak your `console.log` message like so:
146 |
147 | ```js
148 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' :
149 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`)
150 | ```
151 |
152 | That will give other developers a hint about what to do if they try to just run `yarn start` without Webpack Dev Server.
153 |
154 | Alright that was a lot of changes, let's see if everything works as expected:
155 |
156 | 🏁 Run `yarn start` in a terminal. Open an other terminal tab or window, and run `yarn dev:wds` in it. Once Webpack Dev Server is done generating the bundle and its sourcemaps (which should both be ~600kB files) and both processes hang in your terminals, open `http://localhost:8000/` and you should see "Hello Webpack!". Open your Chrome console, and under the Source tab, check which files are included. You should only see `static/css/style.css` under `localhost:8000/`, and have all your ES6 source files under `webpack://./src`. That means sourcemaps are working. In your editor, in `src/client/index.js`, try changing `Hello Webpack!` into any other string. As you save the file, Webpack Dev Server in your terminal should generate a new bundle and the Chrome tab should reload automatically.
157 |
158 | - Kill the previous processes in your terminals with Ctrl+C, then run `yarn prod:build`, and then `yarn prod:start`. Open `http://localhost:8000/` and you should still see "Hello Webpack!". In the Source tab of the Chrome console, you should this time find `static/js/bundle.js` under `localhost:8000/`, but no `webpack://` sources. Click on `bundle.js` to make sure it is minified. Run `yarn prod:stop`.
159 |
160 | Good job, I know this was quite dense. You deserve a break! The next section is easier.
161 |
162 | **Note**: I would recommend to have at least 3 terminals open, one for your Express server, one for the Webpack Dev Server, and one for Git, tests, and general commands like installing packages with `yarn`. Ideally, you should split your terminal screen in multiple panes to see them all.
163 |
164 | ## React
165 |
166 | > 💡 **[React](https://facebook.github.io/react/)** is a library for building user interfaces by Facebook. It uses the **[JSX](https://facebook.github.io/react/docs/jsx-in-depth.html)** syntax to represent HTML elements and components while leveraging the power of JavaScript.
167 |
168 | In this section we are going to render some text using React and JSX.
169 |
170 | First, let's install React and ReactDOM:
171 |
172 | - Run `yarn add react react-dom`
173 |
174 | Rename your `src/client/index.js` file into `src/client/index.jsx` and write some React code in it:
175 |
176 | ```js
177 | // @flow
178 |
179 | import 'babel-polyfill'
180 |
181 | import React from 'react'
182 | import ReactDOM from 'react-dom'
183 |
184 | import App from './app'
185 | import { APP_CONTAINER_SELECTOR } from '../shared/config'
186 |
187 | ReactDOM.render(, document.querySelector(APP_CONTAINER_SELECTOR))
188 | ```
189 |
190 | - Create a `src/client/app.jsx` file containing:
191 |
192 | ```js
193 | // @flow
194 |
195 | import React from 'react'
196 |
197 | const App = () =>
Hello React!
198 |
199 | export default App
200 | ```
201 |
202 | Since we use the JSX syntax here, we have to tell Babel that it needs to transform it with the `babel-preset-react` preset. And while we're at it, we're also going to add a Babel plugin called `flow-react-proptypes` which automatically generates PropTypes from Flow annotations for your React components.
203 |
204 | - Run `yarn add --dev babel-preset-react babel-plugin-flow-react-proptypes` and edit your `.babelrc` file like so:
205 |
206 | ```json
207 | {
208 | "presets": [
209 | "env",
210 | "flow",
211 | "react"
212 | ],
213 | "plugins": [
214 | "flow-react-proptypes"
215 | ]
216 | }
217 | ```
218 |
219 | 🏁 Run `yarn start` and `yarn dev:wds` and hit `http://localhost:8000`. You should see "Hello React!".
220 |
221 | Now try changing the text in `src/client/app.jsx` to something else. Webpack Dev Server should reload the page automatically, which is pretty neat, but we are going to make it even better.
222 |
223 | ## Hot Module Replacement
224 |
225 | > 💡 **[Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/)** (*HMR*) is a powerful Webpack feature to replace a module on the fly without reloading the entire page.
226 |
227 | To make HMR work with React, we are going to need to tweak a few things.
228 |
229 | - Run `yarn add react-hot-loader@next`
230 |
231 | - Edit your `webpack.config.babel.js` like so:
232 |
233 | ```js
234 | import webpack from 'webpack'
235 | // [...]
236 | entry: [
237 | 'react-hot-loader/patch',
238 | './src/client',
239 | ],
240 | // [...]
241 | devServer: {
242 | port: WDS_PORT,
243 | hot: true,
244 | },
245 | plugins: [
246 | new webpack.optimize.OccurrenceOrderPlugin(),
247 | new webpack.HotModuleReplacementPlugin(),
248 | new webpack.NamedModulesPlugin(),
249 | new webpack.NoEmitOnErrorsPlugin(),
250 | ],
251 | ```
252 |
253 | - Edit your `src/client/index.jsx` file:
254 |
255 | ```js
256 | // @flow
257 |
258 | import 'babel-polyfill'
259 |
260 | import React from 'react'
261 | import ReactDOM from 'react-dom'
262 | import { AppContainer } from 'react-hot-loader'
263 |
264 | import App from './app'
265 | import { APP_CONTAINER_SELECTOR } from '../shared/config'
266 |
267 | const rootEl = document.querySelector(APP_CONTAINER_SELECTOR)
268 |
269 | const wrapApp = AppComponent =>
270 |
271 |
272 |
273 |
274 | ReactDOM.render(wrapApp(App), rootEl)
275 |
276 | if (module.hot) {
277 | // flow-disable-next-line
278 | module.hot.accept('./app', () => {
279 | // eslint-disable-next-line global-require
280 | const NextApp = require('./app').default
281 | ReactDOM.render(wrapApp(NextApp), rootEl)
282 | })
283 | }
284 | ```
285 |
286 | We need to make our `App` a child of `react-hot-loader`'s `AppContainer`, and we need to `require` the next version of our `App` when hot-reloading. To make this process clean and DRY, we create a little `wrapApp` function that we use in both places it needs to render `App`. Feel free to move the `eslint-disable global-require` to the top of the file to make this more readable.
287 |
288 | 🏁 Restart your `yarn dev:wds` process if it was still running. Open `localhost:8000`. In the Console tab, you should see some logs about HMR. Go ahead and change something in `src/client/app.jsx` and your changes should be reflected in your browser after a few seconds, without any full-page reload!
289 |
290 | Next section: [05 - Redux, Immutable, Fetch](05-redux-immutable-fetch.md#readme)
291 |
292 | Back to the [previous section](03-express-nodemon-pm2.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
--------------------------------------------------------------------------------
/tutorial/04-webpack-react-hmr_ru.md:
--------------------------------------------------------------------------------
1 | # 04 - Webpack, React и Hot Module Replacement
2 |
3 | Код для этой главы доступен [здесь](https://github.com/verekia/js-stack-walkthrough/tree/master/04-webpack-react-hmr).
4 |
5 | ## Webpack
6 |
7 | > 💡 **[Webpack](https://webpack.js.org/)** - *сборщик модулей*. Он берет все возможные исходные файлы, обрабатывает их и собирает в один (обычно) JavaScript файл, называемый сборкой, и это будет единственный файл исполняемый на клиенте.
8 |
9 | Давайте создадим какой-нибудь простой *hello world* и соберем его с помощью Webpack.
10 |
11 | - В `src/shared/config.js` добавьте следующие константы:
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 | - Создайте файл `src/client/index.js`, содержащий:
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 | Если вы хотите использовать новейшие возможности ES в клиентском коде, такие как `Promise`, то вам нужно включить [Babel Polyfill](https://babeljs.io/docs/usage/polyfill/) до какого-либо другого кода в сборке.
31 |
32 | - Запустите `yarn add babel-polyfill`
33 |
34 | Если вы запустите ESLint на этом файле, он будет жаловаться, что `document` undefined.
35 |
36 | - Добавьте раздел `env` в `.eslintrc.json`, чтобы позволить использование `window` и `document`:
37 |
38 | ```json
39 | "env": {
40 | "browser": true,
41 | "jest": true
42 | }
43 | ```
44 |
45 | Хорошо, теперь нам нужно собрать это клиентское ES6 приложение в ES5 сборку.
46 |
47 | - Создайте файл `webpack.config.babel.js` содержащий:
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 | Этот файл используется для описания того, как должна быть устроена наша сборка: `entry` - стартовая точка нашего приложения, `output.filename` - имя генерируемой сборки, `output.path` и `output.publicPath` описывают путь до папки со сборкой и URL. Мы поместим сборку в папку `dist`, которая будет содержать автоматически генерируемые вещи (в отличие от обитающих в `public` декларативных CSS, которые мы создавали до этого). В `module.rules` мы сообщаем Webpack к каким типам файлов применять какие обработчики. Здесь мы говорим, что хотим пропускать все `.js` и `.jsx` (для реакта) файлы через нечто, называемое `babel-loader`, за исключением того, что находится в `node_modules`. Мы также хотим *разрешать* (`resolve`) эти два расширения при `import` модулей (т.е. эти расширения можно будет опускать при импорте - прим. пер.)
82 |
83 | **Примечание**: Расширение `.babel.js` сообщает Webpack применять трансформации Babel к данному конфигурационному файлу.
84 |
85 | `babel-loader` - это плагин для Webpack, транспилирующий код, так же как мы это делали с начала этого руководства. Единственное на данный момент отличие, что этот код исполняется в браузере а не на сервере.
86 |
87 | - Запустите `yarn add --dev webpack webpack-dev-server babel-core babel-loader`
88 |
89 | `babel-core` is a peer-dependency of `babel-loader`, so we installed it as well.
90 | Мы установили также `babel-core`, поскольку это peer-dependency (требуемая зависимость) для `babel-loader`.
91 |
92 | - Добавьте `/dist/` в `.gitignore`
93 |
94 | ### Обновление задач
95 |
96 | В режиме разработки мы будем использовать `webpack-dev-server` чтобы пользоваться преимуществами Hot Module Reloading (позже в этой главе), а в продакшене мы просто используем `webpack`, чтобы сгенерировать сборку. В обоих случаях, флаг `--progress` будет полезен для вывода дополнительной информации когда Webpack компилирует файлы. В продакшене мы также передаем в `webpack` флаг `-p` для минификации кода и переменную `NODE_ENV` установленную в `production`.
97 |
98 | Давайте обновим наши `scripts` чтобы реализовать это, а также улучшим некоторые другие задачи:
99 |
100 | ```json
101 | "scripts": {
102 | "start": "yarn dev:start",
103 | "dev:start": "nodemon -e js,jsx --ignore lib --ignore dist --exec babel-node src/server",
104 | "dev:wds": "webpack-dev-server --progress",
105 | "prod:build": "rimraf lib dist && babel src -d lib --ignore .test.js && cross-env NODE_ENV=production webpack -p --progress",
106 | "prod:start": "cross-env NODE_ENV=production pm2 start lib/server && pm2 logs",
107 | "prod:stop": "pm2 delete server",
108 | "lint": "eslint src webpack.config.babel.js --ext .js,.jsx",
109 | "test": "yarn lint && flow && jest --coverage",
110 | "precommit": "yarn test",
111 | "prepush": "yarn test && yarn prod:build"
112 | },
113 | ```
114 |
115 | В `dev:start` мы явно указываем расширения для наблюдения: `.js` и `.jsx`, и добавляем `dist` в игнорируемые директории.
116 |
117 | Мы создали отдельную задачу `lint` и добавили `webpack.config.babel.js` в список проверяемых файлов.
118 |
119 | - Затем давайте создадим контейнер для нашего приложения в `src/server/render-app.js` и включим его в генерируемую сборку:
120 |
121 | ```js
122 | // @flow
123 |
124 | import { APP_CONTAINER_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config'
125 | import { isProd } from '../shared/util'
126 |
127 | const renderApp = (title: string) =>
128 | `
129 |
130 |
131 | ${title}
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | `
140 |
141 | export default renderApp
142 | ```
143 |
144 | В зависимости от того, какое у нас окружение, мы включаем сборку Webpack Dev Server либо продакшен сборку. Обратите внимание на *виртуальный* путь к сборке Webpack Dev Server: `dist/js/bundle.js`, который на самом деле не читается с жесткого диска в режиме разработки. Также необходимо задать для Webpack Dev Server порт отличный от основного веб порта.
145 |
146 | - И наконец, в `src/server/index.js`, настройте сообщение от `console.log` таким образом:
147 |
148 | ```js
149 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' :
150 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`)
151 | ```
152 |
153 | Это даст другим разработчикам подсказку, что делать, если они просто пытаются запустить `yarn start` без Webpack Dev Server.
154 |
155 | Хорошо, мы произвели много изменений, давайте посмотрим, все ли работает как ожидалось:
156 |
157 | 🏁 Запустите `yarn start` в терминале. Откройте другую вкладку или окошко с терминалом и запустите в ней `yarn dev:wds`. Как только Webpack Dev Server завершит генерацию сборки и sourcemap карт (вместе должно быть ~600kB файлов), и оба процесса повиснут в терминале, откройте `http://localhost:8000/` и вы должны увидеть "Hello Webpack!". Откройте консоль Chrome и на вкладке Source проверьте какие файлы включены. Вы должны увидеть только `static/css/style.css` под `localhost:8000/`, а все ваши исходные ES6 файлы должны располагаться в `webpack://./src`. Это значит, что sourcemap работают. Попробуйте изменить `Hello Webpack!` в `src/client/index.js` на любую другую строку с помощью редактора. Как только вы сохраните файл, вы должны увидеть в терминале, что Webpack Dev Server сгенерировал новую сборку, и вкладка Chrome автоматически обновилась.
158 |
159 | - Завершите предыдущие процессы в терминалах с помощью Ctrl+C, затем запустите `yarn prod:build` и затем `yarn prod:start`. Откройте `http://localhost:8000/`, и вы по прежнему должны видеть "Hello Webpack!". На этот раз, во вкладке Source консоли Chrome под `localhost:8000/` должно быть `static/js/bundle.js`, но без исходников в `webpack://`. Кликните на `bundle.js` чтобы убедиться, что он минифицирован. Запустите `yarn prod:stop`.
160 |
161 | Отличная работа, знаю, это было довольно плотно. Вы заслужили перерыв! Следующий раздел будет легче.
162 |
163 | **Примечание**: Я бы рекомендовал открывать как минимум 3 терминала: один для сервера Express, один для Webpack Dev Server и один для Git, тестов и основных команд, таких как установка пакетов с помощью `yarn`. В идеале, нужно разделить окно терминала на несколько панелей, чтобы видеть их все.
164 |
165 | ## React
166 |
167 | > 💡 **[React](https://facebook.github.io/react/)** - библиотека для построения пользовательских интерфейсов от Facebook. Она использует синтаксис **[JSX](https://facebook.github.io/react/docs/jsx-in-depth.html)** для представления HTML элементов и компонентов, сочетая его с мощью JavaScript.
168 |
169 | В этой части мы будем генерировать некоторый текст с помощью React и JSX.
170 |
171 | Для начала, давайте установим React и ReactDOM:
172 |
173 | - Запустите `yarn add react react-dom`
174 |
175 | Переименуйте файл `src/client/index.js` в `src/client/index.jsx` и напишите в нем следующий React код:
176 |
177 | ```js
178 | // @flow
179 |
180 | import 'babel-polyfill'
181 |
182 | import React from 'react'
183 | import ReactDOM from 'react-dom'
184 |
185 | import App from './app'
186 | import { APP_CONTAINER_SELECTOR } from '../shared/config'
187 |
188 | ReactDOM.render(, document.querySelector(APP_CONTAINER_SELECTOR))
189 | ```
190 |
191 | - Создайте фпйл `src/client/app.jsx` содержащий:
192 |
193 | ```js
194 | // @flow
195 |
196 | import React from 'react'
197 |
198 | const App = () =>
Hello React!
199 |
200 | export default App
201 | ```
202 |
203 | Поскольку мы тут используем синтаксис JSX, нам нужно чтобы Babel трансформировал его с помощью пресета `babel-preset-react`. Заодно, мы добавим плагин для Babel `flow-react-proptypes`, который автоматически генерирует PropTypes из аннотаций Flow для React компонентов.
204 |
205 |
206 | - Запустите `yarn add --dev babel-preset-react babel-plugin-flow-react-proptypes` и отредактируйте файл `.babelrc` так:
207 |
208 | ```json
209 | {
210 | "presets": [
211 | "env",
212 | "flow",
213 | "react"
214 | ],
215 | "plugins": [
216 | "flow-react-proptypes"
217 | ]
218 | }
219 | ```
220 |
221 | 🏁 Запустите `yarn start` и `yarn dev:wds`, откройте `http://localhost:8000`. Вы должны увидеть "Hello React!".
222 |
223 | Теперь попробуйте изменить текст в `src/client/app.jsx` на какой-нибудь другой. Webpack Dev Server должен автоматически перезагрузить страницу, что довольно изящно, но мы собираемся сделать даже еще лучше.
224 |
225 | ## Hot Module Replacement
226 |
227 | > 💡 **[Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/)** (*HMR*) - мощная способность Webpack заменять модули на лету без перезагрузки целой страницы.
228 |
229 | Чтобы заставить HMR работать с React, нам потребуется немного поднастроить.
230 |
231 | - Запустите `yarn add react-hot-loader@next`
232 |
233 | - Отредактируйте `webpack.config.babel.js` так:
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 | },
247 | plugins: [
248 | new webpack.optimize.OccurrenceOrderPlugin(),
249 | new webpack.HotModuleReplacementPlugin(),
250 | new webpack.NamedModulesPlugin(),
251 | new webpack.NoEmitOnErrorsPlugin(),
252 | ],
253 | ```
254 |
255 | - Отредактируйте файл `src/client/index.jsx`:
256 |
257 | ```js
258 | // @flow
259 |
260 | import 'babel-polyfill'
261 |
262 | import React from 'react'
263 | import ReactDOM from 'react-dom'
264 | import { AppContainer } from 'react-hot-loader'
265 |
266 | import App from './app'
267 | import { APP_CONTAINER_SELECTOR } from '../shared/config'
268 |
269 | const rootEl = document.querySelector(APP_CONTAINER_SELECTOR)
270 |
271 | const wrapApp = AppComponent =>
272 |
273 |
274 |
275 |
276 | ReactDOM.render(wrapApp(App), rootEl)
277 |
278 | if (module.hot) {
279 | // flow-disable-next-line
280 | module.hot.accept('./app', () => {
281 | // eslint-disable-next-line global-require
282 | const NextApp = require('./app').default
283 | ReactDOM.render(wrapApp(NextApp), rootEl)
284 | })
285 | }
286 | ```
287 |
288 | Нам нужно, чтобы `App` был дочерним по отношению к `AppContainer` из `react-hot-loader`, и также нам требуется добавить `require` для получения следующей версии `App` при hot-reloading (горячей перезагрузке). Чтобы сделать этот процесс ясным и следовать принципу DRY, мы создали небольшую функию `wrapApp`, которую используем в обоих местах, где требуется генерировать `App`. Вы можете перенести `eslint-disable global-require` в начало файла чтобы сделать его более читабельным.
289 |
290 | 🏁 Перезапустите процесс `yarn dev:wds`, если онивсе еще запущен. Откройте `localhost:8000`. В консоли, вы должы увидеть некоторые логи об HMR. Возьмите и измените что-нибудь в `src/client/app.jsx`, и ваши изменения будут отражены в браузере через несколько секунд без полной перезагрузки страницы.
291 |
292 | Следующий раздел: [05 - Redux, Immutable, Fetch](05-redux-immutable-fetch.md#readme)
293 |
294 | Назад в [предыдущий раздел](03-express-nodemon-pm2_ru.md#readme) или [содержание](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
--------------------------------------------------------------------------------
/tutorial/05-redux-immutable-fetch.md:
--------------------------------------------------------------------------------
1 | # 05 - Redux, Immutable, and Fetch
2 |
3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/05-redux-immutable-fetch).
4 |
5 | In this chapter we will hook up React and Redux to make a very simple app. The app will consist of a message and a button. The message changes when the user clicks the button.
6 |
7 | Before we start, here is a very quick introduction to ImmutableJS, which is completely unrelated to React and Redux, but will be used in this chapter.
8 |
9 | ## ImmutableJS
10 |
11 | > 💡 **[ImmutableJS](https://facebook.github.io/immutable-js/)** (or just Immutable) is a library by Facebook to manipulate immutable collections, like lists and maps. Any change made on an immutable object returns a new object without mutating the original object.
12 |
13 | For instance, instead of doing:
14 |
15 | ```js
16 | const obj = { a: 1 }
17 | obj.a = 2 // Mutates `obj`
18 | ```
19 |
20 | You would do:
21 |
22 | ```js
23 | const obj = Immutable.Map({ a: 1 })
24 | obj.set('a', 2) // Returns a new object without mutating `obj`
25 | ```
26 |
27 | This approach follows the **functional programming** paradigm, which works really well with Redux.
28 |
29 | When creating immutable collections, a very convenient method is `Immutable.fromJS()`, which takes any regular JS object or array and returns a deeply immutable version of it:
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 | - Run `yarn add immutable@4.0.0-rc.2`
48 |
49 | ## Redux
50 |
51 | > 💡 **[Redux](http://redux.js.org/)** is a library to handle the lifecycle of your application. It creates a *store*, which is the single source of truth of the state of your app at any given time.
52 |
53 | Let's start with the easy part, declaring our Redux actions:
54 |
55 | - Run `yarn add redux redux-actions`
56 |
57 | - Create a `src/client/action/hello.js` file containing:
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 | This file exposes an *action*, `SAY_HELLO`, and its *action creator*, `sayHello`, which is a function. We use [`redux-actions`](https://github.com/acdlite/redux-actions) to reduce the boilerplate associated with Redux actions. `redux-actions` implement the [Flux Standard Action](https://github.com/acdlite/flux-standard-action) model, which makes *action creators* return objects with the `type` and `payload` attributes.
70 |
71 | - Create a `src/client/reducer/hello.js` file containing:
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 | In this file we initialize the state of our reducer with an Immutable Map containing one property, `message`, set to `Initial reducer message`. The `helloReducer` handles `SAY_HELLO` actions by simply setting the new `message` with the action payload. The Flow annotation for `action` destructures it into a `type` and a `payload`. The `payload` can be of `any` type. It looks funky if you've never seen this before, but it remains pretty understandable. For the type of `state`, we use the `import type` Flow instruction to get the return type of `fromJS`. We rename it to `Immut` for clarity, because `state: fromJS` would be pretty confusing. The `import type` line will get stripped out like any other Flow annotation. Note the usage of `Immutable.fromJS()` and `set()` as seen before.
98 |
99 | ## React-Redux
100 |
101 | > 💡 **[react-redux](https://github.com/reactjs/react-redux)** *connects* a Redux store with React components. With `react-redux`, when the Redux store changes, React components get automatically updated. They can also fire Redux actions.
102 |
103 | - Run `yarn add react-redux`
104 |
105 | In this section we are going to create *Components* and *Containers*.
106 |
107 | **Components** are *dumb* React components, in a sense that they don't know anything about the Redux state. **Containers** are *smart* components that know about the state and that we are going to *connect* to our dumb components.
108 |
109 | - Create a `src/client/component/button.jsx` file containing:
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 | **Note**: You can see a case of Flow *type alias* here. We define the `Props` type before annotating our component's destructured `props` with it.
128 |
129 | - Create a `src/client/component/message.jsx` file containing:
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 | These are examples of *dumb* components. They are logic-less, and just show whatever they are asked to show via React **props**. The main difference between `button.jsx` and `message.jsx` is that `Button` contains a reference to an action dispatcher in its props, where `Message` just contains some data to show.
147 |
148 | Again, *components* don't know anything about Redux **actions** or the **state** of our app, which is why we are going to create smart **containers** that will feed the proper action dispatchers and data to these 2 dumb components.
149 |
150 | - Create a `src/client/container/hello-button.js` file containing:
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 | This container hooks up the `Button` component with the `sayHello` action and Redux's `dispatch` method.
172 |
173 | - Create a `src/client/container/message.js` file containing:
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 |
189 | This container hooks up the Redux's app state with the `Message` component. When the state changes, `Message` will now automatically re-render with the proper `message` prop. These connections are done via the `connect` function of `react-redux`.
190 |
191 | - Update your `src/client/app.jsx` file like so:
192 |
193 | ```js
194 | // @flow
195 |
196 | import React from 'react'
197 | import HelloButton from './container/hello-button'
198 | import Message from './container/message'
199 | import { APP_NAME } from '../shared/config'
200 |
201 | const App = () =>
202 |
203 |
{APP_NAME}
204 |
205 |
206 |
207 |
208 | export default App
209 | ```
210 |
211 | We still haven't initialized the Redux store and haven't put the 2 containers anywhere in our app yet:
212 |
213 | - Edit `src/client/index.jsx` like so:
214 |
215 | ```js
216 | // @flow
217 |
218 | import 'babel-polyfill'
219 |
220 | import React from 'react'
221 | import ReactDOM from 'react-dom'
222 | import { AppContainer } from 'react-hot-loader'
223 | import { Provider } from 'react-redux'
224 | import { createStore, combineReducers } from 'redux'
225 |
226 | import App from './app'
227 | import helloReducer from './reducer/hello'
228 | import { APP_CONTAINER_SELECTOR } from '../shared/config'
229 | import { isProd } from '../shared/util'
230 |
231 | const store = createStore(combineReducers({ hello: helloReducer }),
232 | // eslint-disable-next-line no-underscore-dangle
233 | isProd ? undefined : window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
234 |
235 | const rootEl = document.querySelector(APP_CONTAINER_SELECTOR)
236 |
237 | const wrapApp = (AppComponent, reduxStore) =>
238 |
239 |
240 |
241 |
242 |
243 |
244 | ReactDOM.render(wrapApp(App, store), rootEl)
245 |
246 | if (module.hot) {
247 | // flow-disable-next-line
248 | module.hot.accept('./app', () => {
249 | // eslint-disable-next-line global-require
250 | const NextApp = require('./app').default
251 | ReactDOM.render(wrapApp(NextApp, store), rootEl)
252 | })
253 | }
254 | ```
255 |
256 | Let's take a moment to review this. First, we create a *store* with `createStore`. Stores are created by passing reducers to them. Here we only have one reducer, but for the sake of future scalability, we use `combineReducers` to group all of our reducers together. The last weird parameter of `createStore` is something to hook up Redux to browser [Devtools](https://github.com/zalmoxisus/redux-devtools-extension), which are incredibly useful when debugging. Since ESLint will complain about the underscores in `__REDUX_DEVTOOLS_EXTENSION__`, we disable this ESLint rule. Next, we conveniently wrap our entire app inside `react-redux`'s `Provider` component thanks to our `wrapApp` function, and pass our store to it.
257 |
258 | 🏁 You can now run `yarn start` and `yarn dev:wds` and hit `http://localhost:8000`. You should see "Initial reducer message" and a button. When you click the button, the message should change to "Hello!". If you installed the Redux Devtools in your browser, you should see the app state change over time as you click on the button.
259 |
260 | Congratulations, we finally made an app that does something! Okay it's not a *super* impressive from the outside, but we all know that it is powered by one badass stack under the hood.
261 |
262 | ## Extending our app with an asynchronous call
263 |
264 | We are now going to add a second button to our app, which will trigger an AJAX call to retrieve a message from the server. For the sake of demonstration, this call will also send some data, the hard-coded number `1234`.
265 |
266 | ### The server endpoint
267 |
268 | - Create a `src/shared/routes.js` file containing:
269 |
270 | ```js
271 | // @flow
272 |
273 | // eslint-disable-next-line import/prefer-default-export
274 | export const helloEndpointRoute = (num: ?number) => `/ajax/hello/${num || ':num'}`
275 | ```
276 |
277 | This function is a little helper to produce the following:
278 |
279 | ```js
280 | helloEndpointRoute() // -> '/ajax/hello/:num' (for Express)
281 | helloEndpointRoute(1234) // -> '/ajax/hello/1234' (for the actual call)
282 | ```
283 |
284 | Let's actually create a test real quick to make sure this thing works well.
285 |
286 | - Create a `src/shared/routes.test.js` containing:
287 |
288 | ```js
289 | import { helloEndpointRoute } from './routes'
290 |
291 | test('helloEndpointRoute', () => {
292 | expect(helloEndpointRoute()).toBe('/ajax/hello/:num')
293 | expect(helloEndpointRoute(123)).toBe('/ajax/hello/123')
294 | })
295 | ```
296 |
297 | - Run `yarn test` and it should pass successfully.
298 |
299 | - In `src/server/index.js`, add the following:
300 |
301 | ```js
302 | import { helloEndpointRoute } from '../shared/routes'
303 |
304 | // [under app.get('/')...]
305 |
306 | app.get(helloEndpointRoute(), (req, res) => {
307 | res.json({ serverMessage: `Hello from the server! (received ${req.params.num})` })
308 | })
309 | ```
310 |
311 | ### New containers
312 |
313 | - Create a `src/client/container/hello-async-button.js` file containing:
314 |
315 | ```js
316 | // @flow
317 |
318 | import { connect } from 'react-redux'
319 |
320 | import { sayHelloAsync } from '../action/hello'
321 | import Button from '../component/button'
322 |
323 | const mapStateToProps = () => ({
324 | label: 'Say hello asynchronously and send 1234',
325 | })
326 |
327 | const mapDispatchToProps = dispatch => ({
328 | handleClick: () => { dispatch(sayHelloAsync(1234)) },
329 | })
330 |
331 | export default connect(mapStateToProps, mapDispatchToProps)(Button)
332 | ```
333 |
334 | In order to demonstrate how you would pass a parameter to your asynchronous call and to keep things simple, I am hard-coding a `1234` value here. This value would typically come from a form field filled by the user.
335 |
336 | - Create a `src/client/container/message-async.js` file containing:
337 |
338 | ```js
339 | // @flow
340 |
341 | import { connect } from 'react-redux'
342 |
343 | import MessageAsync from '../component/message'
344 |
345 | const mapStateToProps = state => ({
346 | message: state.hello.get('messageAsync'),
347 | })
348 |
349 | export default connect(mapStateToProps)(MessageAsync)
350 | ```
351 |
352 | You can see that in this container, we are referring to a `messageAsync` property, which we're going to add to our reducer soon.
353 |
354 | What we need now is to create the `sayHelloAsync` action.
355 |
356 | ### Fetch
357 |
358 | > 💡 **[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)** is a standardized JavaScript function to make asynchronous calls inspired by jQuery's AJAX methods.
359 |
360 | We are going to use `fetch` to make calls to the server from the client. `fetch` is not supported by all browsers yet, so we are going to need a polyfill. `isomorphic-fetch` is a polyfill that makes it work cross-browsers and in Node too!
361 |
362 | - Run `yarn add isomorphic-fetch`
363 |
364 | Since we're using `eslint-plugin-compat`, we need to indicate that we are using a polyfill for `fetch` to not get warnings from using it.
365 |
366 | - Add the following to your `.eslintrc.json` file:
367 |
368 | ```json
369 | "settings": {
370 | "polyfills": ["fetch"]
371 | },
372 | ```
373 |
374 | ### 3 asynchronous actions
375 |
376 | `sayHelloAsync` is not going to be a regular action. Asynchronous actions are usually split into 3 actions, which trigger 3 different states: a *request* action (or "loading"), a *success* action, and a *failure* action.
377 |
378 | - Edit `src/client/action/hello.js` like so:
379 |
380 | ```js
381 | // @flow
382 |
383 | import 'isomorphic-fetch'
384 |
385 | import { createAction } from 'redux-actions'
386 | import { helloEndpointRoute } from '../../shared/routes'
387 |
388 | export const SAY_HELLO = 'SAY_HELLO'
389 | export const SAY_HELLO_ASYNC_REQUEST = 'SAY_HELLO_ASYNC_REQUEST'
390 | export const SAY_HELLO_ASYNC_SUCCESS = 'SAY_HELLO_ASYNC_SUCCESS'
391 | export const SAY_HELLO_ASYNC_FAILURE = 'SAY_HELLO_ASYNC_FAILURE'
392 |
393 | export const sayHello = createAction(SAY_HELLO)
394 | export const sayHelloAsyncRequest = createAction(SAY_HELLO_ASYNC_REQUEST)
395 | export const sayHelloAsyncSuccess = createAction(SAY_HELLO_ASYNC_SUCCESS)
396 | export const sayHelloAsyncFailure = createAction(SAY_HELLO_ASYNC_FAILURE)
397 |
398 | export const sayHelloAsync = (num: number) => (dispatch: Function) => {
399 | dispatch(sayHelloAsyncRequest())
400 | return fetch(helloEndpointRoute(num), { method: 'GET' })
401 | .then((res) => {
402 | if (!res.ok) throw Error(res.statusText)
403 | return res.json()
404 | })
405 | .then((data) => {
406 | if (!data.serverMessage) throw Error('No message received')
407 | dispatch(sayHelloAsyncSuccess(data.serverMessage))
408 | })
409 | .catch(() => {
410 | dispatch(sayHelloAsyncFailure())
411 | })
412 | }
413 | ```
414 |
415 | Instead of returning an action, `sayHelloAsync` returns a function which launches the `fetch` call. `fetch` returns a `Promise`, which we use to *dispatch* different actions depending on the current state of our asynchronous call.
416 |
417 | ### 3 asynchronous action handlers
418 |
419 | Let's handle these different actions in `src/client/reducer/hello.js`:
420 |
421 | ```js
422 | // @flow
423 |
424 | import Immutable from 'immutable'
425 | import type { fromJS as Immut } from 'immutable'
426 |
427 | import {
428 | SAY_HELLO,
429 | SAY_HELLO_ASYNC_REQUEST,
430 | SAY_HELLO_ASYNC_SUCCESS,
431 | SAY_HELLO_ASYNC_FAILURE,
432 | } from '../action/hello'
433 |
434 | const initialState = Immutable.fromJS({
435 | message: 'Initial reducer message',
436 | messageAsync: 'Initial reducer message for async call',
437 | })
438 |
439 | const helloReducer = (state: Immut = initialState, action: { type: string, payload: any }) => {
440 | switch (action.type) {
441 | case SAY_HELLO:
442 | return state.set('message', action.payload)
443 | case SAY_HELLO_ASYNC_REQUEST:
444 | return state.set('messageAsync', 'Loading...')
445 | case SAY_HELLO_ASYNC_SUCCESS:
446 | return state.set('messageAsync', action.payload)
447 | case SAY_HELLO_ASYNC_FAILURE:
448 | return state.set('messageAsync', 'No message received, please check your connection')
449 | default:
450 | return state
451 | }
452 | }
453 |
454 | export default helloReducer
455 | ```
456 |
457 | We added a new field to our store, `messageAsync`, and we update it with different messages depending on the action we receive. During `SAY_HELLO_ASYNC_REQUEST`, we show `Loading...`. `SAY_HELLO_ASYNC_SUCCESS` updates `messageAsync` similarly to how `SAY_HELLO` updates `message`. `SAY_HELLO_ASYNC_FAILURE` gives an error message.
458 |
459 | ### Redux-thunk
460 |
461 | In `src/client/action/hello.js`, we made `sayHelloAsync`, an action creator that returns a function. This is actually not a feature that is natively supported by Redux. In order to perform these async actions, we need to extend Redux's functionality with the `redux-thunk` *middleware*.
462 |
463 | - Run `yarn add redux-thunk`
464 |
465 | - Update your `src/client/index.jsx` file like so:
466 |
467 | ```js
468 | // @flow
469 |
470 | import 'babel-polyfill'
471 |
472 | import React from 'react'
473 | import ReactDOM from 'react-dom'
474 | import { AppContainer } from 'react-hot-loader'
475 | import { Provider } from 'react-redux'
476 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
477 | import thunkMiddleware from 'redux-thunk'
478 |
479 | import App from './app'
480 | import helloReducer from './reducer/hello'
481 | import { APP_CONTAINER_SELECTOR } from '../shared/config'
482 | import { isProd } from '../shared/util'
483 |
484 | // eslint-disable-next-line no-underscore-dangle
485 | const composeEnhancers = (isProd ? null : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
486 |
487 | const store = createStore(combineReducers({ hello: helloReducer }),
488 | composeEnhancers(applyMiddleware(thunkMiddleware)))
489 |
490 | const rootEl = document.querySelector(APP_CONTAINER_SELECTOR)
491 |
492 | const wrapApp = (AppComponent, reduxStore) =>
493 |
494 |
495 |
496 |
497 |
498 |
499 | ReactDOM.render(wrapApp(App, store), rootEl)
500 |
501 | if (module.hot) {
502 | // flow-disable-next-line
503 | module.hot.accept('./app', () => {
504 | // eslint-disable-next-line global-require
505 | const NextApp = require('./app').default
506 | ReactDOM.render(wrapApp(NextApp, store), rootEl)
507 | })
508 | }
509 | ```
510 |
511 | Here we pass `redux-thunk` to Redux's `applyMiddleware` function. In order for the Redux Devtools to keep working, we also need to use Redux's `compose` function. Don't worry too much about this part, just remember that we enhance Redux with `redux-thunk`.
512 |
513 | - Update `src/client/app.jsx` like so:
514 |
515 | ```js
516 | // @flow
517 |
518 | import React from 'react'
519 | import HelloButton from './container/hello-button'
520 | import HelloAsyncButton from './container/hello-async-button'
521 | import Message from './container/message'
522 | import MessageAsync from './container/message-async'
523 | import { APP_NAME } from '../shared/config'
524 |
525 | const App = () =>
526 |
527 |
{APP_NAME}
528 |
529 |
530 |
531 |
532 |
533 |
534 | export default App
535 | ```
536 |
537 | 🏁 Run `yarn start` and `yarn dev:wds` and you should now be able to click the "Say hello asynchronously and send 1234" button and retrieve a corresponding message from the server! Since you're working locally, the call is instantaneous, but if you open the Redux Devtools, you will notice that each click triggers both `SAY_HELLO_ASYNC_REQUEST` and `SAY_HELLO_ASYNC_SUCCESS`, making the message go through the intermediate `Loading...` state as expected.
538 |
539 | You can congratulate yourself, that was an intense section! Let's wrap it up with some testing.
540 |
541 | ## Testing
542 |
543 | In this section, we are going to test our actions and reducer. Let's start with the actions.
544 |
545 | In order to isolate the logic that is specific to `action/hello.js` we are going to need to *mock* things that don't concern it, and also mock that AJAX `fetch` request which should not trigger an actual AJAX in our tests.
546 |
547 | - Run `yarn add --dev redux-mock-store fetch-mock`
548 |
549 | - Create a `src/client/action/hello.test.js` file containing:
550 |
551 | ```js
552 | import fetchMock from 'fetch-mock'
553 | import configureMockStore from 'redux-mock-store'
554 | import thunkMiddleware from 'redux-thunk'
555 |
556 | import {
557 | sayHelloAsync,
558 | sayHelloAsyncRequest,
559 | sayHelloAsyncSuccess,
560 | sayHelloAsyncFailure,
561 | } from './hello'
562 |
563 | import { helloEndpointRoute } from '../../shared/routes'
564 |
565 | const mockStore = configureMockStore([thunkMiddleware])
566 |
567 | afterEach(() => {
568 | fetchMock.restore()
569 | })
570 |
571 | test('sayHelloAsync success', () => {
572 | fetchMock.get(helloEndpointRoute(666), { serverMessage: 'Async hello success' })
573 | const store = mockStore()
574 | return store.dispatch(sayHelloAsync(666))
575 | .then(() => {
576 | expect(store.getActions()).toEqual([
577 | sayHelloAsyncRequest(),
578 | sayHelloAsyncSuccess('Async hello success'),
579 | ])
580 | })
581 | })
582 |
583 | test('sayHelloAsync 404', () => {
584 | fetchMock.get(helloEndpointRoute(666), 404)
585 | const store = mockStore()
586 | return store.dispatch(sayHelloAsync(666))
587 | .then(() => {
588 | expect(store.getActions()).toEqual([
589 | sayHelloAsyncRequest(),
590 | sayHelloAsyncFailure(),
591 | ])
592 | })
593 | })
594 |
595 | test('sayHelloAsync data error', () => {
596 | fetchMock.get(helloEndpointRoute(666), {})
597 | const store = mockStore()
598 | return store.dispatch(sayHelloAsync(666))
599 | .then(() => {
600 | expect(store.getActions()).toEqual([
601 | sayHelloAsyncRequest(),
602 | sayHelloAsyncFailure(),
603 | ])
604 | })
605 | })
606 | ```
607 |
608 | Alright, Let's look at what's happening here. First we mock the Redux store using `const mockStore = configureMockStore([thunkMiddleware])`. By doing this we can dispatch actions without them triggering any reducer logic. For each test, we mock `fetch` using `fetchMock.get()` and make it return whatever we want. What we actually test using `expect()` is which series of actions have been dispatched by the store, thanks to the `store.getActions()` function from `redux-mock-store`. After each test we restore the normal behavior of `fetch` with `fetchMock.restore()`.
609 |
610 | Let's now test our reducer, which is much easier.
611 |
612 | - Create a `src/client/reducer/hello.test.js` file containing:
613 |
614 | ```js
615 | import {
616 | sayHello,
617 | sayHelloAsyncRequest,
618 | sayHelloAsyncSuccess,
619 | sayHelloAsyncFailure,
620 | } from '../action/hello'
621 |
622 | import helloReducer from './hello'
623 |
624 | let helloState
625 |
626 | beforeEach(() => {
627 | helloState = helloReducer(undefined, {})
628 | })
629 |
630 | test('handle default', () => {
631 | expect(helloState.get('message')).toBe('Initial reducer message')
632 | expect(helloState.get('messageAsync')).toBe('Initial reducer message for async call')
633 | })
634 |
635 | test('handle SAY_HELLO', () => {
636 | helloState = helloReducer(helloState, sayHello('Test'))
637 | expect(helloState.get('message')).toBe('Test')
638 | })
639 |
640 | test('handle SAY_HELLO_ASYNC_REQUEST', () => {
641 | helloState = helloReducer(helloState, sayHelloAsyncRequest())
642 | expect(helloState.get('messageAsync')).toBe('Loading...')
643 | })
644 |
645 | test('handle SAY_HELLO_ASYNC_SUCCESS', () => {
646 | helloState = helloReducer(helloState, sayHelloAsyncSuccess('Test async'))
647 | expect(helloState.get('messageAsync')).toBe('Test async')
648 | })
649 |
650 | test('handle SAY_HELLO_ASYNC_FAILURE', () => {
651 | helloState = helloReducer(helloState, sayHelloAsyncFailure())
652 | expect(helloState.get('messageAsync')).toBe('No message received, please check your connection')
653 | })
654 | ```
655 |
656 | Before each test, we initialize `helloState` with the default result of our reducer (the `default` case of our `switch` statement in the reducer, which returns `initialState`). The tests are then very explicit, we just make sure the reducer updates `message` and `messageAsync` correctly depending on which action it received.
657 |
658 | 🏁 Run `yarn test`. It should be all green.
659 |
660 | Next section: [06 - React Router, Server-Side Rendering, Helmet](06-react-router-ssr-helmet.md#readme)
661 |
662 | Back to the [previous section](04-webpack-react-hmr.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
663 |
--------------------------------------------------------------------------------
/tutorial/06-react-router-ssr-helmet.md:
--------------------------------------------------------------------------------
1 | # 06 - React Router, Server-Side Rendering, and Helmet
2 |
3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/06-react-router-ssr-helmet).
4 |
5 | In this chapter we are going to create different pages for our app and make it possible to navigate between them.
6 |
7 | ## React Router
8 |
9 | > 💡 **[React Router](https://reacttraining.com/react-router/)** is a library to navigate between pages in your React app. It can be used on both the client and the server.
10 |
11 | React Router has received a major update with its v4 release which is still in beta. Since I want this tutorial to be future-proof, we'll be using v4.
12 |
13 | - Run `yarn add react-router@next react-router-dom@next`
14 |
15 | On the client side, we first need to wrap our app inside a `BrowserRouter` component.
16 |
17 | - Update your `src/client/index.jsx` like so:
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 | Our app will have 4 pages:
36 |
37 | - A Home page.
38 | - A Hello page showing a button and message for the synchronous action.
39 | - A Hello Async page showing a button and message for the asynchronous action.
40 | - A 404 "Not Found" page.
41 |
42 | - Create a `src/client/component/page/home.jsx` file containing:
43 |
44 | ```js
45 | // @flow
46 |
47 | import React from 'react'
48 |
49 | const HomePage = () =>
190 |
191 | export default App
192 | ```
193 |
194 | 🏁 Run `yarn start` and `yarn dev:wds`. Open `http://localhost:8000`, and click on the links to navigate between our different pages. You should see the URL changing dynamically. Switch between different pages and use the back button of your browser to see that the browsing history is working as expected.
195 |
196 | Now, let's say you navigated to `http://localhost:8000/hello` this way. Hit the refresh button. You now get a 404, because our Express server only responds to `/`. As you navigated between pages, you were actually only doing it on the client-side. Let's add server-side rendering to the mix to get the expected behavior.
197 |
198 | ## Server-Side Rendering
199 |
200 | > 💡 **Server-Side Rendering** means rendering your app at the initial load of the page instead of relying on JavaScript to render it in the client's browser.
201 |
202 | SSR is essential for SEO and provides a better user experience by showing the app to your users right away.
203 |
204 | The first thing we're going to do here is to migrate most of our client code to the shared / isomorphic / universal part of our codebase, since the server is now going to render our React app too.
205 |
206 | ### The big migration to `shared`
207 |
208 | - Move all the files located under `client` to `shared`, except `src/client/index.jsx`.
209 |
210 | We have to adjust a whole bunch of imports:
211 |
212 | - In `src/client/index.jsx`, replace the 3 occurrences of `'./app'` by `'../shared/app'`, and `'./reducer/hello'` by `'../shared/reducer/hello'`
213 |
214 | - In `src/shared/app.jsx`, replace `'../shared/routes'` by `'./routes'` and `'../shared/config'` by `'./config'`
215 |
216 | - In `src/shared/component/nav.jsx`, replace `'../../shared/routes'` by `'../routes'`
217 |
218 | ### Server changes
219 |
220 | - Create a `src/server/routing.js` file containing:
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 |
275 | This file is where we deal with requests and responses. The calls to business logic are externalized to a different `controller` module.
276 |
277 | **Note**: You will find a lot of React Router examples using `*` as the route on the server, leaving the entire routing handling to React Router. Since all requests go through the same function, that makes it inconvenient to implement MVC-style pages. Instead of doing that, we're here explicitly declaring the routes and their dedicated responses, to be able to fetch data from the database and pass it to a given page easily.
278 |
279 | - Create a `src/server/controller.js` file containing:
280 |
281 | ```js
282 | // @flow
283 |
284 | export const homePage = () => null
285 |
286 | export const helloPage = () => ({
287 | hello: { message: 'Server-side preloaded message' },
288 | })
289 |
290 | export const helloAsyncPage = () => ({
291 | hello: { messageAsync: 'Server-side preloaded message for async page' },
292 | })
293 |
294 | export const helloEndpoint = (num: number) => ({
295 | serverMessage: `Hello from the server! (received ${num})`,
296 | })
297 | ```
298 |
299 | Here is our controller. It would typically make business logic and database calls, but in our case we just hard-code some results. Those results are passed back to the `routing` module to be used to initialize our server-side Redux store.
300 |
301 | - Create a `src/server/init-store.js` file containing:
302 |
303 | ```js
304 | // @flow
305 |
306 | import Immutable from 'immutable'
307 | import { createStore, combineReducers, applyMiddleware } from 'redux'
308 | import thunkMiddleware from 'redux-thunk'
309 |
310 | import helloReducer from '../shared/reducer/hello'
311 |
312 | const initStore = (plainPartialState: ?Object) => {
313 | const preloadedState = plainPartialState ? {} : undefined
314 |
315 | if (plainPartialState && plainPartialState.hello) {
316 | // flow-disable-next-line
317 | preloadedState.hello = helloReducer(undefined, {})
318 | .merge(Immutable.fromJS(plainPartialState.hello))
319 | }
320 |
321 | return createStore(combineReducers({ hello: helloReducer }),
322 | preloadedState, applyMiddleware(thunkMiddleware))
323 | }
324 |
325 | export default initStore
326 | ```
327 |
328 | The only thing we do here, besides calling `createStore` and applying middleware, is to merge the plain JS object we received from the `controller` into a default Redux state containing Immutable objects.
329 |
330 | - Edit `src/server/index.js` like so:
331 |
332 | ```js
333 | // @flow
334 |
335 | import compression from 'compression'
336 | import express from 'express'
337 |
338 | import routing from './routing'
339 | import { WEB_PORT, STATIC_PATH } from '../shared/config'
340 | import { isProd } from '../shared/util'
341 |
342 | const app = express()
343 |
344 | app.use(compression())
345 | app.use(STATIC_PATH, express.static('dist'))
346 | app.use(STATIC_PATH, express.static('public'))
347 |
348 | routing(app)
349 |
350 | app.listen(WEB_PORT, () => {
351 | // eslint-disable-next-line no-console
352 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' :
353 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`)
354 | })
355 | ```
356 |
357 | Nothing special here, we just call `routing(app)` instead of implementing routing in this file.
358 |
359 | - Rename `src/server/render-app.js` to `src/server/render-app.jsx` and edit it like so:
360 |
361 | ```js
362 | // @flow
363 |
364 | import React from 'react'
365 | import ReactDOMServer from 'react-dom/server'
366 | import { Provider } from 'react-redux'
367 | import { StaticRouter } from 'react-router'
368 |
369 | import initStore from './init-store'
370 | import App from './../shared/app'
371 | import { APP_CONTAINER_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config'
372 | import { isProd } from '../shared/util'
373 |
374 | const renderApp = (location: string, plainPartialState: ?Object, routerContext: ?Object = {}) => {
375 | const store = initStore(plainPartialState)
376 | const appHtml = ReactDOMServer.renderToString(
377 |
378 |
379 |
380 |
381 | )
382 |
383 | return (
384 | `
385 |
386 |
387 | FIX ME
388 |
389 |
390 |
391 |
${appHtml}
392 |
395 |
396 |
397 | `
398 | )
399 | }
400 |
401 | export default renderApp
402 | ```
403 |
404 | `ReactDOMServer.renderToString` is where the magic happens. React will evaluate our entire `shared` `App`, and return a plain string of HTML elements. `Provider` works the same as on the client, but on the server, we wrap our app inside `StaticRouter` instead of `BrowserRouter`. In order to pass the Redux store from the server to the client, we pass it to `window.__PRELOADED_STATE__` which is just some arbitrary variable name.
405 |
406 | **Note**: Immutable objects implement the `toJSON()` method which means you can use `JSON.stringify` to turn them into plain JSON strings.
407 |
408 | - Edit `src/client/index.jsx` to use that preloaded state:
409 |
410 | ```js
411 | import Immutable from 'immutable'
412 | // [...]
413 |
414 | /* eslint-disable no-underscore-dangle */
415 | const composeEnhancers = (isProd ? null : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
416 | const preloadedState = window.__PRELOADED_STATE__
417 | /* eslint-enable no-underscore-dangle */
418 |
419 | const store = createStore(combineReducers(
420 | { hello: helloReducer }),
421 | { hello: Immutable.fromJS(preloadedState.hello) },
422 | composeEnhancers(applyMiddleware(thunkMiddleware)))
423 | ```
424 |
425 | Here with feed our client-side store with the `preloadedState` that was received from the server.
426 |
427 | 🏁 You can now run `yarn start` and `yarn dev:wds` and navigate between pages. Refreshing the page on `/hello`, `/hello-async`, and `/404` (or any other URI), should now work correctly. Notice how the `message` and `messageAsync` vary depending on if you navigated to that page from the client or if it comes from server-side rendering.
428 |
429 | ### React Helmet
430 |
431 | > 💡 **[React Helmet](https://github.com/nfl/react-helmet)**: A library to inject content to the `head` of a React app, on both the client and the server.
432 |
433 | I purposely made you write `FIX ME` in the title to highlight the fact that even though we are doing server-side rendering, we currently do not fill the `title` tag properly (or any of the tags in `head` that vary depending on the page you're on).
434 |
435 | - Run `yarn add react-helmet`
436 |
437 | - Edit `src/server/render-app.jsx` like so:
438 |
439 | ```js
440 | import Helmet from 'react-helmet'
441 | // [...]
442 | const renderApp = (/* [...] */) => {
443 |
444 | const appHtml = ReactDOMServer.renderToString(/* [...] */)
445 | const head = Helmet.rewind()
446 |
447 | return (
448 | `
449 |
450 |
451 | ${head.title}
452 | ${head.meta}
453 |
454 |
455 | [...]
456 | `
457 | )
458 | }
459 | ```
460 |
461 | React Helmet uses [react-side-effect](https://github.com/gaearon/react-side-effect)'s `rewind` to pull out some data from the rendering of our app, which will soon contain some `` components. Those `` components are where we set the `title` and other `head` details for each page.
462 |
463 | - Edit `src/shared/app.jsx` like so:
464 |
465 | ```js
466 | import Helmet from 'react-helmet'
467 | // [...]
468 | const App = () =>
469 |
582 |
583 | export default NotFoundPage
584 | ```
585 |
586 | The `` component doesn't actually render anything, it just injects content in the `head` of your document and exposes the same data to the server.
587 |
588 | 🏁 Run `yarn start` and `yarn dev:wds` and navigate between pages. The title on your tab should change when you navigate, and it should also stay the same when you refresh the page. Show the source of the page to see how React Helmet sets the `title` and `meta` tags even for server-side rendering.
589 |
590 | Next section: [07 - Socket.IO](07-socket-io.md#readme)
591 |
592 | Back to the [previous section](05-redux-immutable-fetch.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
593 |
--------------------------------------------------------------------------------
/tutorial/06-react-router-ssr-helmet_ru.md:
--------------------------------------------------------------------------------
1 | # 06 - React Router, Server-Side Rendering, and Helmet
2 |
3 | Код для этой главы доступен [здесь](https://github.com/verekia/js-stack-walkthrough/tree/master/06-react-router-ssr-helmet).
4 |
5 | В этой части мы собираемся создать разные страницы для нашего приложения и возможность перемещаться между ними.
6 |
7 | ## React Router
8 |
9 | > 💡 **[React Router](https://reacttraining.com/react-router/)** это библиотека для навигации между страницами в вашем React приложении, его можно использовать как на клиенте так и на сервере.
10 |
11 | React Router получил большое обновление в версии 4, которое все еще находится в бете. Поскольку я хочу, чтобы этот учебник соотвестветствовал требованиям завтрашнего дня, мы будем использовать v4.
12 |
13 | - Запустите `yarn add react-router@next react-router-dom@next`
14 |
15 | На клиетнской стороне, нам вначале необходимо обернуть наше приложение внутрь `BrowserRouter` компонента.
16 |
17 | - Обновите ваш `src/client/index.jsx` согласно следующему:
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 | В нашем приложении будет 4 страницы:
36 |
37 | - Домашняя страница.
38 | - Hello page показывающая кнопку и сообщение для синхронного action (действия).
39 | - Hello Async page показывающая кнопку и сообщение для асинхронного action (действия).
40 | - 404 "Не найдена" страница.
41 |
42 | - Создайте `src/client/component/page/home.jsx` файл содержащий:
43 |
44 | ```js
45 | // @flow
46 |
47 | import React from 'react'
48 |
49 | const HomePage = () =>
190 |
191 | export default App
192 | ```
193 |
194 | 🏁 Запустите `yarn start` и `yarn dev:wds`. Откройте `http://localhost:8000`, и кликните на ссылку навигации между нашими разнымы страницами. Вы должны видеть как динамически изменяется URL. Перейдите между различными страницами и используйте кнопку назад в вашем браузере, чтобы увидеть, что в истории браузера все работает как ожидалось.
195 |
196 | Сейчас, допустим вы переходите на страницу `http://localhost:8000/hello`. Нажали кнопку обновить. Вы сейчас получите 404, потому что наш экспресс сервер отвечает только по урлу `/`. Как же вы перемещались по страницам? На самом деле вы это происходило на стороне клиента. Давайте добавим серверный рендеринг в наш проект, чтобы получить ожидаемое поведение.
197 |
198 | ## Рендеринг на стороне сервера
199 |
200 | > 💡 **Рендеринг на стороне сервера** средство для рендеринга вашего приложения на начальном этапе загрузки страницы, не полагаясь на JavaScript для рендера на клиентской стороне.
201 |
202 | Рендеринг на стороне сервера необходим для SEO и обеспечивает лучший пользовательский интерфейс, показывающий приложение сразу.
203 |
204 | Первое, что мы собираемся сделать здесь, это переписать большинство нашего клиентского кода к общей / изоморфной / универсальной части нашего кода, так как теперь наше React приложение также будет рендерится на сервере.
205 |
206 | ### Большая миграция к `общему`
207 |
208 | - Переместите все файлы из `client` в `shared`, за исключением `src/client/index.jsx`.
209 |
210 | Мы должны настроить целую кучу импортов:
211 |
212 | - В `src/client/index.jsx`, замените 3 импорта с `'./app'` на `'../shared/app'`, и `'./reducer/hello'` на `'../shared/reducer/hello'`
213 |
214 | - В `src/shared/app.jsx`, замените `'../shared/routes'` на `'./routes'` и `'../shared/config'` на `'./config'`
215 |
216 | - В `src/shared/component/nav.jsx`, замените `'../../shared/routes'` на `'../routes'`
217 |
218 | ### Серверные изменения
219 |
220 | - Создайте `src/server/routing.js` файл содержащий:
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 |
275 | В этом файле где мы имеем дело с запросами и ответами. Вызов бизнес-логики происходит через различные внешние `контроллер` модули.
276 |
277 | **Заметка**: Вы можете найти много примеров React Router примеров использующих `*` как маршрут на сервере, полагаясь на обработку маршрутизации React Router. Поскольку все запросы проходят через одну и ту же функцию, это делает его неудобным реализацию страниц в MVC-стиле. Вместо этого, мы здесь явно прописываем маршруты и их четкие ответы, чтобы иметь возможность выборки данных из базы данных и с легкостью вставить их в страницу.
278 |
279 | - Создайте `src/server/controller.js` файл содержащий:
280 |
281 | ```js
282 | // @flow
283 |
284 | export const homePage = () => null
285 |
286 | export const helloPage = () => ({
287 | hello: { message: 'Server-side preloaded message' },
288 | })
289 |
290 | export const helloAsyncPage = () => ({
291 | hello: { messageAsync: 'Server-side preloaded message for async page' },
292 | })
293 |
294 | export const helloEndpoint = (num: number) => ({
295 | serverMessage: `Hello from the server! (received ${num})`,
296 | })
297 | ```
298 |
299 | Здесь в нашем контроллере. Тут обычно выполняется бизнес логика и запросы к базе данных, но в нашем случае мы просто захардкодаем некоторые результаты. Эти результаты возвращаются назад в модуль `маршрутизации` для использования при инициализации нашего Redux store (хранилища) на стороне сервера.
300 |
301 | - Создайте `src/server/init-store.js` файл содержащий:
302 |
303 | ```js
304 | // @flow
305 |
306 | import Immutable from 'immutable'
307 | import { createStore, combineReducers, applyMiddleware } from 'redux'
308 | import thunkMiddleware from 'redux-thunk'
309 |
310 | import helloReducer from '../shared/reducer/hello'
311 |
312 | const initStore = (plainPartialState: ?Object) => {
313 | const preloadedState = plainPartialState ? {} : undefined
314 |
315 | if (plainPartialState && plainPartialState.hello) {
316 | // flow-disable-next-line
317 | preloadedState.hello = helloReducer(undefined, {})
318 | .merge(Immutable.fromJS(plainPartialState.hello))
319 | }
320 |
321 | return createStore(combineReducers({ hello: helloReducer }),
322 | preloadedState, applyMiddleware(thunkMiddleware))
323 | }
324 |
325 | export default initStore
326 | ```
327 |
328 | Единственная вещь, которую мы здесь делаем, кроме вызова `createStore (создать хранилище)` и применения middleware(промежуточной функции), это объедининие обычного JS объекта, который мы получаем из `контроллера` в стандартное Redux state (хранилище) содержащее Immutable (неизменяемые) объекты.
329 |
330 | - Отредактируйте `src/server/index.js` согласно следующему:
331 |
332 | ```js
333 | // @flow
334 |
335 | import compression from 'compression'
336 | import express from 'express'
337 |
338 | import routing from './routing'
339 | import { WEB_PORT, STATIC_PATH } from '../shared/config'
340 | import { isProd } from '../shared/util'
341 |
342 | const app = express()
343 |
344 | app.use(compression())
345 | app.use(STATIC_PATH, express.static('dist'))
346 | app.use(STATIC_PATH, express.static('public'))
347 |
348 | routing(app)
349 |
350 | app.listen(WEB_PORT, () => {
351 | // eslint-disable-next-line no-console
352 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' :
353 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`)
354 | })
355 | ```
356 |
357 | Ничего особенного здесь, мы просто вызываем `routing(app)` вместо реализации маршрутизации в этом файле.
358 |
359 | - Переименуйте `src/server/render-app.js` в `src/server/render-app.jsx` и отредактируйте согласно следующему:
360 |
361 | ```js
362 | // @flow
363 |
364 | import React from 'react'
365 | import ReactDOMServer from 'react-dom/server'
366 | import { Provider } from 'react-redux'
367 | import { StaticRouter } from 'react-router'
368 |
369 | import initStore from './init-store'
370 | import App from './../shared/app'
371 | import { APP_CONTAINER_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config'
372 | import { isProd } from '../shared/util'
373 |
374 | const renderApp = (location: string, plainPartialState: ?Object, routerContext: ?Object = {}) => {
375 | const store = initStore(plainPartialState)
376 | const appHtml = ReactDOMServer.renderToString(
377 |
378 |
379 |
380 |
381 | )
382 |
383 | return (
384 | `
385 |
386 |
387 | FIX ME
388 |
389 |
390 |
391 |
${appHtml}
392 |
395 |
396 |
397 | `
398 | )
399 | }
400 |
401 | export default renderApp
402 | ```
403 |
404 | `ReactDOMServer.renderToString` здесь происходит магия. React будет сравнивать наше содержимое `shared` `App`, и возвращать простую строку HTML элементов. `Provider` работает аналогично клиентскому, но на сервере, мы оборачиваем наше приложение внутрь `StaticRouter` вместо `BrowserRouter`. Для перехода the Redux store (хранилища) с сервера на клиент, мы передаем `window.__PRELOADED_STATE__` которая является произвольным именем переменной.
405 |
406 | **Заметка**: Неизменяемые объекты реализуют `toJSON()` метод, аналогичный, который вы могли использовать `JSON.stringify` преобразует код к простой JSON строке.
407 |
408 | - Отредактируйте `src/client/index.jsx` для использования предзагрузочного состояния:
409 |
410 | ```js
411 | import Immutable from 'immutable'
412 | // [...]
413 |
414 | /* eslint-disable no-underscore-dangle */
415 | const composeEnhancers = (isProd ? null : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
416 | const preloadedState = window.__PRELOADED_STATE__
417 | /* eslint-enable no-underscore-dangle */
418 |
419 | const store = createStore(combineReducers(
420 | { hello: helloReducer }),
421 | { hello: Immutable.fromJS(preloadedState.hello) },
422 | composeEnhancers(applyMiddleware(thunkMiddleware)))
423 | ```
424 |
425 | Здесь объединяется наш клиентское store (хранилище) с `preloadedState (состояние до загрузки)`, который мы получили с сервера.
426 |
427 | 🏁 Вы можете запустить `yarn start` и `yarn dev:wds` и перейти между страницами. Обновите страницу на `/hello`, `/hello-async`, и `/404` (или любой другой URI), сейчас должно работать корректно. Обратите внимание на `message` и `messageAsync` варьируется, в зависимости от того, перешли вы на эту страницу с клиента или это рендер на стороне сервера.
428 |
429 | ### React Helmet (Реакт Шлем)
430 |
431 | > 💡 **[React Helmet](https://github.com/nfl/react-helmet)**: Библиотека, которая вставляет контент в `head` React приложения, работает на клиенте и на сервере.
432 |
433 | Я специально заставил тебя написать `FIX ME` в названии, чтобы подчеркнуть тот факт, что даже если мы делаем обработку на стороне сервера, в данный момент мы не заполняем тег `title` (или какие-либо теги в `head`, которые могуть варьироваться в зависимости от страницы).
434 |
435 | - Запустите `yarn add react-helmet`
436 |
437 | - Отредактируйте `src/server/render-app.jsx` согласно следующему:
438 |
439 | ```js
440 | import Helmet from 'react-helmet'
441 | // [...]
442 | const renderApp = (/* [...] */) => {
443 |
444 | const appHtml = ReactDOMServer.renderToString(/* [...] */)
445 | const head = Helmet.rewind()
446 |
447 | return (
448 | `
449 |
450 |
451 | ${head.title}
452 | ${head.meta}
453 |
454 |
455 | [...]
456 | `
457 | )
458 | }
459 | ```
460 |
461 | React Helmet использует [react-side-effect](https://github.com/gaearon/react-side-effect) `rewind` для получения данных для рендеринга нашего приложения, которое будет содержать несоколько `` компонентов. Эти `` компоненты в которые мы установим `title` и другие `head` детали для каждой страницы.
462 |
463 | - Отредактируйте `src/shared/app.jsx` согласно следующему:
464 |
465 | ```js
466 | import Helmet from 'react-helmet'
467 | // [...]
468 | const App = () =>
469 |
582 |
583 | export default NotFoundPage
584 | ```
585 |
586 | Этот компонент `` на самом деле не рендерит ничего, он просто вставляет содержимое в `head` нашего документа и предоставляет те же данные на сервере.
587 |
588 | 🏁 Запустите `yarn start` и `yarn dev:wds` и понавигируйте по страницам. Заголовок вашей страницы должен изменяться при навигации, и должен оставаться неизменным при обновлении страницы. Посмотрите исходный код страницы, чтобы понять как React Helmet устанавливает `title` и `meta` теги, даже при рендере на сервере.
589 |
590 | Следующая секция: [07 - Socket.IO](07-socket-io_ru.md#readme)
591 |
592 | Назад [предыдущая секция](05-redux-immutable-fetch_ru.md#readme) или [содержание](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
593 |
--------------------------------------------------------------------------------
/tutorial/07-socket-io.md:
--------------------------------------------------------------------------------
1 | # 07 - Socket.IO
2 |
3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/07-socket-io).
4 |
5 | > 💡 **[Socket.IO](https://github.com/socketio/socket.io)** is a library to easily deal with Websockets. It provides a convenient API and fallback for browsers that don't support Websockets.
6 |
7 | In this chapter, we are going to set up a basic message exchange between the client and the server. In order to not add more pages and components – which would be unrelated to the core feature we're interested in here – we are going to make this exchange happen in the browser console. No UI stuff in this chapter.
8 |
9 | - Run `yarn add socket.io socket.io-client`
10 |
11 | ## Server-side
12 |
13 | - Edit your `src/server/index.js` like so:
14 |
15 | ```js
16 | // @flow
17 |
18 | import compression from 'compression'
19 | import express from 'express'
20 | import { Server } from 'http'
21 | import socketIO from 'socket.io'
22 |
23 | import routing from './routing'
24 | import { WEB_PORT, STATIC_PATH } from '../shared/config'
25 | import { isProd } from '../shared/util'
26 | import setUpSocket from './socket'
27 |
28 | const app = express()
29 | // flow-disable-next-line
30 | const http = Server(app)
31 | const io = socketIO(http)
32 | setUpSocket(io)
33 |
34 | app.use(compression())
35 | app.use(STATIC_PATH, express.static('dist'))
36 | app.use(STATIC_PATH, express.static('public'))
37 |
38 | routing(app)
39 |
40 | http.listen(WEB_PORT, () => {
41 | // eslint-disable-next-line no-console
42 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' :
43 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`)
44 | })
45 | ```
46 |
47 | Note that in order for Socket.IO to work, you need to use `Server` from `http` to `listen` to incoming requests, and not the Express `app`. Fortunately, that doesn't change much of the code. All the Websocket details are externalized in a different file, called with `setUpSocket`.
48 |
49 | - Add the following constants to `src/shared/config.js`:
50 |
51 | ```js
52 | export const IO_CONNECT = 'connect'
53 | export const IO_DISCONNECT = 'disconnect'
54 | export const IO_CLIENT_HELLO = 'IO_CLIENT_HELLO'
55 | export const IO_CLIENT_JOIN_ROOM = 'IO_CLIENT_JOIN_ROOM'
56 | export const IO_SERVER_HELLO = 'IO_SERVER_HELLO'
57 | ```
58 |
59 | These are the *type of messages* your client and your server will exchange. I suggest prefixing them with either `IO_CLIENT` or `IO_SERVER` to make it clearer *who* is sending the message. Otherwise, things can get pretty confusing when you have a lot of message types.
60 |
61 | As you can see, we have a `IO_CLIENT_JOIN_ROOM`, because for the sake of demonstration, we are going to make clients join a room (like a chatroom). Rooms are useful to broadcast messages to specific groups of users.
62 |
63 | - Create a `src/server/socket.js` file containing:
64 |
65 | ```js
66 | // @flow
67 |
68 | import {
69 | IO_CONNECT,
70 | IO_DISCONNECT,
71 | IO_CLIENT_JOIN_ROOM,
72 | IO_CLIENT_HELLO,
73 | IO_SERVER_HELLO,
74 | } from '../shared/config'
75 |
76 | /* eslint-disable no-console */
77 | const setUpSocket = (io: Object) => {
78 | io.on(IO_CONNECT, (socket) => {
79 | console.log('[socket.io] A client connected.')
80 |
81 | socket.on(IO_CLIENT_JOIN_ROOM, (room) => {
82 | socket.join(room)
83 | console.log(`[socket.io] A client joined room ${room}.`)
84 |
85 | io.emit(IO_SERVER_HELLO, 'Hello everyone!')
86 | io.to(room).emit(IO_SERVER_HELLO, `Hello clients of room ${room}!`)
87 | socket.emit(IO_SERVER_HELLO, 'Hello you!')
88 | })
89 |
90 | socket.on(IO_CLIENT_HELLO, (clientMessage) => {
91 | console.log(`[socket.io] Client: ${clientMessage}`)
92 | })
93 |
94 | socket.on(IO_DISCONNECT, () => {
95 | console.log('[socket.io] A client disconnected.')
96 | })
97 | })
98 | }
99 | /* eslint-enable no-console */
100 |
101 | export default setUpSocket
102 | ```
103 |
104 | Okay, so in this file, we implement *how our server should react when clients connect and send messages to it*:
105 |
106 | - When the client connects, we log it in the server console, and get access to the `socket` object, which we can use to communicate back with that client.
107 | - When a client sends `IO_CLIENT_JOIN_ROOM`, we make it join the `room` it wants. Once it has joined a room, we send 3 demo messages: 1 message to every user, 1 message to users in that room, 1 message to that client only.
108 | - When the client sends `IO_CLIENT_HELLO`, we log its message in the server console.
109 | - When the client disconnects, we log it as well.
110 |
111 | ## Client-side
112 |
113 | The client-side of things is going to look very similar.
114 |
115 | - Edit `src/client/index.jsx` like so:
116 |
117 | ```js
118 | // [...]
119 | import setUpSocket from './socket'
120 |
121 | // [at the very end of the file]
122 | setUpSocket(store)
123 | ```
124 |
125 | As you can see, we pass the Redux store to `setUpSocket`. This way whenever a Websocket message coming from the server should alter the client's Redux state, we can `dispatch` actions. We are not going to `dispatch` anything in this example though.
126 |
127 | - Create a `src/client/socket.js` file containing:
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 |
166 | What happens here should not be surprising if you understood well what we did on the server:
167 |
168 | - As soon as the client is connected, we log it in the browser console and join the room `hello-1234` with a `IO_CLIENT_JOIN_ROOM` message.
169 | - We then send `Hello!` with a `IO_CLIENT_HELLO` message.
170 | - If the server sends us a `IO_SERVER_HELLO` message, we log it in the browser console.
171 | - We also log any disconnection.
172 |
173 | 🏁 Run `yarn start` and `yarn dev:wds`, open `http://localhost:8000`. Then, open your browser console, and also look at the terminal of your Express server. You should see the Websocket communication between your client and server.
174 |
175 | Next section: [08 - Bootstrap, JSS](08-bootstrap-jss.md#readme)
176 |
177 | Back to the [previous section](06-react-router-ssr-helmet.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents).
178 |
--------------------------------------------------------------------------------
/tutorial/08-bootstrap-jss.md:
--------------------------------------------------------------------------------
1 | # 08 - Bootstrap and JSS
2 |
3 | Code for this chapter available in the [`master-no-services`](https://github.com/verekia/js-stack-boilerplate/tree/master-no-services) branch of the [JS-Stack-Boilerplate repository](https://github.com/verekia/js-stack-boilerplate).
4 |
5 | Alright! It's time to give our ugly app a facelift. We are going to use Twitter Bootstrap to give it some base styles. We'll then add a CSS-in-JS library to add some custom styles.
6 |
7 | ## Twitter Bootstrap
8 |
9 | > 💡 **[Twitter Bootstrap](http://getbootstrap.com/)** is a library of UI components.
10 |
11 | There are 2 options to integrate Bootstrap in a React app. Both have their pros and cons:
12 |
13 | - Using the official release, **which uses jQuery and Tether** for the behavior of its components.
14 | - Using a third-party library that re-implements all of Bootstrap's components in React, like [React-Bootstrap](https://react-bootstrap.github.io/) or [Reactstrap](https://reactstrap.github.io/).
15 |
16 | Third-party libraries provide very convenient React components that dramatically reduce the code bloat compared to the official HTML components, and integrate greatly with your React codebase. That being said, I must say that I am quite reluctant to use them, because they will always be *behind* the official releases (sometimes potentially far behind). They also won't work with Bootstrap themes that implement their own JS. That's a pretty tough drawback considering that one major strength of Bootstrap is its huge community of designers who make beautiful themes.
17 |
18 | For this reason, I'm going to make the tradeoff of integrating the official release, alongside with jQuery and Tether. One of the concerns of this approach is the file size of our bundle of course. For your information, the bundle weights about 200KB (Gzipped) with jQuery, Tether, and Bootstrap's JS included. I think that's reasonable, but if that's too much for you, you should probably consider an other option for Bootstrap, or even not using Bootstrap at all.
19 |
20 | ### Bootstrap's CSS
21 |
22 | - Delete `public/css/style.css`
23 |
24 | - Run `yarn add bootstrap@4.0.0-alpha.6`
25 |
26 | - Copy `bootstrap.min.css` and `bootstrap.min.css.map` from `node_modules/bootstrap/dist` to your `public/css` folder.
27 |
28 | - Edit `src/server/render-app.jsx` like so:
29 |
30 | ```html
31 |
32 | ```
33 |
34 | ### Bootstrap's JS with jQuery and Tether
35 |
36 | Now that we have Bootstrap's styles loaded on our page, we need the JavaScript behavior for the components.
37 |
38 | - Run `yarn add jquery tether`
39 |
40 | - Edit `src/client/index.jsx` like so:
41 |
42 | ```js
43 | import $ from 'jquery'
44 | import Tether from 'tether'
45 |
46 | // [right after all your imports]
47 |
48 | window.jQuery = $
49 | window.Tether = Tether
50 | require('bootstrap')
51 | ```
52 |
53 | That will load Bootstrap's JavaScript code.
54 |
55 | ### Bootstrap Components
56 |
57 | Alright, it's time for you to copy-paste a whole bunch of files.
58 |
59 | - Edit `src/shared/component/page/hello-async.jsx` like so:
60 |
61 | ```js
62 | // @flow
63 |
64 | import React from 'react'
65 | import Helmet from 'react-helmet'
66 |
67 | import MessageAsync from '../../container/message-async'
68 | import HelloAsyncButton from '../../container/hello-async-button'
69 |
70 | const title = 'Async Hello Page'
71 |
72 | const HelloAsyncPage = () =>
73 |
265 | ```
266 |
267 | This is an example of a *React inline style*.
268 |
269 | This will translate into: `
` in your DOM. We need this style to push the content under the navigation bar, but that's what's important here. [React inline styles](https://speakerdeck.com/vjeux/react-css-in-js) are a great way to isolate your component's styles from the global CSS namespace, but it comes at a price: You cannot use some native CSS features like `:hover`, Media Queries, animations, or `font-face`. That's [one of the reasons](https://github.com/cssinjs/jss/blob/master/docs/benefits.md#compared-to-inline-styles) we're going to integrate a CSS-in-JS library, JSS, later in this chapter.
270 |
271 | - Edit `src/shared/component/nav.jsx` like so:
272 |
273 | ```js
274 | // @flow
275 |
276 | import $ from 'jquery'
277 | import React from 'react'
278 | import { Link, NavLink } from 'react-router-dom'
279 | import { APP_NAME } from '../config'
280 | import {
281 | HOME_PAGE_ROUTE,
282 | HELLO_PAGE_ROUTE,
283 | HELLO_ASYNC_PAGE_ROUTE,
284 | NOT_FOUND_DEMO_PAGE_ROUTE,
285 | } from '../routes'
286 |
287 | const handleNavLinkClick = () => {
288 | $('body').scrollTop(0)
289 | $('.js-navbar-collapse').collapse('hide')
290 | }
291 |
292 | const Nav = () =>
293 |
313 |
314 | export default Nav
315 | ```
316 |
317 | There is something new here, `handleNavLinkClick`. One issue I encountered using Bootstrap's `navbar` in an SPA is that clicking on a link on mobile does not collapse the menu, and does not scroll back to the top of the page. This is a great opportunity to show you an example of how you would integrate some jQuery / Bootstrap-specific code in your app:
318 |
319 | ```js
320 | import $ from 'jquery'
321 | // [...]
322 |
323 | const handleNavLinkClick = () => {
324 | $('body').scrollTop(0)
325 | $('.js-navbar-collapse').collapse('hide')
326 | }
327 |
328 |
329 | ```
330 |
331 | **Note**: I've removed accessibility-related attributes (like `aria` attributes) to make the code more readable *in the context of this tutorial*. **You should absolutely put them back**. Refer to Bootstrap's documentation and code samples to see how to use them.
332 |
333 | 🏁 Your app should now be entirely styled with Bootstrap.
334 |
335 | ## The current state of CSS
336 |
337 | In 2016, the typical modern JavaScript stack settled. The different libraries and tools this tutorial made you set up are pretty much the *cutting-edge industry standard* (*cough – even though it could become completely outdated in a year from now – cough*). Yes, that's a complex stack to set up, but at least, most front-end devs agree that React-Redux-Webpack is the way to go. Now regarding CSS, I have some pretty bad news. Nothing settled, there is no standard way to go, no standard stack.
338 |
339 | 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, and many more that I forget are all different approaches or tools to get the job done. They all do it well, which is the problem, there is no clear winner, it's a big mess.
340 |
341 | The cool React kids tend to favor React inline styles, CSS-in-JS, or CSS Modules approaches though, since they integrate really well with React and solve programmatically many [issues](https://speakerdeck.com/vjeux/react-css-in-js) that regular CSS approaches struggle with.
342 |
343 | CSS Modules work well, but they don't leverage the power of JavaScript and its many features over CSS. They just provide encapsulation, which is fine, but React inline styles and CSS-in-JS take styling to an other level in my opinion. My personal suggestion would be to use React inline styles for common styles (that's also what you have to use for React Native), and use a CSS-in-JS library for things like `:hover` and media queries.
344 |
345 | There are [tons of CSS-in-JS libraries](https://github.com/MicheleBertoli/css-in-js). JSS is a full-featured, well-rounded, and [performant](https://github.com/cssinjs/jss/blob/master/docs/performance.md) one.
346 |
347 | ## JSS
348 |
349 | > 💡 **[JSS](http://cssinjs.org/)** is a CSS-in-JS library to write your styles in JavaScript and inject them into your app.
350 |
351 | Now that we have some base template with Bootstrap, let's write some custom CSS. I mentioned earlier that React inline styles could not handle `:hover` and media queries, so we'll show a simple example of this on the homepage using JSS. JSS can be used via `react-jss`, a library that is convenient to use with React components.
352 |
353 | - Run `yarn add react-jss`
354 |
355 | Add the following to your `.flowconfig` file, as there is currently a Flow [issue](https://github.com/cssinjs/jss/issues/411) with JSS:
356 |
357 | ```flowconfig
358 | [ignore]
359 | .*/node_modules/jss/.*
360 | ```
361 |
362 | ### Server-side
363 |
364 | JSS can render styles on the server for the initial rendering.
365 |
366 | - Add the following constants to `src/shared/config.js`:
367 |
368 | ```js
369 | export const JSS_SSR_CLASS = 'jss-ssr'
370 | export const JSS_SSR_SELECTOR = `.${JSS_SSR_CLASS}`
371 | ```
372 |
373 | - Edit `src/server/render-app.jsx` like so:
374 |
375 | ```js
376 | import { SheetsRegistry, SheetsRegistryProvider } from 'react-jss'
377 | // [...]
378 | import { APP_CONTAINER_CLASS, JSS_SSR_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config'
379 | // [...]
380 | const renderApp = (location: string, plainPartialState: ?Object, routerContext: ?Object = {}) => {
381 | const store = initStore(plainPartialState)
382 | const sheets = new SheetsRegistry()
383 | const appHtml = ReactDOMServer.renderToString(
384 |
385 |
386 |
387 |
388 |
389 |
390 | )
391 | // [...]
392 |
393 |
394 | // [...]
395 | ```
396 |
397 | ## Client-side
398 |
399 | The first thing the client should do after rendering the app client-side, is to get rid of the server-generated JSS styles.
400 |
401 | - Add the following to `src/client/index.jsx` after the `ReactDOM.render` calls (before `setUpSocket(store)` for instance):
402 |
403 | ```js
404 | import { APP_CONTAINER_SELECTOR, JSS_SSR_SELECTOR } from '../shared/config'
405 | // [...]
406 |
407 | const jssServerSide = document.querySelector(JSS_SSR_SELECTOR)
408 | // flow-disable-next-line
409 | jssServerSide.parentNode.removeChild(jssServerSide)
410 |
411 | setUpSocket(store)
412 | ```
413 |
414 | Edit `src/shared/component/page/home.jsx` like so:
415 |
416 | ```js
417 | import injectSheet from 'react-jss'
418 | // [...]
419 | const styles = {
420 | hoverMe: {
421 | '&:hover': {
422 | color: 'red',
423 | },
424 | },
425 | '@media (max-width: 800px)': {
426 | resizeMe: {
427 | color: 'red',
428 | },
429 | },
430 | specialButton: {
431 | composes: ['btn', 'btn-primary'],
432 | backgroundColor: 'limegreen',
433 | },
434 | }
435 |
436 | const HomePage = ({ classes }: { classes: Object }) =>
437 | // [...]
438 |
439 |
JSS
440 |
Hover me.
441 |
Resize the window.
442 |
443 |
444 | // [...]
445 |
446 | export default injectSheet(styles)(HomePage)
447 | ```
448 |
449 | Unlike React inline styles, JSS uses classes. You pass styles to `injectSheet` and the CSS classes end up in the props of your component.
450 |
451 | 🏁 Run `yarn start` and `yarn dev:wds`. Open the homepage. Show the source of the page (not in the inspector) to see that the JSS styles are present in the DOM at the initial render in the `