понедельник, 15 октября 2012 г.

modules.js и модули на клиентской стороне

Проект на github>>

Давно смотрел на Яндекс.БЭМ. В принципе мне все подходило для моего проекта, кроме шаблонизатора, тк не хотелось вводить дополнительный этап компиляции клиентского кода, да и разрабатывать удобный для себя DSL я еще не готов, а использование JSON/XML для шаблонизатора мне не видится отличной идеей.

Разработал modules.js, который позволяет загружать модули без шаблониатора. На производительность подхода не смотрел, тк для моего проекта текущей хватает.

Модули храним в отдельной папке, в моем проекте это папка modules, может быть несколько папок модулей для модулей разного назначения (например commonModules, coreModules и т.д.).

modules.js зависит от require.js и jquery. Они должны располагаться в папке /js/libs и называться require.js и jquery.js соотверственно. В случае иного расположения необходимо поправить requirejs.config  в modules.js.

Модуль состоит из следующих компонентов: папка модуля внутри папки модулей, которая называется по названию модуля, например

/modules/about

или 

/modules/globalNavigationMenu

Если модуль у нас не загружается на страницу, а представляет из себя скрипт js (что мне нужно для core modules) мы добавляем js скрипт, называющийся как имя модуля, например:

/modules/core/core.js 

Если модуль у нас загружается на страницу, например глобальное меню навигации, он состоит из файлов html, css, js, называющихся как имя модуля:

/modules/about/about.html

/modules/about/about.css

/modules/about/about.js

Вот как например это выглядит в моем проекте: 

NewImage

Давайте попробуем использовать модули. Создадим index.html , в который будем загружать модули. Я взял тестовый из geo4geo. В нем подключен общий css стиль для страницы (в данный момент разметка делается в css для index.html и в нее загружаются модули, но я не вижу проблемы грузить и разметку как модули и в нее грузить собственно модули, сделав библиотеку модулей разметок)

<!DOCTYPE html>
<html>
<head>
    <metahttp-equiv="Content-Type"content="text/html; charset=utf-8"/>
    <link rel="shortcut icon"href="/img/logo-black.ico"type="image/x-icon"/>
    <linkrel="icon"href="/img/logo-black.ico"type="image/x-icon"/>
    <title>GEO4GEO</title>
    <linkrel="stylesheet"href="/css/style.css"type="text/css"/>
    <script data-main="/index.js" src="/js/libs/require.js" type="text/javascript"></script>
</head>
<body>
<divid="container">
    <divid="header"></div>
    <divid="body">
        <divid="page_header_row"></div>
        <divid="two_columns_left20"></div>
        <divid="two_columns_right80"></div>
    </div>
    <divid="footer"></div>
</div>
</body>
</html>

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

requirejs.config({
    shim: {
       "jquery" : ["/js/libs/jquery.js"],
       "jqueryUI" : ["/js/libs/jquery-ui.js"]
   },
    baseUrl : "/js/libs"
});

require(["jquery", "modules"], function ($, modules) {
    $(document).ready(function () {
        modules.load("/modules", "globalNavigationMenu", "#header");
        modules.load("/modules", "about", "#footer");
    });
});

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

requirejs.config({
    shim: {
       "jquery" : ["/js/libs/jquery.js"],
       "jqueryUI" : ["/js/libs/jquery-ui.js"]
   },
    baseUrl : "/js/libs"
});

require(["jquery", "modules"], function ($, modules) {
    $(document).ready(function () {
        var modulesInfo = new Array();
        modulesInfo.push(new modules.Info("/modules", "globalNavigationMenu", "#header"));
        modulesInfo.push(new modules.Info("/modules", "about", "#footer"));
        modules.loadInfoArrayWithNotifier(modulesInfo);
    });
});

В данном примере для регистрации модулей в шине сообщений мы создаем список наборов информации о каждом из модулей типа modules.Info, если element (третий параметр) пуст "" – это означает, что нет необходимости загружать модуль в дерево index.html и мы ищем только js файл модуля и исполняем его. Если же третьим параметром есть id или class из index.html – мы загружаем модуль именно туда. Второй параметр - название модуля, первый - путь к папке с модулями.

Типичный модуль: 

globalNavigationMenu.html

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
<div class="globalNavigationMenu">
    <ul class="globalNavigationMenu_unorderedList">
        <li class="globalNavigationMenu_unorderedList_item">
            <a class="globalNavigationMenu_unorderedList_item_link" href="/protected/semantics/index.html">
                Семантическая база данных
            </a>
            <div class="globalNavigationMenu_unorderedList_dropDownListSpace">
                <ul class="globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList">
                    <li class="globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item">
                        <a class="globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item_link"
 href="/protected/semantics/modules/save_to_db/save_to_db.html">
                            Добавить информацию
                        </a>
                    </li>
                    <li class="globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item">
                        <a class="globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item_link"
 href="/protected/semantics/modules/get_from_db/get_from_db.html">
                            Найти информацию
                        </a>
                    </li>
                </ul>
            </div>
 
        </li>
        <li class="globalNavigationMenu_unorderedList_item">
            <a class="globalNavigationMenu_unorderedList_item_link" href="/index.html">
                Главная страница
            </a>
        </li>
    </ul>
</div>
</body>
</html>

globalNavigationMenu.css 

.globalNavigationMenu {
    margin: 0;
    padding: 3mm;
    background: #2b2b2b;
    height: 4mm;
}
 
.globalNavigationMenu_unorderedList
{
    width: 100%;
    margin: 0;
    padding: 0 0 0 0;
    list-style: none;
}
 
.globalNavigationMenu_unorderedList_item
{
    float: left;
    padding: 0 0 5mm 0;
    position: relative;
}
 
.globalNavigationMenu_unorderedList_item_link
{
    float: left;
    height: 5mm;
    padding: 0 3mm;
    color: #d3d3d3;
    text-decoration: none;
}
 
.globalNavigationMenu_unorderedList_item:hover a
{
    color: #fafafa;
}
 
.globalNavigationMenu_unorderedList_item:hover .globalNavigationMenu_unorderedList_dropDownListSpace
{
    display: block;
}
 
/*Подменю*/
 
.globalNavigationMenu_unorderedList_dropDownListSpace
{
    list-style: none;
    margin: 0;
    padding: 0;
    display: none;
    position: absolute;
    top: 7mm;
    left: 0;
    z-index: 99999;
    background: #414141;
    float: left;
 
}
 
.globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList
{
    width: 100%;
    margin: 0;
    padding: 0;
    float: left;
 
}
 
.globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item
{
    width: 100%;
    float: left;
    margin: 0;
    padding: 0;
    display: block;
    box-shadow: 0 1px 0 #111111, 0 2px 0 #777777;
}
 
.globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item:last-child {
    box-shadow: none;
}
 
.globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item_link {
    padding: 2mm;
    height: auto;
    line-height: 1;
    display: block;
    white-space: nowrap;
    float: none;
    text-decoration: none;
    color: #d3d3d3;
}
 
 
 
.globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item:hover {
    background: #0186ba;
}
 
.globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item:first-child a
{
    border-radius: 5px 5px 0 0;
}
 
 
.globalNavigationMenu_unorderedList_dropDownListSpace_dropDownList_item:last-child a
{
    border-radius: 0 0 5px 5px;
}
 
.globalNavigationMenu_unorderedList:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: " ";
    clear: both;
    height: 0;
}

globalNavigationMenu.js

requirejs.config({
    shim: {
        "jquery" : ["/js/libs/jquery.js"]
    },
    baseUrl : "/js/libs"
});
 
define(["jquery", "modules"], function ($, modules) {
 
    return {
        register : function (callback) {
            modules.notifier.subscribe(this.notify);
            callback();
        },
        notify: function (e) {
            if ((e.receiver === "globalNavigationMenu") && (e.evt === "alert")) {
                alert(e.message);
            }
        },
        run: function () {
           modules.notifier.fire("about", "globalNavigationMenu", "alert", "Hi from GNM");
        }
    };
});

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

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

Собственно сам modules.js

requirejs.config({
    shim: {
        "jquery" : ["/js/libs/jquery.js"]
    },
    baseUrl : "/js/libs"
});
 
define(["jquery", "require"], function($, require) {
    function Observer() {
        this.fns = [];
    }
    Observer.prototype = {
        subscribe : function(fn) {
            this.fns.push(fn);
        },
        unsubscribe : function(fn) {
            this.fns = this.fns.filter(
                function(el) {
                    if ( el !== fn ) {
                        return el;
                    }
                }
            );
        },
        fire : function(to, from, e, msg, thisObj) {
            var scope = thisObj || window;
            this.fns.forEach(
                function(el) {
                    el.call(scope, {receiver: to, sender: from, evt: e, message: msg});
                }
            );
        }
    };
    var notifier = new Observer();
    return {
        load: function (path, name, element) {
            $("head").append("<link rel="stylesheet" href="" + path + "/" + name + "/" + name + ".css" + "" type="text/css" />");
            $(element).load(path + "/" + name + "/" + name + ".html ." + name, function() {
                require([path + "/" + name + "/" + name + ".js"], function (module) {
                    module.run();
                });
            });
        },
        loadInfoArrayWithNotifier: function (InfoArray) {
            var registeredCounter = 0;
            var callback = function() {
               registeredCounter++;
               if (registeredCounter == InfoArray.length - 1) {
                   InfoArray.forEach(function (el) {
                       if (el.element === "") {
                           require([el.path + "/" + el.name + "/" + el.name + ".js"], function (module) {
                               module.run();
                           });
                       } else {
                           $("head").append("<link rel="stylesheet" href="" + el.path + "/" + el.name + "/" + el.name + ".css" + "" type="text/css" />");
                           $(el.element).load(el.path + "/" + el.name + "/" + el.name + ".html ." + el.name, function () {
                               require([el.path + "/" + el.name + "/" + el.name + ".js"], function (module) {
                                   module.run();
                               });
                           });
                       }
                   });
               }
            }
            InfoArray.forEach(function (el) {
                require([el.path + "/" + el.name + "/" + el.name + ".js"], function (module) {
                    module.register(callback);
                });
            });
 
        },
        notifier: notifier,
        Info: function (path, name, element) {
            this.path = path;
            this.name = name;
            this.element = element;
        }
    };
});

Он состоит из типичного Observer, и динамической регистрации модулей на странице. 

Проект на github>>

1 комментарий:

  1. Было бы интересно увидеть репозиторий с юзкейсом, в которой эта библиотека очень упрощала бы код.

    А вообще очень интересная библиотека, никогда не встречал чего-то похожего.

    И сразу в опен соурс давай, будем все вместе улучшать ее.

    ОтветитьУдалить