Độ vài tuần trước, mình vừa ra mắt Extension Học English với gái xinh trên Chrome Web Store. Nguyên nhân là sau vụ add-on ngắm bười, lộn, ngắm vếu (Em thèm vếu) hôm trước, mình chợt nhận ra rằng đa phần dân FA và dân dev rất máu gái….
Câu hỏi đặt ra là: Thay vì giải trí, liệu có thể dùng gái để dụ dỗ các thanh niên chăm chỉ học hành hay không?? Đáp áp là có! Add-on “Học English với gái xinh” ra đời từ đây.
Với add-on này, mỗi lần trả lời đúng từ vựng, bạn sẽ được ngắm hình gái (hàng xinh hàng tuyển nhé). Trả lời đúng nhiều câu liên tục, điểm càng cao thì độ nóng bỏng cũng sẽ tăng dần ahihi.
Các bạn có thể tải extension về dùng thử tại đây. Nhớ đánh giá ứng dụng 5 sao nhe.
Trong bài này, chúng ta cùng mổ xẻ kiến trúc và code của ứng dụng trên, các bạn có thể dựa theo đó để viết ứng dụng tương tự nhé.

Kiến trúc tổng quát
Hệ thống có thiết kế khá đơn giản. Phần back-end là 1 RESTful API, trả về một danh sách các link ảnh dưới dạng JSON. Phần front-end là một Chrome Extension. Do extension của Chrome được build bằng HTML/CSS/JS nên mình viết extension cũng như viết web bình thường thôi.
Front-end sẽ request ảnh từ back-end, sau đó lưu ảnh ở phía client và load sẵn, do đó các bạn sẽ thấy ứng dụng load ảnh khá nhanh như ảnh local vậy.

Back-end: AWS Lambda
Muốn làm ứng dụng ngắm ảnh gái thì đầu tiên là phải có… hình gái. Kiếm hình gái ở đâu bây giờ?? Ban đầu mình định lấy ảnh trên xiuren, nhưng do trang này hơi bị… hở hang quá, lại không có gái hình Việt Nam. Thế là mình bắt đầu lên các fanpage ngắm gái trên Facebook để tìm hình.
Tiếc thay, hình ảnh ở các trang này cũng không nhiều! Không sao, chỉ cần lấy hình từ nhiều trang, sau đó chọn random một trang trong đó là xong. Vì Facebook đã có sẵn RestAPI nên mình lấy ảnh cũng khá dễ dàng, code back-end rất ngắn gọn như sau.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let request = require('request'); | |
exports.handler = (event, context, callback) => { | |
let token = 'YOUR_TOKEN_HERE'; | |
// Lấy ID của 1 fanpage bình thường và sexy | |
let sexyPageId = getPageId(true); | |
let normalPageId = getPageId(false); | |
// Load ngẫu nhiên 15 ảnh bình 5 ảnh sexy | |
let promise = Promise.all([ | |
getRandomImage(token, normalPageId, 15, 450), | |
getRandomImage(token, sexyPageId, 5, 500) | |
]); | |
// Trả kết quả về dưới dạng JSON | |
promise.then(result => { | |
let response = { | |
normal: result[0], | |
sexy: result[1] | |
} | |
callback(null, { | |
statusCode: 200, | |
body: JSON.stringify(response), | |
headers: { | |
'Content-Type': 'application/json', | |
'Access-Control-Allow-Origin': '*' | |
} | |
}); | |
}); | |
} | |
function getPageId(isSexy) { | |
var normal = [189695764410814, 429347487112306, 1269826469732326, 323925094473196]; | |
var sexy = [1786207988368795, 637445302949772, 1224869587565040, 169974406437267]; | |
if (isSexy) { | |
return getRandomElement(sexy); | |
} else { | |
return getRandomElement(normal); | |
} | |
} | |
function getRandomElement(array) { | |
let randomIndex = Math.floor((Math.random() * array.length)); | |
return array[randomIndex]; | |
} | |
// Dùng API của Facebook để lấy ngẫu nhiên một số ảnh trên các page | |
function getRandomImage(token, pageId, limit, maxIndex) { | |
var randomIndex = Math.floor((Math.random() * (maxIndex – limit))); | |
return new Promise((resolve, reject) => { | |
request({ | |
url: `https://graph.facebook.com/v2.9/${pageId}/photos/`, | |
qs: { | |
fields: "images", | |
limit: limit, | |
offset: randomIndex, | |
access_token: token | |
}, | |
method: "GET" | |
}, (err, response, body) => { | |
if (err) { | |
reject(err); | |
return; | |
} | |
var rs = JSON.parse(body); | |
var imageUrls = rs.data.map(data => data.images[0].source); | |
//var imageUrl = rs.data[0].images[0].source; | |
resolve(imageUrls); | |
}); | |
}); | |
}; |
Back end có thể viết bằng file PHP đơn giản, sau đó deploy lên host hoặc VPS. Do mình thích Nodejs nên dùng luôn Lambda của Amazon. Với kiến trúc serverless, dù cho có hàng triệu người dùng cũng không lo sập server.
Ngoài ra, do mình đã có sẵn RestAPI nên trong tương lai mình có thể tái sử dụng back-end này để làm app Window hoặc app di động. Tiện quá phải không nào?
Front-end: HTML, AngularJS, Semantic UI
Về cơ bản, Chrome Extension chỉ là HTML/CSS/JS nên các bạn đã quen làm web chỉ cần mất chút thởi gian là có thể làm quen được. Mình sử dụng Angular 1 và Semantic UI để code và làm giao diện nhằm tiết kiệm thời gian.
Điều mình không ngờ là viết front-end lại có khá nhiều vấn đề cần giải quyết hơn back-end. Các vấn đề này đều khá thú vị:
- Tìm danh sách từ vựng tiếng Anh ở đâu? Mình tìm trên mạng nhưng chỉ thấy file PDF. Sau một hồi mò cũng có file Excel, chỉ cần export nó ra csv rồi viết cái script nhỏ để parse nó thành JSON là xong.
- Nên lấy câu hỏi và chấm điểm ở front-end hay back end? Theo lý thuyết, ta nên để để và chấm điểm ở back-end để nguời dùng không hack và chôm được.
- Tuy nhiên, do người dùng trả lời nhiều, nếu xử lý ở back-end, số lượng request lên server sẽ khá nhiều, làm người dùng đợi lâu. Vì vậy mình để việc từ vựng và chấm điểm ở front-end.
- Lúc này việc làm bài và check điểm sẽ diễn ra ngay lập tức ở front-end hơn (xem clip demo bên dưới). Có điều người dùng có thể chôn file từ vựng, hack sửa điểm… Cái này không quan trọng mấy, chấp luôn :))
- Hình ảnh rất nhiều và nặng, không thể để ở front-end sẽ làm tăng dung lượng app. Thế nên mình gọi API để lấy link hình. Dự định ban đầu của mình là: sau khi người dùng trả lời xong một câu hỏi, ta call api để lấy hình.
- => Cách này khá chậm, gây khó chịu cho người dùng vì họ phải chờ có kết quả từ API, sau đó mới load hình.
- Thay vào đó, mình tranh thủ load API và ảnh lúc người dùng đang suy nghĩ. Mỗi lần gọi API, hệ thống sẽ load link ảnh từ api và lưu vào local storage. Front-end cũng load sẵn các ảnh.
- => Do đó bạn đọc thấy hình hiện ngay khi vừa trả lời đúng, không phải chờ api và ảnh load (Xem trong clip trên).
- Khi còn khoảng 4-5 hình trong hệ thống, front-end sẽ tiếp tục load ngầm hình từ API. Các bạn có thể chơi tới vài trăm điểm và vẫn thấy ảnh load cực nhanh như ở local vậy. Cách code này dĩ nhiên là khó hơn, dài hơn, nhưng cải thiện UI/UX và giảm tải server một cách đáng kể.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
loadMoreImageIntoStore: async function() { | |
let newImages = await this.fetchImageFromAPI(); | |
newImages.normal.forEach(imageUrl => { | |
if (this.imageStore.normal.indexOf(imageUrl) === –1) { | |
this.imageStore.normal.push(imageUrl); | |
preloadImage(imageUrl); | |
} | |
}); | |
newImages.sexy.forEach(imageUrl => { | |
if (this.imageStore.sexy.indexOf(imageUrl) === –1) { | |
this.imageStore.sexy.push(imageUrl); | |
preloadImage(imageUrl); | |
} | |
}); | |
StorageService.setImageStore(this.imageStore); | |
}, | |
getRewardImage: function(score) { | |
var imageUrl = ''; | |
// Lấy ảnh từ local storage, trả ra cho người dùng | |
if (score % 4 === 0) { | |
imageUrl = this.imageStore.sexy.shift(); | |
} else { | |
imageUrl = this.imageStore.normal.shift(); | |
} | |
StorageService.setImageStore(this.imageStore); | |
// Tải thêm hình từ API | |
// Chạy ngầm để không ảnh hưởng tới UI/UX | |
if (this.imageStore.normal.length < 6) { | |
this.loadMoreImageIntoStore(); | |
} | |
return imageUrl; | |
} |
Bài học rút ra
Các bạn thấy đấy, tuy chỉ là một ứng dụng nho nhỏ, nhưng chúng ta phải đưa ra nhiều lựa chọn khi design decision, không hề đơn giản. Là một engineer, không phải ta chỉ code sao cho chạy được mà code còn phải tối ưu, đưa ra UI/UX tốt nhất cho người dùng.
Tất nhiên, sau khi viết xong thì mình cũng release ứng dụng để lấy feedback từ người dùng, đồng thời cập nhật cải tiến thêm một số chức năng mới nữa.

