显示导航

使用 Grails 和 AngularJS 1.x 构建 REST API

使用 Grails rest api 配置文件为既有的 AngularJS 应用程序(Angular 1)构建后端

作者:Sergio del Amo

Grails 版本 3.3.0

1 Grails 培训

Grails 培训 - 由创建并积极维护 Grails 框架的人员开发和提供!

2 开始

在本指南中,你将连接一个现有的 AngularJS 待办事项应用至 Grails 后端。

2.1 你需要哪些

若要完成此指南,你需要具备以下内容

  • 一些时间

  • 像样的文本编辑器或 IDE

  • JDK 1.8 或更高版本已安装且 JAVA_HOME 已妥善配置

2.2 如何完成该指南

若要开始,请执行以下操作

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用,并包含一些其他代码,让你能快速上手。

  • complete 已完成的示例。这是对指南所提供步骤进行操作以及对 initial 文件夹应用这些更改的结果。

若要完成该指南,请转至 initial 文件夹

  • 进入 grails-guides/grails-restapi-angularjs/initial

然后按照下一部分中的说明操作。

如果你进入 grails-guides/grails-restapi-angularjs/complete,则可以直接转到完成的示例

3 初始 Angular 应用程序

treehouse

本部分展示在 Treehouse AngularJS 教程 期间开发的代码。在该教程中,会开发一个 AngularJS 应用程序。该应用程序是一个待办事项列表管理器。

初始化的应用程序未连接到任何后端。如果您刷新浏览器,待办事项更改将丢失。
initial

index.html 是我们 AngularJS 应用程序的入口点。

/initial-angularjs/index.html
<!doctype html>
<html lang="en">
<head>
  <title></title>
  <link href='https://fonts.googleapis.com/css?family=Varela+Round' rel='stylesheet' type='text/css'>
  <link href='styles/main.css' rel='stylesheet' type="text/css">
</head>
<body ng-app="todoListApp">
  <h1>My TODOs</h1>
  <todos></todos>
  <script src="vendor/angular.js" type="text/javascript"></script>
  <script src="scripts/app.js" type="text/javascript"></script>
  <script src="scripts/controllers/main.js" type="text/javascript"></script>
  <script src="scripts/services/data.js" type="text/javascript"></script>
  <script src="scripts/directives/todos.js" type="text/javascript"></script>
</body>
</html>

我们在 app.js 中创建 AngularJS 应用程序。

/initial-angularjs/scripts/app.js
angular.module("todoListApp", []);

一个 AngularJS 控制器负责管理应用程序流。

/initial-angularjs/scripts/controllers/main.js
'use strict';

angular.module('todoListApp')
.controller('mainCtrl', function($scope, dataService) {
  $scope.addTodo = function() {
    var todo = {name: "This is a new todo."};
    $scope.todos.unshift(todo);
  };

  dataService.getTodos(function(response) {
      $scope.todos = response.data;
    });

  $scope.deleteTodo = function(todo, $index) {
    dataService.deleteTodo(todo);
    $scope.todos.splice($index, 1);
  };

  $scope.saveTodo = function(todo) {
    dataService.saveTodo(todo);
  };
})

我们有一个 todos 指令。此指令使用前一个控制器和下一个模板。

/initial-angularjs/scripts/directives/todos.js
angular.module('todoListApp')
.directive('todos', function() {
  return {
    templateUrl: 'templates/todos.html',
    controller: 'mainCtrl',
    replace: true
  }
})
/initial-angularjs/templates/todos.html
<div class="list">
  <div class="add">
    <a href="" ng-click="addTodo()">+ Add a New Task</a>
  </div>
  <div class="item" ng-class="{'editing-item': editing, 'edited': todo.edited, 'completed': todo.completed}"
    ng-repeat="todo in todos | orderBy: 'completed'" ng-init="todo.completed = false">
    <input ng-model="todo.completed" type="checkbox"/>
    <span ng-click="todo.completed = !todo.completed; todo.edited = true"></span>
    <label ng-click="editing = true" ng-hide="editing">
      {{todo.name}}</label>
    <input ng-change="todo.edited = true" ng-blur="editing = false;" ng-show="editing" ng-model="todo.name" class="editing-label" type="text"/>
    <div class="actions">
      <a href="" ng-click="saveTodo(todo)">Save</a>
      <a href="" ng-click="deleteTodo(todo, $index)"  class="Delete">delete</a>
    </div>
   </div>
