普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月21日首页

AI 生成的代码都是一坨屎?聊聊怎么给 Agent 制定工程约束

作者 ErpanOmer
2026年4月21日 11:35

Gemini_Generated_Image_jrfm35jrfm35jrfm (1).png

有次前端架构评审会上,隔壁业务线的一个前端组长说:他们团队上个月全员接入了某款国产当红(不用猜,肯定时 GLM 🤣🤣)的 AI 编码 Agent,一开始开发效率确实起飞了,原来排期三天的页面,一天就能搞定。但好景不长,上周他们打算对一个核心模块进行迭代时,发现根本无从下手。

因为那个 Agent 为了实现一个极其复杂的带级联筛选的表格,硬生生在一个 .tsx 文件里堆了快 2000 行代码。里面塞满了不知道从哪里抄来的正则、极其随意的变量、以及深达四层的嵌套回调。

他在会上总结陈词:AI 生成的代码就是一坨屎👎👎,只能做做外包和 Demo,根本上不了大型企业的生产环境。

我在台下听得直摇头,心想:兄弟,是不是你自己没有用好呢?😖

做了这么多年前端,我带过不少实习生和初级开发。你回忆一下,如果你丢给一个刚毕业的新人一个复杂需求,不给他讲团队规范,不给他看底层的 Utils 封装,不给他做 Code Review,他写出来的东西,是不是跟现在 AI 生成的屎山一模一样?

AI 就像一个拥有无限精力和极快的手速,但完全没有工程常识的初级程序员。 它脑子里装满了 GitHub 上的开源代码,但它不知道你们公司的网络请求需要怎么带 Token,不知道你们的 UI 规范规定主色调必须用 Tailwind 的哪个变量,更不知道你为了防竞态条件专门写过一个自定义 Hook

如果你只是在输入框里敲一句帮我写一个用户列表,然后就等着一键合并代码,那你不被屎山淹没谁被淹没?

到了 2026 年,一个合格的高级前端,核心能力早就不是比拼敲键盘的速度了,而是如何像 Tech Lead 一样,给你的 AI Agent 制定严苛的工程约束。

咱们直接拿真实场景讲清楚。


先说说,不受约束的 AI 是怎么写代码的?

假设你让 Agent 写一个 带防抖的搜索框,然后去后端拉取数据。一个没有任何工程约束的 Agent,凭着它的原始本能,大概率会给你生成这样的代码👇:

20260421104111_rec_.gif

// 毫无工程约束的 AI 垃圾代码
import React, { useState, useEffect } from 'react';

const SearchLocation = () => {
  const [query, setQuery] = useState('');
  const [locations, setLocations] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeoutId;
    if (query) {
      setLoading(true);
      // 让它手写防抖,极其容易漏掉清理函数导致内存泄漏
      timeoutId = setTimeout(() => {
        // 它直接裸调 fetch,完全无视项目里封装好的 Axios 拦截器
        fetch(`https://api.company.com/locations?q=${query}`, {
          headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } // 这样安全吗?😢
        })
          .then(res => res.json())
          .then(data => {
            setLocations(data);
            setLoading(false);
          });
      }, 500);
    }
    return () => clearTimeout(timeoutId);
  }, [query]);

  return (
    // 直接写死 16px、#333 这种样式,破坏设计系统
    <div style={{ padding: '16px', background: '#fff' }}>
      <input 
        value={query} 
        onChange={e => setQuery(e.target.value)} 
        style={{ border: '1px solid #ccc', borderRadius: '4px' }}
      />
      {/* 渲染列表 */}
    </div>
  )
}

代码能跑吗?完美运行。 但如果你的项目里全充斥着这种裸调 API、手写样式、自己控制防抖生命周期的代码,这个项目活不过三个月🤷‍♂️。


把规范写进 Agent 的基因里

现在主流的 AI 工具(无论是 Cursor 还是跑在本地的 Agent),都已经支持了全局的规则配置,也就是我们常说的 .cursorrules 或者 System Prompt

