Vert.x Java开发指南——第十章 使用AngularJS的客户端Web应用

第十章 使用AngularJS的客户端Web应用

版权声明:本文为博主自主翻译,转载请标明出处。 https://blog.csdn.net/elinespace/article/details/807115131
相应代码位于本指南仓库的step-9目录下

截止目前,我们的Web界面使用了传统的服务端渲染HTML内容。某些应用类型可以利用客户端渲染,避免全页面重新加载并且接近本地应用体验,以提升用户体验。

许多受欢迎的框架便是因为这个目的而存在。我们为本指南选择了流行的AngularJS框架,但是可以不失一般性的同等选择React、Vue.js、Riot或者其它框架/库。

10.1 单页应用

我们构建的Wiki编辑应用允许选择一个页面并编辑它,前半屏是一个HTML预览,另外半屏是Markdown编辑器:

HTML预览通过调用我们后端的一个新端口进行渲染。渲染在Markdown编辑器文本变更时触发。为了避免用户忙于键入Markdown时不必要的请求使得后端负载过重,我们引入了一个延迟,以便只有当在延迟期间没有变更时触发渲染。

应用程序界面也是动态的,新建页面时删除按钮不显示:

10.2 Vert.x后端

10.2.1 简化HTTP Verticle代码

客户端应用需要后端暴露:

  1. 静态HTML、CSS和JavaScript内容给浏览器中的bootstrap应用
  2. 一个Web API,通常是一个HTTP/JSON服务

我们简化了HTTP Verticle实现以满足需要。从step 8的RxJava版本开始,我们移除了所有服务端渲染代码以及认证和JWT令牌颁发代码,以暴露简单的开放HTTP/JSON接口。

当然,构建一个利用了JWT令牌和认证的版本对于实际部署是有意义的,但是现在我们已经涵盖了这些特征,我们更希望在本指南的这部分集中于必要的部分。

作为一个示例,apiUpdatePage方法的实现代码如下:

private void apiUpdatePage(RoutingContext context) {
    int id = Integer.valueOf(context.request().getParam("id")); 
    JsonObject page = context.getBodyAsJson();
    if(!validateJsonPageDocument(context, page, "markdown")) {
        return; 
    }
    dbService.rxSavePage(id, page.getString("markdown")).subscribe(v->apiResponse(context, 200, null, null),t->apiFailure(context, t));
}

10.2.2 公开路由

HTTP/JSON API通过与上一步中相同的路由公开:

router.get("/api/pages").handler(this::apiRoot); 
router.get("/api/pages/:id").handler(this::apiGetPage);
router.post().handler(BodyHandler.create()); 
router.post("/api/pages").handler(this::apiCreatePage);
router.put().handler(BodyHandler.create()); 
router.put("/api/pages/:id").handler(this::apiUpdatePage); 
router.delete("/api/pages/:id").handler(this::apiDeletePage);

前端应用静态资源来自/app,我们重定向对“/”的请求到/app/index.html静态文件:

router.get("/app/*").handler(StaticHandler.create().setCachingEnabled(false));①②
router.get("/").handler(context->context.reroute("/app/index.html"));

① 在开发环境中,禁用缓存是有用的

② 默认情况下,期望文件在类路径的webroot包下,因此在Maven或者Gradle项目中文件应被放置在src/main/resources/webroot下。

最后但并非不重要,我们预期应用程序需要后端渲染Markdown为HTML,因此我们提供了一个HTTP POST端点用于该目的:

router.post("/app/markdown").handler(context -> {
    String html = Processor.process(context.getBodyAsString());
    context.response().putHeader("Content-Type", "text/html")
        .setStatusCode(200)
        .end(html);
});

10.3 AngularJS前端

本指南不是一份AngularJS的正式介绍(可查看官方入门),我们假设读者已熟悉该框架。

10.3.1应用程序视图

适合于单个HTML文件的界面位于src/main/resources/webroot/index.html。head部分如下:

<html lang="en" ng-app="wikiApp"> ①
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>Wiki Angular App</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous"> 
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"> 
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
        <script src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js"></script>
        <script src="/app/wiki.js"></script> ②
        <style>
            body {
                padding-top: 2rem; padding-bottom: 2rem;
            }
        </style>
   </head>
<body>

① AngularJS模块命名为wikiApp

② wiki.js持有我们的AngularJS模块和控制器代码。

如你所见,除AngularJS之外,我们通过外部CDN使用了以下依赖:

  1. Bootstrap用于我们界面的样式。
  2. Font Awesome用于提供图标。
  3. Lodash帮助我们在Javascript代码中采用一些实用的语法。

Bootstrap需要一些更进一步的脚本,出于性能原因可以在文档的底部加载:

<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
 </body>

我们的AngularJS控制器称为WikiController,它绑定到一个div,该div同时也是Bootstrap容器:

<div class="container" ng-controller="WikiController"> <!-- (...) -->

界面顶部的按钮包含以下元素:

<div class="row">
    <div class="col-md-12">
        <span class="dropdown">
            <button class="btn btn-secondary dropdown-toggle" type="button" id="pageDropdownButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                <i class="fa fa-file-text" aria-hidden="true"></i>Pages
            </button>
            <div class="dropdown-menu" aria-labelledby="pageDropdownButton">
                <a ng-repeat="page in pages track by page.id" class="dropdown-item" ng-click="load(page.id)" href="#">{{page.name}}</a></div>
        </span>
        <span>
            <button type="button" class="btn btn-secondary" ng-click="reload()">
                <i class="fa fa-refresh" aria-hidden="true"></i>Reload
            </button></span>
        <span>
            <button type="button" class="btn btn-secondary" ng-click="newPage()">
                <i class="fa fa-plus-square" aria-hidden="true"></i>New page
            </button>
        </span>
        <span class="float-right">
            <button type="button" class="btn btn-secondary" ng-click="delete()" ng-show="pageExists()">
                <i class="fa fa-trash" aria-hidden="true"></i>Delete page
            </button></span>
    </div>
    <div class="col-md-12"><div class="invisible alert" role="alert" id="alertMessage">
            {{alertMessage}}
        </div>
    </div>
</div>

① 对于每个Wiki页面名称,我们使用ng-repeat生成一个元素,ng-click用于定义它被点击时控制器的动作(load)。

② 刷新按钮被绑定到reload控制器action,所有其他按钮的工作方式相同。

③ ng-show指令允许我们显示或者隐藏元素,依赖于控制器pageExists方法的值。

④ div用于显示成功或者失败的通知。

Markdown预览和编辑元素如下:

<div class="row">
    <div class="col-md-6" id="rendering"></div>
    <div class="col-md-6">
        <form>
            <div class="form-group">
                <label for="markdown">Markdown</label>
                <textarea id="markdown" class="form-control" rows="25" ng-model="pageMarkdown"></textarea></div>
            <div class="form-group">
                <label for="pageName">Name</label>
                <input class="form-control" type="text" value="" id="pageName" ng-model="pageName" ng-disabled="pageExists()">
            </div>
            <button type="button" class="btn btn-secondary" ng-click="save()">
                <i class="fa fa-pencil" aria-hidden="true"></i> Save
            </button>
        </form>
    </div>
</div>

① ng-model绑定textarea内容到控制器的pageMarkdown属性。

10.3.2 应用程序控制器

wiki.js JavaScript以一个AngularJS模块声明作为开始:

