Frontend Elegant Guidance - 前端优雅指南

55 阅读15分钟

我大二开始学习前端,现在研三即将毕业,仍然做这个方向。这有历史原因,但是从整体知识范畴,前后端和运维我都懂点,这篇文章讲我常常喜欢使用的一个前端结构,大家做过前端的应该一眼都明白,似乎过于简单,没什么价值,但在这些年实习兼职过的很多创业公司、小厂大厂中,在这一点上都差很多,因为往往是一开始就没有计划好,后面无数人"金"上雕花,所以除了做过多牛逼的技术外,我更认为最基本的开发素养和习惯是应该的,因为这是我们很多人能直接做到。

AI的出现的确能提交,Vibe Coding的几款工具我基本都用过,让Claude Code写代码能写,UI也比手撸好看,但是它会埋下很多的隐患,尤其是对于我这种有强迫症的人来说,巨难受,比我手写代码量增加三四成。另外就是,和AI对话多了,我发现我真的不想写代码了,且和他对话非常暴躁,会骂他。所以,提到这个也凸显了架构、规范的重要性,我在最后也放了一个claude.md,当然你可以修改,help yourself。

提到这个话有点多,那就是AI能助力,但绝不像你每天刷到的几个小时一天完成一个app上线,一周赚了多少刀,多尝试没错,但擦亮眼睛,脚踏实地。

整体架构

前后端开发时,先搞清楚要做什么,然后再考虑用什么框架和模块,接着可以在AI帮助下完善整体架构,在开发时遵循这个架构和规范,步子迈的不要太大,包括命名、样式、请求、缓存等等。而前后端分离是最常见的开发模式,NextJS 是典型的面向前端的全栈框架,它很适合SEO和SSR,但是假如已经具备后端和Gateway,那么在使用的时候需要注意不要再二次转发API,前端处理好跨域即可。

这里以NextJS框架为例,大概说明每个文件/文件夹的作用,后面我会针对每个模块来详细说明应该注意的事项和做法。注意,开发时一定要把新文件放到对应的位置,在没有AI的时候,假如定义不明,别人是很难阅读的。

NextJS-Frontend
├── README.md            # You Know It!
├── components.json      # React组件库Shadcn-UI的配置文件
├── docs                 # Vide Coding的需求文档、DEBUG文档等
├── next-env.d.ts        # Next.js TypeScript 类型全局声明
├── next.config.js       # Next.js 框架配置
├── package.json         # You Know It!
├── pnpm-lock.yaml       # pnpm 包管理器锁定文件(不建议npm)
├── pnpm-workspace.yaml  # pnpm workspace 配置(Never Mind)
├── postcss.config.js    # PostCSS 配置(CSS 处理)
├── public               # 公共静态资源
│   ├── animations         # 动画库
│   ├── favicon.ico        # 网站标签页左侧显示的icon
│   ├── fonts              # 字体文件
│   ├── icons              # 专有设计icon图标
│   └── images             # 图片资源
├── src
│   ├── app               # 各个页面视图文件(NextJS特性:约定式路由)
│   │   ├── page.tsx        # 页面文件 
│   │   └── layout.tsx      # 布局文件
│   ├── components        # 公共组件库  
│   │   ├── animate-ui      # 动画库
│   │   └── ui              # shadcn-ui组件库
│   ├── apis              # api封装调用模块
│   │   ├── base.ts         # 基础配置
│   │   └── endpoints       # 定义各个接口调用
│   ├── hooks             # hook钩子函数
│   ├── constants         # 静态配置、常量存储
│   ├── utils             # 工具库             
│   ├── stores            # 状态管理(Zustand)
│   ├── styles            # 公共样式
│   └── types             # 存储类型定义
│   │   ├── apis            # 存储API的request和response类型定义 
│   │   └── entity          # 存储实体定义,例如User信息等
├── tailwind.config.js    # Tailwind CSS 配置
└── tsconfig.json         # TypeScript 编译配置

变量命名

  • 前端绝大部分的命名都是驼峰命名法,后端则下划线(Python)和驼峰命名(Go、JAVA)都有涉及,而在接口对接上,字段常常是下划线。
  • 变量命名不宜过长过短,同时语义性要强。AI喜欢起特别长的名字,不别扭吗?
  • 变量名前后必须保持一致,假如叫Card,那就不要改为Widget或者其他别的。

风格样式

前端三件套,分别是html、css、js。html是人的骨架和血肉,css是人的穿搭衣物,js则是人的动作行为。爱美之心人皆有之,因此,美观的一定是前端人所追求的东西之一。

