得物复杂C端项目的重构实践

1. 背景

1.1 项目背景

公司近两年快速发展,社区线C端代码分散在不同仓库中,每个仓库中采用不同的前端框架和选型,且均含有几条业务线的代码,团队整体采用敏捷模式快速迭代,导致开发管理成本较高,升级改造麻烦。比如,所关联的三个仓库中的代码均引了一个内部基础组件库,该组件有非必现bug,导致三个仓库的不同页面均出现了不同表现的异常,由具体负责的不同测试分别报到前端开发,分别沟通、排查、解决并走独立的发布上线流程,耗时耗力。当同一仓库中活跃着不同业务线的开发,一个公共的地方需要修改,开发没有沟通清楚导致冲突线上bug。

此外,公司C端体验分析的统计和报表是应用粒度的,先前代码耦合了其他业务的内容,导致我所在业务线的统计数据不置信。

近期团队对C端项目进行重构,将不同仓库中的代码汇总到一个仓库中管理。以期减少管理成本及方便后续对组内项目做优化和升级改造。

1.2 重构经验

之前我有独立负责过几次较大的重构,也曾2周独立完成近20万行C端代码(不含node_modules)从JS到TS迁移在并行业务需求迭代的情况下实现上线0bug。

1.3 重构基础

Q:什么是重构?

重构是在不改变软件可观察行为的前提下,改善其内部结构。–《重构 – 改善既有代码的设计》

Q:为什么要重构?

重构可以提高理解性和降低修改成本 。–《重构 – 改善既有代码的设计》

Q:什么时候重构?

(1)何时不应该重构?

没有价值,没有意义或者投入产出比很低时。团队资源是有限的,有限的资源应该尽可能投入到有意义的事情上去。从团队的角度考虑投入产出比,对于已经只是维护状态,如无需求、无调整的代码,不要去动它,如果对于新手而言,不仅不会带来好处反而可能挖坑,要知道既有代码可能有不少坑。

(2)何时应该重构?

    1. 项目维护成本很高
    2. 影响项目调优,如性能优化时
    3. 代码长得丑,不优雅时
    4. 既有设计和实现不利于扩展新功能时
    5. 重复性工作,既有的代码无法帮助你轻松添加新特性时
    6. 修补bug时,排查逻辑困难
    7. Code Review 可以让他人来复审代码检查是否具备可读性,可理解性
    8. 太多的代码无注释,已然连自己都无法快速理清代码逻辑

1.4 如何重构

(1)准备(基本功)

推荐值得一读再读经典书籍,重构圣经《重构 – 改善既有代码的设计》。本人从毕业第一年开始,几年下来读了4遍 ,受益匪浅,每次复习都能有所收获,让我经常折腾经手的项目却没出过问题。

(2)重构实践要点

  • 思考清楚(整体有设计,不一定要文档化但需要想清楚)。
  • 协同规划(开发团队内部的配合及重构分支与其他分支的集成、外部资源提前申请如产品、测试、运维等)、整体规划。
  • 分层分步展开,抓大放小从粗到细。善用“批处理”。
  • 一次只做一件事。
  • 不要重复造轮子。
  • 当你觉得一件事很难的时候,停下来思考是不是方法用错了,它应该是怎样的。保持监控及复盘自己的思考方式。
  • 做好对内和对外沟通,尤其在当项目不是只有一个人在开发和维护的情况下。注意提前和相关方(测试、运维)沟通好(方案、主要时间节点、需要投入的资源、需要其配合的事项)。

2. 社区C端的重构实践

本次重构具有一定的复杂度,除了技术迁移改造的成本外,涉及的几个仓库是不同技术选型(框架&上层组件等)、项目快速的敏捷迭代、需求高并发及多人协同开发维护状态。

2.1 现状分析

技术栈:

仓库名

技术栈

社区C端页面数

repo A

React umi3

目标仓库无需统计

repo B

react umi3

5

repo C

vue2 vuex

27

项目侧

