Giải pháp thông báo người dùng làm mới sau khi triển khai frontend

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

  1. 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"
  1. 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
  1. Xử lý lỗi:
  • Xử lý ngoại lệ mạng
  • Phương án sao lưu
  • Cơ chế thử lại

Thẻ: JavaScript Service Worker WebSocket frontend nginx

Đăng vào ngày 21 tháng 5 lúc 16:48