使用浏览器端IndexedDB存储配置文件

10 阅读3分钟

上一个工程中我们实现了一个基于next的索引页,现在需求是配置文件要持久化,这里我们使用IndexedDB存储我们的数据,并实现对配置文件的编辑。


1. 安装必要的依赖

我们需要一个工具来简化 IndexedDB 的操作,这里推荐使用 idb(一个轻量级的 IndexedDB 封装库)。

在项目根目录运行:

npm install idb

2. 创建 IndexedDB 工具函数

新建 utils/db.ts 文件,用于初始化和管理 IndexedDB:

// utils/db.ts
import { openDB, DBSchema, IDBPDatabase } from 'idb';
import { CardItem } from '../types/card';

interface MyDB extends DBSchema {
  cardConfig: {
    key: string;
    value: CardItem[];
  };
}

let dbPromise: Promise<IDBPDatabase<MyDB>> | null = null;

export const getDB = async () => {
  if (!dbPromise) {
    dbPromise = openDB<MyDB>('directoryDB', 1, {
      upgrade(db) {
        db.createObjectStore('cardConfig', { keyPath: 'key' });
      },
    });
  }
  return dbPromise;
};

// 初始化默认配置
export const initCardConfig = async (defaultConfig: CardItem[]) => {
  const db = await getDB();
  const tx = db.transaction('cardConfig', 'readwrite');
  const store = tx.objectStore('cardConfig');
  const existingConfig = await store.get('cards');
  if (!existingConfig) {
    await store.put({ key: 'cards', value: defaultConfig });
  }
  await tx.done;
};

// 获取配置
export const getCardConfig = async (): Promise<CardItem[]> => {
  const db = await getDB();
  const tx = db.transaction('cardConfig', 'readonly');
  const store = tx.objectStore('cardConfig');
  const config = await store.get('cards');
  return config?.value || [];
};

// 更新配置
export const updateCardConfig = async (config: CardItem[]) => {
  const db = await getDB();
  const tx = db.transaction('cardConfig', 'readwrite');
  const store = tx.objectStore('cardConfig');
  await store.put({ key: 'cards', value: config });
  await tx.done;
};

3. 修改 cardConfig.ts

将默认配置作为初始数据,不直接使用,而是通过 IndexedDB 获取:

// config/cardConfig.ts
import { CardItem } from '../types/card';

export const defaultCardConfig: CardItem[] = [
  {
    id: '1',
    title: '首页',
    description: '网站首页导航',
    url: '/',
    apiUrl: '/api/home',
    icon: 'home',
    urls: [
      { label: '管理后台', href: '/admin', port: 8080 },
      { label: '用户中心', href: '/user', port: 8081 },
    ],
  },
  {
    id: '2',
    title: '产品页面',
    description: '查看所有产品信息',
    url: '/products',
    apiUrl: '/api/products',
    icon: 'appstore',
    urls: [
      { label: '产品详情', href: '/products/detail', port: 8080 },
      { label: '产品API', href: '/api/products', port: 8082 },
    ],
  },
];

4. 修改主页面 index.tsx

pages/index.tsx 中使用 IndexedDB 获取数据,并添加编辑功能:

// pages/index.tsx
import { useState, useEffect } from 'react';
import { ConfigProvider, Button, Modal, Input, Space } from '@arco-design/web-react';
import CardList from '../components/CardList';
import { CardItem } from '../types/card';
import { getCardConfig, initCardConfig, updateCardConfig } from '../utils/db';
import { defaultCardConfig } from '../config/cardConfig';

export default function Home() {
  const [cards, setCards] = useState<CardItem[]>([]);
  const [isModalVisible, setIsModalVisible] = useState(false);
  const [editingCard, setEditingCard] = useState<CardItem | null>(null);

  // 初始化和加载数据
  useEffect(() => {
    const loadData = async () => {
      await initCardConfig(defaultCardConfig); // 初始化默认数据
      const config = await getCardConfig();
      setCards(config);
    };
    loadData();
  }, []);

  // 编辑卡片
  const handleEdit = (card: CardItem) => {
    setEditingCard({ ...card });
    setIsModalVisible(true);
  };

  // 保存编辑
  const handleSave = async () => {
    if (editingCard) {
      const updatedCards = cards.map((card) =>
        card.id === editingCard.id ? editingCard : card
      );
      await updateCardConfig(updatedCards);
      setCards(updatedCards);
      setIsModalVisible(false);
      setEditingCard(null);
    }
  };

  return (
    <ConfigProvider>
      <div style={{ padding: '20px' }}>
        <h1 style={{ marginBottom: '20px' }}>页面目录</h1>
        <CardList
          items={cards}
          onEdit={handleEdit} // 传递编辑回调
        />
        <Modal
          title="编辑卡片"
          visible={isModalVisible}
          onOk={handleSave}
          onCancel={() => setIsModalVisible(false)}
          style={{ width: 600 }}
        >
          {editingCard && (
            <Space direction="vertical" size="large" style={{ width: '100%' }}>
              <Input
                addonBefore="标题"
                value={editingCard.title}
                onChange={(value) => setEditingCard({ ...editingCard, title: value })}
              />
              <Input
                addonBefore="描述"
                value={editingCard.description}
                onChange={(value) => setEditingCard({ ...editingCard, description: value })}
              />
              <Input
                addonBefore="主URL"
                value={editingCard.url}
                onChange={(value) => setEditingCard({ ...editingCard, url: value })}
              />
              {/* 可以继续添加其他字段的编辑 */}
            </Space>
          )}
        </Modal>
      </div>
    </ConfigProvider>
  );
}

