快速应用程序开发框架AllcountJS进行开发

本文概述

快速应用程序开发(RAD)的想法是对传统瀑布式开发模型的回应。 RAD存在许多变体。例如, 敏捷开发和Rational Unified Process。但是, 所有这些模型都有一个共同点:它们旨在通过原型设计和迭代开发以最少的开发时间获得最大的业务价值。为此, 快速应用程序开发模型依赖于简化流程的工具。在本文中, 我们将探讨一种这样的工具, 以及如何将其用于关注业务价值和开发过程的优化。

AllcountJS是一个新兴的开源框架, 旨在快速开发应用程序。它基于使用类似于JSON的配置代码进行声明式应用程序开发的思想, 该代码描述了应用程序的结构和行为。该框架建立在Node.js, Express, MongoDB之上, 并且高度依赖AngularJS和Twitter Bootstrap。尽管它依赖于声明式模式, 但该框架仍允许在需要时通过直接访问API来进行进一步的自定义。

AllcountJS作为你的RAD框架

为什么将AllcountJS作为你的RAD框架?

根据Wikipedia的说法, 至少有一百种工具可以保证应用程序的快速开发, 但这提出了一个问题:”快速”有多快。这些工具是否允许在几个小时内开发特定的以数据为中心的应用程序?或者, 如果可以在几天或几周内开发应用程序, 那将是”快速的”。其中一些工具甚至声称几分钟就可以生成一个正常的应用程序。但是, 你不太可能在五分钟之内构建一个有用的应用程序, 而仍然声称满足了所有业务需求。 AllcountJS并不声称是这样的工具; AllcountJS提供的是一种在短时间内原型化想法的方法。

借助AllcountJS框架, 可以以最小的工作量和最少的时间构建具有主题化的自动生成的用户界面, 用户管理功能, RESTful API以及少数其他功能的应用程序。可以将AllcountJS用于各种各样的用例, 但是它最适合于你拥有具有不同视图对象的不同对象集合的应用程序。通常, 业务应用程序非常适合此模型。

AllcountJS已用于构建allcountjs.com, 并为其添加了项目跟踪器。值得注意的是, allcountjs.com是一个自定义的AllcountJS应用程序, 并且AllcountJS允许将静态视图和动态视图组合在一起而不会带来麻烦。它甚至允许将动态加载的零件插入静态内容。例如, AllcountJS管理演示应用程序模板的集合。 allcountjs.com主页上有一个演示小部件, 可从该集合中加载随机的应用程序模板。在allcountjs.com的库中可以找到其他一些示例应用程序。

入门

为了演示RAD框架AllcountJS的某些功能, 我们将为srcmini创建一个简单的应用程序, 我们将其称为srcmini社区。如果你关注我们的博客, 则可能已经知道使用Hoodie构建了类似的应用程序, 这是我们较早的博客文章之一。该应用程序将允许社区成员注册, 创建活动并申请参加。

为了设置环境, 你应该安装Node.js, MongoDB和Git。然后, 通过调用” npm install”命令安装AllcountJS CLI并执行项目初始化:

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

AllcountJS CLI将要求你输入一些有关项目的信息, 以便预填充package.json。

AllcountJS可以用作独立服务器或依赖项。在我们的第一个示例中, 我们不会扩展AllcountJS, 因此独立服务器应该对我们有用。

在这个新创建的app-config目录中, 我们将用以下代码片段替换main.js JavaScript文件的内容:

