LoginSignup
1
2

More than 5 years have passed since last update.

Các bài học lập trình ứng dụng RESTful ~ Chuyên đề phát triển Web - [Chương 7] Thực hiện validation ở cả 2 phía Server-Client

Last updated at Posted at 2015-11-04

:large_blue_circle: Chào mừng các bạn trở lại

Bài viết này được mình dịch từ bài サーバー・クライアントの両面からかけるバリデーション - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第7回】マニュアル của tác giả @k_shimoji .

:large_blue_circle: Tại sao lại cần phải kiểm duyệt (Validate)?

Tại thời điểm hiện tại, khi chúng ta đã validate thì coi như đã validate luôn cho các thao tác trên client side rồi, vậy tại sao lại phải làm cả 2.
Lý do là chúng ta thấy có sự khác biệt ở phía client, những thao tác trên client hoàn toàn có thể giúp cải thiện UX, người dùng nhận ngay kết quả kiểm duyệt, sau khi dữ liệu hợp lệ mới truyền đến server chờ xử lý, thay vì gửi hết lên server.

Lý do cần thực hiện validation trên cả client lẫn server.

Có một sự thật là nếu chỉ validate ở client là chưa đủ, bởi vì "Bạn chọn từ danh sách từ phía client, nhưng chưa kiểm tra nó có hợp lệ ở trến server hay không."
Và đây là điều rất thường xuyên xảy ra.

  • Về cơ bản thì những gì bạn làm trên client side bạn cũng cần phải làm nó trên server side.

    • Mình có thể xử lý nó không cần qua giao diện, thông qua API (POSTMAN!)
  • Đây là những validation mà không thể chỉ làm mỗi trên client side

    • Liên quan đến DB
      • Cái có giá trị date và time
      • Cần tường tác với các hệ thống khác
      • Vân vân ...

Lý do cần thực hiện validation từ phía server

Server side cần xử lý những input không hợp lệ, validation sẽ giúp chúng ta đạt được những mục tiêu sau.

  • Bảo vệ ứng dụng
    • Để ngăn chặn những hành vi không mong muốn ( hoặc thất bại...)
      • Để tăng cường bảo mật (XSS, SQLInjection, etc ...)
  • Bảo vệ dữ liệu
    • Để chắc chắn những dữ liệu không hợp lệ sẽ không được gửi lên server
      • Vượt quá Phạm vi của giá trị, format, encoding ...
      • Giúp các dữ liệu được thống nhất

Lý do cần thực hiện validation từ phía client

Phải thừa nhận rằng sau khi bạn validate ở server xong thì bạn có thể nói validation ở client side không cần thiết cho lắm. Tuy nhiên chúng ta vẫn cần những thao tác "non-server" để người dùng làm việc trên client side, qua đó có những phản hồi tốt hơn về mặt UX, nhất là việc kiểm tra ngay lập tức các lỗi điền form.
※ Ứng dụng của chúng ta đã đủ những điều kiện và môi trường, chỉ còn sự validation chỉ mới làm 1 trong 2 phía client và server.

  • Giới hạn và formart của các giá trị input
  • Kiểm tra việc tổng hợp và sử dụng thông tin từ server
  • Đầu tiên là bạn không được nhập những dữ liệu không hợp lệ từ UI ( nhưng khác với validation, cái này là kiểm tra thôi ...)
    • Ở phần select thêm kiểm tra đã chọn giá trị nào chưa
    • Thêm giới hạn Maxlength

:large_blue_circle: Thực hiện

Chúng ta sẽ thực hiện validation theo server-side và client-side như hướng dẫn dưới đây, kết quả sau khi thành công như sau

:warning: Dưới đây là kết quả từ client-side. Tôi thêm một tiền tố [Client] ở đầu các thông báo để dễ nhận biết đâu là phản hồi từ validation ở client, và làm tương tự với [Server] .

demo notice.PNG

Server-side validation

  1. Giới hạn ký tự cho TODO
    • Giới hạn từ 1~200 ký tự
    • Nếu quá hoặc chưa đủ giới hạn thì xuất thông báo "[Server] TODO must have 1~200 characters."
  2. Trạng thái TODO

    • 0 (chưa hoàn thành) hoặc 1 (hoàn thành)
      • Nếu lỗi, xuất "[Server] status should be entered in the 0 (incomplete) or 1 (completed)."
  3. Kiểm tra sự tồn tại của ID người được phân công

    • ID có tồn tại
      • Trường hợp lỗi, xuất thông báo "The Assignee's not found."
  4. Khi không phải owner thay đổi hoặc xóa người được phân công ( nhưng vẫn ở khu vực được quyền quản lý)

  5. Khi người sửa không phải là owner mà là người được phân công

    • "[Server] Only Owner or Assignee can update."
  6. Khi người xóa TODO không phải là Owner

    • "[Server] Only the owner can delete."
  7. Thêm TODO

    • Toàn bộ nội dung của TODO mới trùng với một TODO đã có
      • "TODO of the same content is already registered."

Cho việc thực hiện validation, có sẵn trong CakePHP.

  • 1-2 sử dụng CakePHP's built-in validation
  • 3-5 tự tạo các hàm validation mới

Bây giờ bắt đầu implement thôi.

Client-side validation

  • Giới hạn ký tự cho TODO (Giống trên server)
    • Giới hạn từ 1~200 ký tự
    • Nếu quá hoặc chưa đủ giới hạn thì xuất thông báo "TODO must have 1~200 characters."
  • Thêm TODO ( cho việc xác nhận, nơi mà phần lớn các validation error chạy đúng )
    • Không được phép điền todo "hoge"
      • Nếu vi phạm "[Client] TODO can not be "hoge" only!"
      • Validation này sẽ tự xuất hiện thông báo. Và đây là tất cả những gì mình cần làm cho phần client side. Mặc dù nó khá ít...

Những gì sẽ thực hiện ở Backbone.js,

  • Để thực hiện validation, chạy hàm validate ở model
  • Tạo các sự kiện invalid cho mỗi validation error

Mục lục Workshop

  • Chuẩn bị
  • Bài 0: xây dựng app giống như bài trước
  • Bài 1: Thực hiện ở server-side
  • Bài 2: Thực hiện ở client-side

Sau khi chuẩn bị, đầu tiên mình sẽ xây dựng app như sau khi hoàn thành bài viết trước, chưa có validation!

:large_blue_circle: Chuẩn bị

Phần chuẩn bị sẽ giống nhau ở mỗi bài, vì vậy tôi đã tổng hợp nó ở một entry riêng. Các bạn vui lòng xem ở link này All 12 times of study sessions in do I have use of Git - RESTful application workshops will be built on AWS ~ Web development workshop ~ - Qiita để chuẩn bị .

  • Git: Bây giờ mình sẽ tạo branch vol/07 và làm việc trên nó.

:warning: *** Tôi xin nhắc lại ở bài 5 và bài 6, chúng ta đã có những table bị thay đổi. ***

  • Bài 5
    • Tạo Table dành cho việc đăng ký thành viên ( Thực hiện cho chức năng login )
  • Bài 6
    • Thêm một cột cho table TODO list ( Thêm cột Owner và cột assignee cho chức năng phân công công việc)

Các bạn nên tham khảo lại các link sau để rõ hơn.
Tạo Table dành cho việc đăng ký thành viên
Thêm một cột cho table TODO list

  • :white_check_mark: Tạo một branch mới tên là vol/07.
  • :white_check_mark: Sửa table theo các bài trước.

Nếu đã sẵn sàng, chúng ta bắt đầu bài 0

:large_blue_circle: Bài 0: Nhìn lại ứng dụng tại thời điểm hiện tại

Hiện tại ứng dụng của chúng ta chưa được thêm validation nào.
Một số cái tối thiểu ở client-side (như MaxLength) đã được thêm, tuy nhiên bạn có thể dễ dàng phá bỏ nó bằng cách gửi request lên POSTMAN, vì vậy sẽ gây ra lỗi Internal Server Error.

Thử với POSTMAN

Hiện tại tình trạng của chương trình như sau

  • :white_check_mark: Số lượng ký tự của TODO vượt quá giới hạn
    • Sinh ra lỗi 500(Internal Server Error)
  • :white_check_mark: TODO là dữ liệu rỗng
    • Sỉnh ra lỗi 500(Internal Server Error)
  • :white_check_mark: Status là 9
    • Chỉ cho phép 2 giá trị là 0 và 1, nhưng vẫn được lưu giá trị
  • :white_check_mark: Không tồn tại ID của user
    • Bạn muốn sinh ra thông báo lỗi, nhưng người đó vẫn thao tác được
  • :white_check_mark:TODO bị trùng với TODO đã tồn tại
    • Sỉnh ra lỗi 500(Internal Server Error)
  • :white_check_mark: Không phải owner sửa TODO mà là assignee
    • Chúng ta muốn có thông báo lỗi, nhưng người dùng vẫn sửa được
  • :white_check_mark: Người xóa TODO không phải Owner
    • Chúng ta muốn có thông báo lỗi, nhưng người dùng vẫn xóa được

Hoàn toàn chưa bắt được các lỗi này, đó là công việc của bài 1, sau khi xong bài 1 các bạn có thể test ở POSTMAN và sẽ ra kết quả như phần Tham khảo dưới đây

:warning: Tham khảo