最初上手写的肯定是原始的css,这是必经之路;接着是用法简单的css预处理器,less、sass、stylus,基本上拿来就用,看一遍就会;然后,样式文件体积和数量的增大,于是聪明人就做出了 Tailwind,它将css融入了class类中,导致我们代码的区块划分为两部分:js和html,同时大量减少手写的样式内容,这将是我主推的一个语法。

<div class="flex flex-col items-center gap-6 p-7 md:flex-row md:gap-8 rounded-2xl">
  <div>
    <img class="size-48 shadow-xl rounded-md" alt="" src="/img/cover.png" />
  </div>
  <div class="flex items-center md:items-start">
    <span class="text-2xl font-medium">Class Warfare</span>
    <span class="font-medium text-sky-500">The Anti-Patterns</span>
    <span class="flex gap-2 font-medium text-gray-600 dark:text-gray-400">
      <span>No. 4</span>
      <span>·</span>
      <span>2025</span>
    </span>
  </div>
</div>
  • Tailwind用来处理通用的样式
  • Pure CSS用来表达写动画、字体或者定义全局样式
  • 尽量避免内联CSS Style,除非需要动态调整宽高等内容
  • 过于复杂可以使用 clsx 函数来处理
  • 全局样式文件还建议分类放置
├── styles            # 公共样式
│   ├── custom.css      # 自定义样式写到这里
│   ├── fonts.css       # 引入的字体样式
│   ├── index.css       
│   ├── scrollbar.css   # 滚动条覆盖样式
│   └── tailwind.css    # tailwind样式

图标库

在项目开发中,常常要用到众多的图标,像jpg、png这样的图标不能定制颜色,都是通过下载到本地来完成的,所以图标使用常以svg为主,它的用法主要有以下三类:

  • 纯SVG内容复制注入代码中
  • 图标下载到本地,通过img注入
  • 图标库,例如lucide,通过组件方式注入
  • 【推荐】类图标,使用iconify,可以通过class类注入,例如material design icons图标库中的播放图标,可以通过class类加上 i-mdi-play 来生效,这个图标库中也包括了现在在使用的 lucide-react 图标,目前已经通过 unocss 配置。

状态管理

对前端项目来说,它需要在浏览器内存中存储大量的临时数据,网页之间以及组件之间会涉及大量的数据交互,我们不可能层层传递变量(称为props drilling),过于丑陋。

这时候就需要状态管理,它们主要用于全局存储数据和定义行为,例如常见的用户信息,在vue里的解决方案从 vuex 到官方推荐的 pinia,在react中则百花争鸣,原生自带的Context/Provider,或轻量级的zustand,这会是我们项目主推去使用的。

  • 定义公共变量,例如 authTokenuserInfo
  • 定义公共行为,例如 login logout updateUserInfo
  • 定义缓存策略,不要操作localstorage,这些模块都配置有插件,例如zustand的 persist
  • 定义更新策略,如果涉及多重嵌套更新状态,可以使用 zustand/immer 模块
  • 相关性能力聚合,这指的是例如要对用户信息做频繁操作,那这个函数就可以放到store中,而不是单独写到页面中

格式化

每个语言都有它的检查和格式化规则,问题就在于每个人的格式化风格不同,甚至于一个文件敲完之后没有格式化,像python有autopep、blank等;另外便是JS仍然是弱类型语言,缺少编译流程,这样就难以发现各种潜在的问题,直到你运行build或者在页面里开始做操作。

这方面最常用的模块就是 EslintPrettier

  • Eslint是一个代码检查工具(linter),主要关注代码质量和语法正确性,以及代码风格的一致性,它会分析代码的语法结构和逻辑,检测潜在的错误(如未定义变量、不合理的类型转换)、不符合最佳实践的写法(如使用 var 而非 let/const),以及团队约定的代码风格(如缩进、命名规范)。
  • Prettier是一个代码格式化工具(formatter),专注于代码的格式美化,不关心代码的逻辑或语法正确性。它会统一代码的格式细节(如缩进、换行、引号类型、分号等),消除团队成员之间因格式偏好产生的差异。

目前更推荐 biome :它是一个现代化的前端开发工具链,旨在整合代码检查(Lint)、格式化(Format)、语法解析(Parse)等功能,提供比传统工具(如 ESLint、Prettier 等)更高效、更统一的开发体验。可以在VSCODE上同时安装插件 biome 这样就可以边写代码边格式化了。

值得注意的一点是,它还应该配置到 git-hooks 中,在做git操作时会触发检查和格式化,这样诸位在commit之前代码就必须满足这些风格并通过检测了,有很多配合方式例如simple-git-hookslint-staged

类型管理

Typescript语言的重要性就在于Type,它是JavaScript的超集,增加了类型,使得它在弱类型语言(JIT解释性语言)的基础上增加了一定的健壮性,所以在项目配置之处,还会引入eslint等模块来进行规则校验,它会阻止常见的容易出错的写法,以及不规范的写法。

