場景:以一體化為例:目前頁面涉及頁簽和大量菜單路由,用戶想要實現頁面緩存,即列表頁、詳情頁甚至是編輯彈框頁都要實現數據緩存。
方案:使用router-view的keep-alive實現 。
一、實現思路
1.需求梳理
需要緩存模塊:
- 打開頁簽的頁面
- 新增、編輯的彈框和抽屜等表單項
- 列表頁點擊同一條數據的編輯頁
無需緩存模塊:
- 首頁
- 登錄頁
- 已關閉的頁簽頁
- 列表頁點擊不同數據的編輯頁
- 特殊聲明無需緩存頁
2.緩存的兩種方式
2.1使用name方式
注意點:name是vue組件實例的name
include:需緩存的vue組件
exclude:不做緩存的vue組件
<router-view v-slot="{ Component }"><keep-alive :include="tabKeepAliveNameList" :exclude="excludeNameList"><component :is="Component"></component></keep-alive></router-view>
2.2使用meta的keepAlive方式
通過v-if實現對應路由keepAlive為true的路由緩存
<router-view v-slot="{ Component }"><keep-alive><component v-if="$route.meta.keepAlive" :key="$route.path" :is="Component" /></keep-alive><component v-if="!$route.meta.keepAlive" :key="$route.path" :is="Component" /></router-view>
2.3最終選擇
采用1.1的vue組件實例的name方式
優點:
- 精確控制:直接指定要緩存的組件名,顆粒度細,適合明確知道需要緩存的組件。
- 靜態匹配:匹配邏輯簡單,性能較高,基于組件自身的name屬性。
- 組件獨立性:不依賴路由配置,組件自身決定是否可被緩存。
- 路由跳轉:結合動態路由生成的name,方便頁面使用name跳轉。
2.4緩存實例的生命周期
請注意:
onActivated
在組件掛載時也會調用,并且onDeactivated
在組件卸載時也會調用。- 這兩個鉤子不僅適用于
<KeepAlive>
緩存的根組件,也適用于緩存樹中的后代組件。
<script setup>
import { onActivated, onDeactivated } from 'vue'onActivated(() => {// 調用時機為首次掛載// 以及每次從緩存中被重新插入時
})onDeactivated(() => {// 在從 DOM 上移除、進入緩存// 以及組件卸載時調用
})
</script>
3.pinia新增緩存維護字段
在pinia中新增keepAliveNameList: [],存入需要緩存的組件實例的name
import { defineStore } from "pinia"
import { loginOut } from "@/api/common.js"
import router from "@/router"export default defineStore("storeUser", {persist: {storage: sessionStorage,paths: ["keepAliveNameList"]},state: () => {return {keepAliveNameList: [],}},getters: {getUserInfo: state => {return state.userInfo}},actions: {async loginOut() {await loginOut()this.clearUserInfo()router.push({ path: "/login" })},clearUserInfo() {sessionStorage.clear()this.keepAliveNameList = [] // 緩存name數據}}
})
4.導航守衛
4.1全局前置守衛router.beforeEach
在跳轉之前判斷地址欄參數是否一致,不同則需要將to頁的緩存去除,正常獲取
例如:兩次點擊列表里的數據編輯按鈕;點擊同一條是需要緩存該條表單數據,點擊不同條時候需要去除緩存重新獲取
router.beforeEach(async (to, _from, next) => {// 對于地址欄變化的需要清空緩存if (userStore.tabStore[userStore.tabStore.findIndex(it => it.path === to.path)] && JSON.stringify(userStore.tabStore[userStore.tabStore.findIndex(it => it.path === to.path)].query) !== JSON.stringify(to.query)) {userStore.$patch(state => {state.refreshUrl = to.path})let oldName = userStore.keepAliveNameListuserStore.$patch(state => {state.keepAliveNameList = oldName.filter(it => it !== to.name)})}...next()
})
4.2全局解析守衛router.beforeResolve
在離開當前頁簽時候,將該頁簽進行數據緩存。結合上方的進入前判斷,是否需要清除緩存,達成頁簽頁面正確的區分加入緩存和清除緩存
注意:此時可通過路由的方式獲取到路由name;但需要保證路由name同vue組件的name一致(目前通過腳本實現一致)
router.beforeResolve((to, from, next) => {const { userStore } = useStore()let keepAliveName = from.matched[from.matched.length - 1]?.namelet tabStoreList = (userStore.tabStore || []).map(ele => ele.name) // 頁簽集合if (!userStore.keepAliveNameList.includes(keepAliveName) && keepAliveName && tabStoreList.includes(keepAliveName)) {userStore.$patch(state => {state.keepAliveNameList.unshift(keepAliveName)})}next()
})
4.3清除緩存
- 在關閉頁簽時候,需要將緩存keepAliveNameList當前頁的name移除
- 相同菜單,但是地址欄參數變化時候,也需要清除緩存(點擊查看、編輯列表頁不同數據)
// 關閉頁簽同時去除緩存
const deleteKeepAliveName = () => {userStore.$patch(state => {state.keepAliveNameList = tabStoreList.value.map(it => it.name)})
}
細節處理
1.vue組件設置name
問題:現有vue組件存在部分未設置name情況,需要統一設置name
方案:通過腳本,統一遍歷src/view下的所有組件,有路由name的設置路由name,無路由的組件使用當前路徑命名
優點:保證路由頁面的name和組件實例name一致
1.1新增auto-set-component-name.mjs腳本
import constantRoutes from "./src/router/constant_routes.js"
import { generateRoutes } from "./src/router/static_routes.js"
// 動態添加路由添加
const dynamicRoutes = constantRoutes.concat(generateRoutes() || [])// 遞歸找對象
const findItem = (pathUrl, array) => {for (const item of array) {let componentPath// 使用示例componentPath = getComponentPath(item.component) ? getComponentPath(item.component).replace(/^@|\.vue$/g, '') : undefined// 檢查當前項的id是否匹配if (componentPath === pathUrl) return item;// 如果有子節點則遞歸查找if (item.children?.length) {const result = findItem(pathUrl, item.children);if (result) return result; // 找到則立即返回}}return undefined; // 未找到返回undefined
}// 提取組件路徑的正則表達式
const IMPORT_PATH_REGEX = /import\(["'](.*?)["']\)/;// 獲取路徑字符串
const getComponentPath = (component) => {if (!component?.toString) return null;const funcString = component.toString();const match = funcString.match(IMPORT_PATH_REGEX);return match ? match[1] : null;
};import fs from "fs"; // 文件系統模塊,用于讀寫文件
import path from "path"; // 路徑處理模塊
import { fileURLToPath } from "url"; // 用于轉換URL路徑const __filename = fileURLToPath(import.meta.url); // 當前文件絕對路徑
const __dirname = path.dirname(__filename); // 當前文件所在目錄// 🔧 配置區 ============================================
const targetDir = path.join(__dirname, "src/views"); // 目標目錄:當前目錄下的src/views
const PATH_DEPTH = Infinity; // 路徑深度設置 自由修改數字:2→最后兩級,3→最后三級,Infinity→全部路徑
// =====================================================const toPascalCase = (str) => {return str// .replace(/[-_](.)/g, (_, c) => c.toUpperCase()) // 轉換連字符/下劃線后的字母為大寫// .replace(/(^\w)/, (m) => m.toUpperCase()) // 首字母大寫.replace(/\.vue$/, ""); // 移除.vue后綴
};const processDirectory = (dir) => {const files = fs.readdirSync(dir, { withFileTypes: true }); // 讀取目錄內容console.log('%c【' + 'dir' + '】打印', 'color:#fff;background:#0f0', dir)files.forEach((file) => {const fullPath = path.join(dir, file.name); // 獲取完整路徑file.isDirectory() ? processDirectory(fullPath) : processVueFile(fullPath); // 遞歸處理目錄,直接處理文件});
};const processVueFile = (filePath) => {if (path.extname(filePath) !== ".vue") return; // 過濾非Vue文件// 生成組件名邏輯const relativePath = path.relative(targetDir, filePath);console.log('%c【' + 'targetDir' + '】打印', 'color:#fff;background:#0f0', targetDir)console.log('%c【' + 'filePath' + '】打印', 'color:#fff;background:#0f0', filePath)console.log('%c【' + 'relativePath' + '】打印', 'color:#fff;background:#0f0', relativePath)const pathSegments = relativePath.split(path.sep) // 按路徑分隔符拆分.slice(-PATH_DEPTH) // 根據配置截取路徑段.map((segment) => toPascalCase(segment)); // 轉換為PascalCaseconst vuePath = '/views/' + pathSegments.join("/"); // 拼接成最終組件名let componentName = findItem(vuePath, dynamicRoutes)?.name ? findItem(vuePath, dynamicRoutes)?.name : vuePathconsole.log(filePath, componentName);let content = fs.readFileSync(filePath, "utf8"); // 文件內容處理const oldContent = content; // 保存原始內容用于后續對比const scriptSetupRegex = /<script\s+((?:.(?!\/script>))*?\bsetup\b[^>]*)>/gim; // 靈活匹配找到scriptlet hasDefineOptions = false; // 標識是否找到defineOptions// 處理已存在的 defineOptionsconst defineOptionsRegex = /defineOptions\(\s*{([\s\S]*?)}\s*\)/g;content = content.replace(defineOptionsRegex, (match, inner) => {hasDefineOptions = true; // 標記已存在defineOptions// 替換或添加 name 屬性const nameRegex = /(name\s*:\s*['"])([^'"]*)(['"])/;let newInner = inner;if (nameRegex.test(newInner)) { // 存在name屬性時替換newInner = newInner.replace(nameRegex, `$1${componentName}$3`);console.log(`? 成功替換【name】: ${componentName} → ${filePath}`);} else { // 不存在時添加newInner = newInner.trim() === ""? `name: '${componentName}'`: `name: '${componentName}',\n${newInner}`;console.log(`? 成功添加【name】: ${componentName} → ${filePath}`);}return `defineOptions({${newInner}})`; // 重組defineOptions});// 新增 defineOptions(如果不存在)if (!hasDefineOptions) {content = content.replace(scriptSetupRegex, (match, attrs) => {return `<script ${attrs}>
defineOptions({name: '${componentName}'
})`;});console.log(`? 成功添加【defineOptions和name】: ${componentName} → ${filePath}`);}// 僅在內容變化時寫入文件if (content !== oldContent) {fs.writeFileSync(filePath, content);// console.log(`? 成功更新 name: ${componentName} → ${filePath}`);}
};processDirectory(targetDir);
console.log("🎉 所有 Vue 組件 name 處理完成!");
1.2通過執行pnpm setName腳本命令,給每個vue組件設置name
"dev": "pnpm setName && vite --mode beta --host","setName": "node auto-set-component-name.mjs",
2.路由跳轉
問題:因為涉及到動態路由,導致原先跳轉到具體菜單頁的邏輯不可行,路徑是不固定的。
方案:使用路由name跳轉;通過需要跳轉的文件路徑,找到對應跳轉的路由name即可
router.push({ name: ComponentA })
3.彈框類緩存處理
問題:默認彈框、抽屜、刪除確認框的遮罩層是全局的,當彈框存在時會阻擋點擊菜單或頁簽進行跳轉
方案:給jg-dialog彈框設置掛載屬性,通過append-to將其掛載在某個具體div下,解決遮罩區域
代碼:
:append-to-body="false":append-to="'.append_to_class'"
修改前:
修改后:
彈框:
抽屜:
刪除確認框:
4.tab類切換緩存
方案:使用<keep-alive>和<component>動態組件實現
<template><el-space class="wbg pt pl"><el-radio-group v-model="activeName"><el-radio-button label="巡檢類" value="1" /><el-radio-button label="巡檢項" value="2" /></el-radio-group></el-space><!-- <inspect-cate v-if="activeName == '1'"></inspec t-cate><inspect-item v-else></inspect-item> --><!-- 采用組件緩存 注釋上方切換刷新--><keep-alive><component :is="componentName[activeName]"></component></keep-alive>
</template><script setup lang="jsx">
defineOptions({name: 'InspectionParam'
})
import { ref, shallowRef } from "vue"
import InspectCate from "./components/inspectCate.vue"
import InspectItem from "./components/inspectItem.vue"
const activeName = ref("1")
const componentName = ref({'1': shallowRef(InspectCate),'2': shallowRef(InspectItem),
})
</script><style lang="scss" scoped></style>