Sviluppo di applicazioni con Rapid Application Development Framework AllcountJS
Pubblicato: 2022-03-11L'idea di Rapid Application Development (RAD) è nata in risposta ai tradizionali modelli di sviluppo a cascata. Esistono molte varianti di RAD; ad esempio, lo sviluppo agile e il processo unificato razionale. Tuttavia, tutti questi modelli hanno una cosa in comune: mirano a produrre il massimo valore aziendale con tempi di sviluppo minimi attraverso la prototipazione e lo sviluppo iterativo. A tal fine, il modello di sviluppo rapido delle applicazioni si basa su strumenti che semplificano il processo. In questo articolo esploreremo uno di questi strumenti e come può essere utilizzato per concentrarsi sul valore aziendale e sull'ottimizzazione del processo di sviluppo.
AllcountJS è un framework open source emergente creato pensando allo sviluppo rapido di applicazioni. Si basa sull'idea di sviluppo di applicazioni dichiarativo utilizzando un codice di configurazione simile a JSON che descrive la struttura e il comportamento dell'applicazione. Il framework è stato costruito su Node.js, Express, MongoDB e fa molto affidamento su AngularJS e Twitter Bootstrap. Sebbene dipenda da modelli dichiarativi, il framework consente comunque un'ulteriore personalizzazione attraverso l'accesso diretto all'API ove necessario.
Perché AllcountJS come tuo framework RAD?
Secondo Wikipedia, ci sono almeno cento strumenti che promettono un rapido sviluppo di applicazioni, ma questo solleva la domanda: quanto sia rapido "rapido". Questi strumenti consentono di sviluppare una particolare applicazione incentrata sui dati in poche ore? O, forse, è "rapido" se l'applicazione può essere sviluppata in pochi giorni o poche settimane. Alcuni di questi strumenti affermano addirittura che bastano pochi minuti per produrre un'applicazione funzionante. Tuttavia, è improbabile che tu possa creare un'applicazione utile in meno di cinque minuti e affermare comunque di aver soddisfatto ogni esigenza aziendale. AllcountJS non pretende di essere uno strumento del genere; quello che offre AllcountJS è un modo per prototipare un'idea in un breve periodo di tempo.
Con il framework AllcountJS, è possibile creare un'applicazione con un'interfaccia utente a tema generata automaticamente, funzionalità di gestione degli utenti, API RESTful e una manciata di altre funzionalità con il minimo sforzo e tempo. È possibile utilizzare AllcountJS per un'ampia varietà di casi d'uso, ma si adatta meglio alle applicazioni in cui sono presenti raccolte di oggetti diverse con viste diverse per loro. In genere, le applicazioni aziendali si adattano bene a questo modello.
AllcountJS è stato utilizzato per creare allcountjs.com, oltre a un tracker di progetto per esso. Vale la pena notare che allcountjs.com è un'applicazione AllcountJS personalizzata e che AllcountJS consente di combinare visualizzazioni statiche e dinamiche con poca fatica. Consente anche di inserire parti caricate dinamicamente in contenuto statico. Ad esempio, AllcountJS gestisce una raccolta di modelli di applicazioni demo. C'è un widget demo nella pagina principale di allcountjs.com che carica un modello di applicazione casuale da quella raccolta. Una manciata di altre applicazioni di esempio sono disponibili nella galleria allcountjs.com.
Iniziare
Per dimostrare alcune delle capacità del framework RAD AllcountJS, creeremo una semplice applicazione per Toptal, che chiameremo Toptal Community. Se segui il nostro blog, potresti già sapere che un'applicazione simile è stata creata utilizzando Hoodie come parte di uno dei nostri precedenti post sul blog. Questa applicazione consentirà ai membri della comunità di iscriversi, creare eventi e fare domanda per parteciparvi.
Per configurare l'ambiente, è necessario installare Node.js, MongoDB e Git. Quindi, installa AllcountJS CLI invocando un comando "npm install" ed esegui project init:
npm install -g allcountjs-cli allcountjs init toptal-community-allcount cd toptal-community-allcount npm install
AllcountJS CLI ti chiederà di inserire alcune informazioni sul tuo progetto per precompilare package.json.
AllcountJS può essere utilizzato come server autonomo o come dipendenza. Nel nostro primo esempio non estenderemo AllcountJS, quindi un server autonomo dovrebbe funzionare per noi.
All'interno di questa directory app-config appena creata, sostituiremo il contenuto del file JavaScript main.js con il seguente frammento di codice:
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") } } } } } });
Sebbene AllcountJS funzioni con i repository Git, per semplicità non lo useremo in questo tutorial. Per eseguire l'applicazione Toptal Community, tutto ciò che dobbiamo fare è invocare il comando AllcountJS CLI run nella directory toptal-community-allcount.
allcountjs run
Vale la pena notare che MongoDB dovrebbe essere in esecuzione quando viene eseguito questo comando. Se tutto va bene, l'applicazione dovrebbe essere attiva e funzionante all'indirizzo http://localhost:9080.
Per effettuare il login utilizzare il nome utente “admin” e la password “admin”.
Meno di 100 righe
Potresti aver notato che l'applicazione definita in main.js richiedeva solo 91 righe di codice. Queste righe includono la dichiarazione di tutti i comportamenti che potresti osservare quando navighi su http://localhost:9080. Allora, cosa sta succedendo esattamente, sotto il cofano? Diamo un'occhiata più da vicino a ogni aspetto dell'applicazione e vediamo come il codice si collega ad essi.
Accedi e registrati
La prima pagina che vedi dopo aver aperto l'applicazione è un accesso. Questo funge anche da pagina di registrazione, supponendo che la casella di controllo - denominata "Registrati" - sia selezionata prima di inviare il modulo.
Questa pagina viene visualizzata perché il file main.js dichiara che solo gli utenti autenticati possono utilizzare questa applicazione. Inoltre, consente agli utenti di registrarsi da questa pagina. Le seguenti due righe sono tutto ciò che era necessario per questo:
A.app({ ..., onlyAuthenticated: true, allowSignUp: true, ... })
Pagina di benvenuto
Dopo l'accesso, verrai reindirizzato a una pagina di benvenuto con un menu dell'applicazione. Questa parte dell'applicazione viene generata automaticamente, in base alle voci di menu definite sotto il tasto "menuItems".
Insieme ad un paio di altre configurazioni rilevanti, il menu è definito nel file main.js come segue:
A.app({ ..., appName: "Toptal Community", appIcon: "rocket", menuItems: [{ name: "Events", entityTypeId: "Event", icon: "calendar" }, { name: "My Events", entityTypeId: "MyEvent", icon: "calendar" }], ... });
AllcountJS utilizza le icone di Font Awesome, quindi tutti i nomi delle icone a cui si fa riferimento nella configurazione sono mappati sui nomi delle icone di Font Awesome.
Navigazione e modifica degli eventi
Dopo aver cliccato su "Eventi" dal menu, verrai indirizzato alla vista Eventi mostrata nello screenshot qui sotto. È una vista AllcountJS standard che fornisce alcune funzionalità CRUD generiche sulle entità corrispondenti. Qui puoi cercare eventi, creare nuovi eventi e modificare o eliminare quelli esistenti. Ci sono due modalità di questa interfaccia CRUD: list e form. Questa parte dell'applicazione viene configurata tramite le seguenti righe di codice 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]], ... } } } });
Questo esempio mostra come vengono configurate le descrizioni delle entità in AllcountJS. Nota come stiamo usando una funzione per definire le entità; ogni proprietà della configurazione di AllcountJS può essere una funzione. Queste funzioni possono richiedere la risoluzione delle dipendenze tramite i nomi degli argomenti. Prima che la funzione venga chiamata, vengono iniettate le dipendenze appropriate. Qui, "Fields" è una delle API di configurazione di AllcountJS utilizzate per descrivere i campi di entità. La proprietà "Entities" contiene coppie nome-valore in cui il nome è un identificatore del tipo di entità e il valore è la sua descrizione. In questo esempio viene descritto un tipo di entità per gli eventi, dove il titolo è "Eventi". Altre configurazioni, come l'ordinamento predefinito, il nome di riferimento e simili, possono anche essere definite qui. L'ordinamento predefinito è definito tramite un array di nomi di campo e direzioni, mentre il nome di riferimento è definito tramite una stringa (leggi di più qui).
Questo particolare tipo di entità è stato definito con quattro campi: "eventName", "date", "time" e "appliedUsers", i primi tre dei quali sono persistenti nel database. Questi campi sono obbligatori, come indicato dall'uso di "required()". I valori in questi campi con tali regole vengono convalidati prima che il modulo venga inviato sul front-end, come mostrato nello screenshot seguente. AllcountJS combina le convalide lato client e lato server per fornire la migliore esperienza utente. Il quarto campo è una relazione che contiene un elenco di utenti che hanno presentato domanda per partecipare all'evento. Naturalmente, questo campo non è persistente nel database, e viene popolato selezionando solo le entità AppliedUser rilevanti per l'evento.
Candidarsi per partecipare agli eventi
Quando un utente seleziona un particolare evento, la barra degli strumenti mostra un pulsante con l'etichetta "Applica". Facendo clic su di esso si aggiunge l'evento alla pianificazione dell'utente. In AllcountJS, azioni simili a questa possono essere configurate semplicemente dichiarandole nella configurazione:

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") }); } }); }) } }]
La proprietà "actions" di qualsiasi tipo di entità accetta una matrice di oggetti che descrivono il comportamento di ogni azione personalizzata. Ogni oggetto ha una proprietà "id" che definisce un identificatore univoco per l'azione, la proprietà "name" definisce il nome visualizzato e la proprietà "actionTarget" viene utilizzata per definire il contesto dell'azione. L'impostazione di "actionTarget" su "single-item" indica che l'azione deve essere eseguita con un particolare evento. Una funzione definita nella proprietà "esegui" è la logica eseguita quando viene eseguita questa azione, in genere quando l'utente fa clic sul pulsante corrispondente.
Le dipendenze possono essere richieste da questa funzione. Ad esempio, in questo esempio la funzione dipende da "Utente", "Azioni" e "Crud". Quando si verifica un'azione, è possibile ottenere un riferimento all'utente, invocando questa azione, richiedendo la dipendenza “Utente”. Anche qui viene richiesta la dipendenza "Crud", che consente la manipolazione dello stato del database per queste entità. I due metodi che restituiscono un'istanza dell'oggetto Crud sono: Il metodo "actionContextCrud()" - restituisce CRUD per il tipo di entità "Event" poiché l'azione "Apply" appartiene ad esso, mentre il metodo "crudForEntityType()" - restituisce CRUD per qualsiasi tipo di entità identificato dal relativo ID di tipo.
L'implementazione dell'azione inizia controllando se questo evento è già programmato per l'utente e, in caso contrario, ne crea uno. Se è già pianificato, viene visualizzata una finestra di dialogo restituendo il valore dalla chiamata "Actions.modalResult()". Oltre a mostrare una modale, un'azione può eseguire diversi tipi di operazioni in modo simile, come "vai alla visualizzazione", "aggiorna la visualizzazione", "mostra finestra di dialogo" e così via.
Programma utente degli eventi applicati
Dopo aver applicato con successo un evento, il browser viene reindirizzato alla vista "I miei eventi", che mostra un elenco di eventi a cui l'utente si è applicato. La vista è definita dalla seguente configurazione:
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'] } } },
In questo caso, stiamo utilizzando una nuova proprietà di configurazione, "filtraggio". Come nel nostro esempio precedente, anche questa funzione si basa sulla dipendenza "Utente". Se la funzione restituisce un oggetto, viene trattata come una query MongoDB; la query filtra la raccolta per eventi che appartengono solo all'utente corrente.
Un'altra proprietà interessante è "Viste". "View" è un tipo di entità normale, ma la sua raccolta MongoDB è la stessa del tipo di entità padre. Ciò consente di creare viste visivamente diverse per gli stessi dati nel database. In effetti, abbiamo utilizzato questa funzione per creare due viste diverse per "UserEvent:" "MyEvent" e "AppliedUser". Poiché il prototipo delle viste secondarie è impostato sul tipo di entità padre, le proprietà che non vengono sovrascritte vengono "ereditate" dal tipo padre.
Elenco dei partecipanti all'evento
Dopo aver fatto domanda per un evento, altri utenti possono vedere un elenco di tutti gli utenti che intendono partecipare. Questo viene generato come risultato dei seguenti elementi di configurazione in main.js:
AppliedUser: { permissions: { write: [] }, showInGrid: ['user'] } // ... appliedUsers: Fields.relation("Applied users", "AppliedUser", "event")
"AppliedUser" è una vista di sola lettura per un tipo di entità "MyEvent". Questa autorizzazione di sola lettura viene applicata impostando una matrice vuota sulla proprietà "Scrittura" dell'oggetto autorizzazioni. Inoltre, poiché l'autorizzazione "Lettura" non è definita, per impostazione predefinita, la lettura è consentita a tutti gli utenti.
Estensione delle implementazioni predefinite
Il tipico backdraw dei framework RAD è la mancanza di flessibilità. Una volta che hai creato la tua app e devi personalizzarla, potresti incontrare ostacoli significativi. AllcountJS è sviluppato pensando all'estendibilità e consente la sostituzione di ogni elemento costitutivo all'interno.
Per ottenere ciò, AllcountJS utilizza la propria implementazione di Dependency Injection (DI). DI consente allo sviluppatore di ignorare i comportamenti predefiniti del framework tramite punti di estensione e, allo stesso tempo, lo consente attraverso il riutilizzo delle implementazioni esistenti. Molti aspetti dell'estensione del framework RAD sono descritti nella documentazione. In questa sezione, esploreremo come estendere due dei numerosi componenti del framework, la logica lato server e le viste.
Continuando con il nostro esempio di Toptal Community, integriamo un'origine dati esterna per aggregare i dati degli eventi. Immaginiamo che ci siano post del blog Toptal che discutono dei piani per gli eventi il giorno prima di ogni evento. Con Node.js dovrebbe essere possibile analizzare il feed RSS del blog ed estrarre tali dati. Per fare ciò, avremo bisogno di alcune dipendenze npm aggiuntive, come "request", "xml2js" (per caricare il feed RSS di Toptal Blog), "q" (per implementare le promesse) e "moment" (per analizzare le date). Queste dipendenze possono essere installate invocando il seguente set di comandi:
npm install xml2js npm install request npm install q npm install moment
Creiamo un altro file JavaScript, chiamalo "toptal-community.js" nella directory toptal-community-allcount e popolalo con quanto segue:
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')); } });
In questo file, stiamo definendo una dipendenza chiamata "DiscussionEventsImport", che possiamo utilizzare nel nostro file main.js aggiungendo un'azione di importazione sul tipo di entità "Evento".
{ id: "import-blog-events", name: "Import Blog Events", actionTarget: "all-items", perform: function (DiscussionEventsImport, Actions) { return DiscussionEventsImport.importEvents().then(function () { return Actions.refreshResult() }); } }
Poiché è importante riavviare il server dopo aver apportato alcune modifiche ai file JavaScript, è possibile uccidere l'istanza precedente e riavviarla eseguendo lo stesso comando di prima:
node toptal-community.js
Se tutto va bene, vedrai qualcosa come lo screenshot qui sotto dopo aver eseguito l'azione "Importa eventi blog".
Fin qui tutto bene, ma non fermiamoci qui. Le visualizzazioni predefinite funzionano, ma a volte possono essere noiose. Personalizziamoli un po'.
Ti piacciono le carte? A tutti piacciono le carte! Per creare una visualizzazione a scheda, inserisci quanto segue in un file denominato events.jade all'interno della directory 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()
Successivamente, fai semplicemente riferimento all'entità "Event" in main.js come "customView: "events"." Esegui la tua app e dovresti vedere un'interfaccia basata su schede invece di quella tabellare predefinita.
Conclusione
Al giorno d'oggi, il flusso di sviluppo delle applicazioni Web è simile in molte tecnologie Web, in cui alcune operazioni vengono ripetute più e più volte. Ne vale davvero la pena? Forse è giunto il momento di ripensare al modo in cui vengono sviluppate le tue applicazioni web?
AllcountJS fornisce un approccio alternativo ai framework di sviluppo rapido delle applicazioni; si inizia creando uno scheletro per l'applicazione definendo le descrizioni delle entità, quindi si aggiungono viste e personalizzazioni del comportamento attorno ad esso. Come puoi vedere, con AllcountJS abbiamo creato un'applicazione semplice, ma completamente funzionale, in meno di cento righe di codice. Forse non soddisfa tutti i requisiti di produzione, ma è personalizzabile. Tutto ciò rende AllcountJS un buon strumento per il bootstrap rapido delle applicazioni web.