Thứ Năm, 5 tháng 1, 2017

Sử dụng dịch vụ $resource xây dựng ứng dụng CRUD trong AngularJS

Các ứng dụng sử dụng web service ngày càng phổ biến. AngularJS cung cấp dịch vụ $resource hỗ trợ tương tác với dịch vụ RESTful một cách dễ dàng. Trong bài viết này, chúng ta sẽ tìm hiểu cách thức sử dụng $resource trong ứng dụng web xây dựng bằng AngularJS.

Nội dung

$http vs $resource

Cả $http và $resource đều có thể làm việc tốt với REST. Vậy lý do là gì để chúng ta sử dụng $resource thay vì sử dụng $http? Sau khi google và theo quan điểm cá nhân thì dưới đây là các lý do được đưa ra:
  • $resource viết ít code hơn.
  • $resource cho phép chúng ta định nghĩa URL dưới dạng các template string (một chuỗi có chứa phần giữ chỗ (placeholder), ví dụ: /api/books/:id) với giá trị các tham số. $resource sẽ thay thế các placeholder bằng các giá trị tham số của các đối tượng cụ thể.
  • Việc xây dựng các service với $resource linh hoạt hơn với việc thiết lập các custom action cho các đối tượng cụ thể và có thể sử dụng nó linh hoạt trong bất kỳ controller bạn muốn.
  • $resource cho phép tạo ra một đối tượng javascript đại diện cho data model riêng biệt. Bằng cách này, với mỗi hoạt động được tính toán trên đối tượng được tạo ra thông qua $resource sẽ được thực hiện ở phía server. Ví dụ, nếu bạn muốn thực hiện update một book với $http, trước hết bạn phải lấy được đối tượng đó, sau đó thực hiện gán giá trị cho các thuộc tính rồi thực hiện save với đối tượng book đi kèm:
$http.get('http://yourserver/api/books/' + 3).success(function(book) {
 book.name = "My name updated"
 $http.put('http://yourserver/api/books/', book);
});
Với $resource, chúng ta gọi hàm $update() trên mỗi phần tử book:
BookService.get({id: 3}, function(book) {
 book.name = "My name edited";
 book.$update();
});

Sử dụng $resource

AngularJS cung cấp dịch vụ $resource thông qua module ngResource riêng biệt. Do đó, để có thể sử dụng dịch vụ này, bạn cần:
<script type="text/javascript" src="angular.js"></script>
<script type="text/javascript" src="angular-resource.js"></script>
  • Thêm nữa, trong module ứng dụng chính, bạn cần khai báo phụ thuộc vào module ngResource:
var app = angular.module('app', ['ngResource']);
  • Bơm (inject) $resource vào bất kỳ đâu (service, controller, …) bạn muốn sử dụng nó.

Các thức $resource làm việc

$resource làm việc với RESTful backend, do đó chúng ta cần có REST endpoint theo định dạng sau:

TẠO MỘT RESOURCE

Chúng ta có thể tạo một Resource bằng Resource factory: $resource(url, paramDefaults, actions, options)
Các tham số trong Resource factory:
  • url – {string} – là địa chỉ REST endpoint, đây là một URL template được tham số hóa với các tham số được bắt đầu với dấu :, chẳng hạn như /books/:bookId.
  • paramDefaults – {Object} – giả sử url template có dạng /books/:bookId và tham số {bookId: ‘12’, author: ‘Fukuzawa’} thì kết quả hiển thị trên URL là /books/12?author=Fukuzawa. Nếu giá trị của tham số có tiền tố @, ví dụ {bookId: ‘@id’} thì việc đặt tham số bookId thành @id có nghĩa là bất cứ khi nào chúng ta gọi phương thức$update() và $delete() trên một resource instance thì giá trị của :bookId sẽ được thiết lập thành thuộc tínhid của resource instance. Cách sử dụng này rất hữu ích cho các PUT request và DELETE request.
  • actions – {Object} – mặc định lời gọi hàm $resource trả về một resource class với 5 phương thức mặc định gồm get(), query(), save(), remove(), delete() (xem Đối tượng trả lại của hàm $resource). Tham số này cho phép chúng ta thêm các phuơng thức tùy chỉnh (custom method) vào resource constructor (hay resource class) với cú pháp:{ action: {method:?, params:?, isArray:?, headers:?, ...} };. Trong đó:
    • action – {string} – tên của action.
    • method – {string} – phương thức HTTP, ví như GET, POST, PUT, DELETE, JSONP, PATCH, …
    • params – {Object} – tập các tùy chọn của các tham số trước khi thực hiện action này.
    • isArray – {boolean} – quy định dữ liệu trả về cho phương thức này là Array hay Object.
    • Ngoài ra nó còn chứa đầy đủ các thuộc tính của $http.config như headerstransformRequest,transformResponsecachetimeoutwithCredentials,responseTypeinterceptor. Bạn có thể xem lại bài viết tìm hiểu về $http để nắm rõ cách thức sử dụng các thuộc tính đó.
  • options – {Object} – tham số này hiện tại chỉ hỗ trợ thiết lập thuộc tính stripTrailingSlashes. Mặc định, thuộc tính này được thiết lập giá trị true, nghĩa là dấu / trong URL sẽ bị bỏ qua khi truyền qua hàm $resource(), chúng ta có thể hủy bỏ tính năng này bằng cách thiết lập { stripTrailingSlashes: false }. Ví dụ:
angular.module('app', ['ngResource']).factory('Book', ['$resource', function($resource){
  return $resource('http://yourserver/books/:bookId', {bookId: '@id'},{ 
    update: {
      method: 'PUT'
    }
  }, {
   stripTrailingSlashes: false
  });
}]);

ĐỐI TƯỢNG TRẢ LẠI CỦA HÀM $RESOURCE

Default actions

Hàm Resource factory $resource() trả lại một resource class (hay Resource constructor) với 5 phương thức mặc định là:
{ 'get':  {method:'GET'},
'save':   {method:'POST'},
'query':  {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'} };

Hãy xem chúng ta sử dụng hàm get()query()save() như thế nào trong controller:
var app = angular.module('app',['ngResource']);

app.factory('Book', function($resource){
 return $resource('http://yourserver/api/books/:id');
});
 
app.controller('ResourceController',function($scope, Book) {
  var book = Book.get({ id: $scope.id }, function() {
    console.log(book);
  }); // get() trả lại một book duy nhất
 
  var books = Book.query(function() {
    console.log(books);
  }); //query() trả về tất cả các books
 
  $scope.book = new Book(); //tạo mới một resource instance
 
  $scope.book.data = 'some data';
 
  Book.save($scope.book, function() {
    //dữ liệu đã được thêm mới, bạn có thể làm gì đó ở đây
  }); //thêm mới một book. Giả sử rằng $scope.book là một đối tượng Book
});
get() function trong kịch bản trên liên quan đến một GET request /api/books/:id. Tham số :id trong URL sẽ được thay thế với $scope.id. Tham số thứ 2 của function get() là hàm success callback, nó được thực thi khi dữ liệu từ server trả về thành công. Cần lưu ý rằng, function get() trả lại một đối tượng rỗng (empty object) và đối tượng này sẽ được tự động gắn dữ liệu khi có dữ liệu thật từ server trả về. Đây là một thủ thuật hữu ích vì thường các tài nguyên được gán vào model và sau đó sẽ được render bởi view, do đó bạn có thể thiết lập đối tượng rỗng đó vào trong $scope và hướng nó vào trong view. Khi dữ liệu thực tế đến và đối tượng đã được thiết lập sẵn trong view, dữ liệu được gắn kết vào các vị trí tương ứng và view cũng được cập nhật. Điều này có nghĩa là trong hầu hết các trường hợp, chúng ta không phải viết một hàm callback cho các action method này.
Function query() liên quan đến GET request /api/books (không có tham số :id) và trả lại một mảng rỗng. Khi dữ liệu từ server đến, mảng này đã có sẵn ở đó. Một lần nữa bạn có thể thiết lập mảng này vào trong $scope model và đề cập đến nó trong view sử dụng ng-repeat.
save() function liên quan đến POST request /api/books với tham số thứ nhất là POST body. Tham số thứ hai là hàm callback sẽ được gọi khi dữ liệu được lưu trữ thành công. $resource() function trả lại một resource class và lời gọi new Book() sẽ tạo ta một đối tượng cụ thể (resource instance), thiết lập các thuộc tính cho nó và lưu trữ đối tượng vào backend.

Các action method của Resource Class và Resource Instance

Các action method này dự kiến có 4 tham số, bao gồm: params object, data object, success function, error function.
  • HTTP GET action của Resource Class: Resource.action(paramsObj, successFn, errorFn). Ví dụ:
    • Book.get(paramsObj, successFn, errorFn);
    • Book.query(paramsObj, successFn, errorFn);
  • non-GET action của Resource Class: Resource.action(paramsObj, dataObj, successFn, errorFn). Ví dụ:
    • Book.save(paramsObj, dataObj, successFn, errorFn);
    • Book.remove(paramsObj, dataObj, successFn, errorFn);
    • Book.delete(paramsObj, dataObj, successFn, errorFn);
  • non-GET action của Resource Instance: instance.$action(paramsObj, successFn, errorFn). Ví dụ:
    • book.$save(paramsObj, successFn, errorFn);
    • book.$remove(paramsObj, successFn, errorFn);
    • book.$delete(paramsObj, successFn, errorFn);
Lưu ý rằng, các non-GET action method của resource instance bắt đầu với dấu $. Và cả class action và instance action có các chức năng tương đương:
Hàm success callback được gọi với tham số (value, responseHeaders). Hàm error callback được gọi với tham số(httpResponse)
Class actions trả lại một empty instanceInstance actions trả lại lời hứa (promise) của action.
Ngoài ra, các Resource instance và collection có thêm các thuộc tính:
  • $promise: việc thực hiện các request thông qua web service thường trả lại dữ liệu với độ trễ nhất định. Để đảm bảo dữ liệu đã sẵn sàng trước khi gán nó vào model hoặc render lên view, chúng ta có thể sử dụng promise. Ví dụ:
var Todo = $resource('/api/todó/:id');

Todo.get({id: 123}).$promise.then(function(todo) {
   // success
   $scope.todos = todos;
}, function(errResponse) {
   // fail
});

Todo.query().$promise.then(function(todos) {
   // success
   $scope.todos = todos;
}, function(errResponse) {
   // fail
});
Và cần nhớ rằng, thuộc tính $promise là một thuộc tính trên cùng giá trị được trả về ở trên, do đó ta có thể sử dụng với cách khác như sau:
var Todo = $resource('/api/todos/:id');

Todo.get({id: 123}).$promise.then(function(todo) {
   $scope.todo = todo;
});
//tương đương với cách viết sau:
var todo = Todo.get({id: 123}, function() {
   $scope.todo = todo;
});
//----------------
var todo = Todo.get({id: 123});
todo.$promise.then(function() {
   $scope.todo = todo;
});
//tương đương với cách viết sau:
Todo.get({id: 123}, function(todo) {
   $scope.todo = todo;
});
  • $resolved: ban đầu thuộc tính này có giá trị false, sau khi tương tác với server lần đầu tiên hoàn thành (dù success hay fail) nó được thiết lập giá trị true. Thuộc tính này hữu ích trong việc biết Resource đã được resolved hay chưa để thực hiện gắn kết dữ liệu (data-binding).

Unit testing

$resource được thực hiện ở tầng cao hơn $http. Nó tương tác với backend thông qua các HTTP request. Bạn có thể sử dụng $httpBackend mock để thực hiện unit test cho $resource.

Áp dụng vào ví dụ cụ thể

Trong ví dụ này, chúng ta sẽ xây dựng một ứng dụng CRUD (Create – Read – Update – Delete) Book sử dụng $resource trong AngularJS. Ví dụ được xây dựng trên Ubuntu nên việc cài đặt các công cụ sẽ được hướng dẫn trên Ubuntu. Với các hệ điều hành khác, bạn vui lòng xem hướng dẫn trong các link đính kèm công cụ.

XÂY DỰNG RESTFUL SERVICE

Trong ví dụ này, chúng ta cần tạo ra một dịch vụ web RESTful phía backend có thể trả về với các request tương ứng. Bạn có thể sử dụng bất cứ công cụ nào miễn sao có thể tạo ra RESTful resource để demo. Ở đây, tôi sử dụngdeployd – một công cụ giúp xây dựng các API đơn giản.
Để sử dụng công cụ này, bạn cần cài đặt deployd và mongodb.
Bạn chạy lệnh sau để cài đặt deployd:
$ npm install deployd -g
Sau đó, chạy lệnh dpd -V để kiểm tra phiên bản của deployd. Nếu bạn thấy phiên bản của deployd ví như 0.7.0 tức là bạn đã cài đặt thành công deployd.
Tiếp đến, bạn cần cài đặt mongodb nếu chưa có:
$ sudo apt-get install -y mongodb-org
Bạn kiểm tra phiên bản mongodb bằng lệnh mongod --version.
Tiếp đó, bạn có thể tải ví dụ cho phần Book web service theo địa chỉ cho git clone. Sau đó bạn di chuyển vào thư mục ứng dụng, tiếp đó chạy lệnh dpd -d để chạy ứng dụng và mở dashboard của ứng dụng trên trình duyệt
Khi đó, ứng dụng sẽ được mở trên trình duyệt với cổng mặc định là 2403 như hình dưới đây:
Với ví dụ đơn giản này, chúng ta tạo ra một Books Collection với các thuộc tính gồm: id, title, category, price. Bạn có thể dễ dàng thêm các bản ghi cho Books Collection. Giả sử tôi thêm 3 bản ghi, khi đó ứng dụng sẽ như sau:
Chuyển sang tab API, chúng ta sẽ thấy danh sách các API mà ứng dụng cung cấp:
Khi chạy ứng dụng phía client, ứng dụng này cần được chạy để cung cấp các REST endpoint và tương tác với client.

XÂY DỰNG ỨNG DỤNG PHÍA CLIENT BẰNG ANGULARJS

Bạn có thể tải demo tại địa chỉ git clone. Trong phần này, chúng ta sẽ sử dụng $resource để thực hiện các thao tác CRUD với Book. Dùng Terminal di chuyển vào thư mục example-angularjs-book-client. Bạn cần cài đặt các gói thư viện cần dùng cho HTTP Server:
$ git clone https://github.com/taly2808/example-angularjs-book-client.git
$ cd example-angularjs-book-client
$ npm install connect serve-static --save-dev
Sau khi npm cài đặt xong các gói thư viện trên, chạy HTTP Server bằng lệnh:
$ node server
Mở cửa sổ trình duyệt mới, nhập vào địa chỉ http://localhost:3000, ta sẽ được giao diện như sau:
Trong phần này, chúng ta tạo ra một service factory Book sử dụng $resource để request đến server và sử dụng nó trong controller để lấy dữ liệu:
var app = angular.module('bookApp', ['ngResource']);

// Book Service
app.factory('Book', ['$resource', function($resource){
  return $resource('http://localhost:2403/books/:id', {id: '@id'},{ 
    update: {
      method: 'PUT'
    }
  });
}]);

//popupService
app.service('popupService', ['$window', function($window){
  this.showPopup = function(msg) {
    return $window.confirm(msg);
  }
}])

// controller
app.controller('bookMgrCtrl', ['$scope', 'Book', 'popupService', function($scope, Book, popupService) {

  $scope.displayMode = "list";
  $scope.currentBook = null;

  $scope.listBooks = function () {
    $scope.books = Book.query();
  }

  $scope.loadEditForm = function (book) {
    $scope.currentBook = book ? Book.get({id: book.id}) : {};
    $scope.displayMode = "edit";
  }

  $scope.cancelEditForm = function () {
    $scope.currentBook = null;
    $scope.displayMode = "list";
  }

  $scope.saveEditForm = function (book) {
    if (angular.isDefined(book.id)) {
      $scope.updateBook(book);
    } else {
      $scope.createBook(book);
    }
  }

  $scope.createBook = function (book) {   
   $scope.currentBook = new Book(book);
   $scope.currentBook.$save(function(newBook){
      $scope.listBooks();
      $scope.displayMode = "list";
   });
    
  }

  $scope.updateBook = function (book) {
    $scope.currentBook.$update(function(){
      $scope.listBooks();
      $scope.displayMode = "list";
    });
  }
  
  $scope.deleteBook = function (book) {
    if (popupService.showPopup('Are you sure delete this book?')){
      book.$delete();
      $scope.listBooks();
    };
  }

  $scope.listBooks();

}]);