使用快速应用程序开发框架 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 应用程序的好工具。