Quản Lý Component Cache Với keep-alive Trong Vue.js: Những Điều Cần Lưu Ý

keep-alive là một component tích hợp sẵn trong Vue.js, được thiết kế để lưu trữ các instance component đã bị hủy kích hoạt. Điều này giúp duy trì trạng thái của chúng hoặc tránh việc render lại từ đầu, từ đó cải thiện hiệu suất ứng dụng. Tuy nhiên, việc sử dụng keep-alive cần được cân nhắc kỹ lưỡng tùy theo từng kịch bản điều hướng.

1. Kịch bản điều hướng một cấp: Danh sách sang Chi tiết và ngược lại

Đối với các luồng điều hướng đơn giản, ví dụ từ trang Danh sách (A) sang trang Chi tiết (B), và sau đó quay lại trang Danh sách (A), bạn có thể cân nhắc cách tiếp cận có điều kiện sau đây để chỉ cache những component cần thiết:

<!-- Các component cần cache dựa trên meta data của route -->
<keep-alive>
   <router-view :key="$route.fullPath" v-if="$route.meta.persistView"></router-view>
</keep-alive>
<!-- Các component không cần cache -->
<router-view :key="$route.fullPath" v-if="!$route.meta.persistView"></router-view>

Trong cấu hình này, thuộc tính persistView trong $route.meta sẽ quyết định liệu component của route hiện tại có được keep-alive giữ lại hay không. :key="$route.fullPath" đảm bảo rằng khi đường dẫn thay đổi, router-view sẽ được render lại ngay cả khi nó nằm trong keep-alive, đặc biệt hữu ích khi chỉ có các tham số query thay đổi.

2. Kịch bản điều hướng đa cấp với cache chọn lọc

Trong các tình huống phức tạp hơn, chẳng hạn như có ba trang: Danh sách (A), Chi tiết (B), và Chi tiết Mở rộng (C). Yêu cầu đặt ra là: khi điều hướng từ A đến B, A cần được cache; khi điều hướng từ B đến C, B cần được cache (để khi quay lại từ C, B vẫn giữ nguyên trạng thái); nhưng B phải được render mới mỗi khi truy cập từ A.

Để xử lý kịch bản này, bạn có thể sử dụng keep-alive với các thuộc tính include hoặc exclude để kiểm soát chính xác component nào được cache:

<keep-alive :include="['ComponentA', 'ComponentB']">
     <router-view></router-view>
</keep-alive>

Vue cung cấp ba thuộc tính chính cho keep-alive để điều khiển hành vi cache:

  • include: (String hoặc RegExp hoặc Array) Chỉ những component có tên khớp mới được cache.
  • exclude: (String hoặc RegExp hoặc Array) Bất kỳ component nào có tên khớp sẽ không được cache.
  • max: (Number) Số lượng tối đa các instance component có thể được cache.

Các ví dụ về cách sử dụng include:

<!-- Chuỗi được phân tách bằng dấu phẩy -->
<keep-alive include="DanhSach,ChiTiet">
  <component :is="activeComponent"></component>
</keep-alive>

<!-- Biểu thức chính quy (sử dụng `v-bind`) -->
<keep-alive :include="/TrangA|TrangB/">
  <component :is="activeComponent"></component>
</keep-alive>

<!-- Mảng (sử dụng `v-bind`) -->
<keep-alive :include="['TrangMot', 'TrangHai']">
  <component :is="activeComponent"></component>
</keep-alive>

Lưu ý quan trọng khi sử dụng phương pháp này:

  1. Cần đảm bảo rằng chỉ có một <router-view></router-view> duy nhất trong component cha để cache hoạt động đúng cách. Nếu có nhiều router-view, tất cả chúng có thể bị cache, dẫn đến hành vi không mong muốn.
  2. Các giá trị trong include hoặc exclude (ví dụ: 'ComponentA', 'ComponentB') phải là thuộc tính name của component.
  3. Dù sử dụng phương pháp nào, nếu bạn chủ động gọi $destroy để hủy component, component đó sẽ không được keep-alive cache.

Phương pháp thứ hai thường phù hợp hơn khi kết hợp với Vuex để quản lý trạng thái global.

Ví dụ về cách quản lý cache thủ công trong dự án

Trong một số trường hợp, nếu cấu trúc ứng dụng của bạn yêu cầu nhiều <router-view></router-view> hoặc có các yêu cầu caching phức tạp, phương pháp include/exclude có thể không đủ. Dưới đây là một ví dụ về cách quản lý cache một cách thủ công hơn, sử dụng các hook của Vue Router và sessionStorage.

