关于重构:架构团队如何重构内部系统

40次阅读

共计 9902 个字符,预计需要花费 25 分钟才能阅读完成。

前端团队不免须要保护一些外部零碎,有些外部零碎因为开始的架构设计不合理,随着业务复杂度的减少,“坏滋味”代码也越来越多,从而导致认知和沟通成本上升,甚至问题频出,此时,重构就天然成了一个抉择。但重构不是一时衰亡,也不是欲速不达的,须要认真的剖析和有序的施行,以试验平台为例,介绍一下智联大前端的重构教训。

试验平台是智联招聘自主研发的 A / B 试验生态,依靠于数据平台,并联合公司的业务和技术特点量身定制,提供了丰盛的试验能力,迷信的试验机制和残缺的流程治理。

Web 端是应用了基于 Vue 实现的 Ant Design Vue 组件库开发实现;API 层是基于 Node.js 开发,事后解决、组合、封装后端微服务所返回的原始数据,无效升高 UI 与后端接口的耦合,实现并行开发和接口变更。

现状

UI 排版和布局的整体设计不对立,前端交互简单,性能冗余,“坏滋味”代码一直增多更是加大了开发与保护的难度;Api 层没有遵循支流 RESTful Web API 规范,只负责了后端接口的转发,逻辑全放在 Web 层实现,没有无效升高 UI 与 Api 层接口的耦合,减轻了 Web 层的累赘;

基于以上起因,咱们决定对试验平台零碎进行重构,进一步提高其易用性、内聚性和可维护性。

剖析

首先一一页面剖析一下试验平台性能及应用状况,以不便对接下来重构工作有初步的理解:

概览页 :次要展示自试验平台上线以来应用状况的统计信息,为了更好展示统计内容,以及日后不便对数据结构的保护,咱们决定数据不再由后端接口提供,改由本人的 Api 层计算;
试验列表页:列表页次要用于展现用户关注的试验的要害信息,所以尽可能的精简展现字段以及优化信息主次排版;同时,提供疾速跳转入口(中转统计、中转调试),优化用户体验;增加可搜寻试验名和创建人,优化搜寻体验;对于试验状态,有些状态不再须要(如申请公布、批准公布、已公布、归档),同时还须要兼容旧的试验状态,为此咱们对试验状态做了新的调整:

  • 草稿:新建
  • 调试:调试状态
  • 运行:运行、申请公布、批准公布
  • 进行:放弃、进行、已公布、归档
  • 世界概览页:基本功能不变,按准则重构代码即可

变量页 :通过考量,变量没有必要再执行开释、或者复原等操作,所以只须要展现正在“运行”试验的变量列表;
设置页 :次要目标是展现和增加管理员,所以没有必要显示所有的用户,所以能够简化为删除和增加管理员即可;
根本信息页 :此页面基本功能不变,优化页面布局排版,对立用户体验,编辑权限由 Api 层对立管制;
统计分析页 :此页面基本功能不变,为了便于保护,统计数据全副由 Api 层计算生成;另外,经剖析大盘指标页面实时性能能够去掉;优化页面整体布局排版及重构代码;
操作记录页 :须要增加克隆试验 id 的信息,优化用户体验;
管制页 :此页面基本功能不变,对立用户体验,编辑权限由 Api 层对立管制,优化页面布局排版及重构代码;
总结页:用于总结试验后果,这个页面通过剖析,曾经不再须要;

准则

至此,依据之前的剖析,咱们曾经对试验平台的现状有了初步的意识。接下来,总结一下造成一些有用的领导准则:

分层,Web 层和 Api 层应各司其职:

  • Web 层只负责 UI 的交互和展现;
  • API 层遵循 Restful Web API 规范,采纳强类型查看的 Typescript 开发,负责所有的性能逻辑解决及权限管制;

布局,整体布局放弃设计统一:

  • 布局自然化,晋升可维护性;
  • 版块规范化,放弃设计对立;
  • 各版块的上下左右间距统一,版块间对齐;

模块,放弃职责繁多,不便保护准则:

  • 依照职责拆分模块,并互相解耦;
  • 尽最大可能不保护状态(尤其是全局状态),而是通过与其余模块交互实现;
  • 逻辑去地方化,扩散到各性能组件;
  • 组件化,放弃组件职责繁多;
  • 未复用的组件均置放于父容器组件的目录之下;