三个仓库 A / B / C 更新活跃,每个仓库均涉及多业务线的开发,并行维护。分别按照2周一个sprint的迭代节奏展开,1周开发1周测试,间或穿插着hotfix。

从 V1主版本发布后开始重构,各个仓库涉及的代码如下:

  1. repo A:A1 A1.* A2 A2.*
  2. repo B:B1 B1.* B2 B2.*
  3. repo C:C1 C1.* C2 C2.*

.*表示hotfix

2.2 重构计划

前端侧的整体思路:

  • repo A 较新,是社区的主要仓库,集中了大部分C端页面,作为目标C端代码的目标仓库。
  • repo B 到 repo A:repo B 与 目标仓库的技术栈很接近,涉及5个页面,通过人肉方式迁移,过程中注意依赖的一并迁移。
  • repo C 到 repo A:repo C 与目标仓库差异较大,且语言异构,上层框架、组件库等都有较大差异,涉及页面较多。

    1. 首先确定有效的页面,将已下线页面的dead code排除在迁移范围之外;具体细节下文会说到,取出待迁移仓库中的前端路由配置,知道页面总范围,查看阿里云sls日志中近期的PV(两种查询方式校对),排除无流量的页面。
    2. 分层分级重构,前期抓大放小,耗时耗力还容易出问题的框架语法转换(vue to react)应采用脚本工具化实现,实现文件级和各个类中整体结构及引用关系的维护的转换。
    3. 细节语法通过自定义脚本批处理(比如 vue中用的 class的key和字符串形式的value转换成react中的className及变量形式的value)。
    4. 为保证迁移后高效自测需要将对应的 *.vue 文件保留,将其看成doc文件,待整个迁移完毕再删除,以提升迁移及测试的效率。注意改造lint规则忽视对这类文件的检测。
    5. 过程中依赖文件一同迁入,有“名称空间隔离”,注意保持整体目录结构的相对关系,做整体迁移,且不去污染目标仓库中的既有文件,防止同名文件覆盖的情况。

通过上述三步将各个仓库代码迁移到 repo A 后,同步 三个仓库中的最新更新。repo C 到 repo A 的过程中(从V1 切出的分支),repo C 还在持续更新代码,repo A 还需要将 repo C 中的 V1.*、V2、V2.* 代码合入(repo B亦然)。由于代码都在不同的仓库中,需要手工合并。Tips:可以在 repo C 中将 V1.*、V2、V2.* 的多个commits合成一个commit,将所有变更项汇总到一处做批量更新。

repo A 中 SSR方案调研和应用也在并行。重构中新迁入的页面要和SSR做集成。

2.3 重构与集成实践

2.3.1 仓库B页面梳理及迁入

这部分迁移在同构语言中进行,且涉及页面数不多,主要通过人为迁移。

2.3.2 仓库C页面梳理及迁入

  • 线上流量查询,排除无用页面
    • 三个代码仓库中路由申明确定总范围

  • 根据阿里云日志确定过去3个月、2个月、1个月中的PV,将无PV的页面从待迁移页面池中剔除。

    • 注意1: 阿里云SLS日志是基于上报的数据,上报和统计过程可能有丢数据的情况,所以综合两个查询入口确定和排查。
    • 注意2: 对于有1-2个PV的页面,可能是团队内部开发前期做调研时产生的,确定访问者后排出“测试”产生PV的页面。

  • 确定最终重构范围(27个过滤13个)。将步骤1中获取的总范围中在步骤2中无用户PV的页面剔除。

  • 异构语言转换和处理
    • 工具转换
    • 仓库C中Vue2 转换为仓库A中的react

这里主要用到了 vue-to-react,然而该工具有不少约束和限制,大概成功转换了一半的代码,转化失败的情况需要自己写脚本实现。原想对该库的源码进行二次封装和改造,看了其实现发现定制的成本高于自己写脚本的成本所以弃了(本人vue的经验一个月不到),时间太紧不容仔细去研究。Tips:避免重复造轮子,当执行很繁琐且很多重复的动作时,可以考虑拥抱团队内部的轮子、社区和开源,没有的话就自己去倒腾一个。

  • 脚本转换
  • 转换
    • 项目目录结构设计及文件的映射过程