对于类型的语法,除了常用的 interfacetype 外,还有很多用于做类型体操的语法,这大多数不需要自己写,我们只需要保证类型按属性、功能整理好。

例如有一个专门的types目录,可以再创建一个文件夹apis用来存储每个接口的入参和回参类型,这类参数的命名常用 XXXParams XXXRequest XXXResponse XXXResult等。也可以再创建一个文件夹 entity 实体表,在UML图中它可以对应数据表,也就是User、Product、Order等,这样的话,例如用户可以专门创建一个 user.ts 文件存储用户信息相关的类型,其他同理。

路由管理

在以往,一个项目的路径常常是通过 vue-router 或者 react-router 来手动配置和管理的,这为我们配置页面增加了很多额外的工作,但是现在约定式路由的出现,使得它节省了我们的人力成本,比例 Next.JS 具备的这个特性。

HOOK钩子

在前端开发中,hook(钩子)和store(状态存储)是两个不同维度的概念,但它们经常结合使用,共同解决状态管理和组件逻辑复用的问题。

例如对于用户信息,它全局只存在一份,那么就应该有一个userStore,但是假如它有能复用的一块功能特性,那就可以定义hook来处理。

关于可复用的 hooks 有很多已经总结好的模块,例如阿里的 ahooks

  • useSetState 可以创建复杂类型的状态,且便于更新
  • useClickAway 用于处理点击某个元素之外的内容

网络请求

其实网络请求主流就http协议和websocket协议请求(在http 1.1基础上upgrade)。在做请求的时候,后端只是在Apifox/Postman/Curl上直接测试接口联通,但是为了保证完善可用,在前端做调用的时候,也要考虑如何统一存储和写入认证信息、统一字段命名、完成请求和响应类型定义(JS是弱类型语言,TS的精髓在于此)、请求和响应拦截器等等。

Http

工欲善其事必先利其器,为了完成前面提到的需要考虑的内容,需要先选择一个合适的http请求库,当然前端自带的 fetch 肯定是可以的,只不过需要做一定的封装,这里建议使用 axios 来封装。我还经常喜欢在响应拦截器里加一个统一的transformCamel,从下划线统一转换为驼峰。

// base.ts
import axios from "axios";
import { transformCamel, transformSnake } from "@/utils/transform";

export const service = axios.create({
  baseURL: "/api",
});

service.interceptors.request.use();

service.interceptors.response.use();

然后接口调用的时候再单独写一个函数去声明:

import { service } from "./base";

// 下面的类型也可以专门放到@/types/apis文件夹下
interface GetExampleReq {
  id: string;
}
interface GetExampleRes {
  id: string;
  name: string;
}

export async function getExample(params: GetExampleReq) {
  return service.get<any, GetExampleRes>("/example", {
    params,
  });
}

接着就是接口格式,下面是一个常用的返回值格式:

  • message是消息提示,假如后端业务逻辑出差,在这里提示,前端可以直接用Toast组件提示
  • code是业务码,0表示调用成功,多用来标识具体的业务错误(status多用于网络状态、调用参数错误、后端未意料的BUG错误)
  • data是实际传输的数据本身,所以在调用的时候就可以直接拿data用,通过响应拦截或者包裹except去捕获错误即可。
  • data尽量不要多重包裹,例如一个列表接口,如果不涉及分支全部返回,那么data本身就应该是这个数组,而不是data.list是这个数组。
{
	"message": "ok",
	"code": 0,
	"data": {
	  "id": "gypsophlia",
	  "username": "走马观花"
	}
}

关于接口设计,有很多风格,例如RESTful风格,教了如何定义这个接口的路径,清晰表明接口的作用,如何使用GET/POST/PUT/DELETE去对应不同的动作,这些整个接口设计中看似微不足道,但却是专业性的表现。

Websocket

由于通信的建立会持久化,因此个人认为较好的方式为使用设计模式中的单例模式,写一个全局的 websocketInstance 来处理,假如需要多个的话,那可以创建多个全局或者用Map来存映射。然后在全局中去处理各种事件触发 sendMessage 和事件监听 handleListen

谈到这个就想到了我的第一篇文章,讲Electron,这个再更新,先不多说websocket了。

性能优化

对于前端而言,有几种比较关键的指标,例如 FCPLCP ,这和用户体验息息相关,且AI很难发现,需要我们主动关注和优化,比如以下几个方面:

  • 资源加载:对于小的资源,例如10B的svg可以存储到本地文件中;但是对于比如字体文件,动辄500KB或者>1MB,最好存储到oss,用cdn方式访问。
  • 资源缓存:例如用户信息、登录Token等内容。
  • 接口调用:把握好接口调用的时机和次数,例如一个用户访问网站,那么 getUserInfo 就只需要触发一次后存储,除非数据更新否则无需再调用;例如页面数据批量更新,可以用定时器+脏标记的方式,减少接口调用且减少全量更新。

