Основные возможности платформы Hexlet не доступны в вашем браузере. Пожалуйста, обновитесь.
,

РазработкаОписание современного JavaScript для динозавров

Это перевод статьи Питера Янга Modern JavaScript Explained For Dinosaurs. Она познакомит вас с инфраструктурой современной фронтэнд-разработки.

Наш курс JS: Настройка окружения выполняет схожую задачу, но чуть глубже, последовательно и без привязки только к фронтэнду. Этим темам также посвящена значительная часть первого проекта в JS Backend и JS Frontend. Там мы на деле устанавливаем и настраиваем среду для разработки, проверки, публикации, сборки и запуска своих приложений.


Изучать современный JavaScript — болезненно, если вы не знакомы с ним с самого его рождения. Экосистема разрастается и меняется с такой скоростью, что сложно разобраться с тем, какие проблемы пытаются решить разные инструменты. Я начал программировать в 1998 году, но к серьёзному изучению JavaScript приступил только в 2014. В то время я помню как анализировал Browserify и изумлённо смотрел на его слоган:

Browserify позволяет запрашивать (require) модули в браузере, объединяя все зависимости.

Можно сказать, я не понимал ни слова в этом предложении, и с трудом осознавал, насколько это может быть полезно мне, как разработчику.

Цель этой статьи — показать исторический контекст развития инструментов JavaScript до их уровня в 2017. Начнём с самых первых моментов и построим шаблон веб-сайта, как бы это сделали динозавры — без инструментов, чистый HTML и JavaScript. Затем мы будем пошагово вводить различные инструменты, чтобы на практике видеть, какие задачи они решают — поочерёдно. Благодаря историческому контексту у вас будет больше возможностей изучить и лучше адаптироваться к бесконечно меняющемуся JavaScript. Давайте начнём!

Использование JavaScript старомодным способом

Давайте начнём со старомодного веб-сайта, написанного на HTML и JavaScript, что включает загрузку и помещение ссылок на файлы вручную. Вот простой index.html файл, который ссылается на JavaScript файл:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>JavaScript Example</title>
  <script src="index.js"></script>
</head>
<body>
  <h1>Hello from HTML!</h1>
</body>
</html>

Строчка <script src="index.js"></script> ссылается на отдельный JavaScript-файл с названием index.js в той же директории:

// index.js
console.log("Hello from JavaScript!");

Это всё, что вам нужно, чтобы сделать веб-сайт!

Теперь давайте предположим, что вы хотите добавить библиотеку, которую написал кто-то другой, вроде moment.js (библиотеку, которая помогает превращать даты в понятные для чтения человеком). Например, вы можете использовать функцию moment в JavaScript вот так:

moment().startOf('day').fromNow();        // 20 hours ago

Но только при условии, что вы включите moment.js в код своего веб-сайта! На домашней странице moment.js вы найдёте такие инструкции:

img

Хмм, как много всего происходит в секции Install справа. Но давайте пока проигнорируем это — мы можем добавить moment.js на веб-сайт, загрузив файл moment.min.js в ту же директорию и включив его в файл index.html.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Example</title>
  <link rel="stylesheet" href="index.css">
  <script src="moment.min.js"></script>
  <script src="index.js"></script>
</head>
<body>
  <h1>Hello from HTML!</h1>
</body>
</html>

Заметьте, что moment.min.js загружается перед index.js, а это значит вы можете использовать функцию moment в index.js таким способом:

// index.js
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());

И вот таким образом мы раньше делали веб-сайты с JavaScript-библиотеками! Положительный момент в том, что всё было достаточно легко понимать. Отрицательный — было муторно искать и загружать новые версии библиотек каждый раз, когда они обновлялись.

Использование пакетного менеджера JavaScript (npm)

