
Картинка для привлечения внимания
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
Комментарии (38)
webmasterx
03.08.2016 14:46как я понял это js фреймворк исключительно для генерации страниц на сервере? А что на клиенте делать?
one_more
03.08.2016 16:00На клиенте страницы рендерятся так же, как и на сервере. Шаблонизатор является изоморфным.
claygod
03.08.2016 16:19Посмотрел сайт (строительный). Походил по страницам. Решил проверить, как он хранит историю. Щёлкаю подряд пункты меню сверху вниз, потом нажимаю «Назад, назад, назад… » и сайт меня кидает по страницам в каком-то странном порядке…
ArtFutureDev
01.12.2016 00:07+1Шикарная история, было интересно почитать. Большое спасибо, жду 2 часи ;)
claygod
04.08.2016 10:23+1@ one_more, вы же не будете каждому посетителю объяснять про якоря. Он будет щёлкать по меню и ожидать при щелчке на кнопку «Назад» или нажатии «Backspace», что попадёт в предыдущее состояние. Это простое юзабилити, и ничего больше.
napa3um
03.08.2016 17:06+4А нужна ли изоморфность в 2016-ом? Ведь с точки зрения современных подходов построения веб-приложений (SPA/PWA) это костыль для поисковиков, ради которого приходится корректировать всю архитектуру приложения (к слову, паук от Google уже не требует такого костыля). Проще этот костыль отдать на откуп Prerender.io, чем вносить лишние ограничения в архитектуру. ИМХО.
one_more
03.08.2016 17:28основная цель феймворка как раз состоит в том, чтобы реализация изоморфности не накладывала никаких ограничений на архитектуру.
napa3um
03.08.2016 17:29+3Абстракции изоморфности фреймворка рано или поздно «протекут» в виде ограничений, даже если их сразу и не было заметно.
one_more
03.08.2016 17:50Будет такое. Но архитектура фреймворка позволяет разбивать код на изоморфный и специфичный для каждой платформы — подобные ограничения можно обходить без использования костылей.
BlessMaster
03.08.2016 18:01+3Помимо гугла есть и другие пауки, причём, не только поисковые.
Хотя насчёт изоморфности не могу не согласиться в том, что это протекающая абстракция.
Речь максимум может идти о частичной изоморфности — у клиента и сервера разные задачи и разные возможности.napa3um
03.08.2016 18:07> Помимо гугла есть и другие пауки, причём, не только поисковые.
И зачем о них тревожиться?
Terras
01.12.2016 12:46Человек ставит паузу — и ты её снять не можешь. В итоге, у тебя просто заканчивается терпение и выходишь.
Tramway
03.08.2016 18:14В некоторых случаях от вас могут потребовать, чтобы first paint был в таком районе, когда еще js толком то не распарсился. Этот случай довольно редкий, но под него нужны инструменты и решения для server-side рендера.
napa3um
03.08.2016 18:19Я уверен, что в любые времена и при любых фреймворках останется место некорректно поставленным, но добросовестно выполненным архитектурным задачам.
tenbits
03.08.2016 18:43У "изоморфности" двя плюса с которыми, как по мне, сложно спорить: переиспользование кода и скорость отображения контента.
napa3um
03.08.2016 18:54+3Первый плюс, как показывает практика, касается только разделяемых валидаторов моделей (хотя в большинстве приложений таки часто получается, что серверные и клиентские модели данных отличаются), а второй плюс нивелируется кешированием скриптов и ассетов SPA/PWA на клиенте. На более-менее посещаемом ресурсе скорее клиент отрисует картинку быстрее, чем это сделает сервер. Ну а статичные страницы, независимые от логина (которые нужно отдавать анонимным пользователям, посетившим ресурс впервые) просто оформляют вне SPA, в виде так называемых лэндинг-страниц.
tenbits
03.08.2016 20:05Ну не скажите, а утилиты, хэлперы, сервисы? А если строить систему на интерфейсах с внедрением зависимостей? А юнит-тесты? В любом случае, валидаторами дело не заканчиваеться, поэтому зря вы ими ограничиваетесь. По второму пункту: с чего вы взяли что клиент отресует картинку быстрее? Даже если скрипты закешированы их нужно вытащить-распарсить; шаблоны закешированы? и сново достать распарсить, без этого шаблонизатор не начинает создавать dom/html; также для рендеринга нужны данные, и за ними обычно идут по http. Как бы вы не противились, если трезво оценить ситуацию, это действительно два плюса. Я не утверждаю, что это прям всем нужно и без этого никак, но от этого они не перестают быть плюсами)
napa3um
03.08.2016 20:10> Ну не скажите, а утилиты, хэлперы, сервисы?
Если считать, что на сервере нет рендеринга, то они оказываются ненужными.
> Я не утверждаю, что это прям всем нужно и без этого никак, но от этого они не перестают быть плюсами)
Если что-то кажется вам (или вашей команде разработчиков) более привычным, чем какие-то абстрактные идеалы и сферические архитектуры в вакууме, то нет особого смысла пытаться оправдать свой выбор технологий чем-то ещё.
DarthVictor
03.08.2016 21:55+3Во-первых, почему вы думаете, что prerender.io — это меньший костыль? По факту вы запускаете браузер на лету, ждете его ответа и потом отдаете. Рендеринг JS, что на клиенте, что на сервере — все-таки документированная возможность у браузера и у Ноды. Вот еще более развернуто.
Во-вторых, индексация сайтов с клиентским рендерингом (то есть в случае, когда от сервера приходит JSON) работает только у гугла и недавно. Даже если забыть, что в РФ у него даже не половина поискового рынка, то подгрузка содержимого нужна еще фейсбуку, вконтакту, скайпу, твиттеру и еще хреновой туче сайтов.
Ну и в третьихnapa3um
03.08.2016 22:00+21) Потому что в минимальной степени влияет на архитектуру приложения. 2) Да, потому о prerender и вспомнили, пока остальные поисковики не подтянутся. 3) https://github.com/prerender/prerender.
one_more
06.08.2016 04:37+1Какое-то у вас костыльно-ориентированное программирование получается — костыль для браузера, костыль для сервера; вместо того, чтобы использовать нормальный инструмент, который хорошо справляется с работой в обоих случаях
gyesa
03.08.2016 22:09Прошу прощения за оффтоп, ну и, конечно, это ни в коем разе не претензия, но просто когда я зашёл на гитхаб-страницу проекта и увидел там всё на русском языке, почему-то возник серьёзный диссонанс! Видимо, настолько привык к тому, что всё-всё-всё англофицируется, даже если проект делается локально, при этом разработчики с английским никак не связаны. Текста на гитхабе у вас пока не так много, думаю, вы будете расширяться, поэтому, как мне кажется, лучше бы вам уже подумать про более распространённый язык в сфере IT — тогда могут к вашему проекту появиться «глаза» и из других стран :)
PS. Спрут у вас, конечно, милашка! Только, вот, в английском такого слова нет (там это обычно octopus), но, думаю, это вы так специально :)one_more
03.08.2016 22:15Насчет sprute — специально, вы правы. А насчет английского — мне пока уровень владения не позволяет — много ошибок в письменной речи. Собираюсь переписать когда буду владеть языком на должном уровне.
gyesa
03.08.2016 22:20Не в моём праве вас поучать или к чему-то принуждать, но, как преподаватель, я всё-таки посоветовал бы вам перевести все имеющиеся части описания проекта на английский. Боитесь большого кол-ва ошибок? А вы не бойтесь — иначе они никогда не исчезнут. Practice makes perfect — пишите, пишите, пишите, т.е. тот самый «должный уровень» — это дело практики и времени, а не удачного стечения обстоятельств! А люди вряд ли будут критично относиться к ошибкам (английским в мире владеет такое несчётное кол-во человек, что какого только уровня он ни бывает!) — напротив, могут исправить какие-то погрешности, PR им в помощь :) Так что моё мнение — дерзайте!
claygod
В вашем фреймворке реализована реактивность? Если да, то как?
one_more
Что вы имеете в виду под реактивностью?
Riim
Привет, claygod недавно интересовался изоморфным фреймворком RiftJS, который использует cellx для реактивности. Если захотите встроить cellx в свой фреймворк — спрашивайте, попробую помочь, чем смогу)) Ну и сам RiftJS стоит поковырять, там много интересных идей, я его к сожалению забросил в пользу чисто клиентского фреймворка.
claygod
Riim, именно это я и имел в виду. Однако возможно, что one_more эту парадигму программирования не применял.
one_more
Архитектура фреймворка позволяет легко добавить такой функционал, если есть необходимость, используя любую реализацию (cellx например). Нет необходимости встраивать это по умолчанию на мой взгляд (это может быть не нужно в некоторых проектах). Я сосредоточился в основном на гибкости и возможности расширения и стараюсь сохранять фреймворк максимально компактным.
Alex10
Реактивность еще очень часто упоминают в контексте meteor.js