Как создать модуль с функциями
Перейти к содержимому

Как создать модуль с функциями

  • автор:

Создание модуля Node.js

Создание модуля Node.js

В Node.js модуль — это набор функций и объектов JavaScript, который могут использовать внешние приложения. Описание части кода как модуля относится не столько к самому коду, сколько к тому, что он делает. Любой файл или набор файлов Node.js можно считать модулем, если его функции и данные готовы для использования внешними программами.

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

В этом обучающем руководстве мы создадим модуль Node.js, предлагающий веб-разработчикам цвета для использования в дизайне. Мы сохраним цвета в массиве и предоставим функцию случайного подбора цвета. После этого мы рассмотрим различные способы импорта модулей в приложение Node.js.

Предварительные требования

  • Необходимо, чтобы в вашей среде разработки были установлены Node.js и npm. В этом обучающем руководстве используется версия 10.17.0. Чтобы установить его в macOS или Ubuntu 18.04, следуйте указаниям руководства Установка Node.js и создание локальной среды разработки в macOS или раздела Установка с помощью PPA руководства Установка Node.js в Ubuntu 18.04. При установке Node.js также выполняется установка npm, в этом обучающем руководстве используется версия 6.11.3.
  • Вы должны быть знакомы с файлом package.json , опыт с командами npm также будет полезен. Чтобы приобрести этот опыт, выполните обучающее руководство Использование модулей Node.js с npm и package.json, в особенности раздел Шаг 1 — Создание файла package.json .
  • Также будет полезно познакомиться со структурой Node.js REPL (чтение-оценка-печать-цикл). Мы используем это для тестирования нашего модуля. Если вам требуется дополнительная информация, обратитесь к руководству Использование REPL в Node.js.

Шаг 1 — Создание модуля

На этом шаге вы создадите свой первый модуль Node.js. Ваш модуль будет содержать массив с набором цветов и предоставлять функцию случайного выбора цвета. Мы используем встроенное свойство Node.js exports , чтобы сделать функцию и массив доступными для внешних программ.

Для начала мы определим, какие данные о цветах будут храниться в вашем модуле. Каждый цвет будет представлять собой объект со свойством name , которое люди смогут легко распознать, и со свойством code , представляющим собой строку с цветовым кодом HTML. Цветовые коды HTML представляют собой шестизначные числа в шестнадцатеричном формате, позволяющие изменять цвет элементов веб-страницы. Дополнительную информацию о цветовых кодах HTML можно узнать в статье Цветовые коды и наименования в HTML.

Затем вы сможете решить, какие цвета должен поддерживать ваш модуль. Наш модуль будет содержать массив allColors , содержащий шесть цветов. Также ваш модуль будет содержать функцию getRandomColor() , которая будет случайным образом выбирать из массива цвет и возвращать его.

Откройте терминал, создайте новую папку colors и перейдите в нее:

Инициализируйте npm, чтобы другие программы могли импортировать этот модуль на последующих шагах этого обучающего руководства:

Мы используем флаг -y , чтобы пропустить обычные диалоги настройки файла package.json . Если бы вы публиковали этот модуль в npm, вы бы ввели в этих диалогах соответствующие данные, как объясняется в статье Использование модулей Node.js с npm и package.json.

В данном случае вывод будет выглядеть так:

Откройте nano или другой текстовый редактор командной строки и создайте новый файл, который будет выступать в качестве точки входа в ваш модуль:

Ваш модуль будет выполнять несколько задач. Вначале вы определите класс Color . Экземпляр класса Color будет создаваться с именем и кодом HTML. Добавьте следующие строки для создания класса:

Теперь у вас имеется структура данных для Color , и вы можете добавить в модуль несколько экземпляров. Запишите в файл следующий выделенный массив:

Введите функцию, которая будет случайным образом выбирать элемент из только что созданного вами массива allColors :

Ключевое слово exports ссылается на глобальный объект, доступный в каждом модуле Node.js. Все функции и объекты, хранящиеся в объекте exports модуля, становятся открытыми, когда другие модули Node.js импортируют этот объект. Например, функция getRandomColor() была создана напрямую на объекте exports . Затем мы добавили свойство allColors в объекте exports . Это свойство ссылается на локальный постоянный массив allColors , созданный на предыдущих шагах сценария.