给 AI 立规矩,本质上就是把你作为老兵的架构品味,强制注入到它的上下文中。

我是怎么约束我手底下的 Agent 的?我会直接在根目录下写死一份极度严苛的工程约束文档。每次让它写代码前,它必须先过这四道防线:

Gemini_Generated_Image_buq06hbuq06hbuq0.png

规则一:严禁私自造轮子,强制依赖倒置

禁止 Agent 自己写原生的 fetch 或者手搓防抖节流。我会明确告诉它:项目中所有的异步请求必须从 @/utils/request 引入,所有的防抖机制必须使用 ahooks 里的 useDebounce

规则二:状态与 UI 强制分离

只要组件超过 100 行,强制要求 Agent 把复杂的 useStateuseEffect 抽离成独立的 useXxx 自定义 HookJSX 部分只允许有纯粹的渲染逻辑。

规则三:锁死样式系统

严禁使用行内样式和传统的 CSS 文件。强制规定:必须且只能使用 Tailwind CSS。如果是特定的颜色值,必须使用 tailwind.config.js 里定义好的主题变量,比如 text-primary-500

规则四:安全底线

严禁在组件内部直接读取 localStorage 获取 Token。所有鉴权状态必须从全局的 Store 或者专门的 useAuth 钩子里拿。


约束后,你的代码品味就上来了

当你在 Agent 的配置里锁死了上面这些工程约束,你再给它下达完全一样的开发指令帮我写一个搜索框。

你来看看这次它给你的反馈是什么级别的:

// 经过工程约束后的 AI 生成代码
import React, { useState } from 'react';
import { useDebounce } from 'ahooks';
import { fetchLocations } from '@/api/location'; // 强制使用了统一的 API 封装
import { useAuth } from '@/hooks/useAuth'; // 强制使用了鉴权 Hook