</div>

我们的数据服务当前正在从一个模拟的 json 文件中加载待办事项。

/initial-angularjs/scripts/services/data.js
'use strict';

angular.module('todoListApp')
.service('dataService', function($http) {
  this.getTodos = function(callback){
    $http.get('mock/todos.json')
    .then(callback)
  };

  this.deleteTodo = function(todo) {
    console.log("The " + todo.name + " todo has been deleted!")
  };

  this.saveTodo = function(todo) {
    console.log("The " + todo.name + " todo has been saved!");
  };

});
/initial-angularjs/mock/todos.json
[
    {"name": "clean the house"},
    {"name": "water the dog"},
    {"name": "feed the lawn"},
    {"name": "pay dem bills"},
    {"name": "run"},
    {"name": "swim"}
  ]

4 编写 Grails 应用程序

我们即将处理的初始项目是使用 Grails REST 配置 生成的。

grails create-app myapp --profile=rest-api

4.1 域类

创建持久实体来存储待办事项。在 Grails 中处理持久性的最常用方式是使用 Grails 域类

一个域类符合 Model View Controller (MVC) 模式中的 M,并代表映射到一个底层数据库表的持久实体。在 Grails 中,一个域存在于 grails-app/domain 目录中的一个类。

complete$ ./grailsw create-domain-class Todo

Todo 域类是我们的数据模型。我们定义不同的属性来存储 Todo 特征。

/grails-app/domain/demo/Todo.groovy
package demo

import grails.rest.Resource

@Resource(uri='/todos')
class Todo {
    String name
    boolean completed

    static constraints = {
    }
}

通过一个 单元测试,我们测试 name 是否是一个必需属性。

/src/test/groovy/demo/TodoSpec.groovy
package demo

import spock.lang.Specification
import grails.testing.gorm.DomainUnitTest

class TodoSpec extends Specification implements DomainUnitTest<Todo> {

    void "test name is required"() {
        when:
        def todo = new Todo(name: name)

        then:
        !todo.validate()
        todo.errors['name'].code == errorCode

        where:
        name | errorCode
        null | 'nullable'
        ''   | 'nullable'
    }
}

然后我们在 BootStrap.groovy 中加载一些测试数据。

/grails-app/init/demo/BootStrap.groovy
package demo

class BootStrap {

    def init = { servletContext ->

        def todos = [
                [name: 'clean the house'],
                [name: 'water the dog'],
                [name: 'feed the lawn'],
                [name: 'pay dem bills'],
                [name: 'run'],
                [name: 'swim']
        ].each { new Todo(name: it.name).save() }
    }
    def destroy = {
    }
}

我们用 @Resource 注解该域类。

/grails-app/domain/demo/Todo.groovy
import grails.rest.Resource

@Resource(uri='/todos')
class Todo {

这个注解将 Todo 域类用作 REST 资源

在 Grails 中创建 RESTful API 最简单的方法是将一个域类用作 REST 资源。只需添加 Resource 转换并指定一个 URI,即可自动将您的域类以 XML 或 JSON 格式作为 REST 资源使用。该转换将自动创建一个名为 TodoController 的控制器并注册必要的 RESTful URL 映射

这是我们域类的注册 URL 映射。

HTTP 方法 URI Grails 操作

URI

Grails 操作

GET

/todos

index

GET

/todos/create

create

POST

/todos

save

GET

/todos/${id}

show

GET

/todos/${id}/edit

edit

PUT

/todos/${id}

update

DELETE

/todos/${id}

delete

4.2 启用 CORS

由于客户端(AngularJS)和服务端(Grails)将分别在不同的端口上运行,CORS 配置是必需的。

修改您的 application.yml 以启用 CORS

/complete/grails-app/conf/application.yml
grails:
    cors:
        enabled: true

5 将 Angular 应用程序连接到 Grails

我们添加一个指令,用于检测用户在编辑 Todo 名称时按下 ENTER 时

/complete-angularjs/index.html
<script src="scripts/directives/ngEnter.js" type="text/javascript"></script>
/complete-angularjs/scripts/directives/ngEnter.js
angular.module('todoListApp')
.directive('ngEnter', function() {
        return function(scope, element, attrs) {
            element.bind("keydown keypress", function(event) {
                if(event.which === 13) {
                    scope.$apply(function(){
                        scope.$eval(attrs.ngEnter, {'event': event});
                    });
                    event.preventDefault();
                }
            });
        };
    });

我们将稍稍修改一下 UI。例如,我们将移除保存按钮,如果用户修改了 todo(更改名称或完成),我们将在服务端保存更改。

/complete-angularjs/templates/todos.html
<div class="list">
  <div class="add">
    <a href="" ng-click="addTodo()">+ Add a New Task</a>
  </div>
  <div class="item" ng-class="{'editing-item': editing, 'edited': todo.edited, 'completed': todo.completed}"
    ng-repeat="todo in todos | orderBy: 'completed'">
    <input ng-model="todo.completed" type="checkbox"/>
    <span ng-click="todo.completed = !todo.completed; todo.edited = true;saveTodo(todo);"></span>
    <label ng-click="editing = true" ng-hide="editing">
      {{todo.name}}</label>
    <input ng-enter="editing=false;saveTodo(todo)" ng-change="todo.edited = true" ng-blur="editing = false;" ng-show="editing" ng-model="todo.name" class="editing-label" type="text"/>
    <div class="actions">
      <a href="" ng-click="deleteTodo(todo, $index)"  class="Delete">delete</a>
    </div>
   </div>
</div>
/complete-angularjs/scripts/controllers/main.js
'use strict';

angular.module('todoListApp')
.controller('mainCtrl', function($scope, dataService) {
  $scope.addTodo = function() {
          dataService.addTodo(function(response) {
                  $scope.todos.unshift(response.data);
        });
  };

  dataService.getTodos(function(response) {
      $scope.todos = response.data;
    });

  $scope.deleteTodo = function(todo, $index) {
    dataService.deleteTodo(todo, function(response) {
            $scope.todos.splice($index, 1);
    });
  };

 $scope.saveTodo = function(todo, $index) {
    dataService.saveTodo(todo, function(response) {
            console.log(response.data)
    });
  };
})

数据服务现在连接到 Grails 后端,而不是加载模拟 JSON 文件。

/complete-angularjs/scripts/services/data.js
'use strict';

angular.module('todoListApp')
.service('dataService', function($http) {

  var todosGrailsServerUri = 'http://localhost:8080/todos';

  this.getTodos = function(callback){
          $http.get(todosGrailsServerUri).then(callback)
  };

  this.addTodo = function(callback) {
      var todo = {name: "This is a new todo.", completed: false};
      $http.post(todosGrailsServerUri,todo).then(callback);
  }

  this.deleteTodo = function(todo, callback) {
      $http.delete(todosGrailsServerUri + '/' + todo.id).then(callback);
  };

  this.saveTodo = function(todo, callback) {
      $http.put(todosGrailsServerUri + '/' + todo.id,todo).then(callback);
  };
});

6 运行应用程序

要运行应用程序,请使用 ./gradlew bootRun 命令,该命令将在 8080 端口上启动应用程序。

打开您的 angular 应用程序,您将享受由 Grails 后端提供支持的 todo 管理器。您可以在不丢失更改的情况下刷新浏览器。

complete

7 您需要 Grails 方面的帮助吗?

Object Computing, Inc. (OCI) 赞助了本指南的创建。提供各种咨询和支持服务。

OCI 是 Grails 的家园

认识团队