为什么我们项目很少出现“技术债”?我总结了 5 个前端工程的基本约束

10,116 阅读4分钟

不是说因为我们写得多牛,而是我们从第一天就立了 5 个基本工程约束,这些规则不是高深技术,而是工程底线。坚持这几个点,不一定能写出完美代码,但至少能避免那种“越写越乱、越维护越痛苦”的局面。


1. 所有逻辑必须可测试 —— 哪怕只是基本的 mock

“写业务不用写单测吧?”

我们早期的确也没有测试,但我们有个共识: “可测性”是模块合格的基础能力

例如一个 API 请求函数:

// ❌ 直接在组件里调用
const fetchUser = async () => {
  const res = await axios.get('/api/user')
  setState(res.data)
}

我们会主动抽出:

// ✅ utils/api/user.ts
export async function fetchUser() {
  const res = await request.get('/api/user')
  return res.data
}

组件中调用:

const load = async () => {
  const user = await fetchUser()
  setState(user)
}

哪怕这个模块你暂时没写测试,也可以未来 mock。不是写不写测试的问题,是结构上必须可测。


2. 所有配置和工具链都必须文档化 & 脚手架化

配置复杂是技术债的第一大来源。

我们内部所有 Vite、ESLint、TSConfig、Stylelint、commitlint、husky 的配置,全部在一个 template/config 仓库里,并通过 create-project CLI 自动生成。

新建项目流程:

npx @our-team/create-project my-app

就能生成带全套约束的工程结构。

每次有新成员,只需要一句话:

“请从 CLI 拉起工程模板,手动搭项目我们不维护。”

我们甚至把 .editorconfig.prettierignore.vscode/settings.json 也包进去。


3. 拒绝“一次性”的逻辑代码

我们总结过一个现象:

项目中 90% 的“技术债”,其实不是写得差,而是“写了个一次性的东西,结果被复用起来了”。

例如写一个上传组件,临时加了个 hook:

useEffect(() => {
  document.body.style.overflow = 'hidden'
  return () => {
    document.body.style.overflow = ''
  }
}, [])

这个代码在某个场景 OK,后来别人复制过去也用了它,但页面结构不同,结果出现滚动异常。

我们现在的做法是:一切不是“组件通用”的逻辑,必须写注释说明场景适用条件。

甚至工具函数开头会写:

/**
 * 仅适用于弹窗场景中,用于锁死背景滚动
 * 不适用于嵌套 iframe 或 shadow DOM 中
 */

防止“拿来就用”的行为。


4. 不允许多人维护的页面使用匿名函数 / 闭包乱穿

我们有条硬性规定:

所有 team-shared 页面(如 layout、dashboard、全局 hook)不允许存在闭包调用状态更新函数,必须封装成独立方法。

这是我们以前踩过的一个坑:

const [count, setCount] = useState(0)

useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1) // 🚨 这个 count 其实永远是 0
  }, 1000)
}, [])

重构后:

useEffect(() => {
  const timer = setInterval(increment, 1000)
  return () => clearInterval(timer)
}, [])

function increment() {
  setCount(prev => prev + 1)
}

不仅闭包安全,而且容易测试、易于代码搜索。

这也是我们不允许“匿名箭头函数当核心业务函数”的原因之一:可维护性远比可写性重要。


5. 所有可复用组件都必须写 usage demo

我们早期封装了一堆组件,但文档没人写,最后大家都懒得用,干脆自己手撸。形成了大量重复组件。

后来的约束是:

  • 每个 components/ 下的组件目录,都必须包含 __demo__/index.tsx
  • 内部项目自建 Storybook,开发时自动引入展示
  • 提交 CI 时,若组件没有 __demo__,CI 报错

这样强制你思考组件边界、props 设计、可控状态,并且顺带解决了一部分测试问题。

例如:

/components
  /UserCard
    index.tsx
    style.scss
    __demo__
      index.tsx  ← 示例文件,必须有

最后的话 🙂

我们不是靠“约定”或“大家自觉”来避免技术债,而是靠结构性约束 + 工具自动化来抵御它。

只要你让“混乱”这件事发生一次,下次它就会被复制。

我们总结的经验是: “能自动化的就不要靠人记住,能写进 CLI 的就别写进 README。”

这样,你就不会等到 3 个月后才说:

“唉,这一坨代码也太难维护了吧。”

你们也有这样的感受吗?

📌 你可以继续看我的系列文章