./avatar.jpg

前后端协作改造记:从「口头协议」到 Mock 驱动开发

很多项目的前后端协作,一开始都是这样:

「你先把页面搭着,接口我晚点给你。」
「返回字段大概就这些,不行再改。」

结果就是:

  • 接口文档滞后甚至缺失,全靠口头约定;
  • 字段今天叫 userName 明天叫 customerName
  • 前端经常等后端联调,后端也抱怨前端改来改去。

这篇文章是一次真实改造的复盘:我们从这种「口头协议」状态,逐步过渡到:

有规范的接口文档 + 可用的 Mock 服务 + 基于契约的联调流程。


1. 先承认问题:我们缺的不是工具,而是「契约」

一开始我们也装过各种工具:

  • Swagger / OpenAPI;
  • YApi / Rap2 之类的平台;
  • 各种 Mock 插件。

但用一段时间就废掉了,根本原因是:文档没人维护,接口变更不走流程

所以改造的第一步,不是换工具,而是统一几个规则:

  1. 所有新接口必须先有文档,再开发
  2. 文档就是契约:前端后端都按契约来实现;
  3. 接口变更要么走版本,要么走兼容字段,不再「直接改」。

工具只是为了让这套流程变得没那么痛苦。


2. 选择和收敛接口文档工具

我们最终选的是:OpenAPI 规格 + 一套接口管理平台(如 YApi 或内部平台)

  • 提供可视化的接口列表、请求例子、响应示例;
  • 支持一键导出 JSON / SDK 生成;
  • 自带 Mock 功能。

接口粒度大致是:

paths:
  /api/order/list:
    post:
      summary: 订单列表
      parameters: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderListRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderListResponse'

当后端定义好这些 schema 后,前端就可以基于文档做代码生成、Mock 数据、类型定义。

配置驱动页面:表单、表格到「低代码」的第一步

在中后台项目里,经常会出现这样的需求:

  • 一堆「结构相似、字段不同」的表单;
  • 一堆「上面查询、下面表格」的列表页。

如果每个页面都手写一次:表单 + 校验 + 表格列 + 操作按钮,不仅枯燥,还很难保持统一风格。

这篇文章讲的是我在项目里做的一次尝试:

用一份 JSON 配置描述页面,再写一个「解释器组件」负责渲染表单和表格。
这是往低代码方向迈出的第一小步。


1. 先从「配置表单」开始

以一个常见的查询表单为例,我们希望用一份配置来描述:

  • 有哪些字段;
  • 每个字段是什么类型(输入框、下拉、日期范围等);
  • 默认值、占位文案、选项等等。

1.1 配置结构示例

// types.ts
export type FieldType = 'input' | 'select' | 'daterange' | 'switch';

export interface FormField {
  prop: string;
  label: string;
  type: FieldType;
  placeholder?: string;
  options?: Array<{ label: string; value: string | number }>;
  span?: number; // 栅格宽度
  required?: boolean;
}

某个页面里的配置:

// orderListPageConfig.ts
export const searchFields: FormField[] = [
  { prop: 'keyword', label: '关键词', type: 'input', placeholder: '订单号/客户名' },
  { prop: 'status', label: '状态', type: 'select', options: [
    { label: '全部', value: '' },
    { label: '待支付', value: 'PENDING' },
    { label: '已支付', value: 'PAID' },
  ]},
  { prop: 'dateRange', label: '创建时间', type: 'daterange' },
];

1.2 通用 FormRenderer 组件

<!-- FormRenderer.vue示意 -->
<template>
  <el-form :model="model" class="config-form" label-width="90px">
    <el-row :gutter="16">
      <el-col
        v-for="field in fields"
        :key="field.prop"
        :span="field.span || 8"
      >
        <el-form-item :label="field.label" :prop="field.prop">
          <component
            :is="getComponent(field)"
            v-model="model[field.prop]"
            v-bind="getComponentProps(field)"
          />
        </el-form-item>
      </el-col>
    </el-row>

    <el-form-item>
      <el-button type="primary" @click="$emit('search', model)">查询</el-button>
      <el-button @click="onReset">重置</el-button>
    </el-form-item>
  </el-form>
