Как-то я захотел попробовать реализовать одно маленькое клиент-серверное приложение. Реализация в задумке была такая: на клиентской стороне рисуем с помощью какого-нибуть JS-фреймворка окошки, на серверной стороне обрабатываем вызовы с клиента с помощью скомпилированного приложения, написанного например на Go.
Я начал подбирать фреймворк, предоставляющий декларативный интерфейс. В моём представлении он на входе должен получать в максимально упрощённом виде описание содержимого формы (например в формате YAML, JSON, BEMJSON, XML, или даже INF), а на выходе рисовать в браузере красивую форму с работающими элементами управления.
Профессионально я веб-разработкой не занимаюсь и поэтому не ожидал, что надолго застряну на данном этапе.
И статью писать не собирался, но потом подумал, что свежий взгляд со стороны на современные веб-технологии, пусть даже новичка, может быть интересен сообществу. Возможность получить обратную связь опять же…
HTML- и CSS-шаблоны
Всё, что я находил, работало не так как мне хотелось. Я не хочу ничего знать про вёрстку и стили. Я так же не хочу заполнять HTML, как это делается при применении фреймворков типа Bootstrap, Skeleton, MDL и т. д.
Пример кода с использованием Bootstrap:
<div class="input-group mb-3">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="checkbox" aria-label="Checkbox for following text input">
</div>
</div>
<input type="text" class="form-control" aria-label="Text input with checkbox">
</div>
<button type="button" class="btn btn-primary btn-lg btn-block">Block level button</button>
Всё, что я хочу подать на вход, должно быть примерно такого вида:
{
"main": {
"prop":{},
"elements": [
{
"type":"checkbox",
"name":"Использовать что-то",
"style":"classic",
"checked": true
},
{
"type":"button",
"name":"Сохранить",
"style":"outline",
"onclick":"btnsave_onclick"
}
}
}
Ну и чтобы можно было элементы в группы объединять и указывать как должны быть расположены элементы внутри группы: вертикально или горизонтально. Какое там будет оформление, мне не особо важно. Я ожидал найти даже какой-нибудь визуальный редактор окошек, однако — нет.
Сборки технологий
Тогда я попробовал поискать какие-то сборки технологий — кто-то наверняка проложил давно короткий путь и не раз.
И я нашёл что-то похожее на то, что мне нужно: Сайт с нуля на полном стеке БЭМ-технологий.
Пример BEMJSON:
({
block: 'page',
title: 'Hello, World!',
styles: [
{ elem: 'css', url: 'index.min.css' }
],
scripts: [
{ elem: 'js', url: 'index.min.js' }
],
content: [
'Hello, World!'
]
})
Когда я прокрутил примерно 20-й экран текста, а ползунок находился всё ещё вверху, я подумал, что этот короткий путь какой-то длинный. Не пойдёт.
Почитал я кстати про этот БЭМ — суть мне понравилась, да ещё хорошо и доступно описано. Есть разные шпаргалки, например мне понравилась вот эта. Поискал ещё информацию и обнаружил, что не всем нравится технология (например тут и тут). А здесь на мой взгляд изложена суть спорных моментов.
Интересно то, что используя поиск, я кроме БЭМ-а, долго не мог обнаружить более никаких альтернативных технологий. Однако они есть: OOCS (вроде как тот же БЭМ, только попроще), SMACSS, AtomicCSS, CSSinJS (найдено тут).
Node.js
Я тогда подумал, ок, можно отказаться от первоначальной идеи и реализовать серверную часть на скриптовом языке, т. е. попросту использовать nodejs. Это не только модно, стильно, молодёжно, это ещё возможность писать на всех слоях на одном языке. Масса статей опять же (про быстрый старт мне понравилась эта). Если я правильно понял, под него написано бесчисленное количество NPM-пакетов, практически под любые задачи. Есть ещё под это дело такая серьёзная штука, как Electron.
Пример кода страницы с использованием Electron:
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="assets/css/variables.css">
<link rel="import" href="sections/about.html">
</head>
<body>
<nav class="nav js-nav">
<header class="nav-header">
<h1 class="nav-title">Electron <strong>API Demos</strong></h1>
</header>
<div class="nav-item u-category-windows">
</div>
<footer class="nav-footer">
<button type="button" id="button-about" data-modal="about" class="nav-footer-button">About</button>
</footer>
</nav>
<main class="content js-content"></main>
<script>
require('./assets/normalize-shortcuts')
</script>
</body>
</html>
Единственный минус nodejs, что можно написать более производительную вещь, используя Go.
Допустим я хочу использовать nodejs. Есть каталог NPM-пакетов, чтобы можно было выбрать себе что-то подходящее.
Есть пакет, который так и называется forms. Вот пример кода:
var forms = require('forms');
var fields = forms.fields;
var validators = forms.validators;
var reg_form = forms.create({
username: fields.string({ required: true }),
password: fields.password({ required: validators.required('You definitely want a password') }),
confirm: fields.password({
required: validators.required('don\'t you know your own password?'),
validators: [validators.matchField('password')]
}),
email: fields.email()
});
В саму форму это собирается командой reg_form.toHTML();
. Интересный вариант, но не то.
Есть ещё Element, Riot, TotalJS и iView. Это можно добавить к списку из Bootstrap-а, MDL-а и т. д.
Пример кода страницы с использованием Element:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- import CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
</head>
<body>
<div id="app">
<el-button @click="visible = true">Button</el-button>
<el-dialog :visible.sync="visible" title="Hello world">
<p>Try Element</p>
</el-dialog>
</div>
</body>
<!-- import Vue before Element -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
el: '#app',
data: function() {
return { visible: false }
}
})
</script>
</html>
Есть ещё Blueprintsjs. Это пожалуй максимально похоже на то, что я искал. Есть возможность настраивать элементы управления по отдельности друг от друга.
Код выглядит как-то так:
/*
* Copyright 2018 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the terms of the LICENSE file distributed with this project.
*/
import * as React from "react";
import { FormGroup, H5, InputGroup, Intent, Switch } from "@blueprintjs/core";
import { Example, handleBooleanChange, handleStringChange, IExampleProps } from "@blueprintjs/docs-theme";
import { IntentSelect } from "./common/intentSelect";
export interface IFormGroupExampleState {
disabled: boolean;
helperText: boolean;
inline: boolean;
intent: Intent;
label: boolean;
requiredLabel: boolean;
}
export class FormGroupExample extends React.PureComponent<IExampleProps, IFormGroupExampleState> {
public state: IFormGroupExampleState = {
disabled: false,
helperText: false,
inline: false,
intent: Intent.NONE,
label: true,
requiredLabel: true,
};
private handleDisabledChange = handleBooleanChange(disabled => this.setState({ disabled }));
private handleHelperTextChange = handleBooleanChange(helperText => this.setState({ helperText }));
private handleInlineChange = handleBooleanChange(inline => this.setState({ inline }));
private handleLabelChange = handleBooleanChange(label => this.setState({ label }));
private handleRequiredLabelChange = handleBooleanChange(requiredLabel => this.setState({ requiredLabel
}));
private handleIntentChange = handleStringChange((intent: Intent) => this.setState({ intent }));
public render() {
const { disabled, helperText, inline, intent, label, requiredLabel } = this.state;
const options = (
<>
<H5>Props</H5>
<Switch label="Disabled" checked={disabled} onChange={this.handleDisabledChange} />
<Switch label="Inline" checked={inline} onChange={this.handleInlineChange} />
<Switch label="Show helper text" checked={helperText} onChange=
{this.handleHelperTextChange} />
<Switch label="Show label" checked={label} onChange={this.handleLabelChange} />
<Switch label="Show label info" checked={requiredLabel} onChange=
{this.handleRequiredLabelChange} />
<IntentSelect intent={intent} onChange={this.handleIntentChange} />
</>
);
return (
<Example options={options} {...this.props}>
<FormGroup
disabled={disabled}
helperText={helperText && "Helper text with details..."}
inline={inline}
intent={intent}
label={label && "Label"}
labelFor="text-input"
labelInfo={requiredLabel && "(required)"}
>
<InputGroup id="text-input" placeholder="Placeholder text" disabled={disabled}
intent={intent} />
</FormGroup>
<FormGroup
disabled={disabled}
helperText={helperText && "Helper text with details..."}
inline={inline}
intent={intent}
label={label && "Label"}
labelFor="text-input"
labelInfo={requiredLabel && "(required)"}
>
<Switch id="text-input" label="Engage the hyperdrive" disabled={disabled} />
<Switch id="text-input" label="Initiate thrusters" disabled={disabled} />
</FormGroup>
</Example>
);
}
}
Результат выглядит так:
Даже если учесть только последнюю часть: всё-равно как-то не очень лаконично.
metadata.js
Вот ещё нашёл на просторах Сети интересную вещь — metadatajs. Быстрый старт — тут. Данная библиотека обладает избыточным функционалом, а в статье нет на мой взгляд самого главного — описания самой библиотеки. Это есть здесь.
Форму описывать нам предлагают так:
{
"enm": {},
"cat": {
"Номенклатура": {
"form": {
"selection": {
"fields": [
"is_folder",
"id",
"Артикул",
"_t_.name as presentation",
"`cat_КлассификаторЕдиницИзмерения`.name as `ЕдиницаИзмерения`",
"`cat_НоменклатурныеГруппы`.name as `НоменклатурнаяГруппа`"
],
"cols": [
{"id": "id", "width": "120", "type": "ro", "align": "left", "sort": "server", "caption": "Код"},
{"id": "Артикул", "width": "150", "type": "ro", "align": "left", "sort": "server", "caption": "Артикул"},
{"id": "presentation", "width": "*", "type": "ro", "align": "left", "sort": "server", "caption": "Наименование"},
{"id": "ЕдиницаИзмерения", "width": "70", "type": "ro", "align": "left", "sort": "server", "caption": "Ед"},
{"id": "НоменклатурнаяГруппа", "width": "170", "type": "ro", "align": "left", "sort": "server", "caption": "Номенклатурная группа"}
]
}
}
},
"ДоговорыКонтрагентов": {
"form": {
"selection": {
"fields": [
"is_folder",
"id",
"_t_.name as presentation",
"`enm_ВидыДоговоров`.synonym as `ВидДоговора`",
"`cat_Организации`.name as `Организация`"
],
"cols": [
{"id": "presentation", "width": "*", "type": "ro", "align": "left", "sort": "server", "caption": "Наименование"},
{"id": "ВидДоговора", "width": "150", "type": "ro", "align": "left", "sort": "server", "caption": "Вид договора"},
{"id": "Организация", "width": "150", "type": "ro", "align": "left", "sort": "server", "caption": "Организация"}
]
}
}
}
},
"doc": {
"ЗаказПокупателя": {
"form": {
"selection": {
"fields": [
"date",
"number_doc",
"Контрагент",
"СуммаДокумента",
"posted",
"СостояниеЗаказа",
"Комментарий"
],
"cols": [
{"id": "date", "width": "120", "type": "ro", "align": "left", "sort": "server", "caption": "Дата"},
{"id": "number_doc", "width": "120", "type": "ro", "align": "left", "sort": "server", "caption": "Номер"},
{"id": "Контрагент", "width": "170", "type": "ro", "align": "left", "sort": "server", "caption": "Контрагент"},
{"id": "СуммаДокумента", "width": "120", "type": "ron", "align": "right", "sort": "server", "caption": "Сумма"},
{"id": "СостояниеЗаказа", "width": "100", "type": "ro", "align": "left", "sort": "server", "caption": "Состояние"},
{"id": "Комментарий", "width": "*", "type": "ro", "align": "left", "sort": "server", "caption": "Комментарий"}
]
},
"obj": {
"head": {
" ": ["number_doc", "date", "ВидОперации", "Организация", "Контрагент", "Договор"],
"Планирование": ["СостояниеЗаказа", "Старт", "ДатаОтгрузки"],
"Дополнительно": ["Проект", "Касса", "Событие", "НалогообложениеНДС", "СуммаВключаетНДС", "Ответственный", "Комментарий",
{"id": "СуммаДокумента", "path": "o.СуммаДокумента", "synonym": "Сумма документа", "type": "ro"}
]
},
"tabular_sections": {
"Запасы": {
"fields": ["row","Номенклатура","Характеристика","Количество","ЕдиницаИзмерения","Цена","ПроцентСкидкиНаценки","Сумма","СтавкаНДС","СуммаНДС","Всего","ДатаОтгрузки","Спецификация","Содержание"],
"headers": "№,Номенклатура,Характеристика,Колич.,Ед,Цена,% Скидки,Сумма,% НДС,НДС,Всего,Отгруз.,Спецификация,Содержание",
"widths": "40,*,*,70,50,70,70,70,70,70,70,70,70,80",
"min_widths": "40,200,140,70,50,70,70,70,70,70,70,70,70,80",
"aligns": "",
"sortings": "na,na,na,na,na,na,na,na,na,na,na,na,na,na",
"types": "cntr,ref,ref,calck,refc,calck,calck,ron,refc,ron,ron,dhxCalendar,ref,txt"
}
}
}
}
}
},
"ireg": {},
"areg": {},
"dp": {},
"rep": {},
"cch": {},
"cacc": {}
}
Это очень похоже на то, что нужно. Однако возникают вопросы: набор кнопок похоже задан по умолчанию? Можно ли его менять? Где описание синтаксиса (что такое "enm", "cat" и т. д.)?
Видно, что на проект много усилий вложено, но библиотека пока сыровата.
Заключение
Давайте порассуждаем. Формочки можно заранее нарисовать на сервере, скомпилировать и отдавать на сторону клиента готовый html/css/js-код. В этом случае нужно искать совсем другое. Но если мы хотим, чтобы пользователь работал у нас в одном окне браузера, и в этом окне видел дочерние окна веб-приложения, не было бы логичным закешировать css/js-код на клиенте, который будет получать от сервера короткие команды с описанием очередной формочки и будет рисовать их за счёт ресурсов клиента?
Коллеги, где вы прячете фреймворк с декларативным интерфейсом? )
Он должен быть! Почему я не могу его найти?
UPD
Мне подсказали несколько вариантов, из которых был один, который совпадает с тем, что я искал. Это Webix.