SPA với AngularJS và Yeoman: part 2 - Zite Web Client
Bài đăng này đã không được cập nhật trong 9 năm
Trong phần trước, mình đã trình bày cách sử dụng Yeoman để tạo và chạy thử một project AngularJS đơn giản. Trong bài này, mình sẽ ứng dụng nó để làm một web client cho Zite.
I. Zite là gì?
Zite là ứng dụng giúp người dùng biết được những sự kiện, bài viết về chủ đề mình quan tâm.
Trên Internet hiện nay có vô vàn nội dung, hàng ngày có một lượng lớn thông tin lại được đưa lên. Do đó, càng ngày càng khó có thể tìm được những nội dung mà mình mong muốn. Từ đó, Zite đã hình thành.
Zite thu thập hàng triệu bản tin, bài viết hàng ngày, sắp xếp chúng vào các chủ đề theo nội dung, và đánh giá lượng chia sẻ của chúng. Từ các thông tin này, Zite chọn ra các bài viết phù hợp với sở thích cá nhân của từng người.
Zite có app cho iOS, Android cũng như Windows Phone nhưng rất tiếc là chưa thể đọc các bài tin tức do Zite gợi ý trực tiếp trên trình duyệt.
II. Zite API
Zite không công bố API cho developer sử dụng, tuy nhiên sau khi tìm kiếm thử trên Google, mình tìm thấy repo Zite-API. Tác giả repo có vẻ đã "reveserve engineering" app Zite trên iOS và viết lại bằng ngôn ngữ PHP.
Trong ứng dụng đơn giản này, mình chỉ quan tâm tới một số API sau:
1. Login
- URL = https://api.zite.com/api/v2/account/login/
- Method: POST
- params = {
appver: '2.0',
deviceType: 'ipad',
email: '{{userEmail}}',
password: '{{userPassword}}'
}
- successResult = {
userId = '{{user ID}}',
accessToken = '{{access Token}}'
}
2. Lấy các bài viết theo chủ đề
- URL = https://api.zite.com/api.zite.com.com/v2/news/
- Method = GET
- params = {
appver: '2.0',
deviceType: 'ipad',
userId = '{{user ID}}',
accessToken = '{{access Token}}',
section = '{{section name}}'
}
- result: tập các bài viết cùng thông tin kèm theo
3. Đánh dấu một bài viết là đã đọc
- URL = https://api.zite.com/api/v2/log/event/
- Method: POST
- params = {
appver: '2.0',
deviceType: 'ipad',
userId = '{{user ID}}',
accessToken = '{{access Token}}',
section = '{{section name}}',
url = '{{article url}}',
event: 'ArticleView',
orientation: 'portrait',
source: 'section',
webmode: false
}
III. Bắt đầu viết web client cho Zite
Đầu tiên là khởi tạo ứng dụng:
yo angular zite --coffee
Tùy chọn ---coffee
đằng sau là để Yeoman sinh cho ta project AngularJS
viết bằng CoffeeScript.
1. Cho phép ứng dụng CORS tới Zite server
Mặc định, khi gửi request loại POST
tới domain khác, trình duyệt sẽ gửi thêm một
request với method là OPTION
. Khi nhận được request này, Zite server
sẽ báo lỗi, khiến request POST
của ta không được gửi đi.
Để khắc phục vấn đề này, ta config $httpProvider
xóa hết header khi
gửi request, như vậy xóa luôn cả origin header.
Sửa trong file app/script/app.coffee
...
angular
.module 'ziteApp', [
'ngAnimate',
...
'ngTouch'
]
.config ($httpProvider) ->
$httpProvider.defaults.headers.common = {}
$httpProvider.defaults.headers.post = {}
$httpProvider.defaults.headers.put = {}
$httpProvider.defaults.headers.patch = {}
.config ($routeProvider) ->
...
2. Viết service ZiteServer để gọi API
Ở đây mình sinh ZiteServer
là một factory
:
yo angular:factory ZiteServer
Yeoman sẽ sinh ra file app/script/service/ziteserver.coffee
cho chúng
ta.
Để gọi API tới Zite server, mình sẽ dùng $http
của AngularJS.
Để có thể gửi request POST
với dữ liệu được encode dạng form data thì
cần phải tự biến đổi dữ liệu gửi đi sử dụng transformRequest
formPost = (url, data) ->
$http {
method: 'POST'
url: url
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
transformRequest: (obj) ->
str = []
for k, v of obj
str.push(encodeURIComponent(k) + "=" + encodeURIComponent(v))
str.join("&")
data: data
}
Sử dụng method trên, 3 API kể bên trên có thể được viết thành service như sau:
'use strict'
angular.module 'ziteApp'
.factory 'ZiteServer', ($http) ->
baseUrl = 'https://api.zite.com/api/v2'
loginPath = '/account/login/'
articlesPath = '/news/'
logEventPath = '/log/event/'
defaultParams =
appver: '2.0'
deviceType: 'ipad'
formPost = (url, data) ->
$http {
method: 'POST'
url: url
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
transformRequest: (obj) ->
str = []
for k, v of obj
str.push(encodeURIComponent(k) + "=" + encodeURIComponent(v))
str.join("&")
data: data
}
credential: {}
login: (email, password) ->
params =
email: email
password: password
angular.merge(params, defaultParams)
formPost(baseUrl + loginPath, params)
getArticles: (section = "topstories") ->
$http.get(baseUrl + articlesPath, { params: angular.merge({ section: section }, defaultParams, @credential) })
markAsRead: (section, url) ->
params =
section: section
url: url
event: 'ArticleView'
orientation: 'portrait'
source: 'section'
webmode: false
angular.merge(params, defaultParams, @credential)
formPost(baseUrl + logEventPath, params)
3. Một số thành phần của ứng dụng
Trong phần này, mình sẽ trình bày một số thành phần của Zite Web client, tất cả source code có thể được xem ở link Github cuối bài.
a. Auth
Dùng Yeoman để tạo Auth service:
yo angular:service Auth
'use strict'
angular.module 'ziteApp'
.service 'Auth', ($location, $window, ZiteServer) ->
@credential = angular.fromJson($window.sessionStorage.getItem('credential')) || {}
auth = @
@is_loggedin = ->
logged = @credential.hasOwnProperty("userId")
$location.path('/login') if !logged
logged
@log_in = (email, password, target) ->
ZiteServer.login(email, password)
.success (data) ->
auth.credential = data
$window.sessionStorage.setItem('credential', angular.toJson(auth.credential))
$location.path(target)
.error (data, status) ->
auth.credential = data
return
Method log_in
dùng email
và password
được cung cấp gọi tới Zite
API để lấy thông tin credential. Thông tin credential sẽ được lưu tại
sesstionStorage thay vì cookie. Sau khi login, method sẽ redirect sang
target
.
Method is_loggedin
sẽ redirect ngưởi dùng về trang login nếu chưa
login.
b. News
Dùng Yeoman để tạo News service:
yo angular:service News
'use strict'
angular.module 'ziteApp'
.service 'News', (ZiteServer, Auth, $q)->
@articles = {}
@load = (section = "topstories") ->
deferred = $q.defer()
ZiteServer.credential = Auth.credential
that = @
ZiteServer.getArticles(section)
.success (data) ->
that.articles[section] = data.documents
deferred.resolve()
.error ->
deferred.reject()
deferred.promise
@fetch = (section = "topstories") ->
deferred = $q.defer()
if @articles.hasOwnProperty(section) && @articles[section] != null
deferred.resolve(@articles[section])
else
that = @
@load(section)
.then -> deferred.resolve(that.articles[section])
.catch -> deferred.reject()
deferred.promise
return
Service này dùng để fetch
dữ liệu thông tin các bài viết theo chủ đề
từ Zite và sau đó cache lại để cung cấp qua method fetch
Cả hai method đều trả về một promise
c. LogEvent
Dùng Yeoman để tạo LogEvent service:
yo angular:service LogEvent
'use strict'
angular.module 'ziteApp'
.service 'LogEvent', (ZiteServer, Auth) ->
@markAsRead = (section, url) ->
ZiteServer.credential = Auth.credential
ZiteServer.markAsRead(section, url)
return
Service này cung cấp method để markAsRead
một bài viết.
d. Article directive
Để sinh một directive dùng Yeoman:
yo angular:directive article
Yeoman sẽ sinh cho ta file app/script/directives/article.coffee
'use strict'
angular.module 'ziteApp'
.directive 'article', (LogEvent) ->
restrict: 'EA'
templateUrl: 'views/templates/article.html'
scope:
document: '='
section: '='
controller: ($scope, $window, $location) ->
for pic in $scope.document.images
if pic.h == pic.w && pic.h >= 120
$scope.document.cover_url = pic.url
break
if pic.w > 800 && pic.h < pic.w / 2
$scope.document.cover_url = pic.url
$scope.document.is_full_pic = true
break
$scope.visitLink = ->
$scope.document.isread = 1
LogEvent.markAsRead($scope.section, $scope.document.url)
$window.open($scope.document.url, '_blank')
return true
$scope.clickTag = (topic) ->
$location.path('/section/' + topic.id)
return true
Zite trả về kèm theo mỗi bài viết là một mảng các hình ảnh có trong bài viết đó. Vì vậy, trong controller mình chọn ra các hình ảnh phù hợp để hiển thị.
Controller còn cung cấp hai method là visitLink
và clickTag
để ứng
với sự kiện người dùng click vào đọc tin hay click vào tag của các chủ
đề liên quan.
Kết
Trong bài này, mình đã trình bày cách mình viết web client cho Zite, một số khó khăn gặp phải và cách giải quyết. Tuy client chỉ mới dừng lại ở mức đơn giản, thậm chí chưa cho phép đăng kí mới, phải dăng kí trên ứng dụng mobile, nhưng hy vọng bài viết này hữu ích cho mọi người, nhất là những ai sử dụng Zite.
Do mới học AngularJS nên bài viết cũng như chương trình còn nhiều thiếu sót, có thể chưa theo best practice. Rất mong nhận được những ý kiến góp ý từ tất cả các bạn.
Links
All rights reserved