При импорте этого модуля другими модулями allColors и getRandomColor() открываются и становятся доступными для использования.

Сохраните и закройте файл.

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

Шаг 2 — Тестирование модуля с REPL

Прежде чем создать полное приложение, нужно убедиться, что наш модуль работает. На этом шаге мы используем REPL для загрузки модуля colors . В REPL вы вызовете функцию getRandomColor() и убедитесь, что она ведет себя ожидаемым образом.

Запустите Node.js REPL в той же папке, что и файл index.js :

После запуска REPL вы увидите командную строку > . Это означает, что вы можете ввести код JavaScript, который немедленно пройдет оценку. Если вы хотите узнать больше об этом, следуйте нашим указаниям по использованию REPL.

Вначале введите следующее:

В этой команде require() загружает модуль colors в точке входа. При нажатии ENTER вы получите следующее:

REPL показывает нам значение colors , где содержатся все функции и объекты, импортированные из файла index.js . При использовании ключевого слова require Node.js возвращает все содержимое объекта exports нашего модуля.

Если вы помните, мы добавили getRandomColor() и allColors в exports в модуле colors . Поэтому вы увидите их в REPL после импорта.

Протестируйте функцию getRandomColor() в командной строке:

Будет выведен случайный цвет:

Поскольку индекс случайный, вывод может отличаться. Мы убедились в работе модуля colors и теперь можем выйти из Node.js REPL:

Эта команда вернет вас в командную строку терминала.

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

Шаг 3 — Сохранение локального модуля как зависимости

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

Установите новый модуль Node.js вне папки colors . Вначале вернитесь в предыдущую директорию и содайте новую папку:

Теперь переходите к новому проекту:

Как и в случае с модулем colors , инициализируйте папку с помощью npm:

Будет сгенерирован следующий файл package.json :

Установите модуль colors и используйте флаг —save , чтобы он был записан в ваш файл package.json :

Вы только что установили модуль colors в новый проект. Откройте файл package.json , чтобы посмотреть новую локальную зависимость:

Вы увидите, что добавлены следующие выделенные строки:

Модуль colors был скопирован в вашу директорию node_modules . Используйте следующую команду, чтобы проверить его расположение:

Результат будет выглядеть следующим образом:

Используйте в новой программе установленный локальный модуль. Заново откройте текстовый редактор и создайте еще один файл JavaScript:

Вначале ваша программа импортирует модуль colors . Затем она выберет случайный цвет с помощью функции getRandomColor() ​​​, предоставленной модулем. В заключение она выведет на консоль сообщение, которое скажет пользователю, какой цвет использовать.

Введите в index.js следующий код:

Сохраните и закройте файл.

Теперь ваше приложение готово рекомендовать пользователю случайные цвета для компонентов сайта.

Запустите этот скрипт с помощью следующей команды:

Вывод будет выглядеть следующим образом:

Вы успешно установили модуль colors и теперь можете управлять им, как и любым другим пакетом npm, используемым в вашем проекте. Если вы добавите дополнительные цвета и функции в локальный модуль colors , вам нужно будет запустить в приложениях команду npm update для использования новых возможностей. На следующем шаге мы используем локальный модуль colors по-другому и получим автоматические обновления при изменении кода модуля.

Шаг 4 — Привязка локального модуля

Если локальный модуль находится в разработке, постоянное обновление пакетов может оказаться непростой задачей. В качестве альтернативы можно выполнить привязку модулей. При привязке модуля все обновления этого модуля немедленно отражаются в использующих его приложениях.

На этом шаге мы привяжем модуль colors к нашему приложению. Также мы изменим модуль colors и подтвердим, что его последние изменения будут работать в приложении без переустановки или обновления.

Вначале следует удалить локальный модуль:

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

  1. Создание глобальной ссылки на модуль. npm создает связь symlink между глобальной директорией node_modules и директорией вашего модуля. В глобальной директории node_modules устанавливаются все системные пакеты npm (любые пакеты, устанавливаемые с флагом -g ).
  2. Создание локальной ссылки. npm создает связь symlink между локальным проектом, который использует модуль, и глобальной ссылкой модуля.

Вначале нужно создать глобальную ссылку, вернувшись в папку colors и используя команду link :

После этого в оболочке появится следующее:

Вы только что создали связь symlink между папкой node_modules и директорией colors .