开发标准,遵循智联前端开发标准及自定义准则:

  • 款式规范化,升高更新老本;
  • 对立输入输出标准;
  • 禁止所有魔法数字,而是通过变量实现;
  • 禁止所有内联款式,而是通过更加通用的 Class 实现;
  • 尽最大可能不应用相对定位和浮动,而是通过 a -layout 组件、规范文档流或 Flex 实现;

流程,采纳渐进式重构形式:

  • 渐进式重构形式,分阶段进行重构,每一阶段都不毁坏现有性能,具备独自公布的能力;

阶段

接下来,咱们将重构周期划分几个不同的阶段进行有序施行。

第一步:js 迁徙到 ts

家喻户晓,JS 是一门动静语言,运行时动静解决类型,应用非常灵活,这就是动静语言的魅力所在,然而,灵便的语言有一个弊病就是没有固定数据类型,短少动态类型查看,这就导致多人开发时乱赋值的景象,这样就很难在编译阶段排除更多的问题,因而,对于须要长期迭代保护以及泛滥开发者参加的我的项目,选一门类型严格的语言、能够在编译期发现错误是十分有必要的,而 TypeScript 采纳强类型束缚和动态查看以及智能 IDE 的提醒,能够无效的升高软件腐化的速度,晋升代码的可读性可维护性。

所以,这次重构工作首先从 js 迁徙到 ts 开始,为后续模型梳理奠定语言根底。

ts 仅限于 API 工程的 node 层,因为,前端应用 Vue2 对 ts 反对不太敌对,所以还放弃应用原有 js。

第二步:梳理数据模型

这个步骤比较简单,次要是梳理现有的 API 接口申请的输出和输入的信息,对后续梳理数据实体打好根底。
首先,整顿出试验平台零碎所有的页面,如下所示:

  • 设置
  • 变量
  • 世界
  • 概览
  • 试验列表
  • 创立试验
  • 查看根本信息
  • 编辑根本信息
  • 查看操作记录
  • 查看统计
  • 管制

而后,别离对每个页面波及的 Api 接口进行进一步统计,例如,设置页:获取用户列表、新增和删除用户,设置用户角色等 API 接口。
其次,依据上一步整顿的后果,对每个 API 接口申请输入输出信息进行演绎整顿,例如,关上试验根本信息页,找到浏览器的开发工具并切换到【NetWork】,鼠标右击申请接口找到【Copy as fetch】复制申请后果,如下图所示:

如下展现了 API 接口申请输入输出信息的代码构造:

【示例】// [分组]: 获取试验分组信息列表
fetch(
  "https://example.com/api/exp/groups?trialId=538",
  {
    credentials: "include",
    headers: {
      accept: "application/json, text/plain, */*",
      "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
      "sec-fetch-mode": "cors",
      "sec-fetch-site": "same-origin"
    },
    referrer: "https://example.com/exps/538",
    referrerPolicy: "no-referrer-when-downgrade",
    body: null,
    method: "GET",
    mode: "cors"
  }
);

const response = {
  code: 200,
  data: {
    groups: [
      {
        desp: "c_app_default_baselinev",
        flow: 20,
        groupId: 1368,
        imageUrl: "",
        type: "A, 对照组",
        vars: ["c_app_default_baselinev"]
      },
      {
        desp: "c_App_flowControl_baseline",
        flow: 20,
        groupId: 1369,
        imageUrl: "",
        type: "B",
        vars: ["c_app_flowControl_baseline"]
      }
    ],
    varName: ["gray_router_prapi_deliver"]
  },
  time: "2019-12-20 17:25:37",
  message: "胜利",
  taskId: "5f4419ea73d8437e9b851a0915232ff4"
};

同样的,咱们依照以上流程顺次对所有页面的对应 API 接口申请的输入输出别离进行整顿,最初,失去如下文件列表:

接下来,剖析每个接口的返回值,提取 UI 层交互会用到的字段,从而定义根本的数据模型,数据模型要可能直观的展现数据的根本组成构造,如下所示:

【示例】// 分组
const group = {
  id: 123,
  name: 'A',  // 组别
  description: 'CAPP 变量',  // 形容
  variableValue: 'c_app_baselinev',   // 变量值
  preview: 'http://abc.jpg', // 预览图
  bandwidth: 10  // 流量
}

第三步:梳理数据实体

后面咱们梳理了数据模型,接下来,咱们须要依据数据模型进一步梳理模型对应的数据实体,如下所示:

