Разработка приложений с помощью платформы быстрой разработки приложений AllcountJS

Опубликовано: 2022-03-11

Идея быстрой разработки приложений (RAD) родилась в ответ на традиционные модели водопадной разработки. Существует множество вариантов RAD; например, Agile-разработка и Rational Unified Process. Однако у всех таких моделей есть одна общая черта: они нацелены на получение максимальной ценности для бизнеса при минимальном времени разработки за счет прототипирования и итеративной разработки. Для этого в модели быстрой разработки приложений используются инструменты, упрощающие процесс. В этой статье мы рассмотрим один из таких инструментов и то, как его можно использовать, чтобы сосредоточиться на ценности для бизнеса и оптимизации процесса разработки.

AllcountJS — это новый фреймворк с открытым исходным кодом, созданный для быстрой разработки приложений. Он основан на идее декларативной разработки приложений с использованием JSON-подобного кода конфигурации, описывающего структуру и поведение приложения. Фреймворк построен на основе Node.js, Express, MongoDB и в значительной степени зависит от AngularJS и Twitter Bootstrap. Несмотря на то, что он зависит от декларативных шаблонов, фреймворк по-прежнему допускает дальнейшую настройку посредством прямого доступа к API, где это необходимо.

AllcountJS как ваш RAD Framework

Почему AllcountJS является вашей RAD Framework?

Согласно Википедии, существует по меньшей мере сто инструментов, обещающих быструю разработку приложений, но тут возникает вопрос: насколько быстрой является «быстрая». Позволяют ли эти инструменты разработать конкретное приложение, ориентированное на данные, за несколько часов? Или, возможно, «быстрое», если приложение можно разработать за несколько дней или несколько недель. Некоторые из этих инструментов даже утверждают, что для создания работающего приложения достаточно нескольких минут. Однако маловероятно, что вы сможете создать полезное приложение менее чем за пять минут и при этом утверждать, что удовлетворили все потребности бизнеса. AllcountJS не претендует на роль такого инструмента; то, что предлагает AllcountJS, — это способ прототипирования идеи за короткий период времени.

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

AllcountJS использовался для создания allcountjs.com, а также трекера проектов для него. Стоит отметить, что allcountjs.com — это настраиваемое приложение AllcountJS, и что AllcountJS позволяет с легкостью комбинировать как статические, так и динамические представления. Он даже позволяет вставлять динамически загружаемые части в статическое содержимое. Например, AllcountJS управляет коллекцией шаблонов демонстрационных приложений. На главной странице allcountjs.com есть демонстрационный виджет, который загружает случайный шаблон приложения из этой коллекции. Несколько других примеров приложений доступны в галерее на сайте allcountjs.com.

Начиная

Чтобы продемонстрировать некоторые возможности фреймворка RAD AllcountJS, мы создадим простое приложение для Toptal, которое назовем Toptal Community. Если вы следите за нашим блогом, вы, возможно, уже знаете, что подобное приложение было создано с использованием Hoodie как часть одной из наших предыдущих записей в блоге. Это приложение позволит членам сообщества регистрироваться, создавать мероприятия и подавать заявки на их участие.

Чтобы настроить среду, вы должны установить Node.js, MongoDB и Git. Затем установите интерфейс командной строки AllcountJS, вызвав команду «npm install», и выполните инициализацию проекта:

 npm install -g allcountjs-cli allcountjs init toptal-community-allcount cd toptal-community-allcount npm install

AllcountJS CLI попросит вас ввести некоторую информацию о вашем проекте, чтобы предварительно заполнить package.json.

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

