Làm sao để Single Page App “chơi đẹp” với crawler?

SPA and Crawler

Thời gian gần đây xu hướng thiết kế web theo dạng Single Page App (SPA) ngày càng phổ biến vì ngầu đem lại một số yếu tố cải thiện trải nghiệm người dùng mà web truyền thống không làm được. Tuy nhiên, việc viết SPA cũng đồng nghĩa với việc bắt buộc phía client phải chạy JavaScript mới hiển thị được nội dung hoàn chỉnh, và các crawler (điển hình nhất là của các search engine như Google, DuckDuckGo…) không thích điều này.

Trong bài viết này tôi sẽ giải thích cụ thể một SPA hoạt động như thế nào, và cách làm cho crawler cũng đọc được nội dung của nó. Code ví dụ sẽ được viết bằng React ở frontend và Google App Engine (python) ở backend.

SPA 101

Ý tưởng

Với một trang web truyền thống, khi người dùng click vào một link, trình duyệt sẽ tải lại toàn bộ nội dung trang web hoàn chỉnh từ server:

Traditional Full-Page Postback Operation
Mô hình này tạo ra một độ trễ nhất định khi trang web phải tải lại cả nội dung trang web.

Single Page Application Elements within a Page

Đối với một SPA, tất cả static assets (JavaScript, CSS, HTML) được tải về một lần, và khi người dùng click vào một link nhất định, trình duyệt sẽ không tải lại trang web. Thay vào đó, nó sẽ gửi một HTTP request đến server để lấy vừa đủ dữ liệu mới để có thể render lại trên browser. Những dữ liệu này thường được tải dưới dạng JSON để JavaScript có thể dễ dàng dịch thành JS object.

Ví dụ

Tải Google App Engine SDK tại đây: https://cloud.google.com/appengine/downloads?hl=en (chọn Python SDK) và giải nén ra $HOME của bạn. Sau đó hãy clone project mẫu về từ github:

Tạo một project mới ở https://console.developers.google.com/start. Trong bài viết này tôi sẽ dùng tên sss-spa-demo.

google app engine - new project

Sau đó sửa giá trị application trong file app.yaml thành tên project bạn vừa tạo:

Thử deploy xem sao nhé:

Nếu làm đúng, ta sẽ thấy app “Hello World” của mình online tại http://sss-spa-demo.appspot.com.

Để nhanh hơn, ta có thể chạy local server thay vì deploy lên appspot:

Server sẽ chạy ở http://localhost:8080.

Bây giờ ta sẽ tạo một SPA đơn giản bằng React. Do lượng code khá nhiều nên tôi đã viết sẵn, bạn có thể checkout branch spa-with-hash để dùng ngay:

Và ta đã có một SPA với hai route là Home và About:

SSS SPA Demo

SSS SPA Demo About

Sau đây là một số điểm đáng lưu ý:

  • Trong ./app.yaml: Ta đã thêm một handler để serve những file trong ./frontend/static ở địa chỉ http://localhost:8080/static. Đây là nơi ta chứa static assets như JS, CSS, hình ảnh…
  • Trong ./main.py: Thay vì trả lời “Hello World”, MainHandler sẽ serve file ./frontend/app.html. File này đơn giản là một HTML document có include các file JS/CSS cần thiết (React, Director, jQuery…), và có độc nhất một div tag rỗng với id là app. Code trong ./frontend/static/main.js sẽ bắt lấy div tag này và render nội dung app trong đó.
  • Trong ./frontend/static/main.js: MyApp là React component ngoài cùng và có một state là route. Khi người dùng click vào link tới một route, Director sẽ bắt lấy event này và gọi lệnh setState() của MyApp để thay đổi giá trị state.route của nó. Khi đó, lệnh render() của MyApp sẽ được gọi lại để render đúng route mới.
  • Trong URL của mỗi route đều có đoạn /#/ trước tên route. Đây là mẹo để trình duyệt không tải lại trang khi người dùng click vào link tới route mới.

Để đơn giản hóa demo này, nội dung mỗi route đều được hardcode trong React component của nó. Trong thực tế mỗi route sẽ phải gửi AJAX request tới một API endpoint nhất định để lấy nội dung về.

Vấn đề với crawler

