Skip to main content

前端存储之IndexedDB

· 5 min read
LIU

技术方案

1. IndexedDB

IndexedDB 是一个强大的浏览器内置数据库,具有以下优势:

  • 支持存储大量结构化数据
  • 支持索引,便于快速检索
  • 支持事务,保证数据一致性
  • 异步 API,不会阻塞主线程

2. 数据加密:jsencrypt

使用 jsencrypt 进行数据加密,确保离线数据的安全性:

  • 使用 RSA 非对称加密
  • 公钥加密,私钥解密
  • 保证数据在本地存储的安全性

核心实现

1. IndexedDB 数据库设计

// db.js
const DB_NAME = "medical_reports";
const DB_VERSION = 1;

const initDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);

request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);

request.onupgradeneeded = (event) => {
const db = event.target.result;

// 报告基本信息表
if (!db.objectStoreNames.contains("reports")) {
const reportStore = db.createObjectStore("reports", { keyPath: "id" });
reportStore.createIndex("patientId", "patientId", { unique: false });
reportStore.createIndex("timestamp", "timestamp", { unique: false });
}

// 波形数据表
if (!db.objectStoreNames.contains("waveforms")) {
const waveformStore = db.createObjectStore("waveforms", {
keyPath: "id",
});
waveformStore.createIndex("reportId", "reportId", { unique: false });
}
};
});
};

2. 数据加密存储

// encryption.js
import JSEncrypt from "jsencrypt";

const encrypt = new JSEncrypt();
encrypt.setPublicKey(process.env.VUE_APP_PUBLIC_KEY);

export const encryptData = (data) => {
return encrypt.encrypt(JSON.stringify(data));
};

export const decryptData = (encryptedData) => {
const decrypt = new JSEncrypt();
decrypt.setPrivateKey(process.env.VUE_APP_PRIVATE_KEY);
return JSON.parse(decrypt.decrypt(encryptedData));
};

3. Vue 组件实现

<!-- ReportViewer.vue -->
<template>
<div class="report-viewer">
<div v-if="loading" class="loading">
<loading-spinner />
</div>
<template v-else>
<div class="report-header">
<h1>{{ report.title }}</h1>
<div class="meta-info">
<span>患者:{{ report.patientName }}</span>
<span>时间:{{ formatDate(report.timestamp) }}</span>
</div>
</div>

<div class="report-content">
<div class="waveform-container">
<waveform-chart :data="waveformData" />
</div>
<div class="report-text">{{ report.content }}</div>
</div>
</template>
</div>
</template>

<script>
import { initDB } from "@/utils/db";
import { decryptData } from "@/utils/encryption";
import WaveformChart from "./WaveformChart.vue";
import LoadingSpinner from "./LoadingSpinner.vue";

export default {
name: "ReportViewer",
components: {
WaveformChart,
LoadingSpinner,
},
data() {
return {
loading: true,
report: null,
waveformData: null,
db: null,
};
},
async created() {
try {
this.db = await initDB();
await this.loadReport();
} catch (error) {
console.error("Failed to load report:", error);
} finally {
this.loading = false;
}
},
methods: {
async loadReport() {
const reportId = this.$route.params.id;

// 从 IndexedDB 加载报告数据
const report = await this.getReportFromDB(reportId);
const waveform = await this.getWaveformFromDB(reportId);

// 解密数据
this.report = decryptData(report.encryptedData);
this.waveformData = decryptData(waveform.encryptedData);
},
async getReportFromDB(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(["reports"], "readonly");
const store = transaction.objectStore("reports");
const request = store.get(id);

request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
async getWaveformFromDB(reportId) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(["waveforms"], "readonly");
const store = transaction.objectStore("waveforms");
const index = store.index("reportId");
const request = index.get(reportId);

request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
formatDate(timestamp) {
return new Date(timestamp).toLocaleString();
},
},
};
</script>

<style scoped>
.report-viewer {
padding: 20px;
}

.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}

.report-header {
margin-bottom: 20px;
}

.meta-info {
color: #666;
font-size: 14px;
}

.meta-info span {
margin-right: 20px;
}

.waveform-container {
margin: 20px 0;
border: 1px solid #eee;
border-radius: 4px;
padding: 10px;
}

.report-text {
line-height: 1.6;
}
</style>

性能优化

1. 数据预加载

// preload.js
export const preloadReports = async (db, reportIds) => {
const transaction = db.transaction(["reports", "waveforms"], "readonly");
const reportStore = transaction.objectStore("reports");
const waveformStore = transaction.objectStore("waveforms");

const reports = await Promise.all(reportIds.map((id) => reportStore.get(id)));

const waveforms = await Promise.all(
reportIds.map((id) => {
const index = waveformStore.index("reportId");
return index.get(id);
})
);

return { reports, waveforms };
};

2. 数据分片存储

对于大型波形数据,采用分片存储策略:

// waveformStorage.js
const CHUNK_SIZE = 1024 * 1024; // 1MB per chunk

export const storeWaveform = async (db, reportId, waveformData) => {
const chunks = splitIntoChunks(waveformData, CHUNK_SIZE);

const transaction = db.transaction(["waveforms"], "readwrite");
const store = transaction.objectStore("waveforms");

for (let i = 0; i < chunks.length; i++) {
await store.put({
id: `${reportId}_${i}`,
reportId,
chunkIndex: i,
data: chunks[i],
});
}
};

const splitIntoChunks = (data, size) => {
const chunks = [];
for (let i = 0; i < data.length; i += size) {
chunks.push(data.slice(i, i + size));
}
return chunks;
};

使用效果

通过以上实现,我们达到了以下目标:

  1. 首屏加载时间 < 1s

    • 使用 IndexedDB 的索引功能实现快速检索
    • 数据预加载策略
    • 分片存储大型波形数据
  2. 完整的离线功能

    • 支持查看完整的报告内容
    • 包含波形图等复杂数据
    • 数据加密存储保证安全性
  3. 良好的用户体验

    • 流畅的加载过程
    • 响应式界面设计
    • 错误处理和加载状态提示

注意事项

  1. 定期清理过期数据,避免存储空间占用过大
  2. 实现数据同步机制,确保离线数据与服务器数据的一致性
  3. 考虑添加数据压缩功能,进一步优化存储空间
  4. 实现错误重试机制,提高系统稳定性