【示例】class ExperimentGroup {
  id: number
  name: string
  description: string
  variableValue: string
  previewImage: string
  bandwidth: number

  constructor () {
    this.id = null
    this.name = null
    this.description = null
    this.variableValue = null
    this.previewImage = null
    this.bandwidth = 0
  }
}

把须要通过计算解决能力失去字段定义在 Object.defineProperties 中,如下所示:

【示例】class ExperimentCompositeMetric extends ExperimentMetric {
   unit: string

   constructor () {super()
      this.unit = ''

      Object.defineProperties(this,  {
          displayName: {get: () => (this.unit ? `${this.title}(${this.unit})` : this.title),
              enumerable: true
          }
      })
   }
}

与此同时,依据接口的输入,定义了对立的 Api 接口输入标准的数据实体,如下所示:

【示例】class Result {
  error: boolean|string|Error
  data: any
  requestId: string|null
  constructor () {
    this.error = false
    this.data = null
    this.requestId = null
  }
}

第四步:交互类型接口从新梳理

接下来,依据 UI 层的交互性能,定义接口的输出标准(蕴含办法、门路,及参数等),一边就把接口关联的实体丰盛起来,最初,在理论开发中依据须要一直调整优化实体,如下所示:

【示例】// 获取指标图表数据
get('/api/v2/experiment/stats/trending', {
  params: {
    id: 123,
    type: "key",
    period: "day", // or hour
    from: "2019-12-17",
    to: "2019-12-18"
  }
})

第五步:UI 方面的从新梳理

这一步骤,次要是 UI 方面的从新梳理(页面、布局、组件等等),定义了页面的根本展现模式及输入输出标准:

【示例】<template>
  <editable-section @click="onEdit">
    <h2 slot="header"> 分组 </h2>
    <table>
      <thead>
        <tr>
          <th> 组别 </th>
          <th> 组名 </th>
          <th> 变量 {{experiment.variable.name}} 的值 </th>
          <th> 预览图 </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="group in experiment.groups">
          <td rowspan="group.name | toRowspan">group.name | toType</td>
          <th>group.name</th>
          <td>
            <p>
              {{group.variableValue}}
              <small>{{group.description}}</small>
            </p>
          </td>
          <td>
            <img src="group.previewImage"/>
          </td>
        </tr>
      </tbody>
    </table>
  </editable-section>
</template>

<script>
import BaseSection from 'shared/components/EditableSection'

export default {
  props: {experiment: Object},
  filters: {toType (value) {return value === 'A' ? '对照组' : '实验组'},
    toRowspan (value) {return value === 'A' ? 1 : this.experiment.groups.length - 1}
  },
  methods: {onEdit () {alert('暂未实现,请应用 V1。')
    }
  }
}
</script>

第六步:定义文件布局

在开始重构前,咱们定义 Web 工程的根本文件目录,同时,依据以往教训,咱们还提取了罕用公共的变量及办法,如下图所示:

shared 文件存用于放公共文件资源,其中,components 文件寄存公共组件资源,api.js 文件寄存所有的 API URL 资源,styles 文件寄存公共 css 文件资源,images 文件寄存公共图片,fonts 寄存公共字体等。

在 variables.postcss 文件里,定义了一些罕用 css 变量,如下所示:

:root {
  --font-family--code: cascadia, pingfang sc, microsoft yahei ui light, 微软雅黑, arial, sans-serif;
  --font-size--super: 70px;
  --font-size--xl: 24px; /* 超大字号 */
  --font-size--lg: 18px; /* 大字号 */
  --font-size: 14px; /* 惯例字号 */
  --font-size--sm: 12px; /* 小字号 */
  --space: 16px; /* 惯例间距,实用于 padding 及 margin */
  --space--sm: 12px; /* 小间距 */
  --space--xs: 8px; /* 超小间距 */
  --color--white: #fff;
  --color--black: #000;
  --color--subtle: rgba(0, 0, 0, 0.45); /* 非显著色彩 */
  --color--message: #93a1a1;
  --color--info: #859900;
  --color--warning: #b58900;
  --color--trace: #657b83;
  --color--error: #dc322f;
  --color--normal: #268bd2;
  --color--lightgrey: #f0f2f5;
  --color--active: #1890ff;
}

在 global.postcss 文件里,对第三方库的款式笼罩及全局款式做了定义,如下所示

@import './variables.postcss';
 
@font-face {
  font-family: 'Cascadia';
  src: url('../fonts/cascadia.ttf');
}
 
// 全局款式
html,
body {min-width: 1200px;}
 
 
.text--description {color: var(--color--subtle);
}
 