// step1:保持整体目录结构的相对性不变.├── apis│ ├── community.ts│ ├── h5community│ ├── ...├── components├── pages│ ├── h5community│ │ ├── App│ │ ├── api│ │ ├── asset│ │ ├── components│ │ ├── config│ │ ├── filter│ │ ├── live.js│ │ ├── main.js│ │ ├── mixins.js│ │ ├── router│ │ ├── style│ │ ├── utils│ │ └── views│ ├── community├── utils└── ...// step2: foo.vue文件转为 foo/ 目录,模板分别映射为jsx及less文件.├── apis│ ├── community.ts│ ├── h5community│ └── ...├── components│ ├── h5community│ └── ...├── config│ ├── h5community.js│ └── ...├── pages│ ├── community│ └── h5community│ ├── column // 原 column.vue 转为目录,分拆成index.tsx及index.scss│ │ ├── index.local_js // index.local_js作为注释保留,用于测试回归的参考│ │ ├── index.scss│ │ └── index.tsx // 首行自动插入对 index.scss 的引用│ └── ...└── utils ├── h5community └── ...

  • 分步转换1: 文件级

对于 vue-to-react 处理失败的页面,通过脚本生成页面模版文件。

// 转换前文件为 foo.vue// 转换后:.└── foo ├── index.jsx ├── index.local_js └── index.scss

自定义脚本转换生成的文件内容结构如下:

得物复杂C端项目的重构实践

  • 分步转换2: 语法级-html lang

Vue 文件转换过程中有很多 lang="pug"类的模版,通过工具 https://pughtml.com/ 转换成“类jsx”的模版(但凡鸡肋人肉的事,首先应该想到工具,如果找不到,不妨Google中尝试用不同的关键词,而不要去人工)。

// 转换前 foor.vue 中<template lang="pug"> article.modal-wrap(@touchmove.stop.prevent @click.stop='close') section.modal p.more 更多精彩内容, 就在得物App p.slogan 有毒的运动 x 潮流 x 好物 .enter-btn(@click.stop='enter') 进入得物App aside.close(@click.stop='close')</template>// 转换后 foo/index.jsx 中<article class="modal-wrap" @touchmove.stop.prevent="@touchmove.stop.prevent" @click.stop="close"> <section class="modal"> <p class="more">更多精彩内容, 就在得物App</p> <p class="slogan">有毒的运动 x 潮流 x 好物</p> <div class="enter-btn" @click.stop="enter">进入得物App</div> <aside class="close" @click.stop="close"></aside> </section></article>

  • 分步转换3: 语法级-className等

上面脚本生成的文件在于文件级的转换,语法差异需要脚本解决。比如 class的替换和解析。这里 html 属性的规则解析正则比较繁琐,实现时会思考哪里会有,很自然就想到了vue的源码中一定会有该正则(框架是要解析做原生映射的),查了下果不其然,稍作修改就可以了,然后再做些定制(业务代码中的模版代码,如import style这些用脚本自动生成按需插入)。

// foo.vue 文件中的写法 <div class="var1">demo1</div><div class="var1 var2">demo1</div>// foo/index.jsx (react中)的写法import style from './index.scss'import classNames from 'classnames'...<div className={style["var1"]}>demo1</div><div className={classNames(style["var1"], style["var2"])}>demo1</div>

    • 逐页面调试与校对

  • 仓库技术选型间的差异问题
    • umi的路由规则与定制
    • 第三方组件库
      • 如Swiper、postcss-px-to-viewport等,vue版与react版有些差异,文档不全,拥抱源码和社区。其中postcss-px-to-viewport在不同仓库中使用不同的viewportWidth设置,转换过程中通过对不同的插件实例处理不同的路径范围实现
      • 基本功:敏感度(这个跟经验有关)。库定位是什么?成熟度怎么样?应该有什么不应该支持什么?如果自己来设计大概会怎么设计(有时候即使文档不全情况下,不看源码也可以倒推出很多内容)?可以去哪里找解决方案?怎么找到?

  • 迁移home页配置
    • 过程中缩小home页的路径范围,隐藏repo A中的访问路径,仅透出待迁移的路径,提高查找效率
  • 迁移过程记录(测试数据及路径等,方便交叉测试和QA回归)
  • 覆盖度自测。一个页面中多业务逻辑的情况,后续需要对各路径进行足够自测
  • 迁移过程中目录和文件结构的设计与变化路径(重要)