Начиная примерно с 2010 появилось несколько конкурирующих пакетных менеджеров JavaScript для автоматизации процесса загрузки и обновления библиотек из центрального репозитория. Bower был, возможно, самым популярным в 2013, но в итоге, примерно в 2015 его настиг npm. (Стоит отметить, что с конца 2016 yarn привлёк много внимания, как альтернатива интерфейсу npm, но он всё ещё использует npm пакеты).

Заметьте, что npm был изначально пакетным менеджером, созданным специально для node.js — среды исполнения JavaScript, предназначенной для запуска на сервере, а не во фронтенде. Что делает его очень странным выбором для фронтенд-пакетного менеджера JavaScript библиотек, предназначенных запускаться в браузере.

Заметьте: Использование пакетного менеджера обычно требует работы с командной строкой, что в прошлом никогда не требовалось от фронтенд-разработки. Если вы никогда ей не пользовались, можете прочитать этот туториал, чтобы составить представление о том, как начать. Так или иначе, умение пользоваться командной строкой — важная часть современного JavaScript (и ещё она открывает двери в другие области разработки).

На Хекслете как раз есть бесплатный курс Bash: Основы командной строки, — прим. ред.

Давайте взглянем как использовать npm, чтобы установить пакет moment.js автоматически, вместо того, чтобы загружать его вручную. Если у вас установлен node.js, у вас уже есть и npm, а это значит, что вы можете через командную строку перейти к папке, в которой находится файл index.html и ввести:

$ npm init

Будет выведено несколько вопросов (можно оставить умолчания: нажимать "Enter" после каждого вопроса) и сгенерируется новый файл с именем package.json. Это конфигурационный файл, который npm использует, чтобы хранить всю информацию о проекте. С настройками по-умолчанию содержимое package.json должно выглядеть подобно этому:

{
  "name": "your-project-name",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Чтобы установить JavaScript-пакет moment.js, мы теперь можем следовать инструкциям npm на их домашней странице, введя в командную строку:

$ npm install moment --save

Эта команда выполняет две задачи: в начале она загружает весь код из пакета moment.js в папку, называемую node_modules. Потом автоматически модифицирует файл package.json, чтобы мониторить moment.js как зависимость (зависимости — это пакеты и библиотеки, от которых зависит наше приложение).

{
  "name": "modern-javascript-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "moment": "^2.19.1"
  }
}

Это будет полезно в будущем, когда вместо расшаривания проекта с остальными — а именно папки node_modules (которая может сильно разрастись) — вам понадобится расшарить только файл package.json, и другие разработчики смогут установить необходимые пакеты автоматически с помощью команды npm install.

Теперь мы больше не должны загружать moment.js вручную с веб-сайта, потому что можем сделать это автоматически и обновить, используя npm. Заглянув внутрь папки node_modules можно увидеть файл moment.min.js в директории node_modules/moment/min. Это значит, мы можем сослаться на npm загружаемую версию moment.min.js в файле index.html, как видно ниже:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>JavaScript Example</title>
  <script src="node_modules/moment/min/moment.min.js"></script>
  <script src="index.js"></script>
</head>
<body>
  <h1>Hello from HTML!</h1>
</body>
</html>

Хорошие новости в том, что теперь мы можем использовать npm, чтобы загружать и обновлять пакеты с помощью командной строки. Плохие в том, что прямо сейчас мы обшариваем папку node_modules в поисках местоположения каждого пакета и вручную включаем их в HTML. Это достаточно неудобно, и следующее, что мы сделаем, это посмотрим как автоматизировать и этот процесс.

Использование модульного упаковщика JavaScript (webpack)

У большинства языков программирования есть способ импорта кода из одного файла в другой. JavaScript изначально не имел этой функции, потому что был создан только для работы в браузере, без доступа к файловой системе компьютера клиента (по соображениям безопасности). Поэтому очень долго организация кода JavaScript в нескольких файлах требовала загрузки каждого файла с глобальными переменными.

