В статье сравниваются два подхода к созданию веб интерфейса пользователя.

  • Один подход - это современные фреймворки с компонентным подходом, который инкапсулирует HTML, js, css.

  • Второй подход к разработке веб-интерфейса аналогичен разработке интерфейса пользователя десктопного приложения.

1. Эволюция веб приложений

1.1. HTML

Сначала нам было достаточно статических страниц, написанных с помощью языка разметки html.

<div id="app">
    <h1>html - это просто.</h1>
</div>

1.2. HTML + JAVASCRIPT

После, к статичным страницам html добавили javascript.

<div id="app">
    <h1>Заголовок</h1>
</div>

<script>
    var h1 = document.querySelector('#app > h1');
    h1.innerText = 'html + js - это гибко.'
</script>

1.3. JAVASCRIPT FRAMEWORK

Потом, объединили html и javascript и появились frontend framework. Вот, на примере одного из популярных фреймворков Vue.js.

<div id="app">
    <h1>Фреймворк {{framework}} один из лучших.</h1>
</div>

<script src="https://unpkg.com/vue"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            framework: 'Vue.js'
        }
    })
</script>

Удобство современных фреймворков - это реактивность, т.е. связанность данных, когда данные на странице

<h1>Фреймворк {{framework}} один из лучших.</h1>

связаны с данными компонента

app.framework = 'Vue js';

2. Два подхода

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

А что, если использовать другой подход? А что если на первое место поставить javascript? Конечно, мы потеряем все то многообразие вариантов красочного и анимированного оформления веб-форм, но приобретем лаконичность и привычную атмосферу разработки десктопного приложения. Этот подход позволит создавать веб приложения на основе готовых библиотек элементов управления. А почему бы и нет, ведь десктопные приложения обходятся же ограниченным набором компонент (это не означает небольшим). И созданные приложения будут не менее красивыми и функциональными, ведь можно использовать множество тем оформления.  

Попытаемся сравнить два подхода.  

