vertx 异步编程指南 step9-使用AngularJS的客户端Web应用程序

到目前为止,我们的Web界面使用传统的HTML内容服务器端呈现。某些类型的应用程序可以利用客户端呈现来避免整页重新加载以及接近本机应用程序的体验,从而改善用户体验。

为此目的存在许多流行的框架。我们为本指南选择了流行的AngularJS框架,但可以同时选择ReactVue.jsRiot或其他框架/库,而不会损失一般性。

单页面应用程序

我们正在构建的维基编辑应用程序允许选择一个页面,并将其前半部分作为HTML预览进行编辑,另一半为Markdown编辑器:

HTML预览是通过在后端调用一个新端点来呈现的。渲染是在Markdown编辑器文本更改时触发的。为了避免在用户忙于输入Markdown时用不必要的请求重载后端,引入了一个延迟,以便只在该延迟期间没有改变时才触发渲染。

应用程序界面也是动态的,因为新页面使删除按钮消失:

Vert.x后端

简化HTTP垂直代码

客户端应用程序需要一个后台来公开:

  1. 静态HTML,CSS和JavaScript内容引导Web浏览器中的应用程序,以及

  2. 一个Web API,通常是一个HTTP / JSON服务。

我们简化了HTTP verticle实施覆盖所需要的。步骤#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(
    () -> apiResponse(context, 200, null, null),
    t -> apiFailure(context, t));
}
暴露的路线

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));  (1) (2)
router.get("/").handler(context -> context.reroute("/app/index.html"));
  1. 禁用缓存在开发中很有用。

  2. 默认情况下,这些文件应该位于类路径中的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);
});

AngularJS前端

应用程序视图

该界面适合位于的单个HTML文件src/main/resources/webroot/index.htmlhead部分是:

<html lang="en" ng-app="wikiApp"> (1)
<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>  (2)
  <style>
    body {
      padding-top: 2rem;
      padding-bottom: 2rem;
    }
  </style>
</head>
<body>
  1. AngularJS模块被命名wikiApp

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

正如您在AngularJS之外可以看到的,我们正在使用来自外部CDN的以下依赖项:

由于性能原因,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>
</html>

我们的AngularJS控制器被调用WikiController并且绑定到一个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> (1)
      </div>
    </span>
    <span>
      <button type="button" class="btn btn-secondary" ng-click="reload()"><i class="fa fa-refresh"
                                                                             aria-hidden="true"></i> Reload</button> (2)
    </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> (3)
    </span>
  </div>

  <div class="col-md-12"> (4)
    <div class="invisible alert" role="alert" id="alertMessage">
      {{alertMessage}}
    </div>
  </div>

</div>

  1. 对于每个wiki页面名称,我们生成一个使用元素,ng-repeatng-clickload被点击时定义控制器action()。

  2. 刷新按钮绑定到reload控制器操作。所有其他按钮的工作方式相同。

  3. ng-show指令允许我们根据控制器pageExists方法值显示或隐藏元素

  4. 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> (1)
      </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>
  1. ng-modeltextarea内容绑定pageMarkdown控制器属性。

应用控制器

wiki.jsJavaScript的开始与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控制器。控制器需要依赖注入下列对象:

  • $scope 为控制器提供DOM范围,以及

  • $http 对后端执行异步HTTP请求,以及

  • $timeout在给定延迟后触发动作,同时保持与AngularJS生命周期相关联(例如,以确保任何状态修改触发视图更改,而使用经典的setTimeout函数时情况并非如此)。

控制器方法被绑定到$scope对象。让我们从3个简单的方法开始:

$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请求方法返回promise)。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承诺方法,第一个参数在成功时调用,第二个参数在错误时调用。我们还引进successerror显示通知(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) {  (1)
  if (markdownRenderingPromise !== null) {
    $timeout.cancel(markdownRenderingPromise);  (3)
  }
  markdownRenderingPromise = $timeout(function() {
    markdownRenderingPromise = null;
    $http.post("/app/markdown", text).then(function(response) { (4)
      $scope.updateRendering(response.data);
    });
  }, 300); (2)
});
  1. $scope.$watch可以通知状态变化。在这里,我们监视pageMarkdown绑定到编辑器属性的更改textarea

  2. 300毫秒是一个精细的延迟,如果没有在编辑器中改变触发渲染。

  3. 超时是承诺,所以如果状态发生了变化,我们会取消前一个并创建一个新状态。这是我们如何延迟渲染,而不是在每次击键时进行渲染。

  4. 我们要求后端将编辑器文本呈现为HTML,然后刷新预览。





  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值