Toàn bộ source code của ứng dụng nằm tại đây: https://github.com/conanak99/learn-english-extension. Mình đã cố gắng viết code clean nhất có thể, module tách bạch rõ ràng, tên biến tên hàm dễ hiểu. Các bạm có thể đọc và tham khảo nhé!
Kết
Trước đây, mình đã từng khuyên các bạn nên viết pet project vào lúc rãnh rỗi. Nếu có ý tưởng hay, thú vị, các bạn cứ thoải mái bắt tay vào làm thôi, đừng ngại ngần gì hết!
Như các bạn thấy đấy, việc viết một ứng dụng thế này không quá phức tạp, chỉ mất của mình chút thời gian (khoảng vài tiếng) thôi. Vừa vui, vừa học được nhiều thứ, lại còn giúp được người khác nữa. Nếu bỏ source code lên github, bạn còn có cái để khoe lúc đi xin việc nữa đấy ;).

Em có làm nhưng ứ thành công
LikeLike
có cần làm các khâu nào khó khăn hơn không
LikeLike
rất hay và ý nghĩa. Dùng cũng rất fun nữa. Thanks Ad.
LikeLiked by 1 person
Hôm trước e có thử viết cái extension load danh ngôn bằng tiếng anh qua api đc cung cấp sẵn, cay dái là nó ko trả về kết quả :v
LikeLike
cool
LikeLike
Có sound đọc các từ thì ngon a ơi :))
LikeLike
ảnh đẹp lắm anh. Có thể cho em xin nguồn được ko keke
LikeLike
Tổng hộ nhiều nguồn em ơi. Em tìm fanpage xinh nhẹ nhàng và xinh không chịu nổi nhé ;).
LikeLike
Access token lấy thế nào vậy anh.
LikeLike
Em vào Facebook Developer tạo cái app, sau đó Google Get Facebook App Access token nhé 😉
LikeLike
Đã lấy được hình, cảm ơn anh.
LikeLiked by 1 person
Phát hiện của bạn thật awesome :)))
LikeLike
Anh ah, khi mình viết xong hết code rồi thì thường a sẽ deploy ở đâu?
LikeLike
Bỏ lên cloud hoặc tìm host free thôi em 😉
LikeLike
Sao lấy ảnh từ page của người khác được nhỉ, có thể hướng dẫn chút dc ko ạ
LikeLike
sao em cài rồi mà nó cứ load hoài v anh :))
LikeLike