Первый подход будет представлять один из популярнейших фреймворков [vue.js](https://ru.vuejs.org/).  

Второй подход будет представлять один новый фреймворк [ui-organizer.ru](https://ui-organizer.ru). Собственно, это прототип библиотеки, но вполне рабочий прототип.

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

3. Разработка простой компоненты

3.1. Простейший компонент Vue js

В Vue.js компоненты необходимо разрабатывать самому, либо использовать сторонние библиотеки. Для тех, кто знает html и javascript, очень просто создать нужный компонент.  

index.html

<div id="app">
    <h1>Фреймворк {{framework}} один из лучших.</h1>
</div>

index.js

const App = {
  data() {
    return {
      framework: 'Vue.js'
    }
  }
}

Vue.createApp(App).mount('#app');

Как видно из примера, программный код написан на html в файле index.html и на javascript в файле index.js. Здесь опускаются вопросы компиляции и сборки приложения, так как в Vue.js процесс сборки отличается в зависимости используете ли вы typescript или javascript. В примере программный код в index.js написан на javascript, поэтому его не надо компилировать и, вообще, вы можете поместить его прямо на страницу index.html вместе с программным кодом, написанном на html. Да, программный код пишем прямо на html. Это будет заметно на более сложных примерах.  

3.2. Простейший компонент ui-organizer

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

main.ts  

var form: IForm = <IForm>{
    type:'IForm',
    name:'form',
    flex: Flex.fixed,
    grouping: Grouping.vertical
    elements: [
        <IDataElement>{
            type: 'IDataElement',
            name: 'dataElement',
            bindingProperty: 'el',
            onAfterSetData: async function(form: IForm, elem: IDataElement, data: any){
                elem.value = `Библиотека ${data} - другой фреймворк.`;
                return true;
            }
        }
    ]
}
AppManager.add([form]);
AppManager.open('form', {"el":"ui-organizer"}, undefined);

Как видите здесь html вообще не трогаем. И нам пришлось создать форму и добавить на нее элемент IDataElement. Html будет построен автоматически. Программа построит следующий html:  

<div>Библиотека ui-organizer - другой фреймворк.</div>

Обращаем внимание на то, что программный код написан на typescript. Это требует компиляции программы, чтобы получить чистый javascript. Реализовать второй подход, наверное, невозможно без использования typescript - это снижает риски ошибок, ведь нам важно не написать очень хитрый код, с использованием всех возможностей javascript, а создать надежное, простое в сопровождении приложение.  

4. Разработка компоненты посложнее

4.1. Компонента "Список задач" Vue js

Сделаем список задач с кнопкой.  

Сначала создаем html. Как вы можете заметить, здесь уже язык разметки превратился в полноценную программу с циклами, событиями и др.  

<div id="list-rendering">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
  <button v-on:click="reverseMessage" @click="setFlag">Перевернуть сообщение</button>
</div>

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

interface ITodo {
    text:string
}

const ListRendering = defineComponent({
  data() {
    return {
      todos: [
        { text: 'Learn JavaScript' },
        { text: 'Learn Vue' },
        { text: 'Build something awesome' }
      ] as Array<ITodo>,
      isReversed: false
    }
  },
  methods: {
    reverseMessage(): void {
      this.todos.forEach(item =>{
          item.split('')
            .reverse()
            .join('')
      })
    },
    setFlag(event):void {
      this.isReversed = !this.isReversed;
    }
  }
})
Vue.createApp(ListRendering).mount('#list-rendering')

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

4.2. Форма со "Списком задач" ui-organizer

Отличие второго подхода (ui-organizer) в том, что мы не создаем новый элемент управления, а используем существующие, чтобы решить задачу. В данном случае используем два элемента управления IList и IButton, причем элемент управления IList мы доработали добавив свойство isReversed и метод reverseText. У элемента IButton добавили реализацию метода onClick .  

На самом деле IList и IButton это интерфейсы, а мы создаем объекты, реализующие эти интерфейсы.  В тексте упростили и написали просто: "элементы управления IList и IButton".  

AppManager открывает форму 'simpleForm' и передает ей массив элементов в формате JSON.

interface IMyList extends IList {
    isReversed: boolean;
    reverseText: ()=>void;
}

var form: IForm = <IForm>{
    type: 'IForm',
    name: 'simpleForm',
    flex: Flex.flexible,
    grouping: Grouping.vertical,
    elements: [
        <IMyList>{
            type: 'IList',
            name: 'list',
            isReversed: false, // Добавленное свойство
            itemsElement: <IStr>{
                type: 'IStr',
                name: 'str',
                bindingProperty: 'elem',
            },
            reverseText():void {// Добавленный метод
                this.items.forEach(item => {
                    var str = <string>item.element.value;
                    item.element.value = str.split('')
                        .reverse()
                        .join('')   
                });
                this.isReversed = !this.isReversed;
            }
        },
        <IButton>{
            type: 'IButton',
            name: 'reverse',
            caption: 'Перевернуть текст',
            onClick: async function (form) {
                (<IMyList>form.getElement('list')).reverseText();
                return true;
            }
        }
    ]
}
AppManager.add([form]);
AppManager.open('simpleForm', [
    { elem: "второй подход" },
    { elem: "новый фреймворк" },
    { elem: "еще один компонент" },
], undefined);

5. Компоненты и повторное использование

5.1. Повторное использование в Vue js

В Vue компоненты - это части приложения, которые можно использовать повторно. Вот например:  

Vue.component('todo-item', {
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>'
})

На Html странице укажите, где хотите вставить компонент:  

<div id="app">
  <ol>
    <todo-item
      v-for="item in groceryList"
      v-bind:todo="item"
      v-bind:key="item.id"
    ></todo-item>
  </ol>
</div>

Здесь мы указали, что для каждого item в groceryList выводим на экран элемент todo-item.  

В приложении передаем в данные этот самый groceryList.

var app = new Vue({
  el: '#app',
  data: {
    groceryList: [
      { id: 0, text: 'Овощи' },
      { id: 1, text: 'Сыр' },
      { id: 2, text: 'Что там ещё люди едят?' }
    ]
  }
})

5.2. Повторное использование в ui-organizer

Здесь повторное использование - это класс элемента, мы можем создать сколько угодно объектов этого класса. Перепишем предыдущий пример из раздела 4.2.  

class MyList extends UIList {
    constructor(_name:string, _bindingProperty: string){
      this.name = _name;
      this.isReversed = false;
      this.itemsElement = <IStr>{
        type: 'IStr',
        name: 'str',
        bindingProperty: _bindingProperty
      };
    }
    reverseText():void {// Добавленный метод
        this.items.forEach(item => {
            var str = <string>item.element.value;
            item.element.value = str.split('')
                .reverse()
                .join('')   
        });
        this.isReversed = !this.isReversed;
    }
}
var form: IForm = <IForm>{
    type: 'IForm',
    name: 'simpleForm',
    flex: Flex.flexible,
    grouping: Grouping.vertical,
    elements: [
        <IList>(new MyList('list', 'elem')),
        <IButton>{
            type: 'IButton',
            name: 'reverse',
            caption: 'Перевернуть текст',
            onClick: async function (form) {
                (<MyList>form.getElement('list')).reverseText();
                return true;
            }
        }
    ]
}
AppManager.add([form]);
AppManager.open('simpleForm', [
    { elem: "второй подход" },
    { elem: "новый фреймворк" },
    { elem: "еще один компонент" },
], undefined);

Здесь мы создали класс MyList и наследовали его от предопределенного класса UIList, который, в свою очередь, реализует интерфейс IList. При описании формы мы просто создали экземпляр MyList, где в конструктор передали название элемента 'list' и название свойства 'elem', из которого будут подставляться данные.  

Как вы могли заметить, в библиотеке ui-organizer не выделено понятие "компонент", так как, в объекте уже инкапсулированы html, данные и поведение в виде методов.

6. Данные и методы

6.1. Данные и методы компонента Vue js

В экземпляре Vue доступны данные и некоторые предопределенные методы:  

var user = { name: 'Иван' };
var vm = new Vue({
  el: '#app',
  data: user
})
vm.name === user.name; // => true
vm.name = 'Дмитрий';
user.name // => Дмитрий

vm.$data === user // => true
vm.$el === document.getElementById('app') // => true
vm.$watch('name', function (newValue, oldValue) {
  // Этот коллбэк будет вызван, когда изменится `vm.name`
})

Здесь компоненту vm  мы передали объект user. И теперь имеем доступ к свойствам этого объекта через компонент vm.name. Это, так называемая система, реактивности, - наверно, ключевое преимущество современных фреймворков.  

В примере также показано использование служебных свойств $data и $el и служебного метода $watch. Конечно, служебных свойств и методов гораздо больше.  

6.2. Данные и методы элемента управления ui-organizer

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

var user = {
  firstName: 'Иван'
}

var form: IForm = <IForm>{
    type:'IForm',
    name:'form',
    grouping: Grouping.vertical
    elements: [
        <IDataElement>{
            type: 'IDataElement',
            name: 'dataElement',
            bindingProperty: 'firstName',
        }
    ]
}

AppManager.add([form]);
AppManager.open('form', user, undefined);
AppManager.activeForm.getData({}).firstName; //=>'Иван'

var dataElem = AppManager.activeForm.getElement('dataElement');
/*№1*/AppManager.activeForm.setData({firstName :'Дмитрий'}); //=>'Дмитрий'
/*№2*/dataElem.getData({}).firstName == 'Дмитрий'; //=>true
/*№3*/dataElem.setData({firstName:'Михаил'});
/*№4*/AppManager.activeForm.collectData({}).firstName; //=>'Михаил'
AppManager.activeForm.dom; //=> HTMLElement

В библиотеке ui-organizer для элемента управления можем установить bindingProperty. В нашем примере bindingProperty элемента управления dataElement установлено в firstName. Это значит, что значение свойства firstName в передаваемых данных будет установлено этому элементу (см. №№1-2). Верно, также обратное, когда мы меняем данные элемента функция collectData формы возвращает новые данные (см.№№3-4).  

Здесь первоначальный объект остается неизменным. Для того, чтобы получить изменения в первоначальном объекте, необходимо функции getData передать этот первоначальный объект:  

AppManager.activeForm.getData(user);
user.firstName; //=>'Михаил'

Служебное свойство dom возвращает HTMLElement, связанный с этим элементом управления. У каждого элемента управления на форме свой DOMHTMLElement. У каждого элемента управления доступны служебные методы, такие как addClass, getElement и другие.

7. CSS и стилевое оформление

7.1. Работа с классами и стилями Vue.js

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

Описываем html:  

<div
  class="element"
  v-bind:class="{ readonly: isReadonly, error: hasError }"
></div>
data: {
  isReadonly: true,
  hasError: false
}

В описании компонента задаем данные, от изменения которых будет зависеть добавление классов к HTML элементу. Например, так как isReadonly = true, то класс readonly будет добавлен, а так как hasError = false, то класс error добавлен не будет. Таким образом, имеем:

<div class="element readonly"></div>

То же самое с inline стилями:  

<div v-bind:style="{ position: userPosition, font-size: fontSize + 'em' }"></div>
data: {
  userPosition: 'relative',
  fontSize: 1.5
}

Здесь значение стилей pozition и font-size определяются соответственно переменными userPosition и fontSize, описанными в секции data.  

7.2. сcs и стилевое оформление ui-organizer

Библиотека включает в поставку два файла: page.css, который определяет компоновку элементов управления и style.css, который определяет стилевое оформление. Вы можете заменить style.css. Кроме этого, можно для элемента IGroup указать файл .css, который будет применен для этой группы элементов. Кроме того каждый элемент имеет два метода addClass и removeClass, которые добавляют и удаляют классы HTML элемента.

var form: IForm = <IForm>{
    type:'IForm',
    name:'form',
    grouping: Grouping.vertical,
    stylesheets: 'custom.css',
    elements: [
        <IDataElement>{
            type: 'IDataElement',
            name: 'dataElement',
            bindingProperty: 'firstName',
            isSomeClass: false;
            onClick: async function(){
                this.isSomeClass = !this.isSomeClass;
                if (this.isSomeClass) elem.addClass("someClass");
                else elem.removeClass("someClass");
            }
        }
    ]
}

AppManager.add([form]);
AppManager.open('form', undefined, undefined);

8. Обработка событий

8.1. Обработка событий vue js

<div id="app">
  <button v-on:click="warn('Что ты наделал???')">Нажми!</button>
</div>
new Vue({
  el: '#app',
  methods: {
    warn: function (message) {
      alert(message)
    }
  }
})

В vue js обработчики событий указываются прямо в html. И есть возможность подписываться на все события, возникающие в HTML элементе. В примере мы подписались на событие click html элемента button.  

Вы также можете генерировать свое событие:  

this.$emit('myevent');

и подписываться на него  

<my-component v-on:myevent="doSomething"></my-component>

8.2. Обработка событий ui-organizer

Подход в ui-organizer другой: вы не имеете доступа к событиям html элементов, но можете обрабатывать предопределенные для каждого элемента управления события и генерировать свои.  

Например, ниже написан обработчик события onAfterLoad, в котором задаются обработчики событий show и hide элемента управления IElement:  

export var baseElem: IElement = <IElement>{
    type: 'IElement',
    name: 'baseElem',
    caption: 'Базовый элемент',
    onAfterLoad: async function (form: IForm, elem: IElement, data: any) {
        elem.on("show", () => {
            elem.addClass('visible');
        });
        elem.on("hide", () => {
            elem.removeClass('visible');
        })
        return true;
    }
}

Можете генерировать свои события и подписываться на них:  

export interface ISomeElement<T=ISomeElementEvents> extends IElement<T>  {
    someProperty: string;
    setSomeProperty(val: string): void;
}

export interface ISomeElementEvents {
    someSetted(form: IForm, element: IElement): void,
}

export var elem: ISomeElement = <ISomeElement>{
    type: 'IElement',
    name: 'elem',
    someProperty: undefined,
    setSomeProperty: function (val: string) {
        someProperty = val;
        this.emit("someSetted", this.form, this);
    }
}

В последнем примере определены два интерфейса для облегчения написания программы на typescript.

9. Работа с формами

9.1. Работа с формами vue js

Для связи компонента vue js c элементом формы input имользуется директива v-model, которая указывается в html. Эта директива связывает данные компонента vue js с элементом ввода input.

<div id="app">
    <input v-model="userText" placeholder="текст">
    <p>Введённое сообщение: {{ userText }}</p>
</div>
new Vue({
  el: '#app',
  data: {
    userText: 'Текст'
  }
})

9.2. Работа с формами ui-organizer

Здесь для пользователя одинаково, что работать c простым элеметом IDataElement, со списком IList или полем ввода IProperty. Все является элементом формы. Программист не работает напрямую с элементом input или textArea, а использует элементы управления. Которым передаются данные как было показано в разделе 6.2.  

Форма с полем ввода и текстом (эхо, дублирующее поле ввода) будет выглядеть следующим образом:  

var form: IForm = <IForm>{
    type:'IForm',
    name:'form',
    grouping: Grouping.vertical
    elements: [
        <IProperty>{
            type: 'IProperty',
            name: 'text',
            bindingProperty: 'userText',
            placeHoleder: 'Введите текст'
        },
        <IStr>{
                type: 'IStr',
                name: 'str',
                bindingProperty: 'elem',
        }
    ],
    onAfterLoad: async function(){
      let input: IProperty = this.getElement('text') as IProperty;
      let srt: IStr = this.getElement('str') as IStr;
      input.on('change', (form, elem, val)=>{
        str.value = val;
      })
    }
}

AppManager.add([form]);
AppManager.open('form', {"userText":"Просто текст"}, undefined);

10. Отладка

Как и vue js, так и ui-organizer имеют возможность отладки в VSCode и в браузере.  

Для отладки в VSCode нужно установить соответствующее расширение отладчика, например Debugger Chrome.  

Для отладки в браузере необходимо настроить конфигурацию сборщика, например webpack.

Выводы

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

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


  1. JordanCpp
    02.11.2023 16:35
    +3

    В статье не запрещено использовать подсветку синтаксиса, нормальное оформление абзацев. Честно, честно.

    Такое чувство, что статью. верстал бэкендер:)


    1. bromzh
      02.11.2023 16:35
      +3

      Так верстка нормальная (в маркдауне), это причуды чудесного хабраредактора.

      Скорее всего автору можно попробовать отрендерить маркдаун в html, и скопировать результат сюда


  1. itmind
    02.11.2023 16:35

    SSR и SSG при таком подходе будут поддерживаться?

    И мне больше нравится вариант отрисовки компонентов через функции, как в Compose Multiplatform (в котором есть компиляция в js) или SwiftUI


    1. ahmoleg Автор
      02.11.2023 16:35

      По поводу серверного рендеринга трудно сейчас сказать. Для поисковых систем было бы неплохо, но, с другой стороны, зачем формы пользователя индексировать в поисковых системах. Может быть кто подскажет для чего еще используется серверный рендеринг, кроме SEO и, может быть, ускорения отрисовки.


  1. venanen
    02.11.2023 16:35
    +2

    Все это хорошо, но вы же понимаете, что HTML во Vue/React - не HTML в привычном виде. Эта разметка компилируется в JS, и вам вообще никто не мешает использовать чистый js, например:

    Vue.component('anchored-heading', {
      render: function (createElement) {
        return createElement(
          'h' + this.level,   // имя тега
          this.$slots.default // массив дочерних элементов
        )
      },
      props: {
        level: {
          type: Number,
          required: true
        }
      }
    })

    И так везде. HTML - это просто синтаксический сахар, потому что это проще, удобнее и нагляднее.


    1. ahmoleg Автор
      02.11.2023 16:35

      Дело не в том, чтобы использовать чиcтый js. Второй подход позволяет абстрагироваться от HTML, от рендеринга и от всплывающих или опускающихся событий, связанных с HTMLElement-ом. Разработчик оперирует не HTML элементами, а элементами управления на форме.
      Хотя, наверное, полностью абстрагироваться не удастся. Все равно, разработчик будет использовать css, да и в сложных формах трудно выверить все до пикселя.


    1. ShadowOfCasper
      02.11.2023 16:35

      При этом автор вообще ни слова не написал про механику парсинга представления


  1. bromzh
    02.11.2023 16:35
    +1

    Второй подход очень похож на ext js. Который довольно печальный. Даже если убрать из внимания то, что он закрытый, очень многие жалуются на него. Именно на сам подход и архитектуру. Если приложение ложится на то, что фреймворк предлагает, то все хорошо. Но шаг в сторону и начинаются мучения.

    И ели глянуть, то все популярные фреймворки исповедуют первый подход, и видимо не случайно.


  1. ShadowOfCasper
    02.11.2023 16:35

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

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

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