.text--mono {font-family: var(--font-family)
}
 
// 第三方款式笼罩
.ant-modal-body {max-height: calc(100vh - 240px);
  overflow: auto;
}
 
.ant-table-body td {vertical-align: top;}
 
.ant-table-thead > tr > th {background: #fafafa !important;}
 
.ant-table-small > .ant-table-content > .ant-table-body {margin: 0;}
 
.ant-table {
  & .ant-empty {
    & .ant-empty-description {display: none;}
 
    &::after {
      content: '目前啥也没有';
      display: block;
    }
  }
}

而 template.js 文件,用于寄存获取 html 模版的办法,如下所示:

import favicon from 'shared/images/favicon.png'
 
function generate ({ctx, title, ...pageContexts}) {const prepareDataString = Object.entries(pageContexts)
    .map(([key, value]) => `var ${key} = ${typeof value === 'object' ? JSON.stringify(value) : value}\n`)
    .join('')
 
  const template = `<!DOCTYPE html>
  <html>
    <head>
      // 自定义 title
      <title>${title ? `${title} | ` : ''}智联试验平台 </title>
      <link rel='shortcut icon' href='${favicon}' />
      // 资源文件占位
      ${ctx.template.placeholders.head.style}
      ${ctx.template.placeholders.head.link}
      ${ctx.template.placeholders.head.script}
      <script>
        // 传入自定义全局页面数据
        ${prepareDataString}
      </script>
    </head>
    <body>
      // 资源文件占位
      ${ctx.template.placeholders.body.root}
      ${ctx.template.placeholders.body.script}
    </body>
  </html>`
 
  return template
}
 
export default {generate}

同样的,咱们也定义了 Api 工程的文件目录,如下图所示:

shared 用于寄存公共文件资源,utils 寄存一些公共的办法,models 文件里的文件就是咱们后面所整顿的数据模型。

第七步:渐进式开发

接下来,咱们就能够正式进入下一阶段,进行渐进式重构了。
依据我的项目的难易水平及性能的依赖关联度,对所有页面定义优先级,如下所示:

  1. 框架
  2. 设置
  3. 变量
  4. 世界
  5. 概览
  6. 试验列表
  7. 查看操作记录
  8. 查看统计
  9. 查看根本信息
  10. 管制
  11. 创立试验
  12. 编辑根本信息

从低到高顺次进行重构,每一阶段都不毁坏现有性能,具备独自公布的能力,定义现版本为 v1,重构版本 v2,在 url 上进行辨别,例如 v2/exps。node 层版本 v1 是 js 的,而后,逐渐替换成用 ts 写的版本 v2(因为 ts 向下兼容 js,所以工程中能够同时存在);

随着,重构工作的有序进行,咱们会发现重构变的越来越得心应手,须要留神的仍然是要管制重构范畴,实现一个功能测试之后,再开始下一个性能。

第八步:抽离公共组件

不难发现,这一步骤,其实和上一步骤同时进行的,在重构过程中,为了保障代码的简洁、对立、易维护性,咱们一直的依据应用场景和性能抽离组件,来保障了代码和界面的对立,哪些场景能够抽离组件呢?

  • 应用超过 3 次的反复代码;
  • 应用场景相似;
  • 逻辑比拟靠近,总是一起更新的代码;

并遵循以下准则:

  • 放弃组件职责繁多,高内聚低耦合;
  • 放弃参数的配置简略、灵便;
  • 放弃颗粒度适合,代码行数适中;

在此基础上,咱们抽离了布局,编辑,头像、菜单组件,试验状态等罕用组件,大大的加重了繁琐反复的工作量,如下:
BaseLayout:主框架布局容器,其左侧导航栏、右侧上可嵌套 Header、右侧下可嵌套 section 容器。
Header:顶部布局,自带默认款式,左侧题目 Title,右侧操作按钮等。
BaseSection:只读布局容器,其左上可设置题目、下嵌套内容区域。
EditableSection:可编辑布局容器,其左上可设置题目、右上设置操作按钮、下嵌套内容区域。

第九步:对立整体布局、交互体验及提示信息等

随着重构工作的进行,咱们须要对 UI 的一些细节做进一步的优化,造成一套对立设计体系:

  1. 整体布局,导航及各版块占比排查 / 调整,例如:

    • 间距
      各版块的上下左右间距统一,版块间对齐;
      版块外部边距(padding)对立;
      雷同元素的间距达到对立;
    • 布局
      同一组件的地位(比方居左或居右)对立;
      采纳左侧导航菜单、右侧内容模式布局,应用 BaseLayout 组件;
      页面右侧顶部应用 Header 组件;
      只读内容模块应用 BaseSection 组件,带编辑则应用 EditableSection 组件等;
    • 表格
      表格数据一律应用紧凑模式;
      表格首行锁定、局部表格首列锁定;
      对于一次性返回所有数据的表格原则上不应用分页;
      对于必须有分页的表格,分页区域应显示在可见区域内;
      表格内容次要字段主动列宽,主要内容列宽设置最小宽度;
  2. 交互体验,例如;

    • 表单编辑应用 EditableSection 组件,点击右上角“编辑”按钮弹出 modal 框进行编辑;
    • “操作按钮”在 Header 的右侧;
    • 删除操作弹出提示框 PopConfirm 组件;
    • 吐司提示框应用 Tooltip 组件;
    • 对立 message 提醒,题目,点击详情展现谬误的具体信息;
  3. 标准字体,同一级别同一场景的字号,色彩、字体款式对立,例如:

    • 顶部 header 的题目的 font-size: 24px;
    • section 的题目 font-size: 20px;
    • 统计数字、英文变量等内容设置宽字体 text–mono;
    • 形容等辅助文字内容设置款式 text–description;
  4. message 提示信息对立

    • 应用形式;
    • 交互模式;
    • 展现信息;

第十步:渐进上线 v2 页面

为了避免新上线的 v2 页面,在某些状况呈现谬误无奈失常应用时,咱们在页面右上角增加了“切换版本“性能,能够疾速的切换到 v1 页面,不影响用户失常应用。且在一段时间自测和用户的反馈状况下,逐步优化 v2 性能。

第十一步:上线全副 v2 版本

所有的 v2 页面和 Api 都全副上线后,通过一周自测和用户应用反馈,不再有应用功能性问题,即可去掉右上角的版本“切换按钮”。

第十二步:删除 v1 版本

运行察看一个月后,v2 版本线上运行稳固,且应用无显著性能问题后,对 v1 页面和代码进行下线操作。自此,重构的大部分工作曾经实现。

第十三步:抽离公共组件库

当初重构工作曾经靠近序幕,试验平台较之前从架构设计、编码标准、布局及用户体验等都有了本性难移的变动。

在宣告重构实现之前,为了进一步对立用户体验、进步零碎迭代效率,咱们抽离了一个公共组件库,公共组件是与业务无关的,能够服用在其余场景。

由此,AntNest 组件库雏形就产生了,它基于 Ant Design Vue 组件库实现,其中涵盖了布局、容器、卡片、提醒、编辑、数据展现等性能的公共组件。并很快公布 v1.0.0 版本,首先在试验平台和 Ada 工作台、魔方管理系统进行利用替换,一直的进行迭代优化,察看一段时间后运行稳固。并在智联的其余外部零碎中逐步推广应用,目前已胜利利用到多个零碎,如鲲鹏、伏羲、经营平台、性能监控平台等十几个我的项目中,将来其余外部零碎都会替换成 AntNest 组件,造成一套对立管理系统体系,实现真正的对立用户体验。

第十四步:提供工程模版

为了进一步简化开发流程,不便大家疾速创立我的项目,为此,咱们还提供了基于 Web 工程和 Api 工程的轻量级模版。

其中,Web 工程模版中基于 AntNest 组件库,除了根本的 Web 框架结构外,咱们还在其中内置管理系统罕用的概览页(一般概览和瀑布流概览)、列表页、详情页(只读和可编辑)、布局页及谬误页等页面,满足用户根本需要。

Api 工程模版是基于 Node.js,并采纳强类型查看的 Typescript 开发,预制了和试验平台统一的的文件目录构造,不便大家疾速进行开发。

总结

回顾一下整个重构过程,会发现咱们做的第一件事件并不是编码,而是对现状进行深刻的分析。在这个过程中,求同存异,一些模式会自然而然地出现进去,它们都是重构的“素材”。

在真正进行编码时,咱们采取了渐进式的策略,将整个过程分解成多个步骤。争取做到每一个步骤实现后,整个模块都能达到公布规范。这就意味着须要把每一步所波及的改变都限定到一个可控的范畴内,并且每个步骤都须要蕴含残缺的测试。

以上就是本次试验平台重构的历程及教训,心愿给日后开发新我的项目或重构老我的项目提供帮忙及借鉴。

正文完
 0