Внутри этого недавно созданного каталога app-config мы заменим содержимое файла JavaScript main.js следующим фрагментом кода:

 A.app({ appName: "Toptal Community", onlyAuthenticated: true, allowSignUp: true, appIcon: "rocket", menuItems: [{ name: "Events", entityTypeId: "Event", icon: "calendar" }, { name: "My Events", entityTypeId: "MyEvent", icon: "calendar" }], entities: function(Fields) { return { Event: { title: "Events", fields: { eventName: Fields.text("Event").required(), date: Fields.date("Date").required(), time: Fields.text("Starts at").masked("99:99").required(), appliedUsers: Fields.relation("Applied users", "AppliedUser", "event") }, referenceName: "eventName", sorting: [['date', -1], ['time', -1]], actions: [{ id: "apply", name: "Apply", actionTarget: 'single-item', perform: function (User, Actions, Crud) { return Crud.actionContextCrud().readEntity(Actions.selectedEntityId()).then(function (eventToApply) { var userEventCrud = Crud.crudForEntityType('UserEvent'); return userEventCrud.find({filtering: {"user": User.id, "event": eventToApply.id}}).then(function (events) { if (events.length) { return Actions.modalResult("Can't apply to event", "You've already applied to this event"); } else { return userEventCrud.createEntity({ user: {id: User.id}, event: {id: eventToApply.id}, date: eventToApply.date, time: eventToApply.time }).then(function () { return Actions.navigateToEntityTypeResult("MyEvent") }); } }); }) } }] }, UserEvent: { fields: { user: Fields.fixedReference("User", "OnlyNameUser").required(), event: Fields.fixedReference("Event", "Event").required(), date: Fields.date("Date").required(), time: Fields.text("Starts at").masked("99:99").required() }, filtering: function (User) { return {"user.id": User.id} }, sorting: [['date', -1], ['time', -1]], views: { MyEvent: { title: "My Events", showInGrid: ['event', 'date', 'time'], permissions: { write: [], delete: null } }, AppliedUser: { permissions: { write: [] }, showInGrid: ['user'] } } }, User: { views: { OnlyNameUser: { permissions: { read: null, write: ['admin'] } }, fields: { username: Fields.text("User name") } } } } } });

Хотя AllcountJS работает с репозиториями Git, для простоты мы не будем использовать его в этом руководстве. Чтобы запустить приложение Toptal Community, все, что нам нужно сделать, это вызвать команду запуска AllcountJS CLI в каталоге toptal-community-allcount.

 allcountjs run

Стоит отметить, что при выполнении этой команды MongoDB должна быть запущена. Если все пойдет хорошо, приложение должно быть запущено по адресу http://localhost:9080.

Для входа используйте логин «admin» и пароль «admin».

Менее 100 строк

Вы могли заметить, что приложение, определенное в main.js, заняло всего 91 строку кода. Эти строки включают описание всех действий, которые вы можете наблюдать при переходе на http://localhost:9080. Итак, что именно происходит под капотом? Давайте подробнее рассмотрим каждый аспект приложения и посмотрим, как код относится к ним.

Войти Зарегистрироваться

Первая страница, которую вы видите после открытия приложения, — это вход в систему. Это удваивается как страница регистрации, при условии, что флажок с надписью «Зарегистрироваться» установлен перед отправкой формы.

Войти Зарегистрироваться

Эта страница отображается, потому что в файле main.js указано, что только авторизованные пользователи могут использовать это приложение. Кроме того, он позволяет пользователям регистрироваться с этой страницы. Следующие две строки — все, что было необходимо для этого:

 A.app({ ..., onlyAuthenticated: true, allowSignUp: true, ... })

Приветственная страница

После входа вы будете перенаправлены на страницу приветствия с меню приложений. Эта часть приложения создается автоматически на основе пунктов меню, определенных в ключе «menuItems».

пример приветственной страницы

Наряду с парой других соответствующих конфигураций меню определено в файле main.js следующим образом:

 A.app({ ..., appName: "Toptal Community", appIcon: "rocket", menuItems: [{ name: "Events", entityTypeId: "Event", icon: "calendar" }, { name: "My Events", entityTypeId: "MyEvent", icon: "calendar" }], ... });

AllcountJS использует значки Font Awesome, поэтому все имена значков, указанные в конфигурации, сопоставляются с именами значков Font Awesome.

Просмотр и редактирование событий

Нажав «События» в меню, вы попадете в представление «События», показанное на снимке экрана ниже. Это стандартное представление AllcountJS, которое предоставляет некоторые общие функции CRUD для соответствующих сущностей. Здесь вы можете искать события, создавать новые события и редактировать или удалять существующие. Есть два режима этого интерфейса CRUD: список и форма. Эта часть приложения настраивается с помощью следующих нескольких строк кода JavaScript.

 A.app({ ..., entities: function(Fields) { return { Event: { title: "Events", fields: { eventName: Fields.text("Event").required(), date: Fields.date("Date").required(), time: Fields.text("Starts at").masked("99:99").required(), appliedUsers: Fields.relation("Applied users", "AppliedUser", "event") }, referenceName: "eventName", sorting: [['date', -1], ['time', -1]], ... } } } });

В этом примере показано, как описания сущностей настраиваются в AllcountJS. Обратите внимание, как мы используем функцию для определения объектов; каждое свойство конфигурации AllcountJS может быть функцией. Эти функции могут запрашивать разрешение зависимостей через имена своих аргументов. Перед вызовом функции вводятся соответствующие зависимости. Здесь «Поля» — это один из API-интерфейсов конфигурации AllcountJS, используемых для описания полей сущности. Свойство «Сущности» содержит пары «имя-значение», где имя является идентификатором типа сущности, а значение — его описанием. В этом примере описывается тип объекта для событий, заголовок которого — «События». Другие конфигурации, такие как порядок сортировки по умолчанию, ссылочное имя и т.п., также могут быть определены здесь. Порядок сортировки по умолчанию определяется через массив имен полей и направлений, а имя ссылки определяется через строку (подробнее читайте здесь).

функция allcountJS

Этот конкретный тип сущности был определен как имеющий четыре поля: «eventName», «date», «time» и «appliedUsers», первые три из которых сохраняются в базе данных. Эти поля являются обязательными, на что указывает использование «required()». Значения в этих полях с такими правилами проверяются перед отправкой формы во внешнем интерфейсе, как показано на снимке экрана ниже. AllcountJS сочетает в себе проверки на стороне клиента и на стороне сервера, чтобы обеспечить лучший пользовательский интерфейс. Четвертое поле — это отношение, в котором содержится список пользователей, подавших заявки на участие в мероприятии. Естественно, это поле не сохраняется в базе данных и заполняется путем выбора только тех сущностей AppliedUser, которые имеют отношение к событию.

правила разработки allcountjs

Подача заявки на участие в мероприятиях

Когда пользователь выбирает определенное событие, на панели инструментов отображается кнопка с надписью «Применить». При нажатии на нее событие добавляется в расписание пользователя. В AllcountJS подобные действия можно настроить, просто объявив их в конфигурации:

 actions: [{ id: "apply", name: "Apply", actionTarget: 'single-item', perform: function (User, Actions, Crud) { return Crud.actionContextCrud().readEntity(Actions.selectedEntityId()).then(function (eventToApply) { var userEventCrud = Crud.crudForEntityType('UserEvent'); return userEventCrud.find({filtering: {"user": User.id, "event": eventToApply.id}}).then(function (events) { if (events.length) { return Actions.modalResult("Can't apply to event", "You've already applied to this event"); } else { return userEventCrud.createEntity({ user: {id: User.id}, event: {id: eventToApply.id}, date: eventToApply.date, time: eventToApply.time }).then(function () { return Actions.navigateToEntityTypeResult("MyEvent") }); } }); }) } }]

