基于Next api与本地文件存储前端配置项

上一篇文章中我们使用了浏览器IndexedDB 存储了 cardConfig 的客户端数据,在实际生产更多的场景需要根据用户获取配置数据,因此更合适的是将数据放在服务端。 因此我们做以下变更:

  1. 搭建服务端:使用 Next.js 的 API 路由。
  2. 选择存储方式:服务端选择文件存储数据。
  3. 修改前端代码:通过 API 请求从服务端获取和更新数据,而不是直接操作 IndexedDB。
  4. 部署调整:确保服务端在生产环境中正确运行。

以下是一个基于 Next.js API 路由和文件系统存储的实现方案,简单且无需额外依赖数据库。


1. 创建服务端 API 路由

Next.js 支持在 pages/api 目录下创建 API 路由。我们将创建一个 API 来处理 cardConfig 的获取和更新。

创建 API 文件

在 pages/api/cardConfig.ts 中添加以下代码:

typescript 复制代码
// pages/api/cardConfig.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs/promises';
import path from 'path';
import { CardItem } from '../../types/card';
import { defaultCardConfig } from '../../config/cardConfig';

// 数据存储路径
const dataFilePath = path.join(process.cwd(), 'data', 'cardConfig.json');

// 初始化数据文件(如果不存在)
const initData = async () => {
  try {
    await fs.access(dataFilePath);
  } catch {
    await fs.mkdir(path.dirname(dataFilePath), { recursive: true });
    await fs.writeFile(dataFilePath, JSON.stringify(defaultCardConfig, null, 2));
  }
};

// 获取配置
const getConfig = async (): Promise<CardItem[]> => {
  await initData();
  const data = await fs.readFile(dataFilePath, 'utf-8');
  return JSON.parse(data);
};

// 更新配置
const updateConfig = async (config: CardItem[]) => {
  await fs.writeFile(dataFilePath, JSON.stringify(config, null, 2));
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    if (req.method === 'GET') {
      const config = await getConfig();
      res.status(200).json(config);
    } else if (req.method === 'POST') {
      const newConfig: CardItem[] = req.body;
      await updateConfig(newConfig);
      res.status(200).json({ message: 'Config updated successfully' });
    } else {
      res.status(405).json({ message: 'Method not allowed' });
    }
  } catch (error) {
    res.status(500).json({ message: 'Server error', error: (error as Error).message });
  }
}
  • 说明 :
    • GET /api/cardConfig:获取当前配置。
    • POST /api/cardConfig:更新配置。
    • 数据存储在项目根目录下的 data/cardConfig.json 文件中。
    • 如果文件不存在,首次请求时会用 defaultCardConfig 初始化。

创建初始数据目录

确保项目根目录下有 data 文件夹,或者让代码自动创建。如果你手动创建,可以添加一个空的 data/cardConfig.json 文件,或者让程序初始化。


2. 修改前端代码

将 pages/index.tsx 从 IndexedDB 操作改为通过 API 获取和更新数据。

typescript 复制代码
// 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";

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

  // 获取数据
  const fetchConfig = async () => {
    const res = await fetch("/api/cardConfig");
    const data = await res.json();
    setCards(data);
  };

  // 更新数据
  const updateConfig = async (newConfig: CardItem[]) => {
    const res = await fetch("/api/cardConfig", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(newConfig),
    });
    if (res.ok) {
      setCards(newConfig);
    } else {
      console.error("Failed to update config");
    }
  };

  // 初始化加载数据
  useEffect(() => {
    fetchConfig();
  }, []);

  // 编辑卡片
  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 updateConfig(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
                prefix="标题"
                value={editingCard.title}
                onChange={(value) =>
                  setEditingCard({ ...editingCard, title: value })
                }
              />
              <Input
                prefix="描述"
                value={editingCard.description}
                onChange={(value) =>
                  setEditingCard({ ...editingCard, description: value })
                }
              />
              <Input
                prefix="主URL"
                value={editingCard.url}
                onChange={(value) =>
                  setEditingCard({ ...editingCard, url: value })
                }
              />
            </Space>
          )}
        </Modal>
      </div>
    </ConfigProvider>
  );
}
  • 说明 :
    • fetchConfig:从 /api/cardConfig 获取数据。
    • updateConfig:通过 POST 请求更新服务端数据。

3. 测试本地运行

  1. 运行项目:

    bash 复制代码
    npm run dev
  2. 访问 http://localhost:3000,检查卡片数据是否正确加载。

  3. 编辑一个卡片,保存后检查 data/cardConfig.json 是否更新。

相关推荐
deli00737 分钟前
32K 上下文塞满后,AI 最先忘掉什么?120 轮对话实测
前端
deli0072 小时前
手机拍灯为什么有一条条黑纹?PWM 240Hz 实测
前端
wangruofeng6 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng6 小时前
给前端开发者的 100 个设计参考网站
前端·设计
星栈10 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技10 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户479492835691510 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马12 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng13 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding14 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js