我如何用 4 行 JSON 让我的作品集实现无限可扩展

发布: (2026年2月21日 GMT+8 23:03)
7 分钟阅读
原文: Dev.to

Source: Dev.to

Sammii

向我的作品集添加一个新项目只需 30 秒。无需更改组件。无需新增路由。无需布局调整。只需在文件夹中放入 4 行 JSON 和一张图片。

我已经厌倦了典型的作品集维护循环:先做一个酷炫的项目,然后花一个小时把它接入作品集网站,调整布局,确保新卡片不会破坏网格。因此,我从一开始就把我的作品集设计成完全数据驱动的。

下面是它的工作原理。

Source:

单一真实来源

我作品集中的每个项目都存放在同一个文件中:projects.js。每个项目都是一个仅包含 4 个字段的对象:

{
  id: 'crystal-index',
  title: 'Crystal Index',
  techStack: 'TypeScript, Next.js, Prisma, SQL, GPT4, React 3 Fiber',
  info: 'Custom CMS for cataloguing crystals with structured filters for colour, chakra, and properties, and GPT-4-generated descriptions.',
}

仅此而已。四行代码。整个作品集就是从这些对象组成的数组渲染出来的。

ID 的三重职责

id 字段是设计的有趣之处。它不仅仅是一个标识符;它同时承担三种功能:

  1. GitHub 链接路径 – 作品集通过将 ID 追加到基础 GitHub URL 来构建仓库链接。
    crystal-indexhttps://github.com/sammii-hk/crystal-index.

  2. 图片文件名查找 – 自动生成的图片映射将 ID 解析为对应的图片文件及其扩展名。
    crystal-index/assets/images/crystal-index.jpg.

  3. React 键 – 在遍历数组时,ID 充当唯一键。

一个字段,三项任务。这消除了冗余数据,并确保 GitHub 链接、图片和 React 键始终保持同步。

对于属于 GitHub 组织的项目,ID 包含组织路径,例如 unicorn-poo/succulent。图片工具会在 / 上拆分,并使用最后一个段作为文件名查找,而完整路径则用于构建正确的 GitHub URL。

自动生成的图片映射

我不想手动跟踪每个项目截图是 .jpg 还是 .png。于是我写了一个构建脚本,扫描 images 目录并生成 JSON 映射:

import { readdir, writeFile } from 'fs/promises';
import { join } from 'path';

const imagesDir = join(process.cwd(), 'public', 'assets', 'images');

async function generateImageMap() {
  const files = await readdir(imagesDir);
  const imageMap = {};

  files.forEach(file => {
    if (file === 'sammii.png') return;
    const name = file.replace(/\.(jpg|png)$/, '');
    const ext = file.endsWith('.png') ? 'png' : 'jpg';
    if (!imageMap[name]) imageMap[name] = ext;
  });

  const outputPath = join(process.cwd(), 'app', 'common', 'utils', 'image-map.json');
  await writeFile(outputPath, JSON.stringify(imageMap, null, 2));
}

generateImageMap();

输出是一个简单的查找表:

{
  "crystal-index": "jpg",
  "lunary": "png",
  "succulent": "png",
  "day-lite": "jpg"
}

一个实用函数可以将任意项目 ID 解析为完整的图片路径:

import imageMapData from './image-map.json';
const imageMap = imageMapData;

export const getImagePath = (projectId) => {
  const projectBaseId = projectId.split('/').pop() || projectId;
  const extension = imageMap[projectBaseId] || 'jpg';
  return `/assets/images/${projectBaseId}.${extension}`;
};

把图片放进文件夹,运行脚本,作品集就会自动识别并使用它们。

零组件更改

该作品集拥有两种完全不同的视图模式——响应式网格(点击展开模态框)和全屏垂直轮播。两者都使用完全相同的 projects 数组。

网格视图 对数组进行映射并渲染卡片:

{projects.map(project => (
   setSelectedProject(project)}>
    
  
))}

轮播视图 对同一数组进行映射并渲染全宽幻灯片:

 (
    
      
    
  )}
/>

ProjectItem 接收一个 isGrid 属性,用于在紧凑卡片布局和展开布局之间切换。相同的组件、相同的数据、两种呈现方式。向数组中添加项目后,它会自动出现在两种视图中,无需额外工作。

30秒工作流

当我完成一个新项目时,我会执行以下操作:

  1. 截取屏幕截图。
  2. 将其放入 /public/assets/images/,命名为 project-name.png
  3. 运行 node scripts/generate-image-map.mjs
  4. projects.js 中添加 4 行代码。
  5. 推送到 GitHub。

作品集会重新构建,新项目会出现在两个视图中,显示正确的图片、正确的 GitHub 链接以及正确的布局。五步、三十秒,未触及任何组件文件。

Source:

背后的哲学

这不仅仅是为了在作品集中节省时间。这是我在各处使用的模式。

我的占星应用 Lunary 拥有一个包含超过 2,000 篇文章的法典。它们以结构化数据存储,并通过共享组件渲染。添加关于水晶或塔罗牌的新文章时,无需触碰任何 UI 代码。

我的发布工具 Spellcast 管理多个社交媒体账号和平台。账号配置是数据对象。添加新平台只需在配置中加入,而不是重建界面。

原则始终如一:将数据与表现层分离。让数据结构承担大部分工作。保持组件足够通用,以至于它们永远不需要了解具体内容。

如果你在为作品集接线新内容上花的时间比构建项目本身还多,那说明你的架构方向弄反了。让你的作品集为你服务,而不是相反。

我是 Sammii,Lunary 的创始人,这是一款真正教你阅读自己出生图的占星应用。我会写关于作为独立开发者构建产品的技术决策。关注我的 Dev.to 或在 GitHub 查看代码。

0 浏览
Back to Blog

相关文章

阅读更多 »

Undefined 与 Not Defined

Undefined undefined 是 JavaScript 中的一个特殊关键字。它表示变量已经在内存中存在,但尚未被赋值。在创建阶段…