5. 修改 CardListCardItem 组件

调整 CardListCardItem,添加编辑按钮:

CardList.tsx

// components/CardList.tsx
import { Grid } from '@arco-design/web-react';
import CardItem from './CardItem';
import { CardItem as CardItemType } from '../types/card';

interface CardListProps {
  items: CardItemType[];
  onEdit?: (card: CardItemType) => void; // 新增编辑回调
}

const Row = Grid.Row;
const Col = Grid.Col;

const CardList: React.FC<CardListProps> = ({ items, onEdit }) => {
  return (
    <Row gutter={[20, 20]}>
      {items.map((item) => (
        <Col key={item.id} span={8}>
          <CardItem item={item} onEdit={onEdit} />
        </Col>
      ))}
    </Row>
  );
};

export default CardList;

CardItem.tsx

// components/CardItem.tsx
import { Card, Button, Space } from '@arco-design/web-react';
import { CardItem } from '../types/card';

interface CardItemProps {
  item: CardItem;
  onEdit?: (card: CardItem) => void; // 新增编辑回调
}

const CardItem: React.FC<CardItemProps> = ({ item, onEdit }) => {
  const getMainUrl = () => {
    if (typeof window === 'undefined') {
      return `http://localhost:3000${item.url}`;
    }
    const host = window.location.hostname;
    const protocol = window.location.protocol;
    const port = window.location.port || '3000';
    return `${protocol}//${host}:${port}${item.url}`;
  };

  const getUrl = (urlItem: { href: string; port?: number }) => {
    if (typeof window === 'undefined') {
      return `http://localhost:${urlItem.port || 3000}${urlItem.href}`;
    }
    const host = window.location.hostname;
    const protocol = window.location.protocol;
    const port = urlItem.port || window.location.port || 3000;
    return `${protocol}//${host}:${port}${urlItem.href}`;
  };

  return (
    <Card
      hoverable
      style={{ width: 300, margin: '0 20px 20px 0' }}
      title={
        <a href={getMainUrl()} target="_blank" rel="noopener noreferrer">
          {item.title}
        </a>
      }
      extra={
        onEdit && (
          <Button type="text" onClick={() => onEdit(item)}>
            编辑
          </Button>
        )
      }
    >
      <Card.Meta description={item.description} />
      {item.apiUrl && (
        <div style={{ marginTop: 10 }}>
          API: <span style={{ color: '#666' }}>{item.apiUrl}</span>
        </div>
      )}
      {item.urls && item.urls.length > 0 && (
        <Space direction="vertical" style={{ marginTop: 10 }}>
          {item.urls.map((urlItem, index) => (
            <Button
              key={index}
              type="primary"
              size="small"
              href={getUrl(urlItem)}
              target="_blank"
              rel="noopener noreferrer"
            >
              {urlItem.label}
            </Button>
          ))}
        </Space>
      )}
    </Card>
  );
};

export default CardItem;

6. 功能说明

  • IndexedDB 存储
    • 项目启动时,initCardConfig 检查 IndexedDB 中是否已有数据,没有则初始化默认配置。
    • getCardConfig 获取存储的数据,updateCardConfig 更新数据。
  • 编辑功能
    • 点击卡片右上角的“编辑”按钮,弹出模态框。
    • 在模态框中修改 titledescriptionurl,点击“确定”保存到 IndexedDB。
    • 当前仅实现了基本字段的编辑,你可以根据需要扩展其他字段(如 urls)。

7. 运行和测试

  1. 运行项目:
    npm run dev
    
  2. 打开浏览器开发者工具,检查 Application -> IndexedDB,确认 directoryDB 已创建。

以上就完成了对IndexedDB的基本使用。