Q:什么是重构?
重构是在不改变软件可观察行为的前提下,改善其内部结构。--《重构 - 改善既有代码的设计》
Q:为什么要重构?
重构可以提高理解性和降低修改成本 。--《重构 - 改善既有代码的设计》
Q:什么时候重构?
(1)何时不应该重构?
没有价值,没有意义或者投入产出比很低时。团队资源是有限的,有限的资源应该尽可能投入到有意义的事情上去。从团队的角度考虑投入产出比,对于已经只是维护状态,如无需求、无调整的代码,不要去动它,如果对于新手而言,不仅不会带来好处反而可能挖坑,要知道既有代码可能有不少坑。
(2)何时应该重构?
(1)准备(基本功)
推荐值得一读再读经典书籍,重构圣经《重构 - 改善既有代码的设计》 。本人从毕业第一年开始,几年下来读了 4 遍 +,受益匪浅,每次复习都能有所收获,让我经常折腾经手的项目却没出过问题。

(2)重构实践要点
本次重构具有一定的复杂度,除了技术迁移改造的成本外,涉及的几个仓库是不同技术选型(框架 & 上层组件等)、项目快速的敏捷迭代、需求高并发及多人协同开发维护状态。
| 仓库名 | 技术栈 | 社区 C 端页面数 |
|---|---|---|
| repo A | React + umi3 | 目标仓库无需统计 |
| repo B | React + umi3 | 5 |
| repo C | vue2 + vuex | 27 |
三个仓库 A / B / C 更新活跃,每个仓库均涉及多业务线的开发,并行维护。分别按照 2 周一个 sprint 的迭代节奏展开,1 周开发 1 周测试,间或穿插着 hotfix。
从 V1 主版本发布后开始重构,各个仓库涉及的代码如下:
.* 表示 hotfix
repo C 到 repo A:repo C 与目标仓库差异较大,且语言异构,上层框架、组件库等都有较大差异,涉及页面较多。
这部分迁移在同构语言中进行,且涉及页面数不多,主要通过人为迁移。
线上流量查询,排除无用页面
根据阿里云日志确定过去 3 个月、2 个月、1 个月中的 PV,将无 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
-
- └── ...
对于 vue-to-react 处理失败的页面,通过脚本生成页面模版文件。
- // 转换前文件为 foo.vue
-
-
- // 转换后:
- .
- └── foo
- ├── index.jsx
- ├── index.local_js
- └── index.scss
自定义脚本转换生成的文件内容结构如下:

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>
上面脚本生成的文件在于文件级的转换,语法差异需要脚本解决。比如 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>
仓库技术选型间的差异问题
- 第三方组件库
- 如 Swiper、postcss-px-to-viewport 等,vue 版与 react 版有些差异,文档不全,拥抱源码和社区。其中 postcss-px-to-viewport 在不同仓库中使用不同的 viewportWidth 设置,转换过程中通过对不同的插件实例处理不同的路径范围实现
- 基本功:敏感度(这个跟经验有关)。库定位是什么?成熟度怎么样?应该有什么不应该支持什么?如果自己来设计大概会怎么设计(有时候即使文档不全情况下,不看源码也可以倒推出很多内容)?可以去哪里找解决方案?怎么找到?
迁移 home 页配置
主版本日前一天下午各个仓库中的迭代功能基本稳定,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 分支上。
社区 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.1 测试
较大范围的重构需要保证充分测试,考虑到占用的测试资源情况,尽可能提前和测试 leader 沟通资源需求。另外,移测前前端内部尽量充分自测。
3.2 运维
提前计划好 页面重定向方案(将最终的跨仓库 / 应用迁移的页面重定向),注意运维侧变更的影响,一旦做了变更,相关的在对应的测试环境就不可用了(QA 回归需要时间,该过程中如果重定向启用了会影响该环境上相应页面的使用)。
在开始规划及启动重构时,团队没有人对涉及的所有三个 C 端仓库足够熟悉。迁移到第二个页时,发现有页面是没有线上流量的 dead code 时,重新沟通客户端及运维等同学,最终通过查询阿里云 sls 日志缩小迁移范围,减少了近一半的工作量。过程中遇到的各种技术问题,还是需要平时多做积累。
复杂项目的重构对研发的基础、经验、规范和各方协同有一定要求。开始时可以多读几遍《重构》基础的打好了,逐渐着手代码模块、简单项目、复杂项目、跨团队复杂项目等的重构,累计经验。事前做好规划(技术侧整体方案、技术方面的疑难病症提前预估、整体推进计划、相关方参与等),过程中思考全面足够细心并持续复盘调整,过程后做好总结沉淀。
事前做好设计、定期 Code Review、过程中和后续持续进行重构可以让项目代码具有更好的可维护性,团队保持重构的习惯的同时不断积累重构经验,能从整体上提升项目的健康度与可维护性。重构看得见改善是关键,在重构中成长,在重构中受益,从重构中收益。
* 文 / SHI FEI
关注得物技术,做最潮技术人!