</template>

getComponent 根据类型返回不同组件:

菜单、按钮、接口:一套权限系统怎么落地到前端

中后台项目到了一定体量之后,绕不开一个问题:权限怎么做?

  • 不同角色看到的菜单不一样;
  • 同一个页面里,有人能点「导出」、有人只能看;
  • 接口层还要做真正的权限校验。

这篇文章从实践角度,串一下我现在常用的一套设计:从「角色-权限-资源」模型,到前端路由、按钮显隐,再到接口鉴权。


1. 模型:角色、权限点、资源

先把概念讲清楚:

  • 用户(User):具体的人;
  • 角色(Role):一类权限集合,例如「运营」、「风控」、「管理员」;
  • 权限点(Permission):最小粒度的能力,比如「订单列表查看」「订单导出」;
  • 资源(Resource):被操作的对象,比如菜单、按钮、接口。

关系一般是:

User → 多个 Role → 多个 Permission → 作用在不同 Resource 上

前端最关心的其实只有一件事:

登录之后,我拿到的「权限点列表」是什么?

因为菜单、按钮显隐、前端路由保护,本质上都是围绕这串权限点做判断。


2. 权限点命名:模块 + 资源 + 动作

命名如果乱,后面就会非常难维护。实践中我会用统一格式:

<模块>:<资源>:<动作>

例如:

  • order:list:view —— 订单列表查看;
  • order:list:export —— 订单列表导出;
  • order:detail:update —— 订单详情修改;
  • user:manage:create —— 用户管理新增。

在数据库或配置里,用一个简单的结构描述:

interface Permission {
  code: string;      // order:list:view
  name: string;      // 订单列表-查看
  description?: string;
}

前端拿到的就是一串 code

["order:list:view", "order:list:export", "order:detail:update"]

3. 菜单与路由:根据权限生成

3.1 菜单数据从后端来

让后端维护一份菜单树:

interface MenuItem {
  id: string;
  parentId?: string;
  title: string;
  path: string;
  icon?: string;
  // 访问该菜单需要的权限点(可以是单个或多个)
  permission?: string;
  children?: MenuItem[];
}

登录成功后,前端请求:

一次代码,多端运行:H5 + 小程序的复用实践

多端已经是老话题了:

  • 产品想要 H5 + 小程序 + 后面可能再来个 App
  • 但人手并不会随平台数量线性增加。

所以问题就变成了:

能不能尽量做到:一套业务逻辑,多端复用,只在各端做少量适配?

这篇文章从一个实际项目出发,聊聊我是怎么拆分层次、抽象业务核心、隔离端能力差异,让 H5 和小程序在同一套代码下运行得比较舒服。


1. 能复用和不能复用的先分清

先别谈框架,先想清楚几件事:

通常可以复用的:

  • 业务模型:订单、用户、商品这些核心数据结构;
  • 领域逻辑:状态流转、金额计算、表单校验等纯逻辑;
  • 接口层:请求参数组装、响应数据提取、错误码处理;
  • 公共工具:日期、金额、校验、格式化等。

通常不能直接复用的:

  • UI 组件和布局(DOM vs 小程序组件);
  • 路由与页面生命周期;
  • 存储层(localStorage vs wx.setStorage 等);
  • 原生能力(扫码、相册、位置等)。

所以设计时的目标是:

把「业务核心」从这些平台差异里剥离出来,用一层「适配器」去对接不同端能力。


2. 目录与分层:core + adapters

我们当时的多端项目采用的是类似 Monorepo 的结构:

project-root
├── packages
│   ├── core          # 端无关的业务核心(可被多端复用)
│   ├── h5            # H5 端工程(Vue/React 等)
│   └── miniapp       # 小程序端工程(uni-app / Taro / 原生)
└── package.json