Để biết URL hay dữ liệu cần set để kiểm tra vào POSTMAN, các bạn hoàn toàn có thể sử dụng chrome developer tools.
Đầu tiên các bạn mở chrome dev tool và bấm vào tab NETWORK -> XHR, thực hiện một thao tác nào đó ( Thêm, sửa, xóa... ) và quan sát sẽ thấy ở cột Name sẽ hiện tên thao tác mình vừa thực hiện, nhấn vào đấy để quan sát, những giá trị cần chú ý tôi đã để mũi tên màu xanh.

chúy.PNG

Bây giờ mình sẽ lấy URL, như trong ảnh trên tôi sẽ lấy http://54.199.231.20/rest-study/todo_lists.json, method là POST.
Các giá trị truyền lên sẽ gồm có todo, status, và assignee ( Lần lượt là nội dung todo, trạng thái thêm vào 0 hay 1, assignee là người được phân công ).
Trường hợp thêm nội dung vẫn xịt thì bạn nên chạy API đăng nhập trước nhé, vì phải đăng nhập xong nó mới có quyền tác động vào todo list và mới nhận được giá trị Owner.
Điền đầy đủ thông tin như trong hình dưới và nhấn SEND trong POSTMAN ( đầu tiên mình thử với giá trị hợp lệ đã )

kq.PNG
Bây giờ mình test với giá trị không hợp lệ để kiểm tra như trên, để cho nhanh thì mình test việc trùng nội dung TODO bằng cách nhấn SEND thêm 1 lần nữa.

ec.PNG
Báo lỗi ngay và luôn.

Các bạn có thể sửa nội dung TODO thành rỗng hoặc quá 200 ký tự, hoặc sửa ID của assignee thành 1 cái ID không tồn tại người dùng trong database, hoặc sửa status thành 9 chẳng hạn để test tiếp, nếu được báo lỗi nghĩa là mình đã validate thành công trên server side.

Bây giờ mình sẽ bắt đầu bài 1

:large_blue_circle: Bài 1: Server-side validation

Ở bài 1 này chúng ta sẽ thực hiện phần server-side của chương trình.
Sử dụng "POSTMAN" để xác nhận sự thay đổi của API sau khi tiến hành validation.

Chú ý: Mô tả phương thức validation

Nó sẽ mô tả rule cho Model, chỉ có thể!
Sau đó, validation dựa trên các rule này sẽ được tự động thêm khi hàm model -> save() được thực thi, Trường hợp lỗi sẽ xuất thông báo lỗi bằng model-> validationErrors.

Ví dụ: Implement vào model