Теперь вернитесь в папку really-large-application и выполните привязку пакета:

Вы получите примерно следующий результат:

Примечание. Если вам нравится сокращать, вы можете использовать синтаксис ln вместо link . Например, команда npm ln colors будет работать точно так же.

Как показывает вывод, вы только что создали связь symlink между локальной директорией node_modules приложения really-large-application и связью symlink директории colors в глобальной node_modules , которая указывает на фактическую директорию в модуле colors .

Процесс привязки завершен. Запустите файл, чтобы проверить его работу:

Вывод будет выглядеть следующим образом:

Функционал вашей программы не пострадал. Протестируйте обновления и убедитесь, что они применяются немедленно. Заново откройте в текстовом редакторе файл index.js в модуле colors :

Теперь добавьте функцию, выбирающую лучший оттенок синего. Она не принимает аргументов и всегда возвращает третью позицию массива allColors . Добавьте эти строки в конец файла:

Сохраните и закройте файл, затем заново откройте файл index.js в папке really-large-application :

Создайте вызов созданной функции getBlue() и распечатайте предложение со свойствами цвета. Добавьте эти выражения в конец файла:

Сохраните и закройте файл.

Теперь в коде используется созданная функция getBlue() . Запустите файл как и раньше:

Вы увидите примерно следующее:

Ваше приложение смогло использовать новую функцию модуля colors без запуска npm update . Это упрощает внесение изменений в приложение в процессе разработки.

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

Заключение

В этом обучающем руководстве мы узнали, что модуль Node.js представляет собой файл JavaScript с функциями и объектами, которые могут использоваться другими программами. Также мы создали модуль и прикрепили свои функции и объекты к глобальному объекту exports , чтобы сделать их доступными для внешних программ. В заключение мы импортировали модуль в программу и показали, как можно сочетать модули в больших приложениях.

Теперь вы знаете, как создавать модули. Подумайте о том, какую программу вы хотите написать, и разбейте ее на компоненты, сохраняя уникальные наборы действий и данных в собственных модулях. Чем больше вы будете тренироваться в написании модулей, тем быстрее вы научитесь писать качественные программы Node.js. Еще один пример приложения Node.js, использующего модули, можно найти в обучающем руководстве Настройка приложения Node.js для работы в производственной среде в Ubuntu 18.04.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Импорт и создание модулей

Питон включает в себя обширную библиотеку модулей, в которых реализовано множество полезных функций. Далее в этом курсе мы будем применять и другие полезные модули: re для регулярных выражений, collections , содержащий множество удобных структур данных, os и shutil для управления файлами и папками.

Для использования модуля его нужно импортировать — попросить питон загрузить его и сделать его функции доступными для использования. Импорт осуществляется с помощью оператора import . Например, модуль random используется для генерации “случайных” чисел.

Ещё один пример: модуль math , содержащий различные математические функции и константы

Использование псевдонимов

Если название модуля слишком длинное и вы не хотите его писать каждый раз, когда используете какую-то функцию, вы можете импортировать этот модуль под псевдонимом с помощью as:

Импорт нескольких модулей

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

Инструкция from

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

Также можно импортировать из модуля всё. То есть все функции, переменные и классы. Тогда нам не нужно каждый раз писать название модуля.

Правило хорошего тона — импортировать модули вначале вашей программы, до любого другого кода и функций.

Создание своего модуля питон

Любой файл с исходным кодом на Python — модуль! Это значит, что любая программа может выступать в роли модуля для другой и импортироваться.

Давайте напишем скрипт с парой функций и импортируем эти функции в другую программу.

Создадим программу mymodule.py:

В ней мы прописали две математические функции: среднее и факториал. Предположим теперь мы хотим воспользоваться ими в какой-нибудь другой программе myscript.py. Тогда мы положим эти два файла в одну директорию, чтобы импорт работал. И в результате мы сможем ипмортировать эти функции в новую программу.

Файл myscript.py:

Кстати, найдите баг в этом коде:

Задача: составление анаграмм

В качестве примера использования функций и модуля стандартной библиотеки random рассмотрим задачу составления анаграмм. В качестве входного файла будем использовать любой текст, из которого мы выберем слова. Пусть текст находится в файле text.txt и имеет следующее содержание (из Яндекс.Рефератов):

