65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2014 年 3 月 3 日

CPOL

9分钟阅读

viewsIcon

53608

downloadIcon

1237

使用 JSON 进行 Web 开发很简单。

引言

上一篇文章中,我演示了如何使用 Java Spring MVCJeneva 创建一个 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.jsclientCreateController.jsclientEditController.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 应用程序的常见任务。该模块依赖于两个不同的模块:ngRoutejenevamodule,它们都作为独立的、可重用的 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 个变量:$scopejeneva。在控制器的主体中,我定义了 $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 由 Jenevajv-error-key 指令控制。实际上,这意味着当失败响应从后端服务器到达浏览器时,具有空键的错误消息将显示在 span 的主体中。有时,您需要使用无键(或无路径)的错误消息,例如,“意外失败”消息或更具体的“您不能删除此客户端”(或者您可以为某些错误消息设置静态键)。无键(或静态键)失败消息最好的地方是它们不需要任何 JSON 数据发布到服务器。除了无键消息外,您还可以随时定义自己的自定义键,例如,您可以定义“server_error”键(路径),然后您可以将具有该键的失败注册到验证上下文中,然后您可以使用 jv-error-key 指令在表单中的任何位置显示该失败消息。在此示例中,我为未处理的异常使用无键消息,并将它们简单地显示在每个表单的顶部。我还使用无键消息来通知用户由于某种原因不能删除客户端(请参阅 ClientService 中的删除客户端)。

现在看看编辑客户端链接

<a ng-href="#/client/edit/{{row.id}}">Edit</a> 

正如您所见,此链接导航到编辑客户端视图,它还包含客户端的 ideditClientController 必须知道如何从链接中提取此 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-ifng-repeat 指令进行装饰。基本上,这意味着,如果 loginFormname 字段验证失败,则 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.jsjeneva.angular.js 以及所有控制器、服务、指令和过滤器。

body 元素只包含一个标签 - <ng-view>,正如您已经猜到的,AngularJS 将用当前活动的视图替换这个标签的内容。就是这样。

结论

使用 AngularJS,您可以毫不费力地创建单页应用程序。Jeneva 通过管理验证例程和后端业务层使其更加简单。

参考文献

历史

  • 2014 年 6 月 27 日:初始版本
© . All rights reserved.