
前端视角下的整洁架构:理论不是空中楼阁
一提“架构”,很多前端同学可能觉得这是后端的事,咱们不就是写写页面、调调接口吗?其实这话只说对了一半——前端项目随着业务复杂度上升,架构问题会比后端更棘手。你想啊,后端有明确的分层(Controller、Service、DAO),前端呢?UI组件、状态管理、API调用、业务逻辑全揉在一起是常态,这就像把厨房、卧室、客厅全打通,看着宽敞,住久了绝对乱成一锅粥。
那整洁架构到底是啥?说白了就是“让核心逻辑稳如老狗,外面怎么变都不怕”。《Clean Architecture》那本书里讲的“依赖规则”,放在前端也一样管用:内层不依赖外层,外层依赖内层。这话听着抽象,我给你翻译成人话:你写的“用户登录验证”“购物车价格计算”这种核心业务逻辑,就不该知道“我用的是React还是Vue”“我调的是Axios还是Fetch”“我状态存在Redux还是Pinia”。就像你做番茄炒蛋,“炒蛋要炒到金黄”这个核心步骤,跟你用铁锅还是不粘锅、煤气灶还是电磁炉没关系,换个工具照样能做。
去年我帮一个朋友的Vue项目做优化,他们之前的代码有多离谱?在Vue组件里,methods
里既有handleSubmit
(调API),又有formatPrice
(价格计算),还有showToast
(弹框提示),甚至localStorage.setItem
(存数据)也混在里面。有次后端改了API返回格式,他们团队5个人改了3天,还漏了两个地方导致线上出了bug。这就是典型的“外层逻辑(API调用、UI交互)和核心逻辑(价格计算)缠在一起”,违反了整洁架构的依赖规则。后来我们把“价格计算”这种核心逻辑抽出来,单独放一个business-logic
文件夹,API请求放api-adapters
,UI交互留在组件里,结果下次后端再改接口,只需要改api-adapters
里的适配层,核心逻辑一行没动,俩小时就搞定了。
可能你会说:“前端不就是要操作DOM、处理用户交互吗?怎么可能让核心逻辑完全独立?”这就要说到前端的“分层智慧”了。咱们可以把前端项目分成四层,从内到外依次是:实体层(Entities)、用例层(Use Cases)、接口适配层(Interface Adapters)、外部层(Frameworks & Drivers)。我给你挨个说清楚:
实体层就是你的“业务核心”,放那些不依赖任何框架的纯JavaScript函数或类。比如电商项目里的“商品信息”(包含名称、价格、库存的结构定义)、“订单计算”(算总价、判断库存是否足够的逻辑)。这层代码应该像“数学公式”一样纯粹,输入确定,输出就确定,跟外面的世界完全没关系。我之前写过一个“优惠券计算”的实体函数,不管是在PC端、H5还是小程序里,只要传商品价格和优惠券规则,就能算出实付金额,换了三个项目都直接复用,这就是实体层的价值。
用例层是“业务流程编排”,负责把实体层的逻辑串起来完成具体功能。比如“用户下单”这个用例,就要调用实体层的“库存检查”“价格计算”,再通过适配层调用“创建订单API”“扣减库存API”。这层也不依赖框架,但它知道“要做什么”,比如“下单前必须先检查库存,库存不够就返回错误”。我见过一个项目把这部分逻辑写在Vuex的action里,结果后来换了Pinia,整个下单流程都得重写,这就是把用例层和外部框架(状态管理库)绑死了,踩了大坑。
接口适配层是“翻译官”,负责把外层的数据格式转换成内层能认的样子,反之亦然。比如后端返回的API数据是{ prod_id: 1, prod_price: 99 }
,但实体层需要的是{ id: 1, price: 99 }
,适配层就要做这个转换;或者组件里的表单数据({ username: '张三', pwd: '123' }
),要转换成用例层需要的{ account: '张三', password: '123' }
,也是适配层的活。这层就像电源适配器,不管外面是220V还是110V,到了里面都是设备需要的电压。
最外层就是咱们熟悉的“框架和工具”,React/Vue组件、Redux/Pinia状态管理、Axios/Fetch请求库、localStorage/sessionStorage存储,都在这一层。这层可以随便换,只要适配层做好“翻译”,内层逻辑完全不受影响。就像你换手机壳,手机本身的功能一点都不会变。
可能你会觉得:“分这么多层,会不会增加代码量?”我实话实说,初期确实会多写几行代码,但长期来看,维护成本会降得很低。就像你整理衣柜,把上衣、裤子、袜子分开,一开始花时间,但以后找衣服效率高多了。我之前那个电商项目重构时,代码量确实多了20%,但半年后团队做了个调研,大家一致觉得“现在改代码心里有底了”,这比少写几行代码重要多了。
五步落地法:从0到1搭建前端整洁架构
光说理论太空泛,咱们直接上干货——五步就能把整洁架构落地到你的前端项目里,我去年带团队重构就是这么一步步来的,亲测有效,你跟着做,哪怕是老项目也能平滑过渡。
第一步:用“用户故事”拆需求,找到核心实体
很多人做架构设计,上来就想“我用什么文件夹结构”,这是本末倒置。架构是为业务服务的,先搞清楚“你要解决什么问题”,才能知道“怎么设计架构”。我的经验是:用“用户故事”把需求拆解开,从故事里找出“不变的核心”——这就是你的实体层。
具体怎么做?拿“用户登录”这个功能举例子,先写用户故事:“作为用户,我输入账号密码,系统验证通过后跳转到首页,验证失败则提示错误”。从这个故事里,你能看到哪些是“不变的核心”?“验证账号密码是否正确”——这就是核心业务逻辑,不管你用什么框架、什么UI库,这个逻辑都不会变。哪些是“会变的部分”?“输入框的样式”“错误提示的弹框类型”“跳转的路由方式”——这些就是外层的东西。
然后把“不变的核心”抽象成实体。比如登录功能里,“用户账号信息”可以定义一个UserAccount
实体,包含account
(账号)、password
(密码)两个属性,再写一个validateUser
函数,接收UserAccount
和后端返回的用户数据,返回{ isValid: true, userId: 123 }
或{ isValid: false, error: '账号或密码错误' }
。这个函数就是纯JavaScript,不引用任何React/Vue的API,甚至不依赖axios
,就像写数学题一样纯粹。
我去年做的那个项目,一开始团队没人重视这一步,直接就开写组件,结果写了两周又推翻重来。后来我们逼着自己每个功能先写3个用户故事,再从中抽实体,虽然多花了两天,但后面开发顺得不行,因为大家对“核心逻辑是什么”有了共识。你也可以试试,哪怕用Excel表格把用户故事和实体列出来,都会清晰很多。
第二步:按“依赖规则”搭骨架,划清层与层的边界
实体找到了,接下来就要搭文件夹结构,这一步的关键是“让内层不依赖外层”。很多前端项目喜欢按“技术类型”分文件夹,比如components/
(组件)、api/
(接口)、utils/
(工具),但这样很容易出现“组件里直接调api,api里又引用utils,utils里还import组件”的混乱依赖。整洁架构推荐按“业务分层”来组织,我通常会这样建文件夹:
src/
├── core/ // 核心层(内层)
│ ├── entities/ // 实体层:业务模型和核心逻辑
│ └── useCases/ // 用例层:业务流程编排
├── adapters/ // 适配层(中间层)
│ ├── api/ // API适配:处理接口请求和数据转换
│ ├── state/ // 状态适配:连接核心层和状态管理库
│ └── ui/ // UI适配:连接核心层和UI组件
└── frameworks/ // 外部层(外层)
├── components/ // UI组件:React/Vue组件
├── router/ // 路由配置:React Router/Vue Router
└── store/ // 状态库:Redux/Pinia
重点来了:只有外层能import内层,内层绝对不能import外层。比如core/entities/
里的文件,不能出现import { useStore } from '../frameworks/store'
这种代码;adapters/api/
可以importcore/useCases/
的用例,但不能被core/
import。怎么确保这一点?你可以在ESLint里配一条规则:禁止core/
目录下的文件importadapters/
或frameworks/
的内容,我之前就是这么做的,谁不小心写反了依赖,IDE直接标红,想犯错都难。
举个具体例子:core/useCases/loginUser.js
(用例层)需要调用API获取用户数据,它不能直接import api from '../../adapters/api'
,而是应该“声明接口”,让适配层来实现。比如用例层定义一个loginUser
函数,接收一个apiClient
参数:
// core/useCases/loginUser.js
export function loginUser(userAccount, apiClient) {
// 核心逻辑:调用实体层验证,再通过apiClient调用接口
const validation = validateUser(userAccount);
if (!validation.isValid) return validation;
return apiClient.getUser(userAccount);
}
然后在适配层实现apiClient
:
// adapters/api/loginAdapter.js
import axios from 'axios';
import { loginUser } from '../../core/useCases/loginUser';
export async function loginUserAdapter(userAccount) {
const apiClient = {
getUser: (account) => axios.post('/api/login', account).then(res => res.data)
};
return loginUser(userAccount, apiClient);
}
这样一来,用例层完全不知道apiClient
是用axios还是fetch实现的,以后想换请求库,只改适配层就行。这就是“依赖注入”的思想,听着高级,其实就是“把会变的部分当参数传进去”,前端同学完全能掌握。
第三步:用“适配层”隔离变化,让核心逻辑稳如泰山
适配层是整洁架构的“防护盾”,专门处理“内层看不懂的外层数据”和“外层需要的内层数据”。前端最常见的适配场景有三个:API数据转换、状态管理适配、UI交互适配,咱们一个个说。
API数据转换
——后端返回的数据格式,十有八九和你实体层定义的不一样。比如后端返回{ user_name: '张三', user_age: 20 }
,你实体层需要{ name: '张三', age: 20 }
,这时候适配层就要做“翻译”。我通常会写一个transformApiData
函数,专门处理这种转换,比如:
// adapters/api/transformers.js
export function transformUserApiData(apiData) {
return {
name: apiData.user_name,
age: apiData.user_age,
id: apiData.user_id
};
}
然后在API适配层调用这个函数:
// adapters/api/userApi.js
import { transformUserApiData } from './transformers';
export async function getUser(account) {
const res = await axios.post('/api/login', account);
return transformUserApiData(res.data);
}
这样实体层拿到的永远是统一格式的数据,后端随便改字段名,你只改这个transform函数就行。去年我们项目后端团队搞“接口规范化”,把所有user_
前缀改成u_
,全团队就我一个人改代码,俩小时搞定,因为所有转换逻辑都在适配层,核心层完全没动。
状态管理适配
——现在前端都用状态管理库(Redux、Pinia等),但状态库是会变的,所以核心逻辑不能直接依赖它们。正确的做法是:用例层返回业务结果,适配层把结果同步到状态库。比如登录成功后,用例层返回{ userId: 123, name: '张三' }
,状态适配层负责调用store.dispatch({ type: 'LOGIN_SUCCESS', payload: ... })
。
我见过最离谱的代码是把用例逻辑写在Redux的thunk里,比如:
// 反面例子:用例逻辑依赖Redux
export const loginThunk = (account) => async (dispatch) => {
dispatch({ type: 'LOGIN_LOADING' });
try {
const res = await axios.post('/api/login', account);
// 这里直接写业务逻辑,和Redux强耦合
if (res.data.user_age < 18) {
dispatch({ type: 'LOGIN_ERROR', payload: '未成年不能登录' });
return;
}
dispatch({ type: 'LOGIN_SUCCESS', payload: res.data });
} catch (e) {
dispatch({ type: 'LOGIN_ERROR', payload: e.message });
}
};
这种代码,以后想换Pinia,或者不用状态库了,整个登录逻辑都得重写。正确的做法是用例层返回结果,适配层处理状态:
// core/useCases/loginUser.js(用例层,纯逻辑)
export function loginUser(userAccount, apiClient) {
// ...验证逻辑...
const userData = await apiClient.getUser(userAccount);
if (userData.age < 18) {
return { success: false, error: '未成年不能登录' };
}
return { success: true, data: userData };
}
// adapters/state/loginStateAdapter.js(适配层,连接状态库)
import { loginUser } from '../../core/useCases/loginUser';
import { useStore } from '../../frameworks/store';
export async function loginAndUpdateState(userAccount) {
const store = useStore();
store.dispatch({ type: 'LOGIN_LOADING' });
const result = await loginUser(userAccount, apiClient);
if (result.success) {
store.dispatch({ type: 'LOGIN_SUCCESS', payload: result.data });
} else {
store.dispatch({ type: 'LOGIN_ERROR', payload: result.error });
}
return result;
}
这样不管状态库怎么变,用例层的loginUser
函数永远能用,这才是“核心逻辑稳如老狗”。
UI交互适配
——组件只负责渲染和用户交互,不处理业务逻辑。比如登录按钮的点击事件,组件里只需要调用适配层的函数,而不是直接写业务逻辑:
// frameworks/components/LoginForm.jsx(UI组件,外层)
import { loginAndUpdateState } from '../../adapters/state/loginStateAdapter';
function LoginForm() {
const [account, setAccount] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async () => {
// 只调用适配层,不写业务逻辑
const result = await loginAndUpdateState({ account, password });
if (!result.success) {
alert(result.error); // 交互逻辑留在组件
}
};
return (
setAccount(e.target.value)} />
setPassword(e.target.value)} />
);
}
这样组件变得非常“轻”,以后想把按钮改成蓝色、把输入框换成自定义组件,都不会影响登录逻辑,维护起来不要太爽。
第四步:老项目平滑迁移?试试“渐进式替换”
如果你接手的是老项目,代码已经一团糟,别想着“推翻重写”——风险太高,老板也不会同意。我的经验是用“渐进式替换”,像给老房子翻新一样,一间一间来,不影响整体居住。
具体怎么做?先选一个“边缘功能”下手,比如“用户资料修改”,这个
你琢磨琢磨,要是改个小功能就得像拆俄罗斯套娃一样,一层层往下扒拉文件,那十有八九就是架构该调整了。我之前帮朋友看一个CRM系统的代码,就遇到过特离谱的情况:客户想把“提交订单”按钮的文案从“确认支付”改成“立即结算”,结果他不光改了按钮组件的label
,还得动API请求里的actionType
参数(因为后端根据这个字段统计埋点),甚至连状态管理库里的orderStatus
枚举值都得跟着改——就为了改几个字,碰了4个文件,差点把线上其他功能搞挂。这种“牵一发而动全身”的情况,说白了就是核心逻辑和UI、接口、状态这些外层东西缠太紧了,这时候不上整洁架构,以后只会越来越难拆。
再说说新人上手的问题,要是团队里来个新人,一周过去了还在问“这个接口调用到底是在组件里还是在store里”,那也得警惕。我带过一个5人小团队,之前项目没做架构分层,组件里既有handleClick
调接口,又有formatMoney
算价格,还有localStorage.setItem
存数据,新人来了光看懂一个“用户下单”流程就得扒拉20多个文件。后来用整洁架构分层后,新人第一天看实体层的价格计算逻辑,第二天看用例层的下单流程,第三天就能跟着适配层的示例代码写新功能了——不是新人突然变厉害了,是架构把“该看什么、不该看什么”划得清清楚楚,就像给迷宫画了张地图,自然走得快。
最后一个信号,你得看看线上bug里,到底有多少是“逻辑错误”。比如用户反馈“优惠券用不了”,一查发现是价格计算时没考虑满减门槛,这就是逻辑错误;要是反馈“按钮位置歪了”,那是UI展示问题,改改CSS就行。我去年统计过,架构混乱的项目里,逻辑错误能占总bug的60%以上,而且修复起来特别费劲——你得先在几百行组件代码里找到计算逻辑,改完还得担心影响其他地方。就像文章里说的那个电商项目,重构前逻辑错误占比58%,重构后降到22%,光这一项就省了团队每周1.5天的bug修复时间。所以啊,要是你项目里三天两头出“算错钱”“权限判断不对”这种硬伤,别犹豫,赶紧用整洁架构把核心逻辑保护起来。
小型前端项目也需要用整洁架构吗?
不一定需要严格按四层架构划分,但核心思想(隔离核心逻辑、减少外部依赖)依然适用。比如一个简单的表单提交功能,至少可以把“表单验证逻辑”抽成独立函数(实体层),和UI组件(外层)分开。我之前帮朋友做的个人博客项目(只有5个页面),就用了简化版架构——把评论提交、文章渲染的核心逻辑抽离,后来他想从Vue换成React,只改了外层组件,核心逻辑一行没动,两周就完成了迁移。
如何判断当前项目是否需要引入整洁架构?
当出现这3个信号时,就该考虑了:
前端框架(如React/Vue)更新时,整洁架构会受影响吗?
几乎不受影响。根据“依赖规则”,核心逻辑(实体层、用例层)不依赖具体框架,框架更新主要影响外层的“框架与驱动层”(如组件写法、路由配置)。比如从Vue2升级到Vue3时,只需修改适配层中“UI组件如何调用用例层”的部分,核心的“购物车计算”“用户权限判断”逻辑完全不用动。我团队去年从React 17升到18,就是这么操作的,3天完成升级,核心业务零故障。
实施整洁架构会拖慢开发进度吗?
初期可能会增加10%-20%的代码量(主要是适配层逻辑),但长期来看开发效率会反超。比如文章中提到的电商项目,重构前开发一个新功能平均要5天,重构后缩短到2.3天,且线上bug率降了62%。这就像整理衣柜——刚开始花时间分类,但后续找衣服的效率会高很多,尤其适合生命周期超过1年的项目。
有没有工具能帮团队遵守整洁架构的分层规则?
有3类工具可以用: