При написании всего веб-приложения на JavaScript очень важно, чтобы оно было хорошо организовано; поддержание проекта со спагетти с единственной причиной — головными болями и кошмарами. В этом уроке я покажу вам, как модулировать ваш код, чтобы упростить управление большими проектами JavaScript.
Вступление
Недавно я пересмотрел одну из моих любимых презентаций JavaScript: «Масштабируемая архитектура приложения JavaScript», автор — Николас С. Закас. Модульные шаблоны JavaScript, которые он рекомендовал, мне показались особенно интересными, поэтому я решил попробовать. Я взял теоретические фрагменты кода из его слайдов, расширил и настроил их и придумал то, что я покажу вам сегодня. Я с трудом могу утверждать, что я эксперт в этом: я думал об этом методе написания JavaScript чуть более недели. Но, надеюсь, я смогу показать вам, как вы можете применить то, что продвигал Закас, на практике, и заставить вас задуматься о кодировании таким образом.
Слово о сопутствующей заставке
Есть скринкаст, который идет вместе с этим уроком; это довольно долго (почти 2 с половиной часа, на самом деле), поэтому вот таблица содержания, чтобы помочь вам ориентироваться в нем; Я дал каждому разделу заголовок «слайд», чтобы было легче найти начало каждого раздела.
- 0:00:00 — начало
- 0:05:15 — Сборка модулей
- 0:40:05 — Строим Песочницу
- 1:04:39 — Создание ядра
- 1:36:55 — Создание интерфейса (HTML & CSS)
- 1:51:08 — Отладка!
- 2:02:27 — Создание ядра (издание Dojo)
- 2:18:47 — Обсуждение преимуществ
Презентация Николаса Закаса
Если вы не смотрели его недавно, вы должны посмотреть презентацию Закаса, прежде чем продолжить; многое из того, что я покажу вам, будет легче понять, если вы за поясом:
Презентация
Примечание: это не оригинальная презентация, на которой был основан этот учебник, а более свежая версия того же доклада.
Слайды
Резюме
Итак, в целом, хорошее приложение JavaScript будет иметь четыре слоя. Снизу вверх, это следующие:
- Основа : это будет ваш JavaScript-фреймворк, такой как jQuery, если вы его используете. Или вы можете написать свой собственный.
- Ядро приложения : этот слой отвечает за объединение всех компонентов и запуск вашего веб-приложения. Это также обеспечивает уровень выравнивания для следующего слоя вверх (песочница); это может показаться излишним, как только вы войдете в него, и вам будет интересно, почему мы не позволяем песочнице общаться непосредственно с базой. Тем не менее, если сначала запустить все через ядро, мы можем легко заменить базу на другой фреймворк. Тогда нам нужно только изменить функции посредника в ядре.
- Песочница : это мини-API и единственный API, к которому имеет доступ каждая часть нашего веб-приложения. Создав песочницу, которой вы довольны, очень важно, чтобы внешний интерфейс не изменился. Это потому, что у вас будет много модулей в зависимости от него, и изменение API будет означать прохождение каждого модуля и его обновление, а не то, что вы хотите делать. Конечно, вы можете изменить код внутри методов песочницы (если они все еще делают то же самое) или добавить функциональность.
- Модули : это реальная работа в нашем приложении. Каждый модуль представляет собой автономный фрагмент кода, который выполняет один аспект веб-приложения. Как я уже писал ранее для веб-приложений, весь код модуля переплетается в одном спагетти-подобном беспорядке, и когда отсутствует одна часть, происходит сбой всего приложения. Когда все это аккуратно помещается в модули (с взаимодействиями, предоставляемыми через песочницу), пропущенная часть не вызывает никаких ошибок.
Чтобы объяснить этот шаблон, мы создадим мини-интернет-магазин (ну, только интерфейс). Это действительно показывает преимущества модулей, потому что интернет-магазин имеет много отдельных частей:
- панель продукта
- корзина
- окно поиска
- способ фильтрации продуктов
Каждая из этих частей, несомненно, является отдельной частью страницы, но все они должны взаимодействовать с другими модулями — либо как инициатор, либо как получатель. Посмотрим, как все это работает!
Хорошо, достаточно теории уже! Давайте начнем кодировать!
Строительные блоки: модули
Когда я начал этот проект, у меня не было ядра или песочницы для работы. Итак, я решил начать с кодирования модулей, потому что это дало бы мне представление о том, что модули должны иметь доступ к песочнице.
При создании модуля я использовал шаблон, очень близкий к примеру кода, который показал Закас:
CORE.create_module ("search-box", function (sb) { возвращение { init: function () {}, destroy: function () {} }; });
Как вы можете видеть, мы используем метод create_module
в CORE
для регистрации этого модуля в веб-приложении (конечно, мы еще не создали ядро, но мы его получим). Эта функция будет принимать два параметра: имя модуля (в данном случае «search-box») и функцию, которая возвращает объект модуля. То, что я показал здесь, является самым простым из возможных модулей. Обратите внимание на несколько вещей о функции создателя: во-первых, у нее есть один параметр, который будет экземпляром нашей песочницы (когда мы посмотрим на песочницу, мы увидим, почему экземпляр лучше, чем все модули имеют доступ к одной и той же песочнице). объект). Этот объект-песочница — единственное соединение, которое модуль имеет с «внешним миром». (Конечно, как сказал Закас, на самом деле нет никаких технических ограничений, которые могли бы помешать вам получить доступ к базе или ядру непосредственно из модуля; вы просто не должны ‘ сделать это.) Как видите, эта функция возвращает объект, который является нашим модулем. По крайней мере, у этого модуля есть только метод init
(используемый при запуске модуля) и метод destroy
(используемый при выключении модуля).
Итак, давайте построим настоящий модуль.
Полный скринкаст
Модуль окна поиска
CORE.create_module ("search-box", function (sb) { ввод var, кнопка, сброс; возвращение { init: function () {}, уничтожить: функция () {}, handleSearch: function () {}, quitSearch: function () {} }; });
Вы должны понимать, что здесь происходит: наш модуль будет использовать три переменные: input
, button
и reset
. Помимо двух необходимых функций модуля в возвращаемом объекте, мы создаем две функции, связанные с поиском. Я не думаю, что есть какая-то причина, по которой они должны быть частью возвращенного объекта модуля; их можно так же легко объявить над оператором возврата и ссылаться на них через замыкание. Давайте углубимся в каждую из этих функций:
init: function () { input = sb.find ('# search_input') [0]; button = sb.find ('# search_button') [0]; reset = sb.find ("# quit_search") [0]; sb.addEvent (кнопка, 'click', this.handleSearch); sb.addEvent (сброс, 'щелчок', this.quitSearch); },
Сначала мы назначаем три переменные, которые нам нужны. Поскольку нам нужно иметь возможность DOM-элементов из наших модулей, нам понадобится метод find в нашей песочнице. Но что с [0]
в конце? Что ж, наш метод sb.find
возвращает нечто похожее на объект jQuery: элементам, соответствующим селектору, присваиваются пронумерованные ключи, а затем есть некоторые методы и свойства. Однако нам понадобятся только необработанные DOM-элементы, и мы получаем их (поскольку только один элемент будет иметь идентификатор, мы можем быть уверены, что возвращаем только один элемент).
Два из этих элементов ( button
и reset
) являются кнопками, поэтому нам нужно подключить некоторые обработчики событий. Мы должны добавить это и в песочницу! Как видите, это ваша стандартная функция add-event: она принимает элемент, событие и функцию.
Как насчет уничтожения модуля:
уничтожить: функция () { sb.removeEvent (button, 'click', this.handleSearch); sb.removeEvent (сброс, 'щелчок', this.quitSearch); вход = ноль; кнопка = ноль; сброс = ноль; },
Это довольно просто: должна быть функция removeEvent, которая отменяет работу addEvent
. Затем мы просто установим для трех переменных модуля значение null.
Эти два прослушивателя событий ссылаются на функции поиска. Давайте посмотрим на первый:
handleSearch: function () { var query = input.value; if (query) { sb.notify ({ тип: «выполнить поиск», данные: запрос }); } },
Сначала мы получаем значение поля поиска. Если что-то есть, мы будем двигаться дальше. Но что нам делать? Обычно при динамическом поиске (который не требует обновления страницы по запросу ajax), у нас был бы доступ к панели продуктов, и мы могли бы соответствующим образом их фильтровать. Но наш модуль должен существовать с панелью продукта или без нее; плюс, это только связь с внешним миром через песочницу. Итак, вот что предложил Закас: мы просто сообщаем песочнице (которая, в свою очередь, сообщает ядру), что пользователь выполнил поиск. Затем ядро предложит эту информацию другим модулям. Если есть ответивший, он возьмет данные и будет работать с ними. Мы делаем это с помощью метода sb.notify
; он принимает объект с двумя свойствами: тип события, которое мы выполняем, и данные, связанные с событием. В этом случае мы делаем событие «выполнить поиск», а соответствующие данные — это поисковый запрос. Это все, что нужно сделать модулю окна поиска; если есть другой модуль, который предоставил возможность поиска, ядро предоставит ему данные.
Следует отметить, что этот метод полностью универсален. Модуль, который будет использовать это событие в нашем примере, не будет ничего делать Ajax-y, но нет никаких причин, по которым другой модуль не мог бы сделать это или искать каким-то совершенно другим способом.
Метод quitSearch
не намного сложнее:
quitSearch: function () { input.value = ""; sb.notify ({ введите: 'quit-search', данные: ноль }); }
Сначала очистим окно поиска; тогда мы дадим песочнице знать, что мы выполняем ‘quit-search’; в этом случае нет соответствующих данных.
Верьте или нет, вот и весь поисковый модуль. Довольно просто, а? Давайте перейдем к следующему.
Модуль панели фильтров
Мы хотим, чтобы в интернет-магазине пользователи могли просматривать товары по категориям. Итак, давайте реализуем панель фильтров, где пользователь щелкает названия категорий, чтобы показать только элементы в данной категории.
CORE.create_module ("filters-bar", function (sb) { вар фильтры; возвращение { init: function () { filters = sb.find ('a'); sb.addEvent (фильтры, 'click', this.filterProducts); }, уничтожить: функция () { sb.removeEvent (фильтры, 'click', this.filterProducts); фильтры = ноль; }, filterProducts: function (e) { sb.notify ({ тип: 'change-filter', данные: e.currentTarget.innerHTML }); } }; });
Этот не очень сложный. Нам понадобится переменная для хранения фильтров. Как видите, мы используем sb.find
для получения всех якорей. Но каковы шансы того, что все якоря на странице будут фильтрами? Не очень хорошо. Как только мы перейдем к написанию метода find
, вы увидите, как он возвращает только элементы в элементе DOM, соответствующие нашему модулю. Затем мы добавим в фильтры событие click, которое вызывает метод filterProduct
. Как вы можете видеть, этот метод просто сообщает песочнице о нашем событии ‘change-filter’, давая ему текст ссылки, по которой щелкнули данные ( e
— объект события, а currentTarget
— элемент, по которому щелкнули).
Конечно, destroy
просто избавляется от слушателей событий.
Модуль панели продукта
Этот будет довольно длинным и, возможно, сложным, так что держитесь крепче! Начнем с оболочки:
CORE.create_module («панель продуктов», функция (sb) { вар продукты; Функция eachProduct (fn) { var i = 0, произведение; for (; product = products [i ++];) { п (продукт); } } сброс функции () { eachProduct (функция (продукт) { product.style.opacity = '1'; }); } возвращение { init: function () {}, сброс: сброс, уничтожить: функция () {}, search: function (query) {}, change_filter: function (filter) {}, addToCart: function (e) {} }; });
Найдите минутку, чтобы посмотреть на это; мало что отличается от других наших модулей; есть только больше этого. Основное отличие состоит в том, что я использовал две вспомогательные функции, созданные вне возвращаемого объекта. Первый называется eachProduct
, и, как вы можете видеть, он просто берет функцию и запускает ее для каждого элемента в списке продуктов. Другая — это функция сброса, которую мы поймем чуть позже.
Теперь давайте посмотрим на функции init
и destroy
.
init: function () { вар то = это; products = sb.find ('li'); sb.listen ({ 'change-filter': this.change_filter, 'reset-fitlers': this.reset, 'выполнить поиск': this.search, 'quit-search': this.reset }); eachProduct (функция (продукт) { sb.addEvent (product, 'click', that.addToCart); }); }, уничтожить: функция () { вар то = это; eachProduct (функция (продукт) { sb.removeEvent (product, 'click', that.addToCart); }); sb.ignore (['change-filter', 'reset-filters', 'execute-search', 'quit-search']); },
Внутри init
мы собираем все продукты (которые представлены в элементах списка). Затем мы должны сообщить песочнице, что мы заинтересованы в нескольких событиях. Мы передаем объект в метод sb.listen
; этот объект использует имя события в качестве ключа и функцию события в качестве значения для каждого свойства. Например, мы говорим «песочнице», что когда кто-то другой выполняет событие «execute-search», мы хотим ответить на него, выполнив нашу функцию search
. Надеюсь, вы начинаете видеть, как это будет работать!
Затем мы используем нашу вспомогательную функцию eachProduct
чтобы назначить функцию щелчка для каждого продукта. Когда продукт щелкнул, мы запускаем addToCart
. Мы должны кэшировать this
потому что его значение изменяется на глобальный объект внутри функции.
В destroy мы просто удаляем обработчики событий из продуктов и даем песочнице знать, что мы больше не заинтересованы в событиях (на самом деле, я не думаю, что это необходимо, из-за того, как мы обрабатываем вещи в ядре, но Я бросил это на всякий случай, если что-то на «конце» изменится).
Теперь мы рассмотрим функции, которые вызываются, когда другие модули запускают события:
search: function (query) { сброс настроек(); query = query.toLowerCase (); eachProduct (функция (продукт) { if (product.getElementsByTagName ('p') [0] .innerHTML.toLowerCase (). indexOf (query) <0) { product.style.opacity = '0.2'; } }); }, change_filter: function (filter) { сброс настроек(); eachProduct (функция (продукт) { if (product.getAttribute ('data-8088-keyword'). toLowerCase (). indexOf (filter.toLowerCase ()) <0) { product.style.opacity = '0.2'; } }); }, addToCart: function (e) { var li = e.currentTarget; sb.notify ({ тип: 'add-item', данные: {id: li.id, имя: li.getElementsByTagName ('p') [0] .innerHTML, цена: parseInt (li.id, 10)} }); }
Начнем с search
; эта функция вызывается, когда происходит действие «выполнить поиск». Как видите, он принимает поисковый запрос в качестве параметра. Сначала мы сбрасываем область продукта (на случай, если результаты предыдущего поиска или фильтрации есть). Затем мы зацикливаемся на каждом продукте с нашей вспомогательной функцией. Помните, что продукт — это элемент списка; внутри него абзац с описанием продукта, и это то, что мы будем искать (в реальном примере это, вероятно, не так). Мы получаем текст абзаца и сравниваем его с текстом запроса (обратите внимание, что оба они были выполнены через toLowerCase()
). Если результат меньше 0, то есть совпадение не найдено, мы установим непрозрачность продукта равной 0,2. Вот как мы будем скрывать продукты в этом примере. Это оно!
Сейчас самое время указать, что вся функция reset
устанавливает непрозрачность всех продуктов на 1.
Метод changeFilter
очень похож на search
; на этот раз вместо поиска по описанию продукта мы используем атрибуты данных data5 * в HTML5. Это позволяет нам добавлять пользовательские атрибуты к нашим элементам HTML, не нарушая правил спецификации. Однако они должны начинаться с «data-», и я также добавил личный префикс, чтобы они не конфликтовали с атрибутами, которые может использовать сторонний код. Фильтр, переданный в эту функцию, сравнивается с атрибутом данных, который будет содержать названия категорий элементов. Если совпадений нет, мы уменьшим непрозрачность элемента.
Последняя функция — addToCart
, которая запускается при нажатии на один из продуктов. Мы получим элемент, по которому щелкнули, и затем отправим в систему уведомление, информирующее его о нашем событии «add-item». На этот раз данные, которые мы передаем, являются объектом. Он содержит идентификатор продукта, название продукта и цену продукта. В этом примере мы ленивы и используем идентификатор элемента в качестве идентификатора и цены, а описание продукта в качестве имени.
Модуль корзины покупок
У нас есть еще один модуль для просмотра. Это модуль «корзина»:
CORE.create_module («корзина», функция (sb) { var cart, cartItems; возвращение { init: function () { cart = sb.find ('ul') [0]; cartItems = {}; sb.listen ({ 'add-item': this.addItem }); }, уничтожить: функция () { cart = null; cartItems = null; sb.ignore ([ 'надстройка элемент']); }, addItem: function (product) { } }; });
Я думаю, что вы уже поняли это; мы используем две переменные: корзину и элементы в корзине. При инициализации модуля мы установим их в ul
в корзине и пустом объекте соответственно. Затем мы дадим песочнице понять, что хотим ответить на одно событие. При уничтожении мы отменим все это.
Вот что должно произойти, когда товар добавлен в корзину:
addItem: function (product) { var entry = sb.find ('# cart-' + product.id + '.quantity') [0]; if (entry) { entry.innerHTML = (parseInt (entry.innerHTML, 10) + 1); cartItems [product.id] ++; } еще { entry = sb.create_element ('li', {id: "cart-" + product.id, потомки: [ sb.create_element ('span', {'class': 'product_name', text: product.name}), sb.create_element ('span', {'class': 'amount', text: '1'}), sb.create_element ('span', {'class': 'price', текст: '$' + product.price.toFixed (2)}) ], 'class': 'cart_entry'}); cart.appendChild (вход); cartItems [product.id] = 1; } }
Эта функция принимает объект продукта, который мы только что видели в модуле панели продукта. Затем мы получаем элемент с селектором ‘# cart-‘ + product.id + ‘.quantity’; это ищет элемент с классом «количество» внутри элемента с идентификатором «cart-id_number». Если этот продукт был добавлен в корзину раньше, он будет найден. Если он найден, мы будем увеличивать innerHTML этого элемента (количество продукта, добавленного пользователем в автомобиль) на единицу и обновлять запись в объекте cartItems
, которая отслеживает покупку.
Если элемент не был найден, то пользователь впервые добавляет этот продукт в корзину. В этом случае мы будем использовать метод create_element
песочницы; как вы можете видеть, он будет принимать объект атрибутов, похожий на jQuery. Особый случай здесь — это свойство children, которое представляет собой массив элементов для вставки в элемент, который мы создаем. Как вы можете видеть, мы в основном создаем элемент списка с тремя интервалами: название продукта, количество и цена. Затем мы добавляем этот элемент списка в корзину и добавляем продукт в объект cartItems
.
Вот и весь код наших модулей; Я должен отметить, что я поместил все это в файл modules.js. Теперь, когда мы знаем, с каким интерфейсом должны работать наши модули, мы готовы создать это … и это песочница.
Поддержка: Песочница
Я знаю, что уже упоминал об этом, но очень важно, чтобы внешний интерфейс песочницы не менялся. Это потому, что все модули зависят от него. Конечно, вы можете добавлять методы или изменять код внутри методов, если вы не меняете методы или то, что функция / возвращает функция.
Если мы modules.js
файл modules.js
, мы увидим, что это методы, которые песочница должна предоставить модулям:
- находить
- addEvent
- removeEvent
- поставить в известность
- Слушать
- игнорировать
- create_element
Итак, приступим к работе. Поскольку я хочу создать экземпляр песочницы, используя код Sandbox.create
, мы сделаем его объектом (в настоящее время) только с одним методом.
var Sandbox = { create: function (core, module_selector) { var CONTAINER = core.dom.query ('#' + module_selector); возвращение { }; } };
Вот наш старт. Как видите, метод create принимает два параметра: ссылку на ядро и имя модуля, которому он будет присвоен. Затем мы создаем переменную CONTAINER
, которая будет ссылаться на элемент DOM, соответствующий коду модуля. Теперь давайте начнем кодировать функции, о которых мы перечислили.
найти: функция (селектор) { вернуть CONTAINER.query (селектор); },
Это довольно просто. На самом деле, большая часть функциональности в песочнице довольно проста, потому что она должна быть тонкой оболочкой, которая дает модулю только необходимый объем доступа к ядру. Когда метод core.dom.query
который мы вызывали, возвратил контейнер, он дал контейнеру метод, который позволяет ему искать дочерний элемент по селектору; мы используем это, чтобы ограничить способность модуля влиять на DOM, тем самым сохраняя его как в HTML, так и в JavaScript.
addEvent: function (element, evt, fn) { core.dom.bind (element, evt, fn); }, removeEvent: function (element, evt, fn) { core.dom.unbind (element, evt, fn); },
Как я уже сказал, большинство из этих функций песочницы довольно маленькие; мы просто перенесем данные о событиях в ядро для подключения.
notify: function (evt) { if (core.is_obj (evt) && evt.type) { core.triggerEvent (EVT); } }, listen: function (evts) { if (core.is_obj (evts)) { core.registerEvents (evts, module_selector); } }, игнорировать: функция (evts) { if (core.is_arr (evts)) { core.removeEvents (evts, module_selector); } },
Эти три функции, как вы помните, являются транспортными средствами, которые модули используют для информирования других модулей о своих действиях. Я добавил к ним небольшую проверку ошибок, чтобы убедиться, что с данными события все в порядке, прежде чем мы отправим их в ядро. Обратите внимание, что при сообщении ядру о том, что мы слушаем или игнорируем, нам нужно также передать имя модуля.
create_element: function (el, config) { var i, текст; el = core.dom.create (el); if (config) { if (config.children && core.is_arr (config.children)) { я = 0; while (config.children [i]) { el.appendChild (config.children [I])); я ++; } удалить config.children; } else if (config.text) { text = document.createTextNode (config.text); удалить config.text; el.appendChild (текст); } core.dom.apply_attrs (el, config); } вернуть эл; }
Это, очевидно, самая длинная функция в песочнице (и, честно говоря, чем больше я об этом думаю, тем больше думаю, что она должна быть в ядре, но в любом случае…). Как мы знаем, он принимает имя элемента и объект конфигурации. Мы начнем с создания элемента DOM (используйте основной метод). Затем, если есть объект конфигурации, и у него есть массив с именем children, мы зациклим каждый дочерний элемент и добавим его к элементу. Затем мы удаляем свойство children. В противном случае, если у нас есть свойство text, мы установим для него текст элемента и удалим свойство text (в этом примере мы не можем иметь как текстовые, так и дочерние элементы). Наконец, мы будем использовать другую основную функцию, чтобы применить оставшиеся атрибуты и вернуть элемент.
И это конец песочницы. Я понимаю, что это может быть довольно упрощенная песочница, но она должна дать вам представление о том, как работает песочница. Кроме того, по мере его использования вы сможете добавлять другие методы, когда ваши модули требуют этого.
Основа: основа и ядро
Теперь мы готовы перейти к сути. Вот с чего мы начнем:
var CORE = (function () { var moduleData = {}, debug = true; возвращение { debug: function (on) { отладка = вкл? правда: ложь; }, }; } ());
Мы будем использовать объект данных модуля для хранения всего, что нужно знать о модулях; переменная отладки контролирует, регистрируются ли ошибки на консоли. У нас есть простая функция отладки для включения и выключения ошибок.
Давайте начнем с той функции create_module
которую мы использовали для регистрации наших модулей:
create_module: функция (moduleID, создатель) { var temp; if (typeof moduleID === 'string' && typeof creator === 'function') { temp = creator (Sandbox.create (this, moduleID)); if (temp.init && temp.destroy && typeof temp.init === 'function' && typeof temp.destroy === 'function') { moduleData [moduleID] = { создать: создатель, экземпляр: ноль }; temp = null; } еще { this.log (1, "Module \" "+ moduleId +" \ "Registration: FAILED: экземпляр не имеет функций инициализации или уничтожения"); } } еще { this.log (1, "Module \" "+ moduleId +" \ "Registration: FAILED: один или несколько аргументов имеют неправильный тип"); } },
Первое, что мы делаем, это подтверждаем, что параметры, переданные функции, имели правильный тип; в противном случае мы вызовем функцию журнала, которая принимает номер серьезности и сообщение (мы увидим, что функция журнала занимает несколько минут).
Затем мы создаем копию рассматриваемого модуля просто для того, чтобы убедиться, что он имеет функции init и destroy; если это не так, мы снова регистрируем ошибку. Однако, если все получится, мы добавим объект в moduleData
; мы сохраняем функцию создателя и пустое место для экземпляра при запуске модуля. Затем мы удалим временную копию модуля.
start: function (moduleID) { var mod = moduleData [moduleID]; if (mod) { mod.instance = mod.create (Sandbox.create (this, moduleID)); mod.instance.init (); } }, start_all: function () { var moduleID; for (moduleID в moduleData) { if (moduleData.hasOwnProperty (moduleID)) { this.start (moduleID); } } },
Далее мы добавим функцию для запуска модулей; как и следовало ожидать, он принимает имя модуля в качестве единственного параметра. Если в moduleData
есть соответствующий модуль, мы запустим его метод create, передав ему новый экземпляр песочницы. Затем мы запустим его, запустив метод init.
Мы можем создать функцию, которая позволит легко запускать все модули одновременно, поскольку это, вероятно, то, что мы хотим сделать. Нам просто нужно moduleData
и отправить каждый moduleID
в метод start. Не забудьте использовать часть hasOwnProperty
; Я знаю, что это кажется ненужным и уродливым (по крайней мере, мне), но оно есть на всякий случай, если кто-то добавил элемент к объекту-прототипу объекта.
stop: function (moduleID) { данные вар; if (data = moduleData [moduleId] && data.instance) { data.instance.destroy (); data.instance = null; } еще { this.log (1, "Остановить модуль" + moduleID + "': FAILED: модуль не существует или не был запущен"); } }, stop_all: function () { var moduleID; for (moduleID в moduleData) { if (moduleData.hasOwnProperty (moduleID)) { this.stop (moduleID); } } },
Следующие две функции должны быть очевидны: stop
и stop_all
. Функция stop
принимает имя модуля; если система знает о модуле с таким именем и этот модуль запущен, мы вызовем метод уничтожения этого модуля, а затем установим для экземпляра значение null. Если модуль не существует или не работает, мы зарегистрируем ошибку.
Функция stop_all
точно такая же, как start_all
, за исключением того, что вызовы stop каждого модуля.
Далее: общение с событиями.
registerEvents: function (evts, mod) { if (this.is_obj (evts) && mod) { if (moduleData [mod]) { moduleData [mod] .events = evts; } еще { this.log (1, ""); } } еще { this.log (1, ""); } }, triggerEvent: function (evt) { var mod; для (мод в moduleData) { if (moduleData.hasOwnProperty (mod)) { mod = moduleData [mod]; if (mod.events && mod.events [evt.type]) { mod.events [evt.type] (evt.data); } } } }, removeEvents: function (evts, mod) { var i = 0, evt; if (this.is_arr (evts) && mod && (mod = moduleData [mod]) && mod.events) { for (; evt = evts [i ++];) { удалить mod.events [evt]; } } },
Как мы знаем, registerEvents
принимает объект событий и модуль, который их регистрирует. Опять же, мы делаем некоторую проверку ошибок (в этом случае я оставил ошибки пустыми, просто для упрощения примера). Если evts
— это объект, и мы знаем, о каком модуле мы говорим, мы просто moduleData
объект в шкафчик модуля в moduleData
.
Когда дело доходит до запуска событий, нам дают объект с типом и данными. Мы снова moduleData
каждый модуль в moduleData
: если у модуля есть свойство события и у этого объекта события есть ключ, соответствующий событию, которое мы выполняем, мы вызовем функцию, сохраненную для этого события, и передадим ей событие данные.
Удаление событий еще проще; мы получаем объект событий и (после обычной проверки ошибок) зацикливаемся на нем и удаляем события в массиве из объекта события модуля. ( Примечание : я думаю, что я немного перепутал это на скринкасте, но это правильная версия.)
log: function (серьезность, сообщение) { if (debug) { консоль [(серьезность === 1)? 'log': (серьезность === 2)? 'warn': 'error'] (сообщение); } еще { // отправить на сервер } },
Вот эта функция журнала, которая преследует нас некоторое время; в основном, если мы находимся в режиме отладки, мы будем регистрировать ошибки на консоли; в противном случае мы отправим их на сервер. Ох, причудливые троичные вещи? Это просто использует аргумент серьезности, чтобы решить, какие функции Firebug использовать для регистрации ошибки: 1 === console.log, 2 === console.warn,> 2 === console.error.
Теперь мы готовы взглянуть на ту часть ядра, которая дает песочнице базовую функциональность; для большей части этого я субклассировал их в объекте dom
, потому что я одержим навязчивым способом таким образом.
Дом: { запрос: функция (селектор, контекст) { var ret = {}, это = это, jqEls, i = 0; if (context && context.find) { jqEls = context.find (селектор); } еще { jqEls = jQuery (селектор); } ret = jqEls.get (); ret.length = jqEls.length; ret.query = function (sel) { вернуть that.query (sel, jqEls); } возвратный ответ; },
Вот наша первая функция, query
. Требуется селектор и контекст. Теперь запомните, это ядро, где мы можем напрямую ссылаться на базу (это jQuery). В этом случае контекст должен быть объектом jQuery. Если у контекста есть метод find, мы установим в jqEls
результат context.find(selector)
; если вы знакомы с jQuery, вы будете знать, что он получит только тот элемент, который является потомком context
; вот как мы получаем функциональность sandbox.query! Затем мы установим наш возвращаемый объект на результат вызова метода get
jQuery; это возвращает объект необработанных элементов dom. Затем мы даем ret
свойство length, чтобы его можно было легко зациклить. Наконец, мы даем ему функцию запроса: эта функция принимает только один параметр, селектор, и вызывает core.dom.query, передавая этот селектор и jqEls
в качестве параметров. Это оно!
bind: function (element, evt, fn) { if (element && evt) { if (typeof evt === 'function') { fn = evt; evt = 'click'; } jQuery (элемент) .bind (evt, fn); } еще { // записывать неверные аргументы } }, unbind: function (element, evt, fn) { if (element && evt) { if (typeof evt === 'function') { fn = evt; evt = 'click'; } jQuery (элемент) .unbind (evt, fn); } еще { // записывать неверные аргументы } },
В функциях bind и unbind событий DOM я решил предоставить пользователю привилегию. Пользователи должны передать в аренду две функции, но если параметр evt
является функцией, мы будем предполагать, что пользователь оставил наш тип события, который он хочет обработать. В этом случае мы примем клик, так как он самый распространенный. Затем мы просто используем функцию привязки jQuery для подключения. Я должен отметить, что, поскольку наша функция запроса возвращает (минимально) обернутые наборы DOM, аналогичные jQuery, эта функция может передавать наш набор объекту Jquery, и с ним нет проблем.
Наша функция unbind точно такая же, за исключением того, что она развязывает события.
create: function (el) { return document.createElement (el); }, apply_attrs: function (el, attrs) { JQuery (эл) .attr (ATTRS); } }, // конец объекта dom is_arr: function (arr) { return jQuery.isArray (arr); }, is_obj: function (obj) { return jQuery.isPlainObject (obj); }
Я сгруппировал последние четыре функции вместе, потому что все они довольно просты; dom.create
просто возвращает новый элемент DOM; dom.apply_attrs
использует метод attr
jQuery для предоставления атрибутов элемента. Наконец, у нас есть две вспомогательные функции, которые мы использовали для проверки наших параметров.
Хотите верьте, хотите нет, но это все ядро; и наша база — jQuery, поэтому мы готовы собрать это.
Собираем все вместе
Теперь мы готовы создать HTML-код, чтобы увидеть наш JavaScript в действии. Я не буду подробно рассказывать вам об этом, потому что не в этом урок.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
<!DOCTYPE HTML>
<html lang=»en»>
<head>
<meta charset=»UTF-8″>
<title>Online Store</title>
<link rel=»stylesheet» href=»default.css» />
</head>
<body>
<div id=»main»>
<div id=»search-box»>
<input id=»search_input» type=»text» name=’q’ />
<button id=»search_button»>Search</button>
<button id=»quit_search»>Reset</button>
</div>
<div id=»filters-bar»>
<ul>
<li><a href=»#red»>Red</a></li>
<li><a href=»#blue»>Blue</a></li>
<li><a href=»#mobile»>Mobile</a></li>
<li><a href=»#accessory»>Accessory</a></li>
</ul>
</div>
<div id=»product-panel»>
<ul>
<li id=»1″ data-8088-keyword=»red»><img src=»img/1.jpg»><p>First Item</p></li>
<li id=»2″ data-8088-keyword=»blue»><img src=»img/2.jpg»><p>Second Item</p></li>
<li id=»3″ data-8088-keyword=»mobile»><img src=»img/3.jpg»><p>Third Item</p></li>
<li id=»4″ data-8088-keyword=»accessory»><img src=»img/4.jpg»><p>Fourth Item</p></li>
<li id=»5″ data-8088-keyword=»red mobile»><img src=»img/5.jpg»><p>Fifth Item</p></li>
<li id=»6″ data-8088-keyword=»blue mobile»><img src=»img/6.jpg»><p>Sixth Item</p></li>
<li id=»7″ data-8088-keyword=»red accessory»><img src=»img/7.jpg»><p>Seventh Item </p></li>
<li id=»8″ data-8088-keyword=»blue accessory»><img src=»img/8.jpg»><p>Eighth Item</p></li>
<li id=»9″ data-8088-keyword=»red blue»><img src=»img/9.jpg»><p>Ninth Item</p></li>
<li id=»10″ data-8088-keyword=»mobile accessory»><img src=»img/10.jpg»><p>Tenth Item</p></li>
</ul>
</div>
<div id=»shopping-cart»>
<ul>
</ul>
</div>
</div>
<script src=»js/jquery.js»></script>
<script src=»js/core-jquery.js»></script>
<script src=»js/sandbox.js»></script>
<script src=»js/modules.js»></script>
</body>
</html>
|
Ничего особенного; важно отметить, что у каждого из основных элементов div есть идентификаторы, соответствующие модулям JavaScript. И не забывайте об атрибутах HTML5 data- *, которые дают нам категории для фильтрации.
Конечно, нам нужно его стилизовать:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
body {
background:#ececec;
font:13px/1.5 helvetica, arial, san-serif;
}
#main {
width:950px;
margin:auto;
overflow:hidden;
}
#search-box, #filters-bar {
margin-left:10px;
}
#filters-bar ul {
list-style-type:none;
margin:10px 0;
padding:0;
border-top:2px solid #474747;
border-bottom:2px solid #474747;
}
#filters-bar li {
display:inline-block;
padding:5px 10px 5px 0;
}
#filters-bar li a {
text-decoration:none;
font-weight:bold;
color:#474747;
}
#product-panel {
float:left;
width: 588px;
}
#product-panel ul {
margin:0;
padding:0;
}
#product-panel li {
list-style-type:none;
display:inline-block;
text-align:center;
background:#474747;
border:1px solid #eee;
padding:15px;
margin:10px;
}
#product-panel li p {
margin:10px 0 0 0;
}
#shopping-cart {
float:left;
background:#ccc;
height:300px;
width:300px;
padding:30px;
border:1px solid #474747;
}
#shopping-cart ul {
list-style-type:none;
padding:0;
}
#shopping-cart li {
padding:3px;
margin:2px 0;
background:#ececec;
border: 1px solid #333;
}
#shopping-cart .product_name {
display:inline-block;
width:230px;
}
#shopping-cart .price {
display: inline-block;
float:right;
}
|
Трудно показать это в действии, (для этого есть скринкаст!), Но вот несколько снимков:
Ну, это было бы все, но давайте сделаем еще одну вещь: давайте создадим ядро, которое работает в Dojo, чтобы показать, как использование ядра, как мы это делали, облегчает переключение баз.
Почему Додзё? Честно говоря, я попробовал Mootools и YUI, но было несколько испытаний, которые могли занять больше времени, чем время, которое я должен был выяснить. Я, конечно, не думаю, что их невозможно использовать; Если вы создадите ядро с помощью Mootools, YUI или какой-либо другой платформы JavaScript, я бы хотел это увидеть. Сейчас давайте создадим один с Dojo.
Конечно, нам не нужно менять какие-либо функции обработки модулей; в нашем случае все, что нам нужно изменить, это раздел dom, is_arr
и is_obj
.
запрос: функция (селектор, контекст) { var ret = {}, это = this, len, i = 0, djEls; djEls = dojo.query (((контекст)? context + "": "") + селектор); len = djEls.length; while (i <len) { ret [i] = djEls [i ++]; } ret.length = len; ret.query = function (sel) { вернуть that.query (sel, селектор); } возвратный ответ; },
Вот функция запроса, переписанная для работы с Dojo. Как вы можете видеть, он делает именно то, что делает версия jQuery; главное отличие в том, что контекст — это строка, которая добавляется в начало селектора.
Теперь для функций событий DOM:
eventStore: {}, bind: function (element, evt, fn) { if (element && evt) { if (typeof evt === 'function') { fn = evt; evt = 'click'; } if (element.length) { var i = 0, len = element.length; for (; i <len;) { this.eventStore [element [i] + evt + fn] = dojo.connect (element [i], evt, element [i], fn); я ++; } } еще { this.eventStore [element + evt + fn] = dojo.connect (element, evt, element, fn); } } }, unbind: function (element, evt, fn) { if (element && evt) { if (typeof evt === 'function') { fn = evt; evt = 'click'; } if (element.length) { var i = 0, len = element.length; for (; i <len;) { dojo.disconnect (this.eventStore [element [i] + evt + fn]); удалите this.eventStore [element [i] + evt + fn]; я ++; } } еще { dojo.disconnect (this.eventStore [element + evt + fn]); удалите this.eventStore [element + evt + fn]; } } },
Вы заметите, что мы добавили объект eventStore
; это связано с тем, что Dojo связывает события с помощью dojo.connect
и отменяет привязку с помощью dojo.disconnect
; подвох в том, что dojo.disconnect
принимает объект, возвращенный из dojo.connect
качестве единственного параметра.Мы используем eventStore
для отслеживания этих значений, чтобы мы могли легко отключить события. Остальная сложность здесь — это просто циклы над нашими упакованными наборами DOM, потому что Dojo не может справиться с ними в одиночку.
create: function (el) { return document.createElement (el); }, apply_attrs: function (el, attrs) { var attr; for (attr в attrs) { dojo.attr (el, attr, attrs [attr]); } } }, // конец объекта dom is_arr: function (arr) { return dojo.isArray (arr); }, is_obj: function (obj) { return dojo.isObject (obj); }
Эта часть не должна быть слишком сложной для понимания; это все очень похоже на jQuery.
Теперь вы сможете включить Dojo в качестве своей базы и наше новое ядро Dojo в качестве своего ядра, и наше приложение продолжит функционировать, как и раньше.
Вывод: стоит ли кодировать таким образом?
Что ж, теперь, когда мы увидели, как именно должна работать такая система, давайте поговорим о том, захотите ли вы писать свои приложения на JavaScript, подобные этой. Очевидно, это не тот шаблон, который вы бы использовали на своем среднем веб-сайте; это для полноценных приложений. Вот что я придумал:
Минусы:
- Изучить этот тип кодирования может быть довольно сложно. Модульное мышление определенно требует смены парадигмы.
- Когда вы начинаете, трудно понять, сколько энергии дать слоям. Что должны делать модули? Должна ли быть какая-то реальная функциональность в песочнице, или это просто для того, чтобы предоставить нужное количество основных функциональных возможностей модулям? Где мы делаем проверку ошибок? Можем ли мы выполнять простые задачи, такие как создание элементов dom, внутри модулей? Я уверен, что у вас есть свои собственные взгляды и больше вопросов, поэтому дайте мне знать, что вы думаете, либо в комментариях к сообщению Nettuts +, либо через форму на моем веб-сайте . Или, что еще лучше: напишите об этом в своем блоге / на веб-сайте, чтобы его увидел весь мир, и обязательно пришлите мне ссылку.
- Наконец, странно использовать JavaScript-фреймворк в качестве основы; с моим нынешним пониманием этого, они на самом деле не годятся для этого.
Pros
- Когда у вас будет солидное ядро и песочница, создание новых веб-приложений будет намного быстрее; это в основном выбор модулей из вашей постоянно растущей библиотеки модулей и их соединение.
- Намного проще протестировать ваш код, потому что он такой модульный.
Ну, это все, что у меня есть для тебя сегодня; Надеюсь, я немного напрягся; Я знаю, что изучение всего этого за последнюю неделю или около того растянуло мое! Я хотел бы знать, что вы думаете обо всем этом, поэтому, пожалуйста, дайте мне знать, если у вас есть комментарии! Спасибо за прочтение!