项目可运行源码地址: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;
});
})
);
}
});
这里我贴出三张图片供大家使用(素材开源于网络)



