Архитектура приложения на Akili framework

    image


    Приветствую! Сегодня хотел бы разобрать пример реализации архитектуры сайта на фреймворке Akili. В данной статье речь пойдет не столько о системе компонентов, как о полноценной реализации приложения, с использованием роутинга, ajax запросов, хранилища и т.д. прямо из коробки, без внешних зависимостей.



    Для сборки проекта используется webpack, для компиляции babel с пресетами env, stage-2, stage-3, для отдачи статики node + express + akili-connect. Присутствует eslint.


    Структура файлов и описание


    Папка для доступа к статике из браузера /public/assets.
    Папка с фронтендом /src, входная точка /src/main.js.
    Поднятие сервера в app.js.


    Бэкенда как такового в примере нет. В файле app.js написана простая реализация отдачи статики + пару строк для серверного рендеринга.


    Демонстрационные данные берутся с сайта https://jsonplaceholder.typicode.com/.


    Структура фронтенда состоит из трех основных частей:


    • components — папка с универсальными компонентами, которые можно использовать много раз в рамках приложения.
    • controllers — папка с уникальными компонентами, отвечающими за логику приложения: маршрутизацию и распределение данных.
    • actions — папка с функциями для получения и сохранения данных.

    И трех второстепенных:


    • fonts – общая папка с шрифтами
    • img – общая папка с изображениями
    • styles – общая папка со стилями

    При этом три папки выше со статикой не являются уникальными и у каждого отдельного компонента могут быть свои собственные.


    Универсальный(простой) компонент полностью самостоятельный. Данные в него передаются через атрибуты, а обратно мы получаем результат через события. Он не должен работать с хранилищем. Этим занимаются компоненты-контроллеры. Контроллер — это связующее звено между хранилищем и простыми компонентами.


    src/main.js


    Наилучшим способом регистрировать компоненты во фреймворке является описание статического метода .define() у каждого компонента, и их вызов в точке входа.


    import App from './controllers/app/app';
    import Posts from './controllers/posts/posts';
    import PostEdit from './controllers/post-edit/post-edit';
    import Users from './controllers/users/users';
    import PostCards from './components/post-cards/post-cards'
    import PostForm from './components/post-form/post-form'
    import UserCards from './components/user-cards/user-cards'
    
    App.define();
    Posts.define();
    PostEdit.define();
    Users.define();
    PostCards.define();
    PostForm.define();
    UserCards.define();

    Для того чтобы совершать ajax запросы мы используем сервис request.


    import request, { Request } from 'akili/src/services/request';

    request.addInstance('api', new Request('https://jsonplaceholder.typicode.com', { json: true }));

    Обратите внимание на интересную деталь. По умолчанию, объект request сам является экземпляром класса Request. И уже с помощью него можно было бы делать любые запросы. Но гораздо удобнее создавать для каждого направления запросов отдельный экземпляр со своими настройками. В данном случаи, для работы с api jsonplaceholder.typicode.com мы создали отдельный.


    Теперь мы в любом месте можем использовать его, импортировав лишь объект request, например:


    request.use.api.get('/posts').then(res => console.log(res.data));

    Запрос будет направлен на https://jsonplaceholder.typicode.com/posts, в заголовках с типом контента json, и в ответе мы сразу получим объект, вместо строки.
    Более подробно об ajax запросах тут.


    Далее в нашем файле мы видим следующий строки:


    import store from 'akili/src/services/store';

    window.addEventListener('state-change', () => store.loader = true);
    window.addEventListener('state-changed', () => store.loader = false);

    Начнем с объекта store. Это хранилище нашего приложения. Здесь можно хранить любые данные. При этом данное хранилище автоматически синхронизируется со всеми местами, где нужны какие-либо изменения. Нужно всего лишь изменить необходимое свойство. В строках выше мы как раз, при определенных событиях, меняем свойство loader, на которое подписан один из компонентов, отображающий прелоадер.


    События state-change и state-changed не являются стандартными для window. Их вызывает роутер фреймворка. Первое, перед любым изменением в адресной строке браузера, второе, сразу после него. Это нам нужно для работы прелоадера. Об этом чуть позже.


    Далее происходит инициализация роутера и фреймворка после загрузки DOM.


    document.addEventListener('DOMContentLoaded', () => {
      router.init('/app/posts', false);
      Akili.init().catch((err) => console.error(err));
    });

    src/controllers/app/app.js


    В этом файле описан корневой контроллер. В нем мы обозначаем первый уровень маршрутизации для отображения хедера в шаблоне и точки входа для вложенных маршрутов.


    import './styles/app.scss'
    import Akili from 'akili';
    import router from 'akili/src/services/router';
    
    export default class App extends Akili.Component {
     static template = require('./app.html');
    
     static define() {
       Akili.component('app', this);
    
       router.add('app', '^/app', {
         component: this,
         title: 'Akili example site'        
       });
     }
    
     compiled() {
       this.store('loader', 'showLoader');
       this.store('posts', posts => this.scope.post = posts.find(p => p.selected));
     }
    }

    Давайте пройдемся по коду выше. Сначала мы подгружаем стили к данному компоненту. Все статические файлы конкретного компонента, стили, изображения, шрифты, хранятся в его личной папке /src/controllers/app, а не общей.


    Дальше идет объявление компонента. Метод .define() не является обязательным, но это очень удобный способ для настройки каждого отдельного компонента. В нем мы описываем все действия, которые необходимы для работы, и потом вызываем его в точке входа (src/main.js).


    Akili.component('app', this); 

    Строка выше регистрирует компонент под тэгом app, чтобы мы могли использовать его в шаблоне. Дальше идет добавление маршрута в роутер и.т.д.


    .compiled() — один из методов лайвцикла компонента, который вызывается после компиляции. В нем происходят две подписки на хранилище. Об одной из них мы говорили ранее:


    this.store('loader', 'showLoader');

    Этой строкой мы связали свойства хранилища loader и свойство скоупа текущего компонента showLoader. По умолчанию, связь создается в обе стороны. Если поменяется store.loader, то мы получим изменения в scope.showLoader и наоборот.


    src/controllers/app/app.html


    Здесь находится шаблон компонента-контроллера app.
    Мы его указали как статическое свойство template в компоненте.


    static template = require('./app.html');

    Рассмотрим интересный кусок из шаблона:


    <img
      src="./img/logo.svg"
      width="60"
      class="d-inline-block align-middle mr-1 ${ utils.class({loader: this.showLoader}) }"
    >

    Это изображение логотипа. Оно же и прелоадер. Если к нему добавить класс loader, то изображение начнет крутиться. Теперь должна быть понятна вся цепь событий связанных с прелоадером. В src/main.js мы подписались на два события. Перед изменением адресной строки мы меняем store.loader на true. В этот момент свойство showLoader в скоупе компонента App тоже станет true, и выражение utils.class({loader: this.showLoader}) вернет класс loader. Когда загрузка будет завершена, все поменяется на false и класс исчезнет.


    Еще важный кусок:


    <div class="container pb-5">
     <route></route>
    </div>

    route — специальный компонент, в который подгружается шаблон соответствующего по уровню вложенности маршрута. В данном случаи, это уже второй уровень. То есть любой маршрут-наследник от app будет подгружен сюда. А сам app был загружен в route, который был указан в body в /public/main.html.


    src/controllers/posts/posts.js


    Здесь описан компонент-контроллер постов.


    import Akili from 'akili';
    import router from 'akili/src/services/router';
    import store from 'akili/src/services/store';
    import { getAll as getPosts } from '../../actions/posts';
    
    export default class Posts extends Akili.Component {
     static template = require('./posts.html');
    
     static define() {
       Akili.component('posts', this);
    
       router.add('app.posts', '/posts', {
         component: this,
         title: 'Akili example | posts',
         handler: () => getPosts()
       });
     }
    
     created() {
       this.scope.setPosts = this.setPosts.bind(this);
       this.scope.posts = store.posts;
     }
    
     setPosts(posts = []) {
       store.posts = this.scope.posts = posts;
     }
    }

    Многое вам уже знакомо, но есть и новые моменты. Например, для указания вложенности мы используем точку в названии маршрута: app.posts. Теперь posts наследуется от app.


    Также, при объявлении маршрута мы указали функцию handler. Она будет вызвана если пользователь попадет на соответствующий url. В нее, в качестве аргумента, будет передан специальный объект, где хранится вся информация о текущем транзите. То что мы вернем в этой функции, тоже попадет в этот объект. Ссылка на объект транзита находится в router.transition и доступна везде.


    В примере выше мы взяли данные из хранилища:


    this.scope.posts = store.posts;

    Потому что наша функция .getPosts() заодно его туда сохранила, но мы могли взять данные и из транзита:


    this.scope.posts = router.transition.path.data;

    Такой вариант вы можете увидеть в контроллере users.


    Хотелось бы еще заметить, что методы компонента не находятся в области видимости его шаблона. Чтобы вызвать какую-либо функцию в шаблоне, нужно добавить ее именно в скоуп шаблона:


    this.scope.setPosts = this.setPosts.bind(this);

    src/controllers/posts/posts.html


    Это шаблон постов. Основная задача здесь — отобразить список постов. Но поскольку данный компонент является контроллером, то мы не будем делать этого непосредственно здесь. Ведь список постов это нечто универсальное, мы должны иметь возможность использовать его где угодно. Поэтому он вынесен в отдельный компонент src/components/post-cards.


    <post-cards
      data="${ this.filteredPosts = utils.filter(this.posts, this.filter, ['title', 'body']) }"
      on-data="${ this.setPosts(event.detail) }"
    ></post-cards>

    Теперь мы просто передадим в компонент PostCards нужный массив, а он уже отобразит все как надо. Правда, у нас здесь есть еще поиск.


    <input class="form-control" placeholder="search..." on-debounce="${ this.filter = event.target.value }">

    <if is="${ !this.filteredPosts.length }">
     <p class="alert alert-warning">Not found anything</p>
    </if>

    Поэтому данные (this.posts) мы передаем отфильтрованные. Событие on-debounce кастомное. Оно возникает с задержкой по последнему нажатию клавиши в инпуте. Можно было бы использовать стандартное on-input, но при большом количестве данных это будет значительно менее производительно. Про события в целом тут.


    При изменении данных внутри PostCards, он вызовет кастомное событие on-data, обработав которое, мы сохраняем изменения постов в хранилище, вызвав this.setPosts(event.detail).


    src/controllers/post-edit/post-edit.js


    Здесь описывается компонент-контроллер страницы редактирования поста.
    Весь код анализировать смысла нет, поскольку в примерах выше почти все аналогично. Остановимся на отличии:


    router.add('app.post-edit', '/post-edit/:id', {
       component: this,
       title: transition => `Akili example | ${ transition.path.data.title }`,
       handler: transition => getPost(transition.path.params.id)
    });

    В данном маршруте мы указали динамический параметр id.
    Поэтому в функции-обработчике мы имеем доступ к его значению в transition.path.params.id. В данном случае это id поста, чтобы получить нужный.


    src/controllers/post-edit/post-edit.html


    Также как и со списком постов, здесь мы форму вынесли в отдельный компонент PostForm, чтобы можно было использовать ее много раз.


    <post-form post="${ this.post }" on-save="${ this.savePost(event.detail) }"></post-form>

    src/components/post-form/post-form.js


    Рассмотрим данный компонент.
    Обратите внимание на комментарии:


    /**
    * Universal component to display a post form
    *
    * {@link https://akilijs.com/docs/best#docs_encapsulation_through_attributes}
    *
    * @tag post-form
    * @attr {object} post - actual post
    * @scope {object} post - actual post
    * @message {object} post - sent on any post's property change 
    * @message {object} save - sent on form save
    */

    Это js-doc с некоторыми кастомными тэгами.


    • @tag — это название компонента при регистрации
    • @selector — точный селектор, описывающий элементы, подходящие под этот компонент
    • @attr — атрибут для передачи данных в компонент извне
    • @scope — свойство скоупа компонента
    • @message — сообщение, посылаемое при вызове кастомного события

    Комментарии в исходниках фреймворка написаны в таком же стиле.


    compiled() {
       this.attr('post', 'post'); 
    }

    В куске кода выше, мы создали связь между атрибутом post и свойством скоупа компонента post. То есть, если передать этот атрибут с каким-то значением, то тут же получим изменения в scope.post. При изменении же scope.post в компоненте, будет автоматически вызвано событие on-post.


    <post-form post=”${ this.parentPost }” on-post=”${ this.parentPost = event.detail }”>

    Если бы мы написали html код выше где-нибудь, то получилась бы двойная связь между родительским scope.parentPost и текущим scope.post.


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


    static events = ['save'];

    save() {
       this.attrs.onSave.trigger(this.scope.post);
    }

    В первой строке мы зарегистрировали кастомное событие. Метод .save() вызывается при клике на кнопку в форме. В нем мы триггерим наше зарегистрированное событие save и передаем новый пост


    <post-form post="${ this.post }" on-save="${ this.savePost(event.detail) }"></post-form>

    Этот кусок кода из шаблона контроллера PageEdit. То есть мы передали пост через атрибут post в компонент PostForm, а обратно получаем изменившийся, обработав on-save.


    src/actions


    Действия — это всего лишь функции для получения и сохранения данных. Для чистоты и удобства они вынесены в отдельную папку.


    Например, src/actions/posts.js:


    import request from 'akili/src/services/request';
    import store from 'akili/src/services/store';
    
    export function getAll() {
     if(store.posts) {
       return Promise.resolve(store.posts);
     }
    
     return request.use.api.get('/posts').then(res => store.posts = res.data);
    }
    
    export function getPost(id) {
     return getAll().then(posts => {
       let post = posts.find(post => post.id == id);
    
       if(!post) {
         throw new Error(`Not fount post with id "${id}"`);
       } 
    
       return post;
     });
    }
    
    export function updatePost(post) {
     return request.use.api.put(`/posts/${post.id}`, { json: post }).then(res => {
       store.posts = store.posts.map(item => item.id == post.id? {...item, ...post}: item);
       return res.data;
     });
    }

    Все достаточно просто. Три функции: для получения списка постов, получения конкретного поста и для обновления поста.


    Подведем итоги


    Файлы с компонентами пользователей рассматривать не будем, поскольку там почти вся логика схожа с вышеописанной.


    В этой статье я не хотел расписывать подробно о всех возможностях компонентной системы фреймворка, хоть это и очень важная составляющая. На сайте есть много примеров: реализация дерева, todo лист, setInterval, табы и.т.д. Документация также полна примерами и достаточно полная. Основной целью было показать как можно легко и быстро создать приложение на Akili.


    Что в итоге получаем, используя Akili:


    • Мощную и интуитивно-понятную компонентную систему, позволяющую стереть грань между разметкой и логикой приложения. Кроме того в нее можно легко обернуть любой сторонний модуль. Будь то, перетаскивания элементов, аккордеоны и прочее.
    • Хранилище для сохранения и распределения данных между компонентами приложения. А-ля redux, но еще проще. Куда еще проще редакса спросите вы? Смотрим ))
    • Маршрутизацию из коробки. Поддерживает весь основной функционал: наследование, динамические данные, шаблоны, работа с hash и без, изменение document.title, resolving данных, абстрактные маршруты, редиректы и многое другое.
    • Возможность совершать ajax запросы из коробки. Можно создавать разные инстансы со своими настройками. Наличие системы кэширования. Отправление любых типов данных, без предварительных танцев с бубнами и.т.д.
    • Серверный рендеринг. Правда на данный момент реализован с ограничением. Код выполняется и на сервере и на клиенте. В планах есть передача хотя бы части стэйта на клиент.
    • Отсутствие всего, что не предусматривают по умолчанию html и javascript. Никаких магических надстроек к разметке или коду.

    Комментарии 26

      0
      Спасибо за статью. Возникло несколько вопросов:

      Мощную и интуитивно-понятную компонентную систему, позволяющую стереть грань между разметкой и логикой приложения.

      А мы разве не должны их отделять?

      Кроме того в нее можно легко обернуть любой сторонний модуль. Будь то, перетаскивания элементов, аккордеоны и прочее.

      Можете описать подробнее? Как именно происходит синхронизация вашего реактивного стейта и тех изменений в DOM, которые может вносить какой-то 3rd-party модуль, например Jquery plugin.

      Куда еще проще редакса спросите вы?

      В каком месте Redux прост?

      resolving данных, абстрактные маршруты

      Можете пояснить по этим пунктам?

      Возможность совершать ajax запросы из коробки.

      Ну это скорее минус. Это реализовано отдельным модулем? Можно как-то выпилить удобно?

      Серверный рендеринг. Правда на данный момент реализован с ограничением. Код выполняется и на сервере и на клиенте. В планах есть передача хотя бы части стэйта на клиент.

      Гидрировать разметку на клиенте умеет? Нужно передавать весь стейт, а как иначе?

      Отсутствие всего, что не предусматривают по умолчанию html и javascript. Никаких магических надстроек к разметке или коду.

      В сравнении с чем?

        0

        Спасибо за интерес =)


        А мы разве не должны их отделять?

        Под стереть грань имеется ввиду декларативный подход к html разметке.


        class Hello extends Akili.Component {
          created() {
             console.log(this.scope);
          }
        }

        <hello> ${ console.log(this) } </hello>

        this.scope в компоненте тот же самый объект, что и this в разметке. Я считаю это очень удобно.


        Можете описать подробнее? Как именно происходит синхронизация вашего реактивного стейта и тех изменений в DOM, которые может вносить какой-то 3rd-party модуль, например Jquery plugin.

        Все биндинги в разметке привязаны к конкретным DOM узлам и не мешают работать с другими узлами. То есть можно легко совмещать компонентную систему фреймворка и стандартные фишки javascript. Вещи типа такого являются нормальными:


        class Hello extends Akili.Component {
          compiled() {
             if(this.attrs.hideDiv) {
               $(this.el).find('.jquery').hide();
             }     
          }
        }

        <hello hide-div="true">
          <div class="jquery"></div>
        </hello>

        Понятно, что это просто пример. Скрыть элемент можно гораздо "правильнее" в рамках фреймворка, но суть надеюсь передал.


        В каком месте Redux прост?

        А что в нем сложного? По сути обычный event emitter с парочкой методов )


        Можете пояснить по этим пунктам?

        Под resolving data имеется ввиду, что указывая маршрут, мы в его обработчике можем вернуть Promise с данными. И вложенные маршруты не будут обработаны, пока не зарезолвится текущий.


        router.add('app', '/app', {
          handler: (transition) => {
            return Promise.resolve('my app data')
          }
        });

        Абстрактные маршруты — это маршруты, у которых нет шаблона. То есть просто в обработчике делаешь что-то, что тебе нужно и все. Они могут быть частью иерархии маршрутов тоже.


        Ну это скорее минус. Это реализовано отдельным модулем? Можно как-то выпилить удобно?

        Можно просто не использовать его. Код этого сервиса занимает несколько килобайт.


        Гидрировать разметку на клиенте умеет? Нужно передавать весь стейт, а как иначе?

        Сейчас он просто выполняет код на сервере, потом происходит повторная инициализация фреймфорка уже на клиенте. В ближайших планах передавать еще кэш всех запросов с сервера и стейт хранилища. Но со всем остальным гораздо все сложнее.


        В сравнении с чем?

        В сравнении с другими фреймворками. Не хочется просто показывать пальцем )

        0
        Под стереть грань имеется ввиду декларативный подход к html разметке.

        Не понимаю в чем здесь стирание грани, но ОК. Только вопрос, вам не кажется что точно также делают все остальные фреймворки? Даже более интуйтивно. Зачем вообще использовать this в шаблонах, а просто не использовать данные из скоупа как они есть:
        class Hello extends Akili.Component {
          created() {
             this.scope.message = 'Hello world';
          }
        }
        

        <hello> ${ message } </hello>
        

        Это еще лаконичнее и удобнее.

        Все биндинги в разметке привязаны к конкретным DOM узлам и не мешают работать с другими узлами. То есть можно легко совмещать компонентную систему фреймворка и стандартные фишки javascript. Вещи типа такого являются нормальными:

        Честно говоря, я говорил о чем-то более разумном и реальном. Самый банальный пример, например есть у нас такая мощная штука как DataTables (знаю что jquery это фууу, но для примера сойдет). И в данных (по вашему в скоупе) у нас лежит массив данных для таблицы.
        this.scope.data = [{}, {}...];
        

        Как вы будете синхронизировать состояние данные в скоупе с теми манипуляциями DOM, которые производит DataTables и в обратном направлении.
        Например, мы добавили новый элемент в массив данных через ваш скоуп и DataTables также обновился.
        this.scope.data.push({});
        

        Или DataTables удалил элемент из DOM и он удалился из массива данных.

        А что в нем сложного? По сути обычный event emitter с парочкой методов )

        Такое ощущение, что вы сейчас ответили в контексте того, как Redux реализован изнутри. Тогда да, он крайне прост. Только юзеров вашего фреймверка не интересует простота реализации стора, если вы писали об этом. В использовании Redux очень сложен, так как весьма многословен и требует тонны бойлерплейта.

        Под resolving data имеется ввиду, что указывая маршрут, мы в его обработчике можем вернуть Promise с данными. И вложенные маршруты не будут обработаны, пока не зарезолвится текущий.

        Абстрактные маршруты — это маршруты, у которых нет шаблона. То есть просто в обработчике делаешь что-то, что тебе нужно и все. Они могут быть частью иерархии маршрутов тоже.

        Разве такое не умеет любой нормальый роутер? Например, PageJS:

        page('/app/*', (ctx, next) => {
            Promise.resolve('my app data').then((data) => {
                 ctx.data = data;
                 next();
            });
        });
        
        page('/app/posts', (ctx, next) => {
             console.log(ctx.data);
        });
        
        page('*', doSomething, oneMore, secondOne);
        

        Причем имхо концепт middleware намного гибче.

        Можно просто не использовать его. Код этого сервиса занимает несколько килобайт.

        Это много. Лучше вообще не ключать его в стандартную сборку и шипить как отдельный модуль.

        Сейчас он просто выполняет код на сервере, потом происходит повторная инициализация фреймфорка уже на клиенте. В ближайших планах передавать еще кэш всех запросов с сервера и стейт хранилища. Но со всем остальным гораздо все сложнее.

        Иными словами, когда код выполняется на клиенте он заново производит рендеринг всех компонентов, которые уже были отрендерены на сервере. Видимо и запросы дергает все. Тогда это фигня, а не поддрежка SSR.

        В сравнении с другими фреймворками. Не хочется просто показывать пальцем )

        Такать не надо, просто интересно, что вы имеете ввиду под «магическими надстроеками к разметке или коду», которых у вас, как я понимаю, нет. Можно просто пару примеров.

          0
          Это еще лаконичнее и удобнее.

          Дело в том, что в выражение можно добавлять глобальные переменные. Например, там уже есть event, utils, console и.т.д. А можно вообще любые свои:


          Akili.options.globals = { my: 1 }

          ${ my + this.x } 

          Как вы будете синхронизировать состояние данные в скоупе с теми манипуляциями DOM, которые производит DataTables и в обратном направлении.
          Например, мы добавили новый элемент в массив данных через ваш скоуп и DataTables также обновился.

          http://jsfiddle.net/vjarL1t6/50/
          Пара комментариев к примеру.


          • Я не нашел как в DataTables получить чистый измененный массив. Пришлось пройтись циклом лишний раз, чтобы очистить лишние ключи.
          • Нужно иметь ввиду, что если бы DataTables работал не с копией переданного массива, то вообще ничего не надо было бы делать. Данные обновлялись бы сами, поскольку все объекты скоупа это Proxy.

          В использовании Redux очень сложен, так как весьма многословен и требует тонны бойлерплейта.

          Не согласен. Его "сложность" мягко говоря преувеличена.


          Разве такое не умеет любой нормальый роутер

          Это много. Лучше вообще не ключать его в стандартную сборку и шипить как отдельный модуль.

          Это просто обычные сервисы, позволяющие реализовать SPA. Понятно, что в них не будет ничего сверх-нового, альтернатив масса. Смысл тут в том, что Akili это фреймворк. У человека должна быть возможность просто подгрузить один файл и написать полноценное приложение без зависимостей.


          Иными словами, когда код выполняется на клиенте он заново производит рендеринг всех компонентов, которые уже были отрендерены на сервере. Видимо и запросы дергает все. Тогда это фигня, а не поддрежка SSR.

          Я не концентрировался на этой задаче достаточно, но:


          • код на сервере исполняется полностью, без необходимости ничего переписывать/ добавлять. Пара строк и все готово.
          • Можно делать асинхронные операции в компонентах, они все тоже будет обработаны
          • Уже реализована система кэширования запросов. То есть в ближайшее время добавлю в SSR передачу этих данных, и запросы не будут выполняется на клиенте больше.
          • Что касается остального состояния, то это очень сложно сделать, может быть даже невозможно до конца. Потому что в идеале нам нужны не только данные, которые можно сериализовать и отправить.

          Такать не надо, просто интересно, что вы имеете ввиду под «магическими надстроеками к разметке или коду», которых у вас, как я понимаю, нет. Можно просто пару примеров.

          • react — jsx
          • angular1 — своя модульная система, DI
          • angular 2+ — своя модульная система, своя разметка, DI, typescript

          Это не значит, что все это не нужно и плохо. Но можно не хуже и без этого.

            0
            Дело в том, что в выражение можно добавлять глобальные переменные. Например, там уже есть event, utils, console и.т.д. А можно вообще любые свои:

            И что? Одно другому не мешает. Можно организовать приоритизацию резолвинга выражений. Если выражение не найдено в контексте текущего компонента, ищем в глобалах. Хотя в целом глобалов все же лучше избегать.

            Не согласен. Его «сложность» мягко говоря преувеличена.

            Сложность понятие относительное, соответственно сложно может быть по сравнению с чем-то. Можете привести пример какого-то популярного стора, который был бы сложнее и многословнее чем Redux?

            Смысл тут в том, что Akili это фреймворк. У человека должна быть возможность просто подгрузить один файл и написать полноценное приложение без зависимостей.

            Фреймворк от слова «каркас». Причем тут ajax-запросы? Да и роутинг тут, по сути, сбоку-припёку. Если у меня SPA по типу «serverless» или не требующее роутинга, тогда зачем мне все это? Фреймворк — это об архитектуре и структуре, компонентах и их взаимодействии, а не о запросах к серверу и роутинге. Имхо.

            Я не концентрировался на этой задаче достаточно, но:

            Похоже что так. Вы так и не рассказали что у вас с гидрацией (hydrate)? Без этого SSR становится кривой надстройкой. Если вдруг не в курсе, можете почитать здесь.

            код на сервере исполняется полностью, без необходимости ничего переписывать/ добавлять. Пара строк и все готово.

            Обычное дело, ничего нового. Все так умеют.

            Можно делать асинхронные операции в компонентах, они все тоже будет обработаны

            Хук resolved() пожалуй единственная интересная штука в вашем фреймворке. Действительно это бывает полезно. Однако я уже видел подобные вещи, обычно они именуются как prefetch(), но смысл тот же. Единственная проблема с этим подходом — это не всегда удобно, когда есть конкретный хук, где можно такое осуществить. Лично я для себя выработал более гибкий подход, который не ограничевает меня фетчингом в рамках какого-то хука.

            Уже реализована система кэширования запросов. То есть в ближайшее время добавлю в SSR передачу этих данных, и запросы не будут выполняется на клиенте больше.

            Под кэшированием запросов вы подразумеваете сбор ответов в некую единую структуру для ее отпраки на клиент вместе со страницей или же настоящее кэширование ответов с сервера?

            Что касается остального состояния, то это очень сложно сделать, может быть даже невозможно до конца. Потому что в идеале нам нужны не только данные, которые можно сериализовать и отправить.

            А какие еще данные нам нужны? Можете привести пример?

            react — jsx

            Просто вид шаблонизатора, частью React не является. Можно и без него, а можно и с ним в других фреймворках.

            angular1 — своя модульная система, DI

            Тогда не было другой модульной системы, так что сорян. DI — обычное дело, пришеднее из бекенда.

            angular 2+ — своя модульная система, своя разметка, DI, typescript

            Почему своя? Там вроде ES6 модули вполне работают. Под своей разметкой вы подразумеваете директивы или что? typescript — диалект, опять же не изобретениие Angular, многие с React на нем пришут.

            В целом, не вижу особых отличий и уж тем более преимуществ вашего фреймворка по сравнению с Vue, Ractive, Riot или Svelte. Все тоже самое, а местами даже хуже.

              0
              И что? Одно другому не мешает. Можно организовать приоритизацию резолвинга выражений. Если выражение не найдено в контексте текущего компонента, ищем в глобалах. Хотя в целом глобалов все же лучше избегать.

              Приоритезация уже и так есть. Каждый скоуп наследуется от родительского. Поэтому впихивание сюда еще и глобальных переменных нарушило бы логику.


              Под кэшированием запросов вы подразумеваете сбор ответов в некую единую структуру для ее отпраки на клиент вместе со страницей или же настоящее кэширование ответов с сервера?

              Будет сделано так, чтобы все http запросы выполнялись только один раз и во время серверного рендеринга. Ближе к делу я буду думать над точной реализацией. Но уже сейчас понятно, что это сделать не сложно.


              Похоже что так. Вы так и не рассказали что у вас с гидрацией (hydrate)? Без этого SSR становится кривой надстройкой. Если вдруг не в курсе, можете почитать здесь.

              А какие еще данные нам нужны? Можете привести пример?

              Например, сохранение в переменные/свойства: функций, классов, экземпляров классов и.т.д.


              class Hello extends Akili.Component {  
                created() {
                  this.scope.setSomething = this.setSomething.bind(this);
                }
              
                compiled() {
                   this.attr('x', fn => this.fn = fn);
                   this.setSomething();
                }
              
                setSomething() {
                   this.scope.data = this.fn();
                }
              }

              <hello x="${ () => 'test' }">
                <div on-click="${ this.setSomething() }">
                 ${ this.data }
                </div>
              </hello>

              Как гидрация тут поможет? Сервер рендерит и возвращает:


              <hello x="[object Function]">
                <div on-click="[object Event]">
                 test
                </div>
              </hello>

              Пользователь кликает на див и ?

                0
                Приоритезация уже и так есть. Каждый скоуп наследуется от родительского. Поэтому впихивание сюда еще и глобальных переменных нарушило бы логику.

                Стоп, у вас скоуп родительского компонента доступен в дочернем? А как же инкапсуляция и изоляция?

                Будет сделано так, чтобы все http запросы выполнялись только один раз и во время серверного рендеринга. Ближе к делу я буду думать над точной реализацией. Но уже сейчас понятно, что это сделать не сложно.

                Ну это конечно не кэш. Просто передача начального состояния данных. Без этого SSR также не работает нормально.

                Например, сохранение в переменные/свойства: функций, классов, экземпляров классов и.т.д.

                Не понимаю, сохранение куда? Зачем их сохранять в разметку? У вас либо какой-то супер уникальный подход, либо вы что-то делаете не так.

                Как гидрация тут поможет? Сервер рендерит и возвращает:

                А почему у вас сервер возвращает какую-то ересь? SSR должен рендерить обычный HTML, без ваших конструкций. А гидрация просто жизнено необходима, потому что никому не хочется рендерить приложение и на сервере и на клиенте. Уж извините.

                Создалось ощущение, что вы выбрали какой-то не удачный подход для реализации базовых вещей в своем фреймворке.
                  0
                  Стоп, у вас скоуп родительского компонента доступен в дочернем? А как же инкапсуляция и изоляция?

                  Это никак не мешает изоляции компонента. Зато является удобной фишкой для многих кейсов, когда изоляция не нужна.


                  Не понимаю, сохранение куда? Зачем их сохранять в разметку? У вас либо какой-то супер уникальный подход, либо вы что-то делаете не так.

                  А почему у вас сервер возвращает какую-то ересь? SSR должен рендерить обычный HTML, без ваших конструкций. А гидрация просто жизнено необходима, потому что никому не хочется рендерить приложение и на сервере и на клиенте. Уж извините.

                  Нет никаких конструкций. Это обычный html. Рендерить не хочется, но гидрация не сможет восстановить правильное состояние в некоторых случаях. Это конечно не значит, что ее реализовать совсем не нужно. На данный момент это достаточно трудоемкая задача, с не самым высоким приоритетом.

                    0
                    Это никак не мешает изоляции компонента.

                    С каких это пор доступность выражений из вышестоящего скоупа не мешает изоляции? Данные должны передаваться либо через пропсы (аттрибуты), либо через какой-то предсказуемый глобальный стор, либо компонент не изолирован.

                    Зато является удобной фишкой для многих кейсов, когда изоляция не нужна.

                    Если изоляция не нужна, то лучше использовать точечные и предсказуемые вещи, например вот так можно сделать в Ractive:

                    {
                        isolated: false
                    }
                    


                    Нет никаких конструкций. Это обычный html.

                    Кастомный тег hello (если это конечно не Web Component) и дерективы x и on-click — это не часть обычного HTML или каких-то управляющих конструкций, которые понятны браузеру. Да, спецификация в целом не запрещает всего этого, но это не значит что ваш сервер должен рендерить такие вещи. Именно поэтому у вас и возникают проблемы с передачей «функций, классов, экземпляров классов и.т.д.». Для того, чтобы ваш SSR стал рабочим, нужно присылать браузеру не это:

                    <hello x="[object Function]">
                      <div on-click="[object Event]">
                       test
                      </div>
                    </hello>
                    

                    А что-то вроде:
                    <div>
                      <div>
                       test
                      </div>
                    </div>
                    

                    А дальше клиентский код уже навешивает все эти обработчики и другие управляющие конструкции. И конечно же нет никакого смысла браузеру заново рендерить разметку, ее нужно гидрировать.

                    Рендерить не хочется, но гидрация не сможет восстановить правильное состояние в некоторых случаях. Это конечно не значит, что ее реализовать совсем не нужно. На данный момент это достаточно трудоемкая задача, с не самым высоким приоритетом.


                    Задача не из простых, это правда. Но если использовать нормальные фреймворки, они с ней вполне справляются. Главное чтобы клиентский и серверный код был общий, а стейт данных, которые использовались сервером для SSR были переданы на клиент. Однако, насколько я понял, в вашем случае она усложняется выбором кривых подходов к фреймворку в целом.

                    Если есть желание подробнее разобраться в этой теме, можете прочитать мои статьи из цикла «Разработка изоморфного RealWorld приложения с SSR и Progressive Enhancement»:

                    1. Введение
                    2. Hello World
                    3. Routing & Fetching
                    4. Компоненты и композиция


                    Демка тут. Туториал еще не завершен.
                      0
                      Главное чтобы клиентский и серверный код был общий, а стейт данных, которые использовались сервером для SSR были переданы на клиент. Однако, насколько я понял, в вашем случае она усложняется выбором кривых подходов к фреймворку в целом.

                      Это не касается конкретно моего фреймворка, а вообще любого.
                      В вашем стейте, который возвращается в демке только простые данные, которые можно вернуть строкой, поэтому кажется, что все ок.


                      И демка почему-то отрисовывается дважды. Не стал вникал с чем именно это связано, но явно не нормальное поведение.

                        0
                        Это не касается конкретно моего фреймворка, а вообще любого.
                        В вашем стейте, который возвращается в демке только простые данные, которые можно вернуть строкой, поэтому кажется, что все ок.

                        Для начала, там они возвращаются не строкой, а полноценным JS объектом в window.__DATA__. При большом желании он может содержать и фукнции, только смысла в этом нет.

                        Еще раз, при нормальном подходе, для гидрации вам нужно передать только динамические данные, которые были использованы во время SSR. Почти в 100% случаев это данные из API. Так что я опять же не понимаю в чем проблема?

                        Если же суть вашей поблемы заключается в некой пре-компиляции динамических выражений из шаблонов, тогда в моем примере это тоже решается просто — на сервере заранее (buildtime) строится AST на основе шаблонов, где все динамические выражения заранее пре-компилируются. Так что в runtime работы с шаблонами нет вообще, только с готовым AST. Это оптимальный подход.

                        И демка почему-то отрисовывается дважды. Не стал вникал с чем именно это связано, но явно не нормальное поведение.

                        Она не обрисовывается дважды. Я тоже обратил внимание на двойное мигание, если открыть страницу в табе браузера первый раз. Если же вы ее просто перезагрузите, то двойного мигания не будет. Связано это с анимацией перехода, которую я использую. Учитывая, как дешево она мне далась, я пока готов этим пожертвовать. Главное что нет повторного рендеринга.

                          0
                          Для начала, там они возвращаются не строкой, а полноценным JS объектом в window.DATA. При большом желании он может содержать и фукнции, только смысла в этом нет.

                          Они в любом случаи возвращаются с сервера строкой. Поэтому функции он содержать не может, как и экземпляры классов и некоторые другие вещи, которые могут быть в стейте.

                            0
                            Так то у вас вообще код на клиент строкой приходит.))) Конечно же отправляется только дефиниция, все что содается в рантайме на сервере должно просто пересоздаваться в рантайме на клиенте. Вообще не должно быть в стейте никаких экземпляров, это какая-то проблема вашей реализации.

                            Вы себе, кроме всего прочего, еще проблему с кросс-ссылками заработаете при таком подходе. Стейт должен быть POJO. Для сериализации простых JS объектов, вместе с функциями и инстансами стандатных конструкторов, можно использовать например serialize-javascript .

                            Короче проблему вы себе сами придумали, честно говоря.
          0
          ссылка на jsdiffle изменилась. Поправил чуть-чуть http://jsfiddle.net/vjarL1t6/55/
            0
            Примерно так я и думал. Также было бы интереснее посмотреть что-то более комплексное, например, можете прислать пример реализации такой вот интеграции: jsfiddle.net/pf8no0hn/14 (пример Ractive с использованием декоратора).
              0
              Не та ссылка: jsfiddle.net/pf8no0hn/15
                0
                  0
                  Вот и сравните реализацию и насколько ваш вариант многословнее. И ладно бы, если бы не было альтернатив, но их полно. Еще раз: Vue, Ractive, Riot, Svelte, все они умеет все то, что делает ваш фреймворк и даже больше и лучше.

                  Так что смысла использовать ваш фреймворк я, честно говоря, не вижу. Если я не прав и у вас есть какая-то киллер-фича (хотя бы одна), тогда поправьте меня пожалуйста.
                    0

                    Сравнил. Код на Ractive похож на конфигурацию. А в Akili выглядит гибким и расширяемым.


                    Если вам нравится строить структуру как это предлагает Ractive или Vue тот же, то смысла использовать Akili нет. Мне такой подход не очень близок.


                    К какому-то моменту я написал больше кол-во проектов на разных фреймворках на наемной работе. Но однажды, когда мне нужно было выбрать, что-то для реализации своего проекта, я долго думал и понял, что ни на чем из этого я не хотел бы писать.


                    Akili был сделан с основной целью — получать удовольствие от процесса.

                      0
                      Сравнил. Код на Ractive похож на конфигурацию. А в Akili выглядит гибким и расширяемым.

                      Вообще-то это называется декларативный код и он, в большенстве случаев, является более чистым, гибким и расширяемым, по сравнению с императивным. Если вам это не известно, жаль.

                      Ваш код очень императивен и это не есть хорошо. Но если вы считаете, что на том же Ractive нельзя писать также императивно, вы ошибаетесь. Только зачем присать так сложно, не красиво и многословно, если можно писать декларативно и просто?

                      Если вам нравится строить структуру как это предлагает Ractive или Vue тот же, то смысла использовать Akili нет. Мне такой подход не очень близок.

                      К какому-то моменту я написал больше кол-во проектов на разных фреймворках на наемной работе. Но однажды, когда мне нужно было выбрать, что-то для реализации своего проекта, я долго думал и понял, что ни на чем из этого я не хотел бы писать.

                      Akili был сделан с основной целью — получать удовольствие от процесса.


                      Боюсь что это типичные доводы велосипедиста. Дело ваше, но я так и не увидел хоть какую-то мало-мальски крутую киллер-фичу и смысла использовать ваш фреймворк кому-то кроме вас тоже.
                        0
                        А в чем киллер-фича любого другого фреймворка?

                        Вы когда машину покупаете, тоже спрашиваете продавца в чем киллер-фича или все-таки садитесь в салон, рассматриваете, пробуете покататься и в итоге выбираете ту, которая подходит именно вам?
                          0
                          Для примера, если посмотреть доки по Svelte, то можно подумать это тоже самое что и Vue + SFC . Но киллер-фича Svelte в том, что это компилятор и у него нет runtime. Компоненты компилируются во время buildtime в «stand-alone vanilla js». Размер бандла Svelte как правило получается в 2-3 раза меньше чем у Vue, а по бенчмаркам уступает только ванилле и inferno.

                          У Vue киллер-фича хотя бы в том, что они умудряются вбирать в себя все лучшие практики других фреймворков, при этом сохраняя простоту интерфейсов и довольно приемлемый вес. Ну и из «большой тройки» — это самый адекватный фреймворк.

                          Киллер-фичи Ractive вообще перечислять можно очень долго. Он умеет все что нужно.

                          Вы когда машину покупаете, тоже спрашиваете продавца в чем киллер-фича или все-таки садитесь в салон, рассматриваете, пробуете покататься и в итоге выбираете ту, которая подходит именно вам?


                          В отличие от большинства тех, кто прочел вашу статью, я как раз «заглянул в салон», а также внимательно посмотрел как «прокатились на ней» вы. Ничего особенного не увидел и если уж продолжать аналогию с машинами: купить ваш фреймворк это то же самое, что купить «Noname» авто вместо проверенный «Toyota», за те же деньги, но с меньшими функциями, да еще с плохой подвеской.

                          Я не увидел ничего что было бы сделано лучше, продуманнее, удобнее или инновационнее, чем у многих других фреймворков. Если сравнивать только с Angular и React, тогда ваш фреймворк имеет свою нишу. Проблема в том, что эта ниша давно полная уже, а вы не предложили ничего принципиально нового. Без обид.

                            0
                            Но киллер-фича Svelte в том, что это компилятор и у него нет runtime.

                            И что программист, которому надо просто на этом написать сайт или пользователь приложения от этого получают в итоге? Надо делать киллер-фичу ради киллер-фичи?


                            умудряются вбирать в себя все лучшие практики других фреймворков

                            Он умеет все что нужно

                            Похоже на "слова велосипедистов", как вы писали ранее ) Так можно написать про любой фреймворк, который человеку подходит для решения какой-то задачи.


                            что купить «Noname» авто вместо проверенный «Toyota»

                            Я специально не проигнорировал цитаты выше. Потому что весь их смысл как раз вот в этой, последней цитате. Как вы думаете кто-то еще "заглянул бы в салон" кроме вас, если бы, скажем, vue еще не был написан, но его бы сейчас выложил я? Можете не отвечать на этот риторический вопрос ))


                            Но я не отрицаю, что если написать что-то действительно инновационное, то это может стать популярным и у "нонейма" =)

                              0
                              И что программист, которому надо просто на этом написать сайт или пользователь приложения от этого получают в итоге? Надо делать киллер-фичу ради киллер-фичи?

                              Много всего. Программист получает возможность писать маленькие, быстрые, автономные компоненты, на чистом JS (после компиляции) без необходимости решать многие проблемы этого JS. Кроме того, программист получае статический анализ кода и много других плюшек. Продробнее можете почитать тут и тут. Вообще тема AoT-компиляции сейчас набирает популярность.

                              Похоже на «слова велосипедистов», как вы писали ранее ) Так можно написать про любой фреймворк, который человеку подходит для решения какой-то задачи.

                              Скорее это слова анти-велосипедиста))) Велосипедист пишет свой фреймворк только потому, что существующий использует двойные фигурные скобки, вместо одинарных, а это ущемляет его чувство прекрасного. Вот вы можете точно ответить, чего вам не хватило в том же Vue, чтобы писать свой фреймворк, который далает плюс-минус тоже самое?

                              Можете не отвечать на этот риторический вопрос ))

                              На самом деле я могу на него ответить. Феномен Vue очень прост — он появился вовремя. Дело в том, что изначальный Vue — это более примитивная калька с Ractive, который появился еще в 2012 года. Сравните:

                              var app = new Vue({
                                el: '#app',
                                data: {
                                  message: 'Hello Vue!'
                                }
                              });
                              


                              var app = new Ractive({
                                el: '#app',
                                data: {
                                  message: 'Hello Ractive!'
                                }
                              });
                              


                              Тогда все плотно сидели на jquery и такие вещи как virtual dom (в Ractive это тогда называлось parallel dom), SSR c гидрацией (в Ractive это называется enhance), «component-scoped» стили (есть из коробки) и другие инновации Ractive были людям не понятны.

                              Прошло время, лицекнига вложились в PR реакта, сейчас все не любят jquery и любят реакт. Но реакт крайне специфичен и привносит кучу всяких идеалогий, которые многим противят. Angular долго был аутсайдером, потому что никак не релизился. Ну и в целом он сложнее и скорее для энтерпрайза. Vue скопировал Ractive и появился как раз вовремя, как альтернатива.

                              Ваша проблема к том, что вы не только не написали ничего нового, но и появились не вовремя. Ractive был инновационный, но не своевременный и без денег на PR. Vue не придумал ничего нового, но сделал это вовремя. Svelte придумал новое, но пока не понятно вовремя ли. Akili не придумал ничего нового и появился слишком поздно. Не вижу шансов на успех.
                                0
                                Я с вами почти согласен ) Спасибо за фидбэк.

            0
            del

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое