使用快速應用程序開發框架 AllcountJS 進行應用程序開發
已發表: 2022-03-11快速應用程序開發 (RAD) 的概念是針對傳統的瀑布式開發模型而誕生的。 存在許多 RAD 變體; 例如,敏捷開發和 Rational Unified Process。 然而,所有這些模型都有一個共同點:它們旨在通過原型設計和迭代開發以最短的開發時間產生最大的商業價值。 為此,快速應用程序開發模型依賴於簡化流程的工具。 在本文中,我們將探討一種這樣的工具,以及如何使用它來關注業務價值和優化開發過程。
AllcountJS 是一個新興的開源框架,其構建時考慮到了快速應用程序開發。 它基於使用描述應用程序結構和行為的類似 JSON 的配置代碼進行聲明式應用程序開發的思想。 該框架建立在 Node.js、Express、MongoDB 之上,並嚴重依賴 AngularJS 和 Twitter Bootstrap。 儘管它依賴於聲明性模式,但該框架仍然允許通過在需要的地方直接訪問 API 來進一步定制。
為什麼將 AllcountJS 作為您的 RAD 框架?
根據 Wikipedia 的說法,至少有一百種工具可以保證應用程序的快速開發,但這提出了一個問題:“快速”有多快。 這些工具是否允許在幾個小時內開發特定的以數據為中心的應用程序? 或者,如果應用程序可以在幾天或幾週內開發出來,也許是“快速”的。 其中一些工具甚至聲稱只需幾分鐘即可生成一個工作應用程序。 但是,您不太可能在五分鐘內構建一個有用的應用程序,並且仍然聲稱滿足了所有業務需求。 AllcountJS 並沒有聲稱自己是這樣的工具。 AllcountJS 提供的是一種在短時間內製作創意原型的方法。
使用 AllcountJS 框架,可以用最少的精力和時間構建具有主題自動生成的用戶界面、用戶管理功能、RESTful API 和一些其他功能的應用程序。 可以將 AllcountJS 用於各種用例,但它最適合具有不同對象集合和不同視圖的應用程序。 通常,業務應用程序非常適合此模型。
AllcountJS 已被用於構建 allcountjs.com,並為其添加了一個項目跟踪器。 值得注意的是,allcountjs.com 是一個定制的 AllcountJS 應用程序,並且 AllcountJS 允許將靜態和動態視圖結合起來毫不費力。 它甚至允許將動態加載的部分插入到靜態內容中。 例如,AllcountJS 管理一組演示應用程序模板。 allcountjs.com 的主頁上有一個演示小部件,可從該集合加載隨機應用程序模板。 allcountjs.com 的圖庫中提供了一些其他示例應用程序。
入門
為了演示 RAD 框架 AllcountJS 的一些功能,我們將為 Toptal 創建一個簡單的應用程序,我們將其稱為 Toptal 社區。 如果您關注我們的博客,您可能已經知道使用 Hoodie 作為我們早期博客文章的一部分構建了一個類似的應用程序。 此應用程序將允許社區成員註冊、創建活動併申請參加。
為了設置環境,您應該安裝 Node.js、MongoDB 和 Git。 然後,通過調用“npm install”命令安裝 AllcountJS CLI 並執行項目初始化:
npm install -g allcountjs-cli allcountjs init toptal-community-allcount cd toptal-community-allcount npm install
AllcountJS CLI 將要求您輸入有關您的項目的一些信息,以便預填充 package.json。
AllcountJS 可以用作獨立服務器或依賴項。 在我們的第一個示例中,我們不會擴展 AllcountJS,因此獨立服務器應該只為我們工作。
在這個新創建的 app-config 目錄中,我們將 main.js JavaScript 文件的內容替換為以下代碼片段:
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 社區應用程序,我們所要做的就是在 toptal-community-allcount 目錄中調用 AllcountJS CLI 運行命令。
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 配置的每個屬性都可以是一個函數。 這些函數可以請求通過其參數名稱解析依賴項。 在調用函數之前,會注入適當的依賴項。 這裡,“Fields”是用於描述實體字段的 AllcountJS 配置 API 之一。 屬性“實體”包含名稱-值對,其中名稱是實體類型標識符,值是其描述。 在此示例中,描述了事件的實體類型,其中標題為“事件”。 其他配置,例如默認排序、參考名稱等,也可以在這裡定義。 默認排序順序是通過字段名稱和方向的數組定義的,而引用名稱是通過字符串定義的(在此處閱讀更多信息)。
這種特定的實體類型被定義為具有四個字段:“eventName”、“date”、“time”和“appliedUsers”,其中前三個字段被持久保存在數據庫中。 這些字段是強制性的,如“required()”的使用所示。 在前端提交表單之前,驗證這些具有此類規則的字段中的值,如下面的屏幕截圖所示。 AllcountJS 結合了客戶端和服務器端驗證,以提供最佳的用戶體驗。 第四個字段是一個關係,其中包含已申請參加活動的用戶列表。 自然,該字段不會保留在數據庫中,而是通過僅選擇與事件相關的那些 AppliedUser 實體來填充。
申請參加活動
當用戶選擇特定事件時,工具欄會顯示一個標有“應用”的按鈕。 單擊它會將事件添加到用戶的日程表中。 在 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”表示應該使用特定事件執行操作。 在屬性“perform”下定義的函數是執行此操作時執行的邏輯,通常是當用戶單擊相應按鈕時。

