Tworzenie aplikacji za pomocą platformy szybkiego tworzenia aplikacji AllcountJS
Opublikowany: 2022-03-11Idea Rapid Application Development (RAD) narodziła się w odpowiedzi na tradycyjne modele rozwoju kaskadowego. Istnieje wiele odmian RAD; na przykład programowanie Agile i Rational Unified Process. Jednak wszystkie takie modele mają jedną wspólną cechę: mają na celu uzyskanie maksymalnej wartości biznesowej przy minimalnym czasie opracowywania poprzez tworzenie prototypów i iteracyjne opracowywanie. Aby to osiągnąć, model Rapid Application Development opiera się na narzędziach, które ułatwiają ten proces. W tym artykule przyjrzymy się jednemu z takich narzędzi i temu, jak można je wykorzystać do skupienia się na wartości biznesowej i optymalizacji procesu rozwoju.
AllcountJS to nowa platforma open source zbudowana z myślą o szybkim tworzeniu aplikacji. Opiera się na idei tworzenia aplikacji deklaratywnych przy użyciu kodu konfiguracyjnego typu JSON, który opisuje strukturę i zachowanie aplikacji. Framework został zbudowany na bazie Node.js, Express, MongoDB i w dużej mierze opiera się na AngularJS i Twitter Bootstrap. Chociaż opiera się na wzorcach deklaratywnych, platforma nadal umożliwia dalsze dostosowywanie poprzez bezpośredni dostęp do interfejsu API w razie potrzeby.
Dlaczego AllcountJS jest Twoim frameworkiem RAD?
Według Wikipedii istnieje co najmniej sto narzędzi, które obiecują szybki rozwój aplikacji, ale to rodzi pytanie: jak szybkie jest „szybkie”. Czy te narzędzia pozwalają na stworzenie konkretnej aplikacji zorientowanej na dane w kilka godzin? A może jest „szybko”, jeśli aplikację można opracować w kilka dni lub kilka tygodni. Niektóre z tych narzędzi twierdzą nawet, że wystarczy kilka minut, aby stworzyć działającą aplikację. Jednak jest mało prawdopodobne, abyś mógł zbudować użyteczną aplikację w mniej niż pięć minut i nadal twierdzić, że zaspokoiłeś każdą potrzebę biznesową. AllcountJS nie twierdzi, że jest takim narzędziem; to, co oferuje AllcountJS, to sposób na prototypowanie pomysłu w krótkim czasie.
Dzięki frameworkowi AllcountJS możliwe jest zbudowanie aplikacji z automatycznie generowanym interfejsem użytkownika z motywami, funkcjami zarządzania użytkownikami, RESTful API i kilkoma innymi funkcjami przy minimalnym nakładzie pracy i czasu. Można używać AllcountJS w wielu różnych przypadkach użycia, ale najlepiej pasuje do aplikacji, w których masz różne kolekcje obiektów z różnymi widokami. Zazwyczaj aplikacje biznesowe są dobrze dopasowane do tego modelu.
AllcountJS został użyty do zbudowania allcountjs.com, wraz z narzędziem do śledzenia projektu. Warto zauważyć, że allcountjs.com to dostosowana aplikacja AllcountJS i że AllcountJS umożliwia łączenie zarówno statycznych, jak i dynamicznych widoków bez większego wysiłku. Umożliwia nawet wstawianie dynamicznie ładowanych części do statycznej zawartości. Na przykład AllcountJS zarządza kolekcją szablonów aplikacji demonstracyjnych. Na stronie głównej allcountjs.com znajduje się widżet demo, który ładuje losowy szablon aplikacji z tej kolekcji. Kilka innych przykładowych aplikacji jest dostępnych w galerii na allcountjs.com.
Pierwsze kroki
Aby zademonstrować niektóre możliwości frameworka RAD AllcountJS, stworzymy prostą aplikację dla Toptal, którą nazwiemy Toptal Community. Jeśli śledzisz naszego bloga, być może wiesz już, że podobna aplikacja została zbudowana przy użyciu Hoodie w ramach jednego z naszych wcześniejszych wpisów na blogu. Ta aplikacja pozwoli członkom społeczności na rejestrowanie się, tworzenie wydarzeń i zgłaszanie się do udziału w nich.
W celu skonfigurowania środowiska należy zainstalować Node.js, MongoDB i Git. Następnie zainstaluj AllcountJS CLI, wywołując polecenie „npm install” i wykonaj init projektu:
npm install -g allcountjs-cli allcountjs init toptal-community-allcount cd toptal-community-allcount npm install
AllcountJS CLI poprosi Cię o podanie informacji o Twoim projekcie, aby wstępnie wypełnić plik package.json.
AllcountJS może być używany jako samodzielny serwer lub jako zależność. W naszym pierwszym przykładzie nie zamierzamy rozszerzać AllcountJS, więc samodzielny serwer powinien po prostu działać dla nas.
Wewnątrz tego nowo utworzonego katalogu app-config zastąpimy zawartość pliku JavaScript main.js następującym fragmentem kodu:
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") } } } } } });
Chociaż AllcountJS współpracuje z repozytoriami Git, dla uproszczenia nie będziemy go używać w tym samouczku. Aby uruchomić aplikację Toptal Community, wystarczy wywołać polecenie uruchamiania CLI AllcountJS w katalogu toptal-community-allcount.
allcountjs run
Warto zauważyć, że MongoDB powinno być uruchomione podczas wykonywania tego polecenia. Jeśli wszystko pójdzie dobrze, aplikacja powinna być uruchomiona i działać pod adresem http://localhost:9080.
Aby się zalogować, użyj nazwy użytkownika „admin” i hasła „admin”.
Mniej niż 100 linii
Być może zauważyłeś, że aplikacja zdefiniowana w main.js zajęła tylko 91 linijek kodu. Te wiersze zawierają deklarację wszystkich zachowań, które możesz zaobserwować, przechodząc do http://localhost:9080. Więc co się właściwie dzieje pod maską? Przyjrzyjmy się bliżej każdemu aspektowi aplikacji i zobaczmy, jak odnosi się do nich kod.
Zaloguj się Zarejestruj się
Pierwsza strona, którą widzisz po otwarciu aplikacji, to logowanie. Jest to również strona rejestracji, przy założeniu, że pole wyboru – oznaczone jako „Zarejestruj się” – jest zaznaczone przed przesłaniem formularza.
Ta strona jest wyświetlana, ponieważ plik main.js deklaruje, że tylko uwierzytelnieni użytkownicy mogą korzystać z tej aplikacji. Ponadto umożliwia użytkownikom rejestrację z tej strony. Potrzebne do tego były tylko dwie linijki:
A.app({ ..., onlyAuthenticated: true, allowSignUp: true, ... })
Strona powitalna
Po zalogowaniu zostaniesz przekierowany na stronę powitalną z menu aplikacji. Ta część aplikacji jest generowana automatycznie na podstawie pozycji menu zdefiniowanych pod klawiszem „menuItems”.
Wraz z kilkoma innymi istotnymi konfiguracjami menu jest zdefiniowane w pliku main.js w następujący sposób:
A.app({ ..., appName: "Toptal Community", appIcon: "rocket", menuItems: [{ name: "Events", entityTypeId: "Event", icon: "calendar" }, { name: "My Events", entityTypeId: "MyEvent", icon: "calendar" }], ... });
AllcountJS używa ikon Font Awesome, więc wszystkie nazwy ikon, do których odwołuje się konfiguracja, są mapowane na nazwy ikon Font Awesome.
Przeglądanie i edytowanie wydarzeń
Po kliknięciu w menu „Wydarzenia” zostaniesz przeniesiony do widoku Wydarzenia pokazanego na poniższym zrzucie ekranu. Jest to standardowy widok AllcountJS, który zapewnia pewne ogólne funkcje CRUD w odpowiednich jednostkach. Tutaj możesz wyszukiwać wydarzenia, tworzyć nowe oraz edytować lub usuwać istniejące. Istnieją dwa tryby tego interfejsu CRUD: lista i formularz. Ta część aplikacji jest konfigurowana za pomocą następujących kilku linijek kodu 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]], ... } } } });
Ten przykład pokazuje, jak opisy encji są skonfigurowane w AllcountJS. Zwróć uwagę, jak używamy funkcji do definiowania bytów; każda właściwość konfiguracji AllcountJS może być funkcją. Te funkcje mogą żądać rozwiązania zależności za pomocą nazw argumentów. Przed wywołaniem funkcji wstrzykiwane są odpowiednie zależności. W tym przypadku „Fields” jest jednym z konfiguracyjnych interfejsów API AllcountJS używanym do opisywania pól encji. Właściwość „Entities” zawiera pary nazwa-wartość, gdzie nazwa jest identyfikatorem typu jednostki, a wartość jest jej opisem. W tym przykładzie opisano typ encji dla zdarzeń, którego tytuł to „Zdarzenia”. W tym miejscu można również zdefiniować inne konfiguracje, takie jak domyślne sortowanie, nazwa referencyjna i tym podobne. Domyślna kolejność sortowania jest definiowana za pomocą tablicy nazw pól i kierunków, podczas gdy nazwa odniesienia jest definiowana za pomocą ciągu (więcej informacji tutaj).
Ten konkretny typ jednostki został zdefiniowany jako posiadający cztery pola: „eventName”, „date”, „time” i „appliedUsers”, z których pierwsze trzy są utrwalane w bazie danych. Pola te są obowiązkowe, na co wskazuje użycie „required()”. Wartości w tych polach z takimi regułami są weryfikowane przed przesłaniem formularza na interfejsie użytkownika, jak pokazano na zrzucie ekranu poniżej. AllcountJS łączy walidacje zarówno po stronie klienta, jak i po stronie serwera, aby zapewnić najlepsze wrażenia użytkownika. Czwarte pole to relacja zawierająca listę użytkowników, którzy zgłosili się do udziału w wydarzeniu. Oczywiście to pole nie jest utrwalane w bazie danych i jest wypełniane przez wybranie tylko tych encji AppliedUser, które są istotne dla zdarzenia.