Vide Coding

以上各种问题,均可能是Vide Coding碰到的问题,它的代码量相比老艺术家手写代码,会提高复杂性,且行数会增加非常多,难以维护,因此我们可以为项目配置一个 .claude/claude.md 文件,相当于为每个chat增加声明。除此之外,ai写出来的代码自己也要多关注,不是功能OK就可以一键完成提交commit。下面是总结的几条原则和最后配置出来的 .claude/claude.md 文件:

  • 按照整体文件架构中去划分、创建和使用对应模块,包括组件、类型等
  • 变量统一采用驼峰命名,且简洁易读
  • 样式尽量采用 tailwind ,减少内联css,过于复杂则使用 cn 函数
  • 图标尽量采用 iconifymdilucide,特殊图标则下载到 public/icons 来使用
  • 减少全局hooks,除非它可在多处复用,多尝试使用 ahooks 中的函数保证代码简洁,例如 useSetState 等。
  • 状态管理使用 zustand ,如果有需要,可以配置 persistimmer
  • 接口请求均整理在 apis 中,并且需要为每个请求在 types/apis 中定义请求和返回值类型
  • 注释尽量精简,减少DEBUG的输入内容
# Claude Code Project Rules

## YAGNI Principle (You Aren't Gonna Need It)

Always adhere to the **YAGNI** principle throughout development:

- **Only implement what’s currently required** – avoid building features that *might* be useful later  
- **Avoid over-engineering** – don’t introduce unnecessary complexity  
- **Keep it simple** – focus on solving the current problem efficiently  
- **Iterate incrementally** – extend functionality only when there’s a real need  

---

## Practice Guidelines

### 1. Feature Development
- Implement **only explicitly requested** features.  
- Avoid speculative or “future-use” functionality.  
- Defer abstractions or refactors until they’re directly justified.

### 2. Code Style & Structure
- Follow the **project structure** below for creating and organizing files.  
- **Variable naming**: use concise, readable **camelCase** throughout.  
- Keep code logic minimal and avoid unnecessary layers or wrappers.

### 3. Styling
- Prefer **TailwindCSS** for basic and medium-complexity styles.  
- For overly complex conditions, use the `cn()` utility for class merging.  
- Inline CSS should be minimized.

### 4. Icon Usage
- Prefer **Iconify**’s `mdi` and `lucide` icon sets.  
- For special icons, download and store them under `public/icons`.

### 5. Hooks
- Always prefer **`ahooks`** where applicable (e.g. `useSetState`, `useRequest`).  
- Reduce the number of **global custom hooks** unless they are reused across multiple pages.  
- Keep hooks simple, composable, and context-specific.

### 6. State Management
- Use **Zustand** for all state management.  
- Configure **`persist`** and **`immer`** if persistence or immutability is required.  
- Keep global state minimal; prefer local state where possible.

### 7. API Layer
- All network requests must be defined under `src/apis`.  
- For each request, define corresponding **request and response types** under `src/types/apis`.  
- Use consistent API naming and avoid inline axios/fetch calls in components.

### 8. Comments & Debugging
- Keep comments **minimal and essential** — explain *why*, not *what*.  
- Avoid excessive `console.log` or debug statements; remove them before commit.

---

## Project Structure

├── public               # Public static assets
│   ├── favicon.ico        # The icon shown on the browser tab
│   ├── fonts              # Font files
│   ├── icons              # Custom-designed icon set
│   └── images             # Image resources
├── src
│   ├── app               # Page view files (Next.js feature: file-based routing)
│   │   ├── page.tsx        # Page component file
│   │   └── layout.tsx      # Layout component file
│   ├── components        # Shared component library
│   │   ├── animate-ui      # Animation UI components
│   │   └── ui              # shadcn-ui component library
│   ├── apis              # API request and response modules
│   ├── hooks             # Custom React hooks
│   ├── constants         # Static configurations and constants
│   ├── utils             # Utility functions
│   ├── stores            # State management (using Zustand)
│   ├── styles            # Global styles
│   │   ├── custom.css      # Custom CSS styles
│   │   ├── fonts.css       # Imported font styles
│   │   ├── index.css
│   │   ├── scrollbar.css   # Custom scrollbar styles
│   │   └── tailwind.css    # Tailwind CSS base styles
│   └── types             # Type definitions
│       ├── apis            # Type definitions for API requests and responses
│       └── entity          # Entity definitions, e.g., User