// 约束生效:UI 与逻辑彻底分离
const useLocationSearch = () => {
  const [query, setQuery] = useState('');
  const [locations, setLocations] = useState<Location[]>([]);
  const [loading, setLoading] = useState(false);
  const { getToken } = useAuth(); // 安全规范生效

  const debouncedQuery = useDebounce(query, { wait: 500 });

  React.useEffect(() => {
    if (!debouncedQuery) return;
    
    const loadData = async () => {
      setLoading(true);
      try {
        // 网络层封装生效,无需关心 Token 拼接
        const data = await fetchLocations(debouncedQuery, getToken());
        setLocations(data);
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };
    loadData();
  }, [debouncedQuery, getToken]);

  return { query, setQuery, locations, loading };
};

export const SearchLocation = () => {
  const { query, setQuery, locations, loading } = useLocationSearch();

  // 严格使用了 Tailwind 设计系统
  return (
    <div className="p-4 bg-white rounded-lg shadow-sm">
      <input
        type="text"
        className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="search locations..."
      />
      {/* 渲染列表 */}
    </div>
  );
};

发现区别了吗? 第二段代码,模块极其清晰,网络请求、样式规范、安全校验全部卡在了你设定的架构底线内。这是一段哪怕我现在离职,下家接手时绝对不会骂娘的高质量代码😁。


你在驾驭,还是在旁观?

很多人觉得写 Prompt 是一门玄学,今天用请你扮演一个资深前端专家,明天用深呼吸,一步一步想。 别扯了!在严肃的商业开发面前,这些花里胡哨的咒语(指:AI 生成的代码就是一坨屎) 毫无意义。

真正的 AI 辅助开发,本质上是一次高强度的自动化 Code Review。

你觉得 AI 生成的代码烂,是因为你作为业务的 Owner,主动放弃了你的系统架构权。

你能不能把项目里乱七八糟的依赖清理干净?

你能不能提炼出极其精准的 TS 类型定义?

你能不能给那些跑在服务器上的自动化 Agent,画出一道道不可逾越的护城河,让它们在这个安全的沙盒里为你疯狂产出?🤔

AI 永远不会写出屎山。写出屎山的,是那个连架构边界都划不清楚,只知道疯狂按 Tab 键和回车的:

你!You !🫵🫵🫵

昨天 — 2026年4月20日首页

为什么我不建议普通前端盲目卷全栈?

作者 ErpanOmer
2026年4月20日 12:00

Gemini_Generated_Image_s62j8ys62j8ys62j (1).png

周末,一个半年前从我们组离职去当 独立开发者 的小伙子,突然约我出来喝了顿大酒。

半年前他提离职的时候,眼里是有光的。当时他手里拿着一个用 Next.js + Node + MongoDB 拼凑出来的 AI 翻译 SaaS 雏形,满脸兴奋地跟我说,现在有了 AI 辅助,前端搞全栈简直易如反掌,他马上就要去赚美金、做数字游民了😊。

半年后的饭桌上,他头发肉眼可见地稀疏了,整个人透着一股被掏空的疲惫😢。

我问他产品跑得怎么样? 他倒了一堆苦水: 上线第二周,因为忘了配置 MongoDB 的白名单,数据库被黑产端了,留了个比特币勒索地址; 换了云数据库后,上个月被海外羊毛党用并发脚本刷爆了注册接口,由于 Node.js 端没做事务锁和限流,AI 服务的 Token 余额一夜之间被刷欠费了两千多刀。

他长叹一口气:老大,写后端真他妈不是人干的活😖😖😖。

这两年,前端圈有一股极其狂热的风气,大厂在逼着前端转全栈,各类博主在教你怎么用 Cursor 一键生成后端 API,似乎只要会写几句 JS,连上个数据库,你就能凭一己之力抗下整个商业闭环。

但作为一个写了 9 年代码、搞过出海独立站、也无数次给新人擦过屁股的老兵,我今天必须把这层窗户纸捅破: 绝大多数普通前端理解的全栈,根本就是个一戳就破的纸老虎。我不建议你盲目去卷全栈🤷‍♂️。


你以为的全栈,只是在写玩具后端

很多前端对后端的认知,还停留在用 Express 或者 NestJS 写一个 router.get('/api/user'),然后调用一下 ORM 查查数据库。代码能跑通,能返回 JSON,就觉得自己是全栈了。

这是巨大的错觉。

真实的后端工程,最难的从来不是业务逻辑(CRUD),而是高并发下的数据一致性、资源隔离与灾备防御。

举个最经典的例子。很多刚转全栈的前端,在处理 用户消耗积分调用 AI 这个逻辑时,代码往往是这么写的:

// 前端思维写出来的后端代码
app.post('/api/generate', async (req, res) => {
  const user = await User.findById(req.userId);
  
  // 判断余额
  if (user.points < 1) {
    return res.status(403).send('积分不足');
  }
  
  // 扣减积分并保存
  user.points -= 1;
  await user.save();
  
  // 调用 AI 接口...
});

本地单步调试,毫无问题。 但只要把它扔到线上,稍微遇到点网络延迟,或者有黑客同时发来 10 个并发请求。这 10 个请求会同时读到 user.points === 1,然后各自往下执行,最终用户的 1 个积分被成功扣减了 1 次,但你的 AI 接口被免费调用了 10 次。

在真正的后端视野里,这叫竞态条件(Race Condition)。解法是利用数据库层面的原子更新(比如 MongoDB$inc),或者是加分布式锁。

但很多前端根本不懂什么是事务隔离级别,什么是乐观锁悲观锁,什么是慢查询引发的连接池打满。他们拿着一套写 UI 的心智模型去搞后端,最后搭出来的系统,防得住正人君子,防不住任何一次稍微猛烈的流量冲击。


2026 年独立开发者真实生存状况

现在的年轻人动不动就想搞独立 SaaS,觉得有个好点子就能变现。 我带你看一眼 2026 年前端做独立开发者的真实时间线:

Gemini_Generated_Image_xi8phxi8phxi8phx.png

第一周: 激情澎湃,花 5 天时间用 Tailwind CSS 把落地页的动效调得丝滑无比,深色模式完美适配,觉得自己真是个产品天才。

第二周: 开始搭后端环境。在 Docker、Nginx 配置、SSL 证书续签里痛苦挣扎。为了省几十块钱服务器钱,买了个廉价 VPS,每天提心吊胆怕宕机。

第四周: 产品终于上线了。发到 Product HuntV2EX 上,迎来了 500 个独立访客。

第五周: 被俄罗斯或者印度的 Bot 盯上了。恶意脚本疯狂轰炸你的登录接口,你那单节点的 Node.js 进程直接 CPU 飙到 100% OOM 死机。你大半夜爬起来看日志,临时去搜 Node.js 怎么做 IP 频控。

第二个月: 热情耗尽,服务器吃灰,域名到期不续费🤷‍♂️。。。

这才是赤裸裸的真相。 很多前端做独立开发,90% 的精力消耗在了配环境、查后端 Bug、修服务器配置上,真正花在打磨核心产品功能和做营销推广上的时间,连 10% 都不到。

你以为你是产品 CEO,其实你只是个免费的初级兼职运维。


普通前端该怎么破局?学会借力,而不是造轮子

说了这么多,难道前端就只能老老实实切图,彻底告别独立开发和全栈了吗?

错❌❌❌。

我的核心观点是:放弃传统后端的玩法,拥抱 Serverless 和 BaaS(后端即服务)。

2026 年了,前端的护城河绝对不是去学怎么配置 K8s 集群,也不是去死磕如何调优 MySQL 索引。你的核心价值是 极速交付业务逻辑

要做全栈,就把脏活累活全甩给成熟的云基础设施。 比如这两年我在搞出海项目时,几乎抛弃了所有传统的自建 Node 服务器部署 (比如 Render, fly.io),全盘转向了 Cloudflare Workers + D1(Serverless SQLite) 或者 Supabase

不用管服务器运维,不用管 Nginx 负载均衡,自带企业级防 DDOS,把代码推到边缘节点(Edge),全球毫秒级生效。

给你看一眼在 Cloudflare Workers 里,如何用极简的代码实现极其硬核的 IP 频控(Rate Limit),这在传统后端里要搭一套 Redis 才能搞定:

// 基于 Cloudflare 的现代前端全栈玩法
export default {
  async fetch(request, env) {
    const ip = request.headers.get('cf-connecting-ip');
    
    // 调用平台自带的限流服务,一行代码解决防刷问题
    const { success } = await env.RATE_LIMITER.limit({ key: ip });
    if (!success) {
      return new Response('请求过于频繁,请稍后再试', { status: 429 });
    }

    // 处理核心业务逻辑...
    return new Response('业务处理成功');
  }
};

发现了吗?这种工程维度的跨越,才是前端走向全栈的正确姿势。 你不需要去理解底层的流量网关是怎么实现的,你只需要站在巨人的肩膀上,把 API 串起来,把精力留在如何优化用户的产品体验上。


别被技术焦虑绑架

很多技术社区都在制造焦虑,好像你不懂点微服务、不懂点高并发,你就不配做一个现代的前端。

但真实的世界是:没有任何一个商业项目,是因为用了多牛逼的后端架构才成功的;绝大部分死掉的项目,都是因为产品根本没人用,或者在早期就被无意义的基础设施消耗拖垮了团队🤔。

如果你是一个前端,有极强的业务嗅觉,想自己做点东西。 那就用熟你手里的 VueReact,用好 Tailwind 快速构建 UI,把后端托管给 Supabase 或者 Firebase,把边缘逻辑交给 Cloudflare。用两周时间把 MVP(最小可行性产品)跑通,直接推向市场验证🫡。

不要去盲目卷传统后端。 把时间留给产品,留给用户,留给真正的商业闭环。这才是 2026 年,一个有独立思考能力的前端老兵,最该具备的技术品味。

你们说是不是?😊

Suggestion (2).gif

昨天以前首页

别再用 JSON.parse 深拷贝了,聊聊 StructuredClone

作者 ErpanOmer
2026年4月15日 09:58

临近下班,我们业务线出了一个极度无语的线上 Bug。

产品侧反馈,在一个非常核心的财务表单里,用户明明选择了 2026-04-14 作为结算日期,但点击提交后,整个页面直接白屏崩溃。

我打开错误监控看了一眼日志,立刻就把组里那个刚入职不久的小伙子叫了过来。 原因极其经典:他在把表单的原始状态同步给历史快照时,为了图省事,顺手写了一段几乎所有前端都写过的代码:

// 模拟用户表单数据
const formData = {
  amount: 1000,
  date: new Date("2026-04-14"), // 用户选的结算日期(Date对象)
};

// 深拷贝
const snapshot = JSON.parse(JSON.stringify(formData));

console.log("原始:", formData.date, typeof formData.date); 
// Date object

console.log("快照:", snapshot.date, typeof snapshot.date); 
// "2026-04-14T00:00:00.000Z" string

// 后续业务代码
function calcSettlementTime(data) {
  // 这里默认 date 是 Date 对象
  return data.date.getTime();
}

// 页面直接崩溃😢
try {
  const time = calcSettlementTime(snapshot);
  console.log("时间戳:", time);
} catch (err) {
  console.error("页面崩溃:", err);
}

他满脸委屈:老大,大家平时深拷贝不都是这么写的吗?🤷‍♂️

我让他自己把这段代码在控制台跑一遍。 当他看到表单里原本好好的 Date 对象,经过这一进一出,硬生生变成了一串 ISO 格式的字符串,导致后面调用 snapshot.date.getTime() 直接抛出 TypeError 时,他自己也沉默了。

作为前端老油条,这种因为 JSON.parse(JSON.stringify()) 引发的血案,我见过太多了。 它不仅会把 Date 变成字符串,还会把 MapSet 变成空对象 {},会把 undefinedSymbol 以及函数直接活生生抹除,更别提遇到循环引用时,它会当场抛出异常让你的主线程直接崩溃。

以前,我们为了解决这个破事,不得不在每个项目里老老实实 npm install lodash,然后引入那个笨重的 cloneDeep

但现在是 2026 年了。浏览器早就原生内置了完美的终极解药——structuredClone

今天咱们不聊虚的架构,就花三分钟,把这个原生 API 的底层逻辑讲清楚。


它是怎么解决历史遗留问题的?

structuredClone 不是什么语法糖,它是浏览器底层暴露出来的 结构化克隆算法(Structured Clone Algorithm)。这就意味着,它在 C++ 引擎层面的处理逻辑,远比 JS 业务层面的递归拷贝要深得多。

看一下原生 API 的用法:

const original = {
  date: new Date(),
  set: new Set([1, 2, 3]),
  map: new Map([['key', 'value']]),
  regex: /hello/i,
  buffer: new Uint8Array([1, 2, 3]).buffer,
};

// 制造一个循环引用
original.self = original;

// 一行代码,原生搞定
const cloned = structuredClone(original);

console.log(cloned.date instanceof Date); // true
console.log(cloned.set instanceof Set);   // true
console.log(cloned.self === cloned);      // true 完美处理循环引用!

发现没有?它不仅完美保留了所有的内置对象类型,连 JSON.parse 绝对搞不定的循环引用,它都处理得游刃有余。由于是在引擎底层运行,不需要像 Lodash 那样在 JS 运行时里疯狂压栈递归,它的执行效率在大部分复杂场景下都具有压倒性优势👍👍👍。


零拷贝转移 (Transferable Objects)

如果你以为 structuredClone 只是为了少引入一个 Lodash,那你就太小看浏览器的底层野心了。

它藏着一个 90% 的前端都不知道的极其硬核的功能:内存转移(Transfer)

在前端处理音视频、WebGL、或者读取几十 MB 的大文件时,我们经常会生成巨大的 ArrayBuffer。如果你用传统的深拷贝,内存瞬间翻倍,几十兆的内存分配极容易引起页面的掉帧卡顿。

structuredClone 提供了一个极其变态的第二个参数配置:{ transfer }

// 假设这是一个极大的 50MB 数据内存块
const u8Array = new Uint8Array(1024 * 1024 * 50);
const hugeBuffer = u8Array.buffer;

// 传统的深拷贝:内存翻倍,耗时极长
// const badCopy = lodash.cloneDeep(hugeBuffer); 

// 直接内存转移
const fastClone = structuredClone(hugeBuffer, { transfer: [hugeBuffer] });

console.log(fastClone.byteLength); // 52428800 (50MB 完美转移)
console.log(hugeBuffer.byteLength); // 0 (原对象的内存地址被转移)

这段代码的核心在于:它压根没有复制数据。 它直接在内存层面,把这块 50MB 数据的所有权,从 hugeBuffer 强行转移给了 fastClone。原对象被彻底掏空(变成了 detached 状态)。

这种零拷贝机制,在结合 Web Worker 处理复杂后台计算时,是打破性能瓶颈的绝对神器。这是任何第三方 JS 库都做不到的底层API。


一些坑要讲清楚🤔

既然这么牛,是不是以后项目里所有的拷贝闭着眼睛用它就行了? 作为一个踩过无数坑的老兵,我必须点出它的几个致命死角。如果你在真实的业务架构里滥用,下场比用 JSON.parse 还要惨。

对于函数和 DOM 节点的处理

JSON.parse 遇到函数,它会默默地忽略掉,至少不报错。 但 structuredClone 很直接。只要你的对象树里藏着一个方法,或者藏着一个 DOM 节点的引用,它会直接给你抛出 DataCloneError

const objWithFunc = {
  data: 123,
  onClick: () => console.log('click')
};

// 只要带有函数,直接抛同步错误
// DOMException: () => console.log('click') could not be cloned.
const copy = structuredClone(objWithFunc); 

这就意味着,如果你要拷贝的是一个 Vue/React 的响应式组件实例,或者是带有业务方法的数据模型,绝对不能用它👋。

原型链的断裂

不管你原本是一个通过 class 实例化的多么高级的业务对象,经过 structuredClone 的洗礼后,它都会变成一个普通的纯对象(Plain Object)。

class User {
  constructor(name) { this.name = name; }
  sayHi() { console.log('hi'); }
}

const user = new User('前端');
const cloneUser = structuredClone(user);

console.log(cloneUser instanceof User); // false 
cloneUser.sayHi(); // TypeError: cloneUser.sayHi is not a function

原型链上的所有方法全部丢失。它只关心纯粹的数据,不关心你的面向对象架构。‘


需要时收藏起来⭐⭐⭐

这几年,前端的工具链卷得飞起,大家的 package.json 越来越臃肿。遇到数组去重找库,遇到时间格式化找库,遇到深拷贝也要找库。

如果你只是单纯地处理一些后端传过来的嵌套数据,或者表单的复杂配置结构,完全可以直接把 structuredClone 敲在你的代码里。不用担心兼容性,目前主流浏览器(包括 Node.js)的支持率早就达到了工业级使用的标准了。

image.png

下次 Code Review 时,别再让我看到满屏的 JSON.parse 了 (玩笑😁😁😁)。

分享完毕,谢谢大家🙌

Suggestion.gif

做中后台业务,为什么我不建议你用 Tailwind CSS?

作者 ErpanOmer
2026年4月14日 10:33

1___f27S-qQF2CAASt5bOwqg.png

大家好,我又来了😁

最近我接手了一个隔壁组转过来的中后台重构项目。

交接的时候,对方的技术负责人特意跟我强调,说这个项目采用了最新的技术栈,全面拥抱了 Tailwind CSS,开发体验极其丝滑。

我当时心里还挺期待,毕竟这两年 Tailwind 的风刮得太大了,各种国内外大佬都在疯狂带货。结果周末我抽空把代码拉下来,打开 VSCode 准备梳理一下业务主流程。盯了屏幕不到十分钟,我感觉自己的眼睛快瞎了。

光说理论没意思,我直接给你们截取一段真实的、承载了各种表单校验和状态联动的业务侧边栏组件。你们自己品鉴一下所谓的极致开发体验👇:

const OrderCard = ({ order, isAdmin, isExpanded }) => {
  return (
    <div 
      className={`flex flex-col w-full p-5 mb-4 border rounded-lg shadow-sm transition-all duration-300 ${
        isAdmin ? 'bg-red-50 border-red-200' : 'bg-white border-gray-200'
      } ${
        isExpanded ? 'max-h-[800px]' : 'max-h-24 overflow-hidden'
      } hover:shadow-md cursor-pointer`}
    >
      <div className='flex items-center justify-between pb-3 mb-3 border-b border-gray-100'>
        <span className='text-sm font-semibold text-gray-800 truncate w-[60%]'>
          {order.id}
        </span>
        <span 
          className={`px-2 py-1 text-xs rounded-full ${
            order.status === 'PAID' 
              ? 'bg-green-100 text-green-700' 
              : 'bg-orange-100 text-orange-700'
          }`}
        >
          {order.statusText}
        </span>
      </div>
      {/* 内部极其复杂的业务字段渲染... */}
    </div>
  );
}

代码跑得通吗?当然跑得通。UI 还原度高吗?也挺高。 但是作为接下来要维护这个项目的组长,我只觉得一阵头皮发麻。

很多前端新人,甚至是做惯了 C 端 独立开发的兄弟,对 Tailwind 简直是顶礼膜拜。因为它不用取名字,不用在 JSCSS 文件之间来回切换,写起来确实有快感。

但作为趟过无数中后台项目的深水区,我今天必须给这股跟风热潮泼一盆冷水: 在绝大多数重型中后台业务场景里,Tailwind CSS 并不是什么神兵利器,而是给后期维护带来不方便。

来,咱们拿真实代码说话,看看它到底是怎么摧毁中后台工程的👇。


彻底掩盖你的业务主线?

做中后台系统,最难的从来不是画 UI,而是处理极度复杂的数据状态流转。

上面那段代码最大的问题在于信噪比极低。作为一个接手代码的前端,我点开这个文件,首先想看的是:这个组件在不同权限、不同展开状态下,业务逻辑是怎么走的?

但在 Tailwind 的体系下,我的视线全被 flexp-5mb-4max-h-[800px] 这种毫无业务价值的视觉原子类给强暴了。如果退回到老古董的 CSS Modules 方案,这段代码在 JS 侧应该长什么样?咱们对比一下:

import classNames from 'classnames';
import styles from './OrderCard.module.less';

const OrderCard = ({ order, isAdmin, isExpanded }) => {
  return (
    <div 
      className={classNames(styles.orderCard, {[styles.adminMode]: isAdmin,
        [styles.expanded]: isExpanded
      })}
    >
      <div className={styles.cardHeader}>
        <span className={styles.orderId}>{order.id}</span>
        <span 
          className={classNames(styles.statusBadge, {
            [styles.statusPaid]: order.status === 'PAID',
            [styles.statusPending]: order.status === 'PENDING'
          })}
        >
          {order.statusText}
        </span>
      </div>
      {/* 业务字段一目了然 */}
    </div>
  );
}

发现区别了吗?重构后的 JSX 变得极其纯粹。我只通过 adminModeexpanded 这种类名,就极其清晰地传达了业务语义。至于那个订单编号到底占百分之几十的宽度,那是 UI 层 该关心的事情,它安静地待在 .less 文件里,绝不会来污染我的业务主逻辑。


跟现成的组件库水土不服

中后台业务是不可能脱离 Ant DesignElement Plus 这类重型组件库的。而组件库的本质,是封装好了一整套内部的 DOM 结构和 ClassName 规范。

这就带来了一个极其致命的冲突:当你用 Tailwind 去覆盖 Ant Design 的内部样式时,你会写出极其恶心的 Hack 代码😖。

比如产品经理要求:在这个特定页面里,把 Ant Design 表格的表头背景色改成浅蓝色,单元格的 padding 改小一点。

正常的 Less 做法是用样式穿透,精确打击:

/* 样式文件覆盖 */
.myCustomTable {
  :deep(.ant-table-thead > tr > th) {
    background-color: #f0f8ff;
    padding: 8px 16px;
  }
}

你知道那个拥抱 Tailwind 的小伙子是怎么写的吗?为了不写 CSS 文件,他强行使用了 Tailwind v3+ 的任意变体语法:

// Tailwind 强行覆盖组件库内部样式
<Table 
  className='[&_.ant-table-thead>tr>th]:bg-blue-50[&_.ant-table-thead>tr>th]:py-2 [&_.ant-table-thead>tr>th]:px-4 [&_.ant-table-tbody>tr>td]:text-gray-600[&_.ant-table-tbody>tr>td]:py-2' 
  dataSource={data} 
  columns={columns} 
/>

这段代码合进主分支的时候,我都替后续维护的兄弟感到悲哀😢。

这玩意儿连换行都没有,密密麻麻挤在一起。未来如果要做主题切换,或者升级 Ant Design 导致内部类名变了,谁敢去动这坨连正则都极难匹配的字符串?

不仅没有提高开发效率,反而为了强行凑 Tailwind 的语法,写出了一堆极难维护的代码。


负边距引发的问题

一行代码被写出来的成本,远远低于它在未来三年里被维护的成本。

Tailwind 本质上就是披着 ClassName 外衣的行内样式。它把所有的样式固化在了 HTML 结构上。

设想真实的维护场景,前任开发为了让一个按钮和旁边的输入框对齐,极其随意地写了一个向左偏移负间距的类名:

<Button className='-ml-2 mt-1'>提交</Button>

半年后你接手这个需求,产品要求在这俩元素中间加一个 Icon。你看着这个 -ml-2mt-1 会陷入极其痛苦的挣扎:他当时为什么要写负边距?是因为外层父元素加了错的 padding 导致的?还是为了抵消 Button 内部自带的 margin?🤷‍♂️

在传统 CSS 中,我们往往会留有注释说明抵消输入框自带的右侧留白。但在 Tailwind 里,没有注释的容身之所。

为了保证不出线上 Bug,你绝对不敢删掉那个 -ml-2。你会选择在它后面再打个补丁,加个 pl-4 试图把它顶回来。 第二年,另一个接手的同事遇到了错位,又在后面补了一个 mt-[-5px]🤣🤣🤣。

日积月累,HTML 标签上的类名越来越长,死代码和冲突代码全堆在 DOM 上,最终变成一座没人敢碰的屎山💩。


别瞎搞,先认清你的场景🫡

说了这么多,难道 Tailwind 真的就是垃圾吗?当然绝对不是。

如果你在做偏 C端 的炫酷落地页、做 SaaS 官网,或者你是独立开发者,没有沉重的历史包袱,不需要配合复杂的重型组件库,那 Tailwind 绝对是神作。它自带极其优秀的设计规范,能让你极速堆出好看的界面。

但咱们讨论的是中后台。中后台是干嘛的?团队十几个人来回交接,动辄几百个页面,充斥着极其复杂的表单联动和权限校验,生命周期长达五年甚至十年。

在这种重型项目中,保持业务逻辑的纯粹性,分离关注点,远比你少写几行 CSS 要重要一万倍。

好了,今天分享到这,谢谢大家😁

谢谢大家.gif

❌
❌