基于 Vue3 + Electron 的离线图片缓存方案

项目可运行源码地址:github 源码地址

有这样一个需求:

默认情况下是有网络的,我通过接口请求拿到了图片列表(其中是所有图片的在线访问地址,图片皆放在云盘存储),可以在页面中展示这些图片,但如果某时突然断网了,无法再通过接口请求到地址,该如何拿到图片进行展示?

最初我尝试了 利用 html 文件 + Service Worker 的方式,这个方式是针对 Web 端提供的离线方案,但这个方案具有一个局限性:**必须在 http 或者 https 协议下使用。**即要求浏览器地址栏的地址是这样的格式:

复制代码
https://www.baidu.com/

如果我们打包好项目之后,在文件夹中直接点开 html 文件,那么在浏览器地址栏中,它的格式会是:

复制代码
file:///D:/my-web/H5.html

此时,使用的 Service Worker 就会失效,注册失败。(文末我会贴出对于 Service Worker 的使用案例)

我将这个图片列表进行本地缓存,这样在没有网络的情况下,我可以直接访问缓存中的图片,那么在electron项目下,我应该如何实现?

Vue3 + Electron

创建项目

我这里使用 Electron + Vite + Vue3

使用管理员身份打开 cmd,进入你要创建项目的根目录,执行:

javascript 复制代码
npm create electron-vite@latest my-electron-app

选择框架 Vue

执行如下命令安装依赖:

javascript 复制代码
cd my-electron-app
npm install

编写项目

如果想让我们设置的高度和宽度不被 Electron 窗口的内容所挤压,可以在 electron/main.ts 中设置:

TypeScript 复制代码
useContentSize: true

后续我们存储文件在本地文件中,需要可以访问 file://,需要设置:

TypeScript 复制代码
webSecurity: false

这部分完整代码如下:

TypeScript 复制代码
function createWindow() {
  win = new BrowserWindow({
    useContentSize: true,
    icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
    webPreferences: {
      preload: path.join(__dirname, 'preload.mjs'),
      webSecurity: false,
    }
  })
}

这部分是判断接口是否可以正常调用,如果调用失败(网络问题),就需要读取缓存;否则将请求到的列表写入缓存。

这里通过 isRequest 参数模拟请求:默认第一次是 true,请求成功写入缓存;之后将 isRequest 置为 false,读取缓存中的图片。

在文件夹中就看到了缓存的图片:

详细代码可以去代码仓查看源码

附加需求:清空缓存(在源码中)

在 electron/main.ts 中添加:

TypeScript 复制代码
ipcMain.handle('cache:clear', async () => {
  try {
    if (fs.existsSync(CACHE_DIR)) {
      fs.rmSync(CACHE_DIR, { recursive: true, force: true });
    }
    fs.mkdirSync(CACHE_DIR, { recursive: true });
    return { success: true };
  } catch (e) {
    return { success: false, error: e.message };
  }
});

在 electron/preload.ts 中添加:

TypeScript 复制代码
contextBridge.exposeInMainWorld('cacheApi', {
  clear: () => ipcRenderer.invoke('cache:clear'),
  saveImages: (list: string[]) => ipcRenderer.invoke('cache/saveImages', list),
  getImages: () => ipcRenderer.invoke('cache/getImages')
});

这样,在每次利用接口获取到图片列表之后,写入缓存之前都会将缓存中原有的图片清掉,相当于重新赋值:

javascript 复制代码
// 清空缓存
await window.cacheApi.clear();
// 缓存图片到本地
await window.cacheApi.saveImages(imageUrls);

