Compare commits
5 Commits
22e7050b23
...
3fedd45180
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fedd45180 | ||
|
|
b425ac4708 | ||
|
|
4a4fd423b2 | ||
|
|
eda4430fa5 | ||
|
|
4a9fe4bfc5 |
@@ -1,268 +0,0 @@
|
||||
# 项目说明文档
|
||||
|
||||
本文档旨在提供对 `zhaoguiyang.site` 项目的全面理解,包括其技术栈、框架、组件库、目录结构等,以便指导 AI 大模型进行代码重构和功能开发。
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
`zhaoguiyang.site` 是一个现代化的个人作品集网站模板,具有动画和玻璃拟态效果。该项目旨在展示个人信息、工作经历、技能和项目,并包含完整的博客功能,支持多语言(英文和中文)。网站采用响应式设计,适配各种设备尺寸,并支持亮色/暗色主题切换。
|
||||
|
||||
## 2. 技术栈和框架
|
||||
|
||||
- **主要框架**: [Astro](https://astro.build/) - 一个用于构建快速、内容驱动的网站的现代前端框架。它允许使用多种 UI 框架(如 React, Vue, Svelte 等)并支持服务器端渲染 (SSR) 和静态站点生成 (SSG)。项目配置在 `astro.config.mjs` 中,包含了对 React、TailwindCSS、MDX 和国际化的支持。
|
||||
- **UI 框架**: [React](https://react.dev/) - 用于构建用户界面的 JavaScript 库。在本项目中,交互性组件(如头部导航、主题切换、语言切换、动画效果等)是使用 React 实现的,并通过 Astro 的集成功能嵌入到页面中。
|
||||
- **样式**: [Tailwind CSS](https://tailwindcss.com/) - 一个实用工具优先的 CSS 框架,用于快速构建自定义用户界面。通过 `@tailwindcss/vite` 集成到 Astro 项目中。项目还使用了 `@tailwindcss/typography` 插件来美化博客文章内容。全局样式定义在 `src/styles/global.css` 中,包含了亮色和暗色主题的颜色变量。
|
||||
- **动画**: [Framer Motion](https://www.framer.com/motion/) (`framer-motion`) - 一个用于 React 的生产级动画库,用于实现声明式的、基于物理的动画以及复杂的交互动效。在项目中,它被用于增强用户体验,例如在页面元素中实现入场动画、悬停效果等。具体使用方式可以参考 `src/components/MotionWrapper.tsx` 组件,它是一个统一处理动画逻辑的封装,提供了淡入和Y轴位移动画效果。
|
||||
- **图标**: [Lucide React](https://lucide.dev/) - 一个简单、美观的 SVG 图标库,用于提供界面图标。
|
||||
- **包管理器**: [pnpm](https://pnpm.io/) - 一个快速、磁盘空间高效的包管理器。
|
||||
- **TypeScript**: 用于类型检查和提高代码质量。项目使用严格的 TypeScript 配置,类型定义集中在 `src/types/index.ts` 文件中。
|
||||
- **国际化**: 使用自定义的国际化解决方案,通过 `src/i18n` 目录下的文件管理多语言内容。`ui.ts` 定义了翻译文本,`utils.ts` 提供了国际化工具函数,如 `useTranslations` 和 `getLocalizedPath`。
|
||||
- **内容管理**: 使用 MDX 进行博客文章的编写,支持 Markdown 语法和 React 组件的混合使用。
|
||||
- **UI 组件增强**: 使用 Radix UI 的 `@radix-ui/react-scroll-area` 和 `@radix-ui/react-slot` 组件来增强用户界面的可访问性和交互性。
|
||||
|
||||
## 3. 组件库和 UI
|
||||
|
||||
- **核心组件**: 项目包含多个自定义 React 组件,位于 `src/components/` 目录下:
|
||||
- `GlassHeader.tsx`: 玻璃拟态效果的导航栏,实现了响应式设计、语言切换、主题切换、滚动效果和移动端菜单。
|
||||
- `Footer.tsx`: 页脚组件,包含社交媒体链接和版权信息。
|
||||
- `LanguageSwitcher.tsx`: 语言切换组件,支持英文和中文切换,使用 Framer Motion 实现动画效果。
|
||||
- `TypewriterEffect.tsx`: 打字机效果组件,支持单文本或文本数组的循环显示、自定义打字/删除速度、延迟和光标样式。
|
||||
- `SkillsMarquee.tsx`: 技能标签滚动展示组件,使用 Framer Motion 实现无限滚动效果,支持自定义滚动方向和速度。
|
||||
- `MotionWrapper.tsx`: 动画包装组件,为子组件添加基于 Framer Motion 的动画效果,包括淡入和Y轴位移。
|
||||
- `AuthorCard.tsx`: 作者信息卡片,用于博客文章页面,显示作者头像、姓名、简介和社交媒体链接。
|
||||
- `ShareButtons.tsx`: 社交媒体分享按钮组件,用于博客文章页面,支持 Twitter、LinkedIn、Facebook 分享和链接复制。
|
||||
- **博客组件 (src/components/blog/)**: 专门用于博客功能的组件集合:
|
||||
- `BlogList.astro`: 博客文章列表组件,支持分类和标签筛选,展示文章标题、描述、特色图片和阅读链接。
|
||||
- `CategoryCard.astro`: 博客分类卡片组件,用于展示和筛选文章分类,支持多语言。
|
||||
- `TagCard.astro`: 博客标签卡片组件,用于展示和筛选文章标签,支持多语言。
|
||||
- `PostMeta.astro`: 博客文章元数据组件,展示发布日期、阅读时间等信息。
|
||||
- **布局组件 (src/layouts/)**: 用于页面布局的组件:
|
||||
- `Layout.astro`: 基础布局组件,定义了页面的基本HTML结构、元数据、字体引入、全局CSS样式和主题切换逻辑。
|
||||
- `BlogLayout.astro`: 博客页面布局组件,继承自 Layout,添加了博客特定的样式和结构。
|
||||
- `BlogPostLayout.astro`: 博客文章页面布局组件,用于展示单篇博客文章,集成了作者信息、目录导航和文章导航功能。
|
||||
- **UI 组件 (src/components/ui/)**: 基于 Shadcn UI 风格,提供可复用的基础 UI 元素:
|
||||
- `button.tsx`: 按钮组件,支持多种变体和尺寸。
|
||||
- `glass-card.tsx`: 自定义玻璃拟态效果的卡片组件,使用 Framer Motion 实现悬停动画效果。
|
||||
- `theme-toggle.tsx`: 主题切换组件,实现亮色/暗色模式切换功能。
|
||||
- `Container.tsx`: 容器组件,用于统一页面内容的宽度和边距。
|
||||
- **工具函数 (src/utils/)**: 提供各种实用功能:
|
||||
- `blog-utils.ts`: 博客相关工具函数,包括获取文章、排序、按分类和标签过滤等功能。
|
||||
- `lib/utils.ts`: 通用工具函数,如类名合并等。
|
||||
- **数据管理 (src/lib/)**:
|
||||
- `data.ts`: 集中管理个人信息和项目数据,支持多语言。
|
||||
- **UI 辅助库**:
|
||||
- `class-variance-authority`: 用于创建可变样式的组件。
|
||||
- `clsx` 和 `tailwind-merge`: 用于有条件地组合和合并 Tailwind CSS 类名。
|
||||
- `tw-animate-css`: 用于集成 Animate.css 的 Tailwind CSS 插件。
|
||||
|
||||
## 4. 目录结构
|
||||
|
||||
```
|
||||
zhaoguiyang.site/
|
||||
├── .github/ # GitHub 相关配置
|
||||
├── public/ # 静态资源
|
||||
│ ├── assets/ # 图片、字体等资源
|
||||
│ └── favicon.svg # 网站图标
|
||||
├── src/ # 源代码
|
||||
│ ├── components/ # React 组件
|
||||
│ │ ├── blog/ # 博客相关组件
|
||||
│ │ │ ├── BlogList.astro # 博客文章列表组件
|
||||
│ │ │ ├── CategoryCard.astro # 分类卡片组件
|
||||
│ │ │ ├── PostMeta.astro # 文章元数据组件
|
||||
│ │ │ └── TagCard.astro # 标签卡片组件
|
||||
│ │ ├── ui/ # UI 组件
|
||||
│ │ │ ├── button.tsx # 按钮组件
|
||||
│ │ │ ├── Container.tsx # 容器组件
|
||||
│ │ │ ├── glass-card.tsx # 玻璃效果卡片组件
|
||||
│ │ │ └── theme-toggle.tsx # 主题切换组件
|
||||
│ │ ├── AuthorCard.tsx # 作者信息卡片组件
|
||||
│ │ ├── Footer.tsx # 页脚组件
|
||||
│ │ ├── GlassHeader.tsx # 玻璃效果导航栏组件
|
||||
│ │ ├── LanguageSwitcher.tsx # 语言切换组件
|
||||
│ │ ├── MotionWrapper.tsx # 动画包装组件
|
||||
│ │ ├── ShareButtons.tsx # 社交分享按钮组件
|
||||
│ │ ├── SkillsMarquee.tsx # 技能标签滚动组件
|
||||
│ │ └── TypewriterEffect.tsx # 打字机效果组件
|
||||
│ ├── i18n/ # 国际化相关
|
||||
│ │ ├── ui.ts # UI 文本翻译
|
||||
│ │ └── utils.ts # 国际化工具函数
|
||||
│ ├── layouts/ # Astro 布局组件
|
||||
│ │ ├── BlogLayout.astro # 博客页面布局
|
||||
│ │ ├── BlogPostLayout.astro # 博客文章页面布局
|
||||
│ │ └── Layout.astro # 基础页面布局
|
||||
│ ├── lib/ # 工具库
|
||||
│ │ ├── data.ts # 个人信息和项目数据
|
||||
│ │ └── utils.ts # 通用工具函数
|
||||
│ ├── pages/ # 页面
|
||||
│ │ ├── blog/ # 博客页面
|
||||
│ │ │ ├── index.astro # 博客首页
|
||||
│ │ │ ├── category/ # 分类页面
|
||||
│ │ │ ├── tag/ # 标签页面
|
||||
│ │ │ └── posts/ # 博客文章
|
||||
│ │ ├── projects.astro # 项目页面
|
||||
│ │ ├── index.astro # 首页
|
||||
│ │ └── zh/ # 中文页面
|
||||
│ │ ├── blog/ # 中文博客页面
|
||||
│ │ │ ├── index.astro # 中文博客首页
|
||||
│ │ │ ├── category/ # 中文分类页面
|
||||
│ │ │ ├── tag/ # 中文标签页面
|
||||
│ │ │ └── posts/ # 中文博客文章
|
||||
│ │ ├── projects.astro # 中文项目页面
|
||||
│ │ └── index.astro # 中文首页
|
||||
│ ├── styles/ # 样式
|
||||
│ │ └── global.css # 全局样式
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ │ └── index.ts # 类型定义文件
|
||||
│ └── utils/ # 工具函数
|
||||
│ └── blog-utils.ts # 博客相关工具函数
|
||||
├── .gitignore # Git 忽略文件
|
||||
├── astro.config.mjs # Astro 配置
|
||||
├── package.json # 项目依赖
|
||||
├── pnpm-lock.yaml # pnpm 锁文件
|
||||
├── tailwind.config.cjs # Tailwind CSS 配置
|
||||
└── tsconfig.json # TypeScript 配置
|
||||
```
|
||||
|
||||
## 5. 关键文件说明
|
||||
|
||||
- **astro.config.mjs**: Astro 配置文件,定义了项目的构建设置、集成(React、TailwindCSS、MDX)和国际化支持。
|
||||
- **tailwind.config.cjs**: Tailwind CSS 配置文件,定义了自定义主题、颜色、动画、插件和其他 Tailwind 相关设置。
|
||||
- **src/i18n/ui.ts**: 包含所有 UI 文本的翻译,按语言(英文和中文)组织,包括导航、按钮、标题和描述等文本。
|
||||
- **src/i18n/utils.ts**: 提供国际化相关的实用函数,如 `useTranslations`(获取翻译文本)和 `getLocalizedPath`(处理多语言路径)。
|
||||
- **src/lib/data.ts**: 集中管理个人信息(姓名、位置、邮箱、社交媒体链接、职位、描述、关于、统计数据、技能等)和项目数据(标题、描述、图片、链接、技术栈等),支持多语言。
|
||||
- **src/lib/utils.ts**: 包含通用工具函数,如 `cn`(合并类名)等。
|
||||
- **src/utils/blog-utils.ts**: 提供博客相关工具函数,包括根据语言获取文章、按日期排序、按类别和标签过滤文章等功能。
|
||||
- **src/styles/global.css**: 全局 CSS 样式,包括 Tailwind 指令、自定义 CSS 变量(亮色和暗色主题的颜色方案)和博客相关样式。
|
||||
- **src/types/index.ts**: 集中定义项目中使用的 TypeScript 类型和接口,如 `Project`、`Author`、`SocialLink` 等。
|
||||
|
||||
## 6. 国际化实现
|
||||
|
||||
项目使用自定义的国际化解决方案,主要包括以下部分:
|
||||
|
||||
1. **语言定义**: 在 `src/i18n/ui.ts` 中定义所有支持的语言(英文和中文)和翻译文本,使用 TypeScript 类型确保翻译的完整性。
|
||||
2. **工具函数**:
|
||||
- `src/i18n/utils.ts` 中的 `useTranslations` 函数用于获取当前语言的翻译文本
|
||||
- `getLocalizedPath` 函数用于处理多语言路径,根据当前语言生成正确的 URL
|
||||
3. **目录结构**: 使用 `/zh/` 前缀区分中文页面,默认路径为英文页面。所有页面内容都有对应的中英文版本。
|
||||
4. **语言切换**: 通过 `LanguageSwitcher.tsx` 组件实现语言切换功能,使用 Framer Motion 实现动画效果,并根据选择的语言更新 URL 路径。
|
||||
5. **数据国际化**: 在 `src/lib/data.ts` 中,所有个人信息和项目数据都支持多语言,根据当前语言显示相应的内容。
|
||||
|
||||
## 7. 数据流和状态管理
|
||||
|
||||
项目使用简单的数据流模式,主要通过以下方式管理数据和状态:
|
||||
|
||||
1. **静态数据**: 大部分内容(如个人信息、工作经历、技能、项目)存储在 `src/lib/data.ts` 中,作为静态数据导出,支持多语言。
|
||||
2. **国际化文本**: UI 文本存储在 `src/i18n/ui.ts` 中,通过 `useTranslations` 钩子在组件中获取当前语言的翻译文本。
|
||||
3. **主题状态**: 使用 `localStorage` 和 DOM 操作管理主题状态(亮色/暗色),通过 `theme-toggle.tsx` 组件实现切换功能,并在页面加载时通过 `useEffect` 钩子恢复主题设置。
|
||||
4. **博客文章**: 使用 Markdown/MDX 文件存储博客文章内容,通过 Astro 的 `import.meta.glob` 在构建时处理,支持按分类和标签筛选。
|
||||
5. **组件状态**: 使用 React 的 `useState` 和 `useEffect` 钩子管理组件内部状态,如打字机效果的当前文本、技能标签滚动的动画控制等。
|
||||
6. **动画状态**: 使用 Framer Motion 的 `animate`、`initial`、`whileHover` 等属性和 `useAnimationControls` 钩子管理动画状态。
|
||||
|
||||
项目没有使用复杂的状态管理库(如 Redux 或 MobX),因为大部分内容是静态的,不需要复杂的状态管理。组件之间的通信主要通过 props 传递和上下文(如当前语言)实现。
|
||||
|
||||
## 8. 博客功能
|
||||
|
||||
博客功能主要通过以下方式实现:
|
||||
|
||||
1. **内容管理**: 使用 Markdown/MDX 文件存储博客文章,位于 `src/pages/blog/posts/` 和 `src/pages/zh/blog/posts/` 目录,支持 Markdown 语法和 React 组件的混合使用。
|
||||
2. **文章列表**: 通过 `src/pages/blog/index.astro` 和 `src/pages/zh/blog/index.astro` 实现博客文章列表页面,使用 `import.meta.glob` 读取所有文章,并处理文章数据。
|
||||
3. **分类和标签**: 支持按分类和标签筛选文章,通过 `CategoryCard.astro` 和 `TagCard.astro` 组件实现,从所有文章中提取分类和标签,并生成对应的链接。
|
||||
4. **文章元数据**: 每篇文章包含标题、描述、日期、作者、分类、标签、特色图片等元数据,通过 Markdown frontmatter 定义。
|
||||
5. **阅读时间**: 自动计算文章的阅读时间,通过 `PostMeta.astro` 组件显示。
|
||||
6. **作者信息**: 通过 `AuthorCard.tsx` 组件显示作者信息,包括头像、姓名、简介和社交媒体链接。
|
||||
7. **社交分享**: 通过 `ShareButtons.tsx` 组件实现社交媒体分享功能,支持 Twitter、LinkedIn、Facebook 分享和链接复制。
|
||||
8. **博客工具函数**: 在 `src/utils/blog-utils.ts` 中提供博客相关工具函数,包括获取文章、排序、按类别和标签过滤等功能。
|
||||
|
||||
## 9. 项目功能
|
||||
|
||||
项目展示功能主要通过以下方式实现:
|
||||
|
||||
1. **项目数据**: 在 `src/lib/data.ts` 中定义项目数据,包括标题、描述、图片、链接、技术栈等信息,支持多语言。
|
||||
2. **项目列表**: 通过 `src/pages/projects.astro` 和 `src/pages/zh/projects.astro` 实现项目列表页面,根据当前语言获取项目数据。
|
||||
3. **项目卡片**: 使用 `glass-card.tsx` 组件展示项目信息,实现玻璃拟态效果和悬停动画效果,使用 Framer Motion 进行动画处理。
|
||||
4. **响应式设计**: 项目列表页面采用响应式设计,在不同设备尺寸下自动调整布局和显示方式。
|
||||
|
||||
## 10. 主题切换
|
||||
|
||||
项目支持亮色/暗色主题切换,主要通过以下方式实现:
|
||||
|
||||
1. **主题定义**: 在 `src/styles/global.css` 中定义亮色和暗色主题的 CSS 变量,包括背景色、文本色、边框色等。
|
||||
2. **主题切换组件**: 通过 `src/components/ui/theme-toggle.tsx` 组件实现主题切换功能,通过操作 `document.documentElement` 的 `classList` 来改变主题。
|
||||
3. **主题持久化**: 使用 `localStorage` 存储用户的主题偏好,在页面加载时通过 `useEffect` 钩子恢复主题设置。
|
||||
4. **系统主题检测**: 支持检测系统主题偏好,并自动应用相应的主题。
|
||||
|
||||
## 11. 特殊效果和动画
|
||||
|
||||
项目使用多种技术实现特殊效果和动画,主要包括:
|
||||
|
||||
1. **玻璃拟态效果**: 通过 `glass-card.tsx` 和 `GlassHeader.tsx` 组件实现玻璃拟态效果,使用 CSS 的 `backdrop-filter` 和 `background-color` 属性。
|
||||
2. **打字机效果**: 通过 `TypewriterEffect.tsx` 组件实现打字机效果,支持单文本或文本数组的循环显示、自定义打字/删除速度、延迟和光标样式,使用 Framer Motion 和 React hooks 实现。
|
||||
3. **技能标签滚动**: 通过 `SkillsMarquee.tsx` 组件实现技能标签的无限滚动效果,使用 Framer Motion 的 `animate` 和 `useAnimationControls` 实现,支持自定义滚动方向和速度。
|
||||
4. **动画包装**: 通过 `MotionWrapper.tsx` 组件为子组件添加基于 Framer Motion 的动画效果,包括淡入和Y轴位移,提供统一的动画处理方式。
|
||||
5. **悬停动画**: 多个组件(如 `glass-card.tsx`、按钮等)实现了悬停动画效果,通过 Framer Motion 的 `whileHover` 属性实现。
|
||||
6. **页面过渡**: 页面元素的入场动画,通过 Framer Motion 的 `initial`、`animate` 和 `exit` 属性实现。
|
||||
|
||||
|
||||
|
||||
|
||||
## 6. 数据流和状态管理
|
||||
|
||||
- **静态数据驱动**:项目主要依赖 `src/lib/data.ts` 中的静态数据来渲染内容。这意味着大部分内容在构建时就已经确定,无需复杂的运行时数据获取或状态管理。
|
||||
- **博客内容管理**:博客文章使用 Markdown 文件存储在 `src/pages/blog/posts/` 和 `src/pages/zh/blog/posts/` 目录中,通过 Astro 的 `import.meta.glob` 功能在构建时读取和处理。
|
||||
- **无复杂状态管理**:由于项目性质(个人简历/作品集/博客),没有引入 Redux、Zustand 或 React Context 等复杂的状态管理库。组件的状态管理主要通过 React 自身的 `useState` 和 `useEffects` Hook 在组件内部完成,或者通过 props 从父组件传递。
|
||||
- **国际化数据流**:`src/i18n/ui.ts` 提供翻译文本,`src/i18n/utils.ts` 提供辅助函数来处理语言切换和文本获取,确保多语言内容正确显示。
|
||||
|
||||
## 7. 博客功能
|
||||
|
||||
项目实现了完整的博客功能,主要特点包括:
|
||||
|
||||
- **Markdown 支持**:使用 Astro 的 Markdown 集成,支持在 Markdown 文件中编写博客文章。
|
||||
- **前置元数据**:通过 Markdown 文件的 frontmatter 部分定义文章的标题、描述、发布日期、标签、分类等元数据。
|
||||
- **多语言支持**:博客文章支持英文和中文两种语言,分别存储在不同的目录中。
|
||||
- **分类和标签**:支持对文章进行分类和添加标签,并提供按分类/标签筛选文章的功能。
|
||||
- **文章导航**:在文章页面提供上一篇/下一篇导航功能,方便读者浏览相关内容。
|
||||
- **目录导航**:自动生成文章目录,支持点击跳转到对应章节。
|
||||
- **作者信息**:在文章页面展示作者信息,包括头像、简介和社交媒体链接。
|
||||
- **分享功能**:提供社交媒体分享按钮,方便读者分享文章。
|
||||
- **响应式设计**:博客页面适配各种屏幕尺寸,提供良好的移动端体验。
|
||||
|
||||
## 8. 构建和部署
|
||||
|
||||
- **构建命令**:项目使用 `pnpm run build` 命令进行构建。此命令会触发 Astro 的构建过程,将项目编译为静态文件,输出到 `dist/` 目录。
|
||||
- **开发环境**:使用 `pnpm run dev` 启动开发服务器,支持热重载和实时预览。
|
||||
- **预览构建**:使用 `pnpm run preview` 预览构建后的静态文件。
|
||||
- **部署**:由于是静态网站,构建产物可以直接部署到任何静态文件托管服务,如 Netlify, Vercel, GitHub Pages 等。
|
||||
|
||||
## 9. AI 代码重构和功能开发指南
|
||||
|
||||
在进行代码重构或新功能开发时,请遵循以下原则:
|
||||
|
||||
- **保持 Astro 核心优势**:优先使用 Astro 的 Islands 架构,确保大部分内容以静态 HTML 形式提供,仅在必要时水合(hydrate)交互式组件。
|
||||
- **React 组件化**:对于需要交互的 UI 部分,使用 React 进行组件化开发,并确保组件是可复用和可测试的。
|
||||
- **Tailwind CSS 优先**:样式应通过 Tailwind CSS 实用程序类实现。只有在 Tailwind 无法满足需求时,才考虑使用自定义 CSS。
|
||||
- **Framer Motion 动画**:所有动画效果通过 Framer Motion 实现,保持动画逻辑的统一性。
|
||||
- **国际化支持**:所有用户可见的文本通过 `src/i18n/ui.ts` 进行管理,确保新功能也支持多语言。
|
||||
- **数据驱动**:新功能所需的数据集中管理,例如添加到 `src/lib/data.ts` 中,而不是硬编码在组件内部。
|
||||
- **性能优化**:关注 Lighthouse 等工具的性能指标,确保代码更改不会引入性能瓶颈。
|
||||
- **可访问性**:确保所有 UI 元素都符合 WCAG 标准,特别是对于交互式组件。
|
||||
- **代码质量**:遵循 TypeScript 最佳实践,编写清晰、可读、有注释的代码。
|
||||
- **测试**:对于复杂逻辑或关键组件,考虑编写单元测试或集成测试。
|
||||
- **博客功能扩展**:在扩展博客功能时,保持与现有的文件结构和数据流一致,确保新功能与多语言支持兼容。
|
||||
|
||||
## 10. 潜在的改进和扩展方向
|
||||
|
||||
- **CMS 集成**:将静态数据(如项目、博客文章)迁移到 CMS(如 Contentful, Sanity, Strapi),实现更灵活的内容管理。
|
||||
- **博客功能增强**:
|
||||
- 添加评论系统(如 Disqus, Giscus)
|
||||
- 实现文章搜索功能
|
||||
- 添加相关文章推荐
|
||||
- 支持更多的内容格式(如代码高亮、数学公式)
|
||||
- **国际化完善**:除了文本,考虑图片、日期格式等内容的国际化。
|
||||
- **性能监控**:集成 Web Vitals 监控,持续跟踪网站性能。
|
||||
- **SEO 优化**:进一步优化元标签、结构化数据等,提升搜索引擎排名。
|
||||
- **无障碍优化**:深入研究 ARIA 属性和键盘导航,提升无障碍体验。
|
||||
- **动画库扩展**:探索 Framer Motion 更多高级特性,或集成其他动画库以实现更丰富的视觉效果。
|
||||
- **组件库扩展**:基于 Radix UI 或其他无头 UI 库构建更多可复用的组件。
|
||||
- **测试框架引入**:引入 Vitest 或 React Testing Library 等测试框架,提高代码质量和可维护性。
|
||||
- **交互式项目展示**:为项目展示部分添加更多交互元素,如项目详情模态框、项目筛选等。
|
||||
- **暗色模式优化**:进一步完善暗色模式的视觉效果和用户体验。
|
||||
- **性能优化**:实现图片懒加载、代码分割等技术,提升页面加载速度。
|
||||
168
AGENTS.md
Normal file
168
AGENTS.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidelines for AI agents operating in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
A modern personal portfolio website built with **Astro 5**, **React 19**, and **Tailwind CSS 4**. Features glassmorphism design, animations via Framer Motion, and full i18n support (EN/ZH).
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Development server
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Preview production build locally
|
||||
npm run preview
|
||||
```
|
||||
|
||||
No lint or test commands are configured. Run `npm run build` to verify changes.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### General Principles
|
||||
- Write concise, self-documenting code
|
||||
- Avoid unnecessary comments (only explain complex logic)
|
||||
- Follow existing patterns in the codebase
|
||||
- Prioritize readability over cleverness
|
||||
|
||||
### Imports
|
||||
- Use `@/` alias for src imports (configured in tsconfig.json)
|
||||
- Order imports: React → libraries → components → utilities
|
||||
- Use named exports for components and utilities
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Avoid
|
||||
import utils from '@/lib/utils';
|
||||
import { useState, useEffect } from 'react';
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
- Use TypeScript for all new files (.ts, .tsx)
|
||||
- Extend Astro's strict TypeScript config
|
||||
- Use explicit types for component props
|
||||
- Avoid `any`, use `unknown` or specific types
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
// Avoid
|
||||
interface ButtonProps { ... } // too verbose
|
||||
const handleClick = (e: any) => { ... } // no type
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
**Astro Components (.astro)**:
|
||||
- Use frontmatter fence `---` at top
|
||||
- Separate imports, props, and logic
|
||||
- Use `client:only="react"` for React components that need browser APIs
|
||||
- Use `client:load` for interactive components
|
||||
- Static content should not have client directives
|
||||
|
||||
**React Components (.tsx)**:
|
||||
- Use `React.forwardRef` for components that need refs
|
||||
- Use class-variance-authority (CVA) for component variants
|
||||
- Use Radix UI primitives when available
|
||||
- Export components as named exports
|
||||
|
||||
### Styling (Tailwind CSS)
|
||||
- Use Tailwind utility classes exclusively
|
||||
- Use `cn()` utility to merge classes with conditional logic
|
||||
- Avoid custom CSS; use Tailwind equivalents
|
||||
- Dark mode: prefix colors with `dark:` modifier
|
||||
|
||||
```tsx
|
||||
// Good
|
||||
<div className={cn(
|
||||
"base-classes",
|
||||
variant === "primary" && "primary-classes",
|
||||
className
|
||||
)} />
|
||||
|
||||
// Avoid
|
||||
<div style={{ display: 'flex', padding: '16px' }}>
|
||||
```
|
||||
|
||||
### i18n (Internationalization)
|
||||
|
||||
- All UI text must be in `src/i18n/translations.ts`
|
||||
- Use `useTranslations(lang)` hook for text
|
||||
- Avoid hardcoded strings in .astro and .tsx files
|
||||
- Structure translations by section (nav, hero, about, etc.)
|
||||
|
||||
```typescript
|
||||
// translations.ts
|
||||
export const translations = {
|
||||
en: { hero: { greeting: 'Hello, I'm' } },
|
||||
zh: { hero: { greeting: '你好,我是' } }
|
||||
}
|
||||
|
||||
// In component
|
||||
const t = useTranslations(lang);
|
||||
<h1>{t('hero.greeting')} {name}</h1>
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
- **Components**: PascalCase (e.g., `Button`, `GlassHeader`)
|
||||
- **Files**: kebab-case for Astro pages, PascalCase for React components
|
||||
- **Variables/functions**: camelCase
|
||||
- **Constants**: SCREAMING_SNAKE_CASE
|
||||
- **Types/Interfaces**: PascalCase with `Props` suffix for component props
|
||||
|
||||
### Error Handling
|
||||
- Use TypeScript types to prevent runtime errors
|
||||
- Handle null/undefined cases explicitly
|
||||
- Use optional chaining `?.` and nullish coalescing `??`
|
||||
- No try-catch unless handling specific async errors
|
||||
|
||||
### Astro-Specific
|
||||
- Pages use file-based routing (`src/pages/**/*.astro`)
|
||||
- Dynamic routes: `[slug].astro`, `[category].astro`
|
||||
- Layouts in `src/layouts/`
|
||||
- Components in `src/components/`
|
||||
- Use Content Collections for blog posts (MDX in `src/pages/blog/posts/`)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # UI components (React & Astro)
|
||||
│ ├── blog/ # Blog-specific components
|
||||
│ ├── layout/ # Layout components
|
||||
│ ├── markdown/ # Markdown rendering components
|
||||
│ └── ui/ # Reusable UI primitives
|
||||
├── layouts/ # Page layouts (Layout.astro, BlogLayout, etc.)
|
||||
├── pages/ # Routes (auto-routed by filename)
|
||||
│ ├── blog/ # Blog pages & posts
|
||||
│ └── zh/ # Chinese locale pages
|
||||
├── lib/ # Data & utilities
|
||||
│ └── data/ # Static data (personal-info, projects, services)
|
||||
├── styles/ # Global CSS
|
||||
├── i18n/ # Internationalization
|
||||
│ └── translations.ts
|
||||
├── types/ # TypeScript type definitions
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- This is a bilingual site (EN/ZH); maintain both locales
|
||||
- Default language is English (`defaultLang = 'en'`)
|
||||
- Chinese pages are under `/zh/` prefix
|
||||
- Use `Astro.currentLocale` to detect current language
|
||||
- The `services` section shows skills/capabilities, not literal services
|
||||
- Include `client:only="react"` for components using browser APIs (localStorage, window, etc.)
|
||||
@@ -19,6 +19,12 @@ export const translations = {
|
||||
description: 'Full Stack Developer specializing in React, Node.js, and modern web technologies',
|
||||
},
|
||||
hero: {
|
||||
greeting: "Hello, I'm",
|
||||
viewProjects: 'View Projects',
|
||||
contactMe: 'Contact Me',
|
||||
lookingForJob: 'Looking for a Frontend/TS Full-stack Engineer (Remote)? Contact me!',
|
||||
digitalNomad: 'Exploring the freelance journey, striving to become a digital nomad',
|
||||
contactInfo: 'Contact Me',
|
||||
githubLink: 'GitHub Profile',
|
||||
linkedinLink: 'LinkedIn Profile',
|
||||
},
|
||||
@@ -44,6 +50,16 @@ export const translations = {
|
||||
blog: {
|
||||
slogan: 'This is where innovative thinking meets complex problems.',
|
||||
},
|
||||
services: {
|
||||
title: 'What I Do',
|
||||
viewAll: 'Learn More',
|
||||
},
|
||||
about: {
|
||||
title: 'About Me',
|
||||
sectionTitle: 'About Me',
|
||||
learnMore: 'Learn More About Me',
|
||||
toolbox: 'My Toolbox',
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
nav: {
|
||||
@@ -59,6 +75,12 @@ export const translations = {
|
||||
description: '专注于 React、Node.js 和现代 Web 技术的全栈开发者',
|
||||
},
|
||||
hero: {
|
||||
greeting: '你好,我是',
|
||||
viewProjects: '查看项目',
|
||||
contactMe: '联系我',
|
||||
lookingForJob: '正在寻找前端/TS 全栈工程师(远程)?联系我!',
|
||||
digitalNomad: '探索自由职业道路,努力成为数字游民',
|
||||
contactInfo: '联系我',
|
||||
githubLink: 'GitHub 主页',
|
||||
linkedinLink: 'LinkedIn 主页',
|
||||
},
|
||||
@@ -69,7 +91,7 @@ export const translations = {
|
||||
tag: {
|
||||
business: '商业项目',
|
||||
opensource: '开源项目',
|
||||
personal: '个人产品',
|
||||
personal: '个人项目',
|
||||
portfolio: '作品集',
|
||||
ecommerce: '电子商务',
|
||||
},
|
||||
@@ -84,6 +106,16 @@ export const translations = {
|
||||
blog: {
|
||||
slogan: '这里是创新思维与复杂问题相遇的地方。',
|
||||
},
|
||||
services: {
|
||||
title: '我能做什么',
|
||||
viewAll: '了解更多',
|
||||
},
|
||||
about: {
|
||||
title: '关于我',
|
||||
sectionTitle: '关于我',
|
||||
learnMore: '了解更多',
|
||||
toolbox: '我的工具箱',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
67
src/pages/blog/posts/2026010801.md
Normal file
67
src/pages/blog/posts/2026010801.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Stop Being Held Hostage by "Best Practices": Confessions of a Full-Stack Developer’s Tech Stack Struggles
|
||||
|
||||
> **Foreword:**
|
||||
> I am a developer who transitioned from Frontend to Node.js Full-Stack. This article is simply a summary of my recent experiences and reflections while developing a project. Given my limited knowledge and perspective, the views expressed here may not be universally "correct" or represent industry standards. This is just a personal debrief after stepping into countless pitfalls, shared in the hope of exchanging ideas with the community and providing a reference for those facing similar dilemmas. If there are any inaccuracies, please feel free to correct me in the comments.
|
||||
|
||||
Recently, I set out to build a small bookmark-style tool with only about 30 endpoints. I thought it would take two weeks; instead, I spent over a month just "wrestling" with the tech stack.
|
||||
|
||||
I felt like a hunter lost in a technical fog: wherever I saw a light (a new tool or an "expert" opinion), I rushed toward it, only to find a deeper pit hidden behind every glow.
|
||||
|
||||
## 1. Chasing Trends is the Start of Internal Friction
|
||||
|
||||
I started by following the crowd and chose **Next.js + NestJS + shadcn-ui**. I thought, "Since everyone says this is the 'Full-Stack Gold Standard,' I can't go wrong." The reality, however, gave me a swift wake-up call.
|
||||
|
||||
In Next.js, I wasted a whole week just deciding on a data-fetching and state management solution (SWR vs. Zustand?). Once I finally started, I was overwhelmed by the complexity of Server Components (RSC) vs. Client Components—constantly defining `"use client"`, fixing mysterious Hydration errors, and manually managing state dependencies while optimizing endless callback functions.
|
||||
|
||||
I kept thinking: **I just want to write some simple business logic. Why am I spending 80% of my energy dealing with the overhead of the framework?**
|
||||
|
||||
## 2. The Heavier the Framework, the Heavier the Cognitive Load
|
||||
|
||||
Later, I switched the frontend to Nuxt, which was indeed smoother. But on the backend, I stuck with **NestJS**, chasing so-called "standardization" and "enterprise engineering."
|
||||
|
||||
But I only had 30 endpoints. The logic was incredibly simple. In NestJS, I was forced to write Controllers, Services, Modules, DTOs... the amount of code tripled. Even worse was the **ESM compatibility issue**. NestJS still clings to the CommonJS dream, leading to constant configuration errors when I tried to use modern ESM-only libraries. To run a simple TypeScript Worker thread, I had to spend hours researching ESM compilers.
|
||||
|
||||
The most frustrating part was **Swagger integration**. Most people prefer Zod for validation now, but Swagger is deeply coupled with the Class-Validator (Decorator) pattern. To get Swagger to recognize my Zod schemas and generate documentation, I had to manually write adapters and custom decorators.
|
||||
|
||||
**I felt like I wasn't building a product; I was repairing a broken tractor with incompatible parts.**
|
||||
|
||||
## 3. Monorepo: The "Tender Trap" for Indie Developers
|
||||
|
||||
To pursue "code reuse," I even set up a **Monorepo**.
|
||||
|
||||
I thought: *Front-end and back-end sharing types, enums, and error codes—how elegant!* The reality: trying to get a pure ESM frontend to share a package with a non-pure ESM backend plunged me into a bottomless pit of build configurations. Due to the NestJS environment, I had to compile and export the shared package every time I made a change, making frequent debugging and code modification an absolute nightmare.
|
||||
|
||||
Code that should have taken one minute to write took ten because I was busy dealing with cross-package debugging, TS type synchronization, and build logic.
|
||||
|
||||
**I finally realized: Monorepos are built to solve "organizational collaboration." For an indie developer, they are often a productivity killer.**
|
||||
|
||||
## 4. Returning to Pragmatism: My "Two-Tier Strategy"
|
||||
|
||||
At the end of all this exhaustion, I reflected: Is there a perfect framework? The answer is no; there is only the *suitable* one. Consequently, I have simplified my selection logic into two tiers:
|
||||
|
||||
* **Tier A: Rapid Validation (MVP / Personal Projects)**
|
||||
**Stack: Nuxt All-in-One.** Don't even separate the frontend and backend. Nuxt’s built-in Server API (Nitro) is more than enough for small to medium businesses. Types are naturally shared, and there are no CORS or build-sync headaches. At the validation stage, **"Speed" is a hundred times more important than "Elegance."**
|
||||
|
||||
* **Tier B: Complex Business (Large Projects / Team Collaboration)**
|
||||
**Stack: Nuxt + NestJS (Decoupled) + Monorepo.** Only when the business is complex enough to require strict layering, Dependency Injection (DI) for decoupling, and multi-person collaboration will I endure the "ceremony" and management costs of these heavy frameworks.
|
||||
|
||||
|
||||
|
||||
## 5. A Side Note: A New Hope in AdonisJS
|
||||
|
||||
Just as I was summarizing these strategies, I stumbled upon a new framework—**AdonisJS**. Many developers describe it as the "Laravel of Node.js."
|
||||
|
||||
I took a quick look at its philosophy, and it seems to precisely hit the pain points I mentioned: it supports ESM natively, has a powerful built-in ORM and Auth solution, and doesn't require jumping through hoops with custom adapters just to get automated Swagger documentation.
|
||||
|
||||
This "Convention over Configuration" full-stack framework seems to balance development efficiency with engineering quality. I plan to use it in my next project and will share my findings once I have more experience.
|
||||
|
||||
## 6. Conclusion: A Few Words of Advice
|
||||
|
||||
1. **There is no perfect framework, only the one that fits the moment.** Don't expect any "star" framework to solve all your problems; they all come with a cost.
|
||||
2. **Do not easily try a tech stack you aren't familiar with during indie development or tight deadlines.** Unless you truly have the time and energy to burn. You think you're learning new tech, but you're actually burning your product's lifespan.
|
||||
3. **Be wary of "Big Tech Best Practices."** Many tools built to solve pain points in giant corporations (like Monorepos or extreme layering) only create pain points in personal projects.
|
||||
4. **Familiarity > Modernity.** Even if a framework is called "old school," if it's intuitive to you, lets you finish work early, and helps you write clearer logic with AI assistance, it is your "silver bullet."
|
||||
|
||||
**The best tech stack is the one that allows you to forget the technology itself and focus on creating value.**
|
||||
|
||||
Finally, the solutions I've summarized are only what fits my personal habits and current understanding; they may not work for everyone. Everyone's business scenarios and technical backgrounds are different. **If you have better ideas or different solutions, I’d love to hear them in the comments so I can learn from you too.** If I've missed anything, please let me know. Thanks in advance!
|
||||
@@ -32,7 +32,7 @@ const pageTitle = t('site.title');
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
|
||||
</svg>
|
||||
<span class="text-purple-500 font-mono text-lg">
|
||||
Hello World! I'm
|
||||
{t('hero.greeting')} {personalInfo.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -72,8 +72,8 @@ const pageTitle = t('site.title');
|
||||
|
||||
<!-- Job availability notice -->
|
||||
<div class="mb-8">
|
||||
<p class="text-lg font-medium text-purple-500 mb-2">Looking for a Frontend/TS Full-stack Engineer (Remote)? Contact me!</p>
|
||||
<p class="text-md text-muted-foreground">Exploring the freelance journey, striving to become a digital nomad</p>
|
||||
<p class="text-lg font-medium text-purple-500 mb-2">{t('hero.lookingForJob')}</p>
|
||||
<p class="text-md text-muted-foreground">{t('hero.digitalNomad')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
@@ -85,7 +85,7 @@ const pageTitle = t('site.title');
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
|
||||
</svg>
|
||||
View Projects
|
||||
{t('hero.viewProjects')}
|
||||
</a>
|
||||
|
||||
<a
|
||||
@@ -95,7 +95,7 @@ const pageTitle = t('site.title');
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
Contact Me
|
||||
{t('hero.contactMe')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -204,7 +204,7 @@ const pageTitle = t('site.title');
|
||||
<section id="services" class="py-20 bg-gradient-to-br from-slate-50 to-blue-50 dark:from-slate-900 dark:to-slate-800">
|
||||
<Container client:load>
|
||||
<h2 class="text-4xl font-bold text-center mb-16 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
Services I Offer
|
||||
{t('services.title')}
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-8">
|
||||
@@ -235,7 +235,7 @@ const pageTitle = t('site.title');
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||
</svg>
|
||||
查看所有服务
|
||||
{t('services.viewAll')}
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
@@ -251,7 +251,7 @@ const pageTitle = t('site.title');
|
||||
<!-- Section Title -->
|
||||
<div class="flex items-center justify-center mb-6">
|
||||
<h2 class="text-3xl md:text-4xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
About Me
|
||||
{t('about.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -263,10 +263,10 @@ const pageTitle = t('site.title');
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
||||
<span class="mr-2">👋</span>
|
||||
About Me
|
||||
{t('about.sectionTitle')}
|
||||
</h3>
|
||||
<div class="prose prose-lg dark:prose-invert max-w-none">
|
||||
{personalInfo.about.en.map((paragraph) => (
|
||||
{personalInfo.about[lang].map((paragraph) => (
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
{paragraph}
|
||||
</p>
|
||||
@@ -277,7 +277,7 @@ const pageTitle = t('site.title');
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
了解更多关于我
|
||||
{t('about.learnMore')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,7 +326,7 @@ const pageTitle = t('site.title');
|
||||
<div class="bg-white/20 dark:bg-gray-800/50 backdrop-blur-sm border border-white/30 dark:border-gray-700/40 rounded-xl p-6 flex flex-col">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
||||
<span class="mr-2">💻</span>
|
||||
My Toolbox
|
||||
{t('about.toolbox')}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-6 flex-1">
|
||||
|
||||
@@ -5,7 +5,8 @@ import CategoryCard from '../../../components/blog/CategoryCard.astro';
|
||||
import TagCard from '../../../components/blog/TagCard.astro';
|
||||
import Container from '../../../components/ui/Container';
|
||||
import { type BlogPost } from '@/types';
|
||||
import { type Lang, useTranslations } from '@/i18n/utils';
|
||||
import { useTranslations } from '@/i18n/utils';
|
||||
import { type Lang } from '@/types/i18n';
|
||||
import { defaultLang } from '@/i18n/ui';
|
||||
import { sortPostsByDate, extractCategories, extractTags } from '@/utils/blog-utils';
|
||||
|
||||
|
||||
65
src/pages/zh/blog/posts/2026010801.md
Normal file
65
src/pages/zh/blog/posts/2026010801.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 别再被“最佳实践”绑架了:一个全栈开发者的选型忏悔录
|
||||
|
||||
> **写在前面:**
|
||||
> 本人是一名从前端转 Node.js 全栈的开发者。这篇文章只是基于我近期开发项目时的一些真实经历和感悟。由于个人知识储备和认知水平有限,文中的观点不一定正确,更不代表行业标准。这仅仅是我在踩了无数坑后的一点自我总结,发出来是希望能和大家交流,也给有类似纠结的朋友提供一个参考。如有谬误,欢迎评论区指正。
|
||||
|
||||
最近我为了做一个只有 30 个接口的书签类小工具,把自己折腾疯了。
|
||||
|
||||
起初,我像个在技术丛林里乱撞的猎人:哪里有亮光(新工具/大牛言论),我就往哪冲。结果发现,每一个亮光后面都藏着一个更深的坑。
|
||||
|
||||
## 1. 追逐流行,是内耗的开始
|
||||
|
||||
我最开始随大流选了 **Next.js + NestJS + shadcn-ui**。我想着,既然大家都说这是“全栈天花板”,那选它准没错。结果现实反手就给了我一巴掌。
|
||||
|
||||
在 Next.js 里,为了选一个数据处理和状态管理方案(SWR 还是 Zustand?),我硬生生磨掉了一周。好不容易开工了,又被服务端渲染(SSR)带来的复杂度搞得头大——频繁定义 `"use client"` 指令、处理莫名其妙的“状态水合(Hydration)”报错、还要手动管理状态依赖并优化一堆回调函数。
|
||||
|
||||
我当时就在想:**我只是想写个简单的业务逻辑,为什么要花 80% 的精力去处理框架带来的麻烦?**
|
||||
|
||||
## 2. 框架重一点,心智负担就大一点
|
||||
|
||||
后来前端切到了 Nuxt 确实顺手了些,但后端我依然守着 **NestJS**,追求所谓的“大而全”。
|
||||
|
||||
但我一共才 30 个接口,业务逻辑极其简单。但在 NestJS 里,我不得不写 Controller、Service、Module、DTO……代码量翻了几倍。更崩溃的是 **ESM 的兼容问题**,NestJS 依然守着 CommonJS 的旧梦,导致我想用一些最新的 ESM 库时,各种配置文件报错。为了跑通一个简单的 TypeScript Worker 线程,我还得自己去研究 ESM 编译器。
|
||||
|
||||
最心累的是 **Swagger 的集成**。现在大家都爱用 Zod 做验证,但 Swagger 深度绑定类装饰器模式(class-validator)。为了让 Swagger 识别 Zod Schema 并生成文档,我得自己手搓适配器和装饰器。
|
||||
|
||||
**我感觉自己不是在开发产品,我是在修一辆零件互不兼容的破拖拉机。**
|
||||
|
||||
## 3. Monorepo:独立开发者的“温柔陷阱”
|
||||
|
||||
为了追求代码复用,我还折腾了 **Monorepo**。
|
||||
|
||||
我想着,前后端共享类型、枚举、错误码,多优雅!但现实是:为了让纯 ESM 的前端和非纯血 ESM 的后端共享一个包,我陷入了无穷无尽的编译配置中。由于 NestJS 的环境问题,我必须为共享包进行编译导出才能使用,导致频繁修改调试代码时异常麻烦。
|
||||
|
||||
原本一分钟写完的代码,因为要处理跨包调试、TS 类型同步和打包逻辑,硬生生变成了十分钟。
|
||||
|
||||
**我悟了:Monorepo 是为了解决“组织协作”的,对于独立开发者,它往往是效率杀手。**
|
||||
|
||||
## 4. 回归务实:我的“两套方案”
|
||||
|
||||
在折腾的终点,我反思:有没有完美的框架?结论是没有,只有适合的。于是,我把我的选型逻辑简化成了两套:
|
||||
|
||||
* **方案 A:快速验证(MVP / 个人项目)**
|
||||
**技术栈:Nuxt 一把梭。** 别分前后端了,Nuxt 的内置 Server API(Nitro)足够处理中小业务。类型天然共享,没有跨域烦恼。在验证阶段,**“快”比“优雅”重要一百倍。**
|
||||
|
||||
* **方案 B:复杂业务(大型项目 / 团队协作)**
|
||||
**技术栈:Nuxt + NestJS 分离 + Monorepo。** 当业务复杂到需要严格的分层、需要依赖注入(DI)解耦、需要多人协作时,才去忍受这种“重”框架带来的仪式感和管理成本。
|
||||
|
||||
## 5. 题外话:意外发现的新希望 AdonisJS
|
||||
|
||||
就在我总结完上述方案后,我最近无意间发现了一个新的框架——**AdonisJS**。它被很多开发者评价为 Node.js 界的 Laravel。
|
||||
|
||||
我简单看了一下它的设计理念,感觉它似乎精准地击中了我上述遇到的很多痛点:它原生支持 ESM、内置了强大的 ORM 和 Auth 方案、不需要像 NestJS 那样去折腾各种复杂的适配器来搞定自动化的 Swagger 文档生成。
|
||||
|
||||
这种“约定优于配置”的全栈框架,似乎能平衡开发效率与工程质量。我准备在接下来的项目中实际试用一下,如果确实好用,有了心得后再专门写篇文章分享给大家。
|
||||
|
||||
## 6. 总结:给开发者的一点建议
|
||||
|
||||
1. **没有完美的框架,只有最适合当下的。** 不要指望任何一个明星框架能解决所有问题,它们都有代价。
|
||||
2. **不要在独立开发或工作中轻易尝试自己不熟悉的技术栈。** 除非你真的有大把的时间和精力去踩坑。你以为在学新技术,其实你是在浪费产品的寿命。
|
||||
3. **警惕“大厂最佳实践”。** 很多在大公司里解决痛点的工具(如 Monorepo、过度分层),在个人项目里往往只会制造痛点。
|
||||
4. **熟悉度大于先进性。** 哪怕一个框架被说成是“老古董”,只要你用得顺手、能让你早点下班,它就是你的银弹。
|
||||
|
||||
**最好的技术栈,是那个能让你忘记技术本身,而专注于创造价值的工具。**
|
||||
|
||||
最后想说,我总结的这些方案也仅仅是适合我个人的开发习惯和目前的认知,并不一定适合所有人。每个人面对的业务场景和技术背景都不同,**大家如果有更好的方案或想法,非常欢迎在评论区讨论,让我也有机会学习一下。** 不对的地方也希望各位大佬多多指教,先行谢过!
|
||||
@@ -1,4 +1,4 @@
|
||||
# 我为什么放弃 Next.js,选择 Nuxt.js 和 NestJS
|
||||
# 从追逐流行到回归工程:我技术选型的“降噪”思考
|
||||
|
||||
过去几年里,我尝试过很多 Node.js 框架和前端技术栈:**Express、Koa、Fastify、Hono.js**,以及前端的 **Next.js**。这些框架和工具都有各自的优点:轻量、灵活、性能好,社区里也有不少人推荐。然而,随着项目的深入,我逐渐发现了一个事实:**简单、快速实现、自己熟悉、生态完善、可扩展性好**,才是最重要的,而不是盲目追逐“最流行”或“性能最高”的框架。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import TaxonomyPageLayout from '../../../../layouts/TaxonomyPageLayout.astro';
|
||||
import { type Lang } from '@/i18n/utils';
|
||||
import { type Lang } from '@/types/i18n';
|
||||
import { defaultLang } from '@/i18n/ui';
|
||||
import { getTaxonomyPageData } from '@/utils/blog-utils';
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ const pageTitle = t('site.title');
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
|
||||
</svg>
|
||||
<span class="text-purple-500 font-mono text-lg">
|
||||
你好世界!我是
|
||||
{t('hero.greeting')} {personalInfo.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -72,8 +72,8 @@ const pageTitle = t('site.title');
|
||||
|
||||
<!-- Job availability notice -->
|
||||
<div class="mb-8">
|
||||
<p class="text-lg font-medium text-purple-500 mb-2">如果你正在寻找一名前端/Ts全栈工程师(远程工作),请联系站长!</p>
|
||||
<p class="text-md text-muted-foreground">探索自由职业者之路,努力成为数字游民中的一员</p>
|
||||
<p class="text-lg font-medium text-purple-500 mb-2">{t('hero.lookingForJob')}</p>
|
||||
<p class="text-md text-muted-foreground">{t('hero.digitalNomad')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
@@ -85,7 +85,7 @@ const pageTitle = t('site.title');
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
|
||||
</svg>
|
||||
查看项目
|
||||
{t('hero.viewProjects')}
|
||||
</a>
|
||||
|
||||
<a
|
||||
@@ -95,7 +95,7 @@ const pageTitle = t('site.title');
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
联系我
|
||||
{t('hero.contactMe')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -204,7 +204,7 @@ const pageTitle = t('site.title');
|
||||
<section id="services" class="py-20 px-4 bg-gradient-to-br from-slate-50 to-blue-50 dark:from-slate-900 dark:to-slate-800">
|
||||
<Container client:load>
|
||||
<h2 class="text-4xl font-bold text-center mb-16 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
我提供的服务
|
||||
{t('services.title')}
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-8">
|
||||
@@ -235,7 +235,7 @@ const pageTitle = t('site.title');
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||
</svg>
|
||||
查看所有服务
|
||||
{t('services.viewAll')}
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
@@ -251,7 +251,7 @@ const pageTitle = t('site.title');
|
||||
<!-- Section Title -->
|
||||
<div class="flex items-center justify-center mb-6">
|
||||
<h2 class="text-3xl md:text-4xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
关于我
|
||||
{t('about.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -263,10 +263,10 @@ const pageTitle = t('site.title');
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
||||
<span class="mr-2">👋</span>
|
||||
关于我
|
||||
{t('about.sectionTitle')}
|
||||
</h3>
|
||||
<div class="prose prose-lg dark:prose-invert max-w-none">
|
||||
{personalInfo.about.zh.map((paragraph) => (
|
||||
{personalInfo.about[lang].map((paragraph) => (
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
{paragraph}
|
||||
</p>
|
||||
@@ -278,7 +278,7 @@ const pageTitle = t('site.title');
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
了解更多关于我
|
||||
{t('about.learnMore')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,7 +327,7 @@ const pageTitle = t('site.title');
|
||||
<div class="bg-white/20 dark:bg-gray-800/50 backdrop-blur-sm border border-white/30 dark:border-gray-700/40 rounded-xl p-6 flex flex-col">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
||||
<span class="mr-2">💻</span>
|
||||
我的工具箱
|
||||
{t('about.toolbox')}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-6 flex-1">
|
||||
|
||||
Reference in New Issue
Block a user