2.3.3 集成repo A、repo B、repo C重构分支代码

  • repo B 中的页面迁移到 repo A 中,如用 chore-repoB 分支
  • repo C 中的页面迁移到 repo A 中,如用 chore-repoC 分支
  • 将repo A master分支 和 chore-repoB、chore-repoC 合并并解决冲突,合并分支记为chore-repoA-repoB-repoC,此时该分支仅有 V1的代码,各个仓库当前版本的迭代功能和及上个版本的hotfix还未被合并入该分支。

2.3.4 集成repo A、repo B、repo C中迭代分支代码

主版本日前一天下午各个仓库中的迭代功能基本稳定,bug已经收敛。此时可以将该各个仓库的各个开发本地的分支 feat-foo、feat-bar 等汇总成一个 pre-release-temp 分支(已含有了master上的hotfix),即 pre-release-temp 分支 是 V1.*、V2 的汇总,将该分支的 增量commits合成一个commit 获取 V1.*、V2影响到的文件变更。人为将这些变更同步到 repo A chore-repoA-repoB-repoC分支上。

2.3.5 集成三个仓库业务代码与SSR代码

社区C端SSR改造方案确定后,新启了一个 A-SSR 仓库。使用SSR POC的框架内容对 A-SSR 仓库进行初始化,再将 repo A中chore-repoA-repoB-repoC 中的代码迁移到该仓库中。遇到的问题:POC中已对原 repo A中的部分模块做了SSR转换,迁移新代码到该仓库中注意文件覆盖代码丢失,用cp然后git diff及人为check多变更源的文件后再提交。

待版本日中再将近1天 各仓库产生的bugfix同步到 A-SSR 仓库,确保代码无丢失

3. 项目推进之外部协同

3.1 测试

较大范围的重构需要保证充分测试,考虑到占用的测试资源情况,尽可能提前和测试leader沟通资源需求。另外,移测前前端内部尽量充分自测。

3.2 运维

提前计划好 页面重定向方案(将最终的跨仓库/应用迁移的页面重定向),注意运维侧变更的影响,一旦做了变更,相关的在对应的测试环境就不可用了(QA回归需要时间,该过程中如果重定向启用了会影响该环境上相应页面的使用)。

3.3 遇到的问题

在开始规划及启动重构时,团队没有人对涉及的所有三个C端仓库足够熟悉。迁移到第二个页时,发现有页面是没有线上流量的 dead code时,重新沟通客户端及运维等同学,最终通过查询阿里云sls日志缩小迁移范围,减少了近一半的工作量。过程中遇到的各种技术问题,还是需要平时多做积累。

4. 总结

复杂项目的重构对研发的基础、经验、规范和各方协同有一定要求。开始时可以多读几遍《重构》基础的打好了,逐渐着手代码模块、简单项目、复杂项目、跨团队复杂项目等的重构,累计经验。事前做好规划(技术侧整体方案、技术方面的疑难病症提前预估、整体推进计划、相关方参与等),过程中思考全面足够细心并持续复盘调整,过程后做好总结沉淀。

事前做好设计、定期Code Review、过程中和后续持续进行重构可以让项目代码具有更好的可维护性,团队保持重构的习惯的同时不断积累重构经验,能从整体上提升项目的健康度与可维护性。重构看得见改善是关键,在重构中成长,在重构中受益,从重构中收益。

相关链接:

  • https://pughtml.com/

*文/石菲

关注得物技术公众号~每周一三五晚18:30更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。