Задача состоит в том, что необходимо составить файл формата TSV, состоящий из 4 колонок: слово из файла и три его случайных анаграммы. Для простоты анаграммы могут совпадать с самим словом или друг с другом. В итоге требуется получить файл table.tsv , который будет начинаться следующим образом:

Полный код для решения этой задачи может выглядеть следующим образом:

Домашнее задание

Пусть какая-то функция получает на вход список из 30 случайных целых чисел от 0 до 100, сгенерированных с помощью модуля random. В вариантах описана функция.

+1 балл для всех: ответьте коротко на вопрос “Почему модуль random на самом деле НЕ генерирует случайные числа?”

Функция берёт два случайных числа из этого списка (с помощью модуля random) и считает по ним количество всевозможных сочетаний этих чисел с точки зрения теории вероятности, С из n по k (использовать функцию из модуля math – factorial). Количество сочетаний (в формате float) печатается. k должно быть меньше n

Функция возвращает произведение значений списка. Нужно пользоваться модулем math. Руководствоваться надо тем, что exp(log(a) + log(b)) = a * b

Функция возвращает строку из 30 букв. Список, полученный на вход, задает порядок букв в строке по номеру буквы в алфавите.

Функция берёт из списка 4 случайных числа, условно принимает их за две точки в двумерном пространстве и возвращает евклидово расстояние между этими точками. Использовать модули random и math.

Функция перемешивает список с помощью random.shuffle(), сравнивает его с исходным списком и возвращает количество индексов, значение элемента по которым поменялось. Запустите функцию в цикле 100 раз и напечатайте в скольки процентов случаев меняются все элементы списка.

Функция возвращает среднее геометрическое списка. Вомпользуйтесь модулем math. Отдельно вне функции покажите, что ее результат лежит между минимальным и максимальным значениями списка для 20 случаев. (Для это нужно на каждой итерации генерировать подаваемый на вход список заново)

Функция возвращает среднее арифметическое элементов списка, округлённое вверх. Используйте модуль math.

Использование модулей и заголовочных файлов

Задача 0.3
Создать модуль (подключаемый файл) с функциями из задачи 0.2. Использовать этот модуль в простой программе, которая генерирует одно слово (набор символов).

В задаче 0.2 я показал вам, как можно программно создавать наборы символов и составлять из них предложения. Как я уже говорил, такие наборы символов могут пригодиться при решении задач по программированию.

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

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

Больше рассказывать особо нечего — просто посмотрите примеры исходных кодов ниже.

ПРИМЕЧАНИЕ
Создать модуль можно из среды разработки. Или просто создать обычный текстовый файл, присвоить ему имя с расширением PAS (в Паскале) или CPP/H (в С++). Более подробно об этом см. по указанным выше ссылкам.

Примеры программ на Паскале и С++.

В современном Паскале также вместо модулей можно создавать подключаемые файлы с помощью директив компилятора.

ВНИМАНИЕ!
Если вам что-то осталось непонятно, то советую почитать книги “Основы программирования” и “Основы С++”.

Выразительный JavaScript: Модули

Начинающий программист пишет программы так, как муравьи строят муравейник – по кусочку, без размышления над общей структурой. Его программы как песок. Они могут недолго простоять, но вырастая, они разваливаются.

Поняв проблему, программист тратит много времени на размышления о структуре. Его программы получаются жёстко структурированными, как каменные изваяния. Они тверды, но когда их нужно менять, над ними приходится совершать насилие.

Мастер-программист знает, когда нужна структура, а когда нужно оставить вещи в простом виде. Его программы словно глина – твёрдые, но податливые.

Мастер Юан-Ма, Книга программирования

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

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

Зачем нужны модули

Есть несколько причин, по которым авторы делят свои книги на главы и секции. Это помогает читателю понять, как построена книга, и найти нужные им части. Автору это помогает концентрироваться на каждой конкретной части. Преимущества организации программ в нескольких файлах, или модулях, примерно те же. Структуризация помогает незнакомым с кодом людям найти то, что им нужно, и помогает программистам хранить связанные друг с другом вещи в одном месте.

Некоторые программы организованы по модели обычного текста, где порядок следования чётко определён, и где читателю предлагается последовательное изучение программы и множество прозы (комментариев) для описания кода. Это делает чтение кода менее пугающим (а чтение чужого кода обычно пугает), но требует больших усилий при создании кода. Также такую программу сложнее менять, потому что части прозы связаны между собой сильнее, чем части кода. Этот стиль называется литературным программированием. Те главы книги, в которых обсуждаются проекты, можно считать литературным кодом.

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

Пространство имён

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

Загрязнение пространства имён (ПИ), когда не связанные друг с другом части кода делят один набор переменных, упоминалась в главе 4. Там объект Math был приведён в качестве примера объекта, который группирует функциональность, связанную с математикой, в виде модуля.

Хотя JavaScript не предлагает непосредственно конструкции для создания модуля, мы можем использовать объекты для создания подпространств имён, доступных отовсюду. А функции можно использовать для создания изолированных частных пространств имён внутри модуля. Чуть дальше мы обсудим способ построения достаточно удобных модулей, изолирующих ПИ при помощи базовых концепций языка.

Повторное использование

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

Когда у вас будет множество таких сдублированных кусков кода, вы обнаружите, что тратите кучу времени и сил на их копирование и обновление. Если разместить связанные между собой части программ в отдельные модули, их будет проще отслеживать, исправлять и обновлять, потому что везде, где этот функционал потребуется, вы сможете просто загрузить этот модуль из файла.

Эту идею можно использовать ещё лучше, если чётко прописать взаимоотношения разных модулей – кто от кого зависит. Тогда можно автоматизировать процесс установки и обновления внешних модулей (библиотек).

Если ещё развить идею – представьте себе онлайн-сервис, который отслеживает и распространяет сотни тысяч таких библиотек, и вы можете искать нужную вам функциональность среди них, а когда найдёте – ваш проект автоматически скачает её.

И такой сервис есть! Он называется NPM (npmjs.org). NPM – онлайн-база модулей и инструмент для скачивания и апгрейда модулей, от которых зависит ваша программа. он вырос из Node.js, окружения JavaScript, не требующего браузера, которое мы обсудим в главе 20, но также может использоваться и в браузерных программах.

Устранение связей (Decoupling)

Ещё одна задача модулей – изолировать несвязанные между собой части кода так, как это делают интерфейсы объектов. Хорошо продуманный модуль предоставляет интерфейс для его использования вовне. Когда модуль обновляют или исправляют, существующий интерфейс остаётся неизменным, чтобы другие модули могли использовать новую, обновлённую версию без изменений в них самих.

Стабильный интерфейс не означает, что в него не добавляют новые функции, методы или переменные. Главное, что существующая функциональность не удаляется и её смысл не меняется. Хороший интерфейс позволяет модулю расти, не ломая старый интерфейс. А это значит – выставлять наружу как можно меньше внутренней кухни модуля, при этом язык интерфейса должен быть достаточно гибким и мощным для применения в различных ситуациях.

Интерфейсы, выполняющие простую задачу, вроде чтения настроек из файла, выходят такими естественным образом. Для других – к примеру, для редактора текстов, у которого есть множество разных аспектов, требующих доступа извне (содержимое, стили, действия пользователя и т.п.) интерфейс необходимо скрупулёзно продумывать.

Использование функций в качестве пространств имён

Функции – единственная вещь в JavaScript, создающая новую область видимости. Если нам нужно, чтобы у модулей была своя область видимости, придётся основывать их на функциях.

Обратите внимание на простейший модуль, связывающий имена с номерами дней недели – как делает метод getDay объекта Date.

Функция dayName – часть интерфейса модуля, а переменная names – нет. Но хотелось бы не загрязнять глобальное пространство имён.

Можно сделать так:

Теперь names – локальная переменная безымянной функции. Функция создаётся и сразу вызывается, а её возвращаемое значение (уже нужная нам функция dayName) хранится в переменной. Мы можем написать много страниц кода в функции, объявить там сотню переменных, и все они будут внутренними для нашего модуля, а не для внешнего кода.

Подобный шаблон можно использовать для изолирования кода. Следующий модуль пишет в консоль значение, но не предоставляет никаких значений для использования другими модулями.

Этот код выводит квадрат сотни, но в реальности это мог бы быть модуль, добавляющий метод к какому-то прототипу, или настраивающий виджет на веб-странице. Он обёрнут в функцию для предотвращения загрязнения глобальной ОВ используемыми им переменными.