Zgłaszanie się do udziału w wydarzeniach
Gdy użytkownik wybierze określone zdarzenie, na pasku narzędzi zostanie wyświetlony przycisk „Zastosuj”. Kliknięcie na nią dodaje wydarzenie do harmonogramu użytkownika. W AllcountJS podobne akcje można skonfigurować, po prostu zadeklarowając je w konfiguracji:
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") }); } }); }) } }]
Właściwość „actions” dowolnego typu jednostki przyjmuje tablicę obiektów opisujących zachowanie każdej akcji niestandardowej. Każdy obiekt posiada właściwość „id”, która definiuje unikalny identyfikator akcji, właściwość „name” określa wyświetlaną nazwę, a właściwość „actionTarget” służy do zdefiniowania kontekstu akcji. Ustawienie „actionTarget” na „single-item” wskazuje, że akcja powinna zostać wykonana z określonym zdarzeniem. Funkcja zdefiniowana we właściwości „wykonaj” jest logiką wykonywaną podczas wykonywania tej akcji, zwykle gdy użytkownik kliknie odpowiedni przycisk.
Ta funkcja może wymagać zależności. Na przykład w tym przykładzie funkcja zależy od „Użytkownika”, „Działań” i „Crud”. Gdy wystąpi akcja, odwołanie do użytkownika wywołującego tę akcję można uzyskać, wymagając zależności „Użytkownik”. Wymagana jest tutaj również zależność „Crud”, która umożliwia manipulowanie stanem bazy danych dla tych encji. Dwie metody zwracające instancję obiektu Crud to: Metoda „actionContextCrud()” – zwraca CRUD dla typu encji „Event”, ponieważ akcja „Apply” należy do niej, natomiast metoda „crudForEntityType()” – zwraca CRUD dla dowolnego typu jednostki identyfikowanego przez jego identyfikator typu.
Realizacja akcji rozpoczyna się od sprawdzenia, czy to zdarzenie jest już zaplanowane dla użytkownika, a jeśli nie, tworzy je. Jeśli jest to już zaplanowane, zostanie wyświetlone okno dialogowe, zwracając wartość z wywołania „Actions.modalResult()”. Oprócz wyświetlania modalnego, akcja może wykonywać różne rodzaje operacji w podobny sposób, takie jak „przejdź do widoku”, „odśwież widok”, „pokaż okno dialogowe” i tak dalej.
Harmonogram użytkownika zastosowanych zdarzeń
Po pomyślnym zastosowaniu się do wydarzenia przeglądarka jest przekierowywana do widoku „Moje wydarzenia”, który pokazuje listę wydarzeń, do których użytkownik się zgłosił. Widok definiuje następująca konfiguracja:
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'] } } },
W tym przypadku używamy nowej właściwości konfiguracyjnej „filtrowanie”. Podobnie jak w naszym poprzednim przykładzie, ta funkcja również opiera się na zależności „Użytkownik”. Jeśli funkcja zwraca obiekt, jest traktowany jako zapytanie MongoDB; zapytanie filtruje kolekcję pod kątem zdarzeń należących tylko do bieżącego użytkownika.
Inną ciekawą właściwością są „Widoki”. „Widok” jest zwykłym typem encji, ale jego kolekcja MongoDB jest taka sama, jak w przypadku nadrzędnego typu encji. Umożliwia to tworzenie wizualnie różnych widoków dla tych samych danych w bazie danych. W rzeczywistości użyliśmy tej funkcji do utworzenia dwóch różnych widoków dla „UserEvent”: „MyEvent” i „AppliedUser”. Ponieważ prototyp widoków podrzędnych jest ustawiony na typ jednostki nadrzędnej, właściwości, które nie są zastępowane, są „dziedziczone” z typu nadrzędnego.
Lista uczestników wydarzenia
Po zgłoszeniu się do wydarzenia inni użytkownicy mogą zobaczyć listę wszystkich użytkowników planujących w nim uczestniczyć. Jest to generowane w wyniku następujących elementów konfiguracyjnych w main.js:
AppliedUser: { permissions: { write: [] }, showInGrid: ['user'] } // ... appliedUsers: Fields.relation("Applied users", "AppliedUser", "event")
„AppliedUser” to widok tylko do odczytu dla encji typu „MyEvent”. To uprawnienie tylko do odczytu jest wymuszane przez ustawienie pustej tablicy na właściwość „Zapis” obiektu uprawnień. Ponadto, ponieważ uprawnienie „Odczyt” nie jest zdefiniowane, domyślnie czytanie jest dozwolone dla wszystkich użytkowników.
Rozszerzanie domyślnych implementacji
Typowym backdrawem frameworków RAD jest brak elastyczności. Po zbudowaniu aplikacji i konieczności jej dostosowania możesz napotkać poważne przeszkody. AllcountJS został opracowany z myślą o rozszerzalności i umożliwia wymianę każdego elementu znajdującego się wewnątrz.
Aby to osiągnąć, AllcountJS używa własnej implementacji Dependency Injection (DI). DI pozwala programiście na przesłonięcie domyślnych zachowań frameworka za pomocą punktów rozszerzeń, a jednocześnie pozwala na ponowne wykorzystanie istniejących implementacji. Wiele aspektów rozszerzenia frameworka RAD opisano w dokumentacji. W tej sekcji zbadamy, w jaki sposób możemy rozszerzyć dwa z wielu komponentów we frameworku, logikę po stronie serwera i widoki.
Kontynuując nasz przykład Toptal Community, zintegrujmy zewnętrzne źródło danych w celu agregowania danych o zdarzeniach. Wyobraźmy sobie, że na blogu Toptal pojawiają się posty omawiające plany wydarzeń na dzień przed każdym wydarzeniem. Dzięki Node.js powinno być możliwe przeanalizowanie kanału RSS bloga i wyodrębnienie takich danych. Aby to zrobić, będziemy potrzebować dodatkowych zależności npm, takich jak „request”, „xml2js” (aby załadować kanał RSS Toptal Blog), „q” (aby zaimplementować obietnice) i „moment” (do przeanalizowania dat). Te zależności można zainstalować, wywołując następujący zestaw poleceń:
npm install xml2js npm install request npm install q npm install moment
Stwórzmy kolejny plik JavaScript, nazwijmy go „toptal-community.js” w katalogu toptal-community-allcount i wypełnijmy go następującym tekstem:
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')); } });
W tym pliku definiujemy zależność o nazwie „DiscussionEventsImport”, której możemy użyć w naszym pliku main.js, dodając akcję importu na encji typu „Event”.
{ id: "import-blog-events", name: "Import Blog Events", actionTarget: "all-items", perform: function (DiscussionEventsImport, Actions) { return DiscussionEventsImport.importEvents().then(function () { return Actions.refreshResult() }); } }
Ponieważ ważne jest, aby zrestartować serwer po wprowadzeniu pewnych zmian w plikach JavaScript, możesz zabić poprzednią instancję i uruchomić ją ponownie, wykonując to samo polecenie co poprzednio:
node toptal-community.js
Jeśli wszystko pójdzie dobrze, po uruchomieniu akcji „Importuj wydarzenia z bloga” zobaczysz coś takiego jak poniższy zrzut ekranu.
Jak dotąd tak dobrze, ale nie zatrzymujmy się tutaj. Widoki domyślne działają, ale czasami mogą być nudne. Dostosujmy je trochę.
Lubisz karty? Każdy lubi karty! Aby utworzyć widok karty, umieść następujące elementy w pliku o nazwie events.jade w katalogu 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()
Następnie po prostu odwołaj się do encji „Event” w main.js jako „customView: „events”.” Uruchom swoją aplikację i powinieneś zobaczyć interfejs oparty na kartach zamiast domyślnego tabelarycznego.
Wniosek
Obecnie proces tworzenia aplikacji internetowych jest podobny w wielu technologiach internetowych, w których niektóre operacje są powtarzane w kółko. Czy naprawdę warto? Może nadszedł czas, aby przemyśleć sposób tworzenia aplikacji internetowych?
AllcountJS zapewnia alternatywne podejście do ram szybkiego tworzenia aplikacji; zaczynasz od utworzenia szkieletu aplikacji, definiując opisy encji, a następnie dodając widoki i dostosowania zachowań wokół niego. Jak widać, z AllcountJS stworzyliśmy prostą, ale w pełni funkcjonalną aplikację, w mniej niż stu linijkach kodu. Może nie spełnia wszystkich wymagań produkcyjnych, ale można go dostosować. Wszystko to sprawia, że AllcountJS jest dobrym narzędziem do szybkiego ładowania aplikacji internetowych.