此函數可能會請求依賴項。 例如,在本例中,函數依賴於“User”、“Actions”和“Crud”。 當一個動作發生時,可以通過要求“用戶”依賴來獲得對用戶的引用,調用這個動作。 這裡還需要“Crud”依賴項,它允許為這些實體操作數據庫狀態。 返回 Crud 對象實例的兩個方法是: 方法“actionContextCrud()” - 為“Event”實體類型返回 CRUD,因為操作“Apply”屬於它,而方法“crudForEntityType()” - 返回 CRUD對於由其類型 ID 標識的任何實體類型。
操作的實現首先檢查是否已經為用戶安排了此事件,如果沒有,則創建一個。 如果它已經被調度,則通過返回調用“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”實體類型的只讀視圖。 通過將空數組設置為權限對象的“寫入”屬性來強制執行此只讀權限。 此外,由於未定義“讀取”權限,默認情況下,所有用戶都允許讀取。
擴展默認實現
RAD 框架的典型缺陷是缺乏靈活性。 構建應用程序並需要對其進行自定義後,您可能會遇到重大障礙。 AllcountJS 的開發考慮了可擴展性,並允許替換內部的每個構建塊。
為了實現這一點,AllcountJS 使用它自己的依賴注入 (DI) 實現。 DI 允許開發人員通過擴展點覆蓋框架的默認行為,同時允許通過重用現有實現來實現。 文檔中描述了 RAD 框架擴展的許多方面。 在本節中,我們將探討如何擴展框架中眾多組件中的兩個,服務器端邏輯和視圖。
繼續我們的 Toptal 社區示例,讓我們集成一個外部數據源來聚合事件數據。 讓我們假設有 Toptal 博客帖子在每個活動的前一天討論活動計劃。 使用 Node.js,應該可以解析博客的 RSS 提要並提取此類數據。 為此,我們將需要一些額外的 npm 依賴項,例如“request”、“xml2js”(加載 Toptal 博客 RSS 提要)、“q”(實現承諾)和“moment”(解析日期)。 可以通過調用以下命令集來安裝這些依賴項:
npm install xml2js npm install request npm install q npm install moment
讓我們創建另一個 JavaScript 文件,在 toptal-community-allcount 目錄中將其命名為“toptal-community.js”,並使用以下內容填充它:
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”的依賴項,我們可以通過在“Event”實體類型上添加一個導入操作在 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
如果一切順利,您將在運行“導入博客事件”操作後看到類似下面的屏幕截圖。
到目前為止一切順利,但我們不要止步於此。 默認視圖有效,但有時它們可能很無聊。 讓我們對它們進行一些定制。
你喜歡卡片嗎? 每個人都喜歡卡片! 要製作卡片視圖,請將以下內容放入 app-config 目錄中名為 events.jade 的文件中:
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 中的“Event”實體中將其引用為“customView: “events”。” 運行您的應用程序,您應該會看到基於卡片的界面,而不是默認的表格界面。
結論
如今,Web 應用程序的開發流程在許多 Web 技術中都是相似的,其中一些操作會一遍又一遍地重複。 是不是真的值得嗎? 也許,是時候重新考慮開發 Web 應用程序的方式了?
AllcountJS 為快速應用程序開發框架提供了另一種方法; 您首先通過定義實體描述為應用程序創建骨架,然後圍繞它添加視圖和行為自定義。 如您所見,使用 AllcountJS,我們用不到一百行代碼創建了一個簡單但功能齊全的應用程序。 也許,它不能滿足所有生產要求,但它是可定制的。 所有這些使 AllcountJS 成為快速引導 Web 應用程序的好工具。