Это именно то, что мы делаем в приведенном выше примере с moment.js — весь moment.min.js файл загружается в HTML, что определяет глобальную переменную moment, которая становится доступна любому файлу, загруженному после moment (независимо от того, нужен ли к нему доступ).

В 2009 году был запущен проект CommonJS, определяющий экосистему для JavaScript за пределами браузера. Большая часть CommonJS была спецификацией для модулей и она, наконец, позволила JavaScript делать импорт и экспорт между файлами, как в большинстве языков программирования, без использования глобальных переменных. Самая известная реализация модулей CommonJS — это node.js.

node js

Как говорилось ранее, node.js — это среда выполнения JavaScript, предназначенная для запуска на сервере. Вот как выглядел более ранний пример с использованием модулей node.js. Вместо того, чтобы загружать все moment.min.js с помощью HTML script тега, вы можете загрузить его напрямую в файле JavaScript вот так:

// index.js
var moment = require('moment');
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());

Опять же, так работает загрузка модуля в node.js, и это здорово, поскольку node.js — это серверный язык с доступом к файловой системе компьютера. Node.js также знает расположение каждого пути модуля npm, поэтому вместо необходимости писать require('./node_modules/moment/min/moment.min.js), можно использовать require('moment') — чудно.

Всё это подходит для node.js, но если вы попытаетесь использовать написанный выше код в браузере, вы получите сообщение об ошибке, в котором будет указано require not defined (не определено). Браузер не имеет доступа к файловой системе, это значит, что загрузка модулей таким способом очень проблематична — загрузка файлов должна выполняться динамически, синхронно (что замедляет исполнение), или асинхронно (что может вызвать проблемы с синхронизацией).

Тут вступает в действие упаковщик модулей. Упаковщик модулей JavaScript — это инструмент, который решает задачу с помощью этапа сборки (он имеет доступ к файловой системе) для создания конечно результата, совместимого с браузером (уже не нужен доступ к файловой системе). В этом случае нам нужен упаковщик модулей для поиска всех утверждений require (а это недопустимый синтаксис JavaScript для браузера) и замены их актуальным содержимым каждого требуемого файла. Конечный результат — один бандл (упакованный файл) JavaScript (без инструкций require)!

Самым популярным упаковщиком модулей был Browserify, он вышел в 2011 году и первым использовал стиль node.js для require во фронтенде (что, по сути, помогло npm стать самым используемым менеджером пакетов для фронтенда). Примерно в 2015 году более популярным стал упаковщик модулей webpack (его подпитывала популярность фроненд-фреймворка React, который максимально пользовался различными функциями webpack).

Давайте посмотрим, как использовать webpack, чтобы заставить показанный выше пример с require('moment') работать в браузере. Сначала нам нужно установить webpack в проект. Сам webpack — это npm-пакет, поэтому мы можем установить его из командной строки:

$ npm install webpack --save-dev

Обратите внимание на аргумент --save-dev, он сохраняется как development-зависимость. Это означает, что это пакет вам нужен в среде разработки, но не на production-сервере. Это можно наглядно увидеть в файле package.json, который был автоматически обновлен:

{
  "name": "modern-javascript-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "moment": "^2.19.1"
  },
  "devDependencies": {
    "webpack": "^3.7.1"
  }
}

Теперь у нас есть webpack, установленный в папку node_modules, как один из пакетов. Вы можете вызывать webpack из командной строки:

$ ./node_modules/.bin/webpack index.js bundle.js

Эта команда запустит инструмент webpack, который был установлен в папке node_modules, начнет с файла index.js, найдет любые require утверждения и заменит их соответствующим кодом, чтобы создать один выходной файл с именем bundle.js.

Это значит, что мы больше не будем использовать index.js в браузере, так как в нем содержатся недопустимые require утверждения. Вместо этого мы будем использовать вывод bundle.js в браузере, что должно будет отразиться в файле index.html:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>JavaScript Example</title>
  <script src="bundle.js"></script>