Service Worker

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>图片离线方案示例</title>
    <style>
        body, html {
            margin: 0;
            padding: 0;
            width: 100vw;
            height: 100vh;
            overflow: hidden;
            background: #000;
        }

        .wrapper {
            width: 100vw;
            height: 100vh;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
            background: #000;
        }

        .content {
            width: 1080px;
            aspect-ratio: 1080 / 1920;
            background: #111;
            overflow: hidden;
            position: relative;
            transform-origin: center center;
        }

        .img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            position: absolute;
            top: 0;
            left: 0;
        }

        .slide-enter {
            transform: translateX(100%);
            opacity: 0;
        }
        .slide-enter-active {
            transition: all 0.6s ease;
            transform: translateX(0%);
            opacity: 1;
        }
        .slide-leave {
            transform: translateX(0);
            opacity: 1;
        }
        .slide-leave-active {
            transition: all 0.6s ease;
            transform: translateX(-100%);
            opacity: 0;
        }
    </style>
</head>

<body>
<div class="wrapper">
    <div class="content" id="content">
        <img id="slide-img" class="img" src="./img/1.jpg" alt="" />
    </div>
</div>

<script>
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('./service-worker.js')
            .then(() => console.log('SW 注册成功'))
            .catch(err => console.error('SW 注册失败', err));
    }
    let images = [];
    let currentIndex = 0;
    let timer = null;

    let imgEl = document.getElementById("slide-img");
    const content = document.getElementById("content");

    const designWidth = 1080;
    const designHeight = 1920;

    function updateScale() {
        const ww = window.innerWidth;
        const wh = window.innerHeight;
        const scale = Math.min(ww / designWidth, wh / designHeight);
        content.style.transform = `scale(${scale})`;
    }
    window.addEventListener("resize", updateScale);

    function nextPage() {
        const oldImg = imgEl;

        let newImg = document.createElement("img");
        newImg.src = images[(currentIndex + 1) % images.length];
        newImg.className = "img slide-enter";

        content.appendChild(newImg);

        newImg.offsetWidth;

        newImg.classList.add("slide-enter-active");
        oldImg.classList.add("slide-leave");
        oldImg.classList.add("slide-leave-active");

        setTimeout(() => {
            oldImg.remove();
            newImg.className = "img";
            imgEl = newImg;
        }, 600);

        currentIndex = (currentIndex + 1) % images.length;
    }

    async function cacheImages(list) {
        const cache = await caches.open('slider-cache-v1');
        await cache.addAll(list);
    }

    async function loadImagesFromCache() {
        const cache = await caches.open('slider-cache-v1');
        const keys = await cache.keys();
        return keys.map(k => k.url);
    }

    async function postData() {
        let isSuccess = false;

        try {
            const res = await fetch('/api/get_img_list', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ page: 0 })
            });

            if(res.ok) {
                isSuccess = true;
                console.log('POST 成功,已缓存图片');
                images = [
                    "./img/1.jpg",
                    "./img/2.jpg",
                    "./img/3.jpg"
                ]
                await cacheImages(images);
            } else {
                console.log('POST 失败');
            }
        } catch (err) {
            console.log('POST 失败', err);
        }

        if (!isSuccess) {
            console.log("接口失败 → 使用缓存图片");
            const cached = await loadImagesFromCache();
            if (cached.length) {
                images = cached;
                console.log("缓存图片:", cached);
            } else {
                console.error("缓存中没有任何图片!");
            }
        }

        updateScale();
        timer = setInterval(nextPage, 5000);
    }

    postData()
</script>
</body>
</html>
javascript 复制代码
// service-worker.js
const CACHE_NAME = 'slider-cache-v1';

const IMAGE_LIST = [];

// 安装阶段:预缓存图片
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => cache.addAll(IMAGE_LIST))
    );
});

// 拦截图片请求:优先从缓存读取,没有则网络获取并写入缓存
self.addEventListener('fetch', event => {
    const url = event.request.url;

    // 是否是我们要缓存的图片
    if (IMAGE_LIST.some(img => url.endsWith(img.replace('./', '')))) {
        event.respondWith(
            caches.match(event.request).then(cached => {
                return cached || fetch(event.request).then(res => {
                    const copy = res.clone();
                    caches.open(CACHE_NAME).then(cache => cache.put(event.request, copy));
                    return res;
                });
            })
        );
    }
});

这里我贴出三张图片供大家使用(素材开源于网络)

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax