From 60ea884fe2ad1677309b3911db27471880ba5ac9 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 30 Jun 2025 17:37:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=A8=E5=B1=80=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=92=8C=E5=9B=BE=E7=89=87=E4=BC=98=E5=8C=96=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.ts | 4 + src/components/cards/MediaCard.vue | 11 +- src/main.ts | 7 +- src/router/index.ts | 36 +++--- src/utils/imageOptimizer.ts | 128 ++++++++++++++++++++++ src/utils/requestOptimizer.ts | 98 +++++++++++++++++ src/views/discover/MediaCardSlideView.vue | 6 +- 7 files changed, 256 insertions(+), 34 deletions(-) create mode 100644 src/utils/imageOptimizer.ts create mode 100644 src/utils/requestOptimizer.ts diff --git a/src/api/index.ts b/src/api/index.ts index b3141e67..9197bc6c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,6 +1,7 @@ import axios from 'axios' import router from '@/router' import { useAuthStore } from '@/stores' +import { initializeRequestOptimizer } from '@/utils/requestOptimizer' // 创建axios实例 const api = axios.create({ @@ -17,6 +18,9 @@ declare global { // 将 API 实例暴露到全局,供插件使用 window.MoviePilotAPI = api +// 初始化请求优化器(必须在其他拦截器之前) +initializeRequestOptimizer(api) + // 添加请求拦截器 api.interceptors.request.use(config => { // 认证 Store diff --git a/src/components/cards/MediaCard.vue b/src/components/cards/MediaCard.vue index 10cfa1b6..65a0c130 100644 --- a/src/components/cards/MediaCard.vue +++ b/src/components/cards/MediaCard.vue @@ -8,7 +8,7 @@ import { useToast } from 'vue-toastification' import { formatSeason, formatRating } from '@/@core/utils/formatters' import { doneNProgress, startNProgress } from '@/api/nprogress' import type { MediaInfo, Subscribe, MediaSeason, Site } from '@/api/types' -import router, { registerAbortController } from '@/router' +import router from '@/router' import { useUserStore } from '@/stores' import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue' import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue' @@ -232,9 +232,6 @@ async function handleCheckSubscribe() { // 查询当前媒体是否已入库 async function handleCheckExists() { try { - const abortController = new AbortController() - registerAbortController(abortController) - const { signal } = abortController const result: { [key: string]: any } = await api.get('mediaserver/exists', { params: { tmdbid: props.media?.tmdb_id, @@ -243,7 +240,6 @@ async function handleCheckExists() { season: props.media?.season, mtype: props.media?.type, }, - signal, }) if (result.success) isExists.value = true @@ -255,16 +251,13 @@ async function handleCheckExists() { // 调用API检查是否已订阅,电视剧需要指定季 async function checkSubscribe(season = 0) { try { - const abortController = new AbortController() - registerAbortController(abortController) - const { signal } = abortController + // AbortController 现在由全局请求优化器自动管理 const mediaid = getMediaId() const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, { params: { season, title: props.media?.title, }, - signal, }) return result.id || null diff --git a/src/main.ts b/src/main.ts index 05018750..7e46ecc0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,6 +21,7 @@ import { CronVuetify } from '@vue-js-cron/vuetify' import { isPWA } from './@core/utils/navigator' import { loadRemoteComponents } from './utils/federationLoader' import { fetchGlobalSettings } from './utils/globalSetting' +import { initializeImageOptimizer } from '@/utils/imageOptimizer' // 5. 其他插件和功能模块 import Toast from 'vue-toastification' @@ -105,5 +106,9 @@ initializeApp().then(() => { }) .use(ConfirmDialog) .use(i18n) - .mount('#app') + + // 初始化全局图片优化器 + initializeImageOptimizer() + + app.mount('#app') }) diff --git a/src/router/index.ts b/src/router/index.ts index ec4501d6..d2401bcd 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,6 +1,8 @@ import { createRouter, createWebHashHistory } from 'vue-router' import { configureNProgress } from '@/api/nprogress' import { useAuthStore } from '@/stores' +import { setNavigatingState as setImageNavigatingState } from '@/utils/imageOptimizer' +import { setNavigatingState as setRequestNavigatingState } from '@/utils/requestOptimizer' // Nprogress configureNProgress() @@ -208,23 +210,12 @@ const router = createRouter({ ], }) -const abortControllers = new Set() - -// 注册中止控制器 -function registerAbortController(controller: AbortController) { - abortControllers.add(controller) -} - -// 中止所有组件的任务 -function abortAllControllers() { - for (const controller of abortControllers) { - controller.abort() - } - abortControllers.clear() -} - // 路由导航守卫 router.beforeEach(async (to: any, from: any, next: any) => { + // 设置导航状态 - 同时暂停图片加载和中断API请求 + setImageNavigatingState(true) + setRequestNavigatingState(true) + // 认证 Store const authStore = useAuthStore() // 总是记录非login路由 @@ -233,15 +224,22 @@ router.beforeEach(async (to: any, from: any, next: any) => { if (to.meta.requiresAuth && !isAuthenticated) { // 用户未登录,重定向到登录页 + setImageNavigatingState(false) + setRequestNavigatingState(false) next('/login') } else { - // 清理所有中止控制器 - abortAllControllers() next() } }) +// 路由导航完成后 +router.afterEach(() => { + setTimeout(() => { + setImageNavigatingState(false) + setRequestNavigatingState(false) + }, 200) +}) + // 导出默认对象 export default router -// 另行导出其他功能 -export { registerAbortController } + // 延迟恢复图片加载,给页面一些初始化时间 diff --git a/src/utils/imageOptimizer.ts b/src/utils/imageOptimizer.ts new file mode 100644 index 00000000..91c2456c --- /dev/null +++ b/src/utils/imageOptimizer.ts @@ -0,0 +1,128 @@ +let isNavigating = false +const MAX_CONCURRENT_IMAGES = 10 // 并发数 +let currentLoadingCount = 0 +const imageQueue: { img: HTMLImageElement; src: string }[] = [] + +// 监听路由状态 +export function setNavigatingState(navigating: boolean) { + isNavigating = navigating + + if (navigating) { + // 路由切换时只是标记状态,不强制中断图片加载 + console.log('Navigation started - pausing new image loads') + } else { + // 路由切换完成后,处理队列中的图片 + setTimeout(() => { + console.log('Navigation ended - resuming image loads') + processImageQueue() + }, 100) + } +} + +// 处理图片队列 +function processImageQueue() { + while (imageQueue.length > 0 && currentLoadingCount < MAX_CONCURRENT_IMAGES && !isNavigating) { + const { img, src } = imageQueue.shift()! + startImageLoad(img, src) + } +} + +// 开始加载图片 +function startImageLoad(img: HTMLImageElement, src: string) { + currentLoadingCount++ + + const originalOnLoad = img.onload + const originalOnError = img.onerror + + img.onload = function (event) { + currentLoadingCount-- + processImageQueue() // 加载完成后处理队列 + if (originalOnLoad) originalOnLoad.call(this, event) + } + + img.onerror = function (event) { + currentLoadingCount-- + processImageQueue() // 出错时也要处理队列 + if (originalOnError) originalOnError.call(this, event) + } + + img.src = src +} + +// 智能图片加载函数 +function smartImageLoad(img: HTMLImageElement, src: string) { + if (isNavigating) { + // 路由切换时,加入队列等待 + imageQueue.push({ img, src }) + return + } + + if (currentLoadingCount < MAX_CONCURRENT_IMAGES) { + // 直接加载 + startImageLoad(img, src) + } else { + // 加入队列 + imageQueue.push({ img, src }) + } +} + +// 初始化图片优化器 +export function initializeImageOptimizer() { + // 只对新创建的img元素进行温和的优化 + const originalCreateElement = document.createElement + document.createElement = function (tagName: string, options?: ElementCreationOptions) { + const element = originalCreateElement.call(this, tagName, options) + + if (tagName.toLowerCase() === 'img') { + const img = element as HTMLImageElement + + // 只拦截src的设置,使用更温和的方式 + const originalSetAttribute = img.setAttribute + img.setAttribute = function (name: string, value: string) { + if (name === 'src' && value) { + // 使用智能加载 + smartImageLoad(this, value) + return + } + return originalSetAttribute.call(this, name, value) + } + } + + return element + } + + // 为现有图片添加懒加载属性 + document.addEventListener('DOMContentLoaded', () => { + const images = document.querySelectorAll('img:not([loading])') + images.forEach(img => { + img.setAttribute('loading', 'lazy') + }) + }) + + // 监听新添加的图片 + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element + + // 为新添加的img标签设置懒加载 + if (element.tagName === 'IMG' && !element.getAttribute('loading')) { + element.setAttribute('loading', 'lazy') + } + + // 为子元素中的img设置懒加载 + const imgs = element.querySelectorAll('img:not([loading])') + imgs.forEach(img => { + img.setAttribute('loading', 'lazy') + }) + } + }) + }) + }) + + observer.observe(document.body, { + childList: true, + subtree: true, + }) +} diff --git a/src/utils/requestOptimizer.ts b/src/utils/requestOptimizer.ts new file mode 100644 index 00000000..de68fcae --- /dev/null +++ b/src/utils/requestOptimizer.ts @@ -0,0 +1,98 @@ +// 全局请求优化器 +// 自动管理所有API请求的中断,无需手动注册 + +let isNavigating = false +const activeRequests = new Set() + +// 监听路由状态 +export function setNavigatingState(navigating: boolean) { + isNavigating = navigating + + if (navigating) { + // 路由切换时,中断所有未完成的请求 + console.log('Navigation started - aborting active requests') + abortAllActiveRequests() + } +} + +// 中断所有活跃的请求 +function abortAllActiveRequests() { + for (const controller of activeRequests) { + if (!controller.signal.aborted) { + controller.abort() + } + } + activeRequests.clear() +} + +// 清理已完成的请求控制器 +function cleanupController(controller: AbortController) { + activeRequests.delete(controller) +} + +// 初始化请求优化器 +export function initializeRequestOptimizer(axiosInstance: any) { + // 拦截请求,自动添加 AbortController + axiosInstance.interceptors.request.use( + (config: any) => { + // 如果请求已经有 signal,跳过(避免覆盖手动设置的) + if (config.signal) { + return config + } + + // 创建新的 AbortController + const controller = new AbortController() + config.signal = controller.signal + + // 将控制器添加到活跃列表 + activeRequests.add(controller) + + // 监听请求完成事件来清理控制器 + const cleanup = () => cleanupController(controller) + + // 监听中断事件 + controller.signal.addEventListener('abort', cleanup, { once: true }) + + return config + }, + (error: any) => { + return Promise.reject(error) + }, + ) + + // 拦截响应,清理对应的控制器 + axiosInstance.interceptors.response.use( + (response: any) => { + // 从配置中获取 signal 对应的控制器并清理 + if (response.config?.signal) { + const controller = Array.from(activeRequests).find(ctrl => ctrl.signal === response.config.signal) + if (controller) { + cleanupController(controller) + } + } + return response + }, + (error: any) => { + // 错误时也要清理控制器 + if (error.config?.signal) { + const controller = Array.from(activeRequests).find(ctrl => ctrl.signal === error.config.signal) + if (controller) { + cleanupController(controller) + } + } + return Promise.reject(error) + }, + ) + + console.log('Request optimizer initialized - all requests will be auto-managed') +} + +// 获取当前活跃请求数量(调试用) +export function getActiveRequestsCount() { + return activeRequests.size +} + +// 手动中断所有请求(备用方法) +export function abortAllRequests() { + abortAllActiveRequests() +} diff --git a/src/views/discover/MediaCardSlideView.vue b/src/views/discover/MediaCardSlideView.vue index b837df25..ea7e9ccf 100644 --- a/src/views/discover/MediaCardSlideView.vue +++ b/src/views/discover/MediaCardSlideView.vue @@ -3,7 +3,6 @@ import api from '@/api' import type { MediaInfo } from '@/api/types' import MediaCard from '@/components/cards/MediaCard.vue' import SlideView from '@/components/slide/SlideView.vue' -import { registerAbortController } from '@/router' import { useI18n } from 'vue-i18n' const { t } = useI18n() @@ -28,10 +27,7 @@ const dataList = ref([]) async function fetchData() { try { if (!props.apipath) return - const abortController = new AbortController() - registerAbortController(abortController) - const { signal } = abortController - dataList.value = await api.get(props.apipath, { signal }) + dataList.value = await api.get(props.apipath) if (dataList.value.length > 0) componentLoaded.value = true } catch (error) { console.error(error)