Mặc dù SPA của chúng ta chạy tốt trên trình duyệt, crawler của các bộ máy tìm kiếm hiện tại không thể chạy javascript, vì vậy chúng sẽ chỉ thấy một trang web rỗng. Ta có thể kiểm chứng bằng cách tắt javascript trên trình duyệt và thử truy cập http://sss-spa-hash.appspot.com/:

SPA with disable javascript

Vấn đề này không chỉ giới hạn ở công cụ tìm kiếm, mà là bất kì dịch vụ nào dùng crawler để lấy preview của trang web, ví dụ như khi chia sẻ link trên Facebook cũng sẽ cho ra preview rỗng tuếch:

Facebook crawler SPA

Giải pháp

Ta sẽ thiết lập server để kiểm tra xem client có phải là crawler hay không bằng User-Agent của mỗi request. Nếu là crawler, server sẽ tự lấy và render nội dung của route đó thành HTML hoàn chỉnh để gửi về client. Mặt khác, nếu client là browser, server sẽ tiếp tục serve file ./frontend/app.html như cũ.

Checkout bản hoàn chỉnh đã cập nhật:

Bản này cũng đã được deploy sẵn ở http://sss-spa-final.appspot.com/. Ta đã giải quyết được những vấn đề trên:

Google:

Khi đổi User-Agent của trình duyệt thành của Google – “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)“ – và tắt javascript:

 Google and SPA

Facebook:

Facebook and SPA

Sau đây ta sẽ cùng xem đã có những cập nhật gì so với bản trước.

Bản cũ có một vấn đề là phần URL phía sau /#/ không được được gửi đến server, nên khi ta gửi 2 request:

Server đều xem chúng là request đến “/”:

requests

Vì vậy ta phải loại bỏ /#/ khỏi URL. Các routes của SPA lúc này sẽ trở thành:

Ở bản mới ta đã thêm đoạn code sau vào main.js để chặn không cho browser tải lại cả trang web khi người dùng click những link như trên:

Lưu ý là đoạn code trên cũng có kiểm tra và bỏ qua những trường hợp Ctrl/Shift + Click để người dùng có thể mở link trong tab mới hoặc cửa sổ mới. Với những link dẫn ra website bên ngoài, ta sẽ thêm class “external” để bỏ qua.

Ta cũng đã thêm config để Director dùng HTML5 History API:

Về phần backend, thay đổi quan trọng nhất là việc sử dụng @crawlable decorator cho các request handler:

Như đã nói ở trên, decorator này kiểm tra User-Agent của HTTP request và serve ra kết quả tương ứng:

  • Nếu User-Agent nằm trong danh sách crawlers: chạy function được pass vào decorator (func)
  • Nếu không, serve nội dung file ./frontend/app.html

Decorator này được dùng cho method get() của 2 handler: HomeHandler và AboutHandler. Mỗi handler này sẽ dùng jinja templating engine để render ra HTML hoàn chỉnh. Trong trường hợp này, cả hai trang đều có cấu trúc giống nhau nên ta dùng chung một template ở ./templates/common.html.

Để biết chi tiết tất cả những thay đổi từ bad-spa branch đến crawlable-spa branch, bạn có thể xem ở git history :P

Cải thiện

Crawable SPA của chúng ta đến đây đã đạt được mục đích ban đầu: chịu chơi với crawler. Tất nhiên với một dự án thật sự, ta sẽ cần một số thay đổi:

  • Home/About routes phải lấy nội dung từ một API endpoint của server chứ không hardcode trong file JS.
  • Dùng thư viện user-agents để nhận biết crawler thay vì tự viết, vì tự mình tìm hiểu tất cả user agent của tất cả các crawler là rất quởn vất vả.
  • Dùng cấu trúc Flux để tách hoàn việc quản lý state ra khỏi React components.
  • Compile JSX code ở ngoài trước khi deploy thay vì dùng JSXTransform trên trình duyệt. Minify, concat JS các kiểu… (duh!)

Nếu bạn không muốn phải viết trùng code ở client và server thì nên tham khảo Isomorphic JavaScript – dùng JavaScript trên cả client và server để có thể reuse code tối đa.

Credits:

Ảnh minh họa được lấy từ http://blog.4psa.com/an-intro-into-single-page-applications-spa/

SSS Full-stack Engineer

Love Silicon Straits and want to know more about our company culture, working environment or job vacancies?
Find out more at careers.siliconstraits.vn.

Silicon Straits
Be Challenged. Be Inspired. Be Different.




Follow us for more later

or subscribe with