Свойство «действия» любого типа сущности принимает массив объектов, описывающих поведение каждого пользовательского действия. Каждый объект имеет свойство «id», которое определяет уникальный идентификатор действия, свойство «name» определяет отображаемое имя, а свойство «actionTarget» используется для определения контекста действия. Установка для «actionTarget» значения «single-item» указывает, что действие должно быть выполнено с определенным событием. Функция, определенная в свойстве «выполнить», представляет собой логику, выполняемую при выполнении этого действия, обычно когда пользователь нажимает соответствующую кнопку.

Эта функция может запрашивать зависимости. Например, в этом примере функция зависит от «Пользователь», «Действия» и «Хлам». Когда происходит действие, ссылку на пользователя, вызывающего это действие, можно получить, запросив зависимость «Пользователь». Здесь также запрашивается зависимость «Crud», которая позволяет манипулировать состоянием базы данных для этих сущностей. Два метода, которые возвращают экземпляр объекта Crud: Метод «actionContextCrud()» — возвращает CRUD для типа сущности «Событие», поскольку ему принадлежит действие «Применить», а метод «crudForEntityType()» — возвращает CRUD. для любого типа объекта, идентифицированного его идентификатором типа.

CRUD-зависимости

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

осуществление действия

Пользовательский график прикладных событий

После успешного применения к событию браузер перенаправляется на представление «Мои события», в котором отображается список событий, к которым применил пользователь. Представление определяется следующей конфигурацией:

 UserEvent: { fields: { user: Fields.fixedReference("User", "OnlyNameUser").required(), event: Fields.fixedReference("Event", "Event").required(), date: Fields.date("Date").required(), time: Fields.text("Starts at").masked("99:99").required() }, filtering: function (User) { return {"user.id": User.id} }, sorting: [['date', -1], ['time', -1]], views: { MyEvent: { title: "My Events", showInGrid: ['event', 'date', 'time'], permissions: { write: [], delete: null } }, AppliedUser: { permissions: { write: [] }, showInGrid: ['user'] } } },

В этом случае мы используем новое свойство конфигурации «фильтрация». Как и в нашем предыдущем примере, эта функция также зависит от зависимости «Пользователь». Если функция возвращает объект, он обрабатывается как запрос MongoDB; запрос фильтрует коллекцию для событий, которые принадлежат только текущему пользователю.

Еще одно интересное свойство — «Просмотры». «Просмотр» — это обычный тип объекта, но его коллекция MongoDB такая же, как и для родительского типа объекта. Это позволяет создавать визуально разные представления для одних и тех же данных в базе данных. Фактически, мы использовали эту функцию для создания двух разных представлений для «UserEvent»: «MyEvent» и «AppliedUser». Поскольку прототип подпредставления настроен на тип родительской сущности, свойства, которые не переопределены, «наследуются» от родительского типа.

Просмотры

Список участников мероприятия

После подачи заявки на мероприятие другие пользователи могут увидеть список всех пользователей, планирующих посетить мероприятие. Это создается в результате следующих элементов конфигурации в main.js:

 AppliedUser: { permissions: { write: [] }, showInGrid: ['user'] } // ... appliedUsers: Fields.relation("Applied users", "AppliedUser", "event")

«AppliedUser» — это представление только для чтения для типа объекта «MyEvent». Это разрешение только для чтения применяется путем установки пустого массива в свойство «Запись» объекта разрешений. Кроме того, поскольку разрешение «Чтение» не определено, по умолчанию чтение разрешено для всех пользователей.

applyuser для типа myevent

Расширение реализации по умолчанию

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

Для этого AllcountJS использует собственную реализацию Dependency Injection (DI). DI позволяет разработчику переопределить поведение платформы по умолчанию с помощью точек расширения и в то же время позволяет повторно использовать существующие реализации. Многие аспекты расширения RAD Framework описаны в документации. В этом разделе мы рассмотрим, как мы можем расширить два из множества компонентов фреймворка, логику на стороне сервера и представления.