2.1 core 里放什么?

核心模块大致有:

  • models/ —— 订单、用户等类型定义和基础逻辑;
  • services/ —— 业务服务,比如下单、取消订单、查询列表;
  • apis/ —— 针对后端的 API 封装(不依赖具体请求实现);
  • utils/ —— 工具方法。

关键点是:core 不能直接用浏览器 API 或小程序 API,一旦用了就失去可复用性。

2019–2020 我的前端工作流与常用工具清单

每隔一段时间,我都会回头看一下:自己每天在用的工具和工作流,哪些真的有效,哪些只是「习惯了就一直没动」

这篇文章算是对 2019–2020 这两年自己前端工作流的一次归档,顺便给以后想重构环境的自己当个参考。


1. 编辑器与基础环境

1.1 VS Code 为主,WebStorm 偶尔上场

这两年主要是 VS Code 打主力,原因很简单:

  • 生态丰富(插件多);
  • 配置灵活(settings.jsontasks 等等);
  • 打开仓库速度相对快。

WebStorm 的优势在于:

  • 更强的「开箱即用」体验(尤其是对 TS / 重构支持);
  • 有些场景下更稳,但是资源占用也更高。

我的做法:

  • 日常写业务、处理多个小项目 → VS Code;
  • 需要重构大型 TS 项目、或者做复杂的重命名/移动 → 偶尔打开 WebStorm。

1.2 VS Code 常驻插件

只列一些我真正离不开的:

  • ESLint:保存自动修复格式/语法问题。
  • Prettier:统一代码风格(也可以用 ESLint 负责格式化)。
  • Vetur / Volar:Vue 项目对应的语法高亮与提示。
  • GitLens:在代码行旁边直接看到最近一次提交记录。
  • Path Intellisense:自动补全文件路径。
  • TODO Highlight:高亮 TODO / FIXME,提醒自己有坑没填。

设置里会开:

"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
  "source.fixAll.eslint": true
}

这样每次保存就是一次「小型代码体检」。


2. Git 工作流

这两年基本稳定在一套很简单的 Git 流程:

给前端加一层 BFF:Node 中间层的第一步

写前端写久了,你大概率遇到过这些场景:

  • 同一个页面要调 3~4 个后端服务,字段名都不一样。
  • 为了兼容多个客户端(H5、小程序、App),接口被搞得非常「通用」,前端要做一堆字段转换。
  • 某些数据其实可以缓存,但后端坚持不做,只能前端自己「临时记一下」。

这个时候,很多团队会在前后端之间加一层 BFF(Backend For Frontend)

这篇文章以一个小例子来说明:什么是 BFF、为什么要用、第一步怎么做。


1. 什么是 BFF?

一句话解释:

BFF 是介于前端和后端业务服务之间的一层「适配器」,为特定前端提供定制化接口。

传统架构:

浏览器 / 小程序  --->  各个后端服务

引入 BFF 后:

浏览器 / 小程序  --->  BFF  --->  各个后端服务

BFF 做的事情包括但不限于:

  • 聚合多个后端接口;
  • 做一些轻量的业务组合逻辑;
  • 做权限校验、请求限流;
  • 做缓存(比如热门数据);
  • 为不同客户端做「视图模型」适配。

常用技术栈:Node.js + Koa/Express/Nest 等。


2. 一个具体例子:用户资产概览

假设我们有一个前端页面「用户资产总览」,需要展示:

  • 用户基本信息(来自 user-service);
  • 用户账户列表(来自 account-service);
  • 用户最近 5 笔交易(来自 trade-service)。

2.1 没有 BFF 时的做法

前端直接请求多个后端服务:

GET /user-service/api/user/profile
GET /account-service/api/account/list
GET /trade-service/api/trade/latest?limit=5

前端需要:

  • 处理不同的返回格式;
  • 做错误合并处理(任何一个接口失败都要考虑);
  • 处理多次往返延迟。

2.2 有 BFF 后的做法

前端只请求 BFF: