基于 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;
                });
            })
        );
    }
});

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

相关推荐
国服第二切图仔1 小时前
Electron for 鸿蒙PC项目实战之拖拽组件示例
javascript·electron·harmonyos
天天向上10241 小时前
Vue 配置一次打包执行多个命令,并将分别输出到不同的文件夹
前端·javascript·vue.js
BD_Marathon1 小时前
【JavaWeb】HTML——超链接标签
前端·html
彭于晏爱编程1 小时前
🐻 Zustand 使用指南:从 0 到精通的最快路线
前端
장숙혜1 小时前
Vue DevTools 速通-掌握开发调试器
前端·javascript·vue.js
谢尔登1 小时前
为什么React 17开始无需在组件中引入React了?
前端·react.js·前端框架
ohyeah1 小时前
JavaScript 面向对象的本质:从对象模板到组合继承的完整演进
前端·javascript
Drift_Dream1 小时前
虚拟滚动:优化长列表性能的利器
前端
逃离疯人院1 小时前
前端性能深度解析:网络响应时间与实际渲染时间的鸿沟
前端