Подключаемые модули

Подключаемые модули

Перед тем как перейти к описанию структуры и функциональности модулей, стоит посмотреть на то, как происходит запуск системы. Файл /static/index.js.

var MSite = (new function(){

    var self = this;

    self.Events = new EventEmitter();

    self.Init = function(done) {
        ModuleManager.Load(function(){
            ModuleManager.Init(function(){
                self.Events.emit("initauth");
                return done && done();
            })                
        })    
    }

    self.Start = function(){
        self.Init(function () {
            self.Events.emit("inited");
            // ...
        });
        History.Adapter.bind(window,'statechange',function(){
            self.Events.emit("navigate");
            window.scrollTo(0,0);
        });
        window.onbeforeunload =function(){
            self.Events.emit("unload");
        };
    }
})


$(document).includeReady(function () {
    MSite.Start();
});

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

  1. Система дожидается пока загрузятся все необходимые js файлы и файлы шаблонов (библиотечные файлы, прописанные в index.html, и файлы модулей) - includeReady.

  2. После этого загружает список модулей с сервера (конфигурации включенных модулей) - ModuleManager.Load

  3. Происходит инициализация модулей - ModuleManager.Init

В зависимости от того, зарегистрирован пользователь в системе или нет, запускаются модули:

  1. доступные только для гостя

  2. доступные для гостя и авторизованного пользователя.

Структура и функционал модуляLogin.

Структура файлов выглядит следующим образом:

Чем занимается модуль:

  1. Авторизация пользователя (Login+Пароль)

  2. Восстановлении пароля через электронную почту

  3. Регистрация пользователя

  4. Редактирование Профиля

  5. Возможность изменения пароля

  6. Проверка пароля на надежность

  7. Предоставление остальным модулям системы информации о том, зарегистрирован пользователь или нет

  8. Отображение в правом верхнем углу системы информации о текущем пользователе

Остановимся на списке файлов:

api.js

серверный код необходимый для работы модуля. На выходе система ожидает от этого файла - Express router. Все маршруты которые описываются в модуле будут автоматически получать prefix /api/modules/{id модуля}. То есть, если мы пишем в маршруте /signup, то на самом деле api вызов будет находится по адресу /api/modules/login/signup.

При написании серверного кода стоит обратить внимание на следующие особенности:

  1. Система обработки ошибок предполагает, что если возникнут ошибки внутри маршрута, то идентификатор ошибки будет передаваться в функцию next. Например, return next("passwordisempty"), приведет к тому, что в ответ на ajax запрос, придет информация об ошибке в виде {err:"passwordisempty"}.

  2. Записи в системе не удаляются - им ставится атрибут IsActive в значение false. Поэтому при обращении к базе данных стоит указывать .isactive()

  3. Если вы хотите получить модель из базы данных в режиме только по чтению - необходимо использовать модификатор .lean() для ускорения работы.

var _ = require('lodash');
var async = require('async');
var router = require('express').Router();
var passport = require(__base + 'src/passport.js');
var mongoose = require("mongoose");
var config = require(__base + "config.js");
var LIB = require(__base + 'lib/helpers/lib.js');
var Mailer = require(__base + 'src/mailer.js');

router.post('/signup', LIB.Require(['Mail', 'NameUser']), function(req, res, next) {
//....   
});

router.get('/requestconfirm', function(req, res, next) {
//....
});

router.get('/byemail', function(req, res, next) {
//....
});

router.post('/bypassword', function(req, res, next) {
//....
});

router.post('/setpassword', function(req, res, next) {
    if (!req.body.password) return next('passwordisempty');
    mongoose.model('user').findOne({
        _id: req.user._id
    }).isactive().exec(function(err, U) {
        if (!U) return next("usernotfound");
        U.password = req.body.password;
        U.DoResetPass = false;
        U.save(req.user.CodeUser, function(err) {
            if (err) return next(err);
            return res.end();
        })
    });
});

router.post('/byemail', function(req, res, next) {
//....
});


router.get('/logout', function(req, res) {
//....
});


router.get('/me', function(req, res) {
//....
});

router.put('/profile', function(req, res, next) {
  //....
});

module.exports = router;

config.json

конфигурационный файл описывающий поведение модуля

{
  "id": "login",
  "title": "Вход/Регистрация",
  "icon": "fa-user",
  "is_enabled": true,
  "class_name": "Login",
  "initial_load":true,
  "guest_load":true,
  "places":{
    "topmenu":true
  },
  "pages": [
      {"id":"profile","title":"Профиль","breadcrumbs":{"path":"/profile","title":"Профиль"}},
      {"id":"login","guest":true}
  ]
}

Стоит обратить внимание на следующие поля в файле настройки:

is_enabled

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

initial_load, guest_load, start_load

означает, что модуль предполагает начальную загрузку данных с сервера. guest_load - модуль работает и не для авторизованных пользователей. start_load - пока только у 1 го модуля Models (про него подробнееМодули системы) - предполагается, что без загрузки этих модулей - совсем ничего не сможет работать - даже обычные модули. Флаг используется очень редко.

places и sort_index

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

На данный момент поддерживаются:

topmenu

предполагается наличие шаблона {id}_top_menu

toolbutton

предполагает наличие шаблона tb_{id}

adminplugin

предполагает наличие шаблона app_{id}

adminpage

предполагает наличие index.html с кодом страницы

documentpage

предполагает наличие index.html

leftmenu

предполагает наличие шаблона {id}_left_menu

rightmenu

Страницы отображаемые аналогично leftmenu, только справа. Пока в системе - нет

homepage

Страницы отображаемые аналогично adminplugin, только на главной странице. Пока в системе - нет

pages

описание дополнительных страниц. Обратите внимание на параметр breadcrumbs (хлебные крошки) если вы хотите, чтобы у вас создавалась дополнительная навигация на страницах, нужно это поле заполнять.

Для каждой страницы, описанной в пункте pages, создастся страница, доступная для навигации через pagerjs.

Кроме выше перечисленных пунктов, каждый модуль может объявить набор привилегий, которые будут использоваться в настройке прав (подробнее о привилегиях читайте в разделеPermissions.md)

permissions и permissionsModels

Массив кодов привелегий. Например, для редактора колонок (модуль columns) выделяются отдельные привелегии для редактирования колонок, колсетов и заголовков: IsColumnEditor, IsColsetEditor, IsHeaderEditor.

template.html

Содержит шаблоны, которые могут использоваться в страницах модулей. Ниже приведен шаблон выводящий информацию о текущем пользователе в системе: Аватар, имя, должность. Кроме информации о пользователе выводится информация о состоянии соединения с сервером (используется флажок из модуля Socket - IsOnline) и при нажатии на блок - отображается выпадающее меню, с возможностью перехода на персональную страницу пользователя и возможностью выйти из системы (закрытия сессии)

<script id="login_topmenu" type="text/html">
    <li class="light-blue" data-bind="with:MLogin.Me">
        <a data-toggle="dropdown" class="dropdown-toggle">
            <span data-bind="user_avatar:$data.UserPhoto"></span>
            <span class="label onlinestatus" data-bind='css:{"label-success":MSocket.IsOnline(),"label-warning":!MSocket.IsOnline()}'> </span>
            <span class="user-info">
                <span data-bind="text:$data.NameUser"></span>
                <small data-bind="text:$data.JobTitle"></small>
            </span>
            <i class="ace-icon fa fa-caret-down"></i>
        </a>
        <ul class="user-menu dropdown-menu-right dropdown-menu dropdown-yellow dropdown-caret dropdown-close" >
            <li>
                <a data-bind='page-href:"/profile"'><i class="ace-icon fa fa-user"></i>Профиль</a>
            </li>
            <li class="divider"></li>
            <li>
                <a data-bind="click:MLogin.Logout"><i class="ace-icon fa fa-power-off"></i>Выход</a>
            </li>
        </ul>
    </li>
</script>

index.css

Дополнительные стили, используемые в модуле

index.html

Код страницы, отображаемой если у модуля установлен флаг, что он является страницей администрирования или страницей документа -documentpageилиadminpage. Для нашего примера - index.html может быть пустым или отсутствовать

lang.json

Список идентификаторов, используемых в модуле с переводами

{
     "Login":"Вход",
     "Signup":"Регистрация",
     "UserPhoto":"Фото",
     "Recover":"Восстановление",
     "usernotconfirmed":"Пользователь не подтвержден",
     "usernotfound":"Пользователь не найден",
     "mailerrorpromt":"Неверная ссылка для восстановления пароля",
     "signuperrorpromt":"Неверная ссылка подтверждения почтового адреса",
     "emailwassend":"Письмо отправлено",
     "passwordisempty":"Пароль - пустой",
     "passwordsaredifferent":"Пароли не совпадают",
     "passwordisweak":"Пароль слишком простой",
     "mailpromt":"На указанный вами почтовый адрес отправлено письмо со ссылкой для восстановления пароля.",
     "requestuserexist":"Пользователь с указанным почтовым адресом уже зарегистрирован в системе.",
     "requestexist":"Заявка с указанным почтовым адресом уже зарегистрирована в системе.",
     "signuppromt":"На указанный вами почтовый адрес отправлено письмо со ссылкой для подтверждения почтового адреса.",
     "signuppromtsuccess":"Ваш запрос на регистрацию поступил в обработку. Администратор системы свяжется с вами в ближайшее время."
}

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

login.html, profile.html ...

Реализация дополнительных страниц, объявленных в конфигурационном файле модуля. Ниже - пример персональной страницы - profile.html

<!-- ko with:MLogin.Me() -->
<div class='row' style='margin-top:10px;'>
    <div class='col-sm-4'>
        <!-- ko template:{
                name:'small_form_with_header',
                data:{Name:'',Fields:MLogin.ProfileFields,Model:MLogin.Me}
        } --><!-- /ko -->
        <div class="space-6"></div>
        <button class="btn btn-sm btn-info btn-white" data-bind="click: MLogin.ChangePasswordModal">
            <div class="pull-left">
                <i class="fa fa-key"></i>
                <span> Изменить пароль</span>
            </div>
        </button>
        <button class="btn btn-sm btn-success btn-white" data-bind="click: MLogin.UpdateProfile">
            <div class="pull-left">
                <i class="fa fa-floppy-o"></i>
                <span> Обновить профиль</span>
            </div>
        </button>
    </div>
</div>
<!-- /ko -->

index.js

Основной файл с логикой запускаемый в браузере. Кроме основной модели представления (View Model), в нашем примере MLogin, в файле могут содержаться расширения для knockout-а (custom binding)

var MLogin = (new function(){

    var self = this;
    self.base = "/api/modules/login/";

    self.Error = ko.observable(null);
    self.Mode  = ko.observable("Login"); // Login, SetPassword, Recover, Signup, ResetEmailSent, SignupEmailSent

    self.Me = ko.observable(null);

    self.Init = function(done){
         MSite.Events.on("initialnavigate",self.ForceRedirect);
         //...
    }

    // ...

    self.PassStrength = function(Password){
            var cl = "p-none";
            if (Password.length){
            var Weights = {
                Cap: Password.match(/[A-ZА-Я]/)!=null? 1:0,
                Low: Password.match(/[a-zа-я]/)!=null? 1:0,
                Num: Password.match(/[0-9]/)!=null? 1:0,
                Spe: Password.match(/[!,%,&,@,#,$,^,*,?,_,~]/)!=null? 2:0,
                Len: Password.length>8? 3:0,
            }
            var p = (_.sum(_.values(Weights))/8)*100;            
            if (p<30) cl = "p-veryweak"; 
            else if (p<50) cl = "p-weak"; 
            else if (p<80) cl = "p-medium";
            else  cl = "p-strong";
         }
         return cl;
    }
    self.Events = new EventEmitter();

    self.ForceRedirect = function(){
        var DoRedirect = false;
        if (!self.Me() && MBreadCrumbs.CurrentRoute()[0]=="error"){
            ;
        } else if (!self.Me() && MBreadCrumbs.CurrentRoute()[0]!="login" ){
            self.Mode("Login");
            DoRedirect = true;
        } else if (self.Me() && MBreadCrumbs.CurrentRoute()[0]=="login"){
            pager.navigate('/');
        } 
        // Установка пароля
        if (self.Me() && self.Me().DoResetPass()){
            DoRedirect = true;
            self.Mode("SetPassword");
        }
        if (DoRedirect){
            self.Events.emit("loginredirect");
            pager.navigate('/login');
        }
    }


    return self;
})

ModuleManager.Modules.Login = MLogin;

ko.bindingHandlers.PasswordStrength = {
    update: function(element, valueAccessor, allBindingsAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        var Password = value+"";
        var cl = MLogin.PassStrength(Password);
        $(element).removeClass("p-none p-veryweak p-weak p-medium p-strong").addClass(cl);
    } 
};

В большинстве случаев общение между модулями происходит через события. Так в нашем примере, при инициализации модели (Init) мы подписываемся на событие от системы: MSite.Events.on("initialnavigate",self.ForceRedirect). При запуске системы, мы можем проверить, что пользователю необходимо сменить пароль и перенаправить его на страницу модуля Login. Любой модуль может объявить внутри себя интерфейс для отправки событий, на который смогут подписаться другие модули self.Events = new EventEmitter(). Следует избегать использование других способов взаимодействия между модулями (исключение могут составлять базовые модули, описанные в следующей главе)

Last updated