'use strict';
angular.module("wikiApp", []).controller("WikiController", ["$scope", "$http", "$timeout", function ($scope, $http, $timeout) {
    var DEFAULT_PAGENAME = "Example page";
    var DEFAULT_MARKDOWN = "# Example page\n\nSome text _here_.\n";
    // (...)

wikiApp模块没有插件依赖,声明了一个单独的WikiController控制器。该控制器需要依赖注入以下对象:

  1. $scope 向Controller提供DOM范围。
  2. $http 执行到后台的HTTP异步请求。
  3. $timeout 当处于AngularJS生命周期时,在指定的延迟之后触发动作(举例来说,确保任何状态修改都会触发视图变更,这些不是使用经典的setTimeout功能的场景)。

Controller方法被绑定到$scope对象。让我们以三个简单的方法作为开始:

$scope.newPage = function() { 
    $scope.pageId = undefined; 
    $scope.pageName = DEFAULT_PAGENAME; 
    $scope.pageMarkdown = DEFAULT_MARKDOWN;
};
$scope.reload = function () { 
    $http.get("/api/pages").then(function (response) {
        $scope.pages = response.data.pages;
    });
};
$scope.pageExists = function() { 
    return $scope.pageId !== undefined;
};

创建一个新页面包括初始化那些需要添加到$scope对象上的控制器属性。从后台重新加载页面对象仅仅是执行一个HTTP GET请求的问题(注意$http请求方法返回promises)。pageExists方法用于显示/隐藏界面中的元素。

加载页面内容也是执行一个HTTP GET请求,并且更新预览DOM:

$scope.load = function (id) {
    $http.get("/api/pages/" + id).then(function(response) {
        var page = response.data.page; 
        $scope.pageId = page.id; 
        $scope.pageName = page.name; 
        $scope.pageMarkdown = page.markdown; 
        $scope.updateRendering(page.html);
    }); 
};
$scope.updateRendering = function(html) { 
    document.getElementById("rendering").innerHTML = html;
};

下面的方法支持保存/更新和删除页面。对于这些操作,我们使用完全的then方法,在方法成功时第一个参数被调用,失败时第二个参数被调用。我们还引入success和error助手方法来显示通知(成功时3秒,错误时5秒):

$scope.save = function() {
    var payload;
    if ($scope.pageId === undefined) {
        payload = {
            "name": $scope.pageName,
            "markdown": $scope.pageMarkdown
        };
        $http.post("/api/pages", payload).then(function(ok) {
            $scope.reload();
            $scope.success("Page created");
            var guessMaxId = _.maxBy($scope.pages, function(page) { return page.id; }); 			$scope.load(guessMaxId.id || 0);
        }, function(err) { 
            $scope.error(err.data.error);
        }); 
    }else{
        var payload = {
          "markdown": $scope.pageMarkdown
       };
       $http.put("/api/pages/" + $scope.pageId, payload).then(function(ok){ 
            $scope.success("Page saved");
        }, function(err) { 
            $scope.error(err.data.error);
        }); 
    }
};

$scope.delete = function() {
    $http.delete("/api/pages/" + $scope.pageId).then(function(ok) {
        $scope.reload();
        $scope.newPage();
        $scope.success("Page deleted");
    }, function(err) { 
        $scope.error(err.data.error);
    }); 
};

$scope.success = function(message) { 
    $scope.alertMessage = message;
    var alert = document.getElementById("alertMessage"); 
    alert.classList.add("alert-success"); 
    alert.classList.remove("invisible"); 
    $timeout(function() {
        alert.classList.add("invisible");
        alert.classList.remove("alert-success"); 
    }, 3000);
};

$scope.error = function(message) { 
    $scope.alertMessage = message;
    var alert = document.getElementById("alertMessage"); 
    alert.classList.add("alert-danger"); 
    alert.classList.remove("invisible");
    $timeout(function() {
        alert.classList.add("invisible");
        alert.classList.remove("alert-danger"); 
    }, 5000);
};

初始化应用程序状态和视图通过获取页面列表完成,以一个空的新页面编辑器开始:

$scope.reload();
$scope.newPage();

最后,是我们如何执行Markdown文本的实时渲染:

var markdownRenderingPromise = null; 
$scope.$watch("pageMarkdown", function(text) {if (markdownRenderingPromise !== null) {
        $timeout.cancel(markdownRenderingPromise); ③
    }
    markdownRenderingPromise = $timeout(function() { 
        markdownRenderingPromise = null;
        $http.post("/app/markdown", text).then(function(response) { ④
            $scope.updateRendering(response.data);
       });
    }, 300); ②
});

$scope.$watch可以通知状态改变。此处我们用于监控绑定到编辑器textarea的pageMarkdown属性的变更。

② 300毫秒是一个可接收的延迟,如果编辑器中没有任何变更,将触发渲染。

③ 超时是一个promise,因此如果状态已经变更,我们会取消上一个并且创建一个新的。这便是我们如何延迟渲染,以取代每次键盘敲击都执行它。

④ 我们请求后台渲染编辑器文本为HTML,然后刷新预览。

猜你喜欢

转载自blog.csdn.net/elinespace/article/details/80711513