[Из песочницы] Sprute.js. Ещё один изоморфный JavaScript фреймворк

image
Картинка для привлечения внимания

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

Комментарии (10)

  • 3 августа 2016 в 14:38

    0

    В вашем фреймворке реализована реактивность? Если да, то как?
    • 3 августа 2016 в 15:56

      0

      Что вы имеете в виду под реактивностью?
      • 3 августа 2016 в 16:23

        +1

        Привет, claygod недавно интересовался изоморфным фреймворком RiftJS, который использует cellx для реактивности. Если захотите встроить cellx в свой фреймворк — спрашивайте, попробую помочь, чем смогу)) Ну и сам RiftJS стоит поковырять, там много интересных идей, я его к сожалению забросил в пользу чисто клиентского фреймворка.

  • 3 августа 2016 в 14:46

    0

    оперделенной привилегией
    За что Вы ее так?)
    • 3 августа 2016 в 15:55

      0

      исправил)
  • 3 августа 2016 в 14:46

    0

    как я понял это js фреймворк исключительно для генерации страниц на сервере? А что на клиенте делать?
    • 3 августа 2016 в 16:00

      0

      На клиенте страницы рендерятся так же, как и на сервере. Шаблонизатор является изоморфным.
  • 3 августа 2016 в 14:58

    +1

    На демо-сайте навигация через хэшбенг и не работает
  • 3 августа 2016 в 16:02

    0

    Кажется, сайт накрыл хабраэффект. У меня ничего не работает.
  • 3 августа 2016 в 16:19

    0

    Посмотрел сайт (строительный). Походил по страницам. Решил проверить, как он хранит историю. Щёлкаю подряд пункты меню сверху вниз, потом нажимаю «Назад, назад, назад… » и сайт меня кидает по страницам в каком-то странном порядке…

© Habrahabr.ru