</head>
<body>
  <h1>Hello from HTML!</h1>
</body>
</html>

Если вы обновите браузер, увидите, что всё работает, как работало раньше!

Обратите внимание, что нам нужно будет запускать команду webpack каждый раз, когда мы будем менять index.js. Это утомительно и станет ещё утомительней, когда мы станем использовать более сложные фичи webpack (например, генерацию source maps, чтобы помочь отлаживать исходный код из преобразованного кода). Webpack может считывать параметры из файла конфигурации в корневом каталоге проекта с именем webpack.config.js, который в нашем случае будет выглядеть так:

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.js'
  }
};

Теперь каждый раз, когда мы меняем index.js, мы можем запускать webpack с помощью команды:

$ ./node_modules/.bin/webpack

Нам больше не нужно указывать параметры index.js и bundle.js, так как webpack загружает эти параметры из файла webpack.config.js. Уже лучше, но все же утомительно вводить эту команду при каждом изменении кода — позже мы сделаем этот процесс более плавным.

В целом, может казаться, что тут нет ничего особенного, но в этой рабочей последовательности есть несколько огромных плюсов. Мы больше не загружаем внешние скрипты через глобальные переменные. Любые новые библиотеки JavaScript будут добавляться через require в JavaScript, в отличие от добавления новых тегов <script> в HTML. Наличие одного бандла часто лучше для производительности. И теперь, когда мы добавили шаг сборки, мы можем добавить несколько мощных фич в процесс разработки!

Transpiling для новых фич языка (babel)

"Transpiling" кода — преобразование исходного кода на одном языке в исходный код на другом схожем языке. Это важный сегмент frontend-разработки: поскольку браузеры медленно добавляют новые фичи, были созданы новые языки с экспериментальными функциями, которые преобразуются (transpile) в совместимый с браузерами код.

Для CSS есть Sass, Less и Stylus. Для JavaScript самым популярным транспайлером какое-то время был CoffeeScript (вышел где-то в 2010 году), а в настоящее время большинство людей используют babel или TypeScript. CoffeeScript — это язык, ориентированный на улучшение JavaScript, через значительное его изменение — опциональные круглые скобки, отступы и т. д.

Babel — это не новый язык, а транспайлер, который преобразует JavaScript нового поколения с фичами, недоступными всем браузерам (ES2015 и выше) в старшую, более совместимую версию JavaScript (ES5). TypeScript — это язык, который практически идентичен JavaScript нового поколения, но в него добавлена опциональная статическая типизация. Многие люди предпочитают использовать babel, потому что он ближе всего к чистому JavaScript.

Давайте рассмотрим пример использования babel с нашим существующим этапом сборки webpack. Сначала мы устанавливаем babel (а это npm-пакет) в проект из командной строки:

$ npm install babel-core babel-preset-env babel-loader --save-dev

Обратите внимание, что мы устанавливаем 3 отдельных пакета как dev зависимости — babel-core — основная часть babel, babel-preset-env — пресет, определяющий, какие новые фичи JavaScript преобразовывать, а babel-loader — это пакет, позволяющий babel работать с webpack. Мы можем настроить webpack для использования babel-loader, отредактировав файл webpack.config.js, как показано ниже:

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['env']
          }
        }
      }
    ]
  }
};

Этот синтаксис может ломать мозг (к счастью, это не то, что мы будем часто редактировать). Мы просим webpack искать любые .js-файлы (исключая те, что в папке node_modules) и применять преобразование babel с помощью babel-loader через пресет babel-preset-env. Здесь вы можете узнать больше о конфигурационном синтаксисе webpack.

У нас есть пример полностью настроенного пакета со всеми описанными штуками — Hexlet Boilerplates: nodejs, — прим. ред.

Теперь, когда все настроено, мы можем начать писать фичи ES2015 в JavaScript! Ниже пример ES2015 template string в файле index.js:

// index.js
var moment = require('moment');

console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());

var name = "Bob", time = "today";
console.log(`Hello ${name}, how are you ${time}?`);

Мы также можем использовать импорты ES2015 вместо require для загрузки модулей. Это то, что вы сегодня увидите во многих базах кода:

// index.js
import moment from 'moment';

console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());

var name = "Bob", time = "today";
console.log(`Hello ${name}, how are you ${time}?`);

В этом примере синтаксис import не сильно отличается от синтаксиса require, но import имеет дополнительную гибкость для более сложных случаев. Поскольку мы изменили index.js, нам нужно снова запустить webpack в командной строке:

$ ./node_modules/.bin/webpack

Теперь вы можете обновить index.html в браузере. На момент написания этой статьи большинство современных браузеров поддерживают все фичи ES2015, поэтому иногда трудно определить, выполнил ли свою работу babel. Вы можете протестировать его в браузере более старой версии, вроде IE9, или покопаться в bundle.js и найти строку транспайлированного кода:

// bundle.js
// ...
console.log('Hello ' + name + ', how are you ' + time + '?');
// ...

Здесь видно, как babel преобразовал интерполированную строку ES2015 в обычную конкатенацию чтобы сохранить совместимость с браузером. Хотя этот пример не слишком захватывающий, способность преобразовать код — очень мощный инструмент. В JavaScript есть некоторые интересные языковые фичи, вроде async/await, которые вы можете начать использовать прямо сейчас, чтобы писать код лучше. И хотя transpilation иногда выглядит утомительным и болезненным занятием, оно привело к резким улучшениям в языке за последние несколько лет, так как сегодня люди тестируют завтрашние фичи.

Мы почти закончили, но в нашем воркфлоу все еще есть неотполированные грани. Если нас беспокоит производительность, мы должны минимизировать бандл-файл, что должно быть уже достаточно лёгкой задачей, поскольку этап сборки уже встроен. Нам также нужно повторно запускать команду webpack каждый раз, когда мы меняем JavaScript. Поэтому следующее, что мы рассмотрим — это несколько удобных инструментов для решения этих проблем.

Использование task runner (скрипты npm)

Теперь, когда мы вложились в использование этапа сборки для работы с модулями JavaScript, имеет смысл использовать task runner, инструмент, который автоматизирует различные части процесса сборки. Для фронтенд разработки в задачи включена минимизация кода, оптимизация изображений, прогон тестов и т.д.

В 2013 году Grunt был самым популярным таск-раннером во фронтенде, чуть позже появился Gulp. Оба полагаются на плагины, которые охватывают другие инструменты командной строки. Сегодня более популярный выбор — использовать возможности скриптов, встроенных в сам менеджер пакетов npm, который не использует плагины, а напрямую работает с другими инструментами командной строки.

Давайте напишем несколько npm-скриптов, чтобы упростить использование webpack. Это простое изменение файла package.json как в примере ниже:

{
  "name": "modern-javascript-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --progress -p",
    "watch": "webpack --progress --watch"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "moment": "^2.19.1"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.1",
    "webpack": "^3.7.1"
  }
}

Здесь мы добавили два новых скрипта, build и watch. Чтобы запустить скрипт сборки, вы можете ввести в командной строке:

$ npm run build

Это запустит webpack (используя конфигурацию из webpack.config.js, которую мы сделали ранее) с опцией --progress, чтобы показать прогресс в процентах и опцию -p, чтобы минимизировать код для production. Чтобы запустить сценарий watch:

$ npm run watch

Используется опция -watch для автоматического перезапуска webpack каждый раз, когда изменяется любой JavaScript-файл, что отлично подходит для разработки.

Обратите внимание, что скрипты в package.json могут запускать webpack без указания полного пути ./node_modules/.bin/webpack, так как node.js знает расположение каждого пути модуля npm. Это круто! Мы можем всё сделать ещё круче, установив webpack-dev-server, отдельный инструмент, который предоставляет простой веб-сервер с живой перезагрузкой. Чтобы установить его как зависимость, введите команду:

$ npm install webpack-dev-server --save-dev

Затем добавьте скрипт npm в package.json:

{
  "name": "modern-javascript-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --progress -p",
    "watch": "webpack --progress --watch",
    "server": "webpack-dev-server --open"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "moment": "^2.19.1"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.1",
    "webpack": "^3.7.1"
  }
}

Теперь вы можете запустить свой dev-сервер выполнив команду:

$ npm run server

Это автоматически откроет сайт index.html в вашем браузере с адресом localhost: 8080 (по умолчанию). Каждый раз, когда вы будете менять JavaScript в index.js, webpack-dev-server будет перестраивать собственный упакованный JavaScript и автоматически обновлять браузер. Это удивительно полезная экономия времени, поскольку у вас есть возможность сосредоточиться на коде, а не на постоянном переключении контекстов между кодом и браузером, чтобы увидеть изменения.

И это только верхушка, есть еще много вариантов с webpack и webpack-dev-server (о которых вы можете прочитать здесь). Конечно, вы можете создавать npm-скрипты для выполнения других задач, таких как преобразование Sass в CSS, сжатие изображений, прогон тестов всего, к чему имеет доступ инструмент командной строки — всё имеет смысл. Также есть несколько отличных расширенных опций и манёвров с самими скриптами npm. Это выступление Кейт Хадсон — отличное начало.

Вывод

Вот, что представляет из себя современный JavaScript в двух словах. Мы перешли с простого HTML и JS к менеджеру пакетов, чтобы автоматически загружать сторонние пакеты, упаковщику модулей для создания единого скрипт-файла, транспайлеру для использования будущих фич JavaScript и таск раннеру для автоматизации различных частей процесса сборки. Определенно тут много движимых частей, особенно для новичков. Веб-разработка когда-то была отличным стартом для новичков в программировании, именно потому что было легко начать работать; сейчас это довольно сложно, особенно потому, что различные инструменты склонны к быстрым изменениям.

Тем не менее, всё не так плохо, как кажется. Всё уравновешивается, особенно после внедрениея экосистемы node, как стабильного способа работы с фронтендом. Довольно приятно использовать npm в качестве менеджера пакетов, require или import для модулей, и npm-скрипты для запуска задач. Это значительно упрощает рабочий процесс по сравнению с тем, что было год или два назад!

Дополнительная польза и для начинающих, и для опытных разработчиков в том, что фреймворки сегодня содержат инструменты, которые упрощают процесс и начать теперь намного легче. У Ember есть ember-cli, что очень сильно повлияло на angular-cli от Angular, create-react-app от React, vue-cli от Vue и т. д. Все эти инструменты создают проект с полным набором того, что вам нужно — всё, что вам нужно сделать, это начать писать код. Тем не менее, эти инструменты не волшебные, они просто задают правильную базовую структуру, но вы можете оказаться в ситуации, когда вам потребуется выполнить дополнительную настройку с помощью webpack, babel и т.д. Поэтому очень важно понять, что делает каждый из этих инструментов, что мы и сделали в этой статье.

Работа с современным JavaScript — не всегда самая приятная штука, потому что он постоянно меняется и развивается со скоростью звука. Но, хоть иногда некоторые моменты и кажутся изобретением колеса, быстрая эволюция JavaScript продвинула инновации вроде горячей перезагрузки, инструмента статического анализа и отладка в контексте времени. Сегодня быть разработчиком — захватывающе, и я надеюсь, что эта информация может послужить путеводителем, который поможет вам в вашем путешествии!

Поделиться Вконтакте
Отправить в Телеграм
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP и Java. Хекслет

Подробнее о том, почему наше обучение работает →