Giả định chúng ta có các component: CategoryPage (A), ProductDetail (B), và ProductSubDetail (C).

Yêu cầu:

  • A vào B: B không cache.
  • B vào C: C không cache.
  • C quay lại B: B được cache nếu không có thay đổi, ngược lại sẽ được tải lại.
  • B quay lại A: A được cache nếu không có thay đổi (kể cả từ C), ngược lại sẽ được tải lại.

Component CategoryPage (A):

Để đảm bảo trang danh mục tải lại khi cần thiết (ví dụ: sau khi chỉnh sửa sản phẩm trong trang chi tiết), chúng ta sẽ kiểm tra cờ trong sessionStorage.

beforeRouteEnter(toRoute, fromRoute, nextCallback) {
    toRoute.meta.shouldCache = true; // Đánh dấu route hiện tại có thể được cache
    nextCallback(componentInstance => {
        // Tải lại danh sách nếu không phải từ trang chi tiết, trang gốc hoặc trang đăng nhập
        if (!fromRoute.path.includes("productDetail") && fromRoute.path !== "/" && fromRoute.path !== "/auth/login") {
            componentInstance.currentState = 0;
            componentInstance.fetchCategoryList();
        }
        // Tải lại danh sách nếu quay lại từ trang chi tiết và có cờ 'needsRefresh'
        if (fromRoute.path.includes("productDetail") && sessionStorage.getItem("needsRefresh") === "true") {
            componentInstance.currentState = 0;
            componentInstance.fetchCategoryList();
            sessionStorage.removeItem("needsRefresh"); // Xóa cờ sau khi xử lý
        }
    });
}

Ở đây, chúng ta kiểm tra fromRoute.path để phân biệt nguồn gốc điều hướng và sessionStorage.getItem("needsRefresh") để xác định liệu có cần tải lại dữ liệu danh sách hay không. `"/auth/login"` được kiểm tra để tránh gọi lại API khi đăng nhập xong.

Component ProductDetail (B):

Trang chi tiết cần được tải lại khi truy cập từ trang danh mục, nhưng được cache khi quay lại từ trang chi tiết con (nếu không có thay đổi).

beforeRouteEnter(toRoute, fromRoute, nextCallback) {
    toRoute.meta.shouldCache = true; // Đánh dấu route hiện tại có thể được cache
    nextCallback(componentInstance => {
        // Tải lại chi tiết nếu từ trang danh mục hoặc truy cập lần đầu
        if (fromRoute.path.includes("categoryPage") || fromRoute.path === "/") {
            componentInstance.displayMode = 1;
            componentInstance.selectedCategoryIndex = "0-0";
            componentInstance.retrieveProductDetails();
        }
        // Tải lại chi tiết nếu quay lại từ trang con và có yêu cầu làm mới
        if (toRoute.meta.forceDetailRefresh) {
            componentInstance.displayMode = 1;
            componentInstance.selectedCategoryIndex = "0-0";
            componentInstance.retrieveProductDetails();
            toRoute.meta.forceDetailRefresh = false; // Đặt lại cờ sau khi xử lý
        }
    });
}

Ở đây, toRoute.meta.forceDetailRefresh là một cờ được thiết lập bởi trang con (C) để báo hiệu cho trang chi tiết (B) rằng cần phải tải lại dữ liệu.

Component ProductSubDetail (C):

Component này sẽ thiết lập các cờ cần thiết khi rời đi, dựa trên trạng thái chỉnh sửa.

beforeRouteLeave(toRoute, fromRoute, nextCallback) {
    // Nếu chuyển hướng về trang chi tiết và có thay đổi
    if (toRoute.path.includes("productDetail") && toRoute.query.id && this.hasPendingChanges) {
        toRoute.meta.shouldCache = false;           // Ngăn không cho trang chi tiết bị cache
        toRoute.meta.forceDetailRefresh = true;     // Báo cho trang chi tiết phải làm mới
        sessionStorage.setItem("needsRefresh", "true"); // Báo cho trang danh mục cần làm mới
    }
    nextCallback();
}

this.hasPendingChanges là một thuộc tính trong component C, mặc định là false. Nó sẽ được đặt thành true nếu người dùng thực hiện bất kỳ thay đổi nào. Khi quay lại trang chi tiết (B), nếu có thay đổi, chúng ta thiết lập toRoute.meta.forceDetailRefresh và sessionStorage.setItem("needsRefresh") để các trang trước đó biết cần phải làm mới dữ liệu.

Thẻ: Vue.js keep-alive Vue Router Component Caching Frontend Optimization

Đăng vào ngày 10 tháng 11 lúc 21:55