Продолжая наш пример Toptal Community, давайте интегрируем внешний источник данных для агрегирования данных о событиях. Давайте представим, что за день до каждого события в блоге Toptal обсуждаются планы мероприятий. С Node.js должна быть возможность анализировать RSS-канал блога и извлекать такие данные. Для этого нам понадобятся некоторые дополнительные зависимости npm, такие как «request», «xml2js» (для загрузки RSS-канала блога Toptal), «q» (для реализации промисов) и «moment» (для анализа дат). Эти зависимости можно установить, вызвав следующий набор команд:

 npm install xml2js npm install request npm install q npm install moment

Давайте создадим еще один файл JavaScript, назовем его «toptal-community.js» в каталоге toptal-community-allcount и заполним его следующим:

 var request = require('request'); var Q = require('q'); var xml2js = require('xml2js'); var moment = require('moment'); var injection = require('allcountjs'); injection.bindFactory('port', 9080); injection.bindFactory('dbUrl', 'mongodb://localhost:27017/toptal-community'); injection.bindFactory('gitRepoUrl', 'app-config'); injection.bindFactory('DiscussionEventsImport', function (Crud) { return { importEvents: function () { return Q.nfcall(request, "https://www.toptal.com/blog.rss").then(function (responseAndBody) { var body = responseAndBody[1]; return Q.nfcall(xml2js.parseString, body).then (function (feed) { var events = feed.rss.channel[0].item.map(function (item) { return { eventName: "Discussion of " + item.title, date: moment(item.pubDate, "DD MMM YYYY").add(1, 'day').toDate(), time: "12:00" }}); var crud = Crud.crudForEntityType('Event'); return Q.all(events.map(function (event) { return crud.find({query: {eventName: event.eventName}}).then(function (createdEvent) { if (!createdEvent[0]) { return crud.createEntity(event); } }); } )); }); }) } }; }); var server = injection.inject('allcountServerStartup'); server.startup(function (errors) { if (errors) { throw new Error(errors.join('\n')); } });

В этом файле мы определяем зависимость под названием «DiscussionEventsImport», которую мы можем использовать в нашем файле main.js, добавив действие импорта для типа сущности «Событие».

 { id: "import-blog-events", name: "Import Blog Events", actionTarget: "all-items", perform: function (DiscussionEventsImport, Actions) { return DiscussionEventsImport.importEvents().then(function () { return Actions.refreshResult() }); } }

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

 node toptal-community.js

Если все пойдет правильно, вы увидите что-то вроде скриншота ниже после выполнения действия «Импортировать события блога».

Действие «Импорт событий блога»

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

Тебе нравятся карты? Карты нравятся всем! Чтобы создать представление карты, поместите следующее в файл с именем events.jade внутри каталога app-config:

 extends main include mixins block vars - var hasToolbar = true block content .refresh-form-controller(ng-app='allcount', ng-controller='EntityViewController') +defaultToolbar() .container.screen-container(ng-cloak) +defaultList() .row: .col-lg-4.col-md-6.col-xs-12(ng-repeat="item in items") .panel.panel-default .panel-heading h3 {{item.date | date}} {{item.time}} div button.btn.btn-default.btn-xs(ng-if="!isInEditMode", lc-tooltip="View", ng-click="navigate(item.id)"): i.glyphicon.glyphicon-chevron-right |   button.btn.btn-danger.btn-xs(ng-if="isInEditMode", lc-tooltip="Delete", ng-click="deleteEntity(item)"): i.glyphicon.glyphicon-trash .panel-body h3 {{item.eventName}} +noEntries() +defaultEditAndCreateForms() block js +entityJs()

После этого просто укажите его из сущности «Событие» в main.js как «customView: «events»». Запустите приложение, и вы должны увидеть карточный интерфейс вместо табличного по умолчанию.

Сущность события в main.js

Заключение

В настоящее время процесс разработки веб-приложений схож во многих веб-технологиях, где некоторые операции повторяются снова и снова. Это действительно того стоит? Может быть, пора переосмыслить то, как разрабатываются ваши веб-приложения?

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