Картинка для привлечения внимания
Sprute.js — новый изоморфный JS фреймворк. При его проектировании и реализации упор делался в первую очередь на удобство разработки и сохранение самого фреймворка максимально простым и компактным. В первую очередь это касается изоморфности.
Зачем еще один фреймворк?
В существующих фреймворках меня не устраивает подход к реализации изоморфности — моей целью было реализовать изоморфность таким образом, чтобы это не определяло архитектуру и не приходилось строить архитектуру вокруг изоморфности, а сделать её максимально прозрачной — чтобы я мог писать серверный код так, как я это привык, и он так же работал на клиенте. Мой подход можно назвать server side first.
Прошу сильно не пинать за скудность текста — развернутое изложение своих мыслей всегда было моим слабым местом.
Подход к изоморфности
Для реализации изоморфности я решил «эмулировать» node.js в браузере — изоморфный код пишется на сервере и работает так же на клиенте. Для этого пришлось портировать node.js'ный require в браузер и эмулировать файловую систему. Так же я частично портировал node.js'ные модули process, fs, events.
Архитектура
Весь код разделяется на 3 категории — серверный, клиентский, изоморфный и разделен по директориям back, front, common. В директориях back и front находятся базовые классы, специфичные для соответствующего окружения, 90% кода находится в директории common. Функционал фреймворка, такой как сервер, шаблонизатор, сокетное соединение реализован в виде компонентов — по сути модулей. Это позволяет инкапсулировать код c определенной зоной ответственности; так же позволяет писать изоморфные обертки для таких вещей, как сокетное соединение, создавая единый api на клиенте и сервере и позволяя менять реализацию компонента в дальнейшем.
Пример компонента:
'use strict';
module.exports = {
init() {
let module;
app.clientSide(() => {
module = require('./lib/client')
});
app.serverSide(() => {
module = require('./lib/server')
});
return module.init()
}
};
Статика
Стили, скрипты, реализующие интерактивность интерфейса и т.п., собираются в темы — директория с файлами и объект конфигурации. Это позволяет отделить логику интерфейса от остальной логики; так же это позволяет для каждой страницы задавать отдельную тему — удобно при редизайне сайта или создании различных админок и т.п.
Работа с данными
Работа с данными реализована с использованием паттерна data mapper. Логика сохранения/выбора данных находится в маппере, бизнес логика — например, обладает ли пользователь оперделенной привилегией — в модели, логика работы с набором — в коллекции. Изоморфность работы маппера реализуется следующим образом — запрос сериализуется в объект и передается на сервер, там объект запроса передается тому же мапперу; результат возвращается на клиент. На данный момент реализован маппер, использующий библиотеку knex для построения запросов и выборки данных.
Пример маппера
const BaseMapper = require(app.get('commonPath')+'/mappers/knex-mapper');
class CoolModel {}
class CoolMapper extends BaseMapper {
constructor() {
let connections;
app.serverSide(() => {
connections = require(process.cwd()+'/configuration/connections')
});
app.clientSide(() => {
connections = {};
});
super({
client: 'mysql',
connection: connections.mysql
})
}
get tableName() {
return 'cool'
}
get model() {
return CoolModel
}
beforeCreateTable(table) {
table.comment('very cool table')
}
addColumns(table) {
table.increments('id').primary();
table.string('field1');
table.integer('field2');
table.string('field3')
}
get validator() {
if(!this._validator) {
let vE = app.get('validationEngine');
this._validator = new vE({
id: 'integer',
field1: 'not_empty',
field2: 'integer',
field3: 'not_empty'
})
}
return this._validator
}
validateModel(model) {
return this.validator.validate(model)
}
}
const mapper = new CoolMapper();
mapper.find().limit(10).offset(5).then(collection => { /* code here */ });
mapper.findOne().where({id:2}).then(model => { /* code here */ })
Как это работает
Точкой входа является класс App. При инициализации класса производится инициализация компонентов — работа фреймворка здесь завершена. Дальше работу на себя берет компонент сервер, затем регистрируются роутеры. Дальнейшая схема работы состоит в обработке запросов роутерами.
Пример роутера:
'use strict';
const BaseRouter = require(app.get('classPath')+'/routers/base'),
process = require('process'),
theme = require(process.cwd()+'/configuration/theme-light');
module.exports = class extends BaseRouter {
constructor(params, DomDocument) {
super(params);
this.DomDocument = DomDocument || require(app.get('classPath')+'/classes/dom-document')
}
index(req, res) {
const view = new (require('../views/main-page'))(theme),
DomDocument = new this.DomDocument(theme);
view.render().then(html => {
DomDocument.setBlock('main', html);
this.loadPage(DomDocument, res)
})
}
};
Состояние на данный момент
В данный момент на нем работает один сайт — bel31stroy.ru и еще один находится в разработке. Сам фреймворк периодически подвергается доработкам. Pull реквесты и баг репорты приветствуются.
Github: github.com/one-more/sprute
Автор: one_more