ValidationRules
class TodoList extends AppModel {

省略

public $validate = array (
    //TODO check
    'todo' => array (
        //Số ký tự
        'rule1' => array (
            'rule' => array(
                'between', 1, 200
            ),
            'message' => 'The characters must be in the range 1~200'
        ),

        Chúng ta sẽ tại ra các rule mới tương tự thế này
  • Tôi đã thêm mỗi key ( trong cặp key : value ) cho mỗi 'todo' để kiểm tra. Nó sẽ thêm các thiết lập theo các trình tự.
    • 'rule1' là tên của rule, tốt nhất là nó sẽ được mang giá trị unique trong danh sách các key.
      • 'rule' Tôi sẽ xác định hàm và arguments của rules để set. Ở đây thì mình xác định bằng một CakePHP built-in là between.
        • 'between', 1, 200 với built-in between của CakePHP, Hàm này được thiết lập chạy bằng cách cho các giá trị 1200. Bây giờ, số lượng ký tự của mỗi TODO sẽ được thiết lập cho thông báo lỗi nếu nó nằm ngoài khoảng từ 1 đến 200. - 'message' sẽ trỏ đến câu thông báo lỗi 'Đây là thông báo khi lỗi vi phạm rule này xuất hiện'.

Mạch chương trình như sau

  • Chạy hàm save() của model trong controller
  • Nếu xuất hiện lỗi (save() sẽ return giá trị false) xác nhận tới model rằng validationErrors
  • Return một thông báo lỗi cho client.
(ThamKhảo)model->formatOfValidationErrorMessagesThatAreSetToValidationErrors
{
    "todo": [
        "The characters must be in the range 1~200"
    ],
    "id": [
        "Only owner can update,."
    ]
}

Chú ý: Chạy validation khi xóa

Vì mặc dù validation khi save được chạy từ Model, delete thình thoảng không chạy,
Cần chú ý là "Chỉ Owner được xóa TODO" , mình sẽ chạy ở Controller.
Validation logic sẽ được chuyển sang Model.

Chú ý: Cách return error

Controller kiểm tra validationErrors models và return thông tin này cho client nếu có lỗi nào xuất hiện,
Mình sẽ phản hồi một HTTP status 400 để chỉ ra rằng đây là một thông báo lỗi cho client..
400 là một Bad Request, phản hồi này chỉ ra rằng có vấn đề xảy ra với nội dung của request từ client.
Nếu validation error mình sẽ return 400 `.

CáchSetHTTPstatus400
$this->response->statusCode(400);

Hay xem mình sẽ thực hiện implement trên những file nào

Chi tiết các file cần chỉnh sửa

Thao tác file Mô tả
Sửa app/Model/TodoList.php Thêm validation rule
Sửa app/Controller/TodoListsController.php Kết quả Validation của xác nhận và sửa tin nhắn

TodoList.php

app/Model/TodoList.php
 <?php
-
 App::uses('AppModel', 'Model');
+App::uses('User', 'Model');
+App::uses('AuthComponent',  'Controller/Component');

 class TodoList extends AppModel {
    public $belongsTo = array (
        'Owner' => array (
            'className' => 'User',
            'foreignKey' => 'owner',
        ),
        'Assignee' => array (
            'className' => 'User',
            'foreignKey' => 'assignee'
        )
    );
+   public $validate = array (
+       //TODO check
+       'todo' => array (
+           //Số ký tự
+           'rule1' => array (
+               'rule' => array(
+                   'between', 1, 200
+               ),
+               'message' => '[Client] The characters must be in the range 1~200'
+           ),
+           //trùng với TODO đã có
+           'rule2' => array (
+               'rule' => 'isUnique',
+               'message' => '[Client] There are the same TODO exists in the TODO list.'
+           )
+       ),
+       //Kiểm tra status
+       'status' => array (
+           'rule1' => array (
+               //0 or 1 (Trường hợp custom)
+//                 'rule' => array(
+//                     'custom',
+//                     '/^[01]$/'
+//                 ),
+               //0 or 1 (trường hợp hợp lệ)
+               'rule' => array(
+                   'inList',
+                   array(0, 1)
+               ),
+               'message' => 'The status value must be 0 or 1'
+           ),
+       ),
+       //Kiểm tra người được phân công
+       'assignee' => array (
+           //ID đã tồn tại trong table users
+           'rule1' => array (
+               'rule' => array(
+                   'existsUser'
+               ),
+               'message' => '[Client] This user ID is already exists'
+           ),
+       ),
+       //Việc sửa/xóa
+       'id' => array (
+           //Kiểm tra người vào trang detail có phải Owner hay Assignee
+           'rule1' => array (
+               'rule' => array(
+                   'isOwnerOrAssignee'
+               ),
+               'message' => '[Client] Only Owner or Assigner can update this TODO'
+           ),
+       )
+   );
+
+   // Owner validation rules
+       // ID đã được phân công công việc, mình sẽ xem ID có tồn tại trong table users hay không
+   public function existsUser($userId){
+       $userModel = new User();
+       $count = $userModel->find('count', array('conditions'=>array('id'=>$userId), 'recursive' => -1));
+       return $count > 0;
+   }
+
+   //Owner validation rules
+   //Tự kiểm tra xem có phải Owner hay người được phân công hay không
+   public function isOwnerOrAssignee($id){
+       if(!isset($id)){
+           //Bỏ qua người đang đăng ký
+           return true;
+       }
+       $me = AuthComponent::user();
+       $todo = $this->findById($id);
+       if($todo){
+           if($todo['TodoList']['owner'] === $me['id']
+               || $todo['TodoList']['assignee'] === $me['id']){
+               return true;
+           }
+       }
+       return false;
+   }
+
+   // Owner validation rules
+       // Yourself to check whether the owner
+   public function isOwner($id){
+       if(!isset($id)){
+           //Không quan tâm đến người mới đăng ký
+           return true;
+       }
+       $me = AuthComponent::user();
+       $todo = $this->findById($id);
+       if($todo){
+           if($todo['TodoList']['owner'] === $me['id']){
+               return true;
+           }
+       }
+       return false;
+   }
 }
  • Chúng ta áp dụng validation rules của những built-in sau của CakePHP.
Áp dụng cho Rules Nội dung kiểm tra
todo between Độ dài mỗi TODO có nằm trong khoảng xác định hay không
todo isUnique Giá trị nhập vào có unique trong database hay không
status inList Giá trị gửi lên có nằm trong danh sách giá trị cho sẵn không
status custom Bạn có thể tạo ra các tùy chỉnh giá trị theo ý mình

:warning: Ngoài những cái đã liệt kê ở trên, CakePHP còn có rất nhiều built-in validation rules khác.
Các bạn có thể xem tại đây→Data validation - CakePHP Cookbook 2.x document

  • Thêm vào đó, chúng ta sẽ implement mội số hàm validation như sau.
Áp dụng với Hàm custom validation Nội dung kiểm tra
assignee existsUser Độ dài của input trong khoảng xác định
id isOwnerOrAssignee Kiểm tra dữ liệu trước khi chỉnh sửa xem user đó đã login hay chưa và có phải owner hay người được phân công hay không

:warning: Delete when | isOwner | Kiểm tra dữ liệu trước khi chỉnh sửa xem có phải là owner hay không

:warning: Vì hàm isOwner sẽ được thực thi tại thời điểm thực hiện hành động xóa, validation rules ở model và chạy ở controller không được mô tả.

TodoListsController.php

Kiểm tra kết quả validation và sửa thông báo để return tới client.

:warning: Bạn chỉ có một thao tác nhỏ ở đây và không cần làm gì với giao diện này cả.
  ※Hiện tại, mình đã thêm view() vì mình chưa xác định được field ở hàm.

TodoListsController.php
class TodoListsController extends AppController {
+   private $fields = array (
+       'TodoList.id',
+       'TodoList.todo',
+       'TodoList.status',
+       'Owner.id',
+       'Owner.name',
+       'Assignee.id',
+       'Assignee.name'
+   );

    public function index() {
        $query = array (
-           'fields' => array (
-               'TodoList.id',
-               'TodoList.todo',
-               'TodoList.status',
-               'Owner.id',
-               'Owner.name',
-               'Assignee.id',
-               'Assignee.name'
-           ),
+           'fields' => $this->fields,
            'order' => "TodoList.id"
        );
        $res = $this->TodoList->find('all', $query);
        // Xử lý sự kiện
        if (count($res) > 0) {
            $loginUserId = $this->Auth->user()['id'];
            foreach ( $res as $key => $row ) {
                // trường hợp "Login user là owner" 
                $res[$key]['TodoList']['owned'] = $row['Owner']['id'] === $loginUserId;
                // trường hợp "Login user là Assignee" 
                $res[$key]['TodoList']['assigned'] = $row['Assignee']['id'] === $loginUserId;
            }
        }
        $this->set(compact('res'));
        $this->set('_serialize', 'res');
    }

    public function view($id = null) {
-       $res = $this->TodoList->findById($id);
+       $res = $this->TodoList->findById($id, $this->fields);
        $this->set(compact('res'));
        $this->set('_serialize', 'res');
    }

    public function add() {
        $data = $this->request->data;
        $data['owner'] = $this->Auth->user()['id'];
        $res = $this->TodoList->save($data);
-       $this->set(compact('res'));
-       $this->set('_serialize', 'res');
+       $response = $this->editResponse($res);
+       $this->set(compact('response'));
+       $this->set('_serialize', 'response');
    }
    public function delete($id) {
-       $res = $this->TodoList->delete($id, false);
-       $this->set(compact('res'));
-       $this->set('_serialize', 'res');
+       //オーナかどうかチェック
+       if(!$this->TodoList->isOwner($id)){
+           $this->setStatusValidationError();
+           $response = $this->editErrors('Only the owner can detete this TODO.');
+       }else{
+           $res = $this->TodoList->delete($id, false);
+           $response = $this->editResponse($res);
+       }
+       $this->set(compact('response'));
+       $this->set('_serialize', 'response');
    }

    public function edit($id) {
        $this->TodoList->id = $id;
        $data = $this->request->data;
        $res = $this->TodoList->save($this->request->data);
        $res = !empty($res);
-       $this->set(compact('res'));
-       $this->set('_serialize', 'res');
+       $response = $this->editResponse($res);
+       $this->set(compact('response'));
+       $this->set('_serialize', 'response');
+   }
+
+   //Sửa các phản hồi
+   private function editResponse($res){
+       if($res){
+           $response = $res;
+       }else{
+           $this->setStatusValidationError();
+           $respnse = array();
+           if(count($this->TodoList->validationErrors) > 0){
+               $response = $this->editErrors($this->TodoList->validationErrors);
+           }else{
+               $response = $this->editErrors('An error occured.');
+           }
+       }
+       return $response;
+   }
+
+   // Khi gặp validation errors set response giá trị 400
+   private function setStatusValidationError(){
+       $this->response->statusCode(400);
+   }
+
+   // Sửa các thông báo lỗi
+   private function editErrors($errors){
+       if(is_array($errors)){
+           $res['errors'] = $errors;
+       }else{
+           $res['errors']  = array('error' => array($errors));
+       }
+       return $res;
    }

 }
  • Hàm Add và hàm edit

Sau khi thực thi save trong model, chúng we chạy hàm editResponse.

  • Hàm editResponse

Nếu giá trị trả về từ hàm save không phải true, kiểm tra giá trị validationErrors của model, xác nhận xem có validation error nào không.
Nếu có validation error, Set HTTP status 400, bạn phải sửa JSON cho việc return lỗi như format sau đây.

ErrorReturnJSONformat
{
    "errors": {
        "item key": [
            "error messsage"
        ],
        "item key": [
            "error messsage"
        ],
         tương tự 
    }
}
SampleOfErrorReturnJSON
{
    "errors": {
        "todo": [
            "length of TODO must be 1~200 characters"
        ],
        "status": [
            "Status must be 0 or 1"
        ],
        "id": [
            "Only Owner or Assignee can update this TODO"
        ]
    }
}

Tiến hành implement

Tóm tắt bài học và các kiểm tra

  • :white_check_mark: app/Model/TodoList.php sửa như trên.
  • :white_check_mark: app/Controller/TodoListsController.php sửa như trên.
  • :white_check_mark: Kiểm tra kết quả.
    • Làm theo hướng dẫn của bài 0
      • :white_check_mark: Số ký tự của TODO vượt giới hạn
        • return The lengths of TODO must in the range of 1~200 characters
      • :white_check_mark: TODO rỗng
        • return The lengths of TODO must in the range of 1~200 characters
      • :white_check_mark: Status là 9
        • return Status must be 0 or 1
      • :white_check_mark: Assignee User ID không tồn tại
        • return Assignee User ID not exist
      • :white_check_mark: Không phải Owner, Assignee thực hiện chức năng chỉnh sửa
        • return Only Owner can update this TODO
      • :white_check_mark: Không phải Owner thực hiện chức năng xóa
        • Only Owner can delete this TODO
  • :white_check_mark: Commit lên Git

:warning: Tài liệu tham khảo dạng hiển thị diff trên Github

7th bài 1 server side · suzukishouten-study/rest-study@b61fdf7

:large_blue_circle: Bài 2: Thực hiện ở client-side

Ở bài 2 này, chúng ta sẽ thực hiện việc implement ở client-side

Chú ý: Mô tả các hàm validation

Chúng ta sẽ thêm một hàm check cho validate method của Backbone.Model
Trong nền tảng của Backbone không có các built-in để validation như trong CakePHP, Tôi sẽ tiếp tục implement logic.
Bởi vì validate method được thực thi tự động khi bạn chạy set method của model, bạn không cần phải gọi validate nữa.
Tại validate method, tổng hợp tất cả các thông báo lỗi vào một mảng, sau đó return chúng.
Mỗi lần cái gì đấy được return từ validate method (not-null), vì sự kiện invalid của model đa chạy xong , tạo thông báo lỗi và hiển thị chúng lên view.

Ví dụ: implementation của Model

ValidationLogicInValidateMethod
    validate : function(attrs) {
        var errors = [];

        //Kiểm tra độ dài
        var todoLength = attrs.todo.length;
        if (todoLength < 1 || todoLength > 200) {
            errors.push('[Client]TODO length must be 1~200 characters.');
        }

        Mình sẽ tiếp tục thêm các validation logic với formart tương tự

        //return error khi sự kiện invalid của model xảy ra
        if (errors.length > 0){
            return errors;
        }else{
            return null;
        }
    }
  • var errors[]; Mảng sẽ chứa các thông báo lỗi
    • //the logic of check length Đây là phần code logic kiểm tra độ dài.
      • Errors.push('[Client] TODO must be in the length of 1~200 characters.'); Hiển thị thông báo lỗi.
      • Return errors length của erros phải lớn hơn giá trị mình đã set từ đầu đó là 0, sau đó trả về tất cả với điều kiện nếu có errors. Nếu This của model là invalid thì event xảy ra.

Chú ý: Thông báo lỗi sẽ được hiển tại tại thời điểm validation error trên server

Khi có validation error trên server side, returned HTTP status 400.
Trên chương trình của chúng ta hiện tại, status không phải 200 khi giao tiếp với server qua app.js đều đã được xử lý ( handle ), sửa các tiến trình mà xảy ra status 400.
Những thông tin chi tiết hơn, tôi sẽ giải thích ở app.js.

Bây giờ chúng ta sẽ sửa chủ yếu ở View

Các file cần thay đổi

Thao tác file Mô tả
Sửa app/webroot/js/models/todo-model.js Implementation của client-side validation logic
Sửa app/webroot/js/views/todo-composite-view.js Kết quả validation của Việc xác nhận và chỉnh sửa TODO trên client-side
Sửa app/webroot/js/views/todo-detail-item-view.js Kết quả validation của Việc xác nhận và chỉnh sửa TODO trên client-side
Sửa app/webroot/js/app.js Bắt lỗi và sửa các validation error của server-side

todo-model.js

Implement validation logic

todo-model.js
 //Model representing County Todo data 1
 define(function() {
    var TodoModel = Backbone.Model.extend({
        urlRoot : '/rest-study/todo_lists',
        parse : function(response) {
            //Model parse
            console.log("モデルをパース");
            console.log(response);
            var parsed = response.TodoList;
            if (response.Owner) {
                parsed.Owner = response.Owner;
                parsed.Assignee = response.Assignee;
            }
            return parsed;
        },
        toggle : function() {
            this.set('status', this.get("status") === '1' ? '0' : '1');
            this.save();
+       },
+       validate : function(attrs) {
+           var errors = [];
+           
+           //Kiểm tra độ dài
+           var todoLength = attrs.todo.length;
+           if (todoLength < 1 || todoLength > 200) {
+               errors.push('[Client]The TODO length must be 1〜200 characters');
+           }
+
+           //Thử nghiệm chỉ cho phép nhập giá trị hoge, bỏ comment để thử chạy
+           //if(attrs.todo !== 'hoge'){
+           //  errors.push('[Client]TODO on ly "hoge" !');
+           //}
+
+           if (errors.length > 0){
+               return errors;
+           }else{
+               return null;
+           }
        }
    });
    return TodoModel;
  • validate method

Tôi nghĩ không cần phải miêu tả đặc biệt đối với logic.
Bởi vì giá trị input được truyền vào biến attrs, cần phải implement logic để check nó.
Nếu có lỗi xảy ra sẽ return vào biến errors variable, nếu không sẽ truyền giá trịnull.
※ Nó sẽ chạy đúng mà không cần phải tự return chính nó

todo-composite-view.js

Khi thêm TODO, tại thời điểm xảy ra validation error trên model, và bắt đầu xử lý sự kiện invalid.

todo-composite-view.js
 //View cho TODO list
 define(function(require) {
    var TodoItemView = require('views/todo-item-view');
+   var TodoModel = require('models/todo-model');

    var TodoCompositeView = Marionette.CompositeView.extend({
        template: '#todo-composite-template',

        childView : TodoItemView,

        childViewContainer : 'tbody',

+       newTodoModel : new TodoModel(),
+       
        ui : {
            addTodo : '#addTodo',
            newTodo : '#new-todo',
            userList : '#user-list'
        },

        events : {
            'click @ui.addTodo' : 'onCreateTodo',
        },

        initialize: function(options){
            _.bindAll( this, 'onCreatedSuccess' );
            this.userList = options.userList;
+           this.listenTo(this.newTodoModel, 'invalid', this.renderErrorMessage);
        },

        onRender : function() {
            //Show user list
            this.showUserList(this.ui.userList, this.userList);
            //Mặc định hiển thị cho những người đã logged-in
            this.ui.userList.val(window.application.loginUser.id);
        },

        //Show user list
        showUserList : function($list, userList){
            $.each(userList, function(index, userModel) {
                $list.append(
                    "<option value='" 
                    + userModel.attributes.id + "'>"
                    + userModel.attributes.name + "</option>");
            });
        },

        onCreateTodo : function() {
-           this.collection.create(this.newAttributes(), {
+           this.newTodoModel.clear({silent : true});
+           this.newTodoModel.set(this.newAttributes());
+           this.collection.create(this.newTodoModel, {
                  silent:  true ,
                  success: this.onCreatedSuccess
            });
            this.ui.newTodo.val('');
        },

        newAttributes : function() {
            return {
                todo : this.ui.newTodo.val().trim(),
                status : 0,
                assignee : this.ui.userList.val()
            };
        },

        onCreatedSuccess : function(){
            this.collection.fetch({ reset : true });
        },
-
+       
+       //Error display
+       renderErrorMessage : function(errors){
+           var message = '';
+           for(var key in errors.validationError){
+               message += errors.validationError[key];
+           }
+           alert(message);
+       }
    });
    return TodoCompositeView;
 }); 
  • newTodoModel : new TodoModel(),
    • Thêm một model để xử lý cho việc validation.
      • Sau đó thêm class require để require các thành phần cần thiết cho model.
  • Hàm initialize
    • Sử dụng hàm listenTo sẽ thêm hàm xử lý sự kiện cho sự kiện invalid event của model như mô tả trên kia (this.renderErrorMessage).
  • Hàm onCreateTodo
    • Bạn chỉ cần sử dụng duy nhất this.collection.create, tuy nhiên để chạy valiate của model, chúng ta cần set giá trị của model ở hàm set. Thêm vào đó, mình sẽ xóa nội dung của model ở hàm clear trước đó.
  • Hàm renderErrorMessage
    • Đây là hàm xử lý sự kiện invalid của model. Sửa nội dung thông báo lỗi sẽ hiển thị ở alert.

todo-detail-item-view.js

Trong khi sửa TODO, khi validation error xảy ra ở model mình sẽ chạy hàm xử lý sự kiện invalid.
todo-composite-view.js mình gần như implement tương tự.

todo-detail-item-view.js
 //View trang detail
 define(function() {
    var TodoDetailItemView = Marionette.ItemView.extend({

 〜 中略 〜

        //Khởi tạo
        initialize: function(options){
            _.bindAll( this, 'onSaveSuccess' );
            this.userList = options.userList;
+           this.listenTo(this.model, 'invalid', this.renderErrorMessage);
        },

        onRender : function() {
            //Show user list
            this.showUserList(this.ui.userList, this.userList);
            //stata dành cho người được phân công
            this.ui.userList.val(this.model.attributes.assignee);
        },

        //Show user list
        showUserList : function($list, userList){
            $.each(userList, function(index, userModel) {
            $list.append(
                "<option value='" 
                + userModel.attributes.id + "'>"
                + userModel.attributes.name + "</option>");
            });
        },

        //Xử lý sự kiện click nút chỉnh sửa (Update) 
        onUpdateClick : function() {
            //Lấy các ký tự từ checkbox
            var todoString = this.ui.todoStatus.val();  // Todo
            var assigneeId = this.ui.userList.val();    // Assignee
-           this.model.save({
+           this.model.set({
                todo : todoString,
                assignee : assigneeId
-           }, {
+           });
+           this.model.save(null,{
                silent : true,
                success : this.onSaveSuccess,
            });
        },

        //Xử lý sự kiện click nút Cancel 
        onCancelClick : function() {
            this.backTodoLists();
        },

        //Update success
        onSaveSuccess : function() {
            this.backTodoLists();
        },

        //Chuyển sang màn hình TODO list
        backTodoLists : function() {
            Backbone.history.navigate('#todo-lists', true);
+
+       },
+       
+       //Hiển thị lỗi
+       renderErrorMessage : function(errors){
+           var message = '';
+           for(var key in errors.validationError){
+               message += errors.validationError[key];
+           }
+           alert(message);
        }

    });
    return TodoDetailItemView;
 });

Bởi vì View này bạn đã thừa kế từ Marionette.ItemView, Vì vậy bạn có thể truy cập vào model qua this.model, mình implement gần như giống hệt ngoại trừ todo-composite-view.js vì đây là nơi không sinh ra model.

app.js

Bây giờ mình sẽ xử lý sự kiện HTTP status 400.

app.js
 //Application
 console.log('load app');
 define(function(require){

 〜 中略 〜


        // tất cả các xử lý sự kiện bằng ajax ở đây
        ajaxErrorHandler : function(e, xhr, options , message){
            if( xhr.status === 401 ){
                this.clearLoginUser();
                // Nếu unauthenticated bỏ qua và chuyển về màn hình login
                Backbone.history.navigate('#login', {trigger : true, replace : true});
-           }else if(xhr.status >= 400 && xhr.status < 500){
+           }else if(xhr.status === 400){
+               if(xhr.responseJSON){
+                   var errors = xhr.responseJSON.errors;
+                   var msg = '';
+                   for(var key in errors){
+                       for(var idx in errors[key]){
+                           msg = msg + errors[key][idx] + '\n';
+                       }
+                   }
+                   alert(msg);
+               }else{
+                   alert(message);
+               }
+           }else if(xhr.status > 400 && xhr.status < 500){
                //Hiển thị thông báo từ ClientError
                alert(message);
            }else if(xhr.status >= 500 && xhr.status < 600){
                //Hiển thị thông báo từ ServerError
                alert(message);
            }
        },

    });
    return Application;
 });

Trước đây mình cần xử lý riêng biệt HTTP status 400500, chúng ta đã thêm mệnh đề if để xử lý riêng biệt riêng số 400.
Vì sẽ nhận được JSON error response sau khi được server xử lý tại xhr.responseJSON, sửa nó và hiển thị nó vào alert.

Thực hành

Hãy kiểm tra các bạn đã thực hiện đầy đủ các bươc sau chưa

  • :white_check_mark: Sửa theo mô tả trên app/webroot/js/models/todo-model.js.
  • :white_check_mark: Sửa theo mô tả trên app/webroot/js/views/todo-composite-view.js.
  • :white_check_mark: Sửa theo mô tả trên app/webroot/js/views/todo-detail-item-view.js.
  • :white_check_mark: Sửa theo mô tả trên app/webroot/js/app.js.
  • :white_check_mark: Kiểm tra kết quả
    • Hãy kiểm tra bằng các thao tác trên trình duyệt
  • :white_check_mark: Commit lên Git

:warning: Tham khảo thay đổi dạng Diff trên Github

Chương 7 : Bài 2 Client-side · suzukishouten-study/rest-study@c0ae3e1

Đó là tất cả của bài học này

Giới thiệu

Ở bài học sau chúng ta sẽ bắt đầu sử dụng Bootstrap để làm giao diện đẹp hơn và các hiệu ứng hay ho, các bạn có thể lên trang chủ bootstrap để làm quen trước.

Rất mong nhận được phản hồi/ góp ý của các bạn

Cảm ơn các bạn đã đi cùng với series này, mình sẽ cố gắng truyền đạt tốt nhất ý đồ của tác giả tới các bạn, tuy nhiên chắc chắn vẫn còn rất nhiều sai sót và mình sẽ update thường xuyên và chỉnh sửa các bài viết cũ, rất mong nhận dược những phản hồi quý báu của các bạn để những bài viết của mình có chất lượng hơn.

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2