A.app({
  appName: "srcmini 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存储库, 但为简单起见, 我们在本教程中不会使用它。要运行srcmini Community应用程序, 我们要做的就是在srcmini-community-allcount目录中调用AllcountJS CLI run命令。

allcountjs run

值得注意的是, 执行此命令时, MongoDB应该正在运行。如果一切顺利, 则该应用程序应已启动并在http:// localhost:9080上运行。

要登录, 请使用用户名” admin”和密码” admin”。

少于100线

你可能已经注意到main.js中定义的应用程序仅使用了91行代码。这些行包括对导航到http:// localhost:9080时可能会观察到的所有行为的声明。那么, 到底发生了什么呢?让我们仔细研究应用程序的各个方面, 并查看代码与它们之间的关系。

登录注册

打开应用程序后, 你看到的第一页是登录页面。如果你在提交表单之前选中了” Sign Up”复选框, 则该页面将作为注册页面加倍。

登录注册

显示此页面的原因是main.js文件声明只有经过身份验证的用户才能使用此应用程序。此外, 它使用户能够从此页面注册。以下两行是完成此操作所必需的:

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

欢迎页面

登录后, 你将通过应用程序菜单重定向到欢迎页面。应用程序的这一部分是根据” menuItems”键下定义的菜单项自动生成的。

欢迎页面示例

连同其他几个相关配置, 菜单在main.js文件中定义如下:

A.app({
  ..., appName: "srcmini 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配置的每个属性都可以是一个函数。这些函数可以要求通过其参数名称来解决依赖项。在调用该函数之前, 将注入适当的依赖项。在这里, “字段”是用于描述实体字段的AllcountJS配置API之一。属性”实体”包含名称/值对, 其中名称是实体类型标识符, 而值是其描述。在此示例中, 描述了事件的实体类型, 其中标题为”事件”。其他配置, 例如默认排序顺序, 参考名称等, 也可以在此处定义。默认排序顺序是通过字段名称和方向的数组定义的, 而参考名称是通过字符串定义的(在此处了解更多)。

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”表示该操作应在特定事件下执行。在属性” perform”下定义的功能是执行此操作时执行的逻辑, 通常是在用户单击相应按钮时执行。

此功能可能要求依赖关系。例如, 在此示例中, 功能取决于”用户”, “操作”和” Crud”。发生操作时, 可以通过要求”用户”依赖项来获得对调用此操作的用户的引用。在此还要求” Crud”依赖性, 该依赖性允许操纵这些实体的数据库状态。返回Crud对象实例的两个方法是:方法” actionContextCrud()”-由于” Apply”操作属于它, 因此为”事件”实体类型返回CRUD, 而方法” crudForEntityType()”-返回CRUD。对于由其类型ID标识的任何实体类型。

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”实体类型的只读视图。通过将一个空数组设置为权限对象的” Write”属性来强制执行此只读权限。另外, 由于未定义”阅读”权限, 因此默认情况下, 所有用户都可以阅读。

myevent类型的AppliedUser

扩展默认实现

RAD框架的典型缺点是缺乏灵活性。构建完应用程序并需要对其进行自定义后, 可能会遇到很多障碍。 AllcountJS在开发时就考虑了可扩展性, 并允许替换其中的每个构造块。

为了实现AllcountJS使用其自己的依赖注入(DI)实现。 DI允许开发人员通过扩展点覆盖框架的默认行为, 并同时允许其通过重用现有实现来实现。文档中介绍了RAD框架扩展的许多方面。在本节中, 我们将探讨如何扩展框架中的许多组件中的两个, 即服务器端逻辑和视图。

继续我们的srcmini社区示例, 让我们集成外部数据源以汇总事件数据。假设有srcmini Blog帖子讨论了每个活动的前一天的活动计划。使用Node.js, 应该可以解析博客的RSS提要并提取此类数据。为此, 我们将需要一些额外的npm依赖项, 例如”请求”, ” xml2js”(用于加载srcmini Blog RSS feed), ” q”(用于实现承诺)和” moment”(用于解析日期)。可以通过调用以下命令来安装这些依赖项:

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

让我们创建另一个JavaScript文件, 在srcmini-community-allcount目录中将其命名为” srcmini-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/srcmini-community');
injection.bindFactory('gitRepoUrl', 'app-config');

injection.bindFactory('DiscussionEventsImport', function (Crud) {
    return {
        importEvents: function () {
            return Q.nfcall(request, "https://www.srcmini02.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 srcmini-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中的”事件”实体将其引用为” customView:”事件”即可。”运行你的应用程序, 你应该看到基于卡的界面, 而不是默认的表格界面。

main.js中的事件实体

总结

如今, Web应用程序的开发流程在许多Web技术中都是相似的, 其中某些操作一遍又一遍地重复。是不是真的值得吗?也许是时候重新考虑Web应用程序的开发方式了吗?

AllcountJS为快速的应用程序开发框架提供了另一种方法。首先, 通过定义实体描述为应用程序创建框架, 然后在该框架周围添加视图和行为自定义项。如你所见, 使用AllcountJS, 我们用不到一百行代码创建了一个简单但功能齐全的应用程序。也许它不能满足所有生产要求, 但可以自定义。所有这些使AllcountJS成为快速引导Web应用程序的好工具。

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?