А зачем мы заключили функцию в круглые скобки? Это связано с глюком синтаксиса JavaScript. Если выражение начинается с ключевого слова function, это функциональное выражение. А если инструкция начинается с function, это объявление функции, которое требует названия, и, так как это не выражение, не может быть вызвано при помощи скобок () после неё. Можно представлять себе заключение в скобки как трюк, чтобы функция принудительно интерпретировалась как выражение.

Объекты в качестве интерфейсов

Представьте, что нам надо добавить ещё одну функцию в наш модуль «день недели». Мы уже не можем возвращать функцию, а должны завернуть две функции в объект.

Когда модуль большой, собирать все возвращаемые значения в объект в конце функции неудобно, потому что многие возвращаемые функции будут большими, и вам было бы удобнее их записывать где-то в другом месте, рядом со связанным с ними кодом. Удобно объявить объект (обычно называемый exports) и добавлять к нему свойства каждый раз, когда нам надо что-то экспортировать. В следующем примере функция модуля принимает объект интерфейса как аргумент, позволяя коду снаружи функции создать его и сохранить в переменной. Снаружи функции this ссылается на объект глобальной области видимости.

Отсоединяемся от глобальной области видимости

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

Подкрутив кое-что, мы можем сделать систему, разрешающую одному модулю обращаться к интерфейсному объекту другого, без выхода в глобальную ОВ. Наша цель – функция require, которая, получая имя модуля, загрузит его файл (с диска или из сети, в зависимости от платформы) и вернёт соответствующее значение с интерфейсом.

Этот подход решает проблемы, упомянутые ранее, и у него есть ещё одно преимущество – зависимости вашей программы становятся явными, и поэтому сложнее случайно вызвать ненужный вам модуль без чёткого его объявления.

Нам понадобятся две вещи. Во-первых, функция readFile, возвращающая содержимое файла в виде строки. В стандартном JavaScript такой функции нет, но разные окружения, такие как браузер или Node.js, предоставляют свои способы доступа к файлам. Пока притворимся, что у нас есть такая функция. Во-вторых, нам нужна возможность выполнить содержимое этой строки как код.

Выполняем данные как код

Есть несколько способов получить данные (строку кода) и выполнить их как часть текущей программы.

Самый очевидный – оператор eval, который выполняет строку кода в текущем окружении. Это плохая идея – он нарушает некоторые свойства окружения, которые обычно у него есть, например изоляция от внешнего мира.

Способ лучше – использовать конструктор Function. Он принимает два аргумента – строку, содержащую список имён аргументов через запятую, и строку, содержащую тело функции.

Это то, что нам надо. Мы обернём код модуля в функцию, и её область видимости станет областью видимости нашего модуля.

Require

Вот минимальная версия функции require:

Так как конструктор new Function оборачивает код модуля в функцию, нам не надо писать функцию, оборачивающую пространство имён, внутри самого модуля. А так как exports является аргументом функции модуля, модулю не нужно его объявлять. Это убирает много мусора из нашего модуля-примера.

При использовании такого шаблона модуль обычно начинается с объявления нескольких переменных, которые загружают модули, от которых он зависит.

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

Это можно решить, храня уже загруженные модули в объекте, и возвращая существующее значение, когда он грузится несколько раз.

Вторая проблема – модуль не может экспортировать переменную напрямую, только через объект export. К примеру, модулю может потребоваться экспортировать только конструктор объекта, объявленного в нём. Сейчас это невозможно, поскольку require всегда использует объект exports в качестве возвращаемого значения.

Традиционное решение – предоставить модули с другой переменной, module, которая является объектом со свойством exports. Оно изначально указывает на пустой объект, созданный require, но может быть перезаписано другим значением, чтобы экспортировать что-либо ещё.

Сейчас у нас есть система модулей, использующих одну глобальную переменную require, чтобы позволять модулям искать и использовать друг друга без выхода в глобальную область видимости.

Такой стиль системы модулей называется CommonJS, по имени псевдостандарта, который первым его описал. Он встроен в систему Node.js. Настоящие реализации делают гораздо больше описанного мною. Главное, что у них есть более умный способ перехода от имени модуля к его коду, который разрешает загружать модули по относительному пути к файлу, или же по имени модуля, указывающему на локально установленные модули.

Медленная загрузка модулей

