Java Spring MVC 单页应用,使用 Upida/Jeneva (前端/AngularJS)





5.00/5 (9投票s)
使用 JSON 进行 Web 开发很简单。
引言
在上一篇文章中,我演示了如何使用 Java Spring MVC 和 Jeneva 创建一个 JSON 驱动的 Web 后端。在本文中,我将展示一些创建单页前端的基本技巧,使用 AngularJS。如果您不熟悉 AngularJS 的基础知识,请参考这个 YouTube 频道。
背景
我们假设我们已经有一个工作的后端,带有 MVC 控制器,可以提供 HTML 视图,并且为我们提供了一个方便的 JSON API。
@Controller
@RequestMapping({"/client"})
public class ClientController {
private IClientService clientService;
@Autowired
public ClientController(IClientService clientService) {
this.clientService = clientService;
}
@RequestMapping(value={"/list"})
public String list() {
return "client/list";
}
@RequestMapping("/create")
public String create() {
return "client/create";
}
@RequestMapping("/edit")
public String edit() {
return "client/edit";
}
@RequestMapping("/getbyid")
@ResponseBody
public Client getById(int id) {
return this.clientService.getById(id);
}
@RequestMapping("/getall")
@ResponseBody
public List<client> getAll() {
return this.clientService.getAll();
}
@RequestMapping("/save")
@ResponseBody
public void save(@RequestBody Client item) {
this.clientService.save(item);
}
@RequestMapping("/update")
@ResponseBody
public void update(@RequestBody Client item) {
this.clientService.update(item);
}
@RequestMapping("/delete/{id}")
@ResponseBody
public void delete(@PathVariable("id") int id) {
this.clientService.delete(id);
}
该控制器看起来非常简单且可用。请参阅我的上一篇文章,了解我是如何创建后端的。我的目标是为这两个控制器创建前端。前端必须是一个单页浏览器应用程序 (SPA)。许多人认为 SPA 并非一种有用的技术,因为它不产生指向不同页面的链接,而且 SPA 需要付出巨大的努力,不值得。我将向您展示 AngularJS 使创建 SPA 和创建多页应用程序一样简单,并且多页应用程序的所有优点都保留在 SPA 中。
基本结构
每个单页应用程序 (SPA) 最重要的部分是其结构。我们将有 3 个视图:“客户端列表”、“创建客户端”和“编辑客户端”。实际上,这意味着我们将为这些视图编写三个 AngularJS 控制器 - clientListController.js、clientCreateController.js 和 clientEditController.js。“vendor”文件夹包含所有第三方库。我们还有自定义过滤器。通常,这种结构也有“directives”和“services”文件夹,但在这种简单的 SPA 中,我没有使用任何自定义指令和服务。
前端的基础部分是位于 js 文件夹根目录下的 app.js 文件。让我们分析一下它的内容。
var myclients = myclients || {};
myclients.app = angular.module("myclients", ["ngRoute", "jenevamodule"]);
myclients.app.config(function ($routeProvider) {
$routeProvider
.when("/", {
templateUrl: "client/list",
controller: "clientListController"
})
.when("/client/list", {
templateUrl: "client/list",
controller: "clientListController"
})
.when("/client/create", {
templateUrl: "client/create",
controller: "clientCreateController"
})
.when("/client/edit/:id", {
templateUrl: "client/edit",
controller: "clientEditController"
})
.otherwise({
templateUrl: "home/notfound"
});
});
$jeneva.baseUrl = "/api/";
在第一行,我定义了应用程序的主命名空间。然后,使用 AngularJS,我创建了我主模块的实例。这是每个 Angular 应用程序的常见任务。该模块依赖于两个不同的模块:ngRoute
和 jenevamodule
,它们都作为独立的、可重用的 angular 模块。最有趣的部分在中间。如您所见,我在这里定义了几个路由。这实际上是任何前端的核心。简单地说 - 在这里,我告诉 AngularJS - 哪个 js 控制器和哪个 视图应该被激活,具体取决于浏览器中的 URL。在任何时候,我的应用程序都只有一个活动的 视图和 控制器,这将取决于在浏览器窗口中输入的 URL,就是这样。例如,如果我输入 url:client/list
,那么 client/list
视图和 clientListController
将被加载并激活。
app.js 的最后一部分是专用于 Jeneva 的。我使用它是因为它使我的应用程序更容易开发和维护。您可以参考我上一篇文章,了解更多关于 Jeneva 的信息。我告诉 Jeneva 我的 JSON API 控制器位于 /api/ 子文件夹中。
JS 控制器
现在,让我们看看控制器。第一个是 clientListController.js。
myclients.app.controller(
"clientListController", ["$scope", "jeneva",
function ($scope, jeneva) {
$scope.clientRows = new Array();
$scope.ClientRow = function (id) {
this.id = id;
this.name = null;
this.lastname = null;
this.age = null;
this.logins = new Array();
};
$scope.loadClients = function() {
jeneva.get("client/getall")
.then(function (items) { // copy the server side json-model
// to the client side model
angular.forEach(items, function (p, i) {
var row = new $scope.ClientRow(p.id);
row.name = p.name;
row.lastname = p.lastname;
row.age = p.age;
angular.forEach(p.logins, function (q, j) {
row.logins.push(q.name);
});
$scope.clientRows.push(row);
});
});
};
$scope.onDelete = function (clientId) {
jeneva.post("client/delete/" + clientId)
.then(function () {
$scope.loadClients();
});
};
$scope.$on("$routeChangeSuccess", function () {
$scope.loadClients();
});
}]);
一些天才开发者可能会问——我为什么要将服务器端的 JSON 模型复制到客户端模型。在这种情况下,这是多余的,直接使用服务器端 JSON 模型作为客户端模型,而无需复制它会更简单。但在大多数情况下,服务器端和客户端模型的结构不同,通过复制它,您可以避免将来的任何问题,并使整个应用程序的代码保持一致。客户端模型 ($scope
) 必须尽可能简单,它不应关心域模型类。您只在与服务器端交互时才需要考虑域模型类,即在接收或发送数据到服务器时。
如您所见,我引用了 app.js 文件前两行创建的 myclients.app 模块。我的控制器名为 clientListController
,并注入了 2 个变量:$scope
和 jeneva
。在控制器的主体中,我定义了 $scope.clientRows
- 这是我的视图的模型 - 数据库中的客户端列表。与后端的交互在 $scope.loadClients
方法中进行,该方法使用 jeneva
服务调用 JSON API 控制器。我使用 jeneva
而不是直接的 angular ($http
) 调用是因为 - jeneva 会自动管理我的后端失败情况,并在视图中的正确位置显示错误消息(基于 jvErrorkey
指令)。当我的 API 返回客户端列表时,我将它们填充到 $scope.clientRows
字段中。
控制器的最后一部分是最重要的。我将一个处理程序附加到 $routeChangeSuccess
事件。每当该控制器变为活动状态时,都会触发此事件,即每次用户导航到“客户端列表”视图时,都会触发此事件。在我的例子中,当触发此事件时,将调用 $scope.loadClients
方法,并从后端提取数据。
其他控制器的工作方式大致相同。它们被注入了 $location
变量,该变量用于在 AngularJS SPA 的视图之间导航。例如,clientCreateController.js
myclients.app.controller(
"clientCreateController",
["$scope", "$location", "jeneva", function ($scope, $location, jeneva) {
$scope.name = null;
$scope.lastname = null;
$scope.age = null;
$scope.loginRows = new Array();
$scope.LoginRow = function () {
this.name = null;
this.password = null;
this.enabled = false;
};
$scope.onRemoveLoginClick = function (item) {
var index = $scope.loginRows.indexOf(item);
$scope.loginRows.splice(index, 1);
};
$scope.onAddLoginClick = function () {
var row = new $scope.LoginRow();
$scope.loginRows.push(row);
};
$scope.onSave = function () {
var data = {};
data.name = $scope.name;
data.lastname = $scope.lastname;
data.age = $scope.age;
data.logins = new Array();
angular.forEach($scope.loginRows, function (p, i) {
var item = {};
item.name = p.name;
item.password = p.password;
item.enabled = p.enabled;
data.logins.push(item);
});
jeneva.post("client/save", data)
.then(function () {
$location.path("client/list");
});
};
$scope.$on("$routeChangeSuccess", function () {
$scope.onAddLoginClick();
});
}]);
看看 $scope.onSave
方法。当用户点击创建客户端视图的保存按钮时,它会触发。它将所有用户输入的数据收集到一个大的 data 变量中,并使用 jeneva 服务将其作为 JSON 发送给 API 控制器。如果数据保存成功,将调用 $location.path()
方法,并将用户导航回客户端列表视图。如果后端失败或后端验证失败,jeneva
服务将处理此问题,并且所有验证问题都将在视图中的正确位置显示,具体取决于 jeneva 指令(本文后面,您将看到 Jeneva 如何处理这些失败)。
clientEditController.js 的工作方式相同,您可以在源代码中看到。
HTML 视图
如您所知,只有三个视图 - 客户端列表、创建客户端和编辑客户端。如果您不熟悉 AngularJS,请参考这个 YouTube 频道。让我们看看列表视图。
<a href="#/client/create">NEW CLIENT</a>
<h2>Clients</h2>
<span class="error" jv-error-key></span>
<hr />
<table border="1" style="width: 50%;">
<thead>
<tr class="head">
<th>ID</th>
<th>NAME</th>
<th>LASTNAME</th>
<th>AGE</th>
<th>LOGINS</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in clientRows">
<td ng-bind="row.id"></td>
<td ng-bind="row.name"></td>
<td ng-bind="row.lastname"></td>
<td ng-bind="row.age"></td>
<td>
<div ng-repeat="login in row.logins">
<span ng-bind="login"></span><br />
</div>
</td>
<td>
<a ng-href="#/client/edit/{{row.id}}">Edit</a>
</td>
</tr>
</tbody>
</table>
对于熟悉 AngularJS 的人来说,理解这段代码会非常简单。此视图与 clientListController.js 链接在一起。基本上,这段代码引用了控制器代码中的 $scope.clientRows
字段,并将客户端列表显示为 HTML 表格。
这里最有趣的地方是链接。看看链接
<a href="#/client/create">NEW CLIENT</a>
正如您所见,URL 以 # 符号开头,这实际上意味着如果您点击链接,浏览器不会重新加载页面。相反,AngularJS 将捕获此事件,并根据 app.js 文件中注册的路由切换活动的视图和活动的控制器。例如,如果您点击 新建客户端 链接,AngularJS 将用创建客户端视图的内容替换当前视图的内容,并且活动的控制器将变为 - clientCreateController.js。
另一个有趣的地方是视图的顶部
<span class="error" jv-error-key></span>
这个 span 由 Jeneva 的 jv-error-key
指令控制。实际上,这意味着当失败响应从后端服务器到达浏览器时,具有空键的错误消息将显示在 span
的主体中。有时,您需要使用无键(或无路径)的错误消息,例如,“意外失败
”消息或更具体的“您不能删除此客户端
”(或者您可以为某些错误消息设置静态键)。无键(或静态键)失败消息最好的地方是它们不需要任何 JSON 数据发布到服务器。除了无键消息外,您还可以随时定义自己的自定义键,例如,您可以定义“server_error
”键(路径),然后您可以将具有该键的失败注册到验证上下文中,然后您可以使用 jv-error-key
指令在表单中的任何位置显示该失败消息。在此示例中,我为未处理的异常使用无键消息,并将它们简单地显示在每个表单的顶部。我还使用无键消息来通知用户由于某种原因不能删除客户端(请参阅 ClientService
中的删除客户端)。
现在看看编辑客户端链接
<a ng-href="#/client/edit/{{row.id}}">Edit</a>
正如您所见,此链接导航到编辑客户端视图,它还包含客户端的 id
。editClientController
必须知道如何从链接中提取此 id
。看看 app.js 文件,编辑客户端视图的路由是如何定义的。
.when("/client/edit/:id", {
templateUrl: "client/edit",
controller: "clientEditController"
})
AngularJS 路由机制能够处理 URL 参数。并且 clientEditController
可以使用 $routeParams
服务访问它们。$routeParams
服务必须以与其他服务相同的方式注入到控制器中。有关更多详细信息,请参见源代码。
现在,让我们看看创建客户端视图。
<a href="#/client/list">ALL CLIENTS</a>|
<h2>New Client</h2>
<span class="error" jv-error-key></span>
<span class="error" jv-error-key="logins"></span>
<hr />
<table ng-form name="form">
<tr>
<td>Name</td>
<td>
<input name="name"
type="text" ng-model="name" jv-path="name" />
<span class="error"
ng-if="form.name.$error.jvpath"
ng-repeat="msg in form.name.$jvlist">{{msg}}</span>
</td>
</tr>
<tr>
<td>Last name</td>
<td>
<input name="lastname"
type="text" ng-model="lastname" jv-path="lastname" />
<span class="error"
ng-if="form.lastname.$error.jvpath"
ng-repeat="msg in form.lastname.$jvlist">{{msg}}</span>
</td>
</tr>
<tr>
<td>Age</td>
<td>
<input name="age"
type="text" ng-model="age" jv-path="age" />
<span class="error"
ng-if="form.age.$error.jvpath"
ng-repeat="msg in form.age.$jvlist">{{msg}}</span>
</td>
</tr>
<tr>
<td>Logins</td>
<td>
<table>
<thead>
<tr>
<th>Login</th>
<th>Password</th>
<th>Enabled</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in loginRows" ng-form name="loginForm">
<td>
<input name="loginName" type="text"
ng-model="row.name"
jv-path="{{'logins['+ $index + '].name'}}" />
<span class="error" ng-if="loginForm.loginName.$error.jvpath"
ng-repeat="msg in loginForm.loginName.$jvlist">{{msg}}</span>
</td>
<td>
<input name="password" type="text"
ng-model="row.password" jv-path="{{'logins['+
$index + '].password'}}" />
<span class="error"
ng-if="loginForm.password.$error.jvpath"
ng-repeat="msg in loginForm.password.$jvlist">{{msg}}</span>
</td>
<td>
<input name="enabled" type="checkbox"
ng-model="row.enabled"
jv-path="{{'logins['+ $index + '].enabled'}}" />
<span class="error" ng-if="loginForm.enabled.$error.jvpath"
ng-repeat="msg in loginForm.enabled.$jvlist">{{msg}}</span>
</td>
<td>
<input type="button"
ng-click="onRemoveLoginClick(row)" value="Delete" />
</td>
</tr>
</tbody>
</table>
<div style="padding-bottom: 0.5em;">
<input type="button"
ng-click="onAddLoginClick()" value="Add" />
</div>
</td>
</tr>
</table>
<hr />
<input type="button" ng-click="onSave()" value="Save" />
每个 input
元素都使用 ng-model
Angular 指令绑定到控制器相应的 $scope
字段。每个 input
元素后面都有一个错误消息 span
。
<input name="name" type="text"
ng-model="name" jv-path="name" />
<span class="error" ng-if="form.name.$error.jvpath"
ng-repeat="msg in form.name.$jvlist">{{msg}}</span>
或者像这样:
<tr ng-repeat="row in loginRows" ng-form name="loginForm">
<td>
<input name="loginName" type="text" ng-model="row.name"
jv-path="{{'logins['+ $index + '].name'}}" />
<span class="error" ng-if="loginForm.loginName.$error.jvpath"
ng-repeat="msg in loginForm.loginName.$jvlist">{{msg}}</span>
正如您所见,span
使用 ng-if
和 ng-repeat
指令进行装饰。基本上,这意味着,如果 loginForm
的 name
字段验证失败,则 span 将显示错误消息。第二种情况看起来很奇怪,但也很简单。jv-path="{{'logins['+ $index + '].name'}}"
这意味着如果 logins[0].name
字段失败 - 那么错误消息将放置在正确的位置。
视图容器
好了,基本上就是这样。我们有视图,有控制器,有配置一切的 app.js。最后一步是创建一个页面,单页。这个页面托管所有视图和控制器。当用户导航到 Web 应用程序的根目录时,将显示此页面。我的容器页面位于此处:views/home/Index.cshtml。它看起来非常简单
<!DOCTYPE html>
<html ng-app="myclients">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>MyClients Single Page Angular Application</title>
<script type="text/javascript"
src="@Url.Content("/Resources/js/vendor/angular.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/vendor/angular-route.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/vendor/jeneva.angular.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/app.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/filters/idtext.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/filters/datetime.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/controllers/clientCreateController.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/controllers/clientListController.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/controllers/clientEditController.js")"></script>
<link href="@Url.Content("~/Resources/css/style.css")" rel="stylesheet"/>
</head>
<body>
<ng-view>
</ng-view>
</body>
</html>
首先,我必须使用 ng-app
指令定义应用程序模块。我的模块称为 myclients
,这是 app.js 文件中引用的模块。然后,我必须包含 angular.js、jeneva.angular.js 以及所有控制器、服务、指令和过滤器。
body
元素只包含一个标签 - <ng-view>
,正如您已经猜到的,AngularJS 将用当前活动的视图替换这个标签的内容。就是这样。
结论
使用 AngularJS,您可以毫不费力地创建单页应用程序。Jeneva 通过管理验证例程和后端业务层使其更加简单。
参考文献
- 在 Codeplex 上下载最新版本和更多示例
- 查看工作示例
- 文章:Java Spring Mvc 单页应用,使用 Jeneva (后端)
- 文章:使用 Jeneva 验证传入的 JSON
- Jeneva 的 ASP.NET 版本
历史
- 2014 年 6 月 27 日:初始版本