1. Giải pháp kiểm tra phiên bản (khuyến nghị)
Nguyên lý
So sánh số phiên bản lưu trữ cục bộ và phiên bản mới nhất từ server để xác định cần cập nhật hay không.
javascript
// quan-ly-phien-ban.js - Công cụ quản lý phiên bản
class QuanLyPhienBan {
constructor() {
this.phienBanHienTai = process.env.REACT_APP_VERSION || '1.0.0';
this.khoangCachKiemTra = 5 * 60 * 1000; // Kiểm tra mỗi 5 phút
this.tapTinPhienBan = '/thong_tin_phiên_bản.json';
}
// Khởi tạo kiểm tra phiên bản
khoiDong() {
this.luuPhienBanHienTai();
this.batDauKiemTra();
this.thietLapSuKienKhiDoi();
}
// Lưu phiên bản hiện tại
luuPhienBanHienTai() {
localStorage.setItem('app_version', this.phienBanHienTai);
}
// Bắt đầu kiểm tra
batDauKiemTra() {
// Kiểm tra ngay lập tức
this.kiemTra();
// Lặp lại kiểm tra
setInterval(() => {
this.kiemTra();
}, this.khoangCachKiemTra);
}
// Kiểm tra phiên bản
async kiemTra() {
try {
const ketQua = await fetch(`${this.tapTinPhienBan}?t=${Date.now()}`);
const duLieu = await ketQua.json();
const phienBanLuu = localStorage.getItem('app_version');
if (duLieu.phienBan !== phienBanLuu) {
this.hienThiThongBaoCapNhat(duLieu.phienBan);
}
} catch (loi) {
console.warn('Lỗi kiểm tra phiên bản:', loi);
}
}
// Hiển thị thông báo cập nhật
hienThiThongBaoCapNhat(phienBanMoi) {
// Xóa thông báo cũ
this.xoaThongBaoCu();
const thongBao = document.createElement('div');
thongBao.id = 'cap-nhat-thong-bao';
thongBao.innerHTML = `
<div style="
position: fixed;
top: 20px;
right: 20px;
background: #1890ff;
color: white;
padding: 16px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 9999;
max-width: 300px;
">
<div style="margin-bottom: 8px;">
<strong>Có phiên bản mới</strong>
</div>
<div style="margin-bottom: 12px; font-size: 14px;">
Phiên bản mới đã phát hành, vui lòng làm mới trang để cập nhật
</div>
<button id="lam-moi" style="
background: white;
color: #1890ff;
border: none;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
margin-right: 8px;
">Làm mới ngay</button>
<button id="sau-nay" style="
background: transparent;
color: white;
border: 1px solid white;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
">Sau này</button>
</div>
`;
document.body.appendChild(thongBao);
// Sự kiện nút
document.getElementById('lam-moi').onclick = () => {
this.lamMoiTrang();
};
document.getElementById('sau-nay').onclick = () => {
this.xoaThongBaoCu();
};
}
xoaThongBaoCu() {
const tonTai = document.getElementById('cap-nhat-thong-bao');
if (tonTai) {
tonTai.remove();
}
}
lamMoiTrang() {
// Xóa cache và làm mới
localStorage.removeItem('app_version');
window.location.reload(true);
}
thietLapSuKienKhiDoi() {
// Lưu trạng thái khi đóng trang (nếu cần)
window.addEventListener('beforeunload', () => {
// Có thể lưu trạng thái
});
}
}
// Ví dụ sử dụng
const quanLyPhienBan = new QuanLyPhienBan();
quanLyPhienBan.khoiDong();
Tạo tập tin phiên bản khi build
javascript
// tao-tap-tin-phien-ban.js - Script build
const fs = require('fs');
const packageJson = require('./package.json');
const thongTinPhienBan = {
phienBan: packageJson.version,
thoiGianXayDung: new Date().toISOString(),
maNgauNhien: Math.random().toString(36).substr(2, 9)
};
fs.writeFileSync(
'./public/thong_tin_phiên_bản.json',
JSON.stringify(thongTinPhienBan, null, 2)
);
Thêm vào package.json:
json
{
"scripts": {
"build": "node tao-tap-tin-phien-ban.js && react-scripts build"
}
}
2. Giải pháp Service Worker
javascript
// service-worker.js - Service Worker
const TEN_LUU_TRU = 'app-cache-v1.0.0';
self.addEventListener('install', (suKien) => {
self.skipWaiting();
});
self.addEventListener('activate', (suKien) => {
suKien.waitUntil(
caches.keys().then((danhSachCache) => {
return Promise.all(
danhSachCache.map((tenCache) => {
if (tenCache !== TEN_LUU_TRU) {
return caches.delete(tenCache);
}
})
);
})
);
self.clients.claim();
});
// Nghe tin nhắn
self.addEventListener('message', (suKien) => {
if (suKien.duLieu === 'skipWaiting') {
self.skipWaiting();
}
});
javascript
// register-service-worker.js - Đăng ký Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js').then((dangKy) => {
dangKy.addEventListener('updatefound', () => {
const workerMoi = dangKy.installing;
workerMoi.addEventListener('statechange', () => {
if (workerMoi.state === 'installed' && navigator.serviceWorker.controller) {
// Hiển thị thông báo cập nhật
hienThiCapNhat(dangKy);
}
});
});
});
// Nghe thay đổi quyền kiểm soát
let daCapNhat = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (!daCapNhat) {
daCapNhat = true;
window.location.reload();
}
});
}
function hienThiCapNhat(dangKy) {
const thongBao = document.createElement('div');
thongBao.innerHTML = `
<div style="/* Giống như trên */">
<div>Có bản cập nhật mới</div>
<button id="cap-nhat">Cập nhật</button>
</div>
`;
document.getElementById('cap-nhat').onclick = () => {
dangKy.waiting.postMessage('skipWaiting');
};
}
3. Giải pháp thông báo theo thời gian thực qua WebSocket
javascript
// web-socket-notify.js
class WebsocketThongBao {
constructor() {
this.ketNoi = null;
this.thoiGianKetNoiLai = 5000;
this.soLanKetNoiToiDa = 5;
this.soLanKetNoi = 0;
}
ketNoi() {
try {
this.ketNoi = new WebSocket('ws://server-web-socket');
this.ketNoi.onopen = () => {
console.log('Đã kết nối WebSocket');
this.soLanKetNoi = 0;
};
this.ketNoi.onmessage = (suKien) => {
const duLieu = JSON.parse(suKien.duLieu);
if (duLieu.loai === 'CO_CAP_NHAT') {
this.hienThiThongBao(duLieu.noiDung);
}
};
this.ketNoi.onclose = () => {
this.xuLyKetNoiLai();
};
this.ketNoi.onerror = (loi) => {
console.error('Lỗi WebSocket:', loi);
};
} catch (loi) {
console.error('Kết nối WebSocket thất bại:', loi);
}
}
xuLyKetNoiLai() {
if (this.soLanKetNoi < this.soLanKetNoiToiDa) {
this.soLanKetNoi++;
setTimeout(() => {
this.ketNoi();
}, this.thoiGianKetNoiLai);
}
}
hienThiThongBao(duLieu) {
// Mã hiển thị thông báo như trên
}
}
4. Giải pháp kiểm tra đơn giản theo định kỳ
javascript
// kiem-tra-cap-nhat.js
class KiemTraCapNhat {
constructor() {
this.khoangCach = 5 * 60 * 1000; // 5 phút
this.duongDanChinh = '/static/js/chinh.js'; // Đường dẫn tệp JS chính
}
batDau() {
setInterval(() => {
this.kiemTra();
}, this.khoangCach);
}
async kiemTra() {
try {
const ketQua = await fetch(this.duongDanChinh, {
method: 'HEAD',
cache: 'no-cache'
});
const thoiGianCapNhat = ketQua.headers.get('last-modified');
const thoiGianLuu = localStorage.getItem('thoi_gian_cap_nhat');
if (thoiGianCapNhat && thoiGianCapNhat !== thoiGianLuu) {
this.hienThiThongBao();
localStorage.setItem('thoi_gian_cap_nhat', thoiGianCapNhat);
}
} catch (loi) {
console.warn('Kiểm tra cập nhật thất bại:', loi);
}
}
hienThiThongBao() {
// Mã thông báo như trên
}
}
Cấu hình triển khai mẫu
Cấu hình Nginx (ngăn cache thong_tin_phiên_bản.json)
nginx
location /thong_tin_phiên_bản.json {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
Gợi ý thực hành tốt
- Trải nghiệm người dùng:
- Không bắt buộc làm mới ngay lập tức, cho phép người dùng lựa chọn
- Hiển thị thông báo ở thời điểm phù hợp (ví dụ: khi người dùng dừng thao tác)
- Cung cấp tùy chọn "Nhắc sau"
- Lựa chọn công nghệ:
- Dự án đơn giản: Sử dụng giải pháp kiểm tra phiên bản
- Ứng dụng PWA: Sử dụng Service Worker
- Yêu cầu thời gian thực cao: WebSocket
- Xử lý lỗi:
- Xử lý ngoại lệ mạng
- Phương án sao lưu
- Cơ chế thử lại