Хотя и возможно использовать стиль CommonJS для браузера, но он не очень подходит для этого. Загрузка файла из Сети происходит медленнее, чем с жёсткого диска. Пока скрипт в браузере работает, на сайте ничего другого не происходит (по причинам, которые станут ясны к 14 главе). Значит, если бы каждый вызов require скачивал что-то с дальнего веб-сервера, страница бы зависла на очень долгое время при загрузке.

Можно обойти это, запуская программу типа Browserify с вашим кодом перед выкладыванием её в веб. Она просмотрит все вызовы require, обработает все зависимости и соберёт нужный код в один большой файл. Веб-сайт просто грузит этот файл и получает все необходимые модули.

Второй вариант – оборачивать код модуля в функцию, чтобы загрузчик модулей сначала грузил зависимости в фоне, а потом вызывал функцию, инициализирующую модуль, после загрузки зависимостей. Этим занимается система AMD (асинхронное определение модулей).

Наша простая программа с зависимости выглядела бы в AMD так:

Функция define здесь самая важная. Она принимает массив имён модулей, а затем функцию, принимающую один аргумент для каждой из зависимостей. Она загрузит зависимости (если они уже не загружены) в фоне, позволяя странице работать, пока файло качается. Когда всё загружено, define вызывает данную ему функцию, с интерфейсами этих зависимостей в качестве аргументов.

Загруженные таким образом модули должны содержать вызовы define. В качестве их интерфейса используется то, что было возвращено функцией, переданной в define. Вот модуль weekDay:

Чтобы показать минимальную реализацию define, притворимся, что у нас есть функция backgroundReadFile, которая принимает имя файла и функцию, и вызывает эту функцию с содержимым этого файла, как только он будет загружен. (В главе 17 будет объяснено, как написать такую функцию).

Чтоб отслеживать модули, пока они загружаются, define использует объекты, описывающие состояние модулей, сообщает нам, доступны ли они уже, и предоставляет их интерфейс по доступности.

Функция getModule принимает имя и возвращает такой объект, и убеждается в том, что модуль поставлен в очередь загрузки. Она использует кеширующий объект, чтобы не грузить один модуль дважды.

Мы предполагаем, что загружаемый файл тоже содержит вызов define. Переменная currentMod используется, чтобы сообщить этому вызову о загружаемом объекте модуля, чтобы тот смог обновить этот объект после загрузки. Мы ещё вернёмся к этому механизму.

Функция define сама использует getModule для загрузки или создания объектов модулей для зависимостей текущего модуля. Её задача – запланировать запуск функции moduleFunction (содержащей сам код модуля) после загрузки зависимостей. Для этого она определяет функцию whenDepsLoaded, добавляемую в массив onLoad, содержащий все пока ещё не загруженные зависимости. Эта функция сразу прекращает работу, если есть ещё незагруженные зависимости, так что она выполнит свою работу только раз, когда последняя зависимость загрузится. Она также вызывается сразу из самого define, в случае когда никакие зависимости не нужно грузить.

Когда все зависимости доступны, whenDepsLoaded вызывает функцию, содержащую модуль, передавая в виде аргументов интерфейсы зависимостей.

Первое, что делает define, это сохраняет значение currentMod, которое было у него при вызове, в переменной myMod. Вспомните, что getModule прямо перед исполнением кода модуля сохранил соответствующий объект модуля в currentMod. Это позволяет whenDepsLoaded хранить возвращаемое значение функции модуля в свойстве exports этого модуля, установить свойство loaded модуля в true, и вызвать все функции, ждавшие загрузки модуля.

Этот код изучать тяжелее, чем функцию require. Его выполнение идёт не по простому и предсказуемому пути. Вместо этого, несколько операций должны быть выполнены в неопределённые моменты в будущем, что затрудняет изучения того, как выполняется этот код.

Настоящая реализация AMD гораздо умнее подходит к превращению имён модулей в URL и более надёжна, чем показано в примере. Проект RequireJS предоставляет популярную реализацию такого стиля загрузчика модулей.

Разработка интерфейса

Разработка интерфейсов – один из самых тонких моментов в программировании. Любую нетривиальную функциональность можно реализовать множеством способов. Поиск работающего способа требует проницательности и предусмотрительности.

