Выразительный JavaScript - Марейн Хавербеке
Шрифт:
Интервал:
Закладка:
console.log(weekDay.name(today.dayNumber()));
});
Функция define здесь самая важная. Она принимает массив имён модулей, а затем функцию, принимающую один аргумент для каждой из зависимостей. Она загрузит зависимости (если они не были загружены ранее) в фоне, позволяя странице работать, пока файлы скачиваются. Когда всё загружено, define вызывает данную ему функцию, с интерфейсами этих зависимостей в качестве аргументов.
Загруженные таким образом модули должны содержать вызовы define. В качестве их интерфейса используется то, что было возвращено функцией, переданной в define. Вот модуль weekDay:
define([], function() {
var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"];
return {
name: function(number) { return names[number]; },
number: function(name) { return names.indexOf(name); }
};
});
Чтобы показать минимальную реализацию define, притворимся, что у нас есть функция backgroundReadFile, которая принимает имя файла и функцию, и вызывает эту функцию с содержимым этого файла, как только он будет загружен. (В главе 17 будет объяснено, как написать такую функцию).
Чтоб отслеживать модули, пока они загружаются, define использует объекты, описывающие состояние модулей, сообщает нам, доступны ли они уже, и предоставляет их интерфейс по доступности.
Функция getModule принимает имя и возвращает такой объект, и убеждается в том, что модуль поставлен в очередь загрузки. Она использует кеширующий объект, чтобы не грузить один модуль дважды.
var defineCache = Object.create(null);
var currentMod = null;
function getModule(name) {
if (name in defineCache)
return defineCache[name];
var module = {exports: null,
loaded: false,
onLoad: []};
defineCache[name] = module;
backgroundReadFile(name, function(code) {
currentMod = module;
new Function("", code)();
});
return module;
}
Мы предполагаем, что загружаемый файл тоже содержит вызов define. Переменная currentMod используется, чтобы сообщить этому вызову о загружаемом объекте модуля, чтобы тот смог обновить этот объект после загрузки. Мы ещё вернёмся к этому механизму.
Функция define сама использует getModule для загрузки или создания объектов модулей для зависимостей текущего модуля. Её задача – запланировать запуск функции moduleFunction (содержащей сам код модуля) после загрузки зависимостей. Для этого она определяет функцию whenDepsLoaded, добавляемую в массив onLoad, содержащий все пока ещё не загруженные зависимости. Эта функция сразу прекращает работу, если есть ещё незагруженные зависимости, так что она выполнит свою работу только раз, когда последняя зависимость загрузится. Она также вызывается сразу из самого define, в случае когда никакие зависимости не нужно грузить.
function define(depNames, moduleFunction) {
var myMod = currentMod;
var deps = depNames.map(getModule);
deps.forEach(function(mod) {
if (!mod.loaded)
mod.onLoad.push(whenDepsLoaded);
});
function whenDepsLoaded() {
if (!deps.every(function(m) { return m.loaded; }))
return;
var args = deps.map(function(m) { return m.exports; });
var exports = moduleFunction.apply(null, args);
if (myMod) {
myMod.exports = exports;
myMod.loaded = true;
myMod.onLoad.every(function(f) { f(); });
}
}
whenDepsLoaded();
}
Когда все зависимости доступны, whenDepsLoaded вызывает функцию, содержащую модуль, передавая в виде аргументов интерфейсы зависимостей.
Первое, что делает define, это сохраняет значение currentMod, которое было у него при вызове, в переменной myMod. Вспомните, что getModule прямо перед исполнением кода модуля сохранил соответствующий объект модуля в currentMod. Это позволяет whenDepsLoaded хранить возвращаемое значение функции модуля в свойстве exports этого модуля, установить свойство loaded модуля в true, и вызвать все функции, ждавшие загрузки модуля.
Этот код изучать тяжелее, чем функцию require. Его выполнение идёт не по простому и предсказуемому пути. Вместо этого, несколько операций должны быть выполнены в неопределённые моменты в будущем, что затрудняет изучения того, как выполняется этот код.
Настоящая реализация AMD гораздо умнее подходит к превращению имён модулей в URL и более надёжна, чем показано в примере. Проект RequireJS предоставляет популярную реализацию такого стиля загрузчика модулей.
Разработка интерфейса
Разработка интерфейсов – один из самых тонких моментов в программировании. Любую нетривиальную функциональность можно реализовать множеством способов. Поиск работающего способа требует проницательности и предусмотрительности.
Лучший способ познать значимость хорошего интерфейса – использовать много интерфейсов. Некоторые будут плохие, некоторые хорошие. Опыт покажет вам, что работает, а что – нет. Никогда не принимайте как должное плохой интерфейс. Исправьте его, или заключите в другой интерфейс, который лучше вам подходит.
Предсказуемость
Если программист может предсказать, как работает ваш интерфейс, ему не придётся часто отвлекаться и смотреть подсказку по его использованию. Постарайтесь следовать общепринятым соглашениям. Если есть модуль или часть языка JavaScript, которая делает что-то похожее на то, что вы пытаетесь реализовать – будет неплохо, если ваш интерфейс будет напоминать существующий. Таким образом, он будет привычен для людей, знакомых с существующим интерфейсом.
В поведении вашего кода предсказуемость также важна. Вас может постичь искушение сделать интерфейс слишком заумным якобы потому, что его удобнее использовать. К примеру, вы можете принимать любые виды типов и комбинаций аргументов и проделывать с ними «то, что надо». Или предоставлять десятки специализированных функций, которые предлагают незначительно отличающуюся функциональность. Это может сделать код, опирающийся на ваш интерфейс, немного короче, зато затруднить людям, работающим с ним, строить чёткую мысленную модель работы вашего модуля.
Компонуемость
Старайтесь использовать в интерфейсах настолько простые структуры данных, насколько это возможно. Делайте так, чтобы функции выполняли простые и понятные вещи. Если это применимо, делайте функции чистыми (см. Главу 3).
К примеру, частенько модули предлагают свою версию массивоподобных коллекций объектов со своим интерфейсом для подсчёта и извлечения элементов. У таких объектов нет методов map или forEach, и никакая функция, ожидающая настоящий массив, не сможет с ними работать. Это пример плохой компонуемости – модуль нельзя легко скомпоновать с другим кодом.
Примером может служить модуль для орфографической проверки текста, который может пригодиться в текстовом редакторе. Проверочный модуль можно сделать таким, чтобы он работал с любыми сложными структурами, используемыми самим редактором, и вызывал внутренние функции редактора для предоставления пользователю выбора вариантов написания. Если вы поступите таким образом, модуль нельзя будет использовать с другими программами. С другой стороны, если мы определим интерфейс модуля проверки, который принимает простую строку и возвращает позицию, на которой в строке есть возможная ошибка, а впридачу – массив предлагаемых поправок, тогда у нас будет интерфейс, который можно скомпоновать с другими системами, потому что строки и массивы всегда доступны в JavaScript.
Многослойные интерфейсы
Разрабатывая интерфейс для сложной системы (к примеру, отправка e-мейл), часто приходишь к дилемме. С одной стороны, не нужно перегружать пользователя интерфейса деталями. Не надо заставлять их изучать его 20 минут перед тем, как они смогут отправить e-мейл. С другой стороны, не хочется и прятать все детали – когда людям надо сделать что-то сложное при помощи вашего модуля, у них должна быть такая возможность.
Часто приходится предлагать два интерфейса: детализированный низкоуровневый для сложных ситуаций, и простой высокоуровневый для обычного использования. Второй можно построить на основе первого. В модуле для отправки e-мейлов высокоуровневый интерфейс может быть просто функцией, которая принимает сообщение, адрес получателя и отправителя, и отправляет письмо. Низкоуровневый должен давать доступ к заголовкам, приложенным файлам, HTML письмам и т. д.
Итог
Модули позволяют структурировать большие программы, разделяя код по разным файлам и пространствам имён. Если обеспечить их хорошо разработанными интерфейсами, их будет просто использовать, применять в других проектах и продолжать использовать при развитии и эволюции самого проекта.