[Перевод] Объясняем современный JavaScript динозавру

mqh0q3nuxx_sfiqkr66svi1jaim.png


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


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


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


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


Олдскульное использование JavaScript


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






  
  JavaScript Example
  


  

Hello from HTML!


Строка ссылается на JavaScript-файл index.js, находящийся в той же директории:


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


Больше для создания сайта ничего не нужно! Допустим, вы хотите добавить стороннюю библиотеку moment.js (меняет формат дат для удобочитаемости). Можно, к примеру, использовать в JS функцию moment:


moment().startOf('day').fromNow();        // 20 часов назад


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


m0zqrtccnysnyq4cyaybr3wwd-4.png


Мда, в разделе «Установка» много всего написано. Но пока это проигнорируем, потому что можно добавить moment.js на сайт, скачав файл moment.min.js в ту же директорию и включив его в наш файл index.html.






  
  Example
  
  
  


  

Hello from HTML!


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


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


Вот так мы создавали сайты с JS-библиотеками. Процедура была простой для понимания, но раздражала необходимость искать и скачивать новые версии библиотек при каждом их обновлении.


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


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


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


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


Давайте посмотрим, как использовать 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"
}


Для установки JS-пакета moment.js можно воспользоваться инструкциями с сайта npm, введя в командной строке:


$ npm install moment --save


Эта команда делает две вещи:


  1. Скачивает весь код из пакета moment.js в папку под названием node_modules.
  2. Автоматически модифицирует файл 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, то в директории node_modules/moment/min увидим файл moment.min.js. Это означает, что в index.html можно сослаться на скачанную через npm версию moment.min.js:






  
  JavaScript Example
  
  


  

Hello from HTML!


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


pl_wxlophhftzmixmt7d6dupk2a.png


Использование бандлера (bundler) JavaScript-модулей (webpack)


В большинстве языков программирования есть возможность импортирования кода из одного файла в другой. Изначально в JS такой возможности не было, потому что этот язык разрабатывался только для исполнения в браузере, без доступа к файловой системе на клиентской машине (по причинам безопасности). Так что долгое время для организации JS-кода в нескольких файлах требовалось загружать каждый файл с глобально доступными переменными.


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


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


day3vrfuiu_b6hvnnydcqufteba.png


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


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


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


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


И здесь появляется бандлер (bundler). Это инструмент для сборки модулей в единые пакеты, имеющий доступ к файловой системе. Получающиеся пакеты совместимы с браузером, которому не нужен доступ к файловой системе. В нашем случае бандлер нужен для поиска всех выражений require (имеющих ошибочный, с точки зрения браузера, JS-синтаксис) и замены на настоящее содержимое каждого требуемого файла. В финале мы получаем единый JS-файл без выражений require!


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


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


$ npm install webpack --save-dev


Обратите внимание на аргумент --save-dev — он сохраняет бандлер как зависимость среды разработки, так что она не понадобится на 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. Можно запускать его из командной строки:


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


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






  
  JavaScript Example
  


  

Hello from HTML!


Если обновите браузер, то всё будет работать, как и прежде.


Обратите внимание, что нам нужно выполнять webpack-команду при каждом изменении index.js. Это утомительно, и чем более продвинутые возможности webpack мы будем использовать (вроде генерирования схемы источников для отладки исходного кода из транспилированного), тем больше станет утомлять постоянный ввод команд. 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. Так лучше, но всё равно утомительно вводить эту команду при каждом изменении кода. Надо ещё больше всё упростить.


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