Files
workdizhi/workdizhi数据持久化修复方案.md

298 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. **迁移**: 如果已有数据,需要提供迁移脚本