Chào mừng các bạn đã trở lại với series
Ở bài viết trước chúng ta đã tìm hiểu cách quản lý sự phụ thuộc của các với nhau bằng require.js, các bạn có thể xem lại ở [Bài 4] Sử dụng RequireJS để viết module cho JavaScript.
Bây giờ chúng ta bắt đầu viết chức năng đăng nhập cho chương trình, bài viết này được dịch từ CakePHPで実装するログイン機能 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第5回】マニュアル của tác giả @k_shimoji, mình đã làm theo đến hết và cố gắng Việt Hóa hoặc chuyển sang tiếng Anh cơ bản nhiều nhất có thể để các bạn dễ hình dung.
Ở chương 2 chúng ta đã tạo ra giao diện đăng nhập với Backbone.js ở bài 2, bây giờ mình sẽ bổ sung các chức năng.
Thông tin của user mình lưu trong database có một thành phần đặc biệt là mật khẩu, mình sẽ mã hóa nó trong quá trình đăng ký, vì vậy các bạn sẽ gặp lỗi nếu thêm dữ liệu trực tiếp bằng phpmyadmin.
Đăng nhập là gì
Chắc nhiều bạn đã biết định nghĩa của login rồi nên không cần nhắc lại nhiều nữa.
Việc chúng ta cần làm là tạo một chức năng đăng nhập với 2 chức năng sau:
- Xác thực - Authentication
- Kiểm duyệt - Authorization
Xác thực - Authentication là gì ?
Authentication hay còn gọi là xác thực "identification".
Mình sắp tạo chức năng login, và mình sẽ sử dụng các form authentication truyền thống, cụ thể hơn là sử dụng "Password" và "User ID" để nhận dạng một người, một tài khoản....
Thường nói đến login là nói đến "Authentication", Chúng ta sẽ thực hiện các bước "approval" và phần set.
Kiểm duyệt - Authorization là gì ?
Một user sau khi đã được "authenticated" sẽ được cho phép truy cập vào các tài nguyên hay chức năng cụ thể. Bước kiểm duyệt ( Authorization) sẽ giúp cho phép họ vào đúng các chức năng mà họ được phép truy cập.
Vị trí đặc biệt của việc thực hiện quá trình đăng nhập hiện tại
Tiêu đề bài học của chúng ta là "REST", "SinglePageApplication (SPA)".
Tuy nhiên không phải REST hay là SPA, đây là một app cơ bản bằng CakePHP ( Một Web application thông dụng để sinh ra các trang ở server side), nếu có truy cập vào trang cần xác thực thì bạn cần một page ở khu vực dành cho những người chưa được kiểm duyệt (unauthorized state) hay chưa đăng nhập, bạn sẽ được chuyển ( redirect ) về trang đăng nhập.
Tuy nhiên, vì SPA được cấu hình để tất cả các truy cập đều thông qua RestAPI, vì vậy việc redirect sẽ không được sử dụng, chúng ta đơn giản là return HTTP status code 401 (Unauthorized) tới client, chương trình ở client-side sẽ tự hiển thị màn hình đăng nhập.
Tất cả các API requests ở khu vực dành cho non-authorized đều sẽ được return HTTP status code 401, client side sẽ phải phản hồi nó.
Nội dung bài học
Ở bài học này các bạn sẽ làm được những hàm dưới đây, hãy cố gắng nhé vì đây là những hàm được sử dụng rất thường xuyên !
Danh sách đặc điểm
- Đăng ký User
- Username và tên (name), mật khẩu (password) và sau khi Enter, mình sẽ tạo được user mới
- Nếu thành công sẽ hiển thị thông báo thành công
- Đăng nhập
- Nhập username, password và Click button Login.
- Nếu các thông tin đăng nhập là đúng, mình sẽ chuyển đến màn hình danh sách TODO list.
- Nếu sai thông tin sẽ xuất ra lỗi.
- Hiển thị thông báo đã đăng nhập nếu người dùng đã đăng nhập trước đó rồi.
- Nếu user đã đăng nhập từ trước sẽ chuyển đến màn hình danh sách TODO list.
- Đăng xuất
- Click vào logout button để quay lại phần login.
- Chuyển qua trang login.
- Hiển thị Header
- Sau khi đăng nhập thì mình sẽ luôn hiển thị username và nút logout ở phần trên cùng của màn hình (header).
- Kiểm tra xác thực
- Với những người chưa được xác thực đăng nhập (unauthorized users), tất cả các truy cập sẽ đều được chuyển trở lại màn hình login.
Màn hình hiển thị sẽ trông như thế này
- Đăng ký user
- Ở một nửa màn hình trang login, phần dưới sẽ là màn hình đăng ký user.
- Ở màn hình đăng ký, bạn cần đăng ký username, tên, và password.
-
Một quy trình đăng ký tốt cần phải thêm một phần xác nhận "re-enter the password" để đề phòng người dùng gõ sai chính tả, tuy nhiên chúng ta chỉ làm demo ví dụ nên sẽ tạm bỏ qua phần này.
- OK bắt đầu làm thôi!
Mục tiêu sau khi xong bài 5 này chúng ta sẽ được kết quả như sau
- Ở một nửa màn hình trang login, phần dưới sẽ là màn hình đăng ký user.
- màn hình TODO list
- Header sẽ xuất hiện ở trên cùng, hiển thị username, tên, và nút logout để đăng xuất.
- Trang detail của mỗi TODO
- Nó cũng hiển thị header ở trang này
Chuẩn bị trước khi tiến hành
Những sự chuẩn bị sẽ giống nhau ở mỗi bài học, tôi đã tổng hợp ở một bài viết riêng biệt
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 các bạn có thể tham khảo .
- Tạo
vol/05
trên git.
Ngoài ra các bạn hãy làm việc rất quan trọng sau nữa nhé
- Tạo table cho việc đăng ký user
Tạo table cho việc đăng ký user
Để tạo, chúng ta sẽ thêm table tên là users
.
Đầu tiên hãy đăng nhập vào phpMyAdmin.
URL vẫn là http://(PublicIP)/phpMyAdmin/
.
Nếu các bạn chưa thay đổi gì thì tên và pass đăng nhập cùng là study
.
Như hình ảnh dưới đây, nhìn vào database [study] ở phần bên phải - Click vào [New] để tạo table mới.
Hãy thiết lập như sau
Tên table
users
Thiết lập cho mỗi column
Name | Data type | length / value | default value | collation | attribute | NULL | Index | A_I (AutoIncrement) | Comments |
---|---|---|---|---|---|---|---|---|---|
id | INT | 10 | None | UNSIGNED | PRIMARY | v | id | ||
username | VARCHAR | 20 | None | utf8_general_ci | UNIQUE | tên user | |||
password | VARCHAR | 100 | None | utf8_general_ci | password | ||||
name | VARCHAR | 50 | None | utf8_general_ci | tên |
Các thiết lập
- Table's comment:
user
- Storage Engine:
InnoDB
- Bảng mã:
utf8_general_ci
- Định nghĩa phân vùng: bỏ trống
Click save
để tạo table
Chuẩn bị
Tóm tắt quá trình chuẩn bị
-
Sao lưu các branch cũ và tạo branch
vol/05
mới. - Tạo table cho việc đăng ký user
Khi đã sẵn sàng hết, chúng ta bắt đầu bài 1
Bài 1: Server side
Ở bài 1, mình sẽ implement phần server side của chương trình.
Để kiểm tra kết quả chạy, dử dụng "POSTMAN" mà mình đã cài đặt ở bài viết đầu tiên để kiểm tra kết quả trả về từ API.
Chú ý
Ứng dụng này không phải SPA.
Nếu bạn thử xem TODO List ở khu vực chưa login
Trường hợp web app không phải SPA ( Phần lớn )
Trình tự thao tác
- Yêu cầu được vào màn hình TODO list gửi đến server
- Về phần server, vì đây là request từ một user chưa được xác thực, nó sẽ trả về 302, và sau đó redirected về màn hình đăng nhập.
- Sau khi trình duyệt nhận được kết quả trả về 302, request cụ thể bằng URL trong
Location
header để chuyển trang - server trả về HTML của trang login
Trong trường hợp SPA
Nó sẽ như sau
- request tới server để vào màn hình TODO list.
- Về phần server server, vì đây là requst từ user chưa login, trả về status code 401, nó sẽ phản hồi rằng request này không được cho phép.
- Khi nhận về kết quả 401 thì Javascript sẽ hiểu và tự hiển thị màn hình login.
Hãy hiểu rằng 2 trường hợp trên khác nhau.
Tạo API
URL | Http Method | Mô tả | Controller | Action |
---|---|---|---|---|
/rest-study/users/login.json | POST | Đăng nhập | UsersController | login |
/rest-study/users/logout.json | POST | Đăng xuất | UsersController | logout |
/rest-study/users/loggedin.json | GET | Lấy thông tin đăng nhập của user ( kiểm tra đã login chưa) | UsersController | loggedIn |
/rest-study/users/signup.json | POST | Đăng ký user | UsersController | signUp |
Các file cần chỉnh sửa
Thao tác | file | Mô tả |
---|---|---|
Chỉnh sửa | app/Config/routes.php | Thêm các đường dẫn root của API |
Chỉnh sửa | app/Controller/AppController.php | Các thiết lập của Auth component |
Thêm mới | app/Controller/UsersController.php | Sử dụng |
Thêm mới | app/Model/User.php | Truy cập vào table user. |
routes.php
Thêm route cho API vừa thêm vào
/*
* API
*/
+
+// Đăng nhập
+Router::connect('/users/login', array (
+ 'controller' => 'users',
+ 'action' => 'login',
+ 'method' => array (
+ 'POST'
+ )
+));
+
+// Đăng xuất
+Router::connect('/users/logout', array (
+ 'controller' => 'users',
+ 'action' => 'logout',
+ 'method' => 'POST'
+));
+
+// Login check (kiểm tra thông tin đăng nhập)
+Router::connect('/users/loggedin', array (
+ 'controller' => 'users',
+ 'action' => 'loggedIn',
+ 'method' => 'GET'
+));
+
+// Đăng ký
+Router::connect('/users/signup', array (
+ 'controller' => 'users',
+ 'action' => 'signUp',
+ 'method' => array (
+ 'POST'
+ )
+));
+
+
Router::mapResources(array (
'todo_lists',
));
Router::parseExtensions('json');
Nếu một API để thực hiện một CRUD đơn giản,
Bằng hàm Router::mapResources()
, Controller luôn được thêm .
route cho các hành động của index
, view
, add
, edit
, delete
sẽ được set, nhưng vì URL đăng nhập không cùng loại nên nó sẽ được thêm riêng
Sử dụng hàm Router::connect()
đã được set với các thành phần theo thứ tự như dưới đây.
- URL
- Controller để chạy
- Hành động để thực thi
- Http method mà bạn thích
VD ở hàm login
- URL ->
/users/login
- Run tới controller ->
users
- Thực thi hành động->
login
- Phương thức HTTP dể trỏ đến. ->
POST
AppController.php
Component, thêm các bước xác thực
class AppController extends Controller {
- public $components = array(
- 'RequestHandler'
- );
+ public $components = array (
+ 'RequestHandler',
+ 'Auth' => array (
+ 'authenticate' => array (
+ 'Form' => array (
+ 'passwordHasher' => 'Blowfish'
+ )
+ )
+ )
+ );
+
+ public function beforeFilter() {
+ $user = $this->Auth->user();
+ if ($user === null
+ && $this->request->params['controller'] !== 'users'
+ && $this->request->params['action'] !== 'login'
+ && $this->request->params['action'] !== 'logout'
+ && $this->request->params['action'] !== 'loggedin'
+ && $this->request->params['action'] !== 'signup') {
+ throw new UnauthorizedException();
+ }
+ //Cho phép tất cả các hành động
+ $this->Auth->allow();
+ }
+
}
Biến $Components
Tiến trình authentication sử dụng Auth
components mà CakePHP đã có sẵn.
Mình sử dụng Blowfish
để xử lý thuật toán hash mã hóa password.
Hàm BeforeFilter
Một hàm để được thực thi trước tất cả các hành động gọi là hàm beforeFilter.
Tại đây sẽ kiểm tra user đang truy cập đã được authenticated hay chưa.
Nếu user đó là unauthenticated thì việc đăng nhập (login), đăng ký user (signup), khác với check trạng thái đăng nhập (loggedin) nên mình không chạy nó được.
-
$This->Auth->user()
để lấy thông tin đăng nhập của user - Nếu thông tin đăng nhập của user không thể lấy được, với exception của
users
controller, sẽ trả về lỗi 401 error bằng cách throwUnauthorizedException
.- login
- loggedin
- signup
UsersController.php
Đăng nhập, xác thực đăng nhập thành công, đăng xuất, thực hiện đăng ký.
<?php
App::uses('AppController', 'Controller');
App::uses('BlowfishPasswordHasher', 'Controller/Component/Auth');
class UsersController extends AppController {
public function login() {
$user = $this->Auth->user();
$res = array();
if ($user) {
if ($user['username'] === $this->request->data['username']) {
$res['User'] = $user;
$res['message'] = "You are already logged in";
} else {
$res['message'] = "another is loged in this account";
}
} else {
$this->request->data['User']['username'] = $this->request->data['username'];
$this->request->data['User']['password'] = $this->request->data['password'];
if ($this->Auth->login()) {
$user = $this->Auth->user();
$res['User'] = $user;
$res['message'] = "Login successfully";
} else {
$res['message'] = "Username or password are incorrect";
}
}
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
public function loggedIn(){
$user = $this->Auth->user();
if ($user) {
$res['User'] = $user;
$res['message'] = "You are already logged in";
$this->set(compact('res'));
$this->set('_serialize', 'res');
}else{
throw new UnauthorizedException();
}
}
public function logout() {
$user = $this->Auth->user();
$res = array();
if ($user !== null) {
$this->Auth->logout();
$res['message'] = "Logout successfully";
} else {
$res['message'] = "can not logout";
}
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
public function signUp() {
$data = $this->request->data;
if (isset($data['password'])) {
$passwordHasher = new BlowfishPasswordHasher();
$data['password'] = $passwordHasher->hash($data['password']);
}
$res = $this->User->save($data);
if($res){
unset($res['User']['password']);
$res['message'] = "Register successfully, you can Login!";
}else{
$res['message'] = "Fail to register";
}
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
}
Hàm login
Nó sẽ chạy tiến trình login. Kết quả sẽ là một trong những cái sau:
- Đăng nhập thành công
- Đăng nhập thất bại (sai username hoặc password)
- Đã đăng nhập
- Người khác đã đăng nhập rồi
Tiến trình đăng nhập,
- Gửi username qua
$this->request->data['User']['username']
- Gửi password qua
$this->request->data['User']['password']
- Chạy
$this->Auth->login()
As JSON item to return to the client, you have to set the following.
-
User
-> Thông tin user (id, username, name) -
message
-> Tin nhắn thông báo kết quả xử lý
{
"User": {
"id": "1",
"username": "test",
"name": "test"
},
"message": "Login successfully"
}
Hàm loggedIn
Check xem user đã đăng nhập hay chưa.
- Lây thông tin đăng nhập của user ở
$this->Auth->user()
, nó sẽ trả về kết quảnull
nếu chưa đăng nhập. - Nếu chưa đăng nhập, sinh ra lỗi 401 error bằng cách throw
UnauthorizedException
. - JSON trả về client giống như hàm
login
, xuấtuser
vàmessage
.
Hàm logout
Nó sẽ chạy tiến trình đăng xuẩt.
-
$This->Auth->user()
để kiểm tra user đã đăng nhập hay chưa, nếu đã đăng nhập thì chạy tiến trình log out bằng$this->Auth->logout()
. - Nếu chưa login thì sẽ không chạy được tiến trình log out, thêm một
message
ở JSON.
Hàm register
Sử dụng User
model để đăng ký thông tin user vào database.
- Password được lưu bằng một chuỗi (string) được mã hóa bởi class
BlowfishPasswordHasher
. - Nếu đã lưu thành công mình sẽ set thông tin user ở JSON, riêng password thì là một ngoại lệ (
unset($res['User']['password']);
).
User.php
Table users là một model cho việc thêm data.
<?php
App::uses('AppModel', 'Model');
class User extends AppModel {
}
Nó sẽ không implement những hàm không đúng tiêu chuẩn của CakePHP model.
Bạn có thể dễ dàng tìm thấy các app phù hợp, nhưng phải implement của việc validation giá trị input tại đây, nó sẽ quyết định không implement chúng vì không phải ở giao diện này.
### Kiểm tra kết quả
Chúng ta sẽ kiểm tra từng API bằng POSTMAN.
Hãy để các ví dụ vào mỗi API, dữ liệu dưới chỉ để tham khảo.
user và password sẽ được tạo sau khi đăng ký ( nó sẽ tự sinh mã bằng đoạn hash cho password và lưu vào database chứ không phải định dạng như thường ), khi bạn nhập pass vào siginin
thì nó lại sinh mã 1 lần nữa rồi so sánh với mã trong database .
Nếu bạn cố gắng thêm user và pass phpMyAdmin thì rất có thể bạn sẽ đăng nhập thất bại.
Sẽ tốt nhất nếu bạn bắt đầu test từ siginup
API, xách POSTMAN lên và test thôi nào.
URL | Request JSON | response code | response JSON |
---|---|---|---|
/rest-study/users/signup.json | { "username": "test2", "password": "test2", "name": "test2" } |
200 | { "User": { "username": "test2", "name": "test2", "id": "65" }, "message": "Registered. You can log in!" } |
/rest-study/users/login.json | { "username":"test", "password":"test" } |
200 | { "User": { "id": "1", "username": "test", "name": "test" }, "message": "Login successfully" } |
/rest-study/users/login.json | { "username":"test", "password":"hoge" } |
200 | { "message": "Wrong user name or password" } |
/rest-study/users/logout.json | - | 200 | { "message": "Log out" } |
/rest-study/users/loggedin.json | - | 200 | { "User": { "id": "1", "username": "test", "name": "Test" }, "message": "You are already logged in" } |
- Ví dụ của lỗi 1 ( Kiểm tra login ở khu vực chưa login - trả về 401)
URL | Request JSON | response code | response JSON |
---|---|---|---|
/rest-study/users/loggedin.json | - | 401 | { "name": "Unauthorized", "message": "Unauthorized", "url": "/rest-study/users/loggedIn.json" } |
- Ví dụ của lỗi 2 ( Truy cập TODO list khi chưa login - trả về 401)
URL | Request JSON | response code | response JSON |
---|---|---|---|
/rest-study/users/todo_lists.json | - | 401 | { "name": "Unauthorized", "message": "Unauthorized", "url": "/rest-study/users/todo_lists.json" } |
Tóm tắt bài học
Hãy kiểm tra sự hoạt động của API xem bạn đã thực sự làm đúng chưa
-
app/Config/routes.php
sửa như hướng dẫn trên. -
app/Controller/AppController.php
sửa như hướng dẫn trên. -
app/Controller/UsersController.php
thêm mới như hướng dẫn trên. -
app/Model/User.php
thêm mới như hướng dẫn trên. - Kiểm tra kết quả.
- Commit lên Git
Tham khảo bản diff hoàn chỉnh trên GitHub
Bài 5: serverside· suzukishouten-study/rest-study@59c29b0
Đến với bài 2 nào:
Bài 2 : Client Side
Ở bài 2 này chúng ta sẽ bắt đầu implement client side của chương trình
###Lưu ý
Hãy check vì có một vài điểm hấp dẫn bạn đây.
Mình sẽ giao tiếp với server bằng Ajax, nó sẽ không đồng bộ nên có thể sẽ gây ra một số sự khó chịu.
Sau đây mình sẽ diễn tả lại những tiến trình sẽ xảy ra sau khi bạn điền URL của màn hình TODO list vào trình duyệt.
Đầu tiên bạn cần gửi giữ liệu của user lên server để xác định đã đăng nhập hay chưa.
( nó cũng sẽ phục vụ việc lấy thông tin của user để hiển thị trên header.)
####Biểu đồ hoạt động
Tiến trình các hoạt động được miêu tả bằng hình vẽ dưới đây
- Bắt đầu việc xác nhận người dùng đăng nhập hay chưa.
- Sử dụng XHR (XmlHttpRequest) để giao tiếp với server. Trong trường hợp này bạn sẽ gọi một hàm callback để thực hiện tại thời điểm nhận được phản hồi từ server đến XHR.
- Sau khi nhận được phản hồi từ server, XHR chạy hàm callback như đã nói ở bước 2.
- Tại hàm callback, xem phản hồi từ server và lưu trữ thông tin user (thông báo trạng thái là
đã đăng nhập
haychưa đăng nhập
). - Chạy routing ( Hàm
Backbone.history.start()
). - Controller được chạy, hàm hiển thị màn hình TODO list bắt đầu chạy.
- Kiểm tra thông tin user lưu tại bước 4
- Nếu thông tin user được xác nhận là
loggedIn
mình sẽ cho họ xem màn hình TODO list.- Bạn có thể dễ dàng tìm thông tin user (chưa đăng nhập) và hiển thị màn hình đăng nhập.
- Nếu thông tin user được xác nhận là
Pattern tĩnh
Mở Chương tình của chúng ta ở thời điểm của bài trước, chạy routing như bước 5 trên kia, chúng ta sẽ chuẩn bị chạy hàm Backbone.history.start()
)
Tại phần này nó sẽ theo trình tự như sau.
- Bật routing (Hàm
Backbone.history.start ()
). - Bắt đầu tiến trình xác nhận user đã đăng nhập chưa.
- Sử dụng XHR (XmlHttpRequest) để giao tiếp với server. Bạn bắt đầu gọi một hàm callback để chạy vào thời ddioeemr nhận phản hồi từ server đến XHR.
- Routing đã được bật ở phần 1 1, controller bắt đầu được thực thi sau khi nhận phản hồi từ server, Hàm hiển thị danh sách TODO list bắt đầu được thực thi
- Kiểm tra thông tin user để lưu lại sau khi nhận phản hồi từ server.
- Tuy nhiên trường hợp không có phản hồi do chưa đăng nhập thành công, mình sẽ hiển thị màn hình login.
- Sau khi nhận phản hồi từ server, XHR chạy hàm callback như bước 2.
- Bằng hàm callback, mình sẽ nhìn vào phản hồi từ server và lưu trữ thông tin user. Tuy nhiên chỉ sau khi bạn đã hiển thị xong màn hình login.
Dưới đây sẽ là trường hợp TODO list không xuất hiện vì chưa đăng nhập.
Những file cần thay đổi
Thao tác | file | Ý nghĩa |
---|---|---|
Sửa | app/View/Layouts/default.ctp | Sửa để thêm template mới |
Sửa | app/webroot/js/main.js | thay đổi sời gian chạy application.start() |
Sửa | app/webroot/js/app.js | Kiểm tra đăng nhập |
Sửa | app/webroot/js/routers/router.js | Thực hiện routing |
Sửa | app/webroot/js/routers/controller.js | Quản lý những thứ liên quan đến login |
Thêm mới | app/webroot/js/models/user-model.js | Trỏ đến API |
Thêm mới | app/webroot/js/views/login-layout-view.js | View cho màn hình login |
Thêm mới | app/webroot/js/views/header-view.js | view cho header |
default.ctp
Thêm như sau
<title>TODO List</title>
</head>
<body>
+ <!-- header -->
+ <div id="header"></div>
<!-- Content -->
<div id="main"></div>
+
+ <!-- Thêm header lên trên cùng, hiển thị tên, username và nút log out-->
+ <script type="text/template" id="header-template">
+ <p>user:<%- username %>(<%- name %>) <input type="button" id="logout" value="Log Out"></input></p>
+ <hr>
+ </script>
<!-- TODO一覧表示のレイアウトテンプレート -->
〜nội dung dài〜
</script>
<!-- 詳細画面のレイアウトテンプレート -->
<script type="text/template" id="todo-detail-layout-template">
〜nội dung dài〜
</script>
<!-- 詳細画面の表示内容テンプレート -->
<script type="text/template" id="todo-detail-item-template">
〜nội dung dài〜
</script>
+ <!-- ログイン画面テンプレート -->
+ <script type="text/template" id="login-layout-template">
+ <h2>Login</h2>
+ <div>
+ <p>User name :<input type="text" id="username" placeholder="username" autofocus></input></p>
+ <p>Password :<input type="password" id="password" placeholder="password"></input></p>
+ <input type="button" id="login" value="Login"></input>
+ </div>
+ <hr>
+ <h2>User Registration</h2>
+ <div>
+ <p>Username <input type="text" id="signup-username" placeholder="username"></input></p>
+ <p>name :<input type="text" id="signup-name" placeholder="name"></input></p>
+ <p>Password :<input type="password" id="signup-password" placeholder="password"></input></p>
+ <input type="button" id="signup" value="Sign Up"></input>
+ </div>
+ </script>
+
<!-- require -->
<script type="text/javascript" src="js/require-config.js"></script>
<script type="text/javascript" src="js/lib/require.js" data-main="main.js"></script>
</body>
</html>
Mình đã thêm vào những thành phần:
- Thẻ Div cho region hiển thị thông tin trên header
- Template cho phần view của việc hiển thị thông tin trên header.
- Username, full name, một nút logout ở đây
- Template cho màn hình login
- Thêm vào các thành phần đăng nhập
- Tên user
- Password
- Nút login
- Các thành phần của việc đăng ký thành viên
- Tên user
- Tên đầy đủ
- mật khẩu
- Nút đăng ký
- Thêm vào các thành phần đăng nhập
main.js
require(['app'], function(Application){
console.log('run main2');
window.application = new Application();
- window.application.start();
- console.log('app start');
});
});
Xóa application.start()
.
Đây là điểm thú vị mà tôi đã nói trước đó.
Khi tiến trình kiểm tra login ở (app.js
) (Kiểm tra login hay chưa) được hoàn tất, mình sẽ chạy application.start (). Sự khác nhau giữa việc xóa
application.star()`, và không xóa sẽ được mô tả dựa trên code dưới đây.
Trường hợp bạn xóa application.start()
( Nên làm)
- Truy cập vào
/rest-study/
- Chạy main.js. applicaion object sẽ được tạo
- Chạy login check tại hàm
initialize
của app.js ( Chạyloggedin
API của giao tiếp Ajax ) - Nhận phản hồi từ
loggedin
API,- Lấy thông tin User nếu đã login
- Bỏ qua thông tin user nếu chưa login
- Chạy application.start()
-
onStart()
được thực thi, chạyBackbone.history.start()
, bắt đầu routing. - controller bắt đầu chạy
- Nếu giữ thông tin user, chuyển nó ra màn hình TODO list
- Nếu bỏ qua thông tin user, Chạy hàm
Backbone.history.navigate()
để chuyển trở lại màn hình login (/#login
)
-
- Lấy thông tin User nếu đã login
Nếu không xóa application.start()
- Truy cập vào
/rest-study/
- Chay main.js. applicaion.start() bắt đầu chạy
- Chạy login check tại hàm
initialize
của app.js ( Chạyloggedin
API của giao tiếp Ajax ) - Trước khi nhận phản hồi từ
loggedin
API,onStart()
của app.js sẽ được chạy,Backbone.history.start()
chạy luôn, bắt đầu routing. Chạy hàm kiểm tra login ( Chạyloggedin
API của giao tiếp Ajax )- controlle được thực thi.
- Bởi vì
loggedin
vẫn chưa nhận được phản hồi bằng giao tiếp AJAX từ API, việc khởi tạo giá trị cho thông tin user đã bị bỏ qua, Chạy hàmBackbone.history.navigate()
để chuyển trở lại màn hình login (/#login
) ( Bất kể đã đăng nhập hay chưa ).
- Bởi vì
- controlle được thực thi.
- Nhận phản hồi từ
loggedin
API, set giá trị như dưới đây- Lấy thông tin User nếu đã login
- Bỏ qua thông tin user nếu chưa login
- Tuy nhiên, vì mình đã hoàn tất tiến trình của controller, màn hình login sẽ hiển thị ...
- Lấy thông tin User nếu đã login
app.js
Thêm mỗi tiến trình của những thứ liên quan đến login
define(function(require){
console.log('run app');
var Router = require('routers/router');
+ var UserModel = require('models/user-model');
var Application = Marionette.Application.extend({
initialize : function(){
console.log('app.initialize');
+ // Set a common handler of error of Ajax
+ $(document).ajaxError(function ( e, xhr, options , message ) {
+ window.application.ajaxErrorHandler( e, xhr, options , message );
+ });
new Router();
+ //Lấy thông tin login từ server
+ this.loginUser = new UserModel();
+ this.getLoginUser();
},
onStart : function(){
Backbone.history.start();
},
regions : {
+ headerRegion : '#header',
mainRegion : '#main'
- }
+ },
+
+ //thông tin user cần lưu trữ
+ loginUser : null,
+
+ //bắt đầu lấy thông tin đăng nhập
+ getLoginUser : function(){
+ this.loginUser.getLoginUser(
+ this.onLoggedIn,
+ this.onNotLoggedIn
+ );
+ },
+
+ //Lấy thông tin user: trường hợp đã login
+ onLoggedIn : function(){
+ window.application.start(); // applicaiton.start() sau khi check login
+ },
+
+ //Lấy thông tin user: trường hợp chưa login
+ onNotLoggedIn : function(){
+ window.application.clearLoginUser(); // mình sẽ bỏ qua thông tin login của user
+ window.application.start(); // applicaiton.start() sau khi check login
+ },
+
+ //Các phần xử lý
+ isLoggedIn : function(){
+ return this.loginUser.isLoggedIn();
+ },
+
+ //Xóa thông tin login của user khi logout
+ clearLoginUser : function(){
+ this.loginUser.clear();
+ },
+
+ // Các hàm bắt lỗi của ajax
+ ajaxErrorHandler : function(e, xhr, options , message){
+ if( xhr.status === 401 ){
+ this.clearLoginUser();
+ // Nếu unauthenticated thì chuyển về màn hình login
+ Backbone.history.navigate('#login', {trigger : true, replace : true});
+ }else if(xhr.status >= 400 && xhr.status < 500){
+ //Hiển thị các lỗi ClientError
+ alert(message);
+ }else if(xhr.status >= 500 && xhr.status < 600){
+ //Hiển thị các lỗi ServerError
+ alert(message);
+ }
+ },
});
return Application;
Những Chỉnh sửa chính.
Thêm require
- Để thực hiện các API liên quan đến đăng nhập,
models/user-model.js
được require.
Hàm initialize
- Thiết đặt các hàm bắt lỗi phổ bến bằng ajax( Hàm
ajaxErrorHandler
) - Để tạo và require
user-model
, login check (loggedin
API) the execution. - API được chạy qua hàmgetLoginUser
- Nếu đã đăng nhập, HàmonLoggedIn
sẽ được thực thi.
- Nếu chưa đăng nhập, hàmonNotLoggedIn
được thực thi.
ajaxErrorHandler
Những hàm bắt lỗi sẽ được dùng để Handler các lỗi của tất cả các giao tiếp Ajax.
- Routing tới http status
401 (Unauthorized)
nghĩa là (not login), Bằng cách chạy hàmBackbone.history.navigate()
, bạn sẽ chuyển tới màn hình login (/#login
) - Hiển thị một alert thay vì http status.
router.js
〜nội dung dài〜
//Cấu hình routing
appRoutes : {
+ 'login' : 'login',
'' : 'todoLists',
'todo-lists' : 'todoLists',
'todo-lists/:id' : 'todoDetail'
},
〜nội dung dài〜
Thêm router cho màn hình login
controller.js
//controller
console.log('load controller');
define(function() {
console.log('run controller');
var TodoController = Marionette.Controller.extend({
+ login : function(){
+ //màn hình login
+ this.nextView('views/login-layout-view', null, true);
+ },
todoLists : function() {
//Routing tới view cho Todo layout
this.nextView('views/todo-layout-view');
},
todoDetail : function(id) {
this.nextView('views/todo-detail-layout-view', {modelId : id});
},
- nextView : function(viewPath, option) {
+ nextView : function(viewPath, option, tryShowLoginScreen) {
+ if(window.application.isLoggedIn()){
+ //Đã đăng nhập
+ if(tryShowLoginScreen){
+ //Nếu bạn đã đăng nhập nhưng vẫn cố chuyển đến trang login
+ //routed nó đến màn hình TODO list
+ Backbone.history.navigate('#todo-lists', {trigger: true, replace: true});
+ return;
+ }
+ }else{
+ //Chưa đăng nhập
+ if(!tryShowLoginScreen){
+ //Nếu bạn vẫn cố vào khu vực cần phải login
+ //route nó đến trang login
+ Backbone.history.navigate('#login', {trigger: true, replace: true});
+ return;
+ }
+ }
+ //Hiển thị header
+ this.showHeaderRegion(tryShowLoginScreen);
+ //Hiển thị nội dung
require([viewPath], function(View){
window.application.mainRegion.show(new View(option));
});
},
-
+
+ showHeaderRegion : function(tryShowLoginScreen){
+ if(tryShowLoginScreen){
+ //Ẩn header khi chuyển sang trang login
+ window.application.headerRegion.empty();
+ }else if(!window.application.headerRegion.hasView()){
+ //Nếu không chuyển thì hiện header
+ require(['views/header-view'], function(View){
+ window.application.headerRegion.show(new View({
+ model : window.application.loginUser
+ }));
+ });
+ }
+ }
+
});
return TodoController;
});
Thêm hàmlogin
- Chạy ở giá trị true sẽ thêm vào hàm nextView tham số
tryShowLoginScreen
(được mô tả dưới đây)
Hàm nextView
- Tại hàm
nextView
sẽ quyết định thêm những logic tại thời điểm login và login thực tế- Nó sẽ điều khiển sự hoạt động của 2 patterns sau ( Trạng thái cố chuyển sang trang login thì được định nghĩa ở tham số
tryShowLoginScreen
).- Route tới màn hình TODO list khi đã có sự cố gắng chuyển sang trang đăng nhập của user đã đăng nhập.
- Route tới màn hình login hi có sự cố gắng chuyển sang trang TODO list và TODO detail (mà không pahri trang login) của user chưa đăng nhập
- Route tới màn hình TODO list khi đã có sự cố gắng chuyển sang trang đăng nhập của user đã đăng nhập.
- Nó sẽ điều khiển sự hoạt động của 2 patterns sau ( Trạng thái cố chuyển sang trang login thì được định nghĩa ở tham số
- Trước khi hiển thị view của content (TODO list hoặc trang detail), mình sẽ hiển thị header.
Hàm showHeaderRegion
- Tại màn hình login mình sẽ ẩn view của header.
- Khi content hiển thị thì mình sẽ hiển thị view của header.
user-model.js
Cọi thêm các API
//login model
define(function() {
var LoginModel = Backbone.Model.extend({
parse : function(response) {
if(response.message){
this.loginMessage = response.message;
}
return response.User;
},
loginMessage : null,
//Xác định đã đăng nhập chưa
isLoggedIn : function(){
return this.get('id') ? true : false;
},
//Lấy thông tin user khi đã login
getLoginUser : function(onLoggedIn, onNotLoggedIn){
this.urlRoot = '/rest-study/users/loggedin';
this.fetch(
{
wait : true,
success : function(){
onLoggedIn();
},
error : function(){
onNotLoggedIn();
},
}
);
},
//Login
login : function(username, password, onLoginSuccess, onLoginError){
this.urlRoot = '/rest-study/users/login';
this.save(
{
username : username,
password : password
}, {
success : function(model){
if(model.get('id')){
onLoginSuccess(model.loginMessage);
}else{
onLoginError(model.loginMessage);
}
},
}
);
},
//Đăng xuất
logout : function(onLogoutSuccess){
this.urlRoot = '/rest-study/users/logout';
this.save(
{},
{
success : function(model){
onLogoutSuccess(model.loginMessage);
},
}
);
},
//Đăng ký ( sign up )
signup : function(username, password, name, onSignUpSuccess, onSignUpError){
this.urlRoot = '/rest-study/users/signup';
this.save(
{
username : username,
password : password,
name : name
}, {
success : function(model){
if(model.get('id')){
onSignUpSuccess(model.loginMessage);
}else{
onSignUpError(model.loginMessage);
}
},
}
);
},
});
return LoginModel;
});
hàm parse
Những phản hồi từ server mình cần xử lý như sau
- Lưu
message
vào biếnloginMessage
. -
User
return để lấy thông tin (user information).
Cách thực hiện của mỗi API
- Chạy một phương thức GET (
loggedin
API) thực hiện hàmfetch
. - Chạy một phương thức POST (
login
,logout
, mỗi API củasignup
chạy hàmsave
) . - URL của API được xác định bằng cách thay đổi giá trị biến
urlRoot
tại runtime của mỗi hàm. - Sau khi nhận phản hồi từ API, hoàn tất các xử lý của view mà có call back những hàm của view.
- Sẽ có thể có lỗi xảy ra, vì vậy hàm
ajaxErrorHandler
được set ởapp.js
sẽ được thực thi, hiện tại nó không làm gì cả.
login-layout-view.js
Thừa kế Marionette.LayoutView để thực hiện
//layout cho view của trang login
define(function(require) {
var LoginModel = require('models/user-model');
var LoginLayoutView = Marionette.LayoutView.extend({
//template
template : '#login-layout-template',
//UI page
ui : {
username : '#username',
password : '#password',
loginButton : '#login',
signupUsername : '#signup-username',
signupPassword : '#signup-password',
name : '#signup-name',
signupButton : '#signup'
},
//event
events : {
//Khi click button login
'click @ui.loginButton' : 'onLoginClick',
//Khi click button Sign up
'click @ui.signupButton' : 'onsignupClick',
},
//xử lý sự kiện click button login
onLoginClick : function(){
//Lấy giá trị trong textbox
var username = this.ui.username.val(); //username
var password = this.ui.password.val(); //password
window.application.loginUser.login(
username,
password,
this.onLoginSuccess,
this.onLoginError);
},
//Callback khi đăng nhập thành công
onLoginSuccess : function(message){
Backbone.history.navigate('todo-lists', {trigger: true, replace: true});
console.log(message);
},
//callback khi đăng nhập thất bại
onLoginError : function(message){
alert(message);
},
//Xử lý event click button sign up
onsignupClick : function(){
//Lấy giá trị từ checkbox
var username = this.ui.signupUsername.val(); //username
var password = this.ui.signupPassword.val(); //password
var name = this.ui.name.val(); //tên
var userModel = new LoginModel();
userModel.signup(
username,
password,
name,
this.onsignupSuccess,
this.onsignupError);
},
//Callback khi đăng ký thành công
onsignupSuccess : function(message){
alert(message);
},
//callback khi đăng ký thất bại
onsignupError : function(message){
alert(message);
},
});
return LoginLayoutView;
});
Đây là các cách thực hiện truyền thống
- Set các phần tử html của mỗi input và button trong
ui
- Tạo các hàm xử lý sự kiện click button ở
event
- Chạy API của mỗi hàm bắt sự kiện Click sử dụng
user-model
- Tại thời điểm login, ( để giữ thông tin user được return trong
app.js
) và sử dụnguser-model
còn lại trongapp.js
. - (Kể từ khi thông tin user được return ) trong quá trình đăng ký user,
user-model
mới được tạo để sử dụng
- Tại thời điểm login, ( để giữ thông tin user được return trong
- Chạy Callback tại thời điểm nhận được phản hồi từ API, chuyển màn hình và hiển thị các message
- Tại hàm callback khi đăng nhập thành công chúng ta sẽ chuyển vào màn hình danh sách TODO list.
header-view.js
thừa kế từ Marionette.ItemView để implement
//Header view
define(function(require){
var UserModel = require('models/user-model');
var HeaderView = Marionette.ItemView.extend({
//Tamplate
template : '#header-template',
ui : {
logoutButton : '#logout',
},
//set các event hander cho DOM
events : {
//khi click log out button
'click @ui.logoutButton' : 'onLogoutClick',
},
onLogoutClick : function(){
var userModel = new UserModel();
userModel.logout(this.onLogoutSuccess);
},
onLogoutSuccess : function(message){
window.application.clearLoginUser();
Backbone.history.navigate('#login', {trigger : true, replace : true});
console.log(message);
},
});
return HeaderView;
});
Nó cũng giống như việc implementation view truyền thống khác.
- Thêm button Log out vào biến
ui
- Tại hàm
event
mình thêm sự kiệnonLogoutClick
. - Tại hàm
onLogoutClick
mình chạy API sử dụnguser-model
- Khi phản hồi từ API cho hàm
onLogoutSuccess
được thực thi thì mình chuyển qua màn hình đăng nhập.
Tổng kết
hãy kiểm tra lại kết quả và fix các lỗi nếu có nhé các bạn.
-
app/View/Layouts/default.ctp
sửa như hướng dẫn trên. -
app/webroot/js/main.js
sửa như hướng dẫn trên. -
app/webroot/js/app.js
sửa như hướng dẫn trên. -
app/webroot/js/routers/router.js
sửa như hướng dẫn trên. -
app/webroot/js/routers/controller.js
sửa như hướng dẫn trên. -
app/webroot/js/models/user-model.js
tạo mới như hướng dẫn trên. -
app/webroot/js/views/login-layout-view.js
tạo mới như hướng dẫn trên. -
app/webroot/js/views/header-view.js
tạo mới như hướng dẫn trên. - Kiểm tra kết quả.
- Commit lên Git
Link tham khảo với hiển thị Diff trên Github
第5回 Lesson2 クライアントサイド · suzukishouten-study/rest-study@d4ae8a4
Đó là tất cả bài học này.
Đóng góp
Rất mong nhận được những phản hồi/góp ý của các bạn để bài viết tốt hơn, một số phần giải thích hơi lủng củng vì tiếng Nhật của mình không được tốt, rất hi vọng các bạn cùng giúp đỡ để có một series hoàn chỉnh nhất cho những người Việt muốn học backbone.
Cảm ơn các bạn rất nhiều vì đã theo dõi bài viết này.
Ngoài ra các bạn có thể thêm vài chức năng mới như kiểm tra mật khẩu xác nhận của đăng ký, thay đổi 1 tí giao diện, sử dụng thêm các event như keypress để thay vì click mình nhấn enter là chạy , mình đã làm tạm thời nó ra thế này và đang modify thêm vài ý tưởng dị dị nữa, hẹn gặp lại các bạn ở bài 6 :D