298 lines
7.8 KiB
Markdown
298 lines
7.8 KiB
Markdown
# Workdizhi 数据持久化修复方案
|
||
|
||
## 问题分析
|
||
|
||
**当前问题**:
|
||
- 数据存储在浏览器的 `localStorage` 中
|
||
- `docker-compose.yml` 中没有数据卷挂载
|
||
- 在新电脑打开时,浏览器的 localStorage 是空的,导致数据丢失
|
||
|
||
**根本原因**:
|
||
- `localStorage` 是浏览器本地存储,不是服务器端存储
|
||
- 容器中没有数据持久化配置
|
||
- 数据没有保存到服务器文件系统
|
||
|
||
## 解决方案
|
||
|
||
### 方案 1: 添加后端 API + 文件存储(推荐)
|
||
|
||
这是最彻底的解决方案,需要修改代码添加后端 API。
|
||
|
||
#### 步骤 1: 创建简单的 Node.js 后端
|
||
|
||
创建 `server.js`:
|
||
|
||
```javascript
|
||
const express = require('express');
|
||
const fs = require('fs').promises;
|
||
const path = require('path');
|
||
const cors = require('cors');
|
||
|
||
const app = express();
|
||
const DATA_FILE = path.join(__dirname, 'data', 'urls.json');
|
||
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
app.use(express.static('public'));
|
||
|
||
// 确保数据目录存在
|
||
async function ensureDataDir() {
|
||
const dataDir = path.dirname(DATA_FILE);
|
||
await fs.mkdir(dataDir, { recursive: true });
|
||
}
|
||
|
||
// 读取数据
|
||
app.get('/api/urls', async (req, res) => {
|
||
try {
|
||
await ensureDataDir();
|
||
const data = await fs.readFile(DATA_FILE, 'utf8');
|
||
res.json(JSON.parse(data));
|
||
} catch (error) {
|
||
if (error.code === 'ENOENT') {
|
||
res.json({ urls: [], categories: [] });
|
||
} else {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
}
|
||
});
|
||
|
||
// 保存数据
|
||
app.post('/api/urls', async (req, res) => {
|
||
try {
|
||
await ensureDataDir();
|
||
await fs.writeFile(DATA_FILE, JSON.stringify(req.body, null, 2));
|
||
res.json({ success: true });
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
app.listen(PORT, () => {
|
||
console.log(`Server running on port ${PORT}`);
|
||
});
|
||
```
|
||
|
||
#### 步骤 2: 修改前端代码
|
||
|
||
修改 `script.js` 中的数据加载和保存方法:
|
||
|
||
```javascript
|
||
// 修改 loadData 方法
|
||
async loadData() {
|
||
try {
|
||
const response = await fetch('/api/urls');
|
||
const data = await response.json();
|
||
this.urls = data.urls || this.getDefaultUrls();
|
||
this.categories = data.categories || this.getDefaultCategories();
|
||
} catch (error) {
|
||
console.error('从服务器加载数据失败,使用默认数据:', error);
|
||
// 降级到 localStorage
|
||
const savedUrls = localStorage.getItem('urlManager_urls');
|
||
const savedCategories = localStorage.getItem('urlManager_categories');
|
||
if (savedUrls) {
|
||
this.urls = JSON.parse(savedUrls);
|
||
} else {
|
||
this.urls = this.getDefaultUrls();
|
||
}
|
||
if (savedCategories) {
|
||
this.categories = JSON.parse(savedCategories);
|
||
} else {
|
||
this.categories = this.getDefaultCategories();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 修改 saveData 方法
|
||
async saveData() {
|
||
try {
|
||
// 先保存到服务器
|
||
const response = await fetch('/api/urls', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
urls: this.urls,
|
||
categories: this.categories
|
||
})
|
||
});
|
||
if (!response.ok) throw new Error('服务器保存失败');
|
||
} catch (error) {
|
||
console.error('保存到服务器失败,降级到 localStorage:', error);
|
||
// 降级到 localStorage
|
||
try {
|
||
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
|
||
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
|
||
} catch (e) {
|
||
console.error('保存到 localStorage 也失败:', e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 步骤 3: 更新 Dockerfile
|
||
|
||
```dockerfile
|
||
FROM node:18-alpine
|
||
|
||
WORKDIR /app
|
||
|
||
# 安装依赖
|
||
COPY package.json .
|
||
RUN npm install
|
||
|
||
# 复制文件
|
||
COPY server.js .
|
||
COPY index.html public/
|
||
COPY script.js public/
|
||
COPY style.css public/
|
||
|
||
# 创建数据目录
|
||
RUN mkdir -p data
|
||
|
||
EXPOSE 3000
|
||
|
||
CMD ["node", "server.js"]
|
||
```
|
||
|
||
#### 步骤 4: 更新 docker-compose.yml
|
||
|
||
```yaml
|
||
version: '3.8'
|
||
|
||
services:
|
||
web:
|
||
build: .
|
||
container_name: workdizhi-web
|
||
ports:
|
||
- "3006:3000"
|
||
restart: unless-stopped
|
||
volumes:
|
||
# 挂载数据目录,持久化存储
|
||
- ./data:/app/data
|
||
networks:
|
||
- workdizhi-network
|
||
|
||
networks:
|
||
workdizhi-network:
|
||
driver: bridge
|
||
```
|
||
|
||
### 方案 2: 使用 Nginx + 文件存储(简单方案)
|
||
|
||
如果不想添加后端,可以使用 Nginx 的静态文件服务 + JavaScript 文件操作。
|
||
|
||
#### 步骤 1: 修改 script.js 使用 fetch API 保存到文件
|
||
|
||
```javascript
|
||
// 添加文件保存方法
|
||
async saveToFile() {
|
||
try {
|
||
const data = {
|
||
urls: this.urls,
|
||
categories: this.categories,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
// 使用 Blob 和下载方式保存(需要用户手动操作)
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'workdizhi_backup.json';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
} catch (error) {
|
||
console.error('保存文件失败:', error);
|
||
}
|
||
}
|
||
|
||
// 添加文件加载方法
|
||
async loadFromFile(file) {
|
||
try {
|
||
const text = await file.text();
|
||
const data = JSON.parse(text);
|
||
this.urls = data.urls || this.getDefaultUrls();
|
||
this.categories = data.categories || this.getDefaultCategories();
|
||
this.saveData();
|
||
this.render();
|
||
} catch (error) {
|
||
console.error('加载文件失败:', error);
|
||
alert('文件格式错误或损坏');
|
||
}
|
||
}
|
||
```
|
||
|
||
### 方案 3: 导出/导入功能(临时方案)
|
||
|
||
添加导出和导入功能,让用户可以手动备份和恢复数据。
|
||
|
||
#### 修改 script.js 添加导出/导入功能
|
||
|
||
```javascript
|
||
// 导出数据
|
||
exportData() {
|
||
const data = {
|
||
urls: this.urls,
|
||
categories: this.categories,
|
||
exportDate: new Date().toISOString()
|
||
};
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `workdizhi_backup_${new Date().toISOString().split('T')[0]}.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// 导入数据
|
||
importData(file) {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
try {
|
||
const data = JSON.parse(e.target.result);
|
||
this.urls = data.urls || [];
|
||
this.categories = data.categories || [];
|
||
this.saveData();
|
||
this.render();
|
||
alert('数据导入成功!');
|
||
} catch (error) {
|
||
alert('文件格式错误,导入失败');
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
```
|
||
|
||
## 推荐实施步骤
|
||
|
||
### 快速修复(方案 3 - 导出/导入)
|
||
|
||
1. 添加导出/导入按钮到界面
|
||
2. 用户可以定期导出数据备份
|
||
3. 在新电脑上导入备份文件
|
||
|
||
### 长期方案(方案 1 - 后端 API)
|
||
|
||
1. 创建 Node.js 后端服务
|
||
2. 修改前端代码使用 API
|
||
3. 添加数据卷挂载
|
||
4. 重新构建和部署
|
||
|
||
## 当前数据位置
|
||
|
||
- **浏览器 localStorage**: `urlManager_urls`, `urlManager_categories`
|
||
- **服务器**: 无持久化存储
|
||
|
||
## 修复后的数据位置
|
||
|
||
- **服务器文件**: `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json`
|
||
- **数据卷**: 挂载到容器内的 `/app/data` 或 `/usr/share/nginx/html/data`
|
||
|
||
## 注意事项
|
||
|
||
1. **备份现有数据**: 在修改前,先导出当前 localStorage 中的数据
|
||
2. **测试**: 修改后充分测试数据保存和加载功能
|
||
3. **迁移**: 如果已有数据,需要提供迁移脚本
|
||
|