Ngày nay, việc xây dựng website dựa trên các framework như Vue hay React đã không còn là điều xa lạ. Trong quá trình làm việc với các dự án Vue, có một lỗi khá là khó chịu đối với user tuy nhiên lại ít dev để ý và sửa. Đó là mỗi lần bạn build và deploy phiên bản mới cho web, một số user đang mở sẵn phiên bản frontend cũ sẽ bị lỗi không thể click sang trang khác hoặc không thể sử dụng được một số chức năng trên web. Mặc dù chỉ cần F5 là trình duyệt sẽ tự tải lại phiên bản web mới và user lại sử dụng bình thường. Tuy nhiên, nếu bạn thường xuyên update frontend, lỗi này sẽ thực sự là pain in the ass.
Vậy làm thế nào đây để người dùng dù chưa cập nhật vẫn sử dụng được website bình thường? Lý do phát sinh lỗi là do mỗi lần build, các file js và css sẽ được đặt tên 1 cách ngẫu nhiên. Do đó, nếu bạn xoá phiên bản cũ đi và thay bằng phiên bản mới vào, một số trình duyệt chưa cập nhật (reload lại) sẽ yêu cầu những file js cũ -vốn đã không còn nữa- và gây ra lỗi.
I. Bắt lỗi phía client
Nếu bạn đang dùng một phiên bản web cũ, trong khi server đã cập nhật bản mới, thì khi nhấn vào 1 liên kết nội bộ trên trang web, bạn sẽ không thấy web được chuyển. Kiểm tra console có thể bạn sẽ nhận được báo lỗi như sau.
Trong trường hợp này, chúng ta có thể bắt lỗi ở router
của webpack, nếu gặp lỗi về router thì sẽ hiện thông báo hướng dẫn người dùng reload trang để cập nhật phiên bản mới.
router.onError(error => { if (/loading chunk \d* failed./i.test(error.message)) { // Hiện thông báo cần reload trang để cập nhật phiên bản // window.location.reload() }
})
Tuy nhiên, nếu người dùng không nhấn vào liên kết chuyển trang mà sử dụng một tính năng nào đó chưa được load. Trong trường hợp này, client (browser) sẽ cố gắng load 1 file js chứa tính năng đó, nhưng file này giờ đã ko còn nữa do server đã update phiên bản và đổi hết tên file js. Lúc này, bạn sẽ nhận được thông báo lỗi như sau unexpected token '<'
, đây là do trình duyệt đang cố load về file js nhưng kết quả nhận được là file index.html
(do file js 404 được rewrite về index.html). Để xử lý lỗi này, ta cần bắt lỗi ở global.
// https://stackoverflow.com/questions/59385650/vuejs-browser-caching-and-loading-chunk-failed-issue
window.onerror = function(mMsg, mSource, mLineNo, mColNo, error){ // mMsg = Char. Error Msg: i.e. "Uncaugh SyntaxError: Unexpected token '<'" // mSource = Char. i.e. 'https://yoursite.com/js/chunk-431587f6.ff603bf5.js' // mLineNo = Numeric. Line no // mColNo = Numeric. Col no // error = Object. Error object if (mMsg.toLowerCase().indexOf("unexpected token '<'") >-1){ // this happens when a new update gets applies but my router.js file hasn't been pulled down for whatever reason. A page refresh fixes it. // mSource = if (navigator.onLine){ // Hiện thông báo cần reload trang để cập nhật phiên bản // window.location.reload() } }
};
Bạn có thể thấy Gmail cũng đang hiện những thông báo như vậy khi có cập nhật:
Tuy nhiên, nếu ứng dụng của chúng ta là 1 ứng dụng phức tạp mà người dùng không thể reload lại trang khi đang giữa chừng công việc. VD bên mình có sản phẩm GoStudio giúp người dùng livestream từ nền web, không thể bắt người dùng reload lại trang khi họ đang livestream vì sẽ làm gián đoạn việc phát live.
Vậy cần tìm một giải pháp mà không cần reload lại trang vẫn nhưng web cũ có thể hoạt động bình thường. Có thể giải quyết bằng cách giữ lại cả phiên bản cũ để trình duyệt yêu cầu thì vẫn có để trả về.
II. Reverse proxy + cache
Thay vì web chính được phục vụ trực tiếp tới client thì chúng ta sẽ có 1 con proxy đứng ở giữa và cache lại tất cả các file đi qua. Kì vọng của chúng ta là khi Main app được update thì con Proxy vẫn còn cache những file cũ để phục vụ những client chưa update phiên bản mới.
Đây là cách đơn giản và tiết kiệm chi phí nhất. Tuy nhiên, việc dựa vào cache là không đáng tin cậy. Vì nhiều lý do khác nhau, file js mà trình duyệt cần có thể chưa được cache hoặc đã bị xoá dẫn đến cache miss và user vẫn lỗi như cũ. Nếu app của bạn đơn giản và ít file, bạn có thể dùng cách này. Với những ứng dụng phức tạp hơn (như GoStudio) chúng ta cần tìm cách có độ tin cậy cao hơn.
III. Reverse proxy + multiple instance
Cách làm này phù hợp nếu bạn đang dùng cloud và mỗi lần deploy, bạn sẽ deploy lên 1 instance khác nhau. Nếu như vậy, thay vì xoá instance cũ mỗi lần update, bạn có thể giữ lại trong 1 khoảng thời gian để các file cũ vẫn khả dụng. Nguyên lý là các request sẽ đi qua con Proxy, nó sẽ tìm file ở instance chứa phiên bản mới nhất trước, nếu ko có thì nó sẽ tìm ở các instance cũ hơn.
Mặc dù có thể đảm bảo gần như 100% sẽ hết lỗi những với cách này bạn sẽ cần cấu hình lại server proxy mỗi lần thêm/xoá các instance phía sau. Chi phí để giữ lại các instance cũ cũng cao hơn. Cấu hình nginx có thể tương tự như sau:
proxy_cache_path /tmp levels=1:2 keys_zone=STATIC:100m inactive=24h max_size=1g; server { listen 80; server_name gateway-domain; sendfile off; recursive_error_pages on; location / { proxy_pass http://main-app-v3; proxy_cache STATIC; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_404; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_ssl_server_name on; proxy_buffering on; proxy_busy_buffers_size 512k; proxy_buffers 4 512k; proxy_buffer_size 256k; proxy_intercept_errors on; error_page 404 = @server2; } location @server2 { proxy_pass http://main-app-v2; proxy_ssl_server_name on; proxy_intercept_errors on; error_page 404 = @server3; } location @server3 { proxy_pass http://main-app-v1; proxy_ssl_server_name on; }
}
Tuy nhiên, do yêu cầu mỗi phiên bản có 1 domain khác nhau nên nếu bạn không dùng cloud thì sẽ setup có thể sẽ phức tạp.
IV. Deploy multi-version
Cách cuối này sẽ ổn nhất, chỉ yêu cầu 1 server để deploy. Ý tưởng tương tự như trên, nhưng thay vì mỗi phiên bản chạy ở 1 instance khác nhau, giờ đây mỗi phiên bản được lưu ở các thư mục khác nhau trên cùng server. Web server sẽ được cấu hình để nếu không tin thấy file ở thư mục này sẽ tìm ở thư mục tiếp theo.
server { listen 80; server_name gateway-domain; sendfile off; recursive_error_pages on; root /deploy/; location / { try_files /v3/$uri /v2/$uri /v1/$uri /v3/index.html =404; }
}
Trong ví dụ trên, chúng ta có thư mục /deploy
để chứa các phiên bản. Mỗi phiên bản nằm trong 1 thư mục như v3, v2, v1. Mỗi lần build phiên bản mới, chúng ta sẽ copy vào thư mục deploy
và sửa lại file cấu hình của web server để nhận phiên bản này.
Toàn bộ các bước cấu hình trên, chúng ta có thể code 1 cái CMS để quản lý cho đỡ cực.
Hi vọng bài viết của này có thể giúp bạn tạo ra những ứng dụng web thân thiện với người dùng hơn.
/boygiandi