袋鼠云研发手记 | 袋鼠云EasyManager的TypeScript重构纪要
作为一家创新驱动的科技公司,袋鼠云每年研发投入达数千万,公司80%员工都是技术人员,袋鼠云产品家族包括企业级一站式数据中台PaaS数栈、交互式数据可视化大屏开发平台Easy[V]等产品也在迅速迭代。在进行产品研发的过程中,技术小哥哥们能文能武,不断提升产品性能和体验的同时,也把这些提升和优化过程记录下来,现录入“袋鼠云研发手记”专栏中,以和业内童鞋们分享交流。
下为“袋鼠云研发手记”专栏第一期,本期作者为袋鼠云前端团队。
袋鼠云前端团队-知乎专栏@DTUX
袋鼠云UX团队拥有十多名专家级别,经验丰富的前端开发工程师,分别支撑公司大数栈产品线的不同子项目的开发需求,具体包括数据中台产品「数栈」与数据可视化产品Easy[V]两大块。
在长期的项目实践与产品迭代过程中,团队成员在 React 技术栈、数据可视化技术、前端工程化等细分领域上不断深耕探索,积累了丰富的经验与最佳实践,并分享在知乎专栏@DTUX。
**第一期
袋鼠云 EasyManager 的 TypeScript 重构纪要**
前言
在 2018 年 Stack overflow 的开发者调查结果中,开发者们最爱的语言一栏中TypeScript 超越了 JavaScript 位居第四。
相较于 JavaScript,开发者们更喜欢 TypeScript 将类型系统带入了 JavaScript 中,如此一来,开发者能够在运行程序之前发现更多的潜在的问题;得益于 TypeScript 编译器的良好支持,结合VSCode, 它还可以提示我们该如何修复这些问题;将类型系统添加进 JavaScript, 同时允许编辑器给开发者提供更多的便利,比如代码补全、更方便的进行项目重构以及自动的模块导入等等。
2018 Stack overflow调查结果
在 TypeScript 官网中列出了很多已经使用 TypeScript 的机构,同样的还有著名测试框架 Jest 也表示尝试将 Jest 迁移到 TypeScript。
TypeScript官网展示的已使用TypeScript机构
TypeScript 的发展势头可见一斑。
Easy Manager
Easy Manager是一款产品安装部署工具,是数栈大数据平台的运维管家,使用Easy Manager可实现数栈产品的安装部署。
在产品安装部署完成后,Easy Manager支持完成大数据平台全方位的运维,包含产品升级、扩缩节点、版本回滚、产品卸载、集群监控、实时告警等功能,致力于帮助客户最大化地节省运维成本,降低线上故障率与运维难度,提供安全稳定的产品部署与监控。
Easy Manager 目前已支持百至千个节点的集群部署,可进行大集群的部署、监控及日常运维。同时,除支持数栈大数据平台部署外,也可支持第三方产品的安装部署。
关于此次重构
本次 EasyManager 的重构主要目标是使用 TypeScript 替代 JavaScript,加上类型约束提高团队协作效率,同时剔除已经不需要的冗余代码。
在本次 EasyManager (下文简写成:EM)的重构之前,已经进行了一部分的重构工作,之前是基于EM2.3 版本进行重构的,但是发现 EM2.4 版本改动较大,需要在之前重构基础上继续进行重构工作。计划是一边开发 EM2.5版本,一边进行 EM2.4 版本的重构工作,在EM2.5基础上,最后进行 EM2.6 版本的开发同时进行遗漏补缺。
对于已经部署使用了EM2.5 版本的客户和未来即将部署使用EM2.6 版本的客户情况,我们保留了 js_master 和 ts_master 两个分支。前者主要进行EM2.5 版本的 bug 修复为客户做支持,后者则是主版本进行版本迭代开发。
整个过程还是比较顺利的,没有遇到阻塞工作流程的情况,下面详细来为大家分析一下本次重构详细情况。
js VS ts 结构区别与利弊
js 版本的目录结构并不属于那种一眼就能看懂的,其类似数栈将每个页面所需的 action、reducer、assets 以及模块需要的其他资源都放在了一个以模块名称命名的文件夹里,我的理解是这里是以“模块”为思想进行的开发结构搭建,比较方便开发,对人协同开发对 changelog 亦能一目了然,管理 redux 也很容易定位。
如下图:
旧版本基本目录结构
ts 版本在这里做了更改,不再是以“模块”为单位进行目录区分,而是以文件“功能”为单位进行搭建,具体表现是:js 版本的 service 模块文件夹下有 actions、reducers、pages、assets 文件夹以及 action types 文件 constants.js 和页面所需的 json 数据文件。而在 ts 版本,actions 现在以单个文件的形式存在于 src 目录下的 actions 文件夹下面,同理,reducers 则已单个文件形式存在于 stores 文件夹下,action types 则与路由文件 routers 和 api 接口定义 api.js 文件同在 constants 文件夹下;这样一来,ts 版本里 pages 目录下各个页面的文件夹下只有一个 index.tsx 入口文件、style.scss 样式文件和**.tsx 的页面主文件,可以说是很精简的目录组成。
如下图:
新版本的目录结构
两个结构各有利弊,ts 版本的结构比较经典,开发者花很短的时间就能读懂搭建者的心思与意图,对新手参与开发很友好,但是对于“按功能区分”文件结构的方式来讲,开发一个模块需要游离在 actions、pages 和 stores 文件夹里,在模块多了之后文件就会变得难以定位和区分。js 版本的目录结构相较于 ts 版本的更深一些,主要是将页面所有资源都存放在页面自己目录下,这种结构很大程度上方便了多人协作开发,也方便定位页面所需的功能文件,唯一不足也许就是对新接触的开发者来说要花点时间熟悉。
重构思路
ts 可以作为类型检查和编译工具使用,对于EasyManager来讲主要作用在于类型检查,因此本次重构使用 webpack、babel 等插件进行项目编译。本次重构以模块为单位进行重构,重构顺序依次是 host、product、dashboard、service。
本次重构原则是尽量不改动业务逻辑代码,对已有的重构基础进行包容叠加新特性,尽量避免改动非常基础的东西,比如目录结构等。重构工作是开着 JavaScript 版本的EasyManager 和 TypeScript 版本的EasyManager, 比较模块功能异同进行同步,同时方便梳理EasyManager功能(得益于目前EasyManager 功能不是很复杂), 在 2.6 版本测试期间发现很多功能与 2.5 版本的不同,甚至还是 2.3 版本,因此建议在项目重构工作开始前根据PRD 和产品进行一次完整的功能梳理避免遗漏。
重构内容
重构内容主要分为代码层面重构和结构层面重构:
代码层面重构主要工作是提取页面 props 和 state 创建 interface 再通过 React.Component 传给组件,然后需要的把一些参数加上类型就可以。需要注意的是,页面/组件需要的 prop 必须现在 interface 中声明出来,一个组件可能包含 redux 的 state 和其父组件传入的 props,此时要将 redux 的 state 设置成可选,这样父组件才可以在不指定其值的情况下使用。
代码层面重构改变的东西并不多,样式文件直接拷贝内容就可以。
结构重构主要是将文章开始讲的那种以模块为单位进行划分的目录结构改成以 ts 文件类型/功能进行划分。
TypeScript 版本的 EM 将各个 action 和各个 reducer 集中在了一个文件夹下,因此每个模块都需要将其相应的 action 和 reducer 新建到 actions 和 stores 文件夹内。
页面内容集中在 pages 文件夹下,其中的一个模块的具体目录长这样:
重构之后的Dashboard模块
style.scss 即是这个模块的样式文件,index.tsx 文件是此模块的入口,dashboard.tsx 则是此模块的逻辑文件,detail 则是此模块的子页面,也包括一个页面文件和入口文件,其他文件则是页面所需的数据、组件等文件。之前的模块构成是这样的:
重构之前的Dashboard模块
跟上面的比起来多了 redux 使用的 actions 和 reducer 文件,TypeScript 版本的已经将 action 和 reducer 提取到 src 目录下的 actions 文件夹和 stores 文件夹。
重构步骤
重构内容在项目跑起来之后就主要包括四个步骤:
Step 1 迁移业务逻辑代码
迁移业务逻辑代码是整个过程中最为繁琐的。
迁移步骤大概可分为:页面主文件代码迁移、service 层迁移,redux 层迁移。
01 页面主文件代码迁移
将页面代码 copy 到新文件里即可,得益于 vscode 对 TypeScript 的良好支持,页面中的错误都会有提示,如下图:
VsCode智能提示
我们对这种错误进行处理就可以了,常见的问题有总结,详情见重构注意事项。
02 service 层迁移
service 层迁移分为两步:补充 api、补充 service
这层迁移还是很方便的,copy 页面代码之后,如果有页面直接调用 service 请求但是当前却没有这个方法的话编译器会报错,我们在页面文件里根据这个错误逐一添加即可。
03 redux 层迁移
redux 层迁移分为 6 步:
- 定义 Reducer 的 model(数据模型)
modal 文件定义了所有 reducer 使用的数据结构模型,在具体的 reducer 中我们使用此模型约束 redux 数据结构: 暴露Store接口。
暴露Store接口
2. 迁移 reducer 文件
迁移 reducer 直接把原来的代码 copy 即可,需要注意的是在这里初始化 redux 数据的时候,需要指定此 state 的数据模型:
reducer里使用约束
同时还需要暴露此数据模型供页面使用,避免页面开发出现 undefined 之类的错误,同时得益于 vscode 对 TypeScript 的良好支持能够一定程度的提高我们的开发效率(在当前版本的 em 里面没有用到这个导出,目前用到的是下面的 index 文件统一包装导出的 modal 接口)。
3. 补充 reducer 的 index 文件
reducer 经过 index 的统一导出给所有页面提供了所有 modal 的数据结构接口:
暴露所有store接口供使用
在页面中使用 AppStoreTypes 即可:
在pages里使用暴露的store接口进行约束
4. 补充 actionType 定义
补充 actionType 即把 2.5 版本的 action 使用的标识补充到新版本的 actionTypes 定义文件即可。
5. 迁移 action 文件
迁移 action 在把之前版本的 action 迁移过来之后另外需要导出此 action 的函数列表接口,这跟 reducer 导出 modal 是一样的意义,开发页面时可以直接进行使用:
暴露action函数提高开发效率
- 使用:
使用暴露的action函数接口
这样我们在使用 actions 的时候浏览器就能够智能提示了,同时对函数的参数也会严格限制。
Step 2 定义路由
本次重构过程因将 React Router V3 版本升级到了 V4 版本,因此路由部分改动蛮大。关于 React Router V3 迁移 V4 的详细内容大家可以在网上找到很多相关文章,本文在“重构障碍”中会详细聊聊。
定义路由主要工作就是将页面地址绑定到模块,因为使用了路由由父组件进行定义,所以总的路由文件很精简:
根路由文件
Step 3 迁移 CSS 内容
迁移 CSS 内容是整个过程中最令人舒适的部分啦!直接将已有的 CSS 文件 copy 到新目录下然后引用即可。
需要注意的是本次EasyManager重构加入了对 antd 组件微调的效果,我们除了全局样式 base.scss 之外还有一个用于 antd 组件的样式文件 dtemStyle.scss 文件:
用于全局规定antd组件微调的样式文件
在开发过程中我们会注意组件样式是否全局统一,设计是否有意差异化等问题,尽量统一对 antd 组件的微调标准。
Step 4 测试
每个模块基本都是遵循此步骤进行重构。期间,复杂的地方主要在第一步和第二步,迁移业务逻辑代码将需要迁移页面 jsx 代码、页面 redux 的 action 以及 reducer 代码。第二步的复杂体现在涉及到嵌套路由的应用上,这块我们在“重构障碍”里详细解释。
重构障碍
React Router V4 的嵌套路由
React Router V4 版本相对于 2、3 版本改动较大,思想上也更向React 思想靠近。现在 React Router 取消了在一处管理所有页面路由的方式,取而代之的是在各个容器里进行管理。Router 像一个字组件一样放在了父容器里。
下面放一个官方 Demo 截图:
react-router4官方示例
本次重构过程中遇到了嵌入页面无法正常显示问题,在发现不可以像 route v3 那样在一个文件里统一进行配置路由之后,我修改了每个模块的入口文件 index.js,把其改成了 react-router 4 风格的路由定义文件,如下图:其中 props.match.path 指向父组件导航过来的当前路由,默认渲染第一个组件,比如从 A 页面点击/tob,那么/tob 就是渲染的 AlertChannel 组件,/tob/addAlertChannel 则渲染 AddAertChannelPanel 组件。(路由从配置文件变成了组件)
browserRouter 造成子组件页面刷新白屏问题,在 HTML 里加入根目录即可:
在HTML中解决,也可通过webpack解决
react refs 问题
主要情景是用户搜索特定 menu 之后我们需要将选中的那个 menu 滚动到可视区域,使用 react 的 refs 的话会这样:
vs提示不存在属性
提示 Element 上没有我们需要的属性,当然了我们可以直接使用(this.refs.container as any).current 来规避 TypeScript 的错误提醒:
万物皆any解决
这样虽好,但是却违反了我们使用 TypeScript 的初衷:类型约束。如果开发者使用这个方法随便写了一个不存在的特性,那么又出现了常见的 undefined 错误。因此这里建议使用私有变量设定值为 react 创建制定 DOM 类型的 Ref:
创建响应类型的REF
在 dom 上绑定这个 ref:
绑定至dom
然后直接使用即可:
正常使用
重构常见处理
导入 react 因为 react 没有默认导出报错:
改成:
antd 的 Form.create 传入了组件 form 属性,但是 ts 不知道:
此时导入 antd 的 formProps 合并到组件即可:
使用 router 传入的 location 等等 props 需要在组件 prop 数据模型接口里声明,否则 TypeScript 也会报错的:
重构总结与下一步
关于 MVC
相较于 Vue 的 MVVM 思想,放佛 MVC 更能让人找到实践的入口点。结合 TypeScript,我们进一步的向 MVC 思想靠拢:
简化版MVC流程图
我们将类型约束加入到了 pages 使用 stores 的过程中,当然还包括 page 中自己的 state 或者父子组件的 prop 等也加入了类型约束。
按照 MVC 的思想,我们所有的操作都要由 actions 去完成,笔者的想法是不需要放到 redux 中的就不需要放,state 已经足够好用,如果仅仅是为了遵循 MVC 思想而把没有必要放在全局数据的变量放到全局里那对后来者理解项目代码和模块化开发来讲将是难以接受的。
因此笔者的建议是能不放 redux 的尽量不放,提高项目的模块化程度。
重构不足
本次重构过于仓促,重构前的准备工作也没有做的很充分。本次最大的不足有以下几点:
使用了过多的 any。包括但不局限于:服务器返回的数据格式,service 层请求入参格式以及各个函数的出参格式。
model 体现过于薄弱。在 reducer 中或者是页面 state 中都缺少了集中统一的 model 定义。
下一步
EM2.6 版本仅仅是完成了 JavaScript 到 TypeScript 的迁移,我们接下来的工作就是要充分发挥 TypeScript 的优势,下一步我们会将 service 层的入参进行接口定义,然后对后端返回的数据进行接口定义。然后剥离现在在一起的 reducer 的 store 接口们,我们会将它们分开存放便于查找与修改。