├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Definitions.md ├── README.md ├── READMEeng.md ├── how-to-translate.md ├── img ├── chai.png ├── eslint.png ├── flow.png ├── gulp.png ├── js.png ├── mocha.png ├── npm.png ├── react.png ├── redux.png ├── webpack.png └── yarn.png ├── mdlint.js ├── package.json ├── tutorial ├── 1-node-npm-yarn-package-json │ ├── README.md │ ├── READMEeng.md │ ├── index.js │ └── package.json ├── 10-immutable-redux-improvements │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── dist │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── actions │ │ │ │ └── dog-actions.js │ │ │ ├── app.jsx │ │ │ ├── components │ │ │ │ ├── button.jsx │ │ │ │ └── message.jsx │ │ │ ├── containers │ │ │ │ ├── bark-button.js │ │ │ │ └── bark-message.js │ │ │ └── reducers │ │ │ │ └── dog-reducer.js │ │ ├── server │ │ │ └── index.js │ │ └── shared │ │ │ └── dog.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── 11-testing-mocha-chai-sinon │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── dist │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── actions │ │ │ │ └── dog-actions.js │ │ │ ├── app.jsx │ │ │ ├── components │ │ │ │ ├── button.jsx │ │ │ │ └── message.jsx │ │ │ ├── containers │ │ │ │ ├── bark-button.js │ │ │ │ └── bark-message.js │ │ │ └── reducers │ │ │ │ └── dog-reducer.js │ │ ├── server │ │ │ └── index.js │ │ ├── shared │ │ │ └── dog.js │ │ └── test │ │ │ ├── client │ │ │ └── state-test.js │ │ │ └── shared │ │ │ └── dog-test.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── 12-flow │ ├── .flowconfig │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── dist │ │ ├── client-bundle.js.map │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── actions │ │ │ │ └── dog-actions.js │ │ │ ├── app.jsx │ │ │ ├── components │ │ │ │ ├── button.jsx │ │ │ │ └── message.jsx │ │ │ ├── containers │ │ │ │ ├── bark-button.js │ │ │ │ └── bark-message.js │ │ │ └── reducers │ │ │ │ └── dog-reducer.js │ │ ├── server │ │ │ └── index.js │ │ ├── shared │ │ │ └── dog.js │ │ └── test │ │ │ ├── client │ │ │ └── state-test.js │ │ │ └── shared │ │ │ └── dog-test.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── 2-packages │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── 3-es6-babel-gulp │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── gulpfile.js │ ├── package.json │ ├── src │ │ └── index.js │ └── yarn.lock ├── 4-es6-syntax-class │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── gulpfile.js │ ├── package.json │ ├── src │ │ ├── dog.js │ │ └── index.js │ └── yarn.lock ├── 5-es6-modules-syntax │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── dog.js │ │ └── index.js │ └── yarn.lock ├── 6-eslint │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── dog.js │ │ └── index.js │ └── yarn.lock ├── 7-client-webpack │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── dist │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ └── app.js │ │ ├── server │ │ │ └── index.js │ │ └── shared │ │ │ └── dog.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── 8-react │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── dist │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ └── app.jsx │ │ ├── server │ │ │ └── index.js │ │ └── shared │ │ │ └── dog.js │ ├── webpack.config.babel.js │ └── yarn.lock └── 9-redux │ ├── .gitignore │ ├── README.md │ ├── READMEeng.md │ ├── dist │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ ├── client │ │ ├── actions │ │ │ └── dog-actions.js │ │ ├── app.jsx │ │ ├── components │ │ │ ├── button.jsx │ │ │ └── message.jsx │ │ ├── containers │ │ │ ├── bark-button.js │ │ │ └── bark-message.js │ │ └── reducers │ │ │ └── dog-reducer.js │ ├── server │ │ └── index.js │ └── shared │ │ └── dog.js │ ├── webpack.config.babel.js │ └── yarn.lock └── yarn.lock /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Type of issue: (feature suggestion, bug, translation?) 2 | 3 | ### Chapter: 4 | 5 | ### If it's a bug: 6 | 7 | Please try using the code provided in this repository 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, a typo, or something like this. 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: 3 | "6" 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Стек технологий JavaScript с нуля 2 | ## JavaScript Stack from Scratch 3 | 4 | [![Build Status](https://travis-ci.org/verekia/js-stack-from-scratch.svg?branch=master)](https://travis-ci.org/verekia/js-stack-from-scratch) [![Join the chat at https://gitter.im/js-stack-from-scratch/Lobby](https://badges.gitter.im/js-stack-from-scratch/Lobby.svg)](https://gitter.im/js-stack-from-scratch/Lobby) 5 | 6 | [![Yarn](/img/yarn.png)](https://yarnpkg.com/) 7 | [![React](/img/react.png)](https://facebook.github.io/react/) 8 | [![Gulp](/img/gulp.png)](http://gulpjs.com/) 9 | [![Redux](/img/redux.png)](http://redux.js.org/) 10 | [![ESLint](/img/eslint.png)](http://eslint.org/) 11 | [![Webpack](/img/webpack.png)](https://webpack.github.io/) 12 | [![Mocha](/img/mocha.png)](https://mochajs.org/) 13 | [![Chai](/img/chai.png)](http://chaijs.com/) 14 | [![Flow](/img/flow.png)](https://flowtype.org/) 15 | 16 | >Это русскоязычная версия руководства Джонатана Верекии ([@verekia](https://twitter.com/verekia)). Оригинальное руководство расположено [здесь](https://github.com/verekia/js-stack-from-scratch). Данное пособие постоянно развивается и дополняется автором, предоставляя читателям максимально свежую и качественную информацию. Текст оригинального пособия и прилагаемый код будут меняться с течением времени. Мы так же будем стараться поддерживать русскоязычную версию в актуальном состоянии. Данный перевод соответствует английской версии по состоянию на [commit](https://github.com/verekia/js-stack-from-scratch/commit/343922149f136bbab2512b14a2fe5efe095d62b7). 17 | Мы будем рады вашим [замечаниям и вопросам](https://github.com/UsulPro/js-stack-from-scratch/issues) 18 |
@UsulPro
19 | 20 | Добро пожаловать в мое современное руководство по стеку технологий JavaScript: **Стек технологий JavaScript с нуля** 21 | 22 | Это минималистичное и практико-ориентированное пособие по применению JavaScript технологий. Вам потребуются общие знания по программированию и основы JavaScript. Это пособие **нацелено на интеграцию необходимых инструментов** и предоставляет **максимально простые примеры** для каждого инструмента. Вы можете рассматривать данный документ, как *возможность создать свой собственный шаблонный проект с нуля*. 23 | 24 | Конечно, вам не нужны все эти технологии, если вы делаете простую веб страницу с парой JS функций (комбинации Browserify / Webpack + Babel + jQuery достаточно, чтобы написать ES6 код в нескольких файлах и скомпилировать все через командную строку), но если вы собираетесь создать масштабируемое веб приложение, и вам нужно все правильно настроить, то это руководство отлично вам подходит. 25 | 26 | Поскольку целью этого руководства является сборка различных инструментов, я не буду вдаваться в детали по каждому из них. Если вы хотите получить по ним более глубокие знания, изучайте их документацию или другие руководства. 27 | 28 | В большой части технологий, описываемых здесь, используется React. Если вы только начинаете использовать React и просто хотите изучить его, то [create-react-app](https://github.com/facebookincubator/create-react-app) поможет вам и кратко ознакомит с инфраструктурой React на основе предустановленной конфигурации. Я бы, например, порекомендовал такой подход для тех, кому нужно влиться в команду, использующую React, и на чем-то потренироваться, чтобы подтянуть свои знания. В этом руководстве мы не будем пользоваться предустановленными конфигурациями, поскольку я хочу, чтобы вы полностью понимали все, что происходит "под капотом". 29 | 30 | Примеры кода имеются в каждой части, и вы можете запускать их через `yarn && yarn start` или `npm install && npm start`. Я рекомендую писать все с нуля самостоятельно, следуя **пошаговым инструкциям** каждого раздела. 31 | 32 | **Каждая часть содержит код, написанный в предыдущих частях**, так что если вы просто хотите получить окончательный вариант проекта, содержащий все необходимое, просто скопируйте последний раздел и смело пользуйтесь. 33 | 34 | Примечание: Порядок частей не всегда обязателен. К примеру, тестирование / типизация могут быть выполнены до введения в React. Довольно сложно перемещать или редактировать опубликованные разделы, поскольку приходится вносить изменения во все следующие за ними части. Возможно, когда все определится, я приведу всю документацию к более удобному виду. 35 | 36 | Код, приведенный в примерах, работает под Linux, macOS и Windows. 37 | 38 | ## Содержание 39 | 40 | [1 - Node, NPM, Yarn и package.json](/tutorial/1-node-npm-yarn-package-json) 41 | 42 | [2 - Установка и использование пакетов](/tutorial/2-packages) 43 | 44 | [3 - Настройка ES6 с Babel и Gulp](/tutorial/3-es6-babel-gulp) 45 | 46 | [4 - Использование ES6 классов](/tutorial/4-es6-syntax-class) 47 | 48 | [5 - Синтаксис модулей ES6](/tutorial/5-es6-modules-syntax) 49 | 50 | [6 - ESLint](/tutorial/6-eslint) 51 | 52 | [7 - Клиентское приложение на основе Webpack](/tutorial/7-client-webpack) 53 | 54 | [8 - React](/tutorial/8-react) 55 | 56 | [9 - Redux](/tutorial/9-redux) 57 | 58 | [10 - Immutable JS и улучшения Redux](/tutorial/10-immutable-redux-improvements) 59 | 60 | [11 - Тестирование с Mocha, Chai и Sinon](/tutorial/11-testing-mocha-chai-sinon) 61 | 62 | [12 - Типизация с Flow](/tutorial/12-flow) 63 | 64 | ## Далее планируется: 65 | 66 | Production / development окружение, Express, React Router, Серверный Рендеринг, Стилизация, Enzyme, Приемы Git. 67 | 68 | ## Переводы на другие языки 69 | 70 | - [Китайский](https://github.com/pd4d10/js-stack-from-scratch) by [@pd4d10](http://github.com/pd4d10) 71 | - [Итальянский](https://github.com/fbertone/js-stack-from-scratch) by [Fabrizio Bertone](https://github.com/fbertone) 72 | - [Японский](https://github.com/takahashim/js-stack-from-scratch) by [@takahashim](https://github.com/takahashim) 73 | - [Тайский](https://github.com/MicroBenz/js-stack-from-scratch) by [MicroBenz](https://github.com/MicroBenz) 74 | 75 | Если вы хотите добавить перевод на другой язык, пожалуйста читайте [рекомендации по переводу](/how-to-translate.md) чтобы начать! 76 | 77 | ## Сведения 78 | 79 | Создано [@verekia](https://twitter.com/verekia) – [verekia.com](http://verekia.com/). 80 | 81 | Переведено [@usulpro](https://github.com/UsulPro) - [react-theming](https://github.com/sm-react/react-theming) 82 | 83 | Лицензия: MIT 84 | -------------------------------------------------------------------------------- /READMEeng.md: -------------------------------------------------------------------------------- 1 | # JavaScript Stack from Scratch 2 | 3 | [![Build Status](https://travis-ci.org/verekia/js-stack-from-scratch.svg?branch=master)](https://travis-ci.org/verekia/js-stack-from-scratch) [![Join the chat at https://gitter.im/js-stack-from-scratch/Lobby](https://badges.gitter.im/js-stack-from-scratch/Lobby.svg)](https://gitter.im/js-stack-from-scratch/Lobby) 4 | 5 | [![Yarn](/img/yarn.png)](https://yarnpkg.com/) 6 | [![React](/img/react.png)](https://facebook.github.io/react/) 7 | [![Gulp](/img/gulp.png)](http://gulpjs.com/) 8 | [![Redux](/img/redux.png)](http://redux.js.org/) 9 | [![ESLint](/img/eslint.png)](http://eslint.org/) 10 | [![Webpack](/img/webpack.png)](https://webpack.github.io/) 11 | [![Mocha](/img/mocha.png)](https://mochajs.org/) 12 | [![Chai](/img/chai.png)](http://chaijs.com/) 13 | [![Flow](/img/flow.png)](https://flowtype.org/) 14 | 15 | Welcome to my modern JavaScript stack tutorial: **JavaScript Stack from Scratch**. 16 | 17 | This is a minimalistic and straight-to-the-point guide to assembling a JavaScript stack. It requires some general programming knowledge, and JavaScript basics. **It focuses on wiring tools together** and giving you the **simplest possible example** for each tool. You can see this tutorial as *a way to write your own boilerplate from scratch*. 18 | 19 | You don't need to use this entire stack if you build a simple web page with a few JS interactions of course (a combination of Browserify/Webpack + Babel + jQuery is enough to be able to write ES6 code in different files with CLI compilation), but if you want to build a web app that scales, and need help setting things up, this tutorial will work great for you. 20 | 21 | Since the goal of this tutorial is to assemble various tools, I do not go into details about how these tools work individually. Refer to their documentation or find other tutorials if you want to acquire deeper knowledge in them. 22 | 23 | A big chunk of the stack described in this tutorial uses React. If you are beginning and just want to learn React, [create-react-app](https://github.com/facebookincubator/create-react-app) will get you up and running with a React environment very quickly with a premade configuration. I would for instance recommend this approach to someone who arrives in a team that's using React and needs to catch up with a learning playground. In this tutorial you won't use a premade configuration, because I want you to understand everything that's happening under the hood. 24 | 25 | Code examples are available for each chapter, and you can run them all with `yarn && yarn start` or `npm install && npm start`. I recommend writing everything from scratch yourself by following the **step-by-step instructions** of each chapter. 26 | 27 | **Every chapter contains the code of previous chapters**, so if you are simply looking for a boilerplate project containing everything, just clone the last chapter and you're good to go. 28 | 29 | Note: The order of chapters is not necessarily the most educational. For instance, testing / type checking could have been done before introducing React. It is quite difficult to move chapters around or edit past ones, since I need to apply those changes to every following chapter. If things settle down, I might reorganize the whole thing in a better way. 30 | 31 | The code of this tutorial works on Linux, macOS, and Windows. 32 | 33 | ## Table of contents 34 | 35 | [1 - Node, NPM, Yarn, and package.json](/tutorial/1-node-npm-yarn-package-json) 36 | 37 | [2 - Installing and using a package](/tutorial/2-packages) 38 | 39 | [3 - Setting up ES6 with Babel and Gulp](/tutorial/3-es6-babel-gulp) 40 | 41 | [4 - Using the ES6 syntax with a class](/tutorial/4-es6-syntax-class) 42 | 43 | [5 - The ES6 modules syntax](/tutorial/5-es6-modules-syntax) 44 | 45 | [6 - ESLint](/tutorial/6-eslint) 46 | 47 | [7 - Client app with Webpack](/tutorial/7-client-webpack) 48 | 49 | [8 - React](/tutorial/8-react) 50 | 51 | [9 - Redux](/tutorial/9-redux) 52 | 53 | [10 - Immutable JS and Redux Improvements](/tutorial/10-immutable-redux-improvements) 54 | 55 | [11 - Testing with Mocha, Chai, and Sinon](/tutorial/11-testing-mocha-chai-sinon) 56 | 57 | [12 - Type Checking with Flow](/tutorial/12-flow) 58 | 59 | ## Coming up next 60 | 61 | Production / development environments, Express, React Router, Server-Side Rendering, Styling, Enzyme, Git Hooks. 62 | 63 | ## Translations 64 | 65 | - [中文](https://github.com/pd4d10/js-stack-from-scratch) by [@pd4d10](http://github.com/pd4d10) 66 | - [Italiano](https://github.com/fbertone/js-stack-from-scratch) by [Fabrizio Bertone](https://github.com/fbertone) 67 | - [日本語](https://github.com/takahashim/js-stack-from-scratch) by [@takahashim](https://github.com/takahashim) 68 | 69 | If you want to add your translation, please read the [translation recommendations](/how-to-translate.md) to get started! 70 | 71 | ## Credits 72 | 73 | Created by [@verekia](https://twitter.com/verekia) – [verekia.com](http://verekia.com/). 74 | 75 | License: MIT 76 | -------------------------------------------------------------------------------- /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 a translation issue open 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 | - Open an issue on the English repo to show you're currently working on a translation. 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/dfab78b581a3da800daeb3686b900dd9ea972da0...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/chai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/chai.png -------------------------------------------------------------------------------- /img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/eslint.png -------------------------------------------------------------------------------- /img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/flow.png -------------------------------------------------------------------------------- /img/gulp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/gulp.png -------------------------------------------------------------------------------- /img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/js.png -------------------------------------------------------------------------------- /img/mocha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/mocha.png -------------------------------------------------------------------------------- /img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/npm.png -------------------------------------------------------------------------------- /img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/react.png -------------------------------------------------------------------------------- /img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/redux.png -------------------------------------------------------------------------------- /img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/img/webpack.png -------------------------------------------------------------------------------- /img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usulpro/js-stack-from-scratch-v1-rus/6463f01fa1b3553d91aac5822799db2751dd09f7/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": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "test": "yarn run mdlint && cd tutorial/1-node-npm-yarn-package-json && yarn && yarn run tutorial-test && cd ../2-packages && yarn && yarn run tutorial-test && cd ../3-es6-babel-gulp && yarn && yarn run tutorial-test && cd ../4-es6-syntax-class && yarn && yarn run tutorial-test && cd ../5-es6-modules-syntax && yarn && yarn run tutorial-test && cd ../6-eslint && yarn && yarn run tutorial-test && cd ../7-client-webpack && yarn && yarn run tutorial-test && cd ../8-react && yarn && yarn run tutorial-test && cd ../9-redux && yarn && yarn run tutorial-test && cd ../10-immutable-redux-improvements && yarn && yarn run tutorial-test && cd ../11-testing-mocha-chai-sinon && yarn && yarn run tutorial-test && cd ../12-flow && yarn && yarn run tutorial-test", 7 | "mdlint": "node mdlint.js" 8 | }, 9 | "devDependencies": { 10 | "glob": "^7.1.1", 11 | "markdownlint": "^0.3.0", 12 | "yarn": "^0.16.1" 13 | }, 14 | "repository": "verekia/js-stack-from-scratch", 15 | "author": "Jonathan Verrecchia - @verekia", 16 | "license": "MIT" 17 | } 18 | -------------------------------------------------------------------------------- /tutorial/1-node-npm-yarn-package-json/README.md: -------------------------------------------------------------------------------- 1 | # 1 - Node, NPM, Yarn и package.json 2 | 3 | В этой части мы настроим Node, NPM, Yarn и простой `package.json`. 4 | 5 | Прежде всего нам нужно установить Node, который необходим не только для серверной части (Back-End), но и для всех тех инструментов, которые входят в состав современных технологий для создания клиентской части (Front-End). 6 | 7 | Зайдите на [страницу загрузки](https://nodejs.org/en/download/current/) дистрибутива для macOS или Windows, или установите [через менеджер пакетов](https://nodejs.org/en/download/package-manager/) для Linux. 8 | 9 | Например, на **Ubuntu / Debian**, чтобы установить Node нужно выполнить следующие команды: 10 | 11 | ```bash 12 | curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 13 | sudo apt-get install -y nodejs 14 | ``` 15 | Вам подойдет любая версия Node > 6.5.0. 16 | 17 | `npm` - стандартный менеджер пакетов для Node, устанавливается автоматически, так что вам не нужно делать это вручную. 18 | 19 | **Примечание**: Если Node уже установлен, установите `nvm` ([Node Version Manager](https://github.com/creationix/nvm) - менеджер версий Node), выполните `nvm` install и пользуйтесь последней версией Node. 20 | 21 | [Yarn](https://yarnpkg.com/) - еще один менеджер пакетов. Он намного быстрее чем NPM, поддерживает работу офлайн и [лучше предугадывает](https://yarnpkg.com/en/docs/yarn-lock) подгрузку нужных зависимостей. С момента его [выхода](https://code.facebook.com/posts/1840075619545360) в октябре 2016, он был очень быстро принят сообществом и постепенно становится лучшим решением для JavaScript. В данном руководстве мы будем использовать Yarn. Если вы предпочитаете оставаться с NPM, просто замените все команды `yarn add` и `yarn add --dev` на `npm install --save` и `npm install --save-dev` в этом пособии. 22 | 23 | - Установите Yarn, следуя [инструкциям](https://yarnpkg.com/en/docs/install). Проще всего это сделать через `npm install -g yarn` или `sudo npm install -g yarn` (верно, мы используем NPM, чтобы установить Yarn, все равно что использовать Internet Explorer или Safari, чтобы установить Chrome!). 24 | 25 | - Создайте новую директорию для работы и зайдите (`cd`) в нее. 26 | - Запустите `yarn init` и ответьте на вопросы, чтобы создать файл `package.json` автоматически (`yarn init -y`, чтобы пропустить вопросы). 27 | - Создайте файл `index.js`, содержащий `console.log('Hello world')`. 28 | - Запустите `node .` в этой директории (`index.js` - файл по умолчанию, который Node будет запускать в текущей папке). Должно выйти "Hello world". 29 | 30 | Команда `node .` - слегка низкоуровневая для запуска программ. Вместо нее мы будем использовать NPM/Yarn скрипты, чтобы запускать выполнение нужного кода. Это даст нам хороший уровень абстракции, позволяющий всегда использовать `yarn start`, даже когда наша программа станет более сложной. 31 | 32 | - В файле `package.json`, в корневом объекте создайте объект `scripts`, чтобы было так: 33 | 34 | ```json 35 | "scripts": { 36 | "start": "node ." 37 | } 38 | ``` 39 | 40 | `package.json` должен быть действительным JSON файлом, это значит, что он не может содержать завершающие запятые (trailing commas). Так что будьте внимательны, редактируя его вручную. 41 | 42 | - Запустите `yarn start`. Должно выйти `Hello world`. 43 | 44 | - Создайте файл `.gitignore` и добавьте туда следующее: 45 | 46 | ```gitignore 47 | npm-debug.log 48 | yarn-error.log 49 | ``` 50 | 51 | **Примечание**: Если вы посмотрите на файлы `package.json`, которые я прилагаю к этому руководству, вы увидите там скрипт `tutorial-test`. Он есть в каждой части. Этот скрипт позволяет тестировать код на правильную работу при запуске `yarn && yarn start`. Вы можете удалить его из вашего проекта. 52 | 53 | Следующий раздел: [2 - Установка и использование пакетов](/tutorial/2-packages) 54 | 55 | Назад в [Содержание](/../../#Содержание). 56 | -------------------------------------------------------------------------------- /tutorial/1-node-npm-yarn-package-json/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 1 - Node, NPM, Yarn, and package.json 2 | 3 | In this section we will set up Node, NPM, Yarn, and a basic `package.json` file. 4 | 5 | First, we need to install Node, which is not only used for back-end JavaScript, but all the tools we need to build a modern Front-End stack. 6 | 7 | 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. 8 | 9 | For instance, on **Ubuntu / Debian**, you would run the following commands to install Node: 10 | 11 | ```bash 12 | curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 13 | sudo apt-get install -y nodejs 14 | ``` 15 | 16 | You want any version of Node > 6.5.0. 17 | 18 | `npm`, the default package manager for Node, comes automatically with Node, so you don't have to install it yourself. 19 | 20 | **Note**: If Node is already installed, install `nvm` ([Node Version Manager](https://github.com/creationix/nvm)), make `nvm` install and use the latest version of Node for you. 21 | 22 | [Yarn](https://yarnpkg.com/) is another package manager which is much faster than NPM, has offline support, and fetches dependencies [more predictably](https://yarnpkg.com/en/docs/yarn-lock). Since it [came out](https://code.facebook.com/posts/1840075619545360) in October 2016, it received a very quick adoption and is becoming the new package manager of choice of the JavaScript community. We are going to use Yarn in this tutorial. 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 --dev`. 23 | 24 | - Install Yarn by following the [instructions](https://yarnpkg.com/en/docs/install). You can likely install it with `npm install -g yarn` or `sudo npm install -g yarn` (yeah, we're using NPM to install Yarn, much like you would use Internet Explorer or Safari to install Chrome!). 25 | 26 | - Create a new folder to work in, and `cd` in it. 27 | - Run `yarn init` and answer the questions (`yarn init -y` to skip all questions), to generate a `package.json` file automatically. 28 | - Create an `index.js` file containing `console.log('Hello world')`. 29 | - Run `node .` in this folder (`index.js` is the default file Node looks for in the current folder). It should print "Hello world". 30 | 31 | 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. 32 | 33 | - In `package.json`, add a `scripts` object to the root object like so: 34 | 35 | ```json 36 | "scripts": { 37 | "start": "node ." 38 | } 39 | ``` 40 | 41 | `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. 42 | 43 | - Run `yarn start`. It should print `Hello world`. 44 | 45 | - Create a `.gitignore` file and add the following to it: 46 | 47 | ```gitignore 48 | npm-debug.log 49 | yarn-error.log 50 | ``` 51 | 52 | **Note**: If you take a look at the `package.json` files I provide, you will see a `tutorial-test` script in every chapter. Those scripts let me test that the chapter works fine when running `yarn && yarn start`. You can delete them in your own projects. 53 | 54 | Next section: [2 - Installing and using a package](/tutorial/2-packages) 55 | 56 | Back to the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 57 | -------------------------------------------------------------------------------- /tutorial/1-node-npm-yarn-package-json/index.js: -------------------------------------------------------------------------------- 1 | console.log('Hello world'); 2 | -------------------------------------------------------------------------------- /tutorial/1-node-npm-yarn-package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "node .", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "yarn start" 9 | }, 10 | "repository": "verekia/js-stack-from-scratch", 11 | "author": "Jonathan Verrecchia - @verekia", 12 | "license": "MIT" 13 | } 14 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/README.md: -------------------------------------------------------------------------------- 1 | # 10 - Immutable JS и улучшения Redux 2 | 3 | ## Immutable JS 4 | 5 | В отличие от предыдущей части, эта довольно простая и состоит из незначительных улучшений. 6 | 7 | Сначала мы добавим **Immutable JS** в наш проект. Immutable - это библиотека, позволяющая манипулировать объектами, не изменяя их. Вместо: 8 | 9 | ```javascript 10 | const obj = { a: 1 }; 11 | obj.a = 2; // Изменяет `obj` 12 | ``` 13 | 14 | Мы можем сделать так: 15 | 16 | ```javascript 17 | const obj = Immutable.Map({ a: 1 }); 18 | obj.set('a', 2); // Возвращает новый объект не изменяя `obj` 19 | ``` 20 | 21 | Такой подход соответствует парадигме **функционального программирования**, которая хорошо подходит для работы с Redux. Ваши reducer-функции вообще-то **должны** быть чистыми и не изменять состояние хранилища (переданного в качестве параметра), а вместо этого возвращать абсолютно новое. Давайте воспользуемся Immutable, чтобы достичь этого. 22 | 23 | - Запустите `yarn add immutable` 24 | 25 | Мы будем использовать имя `Map` в нашем проекте, но ESLint и конфигурация Airbnb начнут жаловаться на использование capitalized (где первая буква заглавная) имен, если это не имя класса. Добавьте следующее в `package.json` после `eslintConfig`: 26 | 27 | ```json 28 | "rules": { 29 | "new-cap": [ 30 | 2, 31 | { 32 | "capIsNewExceptions": [ 33 | "Map", 34 | "List" 35 | ] 36 | } 37 | ] 38 | } 39 | ``` 40 | 41 | Таким образом, мы внесли `Map` и `List` (два Immutable объекта, которые мы будем использовать постояно) в исключения к этому ESLint правилу. Такой подробный стиль форматирования JSON выполняется автоматически Yarn/NPM, так что мы, к сожалению, не можем сделать его более компактным. 42 | 43 | В любом случае, вернемся к Immutable: 44 | 45 | Настройте `dog-reducer.js`, чтобы он выглядел так: 46 | 47 | ```javascript 48 | import Immutable from 'immutable'; 49 | import { MAKE_BARK } from '../actions/dog-actions'; 50 | 51 | const initialState = Immutable.Map({ 52 | hasBarked: false, 53 | }); 54 | 55 | const dogReducer = (state = initialState, action) => { 56 | switch (action.type) { 57 | case MAKE_BARK: 58 | return state.set('hasBarked', action.payload); 59 | default: 60 | return state; 61 | } 62 | }; 63 | 64 | export default dogReducer; 65 | ``` 66 | 67 | Теперь мы создаем исходное состояние, используя Immutable Map, а новое состояние получаем применяя `set()`, что исключает любые мутации предыдущего состояния. 68 | 69 | В `containers/bark-message.js` обновите функцию `mapStateToProps`, чтобы она использовала `.get('hasBarked')` вместо `.hasBarked`: 70 | 71 | ```javascript 72 | const mapStateToProps = state => ({ 73 | message: state.dog.get('hasBarked') ? 'The dog barked' : 'The dog did not bark', 74 | }); 75 | ``` 76 | 77 | Приложение должно вести себя точно так же, как и до этого. 78 | 79 | **Примечание**: Если Babel жалуется на то, что Immutable превышает 100KB, добавьте `"compact": false` в `package.json` после `babel`. 80 | 81 | Как вы можете видеть из предыдущего фрагмента кода, сам объект state все еще содержит старый атрибут `dog`, являющийся простым объектом и подверженный мутациям. В нашем случае это нормально, но если вы хотите манипулировать только немутируемыми объектами, можете установить пакет `redux-immutable`, чтобы заменить функцию `combineReducers` у Redux. 82 | 83 | **Не обязательно**: 84 | 85 | - Запустите `yarn add redux-immutable` 86 | - Замените функцию `combineReducers` из `app.jsx` на ту, что мы импортировали из `redux-immutable`. 87 | - В `bark-message.js` замените `state.dog.get('hasBarked')` на `state.getIn(['dog', 'hasBarked'])`. 88 | 89 | ## Redux Действя (Actions) 90 | 91 | По мере того, как вы добавляете все больше и больше действий в ваше приложение, вы обнаружите, что приходится писать довольно много одного и того же кода. Пакет `redux-actions` помогает уменьшить этот повторяющийся код. С помощью `redux-actions` вы можете привести файл `dog-actions.js` к более компактному виду: 92 | 93 | ```javascript 94 | import { createAction } from 'redux-actions'; 95 | 96 | export const MAKE_BARK = 'MAKE_BARK'; 97 | export const makeBark = createAction(MAKE_BARK, () => true); 98 | ``` 99 | 100 | `redux-actions` основывается на модели [Flux Standard Action](https://github.com/acdlite/flux-standard-action), так же, как и действия, которые мы создавали до этого, так что интеграция `redux-actions` будет бесшовной, если вы придерживаетесь этой модели. 101 | 102 | - Не забудьте запустить `yarn add redux-actions`. 103 | 104 | Следующий раздел: [11 - Тестирование с Mocha, Chai и Sinon](/tutorial/11-testing-mocha-chai-sinon) 105 | 106 | Назад в [предыдущий раздел](/tutorial/9-redux) или [Содержание](/../../#Содержание). 107 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 10 - Immutable JS and Redux Improvements 2 | 3 | ## Immutable JS 4 | 5 | Unlike the previous chapter, this one is rather easy, and consists in minor improvements. 6 | 7 | First, we are going to add **Immutable JS** to our codebase. Immutable is a library to manipulate objects without mutating them. Instead of doing: 8 | 9 | ```javascript 10 | const obj = { a: 1 }; 11 | obj.a = 2; // Mutates `obj` 12 | ``` 13 | 14 | You would do: 15 | 16 | ```javascript 17 | const obj = Immutable.Map({ a: 1 }); 18 | obj.set('a', 2); // Returns a new object without mutating `obj` 19 | ``` 20 | 21 | This approach follows the **functional programming** paradigm, which works really well with Redux. Your reducer functions actually *have* to be pure functions that don't alter the state passed as parameter, but return a brand new state object instead. Let's use Immutable to enforce this. 22 | 23 | - Run `yarn add immutable` 24 | 25 | We are going to use `Map` in our codebase, but ESLint and the Airbnb config will complain about using a capitalized name without it being a class. Add the following to your `package.json` under `eslintConfig`: 26 | 27 | ```json 28 | "rules": { 29 | "new-cap": [ 30 | 2, 31 | { 32 | "capIsNewExceptions": [ 33 | "Map", 34 | "List" 35 | ] 36 | } 37 | ] 38 | } 39 | ``` 40 | 41 | This makes `Map` and `List` (the 2 Immutable objects you'll use all the time) exceptions to that ESLint rule. This verbose JSON formatting is actually done automatically by Yarn/NPM, so we cannot make it more compact unfortunately. 42 | 43 | Anyway, back to Immutable: 44 | 45 | In `dog-reducer.js` tweak your file so it looks like this: 46 | 47 | ```javascript 48 | import Immutable from 'immutable'; 49 | import { MAKE_BARK } from '../actions/dog-actions'; 50 | 51 | const initialState = Immutable.Map({ 52 | hasBarked: false, 53 | }); 54 | 55 | const dogReducer = (state = initialState, action) => { 56 | switch (action.type) { 57 | case MAKE_BARK: 58 | return state.set('hasBarked', action.payload); 59 | default: 60 | return state; 61 | } 62 | }; 63 | 64 | export default dogReducer; 65 | ``` 66 | 67 | The initial state is now built using an Immutable Map, and the new state is generated using `set()`, preventing any mutation of the previous state. 68 | 69 | In `containers/bark-message.js`, update the `mapStateToProps` function to use `.get('hasBarked')` instead of `.hasBarked`: 70 | 71 | ```javascript 72 | const mapStateToProps = state => ({ 73 | message: state.dog.get('hasBarked') ? 'The dog barked' : 'The dog did not bark', 74 | }); 75 | ``` 76 | 77 | The app should still behave exactly the way it did before. 78 | 79 | **Note**: If Babel complains about Immutable exceeding 100KB, add `"compact": false` to your `package.json` under `babel`. 80 | 81 | As you can see from the code snippet above, our state object still contains a plain old `dog` object attribute, which isn't immutable. It is fine this way, but if you want to only manipulate immutable objects, you could install the `redux-immutable` package to replace Redux's `combineReducers` function. 82 | 83 | **Optional**: 84 | 85 | - Run `yarn add redux-immutable` 86 | - Replace your `combineReducers` function in `app.jsx` to use the one imported from `redux-immutable` instead. 87 | - In `bark-message.js` replace `state.dog.get('hasBarked')` by `state.getIn(['dog', 'hasBarked'])`. 88 | 89 | ## Redux Actions 90 | 91 | As you add more and more actions to your app, you will find yourself writing quite a lot of the same boilerplate. The `redux-actions` package helps reducing that boilerplate code. With `redux-actions` you can rewrite your `dog-actions.js` file in a more compact way: 92 | 93 | ```javascript 94 | import { createAction } from 'redux-actions'; 95 | 96 | export const MAKE_BARK = 'MAKE_BARK'; 97 | export const makeBark = createAction(MAKE_BARK, () => true); 98 | ``` 99 | 100 | `redux-actions` implement the [Flux Standard Action](https://github.com/acdlite/flux-standard-action) model, just like the action we previously wrote, so integrating `redux-actions` is seamless if you follow this model. 101 | 102 | - Don't forget to run `yarn add redux-actions`. 103 | 104 | Next section: [11 - Testing with Mocha, Chai, and Sinon](/tutorial/11-testing-mocha-chai-sinon) 105 | 106 | Back to the [previous section](/tutorial/9-redux) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 107 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import webpack from 'webpack-stream'; 8 | import webpackConfig from './webpack.config.babel'; 9 | 10 | const paths = { 11 | allSrcJs: 'src/**/*.js?(x)', 12 | serverSrcJs: 'src/server/**/*.js?(x)', 13 | sharedSrcJs: 'src/shared/**/*.js?(x)', 14 | clientEntryPoint: 'src/client/app.jsx', 15 | clientBundle: 'dist/client-bundle.js?(.map)', 16 | gulpFile: 'gulpfile.babel.js', 17 | webpackFile: 'webpack.config.babel.js', 18 | libDir: 'lib', 19 | distDir: 'dist', 20 | }; 21 | 22 | gulp.task('lint', () => 23 | gulp.src([ 24 | paths.allSrcJs, 25 | paths.gulpFile, 26 | paths.webpackFile, 27 | ]) 28 | .pipe(eslint()) 29 | .pipe(eslint.format()) 30 | .pipe(eslint.failAfterError()) 31 | ); 32 | 33 | gulp.task('clean', () => del([ 34 | paths.libDir, 35 | paths.clientBundle, 36 | ])); 37 | 38 | gulp.task('build', ['lint', 'clean'], () => 39 | gulp.src(paths.allSrcJs) 40 | .pipe(babel()) 41 | .pipe(gulp.dest(paths.libDir)) 42 | ); 43 | 44 | gulp.task('main', ['lint', 'clean'], () => 45 | gulp.src(paths.clientEntryPoint) 46 | .pipe(webpack(webpackConfig)) 47 | .pipe(gulp.dest(paths.distDir)) 48 | ); 49 | 50 | gulp.task('watch', () => { 51 | gulp.watch(paths.allSrcJs, ['main']); 52 | }); 53 | 54 | gulp.task('default', ['watch', 'main']); 55 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | }, 18 | "rules": { 19 | "new-cap": [ 20 | 2, 21 | { 22 | "capIsNewExceptions": [ 23 | "Map", 24 | "List" 25 | ] 26 | } 27 | ] 28 | } 29 | }, 30 | "babel": { 31 | "presets": [ 32 | "latest", 33 | "react" 34 | ], 35 | "compact": false 36 | }, 37 | "dependencies": { 38 | "babel-polyfill": "^6.16.0", 39 | "immutable": "^3.8.1", 40 | "react": "^15.3.2", 41 | "react-dom": "^15.3.2", 42 | "react-redux": "^4.4.5", 43 | "redux": "^3.6.0", 44 | "redux-actions": "^0.12.0", 45 | "redux-immutable": "^3.0.8" 46 | }, 47 | "devDependencies": { 48 | "babel-loader": "^6.2.5", 49 | "babel-preset-latest": "^6.16.0", 50 | "babel-preset-react": "^6.16.0", 51 | "del": "^2.2.2", 52 | "eslint": "^3.8.1", 53 | "eslint-config-airbnb": "^12.0.0", 54 | "eslint-plugin-import": "^2.0.1", 55 | "eslint-plugin-jsx-a11y": "^2.2.3", 56 | "eslint-plugin-react": "^6.4.1", 57 | "gulp": "^3.9.1", 58 | "gulp-babel": "^6.1.2", 59 | "gulp-eslint": "^3.0.1", 60 | "webpack-stream": "^3.2.0" 61 | }, 62 | "repository": "verekia/js-stack-from-scratch", 63 | "author": "Jonathan Verrecchia - @verekia", 64 | "license": "MIT" 65 | } 66 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/actions/dog-actions.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | export const MAKE_BARK = 'MAKE_BARK'; 4 | export const makeBark = createAction(MAKE_BARK, () => true); 5 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { createStore } from 'redux'; 6 | import { Provider } from 'react-redux'; 7 | import { combineReducers } from 'redux-immutable'; 8 | import dogReducer from './reducers/dog-reducer'; 9 | import BarkMessage from './containers/bark-message'; 10 | import BarkButton from './containers/bark-button'; 11 | 12 | const store = createStore(combineReducers({ 13 | dog: dogReducer, 14 | })); 15 | 16 | ReactDOM.render( 17 | 18 |
19 | 20 | 21 |
22 |
23 | , document.querySelector('.app') 24 | ); 25 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/components/button.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Button = ({ action, actionLabel }) => ; 4 | 5 | Button.propTypes = { 6 | action: PropTypes.func.isRequired, 7 | actionLabel: PropTypes.string.isRequired, 8 | }; 9 | 10 | export default Button; 11 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/components/message.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Message = ({ message }) =>
{message}
; 4 | 5 | Message.propTypes = { 6 | message: PropTypes.string.isRequired, 7 | }; 8 | 9 | export default Message; 10 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/containers/bark-button.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Button from '../components/button'; 3 | import { makeBark } from '../actions/dog-actions'; 4 | 5 | const mapDispatchToProps = dispatch => ({ 6 | action: () => { dispatch(makeBark()); }, 7 | actionLabel: 'Bark', 8 | }); 9 | 10 | export default connect(null, mapDispatchToProps)(Button); 11 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/containers/bark-message.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Message from '../components/message'; 3 | 4 | const mapStateToProps = state => ({ 5 | message: state.getIn(['dog', 'hasBarked']) ? 'The dog barked' : 'The dog did not bark', 6 | }); 7 | 8 | export default connect(mapStateToProps)(Message); 9 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/reducers/dog-reducer.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { MAKE_BARK } from '../actions/dog-actions'; 3 | 4 | const initialState = Immutable.Map({ 5 | hasBarked: false, 6 | }); 7 | 8 | const dogReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case MAKE_BARK: 11 | return state.set('hasBarked', action.payload); 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default dogReducer; 18 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/README.md: -------------------------------------------------------------------------------- 1 | # 11 - Тестирование с Mocha, Chai и Sinon 2 | 3 | ## Mocha и Chai 4 | 5 | - Создайте директорию `src/test`. Эта папка будет отражать структуру директорий нашего приложения, поэтому создайте также `src/test/client` (можете также добавить `server` и `shared`, если хотите, но мы не будем писать тесты для них). 6 | 7 | - В `src/test/client` создайте файл `state-test.js`, в котором мы будем тестировать жизненный цикл нашего Redux приложения. 8 | 9 | Мы будем использовать [Mocha](http://mochajs.org/) в качесве основного фреймворка для тестирования. Mocha прост в использовании, имеет множество возможностей, и на данный момент [самый популярный фреймворк для тестирования](http://stateofjs.com/2016/testing/). Он модульный и очень гибкий. В частности, он позволяет использовать любые библиотеки утверждений (assertion) по вашему желанию. [Chai](http://chaijs.com/) - замечательная библиотека утверждений, имеющая много доступных [плагинов](http://chaijs.com/plugins/) и позволяющая вам выбирать между различными стилями утверждений. 10 | 11 | - Установим Mocha и Chai, выполним `yarn add --dev mocha chai` 12 | 13 | В `state-test.js` напишите следующее: 14 | 15 | ```javascript 16 | /* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */ 17 | 18 | import { createStore } from 'redux'; 19 | import { combineReducers } from 'redux-immutable'; 20 | import { should } from 'chai'; 21 | import { describe, it, beforeEach } from 'mocha'; 22 | import dogReducer from '../../client/reducers/dog-reducer'; 23 | import { makeBark } from '../../client/actions/dog-actions'; 24 | 25 | should(); 26 | let store; 27 | 28 | describe('App State', () => { 29 | describe('Dog', () => { 30 | beforeEach(() => { 31 | store = createStore(combineReducers({ 32 | dog: dogReducer, 33 | })); 34 | }); 35 | describe('makeBark', () => { 36 | it('should make hasBarked go from false to true', () => { 37 | store.getState().getIn(['dog', 'hasBarked']).should.be.false; 38 | store.dispatch(makeBark()); 39 | store.getState().getIn(['dog', 'hasBarked']).should.be.true; 40 | }); 41 | }); 42 | }); 43 | }); 44 | ``` 45 | 46 | Хорошо, теперь давайте все это проанализируем. 47 | 48 | Во-первых, заметьте, что мы импортировали стиль утверждений `should` из пакета `chai`. Это позволит нам делать утверждения, используя синтаксис вида `mynumber.should.equal(3)` (что в русском переводе можно представить как `моечисло.должно.ровняться(3)` - прим. пер.), довольно изящно. Для того чтобы мы могли вызывать `should` на любом объекте, мы должны прежде всего запустить функцию `should()`. Некоторые из этих утверждений являются *выражениями*, как `mybook.should.be.true`, что заставляет ESLint сердиться, так что мы добавляем для него комментарий в начале, чтобы отключить правило `no-unused-expressions` для этого файла. 49 | 50 | Тесты Mocha устроены наподобие дерева. В нашем случае мы хотим протестировать функцию `makeBark`, которая должна воздействовать на атрибут `dog` состояния нашего приложения, поэтому имеет смысл использовать следующую иерархию тестов: `App State > Dog > makeBark`, что мы и описали, используя `describe()`. `it()` - это собственно, тестирующая функция, а `beforeEach()` - это функция, вызываемая перед каждым вызовом теста `it()`. В нашем случае мы хотим иметь новую чистую версию хранилища перед запуском каждого теста. Мы объявили переменную `store` в начале файла, поскольку она нам пригодится в каждом тесте. 51 | 52 | Тест `makeBark` вполне понятен, а строка с описанием в `it()` делает его еще яснее: мы проверяем, что `hasBarked` меняется с `false` на `true` после вызова `makeBark`. 53 | 54 | Отлично, запустим этот тест! 55 | 56 | - В файле `gulpfile.babel.js` создайте следующую задачу `test`, которая основывается на плагине `gulp-mocha`: 57 | 58 | ```javascript 59 | import mocha from 'gulp-mocha'; 60 | 61 | const paths = { 62 | // [...] 63 | allLibTests: 'lib/test/**/*.js', 64 | }; 65 | 66 | // [...] 67 | 68 | gulp.task('test', ['build'], () => 69 | gulp.src(paths.allLibTests) 70 | .pipe(mocha()) 71 | ); 72 | ``` 73 | 74 | - Конечно же, выполните `yarn add --dev gulp-mocha`. 75 | 76 | Как вы можете видеть, тесты запускаются на транспилированом коде из папки `lib`, вот почему задачу `test` предваряет запуск `build`. `build`, в свою очередь, предваряется задачей `lint`, а сам `test` мы будем запускать перед `main`, что в итоге даст нам следующий каскад задач для `default`: `lint` > `build` > `test` > `main`. 77 | 78 | - Установите в `main` предварительный запуск команды `test`: 79 | 80 | ```javascript 81 | gulp.task('main', ['test'], () => /* ... */ ); 82 | ``` 83 | 84 | - В `package.json`, замените текущее значение скрипта `"test"` на следующее: `"test": "gulp test"`. Таким образом, мы можем использовать `yarn test`, чтобы просто запустить наши тесты. Так же `test` - это стандартный скрипт, который автоматически запускается такими инструментами как, например, сервисы непрерывной интеграции (continuous integration services, CI), так что всегда добавляйте запуск тестов через него. `yarn start` также запустит тестирование перед построением сборки Webpack, так что сборка сгенерируется только, если все тесты будут пройдены. 85 | 86 | - Запустите `yarn test` или `yarn start`, и должны будут выйти результаты ваших тестов, предпочтительно зеленые. 87 | 88 | ## Sinon 89 | 90 | В некоторых случаях, мы хотим иметь возможность *эмулировать* некоторые вещи в юнит тестах. Например, давайте скажем, у нас есть функция `deleteEverything`, которая содержит вызов `deleteDatabases()`. Запуск `deleteDatabases()` вызовет много побочных эффектов, которые нам абсолютно нежелательны во время тестирования. 91 | 92 | [Sinon](http://sinonjs.org/) - библиотека тестирования, предлагающая **Заглушки** (и многие другие вещи), позволяет нейтрализовать `deleteDatabases` и просто мониторить ее, не запуская на самом деле. Таким образом, к примеру, мы можем проверить была ли она запущена или с какими параметрами она была запущена. Обычно это очень полезно для эмуляции или исключения AJAX вызовов, которые могут вызвать побочные эффекты на сервере. 93 | 94 | В рамках нашего приложения, мы добавим метод `barkInConsole` в класс `Dog` в файле `src/shared/dog.js`: 95 | 96 | ```javascript 97 | class Dog { 98 | constructor(name) { 99 | this.name = name; 100 | } 101 | 102 | bark() { 103 | return `Wah wah, I am ${this.name}`; 104 | } 105 | 106 | barkInConsole() { 107 | /* eslint-disable no-console */ 108 | console.log(this.bark()); 109 | /* eslint-enable no-console */ 110 | } 111 | } 112 | 113 | export default Dog; 114 | ``` 115 | Если мы запустим `barkInConsole` в нашем юнит тесте, то `console.log()` выведет что-то в терминал. Давайте мы будем это рассматривать, как нежелательный побочный эффект в рамках нашего юнит теста. Тем не менее мы желаем знать, была ли `console.log()` *нормально запущена*, и какие параметры были *переданы ей при вызове*. 116 | 117 | - Создайте новый файл `src/test/shared/dog-test.js` и добавьте туда следующее: 118 | 119 | ```javascript 120 | /* eslint-disable import/no-extraneous-dependencies, no-console */ 121 | 122 | import chai from 'chai'; 123 | import { stub } from 'sinon'; 124 | import sinonChai from 'sinon-chai'; 125 | import { describe, it } from 'mocha'; 126 | import Dog from '../../shared/dog'; 127 | 128 | chai.should(); 129 | chai.use(sinonChai); 130 | 131 | describe('Shared', () => { 132 | describe('Dog', () => { 133 | describe('barkInConsole', () => { 134 | it('should print a bark string with its name', () => { 135 | stub(console, 'log'); 136 | new Dog('Test Toby').barkInConsole(); 137 | console.log.should.have.been.calledWith('Wah wah, I am Test Toby'); 138 | console.log.restore(); 139 | }); 140 | }); 141 | }); 142 | }); 143 | ``` 144 | 145 | Тут мы используем *заглушки* от Sinon и плагин для Chai, позволяющий использовать Chai-утверждения на Sinon и им подобных заглушках. 146 | 147 | - Запустите `yarn add --dev sinon sinon-chai`, чтобы установить эти библиотеки. 148 | 149 | Что здесь нового? Ну прежде всего мы вызываем `chai.use(sinonChai)`, чтобы активировать плагин для Chai. Затем вся магия происходит внутри `it()`: `stub(console, 'log')` нейтрализует `console.log` и следит за ней. Во время выполнения `new Dog('Test Toby').barkInConsole()`, `console.log` должна была бы сработать. Мы проверяем этот вызов `console.log` с помощью `console.log.should.have.been.calledWith()`, а затем восстанавливаем с помощью `restore` нейтрализированную `console.log`, чтобы позволить ей дальше работать нормально. 150 | 151 | **Важное замечание**: Заглушать `console.log` не рекомендуется, потому что если тест провалится, то `console.log.restore()` никогда не запустится, и следовательно `console.log` останется неисправной для всех остальных команд, выполняемых в терминале. При этом даже не выйдет сообщения об ошибке прохождения теста, так что вы останетесь с очень малой информацией о том, что же произошло. Это может оказаться достаточно неприятно. Тем не менее, это хороший пример, иллюстрирующий применение заглушек в этом простом приложении. 152 | 153 | Если в этом разделе все прошло хорошо, то у вас должно быть два пройденных теста. 154 | 155 | Следующий раздел: [12 - Типизация с Flow](/tutorial/12-flow) 156 | 157 | Назад в [предыдущий раздел](/tutorial/10-immutable-redux-improvements) или [Содержание](/../../#Содержание). 158 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 11 - Testing with Mocha, Chai, and Sinon 2 | 3 | ## Mocha and Chai 4 | 5 | - Create an `src/test` folder. This folder will mirror our application folder structure, so create a `src/test/client` folder as well (feel free to add `server` and `shared` if you want, but we're not going to write tests for these). 6 | 7 | - In `src/test/client`, create a `state-test.js` file, which we are going to use to test our Redux application life cycle. 8 | 9 | We are going to use [Mocha](http://mochajs.org/) as our main testing framework. Mocha is easy to use, has tons of features, and is currently the [most popular JavaScript testing framework](http://stateofjs.com/2016/testing/). It is very flexible and modular. In particular, it lets you use any assertion library you want. [Chai](http://chaijs.com/) is a great assertion library that has a lot of [plugins](http://chaijs.com/plugins/) available and lets you choose between different assertion styles. 10 | 11 | - Let's install Mocha and Chai by running `yarn add --dev mocha chai` 12 | 13 | In `state-test.js`, write the following: 14 | 15 | ```javascript 16 | /* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */ 17 | 18 | import { createStore } from 'redux'; 19 | import { combineReducers } from 'redux-immutable'; 20 | import { should } from 'chai'; 21 | import { describe, it, beforeEach } from 'mocha'; 22 | import dogReducer from '../../client/reducers/dog-reducer'; 23 | import { makeBark } from '../../client/actions/dog-actions'; 24 | 25 | should(); 26 | let store; 27 | 28 | describe('App State', () => { 29 | describe('Dog', () => { 30 | beforeEach(() => { 31 | store = createStore(combineReducers({ 32 | dog: dogReducer, 33 | })); 34 | }); 35 | describe('makeBark', () => { 36 | it('should make hasBarked go from false to true', () => { 37 | store.getState().getIn(['dog', 'hasBarked']).should.be.false; 38 | store.dispatch(makeBark()); 39 | store.getState().getIn(['dog', 'hasBarked']).should.be.true; 40 | }); 41 | }); 42 | }); 43 | }); 44 | ``` 45 | 46 | Alright, let's analyze this whole thing. 47 | 48 | First, notice how we import the `should` assertion style from `chai`. This lets us assert things using a syntax like `mynumber.should.equal(3)`, pretty neat. In order to be able to call `should` on any object, we need to run the function `should()` before anything. Some of these assertion are *expressions*, like `mybook.should.be.true`, which will make ESLint grumpy, so we've added an ESLint comment at the top to disable the `no-unused-expressions` rule in this file. 49 | 50 | Mocha tests work like a tree. In our case, we want to test the `makeBark` function which should affect the `dog` attribute of the application state, so it makes sense to use the following hierarchy of tests: `App State > Dog > makeBark`, that we declare using `describe()`. `it()` is the actual test function and `beforeEach()` is a function that is executed before each `it()` test. In our case, we want a fresh new store before running each test. We declare a `store` variable at the top of the file because it should be useful in every test of this file. 51 | 52 | Our `makeBark` test is very explicit, and the description provided as a string in `it()` makes it even clearer: we test that `hasBarked` go from `false` to `true` after calling `makeBark`. 53 | 54 | Alright, let's run this test! 55 | 56 | - In `gulpfile.babel.js`, create the following `test` task, which relies on the `gulp-mocha` plugin: 57 | 58 | ```javascript 59 | import mocha from 'gulp-mocha'; 60 | 61 | const paths = { 62 | // [...] 63 | allLibTests: 'lib/test/**/*.js', 64 | }; 65 | 66 | // [...] 67 | 68 | gulp.task('test', ['build'], () => 69 | gulp.src(paths.allLibTests) 70 | .pipe(mocha()) 71 | ); 72 | ``` 73 | 74 | - Run `yarn add --dev gulp-mocha` of course. 75 | 76 | As you can see, tests are run on transpiled code in `lib`, which is why `build` is a prerequisite task of `test`. `build` also has a prerequisite, `lint`, and finally, we are making `test` a prerequisite of `main`, which gives us the following task cascade for the `default` task: `lint` > `build` > `test` > `main`. 77 | 78 | - Change the prerequisite of `main` to `test`: 79 | 80 | ```javascript 81 | gulp.task('main', ['test'], () => /* ... */ ); 82 | ``` 83 | 84 | - In `package.json`, replace the current `"test"` script by: `"test": "gulp test"`. This way you can use `yarn test` to just run your tests. `test` is also the standard script that will be automatically called by tools like continuous integration services for instance, so you should always bind your test task to it. `yarn start` will run the tests before building the Webpack client bundle as well, so it will only build it if all tests pass. 85 | 86 | - Run `yarn test` or `yarn start`, and it should print the result for our test, hopefully green. 87 | 88 | ## Sinon 89 | 90 | In some cases, we want to be able to *fake* things in a unit test. For instance, let's say we have a function, `deleteEverything`, which contains a call to `deleteDatabases()`. Running `deleteDatabases()` causes a lot of side-effects, which we absolutely don't want to happen when running our test suite. 91 | 92 | [Sinon](http://sinonjs.org/) is a testing library that offers **Stubs** (and a lot of other things), which allow us to neutralize `deleteDatabases` and simply monitor it without actually calling it. This way we can test if it got called, or which parameters it got called with for instance. This is typically very useful to fake or avoid AJAX calls - which can cause side-effects on the back-end. 93 | 94 | In the context of our app, we are going to add a `barkInConsole` method to our `Dog` class in `src/shared/dog.js`: 95 | 96 | ```javascript 97 | class Dog { 98 | constructor(name) { 99 | this.name = name; 100 | } 101 | 102 | bark() { 103 | return `Wah wah, I am ${this.name}`; 104 | } 105 | 106 | barkInConsole() { 107 | /* eslint-disable no-console */ 108 | console.log(this.bark()); 109 | /* eslint-enable no-console */ 110 | } 111 | } 112 | 113 | export default Dog; 114 | ``` 115 | 116 | If we run `barkInConsole` in a unit test, `console.log()` will print things in the terminal. We are going to consider this to be an undesired side-effect in the context of our unit tests. We are interested in knowing if `console.log()` *would have normally been called* though, and we want to test what parameters it *would have been called with*. 117 | 118 | - Create a new `src/test/shared/dog-test.js` file, and add write the following: 119 | 120 | ```javascript 121 | /* eslint-disable import/no-extraneous-dependencies, no-console */ 122 | 123 | import chai from 'chai'; 124 | import { stub } from 'sinon'; 125 | import sinonChai from 'sinon-chai'; 126 | import { describe, it } from 'mocha'; 127 | import Dog from '../../shared/dog'; 128 | 129 | chai.should(); 130 | chai.use(sinonChai); 131 | 132 | describe('Shared', () => { 133 | describe('Dog', () => { 134 | describe('barkInConsole', () => { 135 | it('should print a bark string with its name', () => { 136 | stub(console, 'log'); 137 | new Dog('Test Toby').barkInConsole(); 138 | console.log.should.have.been.calledWith('Wah wah, I am Test Toby'); 139 | console.log.restore(); 140 | }); 141 | }); 142 | }); 143 | }); 144 | ``` 145 | 146 | Here, we are using *stubs* from Sinon, and a Chai plugin to be able to use Chai assertions on Sinon stubs and such. 147 | 148 | - Run `yarn add --dev sinon sinon-chai` to install these libraries. 149 | 150 | So what is new here? Well first of all, we call `chai.use(sinonChai)` to activate the Chai plugin. Then, all the magic happens in the `it()` statement: `stub(console, 'log')` is going to neutralize `console.log` and monitor it. When `new Dog('Test Toby').barkInConsole()` is executed, a `console.log` is normally supposed to happen. We test this call to `console.log` with `console.log.should.have.been.calledWith()`, and finally, we `restore` the neutralized `console.log` to make it work normally again. 151 | 152 | **Important note**: Stubbing `console.log` is not recommended, because if the test fails, `console.log.restore()` is never called, and therefore `console.log` will remain broken for the rest of the command you executed in your terminal! It won't even print the error message that caused the test to fail, so it leaves you with very little information about what happened. That can be quite confusing. It is a good example to illustrate stubs in this simple app though. 153 | 154 | If everything went well in this chapter, you should have 2 passing tests. 155 | 156 | Next section: [12 - Type Checking with Flow](/tutorial/12-flow) 157 | 158 | Back to the [previous section](/tutorial/10-immutable-redux-improvements) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 159 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import mocha from 'gulp-mocha'; 7 | import del from 'del'; 8 | import webpack from 'webpack-stream'; 9 | import webpackConfig from './webpack.config.babel'; 10 | 11 | const paths = { 12 | allSrcJs: 'src/**/*.js?(x)', 13 | serverSrcJs: 'src/server/**/*.js?(x)', 14 | sharedSrcJs: 'src/shared/**/*.js?(x)', 15 | allLibTests: 'lib/test/**/*.js', 16 | clientEntryPoint: 'src/client/app.jsx', 17 | clientBundle: 'dist/client-bundle.js?(.map)', 18 | gulpFile: 'gulpfile.babel.js', 19 | webpackFile: 'webpack.config.babel.js', 20 | libDir: 'lib', 21 | distDir: 'dist', 22 | }; 23 | 24 | gulp.task('lint', () => 25 | gulp.src([ 26 | paths.allSrcJs, 27 | paths.gulpFile, 28 | paths.webpackFile, 29 | ]) 30 | .pipe(eslint()) 31 | .pipe(eslint.format()) 32 | .pipe(eslint.failAfterError()) 33 | ); 34 | 35 | gulp.task('clean', () => del([ 36 | paths.libDir, 37 | paths.clientBundle, 38 | ])); 39 | 40 | gulp.task('build', ['lint', 'clean'], () => 41 | gulp.src(paths.allSrcJs) 42 | .pipe(babel()) 43 | .pipe(gulp.dest(paths.libDir)) 44 | ); 45 | 46 | gulp.task('test', ['build'], () => 47 | gulp.src(paths.allLibTests) 48 | .pipe(mocha()) 49 | ); 50 | 51 | gulp.task('main', ['test'], () => 52 | gulp.src(paths.clientEntryPoint) 53 | .pipe(webpack(webpackConfig)) 54 | .pipe(gulp.dest(paths.distDir)) 55 | ); 56 | 57 | gulp.task('watch', () => { 58 | gulp.watch(paths.allSrcJs, ['main']); 59 | }); 60 | 61 | gulp.task('default', ['watch', 'main']); 62 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "gulp test", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | }, 18 | "rules": { 19 | "new-cap": [ 20 | 2, 21 | { 22 | "capIsNewExceptions": [ 23 | "Map", 24 | "List" 25 | ] 26 | } 27 | ] 28 | } 29 | }, 30 | "babel": { 31 | "presets": [ 32 | "latest", 33 | "react" 34 | ], 35 | "compact": false 36 | }, 37 | "dependencies": { 38 | "babel-polyfill": "^6.16.0", 39 | "immutable": "^3.8.1", 40 | "react": "^15.3.2", 41 | "react-dom": "^15.3.2", 42 | "react-redux": "^4.4.5", 43 | "redux": "^3.6.0", 44 | "redux-actions": "^0.12.0", 45 | "redux-immutable": "^3.0.8" 46 | }, 47 | "devDependencies": { 48 | "babel-loader": "^6.2.5", 49 | "babel-preset-latest": "^6.16.0", 50 | "babel-preset-react": "^6.16.0", 51 | "chai": "^3.5.0", 52 | "del": "^2.2.2", 53 | "eslint": "^3.8.1", 54 | "eslint-config-airbnb": "^12.0.0", 55 | "eslint-plugin-import": "^2.0.1", 56 | "eslint-plugin-jsx-a11y": "^2.2.3", 57 | "eslint-plugin-react": "^6.4.1", 58 | "gulp": "^3.9.1", 59 | "gulp-babel": "^6.1.2", 60 | "gulp-eslint": "^3.0.1", 61 | "gulp-mocha": "^3.0.1", 62 | "mocha": "^3.1.2", 63 | "sinon": "^1.17.6", 64 | "sinon-chai": "^2.8.0", 65 | "webpack-stream": "^3.2.0" 66 | }, 67 | "repository": "verekia/js-stack-from-scratch", 68 | "author": "Jonathan Verrecchia - @verekia", 69 | "license": "MIT" 70 | } 71 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/actions/dog-actions.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | export const MAKE_BARK = 'MAKE_BARK'; 4 | export const makeBark = createAction(MAKE_BARK, () => true); 5 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { createStore } from 'redux'; 6 | import { Provider } from 'react-redux'; 7 | import { combineReducers } from 'redux-immutable'; 8 | import dogReducer from './reducers/dog-reducer'; 9 | import BarkMessage from './containers/bark-message'; 10 | import BarkButton from './containers/bark-button'; 11 | 12 | const store = createStore(combineReducers({ 13 | dog: dogReducer, 14 | })); 15 | 16 | ReactDOM.render( 17 | 18 |
19 | 20 | 21 |
22 |
23 | , document.querySelector('.app') 24 | ); 25 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/components/button.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Button = ({ action, actionLabel }) => ; 4 | 5 | Button.propTypes = { 6 | action: PropTypes.func.isRequired, 7 | actionLabel: PropTypes.string.isRequired, 8 | }; 9 | 10 | export default Button; 11 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/components/message.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Message = ({ message }) =>
{message}
; 4 | 5 | Message.propTypes = { 6 | message: PropTypes.string.isRequired, 7 | }; 8 | 9 | export default Message; 10 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/containers/bark-button.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Button from '../components/button'; 3 | import { makeBark } from '../actions/dog-actions'; 4 | 5 | const mapDispatchToProps = dispatch => ({ 6 | action: () => { dispatch(makeBark()); }, 7 | actionLabel: 'Bark', 8 | }); 9 | 10 | export default connect(null, mapDispatchToProps)(Button); 11 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/containers/bark-message.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Message from '../components/message'; 3 | 4 | const mapStateToProps = state => ({ 5 | message: state.getIn(['dog', 'hasBarked']) ? 'The dog barked' : 'The dog did not bark', 6 | }); 7 | 8 | export default connect(mapStateToProps)(Message); 9 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/reducers/dog-reducer.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { MAKE_BARK } from '../actions/dog-actions'; 3 | 4 | const initialState = Immutable.Map({ 5 | hasBarked: false, 6 | }); 7 | 8 | const dogReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case MAKE_BARK: 11 | return state.set('hasBarked', action.payload); 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default dogReducer; 18 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | 11 | barkInConsole() { 12 | /* eslint-disable no-console */ 13 | console.log(this.bark()); 14 | /* eslint-enable no-console */ 15 | } 16 | } 17 | 18 | export default Dog; 19 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/test/client/state-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */ 2 | 3 | import { createStore } from 'redux'; 4 | import { combineReducers } from 'redux-immutable'; 5 | import { should } from 'chai'; 6 | import { describe, it, beforeEach } from 'mocha'; 7 | import dogReducer from '../../client/reducers/dog-reducer'; 8 | import { makeBark } from '../../client/actions/dog-actions'; 9 | 10 | should(); 11 | let store; 12 | 13 | describe('App State', () => { 14 | describe('Dog', () => { 15 | beforeEach(() => { 16 | store = createStore(combineReducers({ 17 | dog: dogReducer, 18 | })); 19 | }); 20 | describe('makeBark', () => { 21 | it('should make hasBarked go from false to true', () => { 22 | store.getState().getIn(['dog', 'hasBarked']).should.be.false; 23 | store.dispatch(makeBark()); 24 | store.getState().getIn(['dog', 'hasBarked']).should.be.true; 25 | }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/test/shared/dog-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, no-console */ 2 | 3 | import chai from 'chai'; 4 | import { stub } from 'sinon'; 5 | import sinonChai from 'sinon-chai'; 6 | import { describe, it } from 'mocha'; 7 | import Dog from '../../shared/dog'; 8 | 9 | chai.should(); 10 | chai.use(sinonChai); 11 | 12 | describe('Shared', () => { 13 | describe('Dog', () => { 14 | describe('barkInConsole', () => { 15 | it('should print a bark string with its name', () => { 16 | stub(console, 'log'); 17 | new Dog('Test Toby').barkInConsole(); 18 | console.log.should.have.been.calledWith('Wah wah, I am Test Toby'); 19 | console.log.restore(); 20 | }); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/12-flow/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | .*/node_modules/gulp-flowtype/.* 4 | -------------------------------------------------------------------------------- /tutorial/12-flow/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/12-flow/README.md: -------------------------------------------------------------------------------- 1 | # 12 - Flow 2 | 3 | [Flow](https://flowtype.org/) - статический типизатор (static type checker). Он определяет несоответствие типов в вашем коде и позволяет напрямую декларировать типы через аннотации. 4 | 5 | - Для того чтобы Babel мог понимать и убирать аннотации Flow в процессе транспиляции, установите плагин Flow для Babel, выполнив `yarn add --dev babel-preset-flow`. Затем добавьте `"flow"` после `babel.presets` в `package.json`. 6 | 7 | - Создайте пустой файл `.flowconfig` в корне проекта. 8 | 9 | - Запустите `yarn add --dev gulp-flowtype`, чтобы установить Gulp плагин для Flow, и добавьте `flow()` в задачу `lint`: 10 | 11 | ```javascript 12 | import flow from 'gulp-flowtype'; 13 | 14 | // [...] 15 | 16 | gulp.task('lint', () => 17 | gulp.src([ 18 | paths.allSrcJs, 19 | paths.gulpFile, 20 | paths.webpackFile, 21 | ]) 22 | .pipe(eslint()) 23 | .pipe(eslint.format()) 24 | .pipe(eslint.failAfterError()) 25 | .pipe(flow({ abort: true })) // Add Flow here 26 | ); 27 | ``` 28 | 29 | Опция `abort` прерывает задачу Gulp, если Flow обнаруживает проблему. 30 | 31 | Отлично, теперь мы можем запустить Flow. 32 | 33 | - Добавьте аннотации Flow в `src/shared/dog.js` так, чтобы: 34 | 35 | ```javascript 36 | // @flow 37 | 38 | class Dog { 39 | name: string; 40 | 41 | constructor(name: string) { 42 | this.name = name; 43 | } 44 | 45 | bark(): string { 46 | return `Wah wah, I am ${this.name}`; 47 | } 48 | 49 | barkInConsole() { 50 | /* eslint-disable no-console */ 51 | console.log(this.bark()); 52 | /* eslint-enable no-console */ 53 | } 54 | 55 | } 56 | 57 | export default Dog; 58 | ``` 59 | 60 | Комментарий `// @flow` говорит Flow, что мы хотим проверять типы для этого файла. Сами аннотации Flow предваряются двоеточием и обычно добавляются после параметра функции или имени функции. Подробности смотрите в документации. 61 | 62 | Теперь, если вы запустите `yarn start`, Flow будет работать, но ESLint начнет жаловаться, что используется нестандартный синтаксис. 63 | Поскольку парсер Babel отлично справляется с парсингом Flow контента (благодаря установленному нами плагину `babel-preset-flow`), было бы здорово, если бы ESLint мог опираться на парсер Babel, вместо того чтобы пытаться самому понять аннотации Flow. Это возможно при использовании пакета `babel-eslint`. Давайте сделаем это. 64 | 65 | - Запустите `yarn add --dev babel-eslint` 66 | 67 | - В `package.json`, после `eslintConfig`, добавьте следующее свойство: `"parser": "babel-eslint"` 68 | 69 | Теперь `yarn start` должен одновременно анализировать код с помощью ESLint и проверять типы посредством Flow. 70 | 71 | Далее, поскольку ESLint и Babel совместно используют общий парсер, мы можем заставить ESLint проверять наши Flow аннотации, используя плагин `eslint-plugin-flowtype`. 72 | 73 | - Запустите `yarn add --dev eslint-plugin-flowtype` и добавьте `"flowtype"` после `eslintConfig.plugins` в `package.json`. Затем добавьте `"plugin:flowtype/recommended"` после `eslintConfig.extends` в массив после `"airbnb"`. 74 | 75 | Теперь, если вы, например, введете `name:string` в качестве аннотации, ESLint должен пожаловаться, что вы забыли пробел после двоеточия. 76 | 77 | **Примечание**: Свойство `"parser": "babel-eslint"`, которое я заставил вас написать в `package.json`, вообще-то входит в конфигурацию `"plugin:flowtype/recommended"`, так что теперь можете убрать его, чтобы сократить `package.json`. С другой стороны, оставить его здесь будет более наглядным, так что это на ваше усмотрение. Поскольку это руководство нацелено на максимальную краткость, я его уберу. 78 | 79 | - Теперь вы можете добавить `// @flow` в каждый `.js` и `.jsx` файл в папке `src`, запустить `yarn test` или `yarn start` и добавлять аннотации везде, где этого попросит Flow. 80 | 81 | Вы можете обнаружить неожиданный пример в `src/client/components/message.jsx`: 82 | 83 | ```javascript 84 | const Message = ({ message }: { message: string }) =>
{message}
; 85 | ``` 86 | 87 | Как вы видите, при деструктурировании параметра функции, необходимо делать аннотации для выделяемых свойств в виде объекта в литеральной нотации. 88 | 89 | Другой случай, с которым вы столкнетесь, будет в `src/client/reducers/dog-reducer.js`. Flow начнет жаловаться, что Immutable не имеет возвращаемого значения по умолчанию. Эта проблема описана тут: [#863 on Immutable](https://github.com/facebook/immutable-js/issues/863), и имеет два обходных пути: 90 | 91 | ```javascript 92 | import { Map as ImmutableMap } from 'immutable'; 93 | // или 94 | import * as Immutable from 'immutable'; 95 | ``` 96 | 97 | Пока Immutable официально не решит проблему, просто выберите то, что вам больше нравится, когда импортируете компоненты Immutable. Лично я буду использовать `import * as Immutable from 'immutable'`, поскольку это короче и не потребует рефакторинга кода, после того как проблема будет решена. 98 | 99 | **Примечание**: Если Flow выявляет ошибки типизации в папке `node_modules`, добавьте раздел `[ignore]` в файл `.flowconfig`, чтобы указать какие именно пакеты игнорировать (не игнорируйте полностью директорию `node_modules`). Это может выглядеть так: 100 | 101 | ```flowconfig 102 | [ignore] 103 | 104 | .*/node_modules/gulp-flowtype/.* 105 | ``` 106 | 107 | В моем случае, плагин `linter-flow` для Atom обнаружил ошибки типизации в директории `node_modules/gulp-flowtype`, которая содержит файлы, аннотированные `// @flow`. 108 | 109 | Теперь у вас есть "пуленепробиваемый" код, который проанализирован, протипизирован и протестирован - отличная работа! 110 | 111 | Назад в [предыдущий раздел](/tutorial/11-testing-mocha-chai-sinon) или [Содержание](/../../#Содержание). 112 | -------------------------------------------------------------------------------- /tutorial/12-flow/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 12 - Flow 2 | 3 | [Flow](https://flowtype.org/) is a static type checker. It detects inconsistent types in your code and you can add explicit type declarations in it via annotations. 4 | 5 | - In order for Babel to understand and remove Flow annotations during the transpilation process, install the Flow preset for Babel by running `yarn add --dev babel-preset-flow`. Then, add `"flow"` under `babel.presets` in your `package.json`. 6 | 7 | - Create an empty `.flowconfig` file at the root of your project 8 | 9 | - Run `yarn add --dev gulp-flowtype` to install the Gulp plugin for Flow, and add `flow()` to your `lint` task: 10 | 11 | ```javascript 12 | import flow from 'gulp-flowtype'; 13 | 14 | // [...] 15 | 16 | gulp.task('lint', () => 17 | gulp.src([ 18 | paths.allSrcJs, 19 | paths.gulpFile, 20 | paths.webpackFile, 21 | ]) 22 | .pipe(eslint()) 23 | .pipe(eslint.format()) 24 | .pipe(eslint.failAfterError()) 25 | .pipe(flow({ abort: true })) // Add Flow here 26 | ); 27 | ``` 28 | 29 | The `abort` option is to interrupt the Gulp task if Flow detects an issue. 30 | 31 | Alright, we should be able to run Flow now. 32 | 33 | - Add Flow annotations to `src/shared/dog.js` like so: 34 | 35 | ```javascript 36 | // @flow 37 | 38 | class Dog { 39 | name: string; 40 | 41 | constructor(name: string) { 42 | this.name = name; 43 | } 44 | 45 | bark(): string { 46 | return `Wah wah, I am ${this.name}`; 47 | } 48 | 49 | barkInConsole() { 50 | /* eslint-disable no-console */ 51 | console.log(this.bark()); 52 | /* eslint-enable no-console */ 53 | } 54 | 55 | } 56 | 57 | export default Dog; 58 | ``` 59 | 60 | The `// @flow` comment tells Flow that we want this file to be typechecked. For the rest, Flow annotations are typically a colon after a function parameter or a function name. Check the documentation for more details. 61 | 62 | Now if you run `yarn start`, Flow will work fine, but ESLint is going to complain about that non-standard syntax we're using. Since Babel's parser is all up-and-running with parsing Flow content thanks to the `babel-preset-flow` plugin we installed, it'd be nice if ESLint could rely on Babel's parser instead of trying to understand Flow annotations on its own. That's actually possible using the `babel-eslint` package. Let's do this. 63 | 64 | - Run `yarn add --dev babel-eslint` 65 | 66 | - In `package.json`, under `eslintConfig`, add the following property: `"parser": "babel-eslint"` 67 | 68 | `yarn start` should now both lint and typecheck your code fine. 69 | 70 | Now that ESLint and Babel are able to share a common parser, we can actually get ESLint to lint our Flow annotations via the `eslint-plugin-flowtype` plugin. 71 | 72 | - Run `yarn add --dev eslint-plugin-flowtype` and add `"flowtype"` under `eslintConfig.plugins` in `package.json`, and add `"plugin:flowtype/recommended"` under `eslintConfig.extends` in an array next to `"airbnb"`. 73 | 74 | Now if you type `name:string` as an annotation, ESLint should complain that you forgot a space after the colon for instance. 75 | 76 | **Note**: The `"parser": "babel-eslint"` property that I made you write in `package.json` is actually included in the `"plugin:flowtype/recommended"` config, so you can now remove it for a more minimal `package.json`. Leaving it there is more explicit though, so that's up to your personal preference. Since this tutorial is about the most minimal setup, I removed it. 77 | 78 | - You can now add `// @flow` in every `.js` and `.jsx` file under `src`, run `yarn test` or `yarn start`, and add type annotations everywhere Flow asks you to do so. 79 | 80 | One counterintuitive case is the following, for `src/client/component/message.jsx`: 81 | 82 | ```javascript 83 | const Message = ({ message }: { message: string }) =>
{message}
; 84 | ``` 85 | 86 | As you can see, when destructuring function parameters, you must annotate the extracted properties using a sort of object literal notation. 87 | 88 | Another case you will encounter is that in `src/client/reducers/dog-reducer.js`, Flow will complain about Immutable not having a default export. This issue is discussed in [#863 on Immutable](https://github.com/facebook/immutable-js/issues/863), which highlights 2 workarounds: 89 | 90 | ```javascript 91 | import { Map as ImmutableMap } from 'immutable'; 92 | // or 93 | import * as Immutable from 'immutable'; 94 | ``` 95 | 96 | Until Immutable officially adresses the issue, just pick whichever looks better to you when importing Immutable components. I'm personally going for `import * as Immutable from 'immutable'` since it's shorter and won't require refactoring the code when this issue gets fixed. 97 | 98 | **Note**: If Flow detects type errors in your `node_modules` folder, add an `[ignore]` section in your `.flowconfig` to ignore the packages causing issues specifically (do not ignore the entire `node_modules` directory). It could look like this: 99 | 100 | ```flowconfig 101 | [ignore] 102 | 103 | .*/node_modules/gulp-flowtype/.* 104 | ``` 105 | 106 | In my case, the `linter-flow` plugin for Atom was detecting type errors in the `node_modules/gulp-flowtype` directory, which contains files annotated with `// @flow`. 107 | 108 | You now have bullet-proof code that is linted, typechecked, and tested, good job! 109 | 110 | Back to the [previous section](/tutorial/11-testing-mocha-chai-sinon) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 111 | -------------------------------------------------------------------------------- /tutorial/12-flow/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/12-flow/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import flow from 'gulp-flowtype'; 7 | import mocha from 'gulp-mocha'; 8 | import del from 'del'; 9 | import webpack from 'webpack-stream'; 10 | import webpackConfig from './webpack.config.babel'; 11 | 12 | const paths = { 13 | allSrcJs: 'src/**/*.js?(x)', 14 | serverSrcJs: 'src/server/**/*.js?(x)', 15 | sharedSrcJs: 'src/shared/**/*.js?(x)', 16 | allLibTests: 'lib/test/**/*.js', 17 | clientEntryPoint: 'src/client/app.jsx', 18 | clientBundle: 'dist/client-bundle.js?(.map)', 19 | gulpFile: 'gulpfile.babel.js', 20 | webpackFile: 'webpack.config.babel.js', 21 | libDir: 'lib', 22 | distDir: 'dist', 23 | }; 24 | 25 | gulp.task('lint', () => 26 | gulp.src([ 27 | paths.allSrcJs, 28 | paths.gulpFile, 29 | paths.webpackFile, 30 | ]) 31 | .pipe(eslint()) 32 | .pipe(eslint.format()) 33 | .pipe(eslint.failAfterError()) 34 | .pipe(flow({ abort: true })) 35 | ); 36 | 37 | gulp.task('clean', () => del([ 38 | paths.libDir, 39 | paths.clientBundle, 40 | ])); 41 | 42 | gulp.task('build', ['lint', 'clean'], () => 43 | gulp.src(paths.allSrcJs) 44 | .pipe(babel()) 45 | .pipe(gulp.dest(paths.libDir)) 46 | ); 47 | 48 | gulp.task('test', ['build'], () => 49 | gulp.src(paths.allLibTests) 50 | .pipe(mocha()) 51 | ); 52 | 53 | gulp.task('main', ['test'], () => 54 | gulp.src(paths.clientEntryPoint) 55 | .pipe(webpack(webpackConfig)) 56 | .pipe(gulp.dest(paths.distDir)) 57 | ); 58 | 59 | gulp.task('watch', () => { 60 | gulp.watch(paths.allSrcJs, ['main']); 61 | }); 62 | 63 | gulp.task('default', ['watch', 'main']); 64 | -------------------------------------------------------------------------------- /tutorial/12-flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "gulp test", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": [ 12 | "airbnb", 13 | "plugin:flowtype/recommended" 14 | ], 15 | "parser": "babel-eslint", 16 | "plugins": [ 17 | "import", 18 | "flowtype" 19 | ], 20 | "env": { 21 | "browser": true 22 | }, 23 | "rules": { 24 | "new-cap": [ 25 | 2, 26 | { 27 | "capIsNewExceptions": [ 28 | "Map", 29 | "List" 30 | ] 31 | } 32 | ] 33 | } 34 | }, 35 | "babel": { 36 | "presets": [ 37 | "latest", 38 | "react", 39 | "flow" 40 | ], 41 | "compact": false 42 | }, 43 | "dependencies": { 44 | "babel-polyfill": "^6.16.0", 45 | "immutable": "^3.8.1", 46 | "react": "^15.3.2", 47 | "react-dom": "^15.3.2", 48 | "react-redux": "^4.4.5", 49 | "redux": "^3.6.0", 50 | "redux-actions": "^0.12.0", 51 | "redux-immutable": "^3.0.8" 52 | }, 53 | "devDependencies": { 54 | "babel-eslint": "^7.0.0", 55 | "babel-loader": "^6.2.5", 56 | "babel-preset-flow": "^1.0.0", 57 | "babel-preset-latest": "^6.16.0", 58 | "babel-preset-react": "^6.16.0", 59 | "chai": "^3.5.0", 60 | "del": "^2.2.2", 61 | "eslint": "^3.8.1", 62 | "eslint-config-airbnb": "^12.0.0", 63 | "eslint-plugin-flowtype": "^2.21.0", 64 | "eslint-plugin-import": "^2.0.1", 65 | "eslint-plugin-jsx-a11y": "^2.2.3", 66 | "eslint-plugin-react": "^6.4.1", 67 | "gulp": "^3.9.1", 68 | "gulp-babel": "^6.1.2", 69 | "gulp-eslint": "^3.0.1", 70 | "gulp-flowtype": "^1.0.0", 71 | "gulp-mocha": "^3.0.1", 72 | "mocha": "^3.1.2", 73 | "sinon": "^1.17.6", 74 | "sinon-chai": "^2.8.0", 75 | "webpack-stream": "^3.2.0" 76 | }, 77 | "repository": "verekia/js-stack-from-scratch", 78 | "author": "Jonathan Verrecchia - @verekia", 79 | "license": "MIT" 80 | } 81 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/actions/dog-actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createAction } from 'redux-actions'; 4 | 5 | export const MAKE_BARK = 'MAKE_BARK'; 6 | export const makeBark = createAction(MAKE_BARK, () => true); 7 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import 'babel-polyfill'; 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import { createStore } from 'redux'; 8 | import { Provider } from 'react-redux'; 9 | import { combineReducers } from 'redux-immutable'; 10 | import dogReducer from './reducers/dog-reducer'; 11 | import BarkMessage from './containers/bark-message'; 12 | import BarkButton from './containers/bark-button'; 13 | 14 | const store = createStore(combineReducers({ 15 | dog: dogReducer, 16 | })); 17 | 18 | ReactDOM.render( 19 | 20 |
21 | 22 | 23 |
24 |
25 | , document.querySelector('.app') 26 | ); 27 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/components/button.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { PropTypes } from 'react'; 4 | 5 | const Button = ({ action, actionLabel }: { action: Function, actionLabel: string}) => 6 | ; 7 | 8 | Button.propTypes = { 9 | action: PropTypes.func.isRequired, 10 | actionLabel: PropTypes.string.isRequired, 11 | }; 12 | 13 | export default Button; 14 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/components/message.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { PropTypes } from 'react'; 4 | 5 | const Message = ({ message }: { message: string }) =>
{message}
; 6 | 7 | Message.propTypes = { 8 | message: PropTypes.string.isRequired, 9 | }; 10 | 11 | export default Message; 12 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/containers/bark-button.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import Button from '../components/button'; 5 | import { makeBark } from '../actions/dog-actions'; 6 | 7 | const mapDispatchToProps = dispatch => ({ 8 | action: () => { dispatch(makeBark()); }, 9 | actionLabel: 'Bark', 10 | }); 11 | 12 | export default connect(null, mapDispatchToProps)(Button); 13 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/containers/bark-message.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import Message from '../components/message'; 5 | 6 | const mapStateToProps = state => ({ 7 | message: state.getIn(['dog', 'hasBarked']) ? 'The dog barked' : 'The dog did not bark', 8 | }); 9 | 10 | export default connect(mapStateToProps)(Message); 11 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/reducers/dog-reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as Immutable from 'immutable'; 4 | import { MAKE_BARK } from '../actions/dog-actions'; 5 | 6 | const initialState = Immutable.Map({ 7 | hasBarked: false, 8 | }); 9 | 10 | const dogReducer = (state: Object = initialState, action: Object) => { 11 | switch (action.type) { 12 | case MAKE_BARK: 13 | return state.set('hasBarked', action.payload); 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default dogReducer; 20 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/server/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable no-console */ 4 | 5 | import Dog from '../shared/dog'; 6 | 7 | const toby = new Dog('Toby'); 8 | 9 | console.log(toby.bark()); 10 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | class Dog { 4 | name: string; 5 | 6 | constructor(name: string) { 7 | this.name = name; 8 | } 9 | 10 | bark(): string { 11 | return `Wah wah, I am ${this.name}`; 12 | } 13 | 14 | barkInConsole() { 15 | /* eslint-disable no-console */ 16 | console.log(this.bark()); 17 | /* eslint-enable no-console */ 18 | } 19 | 20 | } 21 | 22 | export default Dog; 23 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/test/client/state-test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */ 4 | 5 | import { createStore } from 'redux'; 6 | import { combineReducers } from 'redux-immutable'; 7 | import { should } from 'chai'; 8 | import { describe, it, beforeEach } from 'mocha'; 9 | import dogReducer from '../../client/reducers/dog-reducer'; 10 | import { makeBark } from '../../client/actions/dog-actions'; 11 | 12 | should(); 13 | let store; 14 | 15 | describe('App State', () => { 16 | describe('Dog', () => { 17 | beforeEach(() => { 18 | store = createStore(combineReducers({ 19 | dog: dogReducer, 20 | })); 21 | }); 22 | describe('makeBark', () => { 23 | it('should make hasBarked go from false to true', () => { 24 | store.getState().getIn(['dog', 'hasBarked']).should.be.false; 25 | store.dispatch(makeBark()); 26 | store.getState().getIn(['dog', 'hasBarked']).should.be.true; 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/test/shared/dog-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, no-console */ 2 | 3 | import chai from 'chai'; 4 | import { stub } from 'sinon'; 5 | import sinonChai from 'sinon-chai'; 6 | import { describe, it } from 'mocha'; 7 | import Dog from '../../shared/dog'; 8 | 9 | chai.should(); 10 | chai.use(sinonChai); 11 | 12 | describe('Shared', () => { 13 | describe('Dog', () => { 14 | describe('barkInConsole', () => { 15 | it('should print a bark string with its name', () => { 16 | stub(console, 'log'); 17 | new Dog('Test Toby').barkInConsole(); 18 | console.log.should.have.been.calledWith('Wah wah, I am Test Toby'); 19 | console.log.restore(); 20 | }); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tutorial/12-flow/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/2-packages/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | -------------------------------------------------------------------------------- /tutorial/2-packages/README.md: -------------------------------------------------------------------------------- 1 | # 2 - Установка и использование пакетов 2 | 3 | В этой части мы установим и будем использовать один пакет (package). "Пакет" - это просто кусок кода, написанный кем-то другим, и который вы можете использовать в своей программе. Это может быть что угодно. Например, сейчас мы попробуем воспользоваться пакетом, который позволяет манипулировать цветами. 4 | 5 | - Установим созданный сообществом пакет, называемый `color`, путем запуска команды `yarn add color`. 6 | 7 | Откройте `package.json`, чтобы увидеть, как Yarn автоматически добавил `color` в `dependencies` (зависимости). 8 | 9 | Сам пакет находится в созданной папке `node_modules`, в которой хранятся импортируемые пакеты. 10 | 11 | - Добавьте `node_modules/` в файл `.gitignore` (и запустите `git init`, чтобы создать новый репозиторий, если вы еще этого не сделали) 12 | 13 | Вы также могли заметить файл `yarn.lock`, сгенерированный Yarn. Вам необходимо зафиксировать (commit) этот файл в репозитории, так как это гарантирует, что любой член вашей команды использует те же самые версии пакетов, что и вы. Если вы все еще предпочитаете NPM, а не Yarn, эквивалентом этого файла будет *shrinkwrap*. 14 | 15 | - Добавьте `const Color = require('color');` в `index.js` 16 | - Используем пакет, например таким образом: `const redHexa = Color({r: 255, g: 0, b: 0}).hex();` 17 | - Добавьте `console.log(redHexa)`. 18 | - Запустите `yarn start` - должны увидеть `#FF0000`. 19 | 20 | Поздравляю! Вы установили и применили пакет! 21 | 22 | `color` здесь просто пример, чтобы научить вас использовать пакеты. Больше он нам не нужен, можно его удалить: 23 | 24 | - Выполните `yarn remove color` 25 | 26 | **Примечание**: Существует два вида зависимостей для подключения пакетов: `"dependencies"` и `"devDependencies"`. `"dependencies"` - более общее понятие, чем `"devDependencies"`, так как последние используются только для разработки, а не выпуска (production) приложения (обычно это пакеты, связанные с процессом построения, анализаторы кода и т.п.). Для `"devDependencies"` будем использовать `yarn add --dev [package]` (прим. пер. `npm install [package] --save-dev`). 27 | 28 | Следующий раздел: [3 - Настройка ES6 с Babel и Gulp](/tutorial/3-es6-babel-gulp) 29 | 30 | Назад в [предыдущий раздел](/tutorial/1-node-npm-yarn-package-json) или [Содержание](/../../#Содержание). 31 | -------------------------------------------------------------------------------- /tutorial/2-packages/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 2 - Installing and using a package 2 | 3 | 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. 4 | 5 | - Install the community-made package called `color` by running `yarn add color`. 6 | 7 | Open `package.json` to see how Yarn automatically added `color` in `dependencies`. 8 | 9 | A `node_modules` folder has been created to store the package. 10 | 11 | - Add `node_modules/` to your `.gitignore` file (and `git init` a new repo if you haven't done that yet). 12 | 13 | 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*. 14 | 15 | - Add `const Color = require('color');` in `index.js` 16 | - Use the package like this for instance: `const redHexa = Color({r: 255, g: 0, b: 0}).hexString();` 17 | - Add `console.log(redHexa)`. 18 | - Running `yarn start` should show `#FF0000`. 19 | 20 | Congratulations, you installed and used a package! 21 | 22 | `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: 23 | 24 | - Run `yarn remove color` 25 | 26 | **Note**: There are 2 kinds of package dependencies, `"dependencies"` and `"devDependencies"`. `"dependencies"` is more general than `"devDependencies"`, which are packages that you only need during development, not production (typically, build-related packages, linters, etc). For `"devDependencies"`, we will use `yarn add --dev [package]`. 27 | 28 | Next section: [3 - Setting up ES6 with Babel and Gulp](/tutorial/3-es6-babel-gulp) 29 | 30 | Back to the [previous section](/tutorial/1-node-npm-yarn-package-json) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 31 | -------------------------------------------------------------------------------- /tutorial/2-packages/index.js: -------------------------------------------------------------------------------- 1 | const Color = require('color'); 2 | 3 | const redHexa = Color({r: 255, g: 0, b: 0}).hex(); 4 | 5 | console.log(redHexa); 6 | -------------------------------------------------------------------------------- /tutorial/2-packages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "node .", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "yarn start" 9 | }, 10 | "dependencies": { 11 | "color": "^1.0.0" 12 | }, 13 | "repository": "verekia/js-stack-from-scratch", 14 | "author": "Jonathan Verrecchia - @verekia", 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /tutorial/2-packages/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | color-convert@^1.8.2: 4 | version "1.8.2" 5 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.8.2.tgz#be868184d7c8631766d54e7078e2672d7c7e3339" 6 | dependencies: 7 | color-name "^1.1.1" 8 | 9 | color-name@^1.0.0, color-name@^1.1.1: 10 | version "1.1.1" 11 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689" 12 | 13 | color-string@^1.3.1: 14 | version "1.3.1" 15 | resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.3.1.tgz#fc3cc33dd560da0cc7c2dcfc13847900ec665fb1" 16 | dependencies: 17 | color-name "^1.0.0" 18 | simple-swizzle "^0.2.2" 19 | 20 | color@^1.0.0: 21 | version "1.0.1" 22 | resolved "https://registry.yarnpkg.com/color/-/color-1.0.1.tgz#be2e95e899def858c8e8a5c2a710913c45b81f3e" 23 | dependencies: 24 | color-convert "^1.8.2" 25 | color-string "^1.3.1" 26 | 27 | is-arrayish@^0.3.1: 28 | version "0.3.1" 29 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd" 30 | 31 | simple-swizzle@^0.2.2: 32 | version "0.2.2" 33 | resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" 34 | dependencies: 35 | is-arrayish "^0.3.1" 36 | 37 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/README.md: -------------------------------------------------------------------------------- 1 | # 3 - Настройка ES6 с Babel и Gulp 2 | 3 | Мы начинаем использовать синтаксис ES6, значительно более продвинутый относительно "старого" ES5. Все браузеры и JS окружения хорошо понимают ES5, но не ES6. Поэтому мы будем использовать инструмент, называемый Babel, чтобы преобразовать файлы ES6 в ES5 формат. Чтобы запустить Babel, мы будем использовать Gulp - менеджер задач. Это похоже на создание задач, расположенных в `scripts` файла `package.json`, но написание задач в JS файле проще и понятнее чем в JSON, поэтому установим Gulp и плагин Babel для него. 4 | 5 | - Запустите `yarn add --dev gulp` 6 | - Запустите `yarn add --dev gulp-babel` 7 | - Запустите `yarn add --dev babel-preset-latest` 8 | - Запустите `yarn add --dev del` (для задачи `clean` - очистки, как вы увидите ниже) 9 | - В `package.json` добавьте поле `babel` для конфигурации Babel. Укажем, что хотим использовать новейшую конфигурацию Babel, следующим образом: 10 | 11 | ```json 12 | "babel": { 13 | "presets": [ 14 | "latest" 15 | ] 16 | }, 17 | ``` 18 | 19 | **Примечание**: Файл `.babelrc` в корне проекта так же может быть использован вместо свойства `babel` в `package.json`. Но поскольку корневая директория вашего проекта с течением времени будет все больше и больше раздуваться, лучше храните конфигурацию Babel в `package.json` до тех пор, пока она не станет слишком большой. 20 | 21 | - Переместите файл `index.js` в директорию `src`. Там вы будете писать ES6 код. А папка `lib` будет местом, куда ваш код будет компилироваться в ES5. Gulp и Babel позаботятся о ее создании. Уберите предыдущий, относящийся к `color`, код из `index.js`, и замените его на нечто простое, например: 22 | 23 | ```javascript 24 | const str = 'ES6'; 25 | console.log(`Hello ${str}`); 26 | ``` 27 | 28 | Здесь мы использовали *шаблонную строку* - возможность ES6 внедрять с помощью `${}` переменные прямо в строки, без использования конкантенации. Обратите внимание, что шаблонные строки пишутся в **\`обратных кавычках\`** 29 | 30 | - Создайте `gulpfile.js`, содержащий: 31 | 32 | ```javascript 33 | const gulp = require('gulp'); 34 | const babel = require('gulp-babel'); 35 | const del = require('del'); 36 | const exec = require('child_process').exec; 37 | 38 | const paths = { 39 | allSrcJs: 'src/**/*.js', 40 | libDir: 'lib', 41 | }; 42 | 43 | gulp.task('clean', () => { 44 | return del(paths.libDir); 45 | }); 46 | 47 | gulp.task('build', ['clean'], () => { 48 | return gulp.src(paths.allSrcJs) 49 | .pipe(babel()) 50 | .pipe(gulp.dest(paths.libDir)); 51 | }); 52 | 53 | gulp.task('main', ['build'], (callback) => { 54 | exec(`node ${paths.libDir}`, (error, stdout) => { 55 | console.log(stdout); 56 | return callback(error); 57 | }); 58 | }); 59 | 60 | gulp.task('watch', () => { 61 | gulp.watch(paths.allSrcJs, ['main']); 62 | }); 63 | 64 | gulp.task('default', ['watch', 'main']); 65 | 66 | ``` 67 | 68 | Давайте приостановимся и разберемся. 69 | 70 | Gulp сам по себе имеет довольно понятный API. Он определяет задачи `gulp.task`, которые могут брать файлы из `gulp.src`, применять к ним цепочки обработчиков - `.pipe()` (как `babel()` в нашем случае) и сохранять новые файлы в `gulp.dest`. Также он может отслеживать (`gulp.watch`) изменения в файловой системе. Одни задачи Gulp могут предварять запуск других, если указать их в массиве (как `['build']`) в качестве второго аргумента в `gulp.task`. В [документации](https://github.com/gulpjs/gulp) все изложено более основательно. 71 | 72 | Сначала мы определили объект `paths`, чтобы хранить в одном месте все нужные пути и использовать принцип "не повторяйся" (DRY). 73 | 74 | Затем мы определили пять задач: `build` *(создать)*, `clean` *(очистить)*, `main` *(основная)*, `watch` *(наблюдать)* и `default` *(по умолчанию)*. 75 | 76 | - `build` вызывает Babel, чтобы преобразовать все исходные файлы из `src`, и записывает результат в `lib`. 77 | - `clean` - задача, которая просто удаляет всю нашу автоматически сгенерированную директорию `lib` перед каждым `build`. Как правило, полезно избавляться от старых скомпилированых файлов (которые могут остаться после переименования или удаления чего-то в `src`), для того чтобы быть уверенным, что директория `lib` всегда синхронна с `src`, даже если `build` не завершился успешно, а вы этого не заметили. Мы используем пакет `del`, чтобы удалять файлы путем, наиболее подходящим для задач Gulp (это [рекомендованый](https://github.com/gulpjs/gulp/blob/master/docs/recipes/delete-files-folder.md) для Gulp способ) 78 | - `main` - аналогично запуску `node .` из предыдущей части, за исключением того, что теперь мы используем `lib/index.js`. Мы можем просто писать `node lib`, потому что по умолчанию Node найдет и запустит `index.js` из указанной папки (мы используем переменную `libDir`, чтобы соответствовать принципу DRY). `require('child_process').exec` и `exec` - это функции самого Node, вызывающие консольные команды. Мы перенаправим `stdout` в `console.log()` и возвратим возможную ошибку через функцию обратного вызова (callback), которая передается в `gulp.task` в качестве аргумента. Не переживайте, если эта команда не совсем ясна для вас, помните, что эта задача просто запускает `node lib`. 79 | - `watch` запускает задачу `main`, когда происходят изменения файловой системы для указанных файлов. 80 | - `default` - это специальная задача, которая запускается, если вы просто вызываете `gulp` из CLI (командной строки). В нашем случае мы хотим сначала запустить `main` (один раз), а затем `watch`. 81 | 82 | **Примечание**: Возможно вы удивились тому, что мы используем синтаксис ES6 в этом Gulp файле, хотя он не транспилируется в ES5 со помощью Babel. Это потому что мы используем версию Node, которая поддерживает возможности ES6 из коробки (убедитесь, что вы используете версию Node > 6.5.0, запустив `node -v`). 83 | 84 | Отлично! Посмотрим, как это работает. 85 | 86 | - В `package.json`, замените скрипт `start` на: `"start": "gulp"`. 87 | - Запустите `yarn start`. Должно выйти "Hello ES6" и запуститься автоматическое отслеживание изменений. Попробуйте ввести неверный код в `src/index.js` и увидите после сохранения, как Gulp автоматически указывает на ошибку. 88 | 89 | - Добавьте `/lib/` в `.gitignore` 90 | 91 | Следующий раздел: [4 - Использование ES6 классов](/tutorial/4-es6-syntax-class) 92 | 93 | Назад в [предыдущий раздел](/tutorial/2-packages) или [Содержание](/../../#Содержание). 94 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 3 - Setting up ES6 with Babel and Gulp 2 | 3 | We're now going to use ES6 syntax, which is a great improvement over the "old" ES5 syntax. All browsers and JS environments understand ES5 well, but not ES6. So we're going to use a tool called Babel to transform ES6 files into ES5 files. To run Babel, we are going to use Gulp, a task runner. It is similar to the tasks located under `scripts` in `package.json`, but writing your task in a JS file is simpler and clearer than a JSON file, so we'll install Gulp, and the Babel plugin for Gulp too: 4 | 5 | - Run `yarn add --dev gulp` 6 | - Run `yarn add --dev gulp-babel` 7 | - Run `yarn add --dev babel-preset-latest` 8 | - Run `yarn add --dev del` (for the `clean` task, as you will see below) 9 | - In `package.json`, add a `babel` field for the Babel configuration. Make it use the latest Babel preset like this: 10 | 11 | ```json 12 | "babel": { 13 | "presets": [ 14 | "latest" 15 | ] 16 | }, 17 | ``` 18 | 19 | **Note**: A `.babelrc` file at the root of your project could also be used instead of the `babel` field of `package.json`. Your root folder will get more and more bloated over time, so keep the Babel config in `package.json` until it grows too large. 20 | 21 | - Move your `index.js` into a new `src` folder. This is where you will write your ES6 code. A `lib` folder is where the compiled ES5 code will go. Gulp and Babel will take care of creating it. Remove the previous `color`-related code in `index.js`, and replace it with a simple: 22 | 23 | ```javascript 24 | const str = 'ES6'; 25 | console.log(`Hello ${str}`); 26 | ``` 27 | 28 | 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**. 29 | 30 | - Create a `gulpfile.js` containing: 31 | 32 | ```javascript 33 | const gulp = require('gulp'); 34 | const babel = require('gulp-babel'); 35 | const del = require('del'); 36 | const exec = require('child_process').exec; 37 | 38 | const paths = { 39 | allSrcJs: 'src/**/*.js', 40 | libDir: 'lib', 41 | }; 42 | 43 | gulp.task('clean', () => { 44 | return del(paths.libDir); 45 | }); 46 | 47 | gulp.task('build', ['clean'], () => { 48 | return gulp.src(paths.allSrcJs) 49 | .pipe(babel()) 50 | .pipe(gulp.dest(paths.libDir)); 51 | }); 52 | 53 | gulp.task('main', ['build'], (callback) => { 54 | exec(`node ${paths.libDir}`, (error, stdout) => { 55 | console.log(stdout); 56 | return callback(error); 57 | }); 58 | }); 59 | 60 | gulp.task('watch', () => { 61 | gulp.watch(paths.allSrcJs, ['main']); 62 | }); 63 | 64 | gulp.task('default', ['watch', 'main']); 65 | 66 | ``` 67 | 68 | Let's take a moment to understand all this. 69 | 70 | The API of Gulp itself is pretty straightforward. It defines `gulp.task`s, that can reference `gulp.src` files, applies a chain of treatments to them with `.pipe()` (like `babel()` in our case) and outputs the new files to `gulp.dest`. It can also `gulp.watch` for changes on your filesystem. Gulp tasks can run prerequisite tasks before them, by passing an array (like `['build']`) as a second parameter to `gulp.task`. Refer to the [documentation](https://github.com/gulpjs/gulp) for a more thorough presentation. 71 | 72 | First we define a `paths` object to store all our different file paths and keep things DRY. 73 | 74 | Then we define 5 tasks: `build`, `clean`, `main`, `watch`, and `default`. 75 | 76 | - `build` is where Babel is called to transform all of our source files located under `src` and write the transformed ones to `lib`. 77 | - `clean` is a task that simply deletes our entire auto-generated `lib` folder before every `build`. This is typically useful to get rid of old compiled files after renaming or deleting some in `src`, or to make sure the `lib` folder is in sync with the `src` folder if your build fails and you don't notice. We use the `del` package to delete files in a way that integrates well with Gulp's stream (this is the [recommended](https://github.com/gulpjs/gulp/blob/master/docs/recipes/delete-files-folder.md) way to delete files with Gulp). 78 | - `main` is the equivalent of running `node .` in the previous chapter, except this time, we want to run it on `lib/index.js`. Since `index.js` is the default file Node looks for, we can simply write `node lib` (we use the `libDir` variable to keep things DRY). The `require('child_process').exec` and `exec` part in the task is a native Node function that executes a shell command. We forward `stdout` to `console.log()` and return a potential error using `gulp.task`'s callback function. Don't worry if this part is not super clear to you, remember that this task is basically just running `node lib`. 79 | - `watch` runs the `main` task when filesystem changes happen in the specified files. 80 | - `default` is a special task that will be run if you simply call `gulp` from the CLI. In our case we want it to run both `watch` and `main` (for the first execution). 81 | 82 | **Note**: You might be wondering how come we're using some ES6 code in this Gulp file, since it doesn't get transpiled into ES5 by Babel. This is because we're using a version of Node that supports ES6 features out of the box (make sure you are running Node > 6.5.0 by running `node -v`). 83 | 84 | Alright! Let's see if this works. 85 | 86 | - In `package.json`, change your `start` script to: `"start": "gulp"`. 87 | - Run `yarn start`. It should print "Hello ES6" and start watching for changes. Try writing bad code in `src/index.js` to see Gulp automatically showing you the error when you save. 88 | 89 | - Add `/lib/` to your `.gitignore` 90 | 91 | Next section: [4 - Using the ES6 syntax with a class](/tutorial/4-es6-syntax-class) 92 | 93 | Back to the [previous section](/tutorial/2-packages) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 94 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | const del = require('del'); 4 | const exec = require('child_process').exec; 5 | 6 | const paths = { 7 | allSrcJs: 'src/**/*.js', 8 | libDir: 'lib', 9 | }; 10 | 11 | gulp.task('clean', () => { 12 | return del(paths.libDir); 13 | }); 14 | 15 | gulp.task('build', ['clean'], () => { 16 | return gulp.src(paths.allSrcJs) 17 | .pipe(babel()) 18 | .pipe(gulp.dest(paths.libDir)); 19 | }); 20 | 21 | gulp.task('main', ['build'], (callback) => { 22 | exec(`node ${paths.libDir}`, (error, stdout) => { 23 | console.log(stdout); 24 | return callback(error); 25 | }); 26 | }); 27 | 28 | gulp.task('watch', () => { 29 | gulp.watch(paths.allSrcJs, ['main']); 30 | }); 31 | 32 | gulp.task('default', ['watch', 'main']); 33 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "babel": { 11 | "presets": [ 12 | "latest" 13 | ] 14 | }, 15 | "devDependencies": { 16 | "babel-preset-latest": "^6.16.0", 17 | "del": "^2.2.2", 18 | "gulp": "^3.9.1", 19 | "gulp-babel": "^6.1.2" 20 | }, 21 | "repository": "verekia/js-stack-from-scratch", 22 | "author": "Jonathan Verrecchia - @verekia", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/src/index.js: -------------------------------------------------------------------------------- 1 | const str = 'ES6 now'; 2 | console.log(`Hello ${str}`); 3 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/README.md: -------------------------------------------------------------------------------- 1 | # 4 - Использование ES6 классов 2 | 3 | - Создайте новый файл `src/dog.js`, содержащий следующий ES6 класс: 4 | 5 | ```javascript 6 | class Dog { 7 | constructor(name) { 8 | this.name = name; 9 | } 10 | 11 | bark() { 12 | return `Wah wah, I am ${this.name}`; 13 | } 14 | } 15 | 16 | module.exports = Dog; 17 | ``` 18 | 19 | Это не должно вас смутить, если вы сталкивались с ООП в любом другом языке программирования, хотя в JavaScript классы появились относительно недавно. Класс взаимодействует с окружающим миром, путем определения `module.exports`. 20 | 21 | Обычно в ES6 коде присутствуют классы, `const` и `let`, "шаблонные строки" (в обратных кавычках), как в функции `bark()`, и стрелочные функции (`(param) => { console.log('Hi'); }`) (последнее мы не используем в нашем примере). 22 | 23 | В `src/index.js` напишите следующее: 24 | 25 | ```javascript 26 | const Dog = require('./dog'); 27 | 28 | const toby = new Dog('Toby'); 29 | 30 | console.log(toby.bark()); 31 | ``` 32 | 33 | Как вы видите, в отличие от созданного сообществом пакета `color`, который мы использовали ранее, когда мы подключаем один из наших файлов, мы используем `./` в `require()`. 34 | 35 | - Запустите `yarn start` и должно отобразиться 'Wah wah, I am Toby'. 36 | 37 | - Посмотрите на код, сгенерированный в директории `lib`, чтобы увидеть как он выглядит в скомпилированном виде (например `var` вместо `const`). 38 | 39 | 40 | Следующий раздел: [5 - Синтаксис модулей ES6](/tutorial/5-es6-modules-syntax) 41 | 42 | Назад в [предыдущий раздел](/tutorial/3-es6-babel-gulp) или [Содержание](/../../#Содержание). 43 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 4 - Using the ES6 syntax with a class 2 | 3 | - Create a new file, `src/dog.js`, containing the following ES6 class: 4 | 5 | ```javascript 6 | class Dog { 7 | constructor(name) { 8 | this.name = name; 9 | } 10 | 11 | bark() { 12 | return `Wah wah, I am ${this.name}`; 13 | } 14 | } 15 | 16 | module.exports = Dog; 17 | ``` 18 | 19 | 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. 20 | 21 | Typical ES6 code uses classes, `const` and `let`, "template strings" (with back ticks) like the one in `bark()`, and arrow functions (`(param) => { console.log('Hi'); }`), even though we're not using any in this example. 22 | 23 | In `src/index.js`, write the following: 24 | 25 | ```javascript 26 | const Dog = require('./dog'); 27 | 28 | const toby = new Dog('Toby'); 29 | 30 | console.log(toby.bark()); 31 | ``` 32 | 33 | 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()`. 34 | 35 | - Run `yarn start` and it should print 'Wah wah, I am Toby'. 36 | 37 | - Take a look at the code generated in `lib` to see what your compiled code looks like (`var` instead of `const` for instance). 38 | 39 | Next section: [5 - The ES6 modules syntax](/tutorial/5-es6-modules-syntax) 40 | 41 | Back to the [previous section](/tutorial/3-es6-babel-gulp) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 42 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | const del = require('del'); 4 | const exec = require('child_process').exec; 5 | 6 | const paths = { 7 | allSrcJs: 'src/**/*.js', 8 | libDir: 'lib', 9 | }; 10 | 11 | gulp.task('clean', () => { 12 | return del(paths.libDir); 13 | }); 14 | 15 | gulp.task('build', ['clean'], () => { 16 | return gulp.src(paths.allSrcJs) 17 | .pipe(babel()) 18 | .pipe(gulp.dest(paths.libDir)); 19 | }); 20 | 21 | gulp.task('main', ['build'], (callback) => { 22 | exec(`node ${paths.libDir}`, (error, stdout) => { 23 | console.log(stdout); 24 | return callback(error); 25 | }); 26 | }); 27 | 28 | gulp.task('watch', () => { 29 | gulp.watch(paths.allSrcJs, ['main']); 30 | }); 31 | 32 | gulp.task('default', ['watch', 'main']); 33 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "babel": { 11 | "presets": [ 12 | "latest" 13 | ] 14 | }, 15 | "devDependencies": { 16 | "babel-preset-latest": "^6.16.0", 17 | "del": "^2.2.2", 18 | "gulp": "^3.9.1", 19 | "gulp-babel": "^6.1.2" 20 | }, 21 | "repository": "verekia/js-stack-from-scratch", 22 | "author": "Jonathan Verrecchia - @verekia", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/src/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | module.exports = Dog; 13 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/src/index.js: -------------------------------------------------------------------------------- 1 | const Dog = require('./dog'); 2 | 3 | const toby = new Dog('Toby'); 4 | 5 | console.log(toby.bark()); 6 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/README.md: -------------------------------------------------------------------------------- 1 | # 5 - Синтаксис модулей ES6 2 | 3 | Здесь мы просто заменим `const Dog = require('./dog')` на `import Dog from './dog'`, что является более новым синтаксисом ES6 модулей (по сравнению с синтаксисом "CommonJS" модулей). 4 | 5 | В `dog.js` мы также заменим `module.exports = Dog` на `export default Dog`. 6 | 7 | Заметьте, что в `dog.js` переменная `Dog` используется только в `export`. Поэтому можно напрямую экспортировать анонимный класс следующим образом: 8 | 9 | ```javascript 10 | export default class { 11 | constructor(name) { 12 | this.name = name; 13 | } 14 | 15 | bark() { 16 | return `Wah wah, I am ${this.name}`; 17 | } 18 | } 19 | ``` 20 | 21 | Вы возможно уже догадались, что имя 'Dog' используется в строке`import` файла `index.js` абсолютно по вашему усмотрению. Вполне будет работать: 22 | 23 | ```javascript 24 | import Cat from './dog'; 25 | 26 | const toby = new Cat('Toby'); 27 | ``` 28 | 29 | Очевидно, что в основном вы будете использовать то же имя, что и имя класса/модуля, который вы импортируете. 30 | Один из примеров когда можно сделать по-другому, это то как мы применили `const babel = require('gulp-babel')` в нашем Gulp файле. 31 | 32 | Так что насчет `require()` в нашем `gulpfile.js`? Можем ли мы использовать вместо них `import`? Последняя версия Node поддерживает большую часть возможностей ES6, но пока что не ES6 модули. К счастью для нас Gulp способен призывать Babel на помощь. Если мы переименуем наш `gulpfile.js` в `gulpfile.babel.js`, Babel позаботится о передаче импортируемых через `import` модулей в Gulp. 33 | 34 | - Переименуйте ваш `gulpfile.js` в `gulpfile.babel.js` 35 | 36 | - Замените все `require()` на: 37 | 38 | ```javascript 39 | import gulp from 'gulp'; 40 | import babel from 'gulp-babel'; 41 | import del from 'del'; 42 | import { exec } from 'child_process'; 43 | ``` 44 | 45 | Обратите внимение на "синтаксический сахар", позволяющий получать `exec` напрямую из `child_process`. Довольно элегантно! 46 | 47 | - `yarn start` должно по-прежнему выдавать "Wah wah, I am Toby". 48 | 49 | Следующий раздел: [6 - ESLint](/tutorial/6-eslint) 50 | 51 | Назад в [предыдущий раздел](/tutorial/4-es6-syntax-class) или [Содержание](/../../#Содержание). 52 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 5 - The ES6 modules syntax 2 | 3 | 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). 4 | 5 | In `dog.js`, we also replace `module.exports = Dog` by `export default Dog`. 6 | 7 | Note that in `dog.js`, the name `Dog` is only used in the `export`. Therefore it could be possible to export directly an anonymous class like this instead: 8 | 9 | ```javascript 10 | export default class { 11 | constructor(name) { 12 | this.name = name; 13 | } 14 | 15 | bark() { 16 | return `Wah wah, I am ${this.name}`; 17 | } 18 | } 19 | ``` 20 | 21 | You might now guess that the name 'Dog' used in the `import` in `index.js` is actually completely up to you. This would work just fine: 22 | 23 | ```javascript 24 | import Cat from './dog'; 25 | 26 | const toby = new Cat('Toby'); 27 | ``` 28 | 29 | Obviously, most of the time you will use the same name as the class / module you're importing. 30 | A case where you don't do that is how we `const babel = require('gulp-babel')` in our Gulp file. 31 | 32 | So what about those `require()`s in our `gulpfile.js`? Can we use `import` instead? The latest version of Node supports most ES6 features, but not ES6 modules yet. Luckily for us, Gulp is able to call Babel for help. If we rename our `gulpfile.js` to `gulpfile.babel.js`, Babel will take care of passing `import`ed modules to Gulp. 33 | 34 | - Rename your `gulpfile.js` to `gulpfile.babel.js` 35 | 36 | - Replace your `require()`s by: 37 | 38 | ```javascript 39 | import gulp from 'gulp'; 40 | import babel from 'gulp-babel'; 41 | import del from 'del'; 42 | import { exec } from 'child_process'; 43 | ``` 44 | 45 | Note the syntactic sugar to extract `exec` directly from `child_process`. Pretty elegant! 46 | 47 | - `yarn start` should still print "Wah wah, I am Toby". 48 | 49 | Next section: [6 - ESLint](/tutorial/6-eslint) 50 | 51 | Back to the [previous section](/tutorial/4-es6-syntax-class) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 52 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import babel from 'gulp-babel'; 3 | import del from 'del'; 4 | import { exec } from 'child_process'; 5 | 6 | const paths = { 7 | allSrcJs: 'src/**/*.js', 8 | libDir: 'lib', 9 | }; 10 | 11 | gulp.task('clean', () => { 12 | return del(paths.libDir); 13 | }); 14 | 15 | gulp.task('build', ['clean'], () => { 16 | return gulp.src(paths.allSrcJs) 17 | .pipe(babel()) 18 | .pipe(gulp.dest(paths.libDir)); 19 | }); 20 | 21 | gulp.task('main', ['build'], (callback) => { 22 | exec(`node ${paths.libDir}`, (error, stdout) => { 23 | console.log(stdout); 24 | return callback(error); 25 | }); 26 | }); 27 | 28 | gulp.task('watch', () => { 29 | gulp.watch(paths.allSrcJs, ['main']); 30 | }); 31 | 32 | gulp.task('default', ['watch', 'main']); 33 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "babel": { 11 | "presets": [ 12 | "latest" 13 | ] 14 | }, 15 | "devDependencies": { 16 | "babel-preset-latest": "^6.16.0", 17 | "del": "^2.2.2", 18 | "gulp": "^3.9.1", 19 | "gulp-babel": "^6.1.2" 20 | }, 21 | "repository": "verekia/js-stack-from-scratch", 22 | "author": "Jonathan Verrecchia - @verekia", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/src/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/src/index.js: -------------------------------------------------------------------------------- 1 | import Dog from './dog'; 2 | 3 | const toby = new Dog('Toby'); 4 | 5 | console.log(toby.bark()); 6 | -------------------------------------------------------------------------------- /tutorial/6-eslint/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | -------------------------------------------------------------------------------- /tutorial/6-eslint/README.md: -------------------------------------------------------------------------------- 1 | # 6 - ESLint 2 | 3 | Мы собираемся контролировать качество кода (to lint), чтобы перехватывать потенциальные проблемы. ESLint - наиболее предпочтительный анализатор кода (linter) для ES6. Вместо того чтобы самим определять правила для нашего кода, мы воспользуемся конфигурацией, созданной Airbnb. В этой конфигурации используется несколько плагинов, поэтому мы их тоже установим. 4 | 5 | - Запустите `yarn add --dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react` 6 | 7 | Как вы видите, вы можете установить несколько пакетов одной командой. Как обычно, они все добавятся в ваш `package.json`. 8 | 9 | В `package.json` добавьте свойство `eslintConfig` со следующим содержанием: 10 | 11 | ```json 12 | "eslintConfig": { 13 | "extends": "airbnb", 14 | "plugins": [ 15 | "import" 16 | ] 17 | }, 18 | ``` 19 | 20 | В разделе `plugins` мы сообщаем ESLint, что используем синтаксис ES6 `import`. 21 | 22 | **Примечание**: Вместо свойства `eslintConfig` в `package.json` можно использовать файл `.eslintrc.js`, `.eslintrc.json` или `.eslintrc.yaml` в корне вашего проекта. Так же как и с конфигурацией Babel, мы стараемся избегать загромождения корневой директории большим количеством файлов, но если у вас сложная конфигурация ESLint, рассмотрите такую альтернативу. 23 | 24 | Создадим задачу для Gulp, которая будет запускать ESLint. Для этого установим еще и плагин ESLint для Gulp: 25 | 26 | - запустите `yarn add --dev gulp-eslint` 27 | 28 | Добавьте следующую задачу в ваш `gulpfile.babel.js`: 29 | 30 | ```javascript 31 | import eslint from 'gulp-eslint'; 32 | 33 | const paths = { 34 | allSrcJs: 'src/**/*.js', 35 | gulpFile: 'gulpfile.babel.js', 36 | libDir: 'lib', 37 | }; 38 | 39 | // [...] 40 | 41 | gulp.task('lint', () => { 42 | return gulp.src([ 43 | paths.allSrcJs, 44 | paths.gulpFile, 45 | ]) 46 | .pipe(eslint()) 47 | .pipe(eslint.format()) 48 | .pipe(eslint.failAfterError()); 49 | }); 50 | ``` 51 | 52 | Здесь мы сообщаем Gulp, что для этой задачи хотим подключить `gulpfile.babel.js` и JS файлы, расположенные в `src`. 53 | 54 | Откорректируйте задачу `build` так, чтобы `lint` предваряла ее запуск: 55 | 56 | ```javascript 57 | gulp.task('build', ['lint', 'clean'], () => { 58 | // ... 59 | }); 60 | ``` 61 | 62 | - Запустите `yarn start`. Вы должны увидеть набор ошибок кода (linting errors) в этом Gulp-файле и предупреждения об использовании `console.log()` в `index.js`. 63 | 64 | Один из видов ошибок будет выглядеть следующим образом: `'gulp' should be listed in the project's dependencies, not devDependencies (import/no-extraneous-dependencies)` ('gulp' должен подключаться в разделе `dependencies`, а не `devDependencies`). Вообще-то это неверная ошибка. ESLint не может знать какие JS файлы будут входить только в скомпилированное приложение (build), а какие нет. Поэтому мы немного поможем ESLint, используя комментарии в коде. В `gulpfile.babel.js`, в самом верху, добавьте: 65 | 66 | ```javascript 67 | /* eslint-disable import/no-extraneous-dependencies */ 68 | ``` 69 | 70 | Таким образом, ESLint не будет применять правило `import/no-extraneous-dependencies` в этом файле. 71 | 72 | Теперь у нас осталась проблема с `Unexpected block statement surrounding arrow body (arrow-body-style)` (неожиданное определение блока, окружающего тело стрелочной функции). Это важно. ESLint сообщает нам, что существует лучший способ написать следующий код: 73 | 74 | ```javascript 75 | () => { 76 | return 1; 77 | } 78 | ``` 79 | 80 | Это нужно переписать следующим образом: 81 | 82 | ```javascript 83 | () => 1 84 | ``` 85 | 86 | Потому что когда ES6 функция содержит только возвращаемое выражение, можно опустить фигурные скобки, оператор return и точку с запятой. 87 | 88 | Так что давайте обновим Gulp-файл соответственно: 89 | 90 | ```javascript 91 | gulp.task('lint', () => 92 | gulp.src([ 93 | paths.allSrcJs, 94 | paths.gulpFile, 95 | ]) 96 | .pipe(eslint()) 97 | .pipe(eslint.format()) 98 | .pipe(eslint.failAfterError()) 99 | ); 100 | 101 | gulp.task('clean', () => del(paths.libDir)); 102 | 103 | gulp.task('build', ['lint', 'clean'], () => 104 | gulp.src(paths.allSrcJs) 105 | .pipe(babel()) 106 | .pipe(gulp.dest(paths.libDir)) 107 | ); 108 | ``` 109 | 110 | Последняя оставшаяся проблема связана с `console.log()`. Давайте скажем, что мы хотим в этом примере, чтобы использование `console.log()` в `index.js` было правомерным, а не вызывало предупреждение. Как вы, возможно, догадались, мы поместим `/* eslint-disable no-console */` в начале нашего `index.js` файла. 111 | 112 | - Запустите `yarn start` - теперь все снова без ошибок. 113 | 114 | **Примечание**: В этой части мы работали с ESLint через консоль. Это хорошо для поиска ошибок во время компиляции / перед публикацией, но вы также, возможно, захотите интегрировать его в вашу IDE. НЕ ИСПОЛЬЗУЙТЕ встроенный в вашу среду анализатор кода для ES6. Сконфигурируйте ее так, чтобы для этого использовались модули, расположенные в директории `node_modules`. В этом случае будут использоваться все настройки вашего проекта, правила Airbnb и так далее. Иначе, вы получите лишь усредненный ES6 анализатор. 115 | 116 | 117 | Следующий раздел: [7 - Клиентское приложение на основе Webpack](/tutorial/7-client-webpack) 118 | 119 | Назад в [предыдущий раздел](/tutorial/5-es6-modules-syntax) или [Содержание](/../../#Содержание). 120 | -------------------------------------------------------------------------------- /tutorial/6-eslint/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 6 - ESLint 2 | 3 | We're going to lint our code to catch potential issues. ESLint is the linter of choice for ES6 code. 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 to use their config. 4 | 5 | - Run `yarn add --dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y@2.2.3 eslint-plugin-react` 6 | 7 | As you can see, you can install several packages in one command. It will add all of these to your `package.json`, as usual. 8 | 9 | In `package.json`, add an `eslintConfig` field like so: 10 | 11 | ```json 12 | "eslintConfig": { 13 | "extends": "airbnb", 14 | "plugins": [ 15 | "import" 16 | ] 17 | }, 18 | ``` 19 | 20 | The `plugins` part is to tell ESLint that we use the ES6 import syntax. 21 | 22 | **Note**: An `.eslintrc.js`, `.eslintrc.json`, or `.eslintrc.yaml` file at the root of your project could also be used instead of the `eslintConfig` field of `package.json`. Just like for the Babel configuration, we try to avoid bloating the root folder with too many files, but if you have a complex ESLint config, consider this alternative. 23 | 24 | We'll create a Gulp task that runs ESLint for us. So we'll install the ESLint Gulp plugin as well: 25 | 26 | - Run `yarn add --dev gulp-eslint` 27 | 28 | Add the following task to your `gulpfile.babel.js`: 29 | 30 | ```javascript 31 | import eslint from 'gulp-eslint'; 32 | 33 | const paths = { 34 | allSrcJs: 'src/**/*.js', 35 | gulpFile: 'gulpfile.babel.js', 36 | libDir: 'lib', 37 | }; 38 | 39 | // [...] 40 | 41 | gulp.task('lint', () => { 42 | return gulp.src([ 43 | paths.allSrcJs, 44 | paths.gulpFile, 45 | ]) 46 | .pipe(eslint()) 47 | .pipe(eslint.format()) 48 | .pipe(eslint.failAfterError()); 49 | }); 50 | ``` 51 | 52 | Here we tell Gulp that for this task, we want to include `gulpfile.babel.js`, and the JS files located under `src`. 53 | 54 | Modify your `build` Gulp task by making the `lint` task a prerequisite to it, like so: 55 | 56 | ```javascript 57 | gulp.task('build', ['lint', 'clean'], () => { 58 | // ... 59 | }); 60 | ``` 61 | 62 | - Run `yarn start`, and you should see a bunch of linting errors in this Gulpfile, and a warning for using `console.log()` in `index.js`. 63 | 64 | One type of issue you will see is `'gulp' should be listed in the project's dependencies, not devDependencies (import/no-extraneous-dependencies)`. That's actually a false negative. ESLint cannot know which JS files are part of the build only, and which ones aren't, so we'll need to help it a little bit using comments in code. In `gulpfile.babel.js`, at the very top, add: 65 | 66 | ```javascript 67 | /* eslint-disable import/no-extraneous-dependencies */ 68 | ``` 69 | 70 | This way, ESLint won't apply the rule `import/no-extraneous-dependencies` in this file. 71 | 72 | Now we are left with the issue `Unexpected block statement surrounding arrow body (arrow-body-style)`. That's a great one. ESLint is telling us that there is a better way to write the following code: 73 | 74 | ```javascript 75 | () => { 76 | return 1; 77 | } 78 | ``` 79 | 80 | It should be rewritten into: 81 | 82 | ```javascript 83 | () => 1 84 | ``` 85 | 86 | Because when a function only contains a return statement, you can omit the curly braces, return statement, and semicolon in ES6. 87 | 88 | So let's update the Gulp file accordingly: 89 | 90 | ```javascript 91 | gulp.task('lint', () => 92 | gulp.src([ 93 | paths.allSrcJs, 94 | paths.gulpFile, 95 | ]) 96 | .pipe(eslint()) 97 | .pipe(eslint.format()) 98 | .pipe(eslint.failAfterError()) 99 | ); 100 | 101 | gulp.task('clean', () => del(paths.libDir)); 102 | 103 | gulp.task('build', ['lint', 'clean'], () => 104 | gulp.src(paths.allSrcJs) 105 | .pipe(babel()) 106 | .pipe(gulp.dest(paths.libDir)) 107 | ); 108 | ``` 109 | 110 | The last issue left is about `console.log()`. Let's say that we want this `console.log()` to be valid in `index.js` instead of triggering a warning in this example. You might have guessed it, we'll put `/* eslint-disable no-console */` at the top of our `index.js` file. 111 | 112 | - Run `yarn start` and we are now all clear again. 113 | 114 | **Note**: This section sets you up with ESLint in the console. It is great for catching errors at build time / before pushing, but you also probably want it integrated to your IDE. Do NOT use your IDE's native linting for ES6. Configure it so the binary it uses for linting is the one in your `node_modules` folder. This way it can use all of your project's config, the Airbnb preset, etc. Otherwise you will just get a generic ES6 linting. 115 | 116 | Next section: [7 - Client app with Webpack](/tutorial/7-client-webpack) 117 | 118 | Back to the [previous section](/tutorial/5-es6-modules-syntax) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 119 | -------------------------------------------------------------------------------- /tutorial/6-eslint/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, no-console */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import { exec } from 'child_process'; 8 | 9 | const paths = { 10 | allSrcJs: 'src/**/*.js', 11 | gulpFile: 'gulpfile.babel.js', 12 | libDir: 'lib', 13 | }; 14 | 15 | gulp.task('lint', () => 16 | gulp.src([ 17 | paths.allSrcJs, 18 | paths.gulpFile, 19 | ]) 20 | .pipe(eslint()) 21 | .pipe(eslint.format()) 22 | .pipe(eslint.failAfterError()) 23 | ); 24 | 25 | gulp.task('clean', () => del(paths.libDir)); 26 | 27 | gulp.task('build', ['lint', 'clean'], () => 28 | gulp.src(paths.allSrcJs) 29 | .pipe(babel()) 30 | .pipe(gulp.dest(paths.libDir)) 31 | ); 32 | 33 | gulp.task('main', ['build'], (callback) => { 34 | exec(`node ${paths.libDir}`, (error, stdout) => { 35 | console.log(stdout); 36 | return callback(error); 37 | }); 38 | }); 39 | 40 | gulp.task('watch', () => { 41 | gulp.watch(paths.allSrcJs, ['main']); 42 | }); 43 | 44 | gulp.task('default', ['watch', 'main']); 45 | -------------------------------------------------------------------------------- /tutorial/6-eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ] 15 | }, 16 | "babel": { 17 | "presets": [ 18 | "latest" 19 | ] 20 | }, 21 | "devDependencies": { 22 | "babel-preset-latest": "^6.16.0", 23 | "del": "^2.2.2", 24 | "eslint": "^3.8.1", 25 | "eslint-config-airbnb": "^12.0.0", 26 | "eslint-plugin-import": "^2.0.1", 27 | "eslint-plugin-jsx-a11y": "^2.2.3", 28 | "eslint-plugin-react": "^6.4.1", 29 | "gulp": "^3.9.1", 30 | "gulp-babel": "^6.1.2", 31 | "gulp-eslint": "^3.0.1" 32 | }, 33 | "repository": "verekia/js-stack-from-scratch", 34 | "author": "Jonathan Verrecchia - @verekia", 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /tutorial/6-eslint/src/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/6-eslint/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from './dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/README.md: -------------------------------------------------------------------------------- 1 | # 7 - Клиентское приложение на основе Webpack 2 | 3 | ## Структура нашего приложения 4 | 5 | - Создайте директорию `dist` в корне вашего проекта и добавьте туда файл `index.html` со следующим содержанием: 6 | 7 | ```html 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ``` 18 | 19 | В директории `src` создайте следующие поддиректории: `server`, `shared`, `client` и переместите текущий `index.js` в папку `server`, а `dog.js` в `shared`. Создайте `app.js` в директории `client`. 20 | 21 | Мы пока не собираемся создавать на Node серверную часть, но это разделение поможет более ясно понять, что к чему относится. Вам нужно заменить `import Dog from './dog';` в `server/index.js` на `import Dog from '../shared/dog';`, иначе ESLint обнаружит ошибки неразрешаемых модулей. 22 | 23 | Напишите в `client/app.js`: 24 | 25 | ```javascript 26 | import Dog from '../shared/dog'; 27 | 28 | const browserToby = new Dog('Browser Toby'); 29 | 30 | document.querySelector('.app').innerText = browserToby.bark(); 31 | ``` 32 | 33 | Добавьте следующее в `package.json`, после `eslintConfig`: 34 | 35 | ```json 36 | "env": { 37 | "browser": true 38 | } 39 | ``` 40 | 41 | Таким образом мы сможем использовать такие переменные как window или document, которые всегда доступны в браузере, без предупреждений ESLint о необъявленных переменных. 42 | 43 | Если вы желаете использовать самые последние возможности ES6 в клиентском коде, такие как `Promise` (обещания), вам нужно включить полифил (polyfill) [Babel Polyfill](https://babeljs.io/docs/usage/polyfill/) в ваш код. 44 | 45 | - Запустите `yarn add babel-polyfill` 46 | 47 | И вставьте в самое начало `app.js` импорт этого модуля: 48 | 49 | ```javascript 50 | import 'babel-polyfill'; 51 | ``` 52 | 53 | Включение полифила (polyfill) прибавляет объема вашей сборке, поэтому подключайте его только тогда, когда применяете конструкции, которые он охватывает. Для того, чтобы показать более полный шаблон кода в этом руководстве, я буду его применять. Он появится в примерах в следующих частях. 54 | 55 | ## Webpack 56 | 57 | В среде Node вы можете свободно использовать `import` для различных файлов, и Node разрешит (англ. resolve) их посредством файловой системы. В браузере файловая система отсутствует, следовательно `import` ведет в никуда. Для того чтобы `app.js`, являющийся точкой входа для приложения, получил всю древовидную структуру импортируемых файлов, мы собираемся "собрать" все это дерево зависимостей в один файл. Webpack - нужный для этого инструмент. 58 | 59 | Webpack, как и Gulp, использует конфигурационные файлы вида `webpack.config.js`. В них возможно использование ES6 импорта и экспорта точно таким же способом, как мы делали с Gulp относительно Babel: обозначая этот файл как `webpack.config.babel.js`. 60 | 61 | - Создайте пустой файл `webpack.config.babel.js` 62 | 63 | - Пока вы в нем, добавьте `webpack.config.babel.js` в задачу `lint` в Gulp, и еще несколько констант с путями (`paths`): 64 | 65 | ```javascript 66 | const paths = { 67 | allSrcJs: 'src/**/*.js', 68 | gulpFile: 'gulpfile.babel.js', 69 | webpackFile: 'webpack.config.babel.js', 70 | libDir: 'lib', 71 | distDir: 'dist', 72 | }; 73 | 74 | // [...] 75 | 76 | gulp.task('lint', () => 77 | gulp.src([ 78 | paths.allSrcJs, 79 | paths.gulpFile, 80 | paths.webpackFile, 81 | ]) 82 | .pipe(eslint()) 83 | .pipe(eslint.format()) 84 | .pipe(eslint.failAfterError()) 85 | ); 86 | ``` 87 | 88 | Мы должны научить Webpack обрабатывать ES6 файлы с помощью Babel (точно так же, как мы показали Gulp, как это делать через `gulp-babel`). Для Webpack, когда нужно обработать файлы, не являющиеся простым старым JavaScript, мы используем *загрузчики* (loaders). Давайте установим загрузчик Babel для Webpack: 89 | 90 | - Запустите `yarn add --dev babel-loader` 91 | 92 | - Напишите следующее в `webpack.config.babel.js` файле: 93 | 94 | ```javascript 95 | export default { 96 | output: { 97 | filename: 'client-bundle.js', 98 | }, 99 | devtool: 'source-map', 100 | module: { 101 | loaders: [ 102 | { 103 | test: /\.jsx?$/, 104 | loader: 'babel-loader', 105 | exclude: [/node_modules/], 106 | }, 107 | ], 108 | }, 109 | resolve: { 110 | extensions: ['', '.js', '.jsx'], 111 | }, 112 | }; 113 | ``` 114 | 115 | Давайте это немного проанализируем: 116 | 117 | Этот файл нужен, чтобы сообщить Webpack некоторую информацию. `output.filename` - имя файла генерируемой сборки. `devtool: 'source-map'` - позволяет использовать source maps (карты кода) для упрощения отладки в браузере. В `module.loaders` есть свойство `test` с регулярным выражением, определяющим какие файлы должны обрабатываться загрузчиком `babel-loader`. Поскольку мы будем использовать как `.js` так и `.jsx` файлы (для React) в следующих частях, наше выражение выглядит как `/\.jsx?$/`. Директория `node_modules` исключена (exclude), поскольку ее не нужно транспилировать. Таким образом, когда код импортирует (`import`) пакеты, расположенные в `node_modules`, Babel не тратит время на обработку этих файлов. Раздел `resolve` сообщает Webpack, файлы какого типа мы хотим подключать через `import`, позволяя тем самым опускать расширения в именах файлов, например как в `import Foo from './foo'`, где `foo` может быть `foo.js` или `foo.jsx`. 118 | 119 | Итак, мы настроили Webpack, но нам до сих пор требуется способ *запустить* его. 120 | 121 | ## Подключение Webpack к Gulp 122 | 123 | Webpack может делать множество вещей. Он даже может полностью заменить Gulp, если проект в основном выполняется на стороне клиента. Gulp в свою очередь, как более общий инструмент, больше подходит для таких вещей как анализ кода (linting), тестирование, запуск задач на стороне сервера. Он также проще в понимании для новичков, чем сложное конфигурирование Webpack. У нас уже довольно хорошо настроен рабочий процесс на базе Gulp, так что интеграция Webpack в процесс сборки пройдет проще простого. 124 | 125 | Давайте создадим в Gulp задачу по запуску Webpack. Откройте `gulpfile.babel.js`. 126 | 127 | Поскольку мы будем запускать наше приложение, открывая `index.html`, нам больше не требуется задача `main`, чтобы выполнять `node lib/`. 128 | 129 | - Уберите `import { exec } from 'child_process'`. 130 | 131 | Аналогично плагинам для Gulp, пакет `webpack-stream` позволяет очень просто интегрировать Webpack в Gulp. 132 | 133 | - Устанавливаем пакет: `yarn add --dev webpack-stream` 134 | 135 | - Добавляем следующие `import`: 136 | 137 | ```javascript 138 | import webpack from 'webpack-stream'; 139 | import webpackConfig from './webpack.config.babel'; 140 | ``` 141 | 142 | Вторая строчка просто подключает наш конфигурационный файл. 143 | 144 | Как было сказанно ранее, в следующей части мы собираемся использовать `.jsx` файлы (на клиенте и даже позже на сервере), так что давайте настроим это сейчас, чтобы затем иметь фору. 145 | 146 | - Измените константы следующим образом: 147 | 148 | ```javascript 149 | const paths = { 150 | allSrcJs: 'src/**/*.js?(x)', 151 | serverSrcJs: 'src/server/**/*.js?(x)', 152 | sharedSrcJs: 'src/shared/**/*.js?(x)', 153 | clientEntryPoint: 'src/client/app.js', 154 | gulpFile: 'gulpfile.babel.js', 155 | webpackFile: 'webpack.config.babel.js', 156 | libDir: 'lib', 157 | distDir: 'dist', 158 | }; 159 | ``` 160 | 161 | Здесь `.js?(x)` - просто шаблон, соответствующий `.js` и `.jsx` файлам. 162 | 163 | Теперь у нас есть константы для различных частей приложения и файл, указывающий на начальную точку сборки (entry point file). 164 | 165 | - Измените задачу `main` так: 166 | 167 | ```javascript 168 | gulp.task('main', ['lint', 'clean'], () => 169 | gulp.src(paths.clientEntryPoint) 170 | .pipe(webpack(webpackConfig)) 171 | .pipe(gulp.dest(paths.distDir)) 172 | ); 173 | ``` 174 | 175 | **Примечание**: Задача `build` сейчас транспилирует код ES6 в ES5 для каждого `.js` файла, расположенного в `src`. Поскольку мы разделили код на `server`, `shared` и `client`, мы могли бы компилировать только `server` и `shared` (т.к. Webpack позаботится о `client`). Тем не менее, в разделе Тестирование нам потребуется Gulp для компиляции клиентского кода, чтобы тестировать его вне Webpack. Так что пока мы не дойдем до этого раздела, будет несколько избыточное дублирование в `build`. Давайте договоримся, что пока это нормально. Вообще, мы даже не будем использовать `build` и директорию `lib`, пока не доберемся до этой части, так что все, что нас сейчас волнует - это клиентская сборка. 176 | 177 | - Запустите `yarn start`, вы должны увидеть построенный Webpack файл `client-bundle.js`. Откройте `index.html` в браузере. Должно отобразиться "Wah wah, I am Browser Toby". 178 | 179 | Еще одна вещь: в отличие от директории `lib`, файлы `dist/client-bundle.js` и `dist/client-bundle.js.map` не очищаются задачей `clean` перед каждой сборкой. 180 | 181 | - Добавьте `clientBundle: 'dist/client-bundle.js?(.map)'` в нашу конфигурацию путей (`paths`), и настройте задачу `clean` следующим образом: 182 | 183 | ```javascript 184 | gulp.task('clean', () => del([ 185 | paths.libDir, 186 | paths.clientBundle, 187 | ])); 188 | ``` 189 | 190 | - Добавьте `/dist/client-bundle.js*` в файл `.gitignore`: 191 | 192 | Следующий раздел: [8 - React](/tutorial/8-react) 193 | 194 | Назад в [предыдущий раздел](/tutorial/6-eslint) или [Содержание](/../../#Содержание). 195 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 7 - Client App with Webpack 2 | 3 | ## Structure of our app 4 | 5 | - Create a `dist` folder at the root of your project, and add the following `index.html` file to it: 6 | 7 | ```html 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ``` 18 | 19 | In your `src` folder, create the following subfolders: `server`, `shared`, `client`, and move your current `index.js` into `server`, and `dog.js` into `shared`. Create `app.js` in `client`. 20 | 21 | We are not going to do any Node back-end yet, but this separation will help you see more clearly where things belong. You'll need to change the `import Dog from './dog';` in `server/index.js` to `import Dog from '../shared/dog';` though, or ESLint will detect errors for unresolved modules. 22 | 23 | Write this in `client/app.js`: 24 | 25 | ```javascript 26 | import Dog from '../shared/dog'; 27 | 28 | const browserToby = new Dog('Browser Toby'); 29 | 30 | document.querySelector('.app').innerText = browserToby.bark(); 31 | ``` 32 | 33 | Add the following to your `package.json`, under `eslintConfig`: 34 | 35 | ```json 36 | "env": { 37 | "browser": true 38 | } 39 | ``` 40 | 41 | This way we can use variables such as `window` or `document` which are always accessible in the browser without ESLint complaining about undeclared variables. 42 | 43 | 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/) in your client code. 44 | 45 | - Run `yarn add babel-polyfill` 46 | 47 | And before anything else in `app.js`, add this import: 48 | 49 | ```javascript 50 | import 'babel-polyfill'; 51 | ``` 52 | 53 | Including the polyfill adds some weight to your bundle, so add it only if you use the features it covers. In order to provide some solid boilerplate code with this tutorial, I am including it and it will appear in code samples in the next chapters. 54 | 55 | ## Webpack 56 | 57 | In a Node environment, you can freely `import` different files and Node will resolve these files using your filesystem. In a browser, there is no filesystem, and therefore your `import`s point to nowhere. In order for our entry point file `app.js` to retrieve the tree of imports it needs, we are going to "bundle" that entire tree of dependencies into one file. Webpack is a tool that does this. 58 | 59 | Webpack uses a config file, just like Gulp, called `webpack.config.js`. It is possible to use ES6 imports and exports in it, in the exact same way that we made Gulp rely on Babel to do so: by naming this file `webpack.config.babel.js`. 60 | 61 | - Create an empty `webpack.config.babel.js` file 62 | 63 | - While you're at it, add `webpack.config.babel.js` to your Gulp `lint` task, and a few more `paths` constants: 64 | 65 | ```javascript 66 | const paths = { 67 | allSrcJs: 'src/**/*.js', 68 | gulpFile: 'gulpfile.babel.js', 69 | webpackFile: 'webpack.config.babel.js', 70 | libDir: 'lib', 71 | distDir: 'dist', 72 | }; 73 | 74 | // [...] 75 | 76 | gulp.task('lint', () => 77 | gulp.src([ 78 | paths.allSrcJs, 79 | paths.gulpFile, 80 | paths.webpackFile, 81 | ]) 82 | .pipe(eslint()) 83 | .pipe(eslint.format()) 84 | .pipe(eslint.failAfterError()) 85 | ); 86 | ``` 87 | 88 | We need to teach Webpack how to process ES6 files via Babel (just like we taught Gulp how to process ES6 files with `gulp-babel`). In Webpack, when you need to process files that are not plain old JavaScript, you use *loaders*. So let's install the Babel loader for Webpack: 89 | 90 | - Run `yarn add --dev babel-loader` 91 | 92 | - Write the following to your `webpack.config.babel.js` file: 93 | 94 | ```javascript 95 | export default { 96 | output: { 97 | filename: 'client-bundle.js', 98 | }, 99 | devtool: 'source-map', 100 | module: { 101 | loaders: [ 102 | { 103 | test: /\.jsx?$/, 104 | loader: 'babel-loader', 105 | exclude: [/node_modules/], 106 | }, 107 | ], 108 | }, 109 | resolve: { 110 | extensions: ['', '.js', '.jsx'], 111 | }, 112 | }; 113 | ``` 114 | 115 | Let's analyze this a bit: 116 | 117 | We need this file to `export` stuff for Webpack to read. `output.filename` is the name of the bundle we want to generate. `devtool: 'source-map'` will enable source maps for a better debugging experience in your browser. In `module.loaders`, we have a `test`, which is the JavaScript regex that will be used to test which files should be processed by the `babel-loader`. Since we will use both `.js` files and `.jsx` files (for React) in the next chapters, we have the following regex: `/\.jsx?$/`. The `node_modules` folder is excluded because there is no transpilation to do there. This way, when your code `import`s packages located in `node_modules`, Babel doesn't bother processing those files, which reduces build time. The `resolve` part is to tell Webpack what kind of file we want to be able to `import` in our code using extension-less paths like `import Foo from './foo'` where `foo` could be `foo.js` or `foo.jsx` for instance. 118 | 119 | Okay so now we have Webpack set up, but we still need a way to *run* it. 120 | 121 | ## Integrating Webpack to Gulp 122 | 123 | Webpack can do a lot of things. It can actually replace Gulp entirely if your project is mostly client-side. Gulp being a more general tool, it is better suited for things like linting, tests, and back-end tasks though. It is also simpler to understand for newcomers than a complex Webpack config. We have a pretty solid Gulp setup and workflow here, so integrating Webpack to our Gulp build is going to be easy peasy. 124 | 125 | Let's create the Gulp task to run Webpack. Open your `gulpfile.babel.js`. 126 | 127 | We don't need the `main` task to execute `node lib/` anymore, since we will open `index.html` to run our app. 128 | 129 | - Remove `import { exec } from 'child_process'`. 130 | 131 | Similarly to Gulp plugins, the `webpack-stream` package lets us integrate Webpack into Gulp very easily. 132 | 133 | - Install the package with: `yarn add --dev webpack-stream` 134 | 135 | - Add the following `import`s: 136 | 137 | ```javascript 138 | import webpack from 'webpack-stream'; 139 | import webpackConfig from './webpack.config.babel'; 140 | ``` 141 | 142 | The second line just grabs our config file. 143 | 144 | Like I said earlier, in the next chapter we are going to use `.jsx` files (on the client, and even on the server later on), so let's set that up right now to have a bit of a head start. 145 | 146 | - Change the constants to the following: 147 | 148 | ```javascript 149 | const paths = { 150 | allSrcJs: 'src/**/*.js?(x)', 151 | serverSrcJs: 'src/server/**/*.js?(x)', 152 | sharedSrcJs: 'src/shared/**/*.js?(x)', 153 | clientEntryPoint: 'src/client/app.js', 154 | gulpFile: 'gulpfile.babel.js', 155 | webpackFile: 'webpack.config.babel.js', 156 | libDir: 'lib', 157 | distDir: 'dist', 158 | }; 159 | ``` 160 | 161 | The `.js?(x)` is just a pattern to match `.js` or `.jsx` files. 162 | 163 | We now have constants for the different parts of our application, and an entry point file. 164 | 165 | - Modify the `main` task like so: 166 | 167 | ```javascript 168 | gulp.task('main', ['lint', 'clean'], () => 169 | gulp.src(paths.clientEntryPoint) 170 | .pipe(webpack(webpackConfig)) 171 | .pipe(gulp.dest(paths.distDir)) 172 | ); 173 | ``` 174 | 175 | **Note**: Our `build` task currently transpiles ES6 code to ES5 for every `.js` file located under `src`. Now that we've split our code into `server`, `shared`, and `client` code, we could make this task only compile `server` and `shared` (since Webpack takes care of `client`). However, in the Testing chapter, we are going to need Gulp to also compile the `client` code to test it outside of Webpack. So until you reach that chapter, there is a bit of useless duplicated build being done. I'm sure we can all agree that it's fine for now. We actually aren't even going to be using the `build` task and `lib` folder anymore until that chapter, since all we care about right now is the client bundle. 176 | 177 | - Run `yarn start`, you should now see Webpack building your `client-bundle.js` file. Opening `index.html` in your browser should display "Wah wah, I am Browser Toby". 178 | 179 | One last thing: unlike our `lib` folder, the `dist/client-bundle.js` and `dist/client-bundle.js.map` files are not being cleaned up by our `clean` task before each build. 180 | 181 | - Add `clientBundle: 'dist/client-bundle.js?(.map)'` to our `paths` configuration, and tweak the `clean` task like so: 182 | 183 | ```javascript 184 | gulp.task('clean', () => del([ 185 | paths.libDir, 186 | paths.clientBundle, 187 | ])); 188 | ``` 189 | 190 | - Add `/dist/client-bundle.js*` to your `.gitignore` file: 191 | 192 | Next section: [8 - React](/tutorial/8-react) 193 | 194 | Back to the [previous section](/tutorial/6-eslint) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 195 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import webpack from 'webpack-stream'; 8 | import webpackConfig from './webpack.config.babel'; 9 | 10 | const paths = { 11 | allSrcJs: 'src/**/*.js?(x)', 12 | serverSrcJs: 'src/server/**/*.js?(x)', 13 | sharedSrcJs: 'src/shared/**/*.js?(x)', 14 | clientEntryPoint: 'src/client/app.js', 15 | clientBundle: 'dist/client-bundle.js?(.map)', 16 | gulpFile: 'gulpfile.babel.js', 17 | webpackFile: 'webpack.config.babel.js', 18 | libDir: 'lib', 19 | distDir: 'dist', 20 | }; 21 | 22 | gulp.task('lint', () => 23 | gulp.src([ 24 | paths.allSrcJs, 25 | paths.gulpFile, 26 | paths.webpackFile, 27 | ]) 28 | .pipe(eslint()) 29 | .pipe(eslint.format()) 30 | .pipe(eslint.failAfterError()) 31 | ); 32 | 33 | gulp.task('clean', () => del([ 34 | paths.libDir, 35 | paths.clientBundle, 36 | ])); 37 | 38 | gulp.task('build', ['lint', 'clean'], () => 39 | gulp.src(paths.allSrcJs) 40 | .pipe(babel()) 41 | .pipe(gulp.dest(paths.libDir)) 42 | ); 43 | 44 | gulp.task('main', ['lint', 'clean'], () => 45 | gulp.src(paths.clientEntryPoint) 46 | .pipe(webpack(webpackConfig)) 47 | .pipe(gulp.dest(paths.distDir)) 48 | ); 49 | 50 | gulp.task('watch', () => { 51 | gulp.watch(paths.allSrcJs, ['main']); 52 | }); 53 | 54 | gulp.task('default', ['watch', 'main']); 55 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | } 18 | }, 19 | "babel": { 20 | "presets": [ 21 | "latest" 22 | ] 23 | }, 24 | "devDependencies": { 25 | "babel-loader": "^6.2.5", 26 | "babel-preset-latest": "^6.16.0", 27 | "del": "^2.2.2", 28 | "eslint": "^3.8.1", 29 | "eslint-config-airbnb": "^12.0.0", 30 | "eslint-plugin-import": "^2.0.1", 31 | "eslint-plugin-jsx-a11y": "^2.2.2", 32 | "eslint-plugin-react": "^6.4.1", 33 | "gulp": "^3.9.1", 34 | "gulp-babel": "^6.1.2", 35 | "gulp-eslint": "^3.0.1", 36 | "webpack-stream": "^3.2.0" 37 | }, 38 | "dependencies": { 39 | "babel-polyfill": "^6.16.0" 40 | }, 41 | "repository": "verekia/js-stack-from-scratch", 42 | "author": "Jonathan Verrecchia - @verekia", 43 | "license": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/src/client/app.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const browserToby = new Dog('Browser Toby'); 6 | 7 | document.querySelector('.app').innerText = browserToby.bark(); 8 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/8-react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/8-react/README.md: -------------------------------------------------------------------------------- 1 | # 8 - React 2 | 3 | Теперь мы начнем генерировать наше приложение с помощью React. 4 | 5 | Сначала установим React и ReactDOM: 6 | 7 | - запустите `yarn add react react-dom` 8 | 9 | Эти два пакета будут включены в раздел `"dependencies"`, а не `"devDependencies"`, потому что, в отличие от инструментов разработки, они должны быть в итоговой клиентской сборке (production). 10 | 11 | Давайте переименуем файл `src/client/app.js` в `src/client/app.jsx` и напишем в нем немного кода на React и JSX: 12 | 13 | ```javascript 14 | import 'babel-polyfill'; 15 | 16 | import React, { PropTypes } from 'react'; 17 | import ReactDOM from 'react-dom'; 18 | import Dog from '../shared/dog'; 19 | 20 | const dogBark = new Dog('Browser Toby').bark(); 21 | 22 | const App = props => ( 23 |
24 | The dog says: {props.message} 25 |
26 | ); 27 | 28 | App.propTypes = { 29 | message: PropTypes.string.isRequired, 30 | }; 31 | 32 | ReactDOM.render(, document.querySelector('.app')); 33 | ``` 34 | 35 | **Примечание**: Если вы не знакомы с React или его PropTypes (типы параметров), ознакомьтесь сначала с ним и затем возвращайтесь в это руководство. В следующих главах мы будем использовать React, поэтому вам понадобится его хорошее понимание. 36 | 37 | В Gulp файле, измените в `clientEntryPoint` расширение на `.jsx`: 38 | 39 | ```javascript 40 | clientEntryPoint: 'src/client/app.jsx', 41 | ``` 42 | 43 | Поскольку мы используем синтаксис JSX, нам так же нужен Babel для его обработки. 44 | Установите React Babel preset, который научит Babel обрабатывать JSX синтаксис 45 | `yarn add --dev babel-preset-react`, и откорректируйте раздел `babel` в `package.json` следующим образом: 46 | 47 | ```json 48 | "babel": { 49 | "presets": [ 50 | "latest", 51 | "react" 52 | ] 53 | }, 54 | ``` 55 | 56 | Теперь после запуска `yarn start`, открыв `index.html`, мы должны увидеть "The dog says: Wah wah, I am Browser Toby", сгенерированное React. 57 | 58 | Следующий раздел: [9 - Redux](/tutorial/9-redux) 59 | 60 | Назад в [предыдущий раздел](/tutorial/7-client-webpack) или [Содержание](/../../#Содержание). 61 | -------------------------------------------------------------------------------- /tutorial/8-react/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 8 - React 2 | 3 | We're now going to render our app using React. 4 | 5 | First, let's install React and ReactDOM: 6 | 7 | - Run `yarn add react react-dom` 8 | 9 | These 2 packages go to our `"dependencies"` and not `"devDependencies"` because unlike build tools, the client bundle needs them in production. 10 | 11 | Let's rename our `src/client/app.js` file into `src/client/app.jsx` and write some React and JSX code in it: 12 | 13 | ```javascript 14 | import 'babel-polyfill'; 15 | 16 | import React, { PropTypes } from 'react'; 17 | import ReactDOM from 'react-dom'; 18 | import Dog from '../shared/dog'; 19 | 20 | const dogBark = new Dog('Browser Toby').bark(); 21 | 22 | const App = props => ( 23 |
24 | The dog says: {props.message} 25 |
26 | ); 27 | 28 | App.propTypes = { 29 | message: PropTypes.string.isRequired, 30 | }; 31 | 32 | ReactDOM.render(, document.querySelector('.app')); 33 | ``` 34 | 35 | **Note**: If you are unfamiliar with React or its PropTypes, learn about React first and come back to this tutorial later. There is going to be quite some React things in the upcoming chapters, so you need a good understanding of it. 36 | 37 | In your Gulpfile, change the value of `clientEntryPoint` to give it a `.jsx` extension: 38 | 39 | ```javascript 40 | clientEntryPoint: 'src/client/app.jsx', 41 | ``` 42 | 43 | Since we use the JSX syntax here, we have to tell Babel that it needs to transform it as well. 44 | Install the React Babel preset, which will teach Babel how to process the JSX syntax: 45 | `yarn add --dev babel-preset-react` and change the `babel` entry in your `package.json` file like so: 46 | 47 | ```json 48 | "babel": { 49 | "presets": [ 50 | "latest", 51 | "react" 52 | ] 53 | }, 54 | ``` 55 | 56 | Now after running `yarn start`, if we open `index.html`, we should see "The dog says: Wah wah, I am Browser Toby" rendered by React. 57 | 58 | Next section: [9 - Redux](/tutorial/9-redux) 59 | 60 | Back to the [previous section](/tutorial/7-client-webpack) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 61 | -------------------------------------------------------------------------------- /tutorial/8-react/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/8-react/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import webpack from 'webpack-stream'; 8 | import webpackConfig from './webpack.config.babel'; 9 | 10 | const paths = { 11 | allSrcJs: 'src/**/*.js?(x)', 12 | serverSrcJs: 'src/server/**/*.js?(x)', 13 | sharedSrcJs: 'src/shared/**/*.js?(x)', 14 | clientEntryPoint: 'src/client/app.jsx', 15 | clientBundle: 'dist/client-bundle.js?(.map)', 16 | gulpFile: 'gulpfile.babel.js', 17 | webpackFile: 'webpack.config.babel.js', 18 | libDir: 'lib', 19 | distDir: 'dist', 20 | }; 21 | 22 | gulp.task('lint', () => 23 | gulp.src([ 24 | paths.allSrcJs, 25 | paths.gulpFile, 26 | paths.webpackFile, 27 | ]) 28 | .pipe(eslint()) 29 | .pipe(eslint.format()) 30 | .pipe(eslint.failAfterError()) 31 | ); 32 | 33 | gulp.task('clean', () => del([ 34 | paths.libDir, 35 | paths.clientBundle, 36 | ])); 37 | 38 | gulp.task('build', ['lint', 'clean'], () => 39 | gulp.src(paths.allSrcJs) 40 | .pipe(babel()) 41 | .pipe(gulp.dest(paths.libDir)) 42 | ); 43 | 44 | gulp.task('main', ['lint', 'clean'], () => 45 | gulp.src(paths.clientEntryPoint) 46 | .pipe(webpack(webpackConfig)) 47 | .pipe(gulp.dest(paths.distDir)) 48 | ); 49 | 50 | gulp.task('watch', () => { 51 | gulp.watch(paths.allSrcJs, ['main']); 52 | }); 53 | 54 | gulp.task('default', ['watch', 'main']); 55 | -------------------------------------------------------------------------------- /tutorial/8-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | } 18 | }, 19 | "babel": { 20 | "presets": [ 21 | "latest", 22 | "react" 23 | ] 24 | }, 25 | "dependencies": { 26 | "babel-polyfill": "^6.16.0", 27 | "react": "^15.3.2", 28 | "react-dom": "^15.3.2" 29 | }, 30 | "devDependencies": { 31 | "babel-loader": "^6.2.5", 32 | "babel-preset-latest": "^6.16.0", 33 | "babel-preset-react": "^6.16.0", 34 | "del": "^2.2.2", 35 | "eslint": "^3.8.1", 36 | "eslint-config-airbnb": "^12.0.0", 37 | "eslint-plugin-import": "^2.0.1", 38 | "eslint-plugin-jsx-a11y": "^2.2.3", 39 | "eslint-plugin-react": "^6.4.1", 40 | "gulp": "^3.9.1", 41 | "gulp-babel": "^6.1.2", 42 | "gulp-eslint": "^3.0.1", 43 | "webpack-stream": "^3.2.0" 44 | }, 45 | "repository": "verekia/js-stack-from-scratch", 46 | "author": "Jonathan Verrecchia - @verekia", 47 | "license": "MIT" 48 | } 49 | -------------------------------------------------------------------------------- /tutorial/8-react/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React, { PropTypes } from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Dog from '../shared/dog'; 6 | 7 | const dogBark = new Dog('Browser Toby').bark(); 8 | 9 | const App = props => ( 10 |
11 | The dog says: {props.message} 12 |
13 | ); 14 | 15 | App.propTypes = { 16 | message: PropTypes.string.isRequired, 17 | }; 18 | 19 | ReactDOM.render(, document.querySelector('.app')); 20 | -------------------------------------------------------------------------------- /tutorial/8-react/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/8-react/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/8-react/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/9-redux/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/9-redux/README.md: -------------------------------------------------------------------------------- 1 | # 9 - Redux 2 | 3 | В этой части (которая наиболее сложная из пройденных), мы добавим в наше приложение [Redux](http://redux.js.org/) ([документация на русском](https://www.gitbook.com/book/rajdee/redux-in-russian/details)) и подключим его к React. Redux управляет состоянием приложения. Он включает в себя такие понятия, как: 4 | 5 | - **хранилище** (store) - простой JavaScript объект, представляющий состояние вашего приложения; 6 | - **действия** (actions), которые обычно запускаются пользователем; 7 | - **редюсеры** (reducers), которые можно рассматривать как обработчики действий. 8 | 9 | Редюсеры воздействуют на состояние приложения (**хранилище**), и когда состояние приложения изменяется, что-то происходит в вашем приложении. Хорошая визуальная демонстрация Redux находится [здесь](http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production#/9). 10 | 11 | Для того чтобы продемонстрировать использование Redux наиболее доступным способом, наше приложение будет состоять из сообщения и кнопки. Сообщение будет показывать лает собака или нет (изначально - нет), а кнопка будет заставлять ее лаять, что должно будет отражаться в сообщении. 12 | 13 | В этой части нам потребуется два пакета: `redux` и `react-redux`. 14 | 15 | - Запустите `yarn add redux react-redux`. 16 | 17 | Давайте начнем с создания двух папок: `src/client/actions` и `src/client/reducers`. 18 | 19 | - В `actions`, создайте `dog-actions.js`: 20 | 21 | ```javascript 22 | export const MAKE_BARK = 'MAKE_BARK'; 23 | 24 | export const makeBark = () => ({ 25 | type: MAKE_BARK, 26 | payload: true, 27 | }); 28 | ``` 29 | 30 | Тут мы определяем тип действия - `MAKE_BARK`, и функцию `makeBark` (именуемую *генератор действий*), которая запускает действие `MAKE_BARK`. Мы их экспортируем, т.к. они нам понадобятся в других файлах. Это действие построено на основе модели [Flux Standard Action](https://github.com/acdlite/flux-standard-action), вот почему оно имеет атрибуты `type` и `payload`. 31 | 32 | - В `reducers` создайте `dog-reducer.js`: 33 | 34 | ```javascript 35 | import { MAKE_BARK } from '../actions/dog-actions'; 36 | 37 | const initialState = { 38 | hasBarked: false, 39 | }; 40 | 41 | const dogReducer = (state = initialState, action) => { 42 | switch (action.type) { 43 | case MAKE_BARK: 44 | return { hasBarked: action.payload }; 45 | default: 46 | return state; 47 | } 48 | }; 49 | 50 | export default dogReducer; 51 | ``` 52 | 53 | Здесь мы определили исходное состояние приложения, являющееся объектом, содержащим свойство `hasBarked`, установленное в `false`, и `dogReducer` - функцию, ответственную за перемену состояния, в зависимости от того, какое действие произошло. Состояние не может быть изменено в этой функции, но должен быть возвращен совершенно новый объект состояния. 54 | 55 | - Изменим `app.jsx`, чтобы создать *хранилище*. Можете заменить весь файл следующим содержимым: 56 | 57 | ```javascript 58 | import React from 'react'; 59 | import ReactDOM from 'react-dom'; 60 | import { createStore, combineReducers } from 'redux'; 61 | import { Provider } from 'react-redux'; 62 | import dogReducer from './reducers/dog-reducer'; 63 | import BarkMessage from './containers/bark-message'; 64 | import BarkButton from './containers/bark-button'; 65 | 66 | const store = createStore(combineReducers({ 67 | dog: dogReducer, 68 | })); 69 | 70 | ReactDOM.render( 71 | 72 |
73 | 74 | 75 |
76 |
77 | , document.querySelector('.app') 78 | ); 79 | ``` 80 | 81 | Наше хранилище создано функцией Redux `createStore`, вполне наглядно. Объект хранилища собирается путем комбинирования всех редюсеров (в нашем случае одного) с помощью функции Redux `combineReducers`. Каждый редюсер в ней имеет имя, наш назовем `dog`. 82 | 83 | Мы сделали достаточно в части, относящейся к чистому Redux. 84 | 85 | Теперь мы подключим Redux к React, используя пакет `react-redux`. Для того чтобы `react-redux` мог передать хранилище в наше приложение на React, нам нужно обернуть все приложение в компонент ``. Этот компонент должен содержать единственный дочерний элемент, так что мы добавили `
`, и этот `
` содержит два основных элемента нашего приложения: `BarkMessage` и `BarkButton`. 86 | 87 | Как вы могли заметить, в разделе `import` мы импортируем `BarkMessage` и `BarkButton` из директории `containers`. Сейчас самое время представить концепцию **Компонентов** и **Контейнеров**. 88 | 89 | *Компоненты* - это *"глупые"* компоненты React, в том смысле, что они ничего не знают о состоянии Redux. *Контейнеры* - *"умные"*, знают о состоянии и о том, что мы собираемся *подключиться* (*connect*) к "глупым" компонентам. 90 | 91 | - Создайте 2 папки, `src/client/components` и `src/client/containers`. 92 | 93 | - В `components` создайте следующие файлы: 94 | 95 | **button.jsx** 96 | 97 | ```javascript 98 | import React, { PropTypes } from 'react'; 99 | 100 | const Button = ({ action, actionLabel }) => ; 101 | 102 | Button.propTypes = { 103 | action: PropTypes.func.isRequired, 104 | actionLabel: PropTypes.string.isRequired, 105 | }; 106 | 107 | export default Button; 108 | ``` 109 | 110 | и **message.jsx**: 111 | 112 | ```javascript 113 | import React, { PropTypes } from 'react'; 114 | 115 | const Message = ({ message }) =>
{message}
; 116 | 117 | Message.propTypes = { 118 | message: PropTypes.string.isRequired, 119 | }; 120 | 121 | export default Message; 122 | 123 | ``` 124 | 125 | Это примеры *"глупых"* компонентов. Они практически лишены логики и просто отображают то что потребуется, путем передачи им **свойств** (props) React. Основное отличие `button.jsx` от `message.jsx` в том, что первый содержит **действие** в качестве одного из параметров. Это действие срабатывает по событию `onClick`. В контексте нашего приложения надпись `Button` никогда не будет изменяться, однако компонент `Message` должен отражать состояние нашего приложения и будет изменяться на основе этого. 126 | 127 | Опять же, *компоненты* ничего не знают о Redux **действиях** или о **состоянии** нашего приложения. Вот почему мы собираемся создать "умные" **контейнеры**, которые "подведут" нужные *действия* и *данные* к этим двум "глупым" компонентам. 128 | 129 | - В `containers` создайте следующие файлы: 130 | 131 | **bark-button.js** 132 | 133 | ```javascript 134 | import { connect } from 'react-redux'; 135 | import Button from '../components/button'; 136 | import { makeBark } from '../actions/dog-actions'; 137 | 138 | const mapDispatchToProps = dispatch => ({ 139 | action: () => { dispatch(makeBark()); }, 140 | actionLabel: 'Bark', 141 | }); 142 | 143 | export default connect(null, mapDispatchToProps)(Button); 144 | ``` 145 | 146 | и **bark-message.js**: 147 | 148 | ```javascript 149 | import { connect } from 'react-redux'; 150 | import Message from '../components/message'; 151 | 152 | const mapStateToProps = state => ({ 153 | message: state.dog.hasBarked ? 'The dog barked' : 'The dog did not bark', 154 | }); 155 | 156 | export default connect(mapStateToProps)(Message); 157 | ``` 158 | 159 | `BarkButton` подключает действие `makeBark` и метод Redux `dispatch` к `Button`. А `BarkMessage` подключает `Message` к состоянию приложения. Когда состояние изменится, `Message` автоматически перегенерируется с нужным значением свойства `message`. Эти подключения выполнены через функцию `connect` пакета `react-redux`. 160 | 161 | - Теперь можете запустить `yarn start` и открыть `index.html`. Вы должны увидеть надпись "The dog did not bark" и кнопку. Когда вы нажмете на кнопку, сообщение должно измениться на "The dog barked". 162 | 163 | Следующий раздел: [10 - Immutable JS и улучшения Redux](/tutorial/10-immutable-redux-improvements) 164 | 165 | Назад в [предыдущий раздел](/tutorial/8-react) или [Содержание](/../../#Содержание). 166 | -------------------------------------------------------------------------------- /tutorial/9-redux/READMEeng.md: -------------------------------------------------------------------------------- 1 | # 9 - Redux 2 | 3 | In this chapter (which is the most difficult so far) we will be adding [Redux](http://redux.js.org/) to our application and will hook it up with React. Redux manages the state of your application. It is composed of a **store** which is a plain JavaScript object representing the state of your app, **actions** which are typically triggered by users, and **reducers** which can be seen as action handlers. Reducers affect your application state (the *store*), and when the application state is modified, things happen in your app. A good visual demonstration of Redux can be found [here](http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production#/9). 4 | 5 | In order to demonstrate how to use Redux in the simplest possible way, our app will consist of a message and a button. The message says whether the dog has barked or not (it initially hasn't), and the button makes the dog bark, which should update the message. 6 | 7 | We are going to need 2 packages in this part, `redux` and `react-redux`. 8 | 9 | - Run `yarn add redux react-redux`. 10 | 11 | Lets start by creating 2 folders: `src/client/actions` and `src/client/reducers`. 12 | 13 | - In `actions`, create `dog-actions.js`: 14 | 15 | ```javascript 16 | export const MAKE_BARK = 'MAKE_BARK'; 17 | 18 | export const makeBark = () => ({ 19 | type: MAKE_BARK, 20 | payload: true, 21 | }); 22 | ``` 23 | 24 | Here we define an action type, `MAKE_BARK`, and a function (also known as *action creator*) that triggers a `MAKE_BARK` action called `makeBark`. Both are exported because we'll need them both in other files. This action implements the [Flux Standard Action](https://github.com/acdlite/flux-standard-action) model, which is why it has `type` and `payload` attributes. 25 | 26 | - In `reducers`, create `dog-reducer.js`: 27 | 28 | ```javascript 29 | import { MAKE_BARK } from '../actions/dog-actions'; 30 | 31 | const initialState = { 32 | hasBarked: false, 33 | }; 34 | 35 | const dogReducer = (state = initialState, action) => { 36 | switch (action.type) { 37 | case MAKE_BARK: 38 | return { hasBarked: action.payload }; 39 | default: 40 | return state; 41 | } 42 | }; 43 | 44 | export default dogReducer; 45 | ``` 46 | 47 | Here we define the initial state of our app, which is an object containing the `hasBarked` property set to `false`, and the `dogReducer`, which is the function responsible for altering the state based on which action happened. The state cannot be modified in this function, a brand new state object must be returned. 48 | 49 | - We are now going to modify `app.jsx` to create the *store*. You can replace the entire content of that file by the following: 50 | 51 | ```javascript 52 | import React from 'react'; 53 | import ReactDOM from 'react-dom'; 54 | import { createStore, combineReducers } from 'redux'; 55 | import { Provider } from 'react-redux'; 56 | import dogReducer from './reducers/dog-reducer'; 57 | import BarkMessage from './containers/bark-message'; 58 | import BarkButton from './containers/bark-button'; 59 | 60 | const store = createStore(combineReducers({ 61 | dog: dogReducer, 62 | })); 63 | 64 | ReactDOM.render( 65 | 66 |
67 | 68 | 69 |
70 |
71 | , document.querySelector('.app') 72 | ); 73 | ``` 74 | 75 | Our store is created by the Redux function `createStore`, pretty explicit. The store object is assembled by combining all our reducers (in our case, only one) using Redux's `combineReducers` function. Each reducer is named here, and we'll name ours `dog`. 76 | 77 | That's pretty much it for the pure Redux part. 78 | 79 | Now we are going to hook up Redux with React using `react-redux`. In order for `react-redux` to pass the store to our React app, it needs to wrap the entire app in a `` component. This component must have a single child, so we created a `
`, and this `
` contains the 2 main elements of our app, a `BarkMessage` and a `BarkButton`. 80 | 81 | As you can tell in the `import` section, `BarkMessage` and `BarkButton` are imported from a `containers` folder. Now is a good time to introduce the concept of **Components** and **Containers**. 82 | 83 | *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. 84 | 85 | - Create 2 folders, `src/client/components` and `src/client/containers`. 86 | 87 | - In `components`, create the following files: 88 | 89 | **button.jsx** 90 | 91 | ```javascript 92 | import React, { PropTypes } from 'react'; 93 | 94 | const Button = ({ action, actionLabel }) => ; 95 | 96 | Button.propTypes = { 97 | action: PropTypes.func.isRequired, 98 | actionLabel: PropTypes.string.isRequired, 99 | }; 100 | 101 | export default Button; 102 | ``` 103 | 104 | and **message.jsx**: 105 | 106 | ```javascript 107 | import React, { PropTypes } from 'react'; 108 | 109 | const Message = ({ message }) =>
{message}
; 110 | 111 | Message.propTypes = { 112 | message: PropTypes.string.isRequired, 113 | }; 114 | 115 | export default Message; 116 | 117 | ``` 118 | 119 | 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 an **action** in its props. That action is bound on the `onClick` event. In the context of our app, the `Button` label is never going to change, however, the `Message` component is going to reflect the state of our app, and will vary based on the state. 120 | 121 | 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 *actions* and *data* to these 2 dumb components. 122 | 123 | - In `containers`, create the following files: 124 | 125 | **bark-button.js** 126 | 127 | ```javascript 128 | import { connect } from 'react-redux'; 129 | import Button from '../components/button'; 130 | import { makeBark } from '../actions/dog-actions'; 131 | 132 | const mapDispatchToProps = dispatch => ({ 133 | action: () => { dispatch(makeBark()); }, 134 | actionLabel: 'Bark', 135 | }); 136 | 137 | export default connect(null, mapDispatchToProps)(Button); 138 | ``` 139 | 140 | and **bark-message.js**: 141 | 142 | ```javascript 143 | import { connect } from 'react-redux'; 144 | import Message from '../components/message'; 145 | 146 | const mapStateToProps = state => ({ 147 | message: state.dog.hasBarked ? 'The dog barked' : 'The dog did not bark', 148 | }); 149 | 150 | export default connect(mapStateToProps)(Message); 151 | ``` 152 | 153 | `BarkButton` will hook up `Button` with the `makeBark` action and Redux's `dispatch` method, and `BarkMessage` will hook up the app state with `Message`. 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`. 154 | 155 | - You can now run `yarn start` and open `index.html`. You should see "The dog did not bark" and a button. When you click the button, the message should show "The dog barked". 156 | 157 | Next section: [10 - Immutable JS and Redux Improvements](/tutorial/10-immutable-redux-improvements) 158 | 159 | Back to the [previous section](/tutorial/8-react) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 160 | -------------------------------------------------------------------------------- /tutorial/9-redux/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/9-redux/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import webpack from 'webpack-stream'; 8 | import webpackConfig from './webpack.config.babel'; 9 | 10 | const paths = { 11 | allSrcJs: 'src/**/*.js?(x)', 12 | serverSrcJs: 'src/server/**/*.js?(x)', 13 | sharedSrcJs: 'src/shared/**/*.js?(x)', 14 | clientEntryPoint: 'src/client/app.jsx', 15 | clientBundle: 'dist/client-bundle.js?(.map)', 16 | gulpFile: 'gulpfile.babel.js', 17 | webpackFile: 'webpack.config.babel.js', 18 | libDir: 'lib', 19 | distDir: 'dist', 20 | }; 21 | 22 | gulp.task('lint', () => 23 | gulp.src([ 24 | paths.allSrcJs, 25 | paths.gulpFile, 26 | paths.webpackFile, 27 | ]) 28 | .pipe(eslint()) 29 | .pipe(eslint.format()) 30 | .pipe(eslint.failAfterError()) 31 | ); 32 | 33 | gulp.task('clean', () => del([ 34 | paths.libDir, 35 | paths.clientBundle, 36 | ])); 37 | 38 | gulp.task('build', ['lint', 'clean'], () => 39 | gulp.src(paths.allSrcJs) 40 | .pipe(babel()) 41 | .pipe(gulp.dest(paths.libDir)) 42 | ); 43 | 44 | gulp.task('main', ['lint', 'clean'], () => 45 | gulp.src(paths.clientEntryPoint) 46 | .pipe(webpack(webpackConfig)) 47 | .pipe(gulp.dest(paths.distDir)) 48 | ); 49 | 50 | gulp.task('watch', () => { 51 | gulp.watch(paths.allSrcJs, ['main']); 52 | }); 53 | 54 | gulp.task('default', ['watch', 'main']); 55 | -------------------------------------------------------------------------------- /tutorial/9-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | } 18 | }, 19 | "babel": { 20 | "presets": [ 21 | "latest", 22 | "react" 23 | ] 24 | }, 25 | "dependencies": { 26 | "babel-polyfill": "^6.16.0", 27 | "react": "^15.3.2", 28 | "react-dom": "^15.3.2", 29 | "react-redux": "^4.4.5", 30 | "redux": "^3.6.0" 31 | }, 32 | "devDependencies": { 33 | "babel-loader": "^6.2.5", 34 | "babel-preset-latest": "^6.16.0", 35 | "babel-preset-react": "^6.16.0", 36 | "del": "^2.2.2", 37 | "eslint": "^3.8.1", 38 | "eslint-config-airbnb": "^12.0.0", 39 | "eslint-plugin-import": "^2.0.1", 40 | "eslint-plugin-jsx-a11y": "^2.2.3", 41 | "eslint-plugin-react": "^6.4.1", 42 | "gulp": "^3.9.1", 43 | "gulp-babel": "^6.1.2", 44 | "gulp-eslint": "^3.0.1", 45 | "webpack-stream": "^3.2.0" 46 | }, 47 | "repository": "verekia/js-stack-from-scratch", 48 | "author": "Jonathan Verrecchia - @verekia", 49 | "license": "MIT" 50 | } 51 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/actions/dog-actions.js: -------------------------------------------------------------------------------- 1 | export const MAKE_BARK = 'MAKE_BARK'; 2 | 3 | export const makeBark = () => ({ 4 | type: MAKE_BARK, 5 | payload: true, 6 | }); 7 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { createStore, combineReducers } from 'redux'; 6 | import { Provider } from 'react-redux'; 7 | import dogReducer from './reducers/dog-reducer'; 8 | import BarkMessage from './containers/bark-message'; 9 | import BarkButton from './containers/bark-button'; 10 | 11 | const store = createStore(combineReducers({ 12 | dog: dogReducer, 13 | })); 14 | 15 | ReactDOM.render( 16 | 17 |
18 | 19 | 20 |
21 |
22 | , document.querySelector('.app') 23 | ); 24 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/components/button.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Button = ({ action, actionLabel }) => ; 4 | 5 | Button.propTypes = { 6 | action: PropTypes.func.isRequired, 7 | actionLabel: PropTypes.string.isRequired, 8 | }; 9 | 10 | export default Button; 11 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/components/message.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Message = ({ message }) =>
{message}
; 4 | 5 | Message.propTypes = { 6 | message: PropTypes.string.isRequired, 7 | }; 8 | 9 | export default Message; 10 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/containers/bark-button.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Button from '../components/button'; 3 | import { makeBark } from '../actions/dog-actions'; 4 | 5 | const mapDispatchToProps = dispatch => ({ 6 | action: () => { dispatch(makeBark()); }, 7 | actionLabel: 'Bark', 8 | }); 9 | 10 | export default connect(null, mapDispatchToProps)(Button); 11 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/containers/bark-message.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Message from '../components/message'; 3 | 4 | const mapStateToProps = state => ({ 5 | message: state.dog.hasBarked ? 'The dog barked' : 'The dog did not bark', 6 | }); 7 | 8 | export default connect(mapStateToProps)(Message); 9 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/reducers/dog-reducer.js: -------------------------------------------------------------------------------- 1 | import { MAKE_BARK } from '../actions/dog-actions'; 2 | 3 | const initialState = { 4 | hasBarked: false, 5 | }; 6 | 7 | const dogReducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case MAKE_BARK: 10 | return { hasBarked: action.payload }; 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | export default dogReducer; 17 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/9-redux/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | --------------------------------------------------------------------------------