Структура Модуля
Перед тем как перейти к описанию структуры и функциональности модулей, стоит посмотреть на то, как происходит запуск системы. Файл /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();
});
Если попытаться описать словами, что происходит для запуска системы, то получится следующее:
- Система дожидается пока загрузятся все необходимые js файлы и файлы шаблонов (библиотечные файлы, прописанные в index.html, и файлы модулей) - includeReady.
- После этого загружает список модулей с сервера (конфигурации включенных модулей) - ModuleManager.Load
- Происходит инициализация модулей - ModuleManager.Init
В зависимости от того, зарегистрирован пользователь в системе или нет, запускаются модули:
- доступные только для гостя
- доступные для гостя и авторизованного пользователя.
Структура и функционал модуля Login.
Структура файлов выглядит следующим образом:
Чем занимается модуль:
- Авторизация пользователя (Login+Пароль)
- Восстановлении пароля через электронную почту
- Регистрация пользователя
- Редактирование Профиля
- Возможность изменения пароля
- Проверка пароля на надежность
- Предоставление остальным модулям системы информации о том, зарегистрирован пользователь или нет
- Отображение в правом верхнем углу системы информации о текущем пользователе
Остановимся на списке файлов:
api.js
серверный код необходимый для работы модуля. На выходе система ожидает от этого файла - Express router. Все маршруты которые описываются в модуле будут автоматически получать prefix /api/modules/{id модуля}. То есть, если мы пишем в маршруте /signup, то на самом деле api вызов будет находится по адресу /api/modules/login/signup.
При написании серверного кода стоит обратить внимание на следующие особенности:
- Система обработки ошибок предполагает, что если возникнут ошибки внутри маршрута, то идентификатор ошибки будет передаваться в функцию next. Например, return next("passwordisempty"), приведет к тому, что в ответ на ajax запрос, придет информация об ошибке в виде {err:"passwordisempty"}.
- Записи в системе не удаляются - им ставится атрибут IsActive в значение false. Поэтому при обращении к базе данных стоит указывать .isactive()
- Если вы хотите получить модель из базы данных в режиме только по чтению - необходимо использовать модификатор .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"></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(). Следует избегать использование других способов взаимодействия между модулями (исключение могут составлять базовые модули, описанные в следующей главе)