
Картинка для привлечения внимания
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