Лучший способ познать значимость хорошего интерфейса – использовать много интерфейсов. Некоторые будут плохие, некоторые хорошие. Опыт покажет вам, что работает, а что – нет. Никогда не принимайте как должное плохой интерфейс. Исправьте его, или заключите в другой интерфейс, который лучше вам подходит.

Предсказуемость

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

В поведении вашего кода предсказуемость также важна. Вас может постичь искушение сделать интерфейс слишком заумным якобы потому, что его удобнее использовать. К примеру, вы можете принимать любые виды типов и комбинаций аргументов и проделывать с ними «то, что надо». Или предоставлять десятки специализированных функций, которые предлагают незначительно отличающуюся функциональность. Это может сделать код, опирающийся на ваш интерфейс, немного короче, зато затруднить людям, работающим с ним, строить чёткую мысленную модель работы вашего модуля.

Компонуемость

Старайтесь использовать в интерфейсах настолько простые структуры данных, насколько это возможно. Делайте так, чтобы функции выполняли простые и понятные вещи. Если это применимо, делайте функции чистыми (см. Главу 3).

К примеру, частенько модули предлагают свою версию массивоподобных коллекций объектов со своим интерфейсом для подсчёта и извлечения элементов. У таких объектов нет методов map или forEach, и никакая функция, ожидающая настоящий массив, не сможет с ними работать. Это пример плохой компонуемости – модуль нельзя легко скомпоновать с другим кодом.

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

Многослойные интерфейсы

Разрабатывая интерфейс для сложной системы (к примеру, отправка емейл), часто приходишь к дилемме. С одной стороны, не нужно перегружать пользователя интерфейса деталями. Не надо заставлять их изучать его 20 минут перед тем, как они смогут отправить емейл. С другой стороны, не хочется и прятать все детали – когда людям надо сделать что-то сложное при помощи вашего модуля, у них должна быть такая возможность.

Часто приходится предлагать два интерфейса: детализированный низкоуровневый для сложных ситуаций, и простой высокоуровневый для обычного использования. Второй можно построить на основе первого. В модуле для отправки емейлов высокоуровневый интерфейс может быть просто функцией, которая принимает сообщение, адрес получателя и отправителя, и отправляет письмо. Низкоуровневый должен давать доступ к заголовкам, приложенным файлам, HTML письмам и т.д.

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

Хотя JavaScript совершенно не помогает делать модули, его гибкие функции и объекты позволяют сделать достаточно неплохую систему модулей. Область видимости функций используется как внутреннее пространство имён модуля, а объекты используются для хранения наборов переменных.

Есть два популярных подхода к использованию модулей. Один – CommonJS, построенный на функции require, которая вызывает модули по имени и возвращает их интерфейс. Другой – AMD, использующий функцию define, принимающую массив имён модулей и, после их загрузки, исполняющую функцию, аргументами которой являются их интерфейсы.

Упражнения
Названия месяцев

Напишите простой модуль типа weekday, преобразующий номера месяцев (начиная с нуля) в названия и обратно. Выделите ему собственное пространство имён, т.к. ему потребуется внутренний массив с названиями месяцев, и используйте чистый JavaScript, без системы загрузки модулей.

Вернёмся к электронной жизни

Надеюсь, что глава 7 ещё не стёрлась из вашей памяти. Вернитесь к разработанной там системе и предложите способ разделения кода на модули. Чтобы освежить вам память – вот список функций и типов, по порядку появления:

Не надо создавать слишком много модулей. Книга, в которой на каждой странице была бы новая глава, действовала бы вам на нервы (хотя бы потому, что всё место съедали бы заголовки). Не нужно делать десять файлов для одного мелкого проекта. Рассчитывайте на 3-5 модулей.

Некоторые функции можно сделать внутренними, недоступными из других модулей. Правильного варианта здесь не существует. Организация модулей – вопрос вкуса.

Круговые зависимости

Запутанная тема в управлении зависимостями – круговые зависимости, когда модуль А зависит от Б, а Б зависит от А. Многие системы модулей это просто запрещают. Модули CommonJS допускают ограниченный вариант: это работает, пока модули не заменяют объект exports, существующий по-умолчанию, другим значением, и начинают использовать интерфейсы друг друга только после окончания загрузки.

Можете ли вы придумать способ, который позволил бы воплотить систему поддержки таких зависимостей? Посмотрите на определение require и подумайте, что нужно сделать этой функции для этого.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *