作者简介:aoto 蚂蚁金服·数据体验技术团队
摘要:之前写过一篇《TypeScript 体系调研报告》,经过半年多的蚂蚁金服数据平台大规模 JS 项目实战,沉淀了一些编程实战经验和感悟。
TypeScript 是有类型定义的 JS 的超集,包括 ES5、ES5+ 和其他一些诸如泛型、类型定义、命名空间等特征的集合,为了大规模 JS 应用而生。对于 TypeScript 本身,更多信息请参考《TypeScript 体系调研报告》。本文只记录 TypeScript 在我们实际项目中产生的一些实际有用的价值。我们的项目基于 React 体系,因此本文着重关注 React 体系和 TypeScript 结合使用的经验。
在 TypeScript 开发环境下写 React 组件,与 ES6 的区别主要就是 Props 和 State 的定义。如果是 ES6,大概是这样:
import PropTypes from 'prop-types';
class App extends React.PureComponent {
state = {
aState: '',
bState: '',
};
}
App.propTypes = {
aProps: PropTypes.string.isRequired,
bProps: PropTypes.string.isRequired,
};
如果用 TypeScript 来写,大概是这样(本文的 interface 定义默认以 I 开头):
interface IProps {
aProps: string;
bProps: string;
}
interface IState {
aState: string;
bState: string;
}
class App extends React.PureComponent<IProps, IState> {
state = {
aState: '',
bState: '',
};
}
TypeScript 自带 JSX 解析器,因此为了充分利用它本身的是静态检查功能,所以用泛型来定义 Props 和 State 的类型。如上定义后,在成员方法中通过this.props.
和this.state.
使用 Props 和 State 时可以智能提示,而且会做类型检查。看起来写没有变的简洁多少,但 TypeScript 来开发 React 应用有一个很大的优势,TS 环境中的 React 组件属性是静态的,也就是说可以做静态检查,并且在支持 TS 的 IDE 下写的过程中可以自动自动提示(是否存在以及类型是否正确)/补全,**这对于保障大型 React 应用的代码质量和运行时质量很有帮助。**而 ES6 环境中的 prop-types 属性是动态的,也就是运行期做检查,也不能做自动提示/补全。由此看来除了 JS 代码本身, TypeScript 对 React 组件开发也是很有好处的。
Redux 是最流行的状态管理库,一般和 React 结合使用。在 TypeScript 和 ES6 环境下,Redux 代码写法区别不大,有个问题就是,React 和 Redux 的绑定函数 connect 需用函数写法,而不建议用装饰器写法。因为 TS 对 类装饰器的静态解析还不支持,用了装饰器写 connect 就不能利用 TS 的静态解析的好处了。
const mapStateToProps = (state: {}, ownProps: IProps) => {};
const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Antd 是一个流行的 React 组件库,提供了 TS 类型声明,在写组件的时候可以进行属性的自动提示和检查,不确定的可以进入类型声明文件查看,减少了查阅在线 API 文档的次数和属性拼写错误的可能性。
React + Redux + TypeScript 的编程体验:
复杂软件需要用复杂的设计,面向对象就是很好的一种设计方式,使用 TS 的一大好处就是 TS 提供了业界认可的类( ES5+ 也支持)、泛型、封装、接口面向对象设计能力,以提升 JS 的面向对象设计能力。
泛型,简单说就是泛化的类型。我们项目中的泛型实践主要在 React 组件中,这也是借鉴了 React 的 Component 和 PureComponent 在 TS 中的定义。代码如下:
class Component<P, S> {}
class PureComponent<P = {}, S = {}> extends Component<P, S> {}
P 和 S 就是泛型,这里的 P 是 Props,S 是 State。从这里可以看到如果我们要基于 React 组件进行基于继承的设计,那泛型就可以发挥作用了。这里举一个例子,假如我们要设计一个复杂 UI 模块,有两层继承,BaseComponent 继承 React.PureComponent,XComponent 继承 BaseComponent,效果就是 XComponent extends BaseComponent extends React.PureComponent
,示例代码如下:
export interface IBaseProps {}
export interface IXProps {}
export class BaseComponent<
IProps extends IBaseProps,
IState = {}
> extends React.PureComponent<IProps, IState> {}
export class XComponent<
IProps extends IXProps,
IState = {}
> extends BaseComponent<IProps, IState> {}
BaseComponent 的属性(props)定义是 IProps,继承自 IBaseProps,这样 BaseComponent 组件中就可以使用 IBaseProps 中定义的属性了。
XComponent 的属性定义是 IProps,继承自 IXProps,同时 XComponent 又继承自 BaseComponent,因为我们在 BaseComponent 类的属性泛型定义中 IProps 是继承 IBaseProps,这样两个条件共同作用于 XComponent,就有了 IProps extends IXProps extends IBaseProps
的效果了,就可以在 XComponent 中同时使用 IBaseProps 和 IXProps 的属性了。
我们都知道,封装在对于面向对象软件设计非常有用,而封装各个模块的实现细节,就可以在有效管理软件的复杂度。 TS 对于代码封装性的帮助主要体现在它提供了类似于 Java 的访问控制符。有了 private/protected/public
,我们可以自主的控制类需要对外暴露的接口。访问权限需尽可能严,也就是说无需对外暴露的用 private 或者 protected ,如无需被子类使用的就用 private,只有明确需要对外暴露的接口采用 public 来描述。在一些非 React 组件的公共类,封装特性尤为必要。
接口在面向对象设计里面也是很重要的,接口也就是对外提供服务的端口。我们举一个例子,一个公共模块类 A ,提供了几个接口findById
,updateData
,destroy
。那么我们要定义一个接口的定义,方便别的模块使用(这和传统编程语言的接口有点不同,TS 的接口并不会自己运行):
export interface IClassA {
findById(id: string): IModel;
updateData(data: IModel): void;
destroy(): void;
}
调用方:
const a = new IClassA();
a.findById('1');
a.updateData({ id: '1', name: 'a' });
a.destroy();
有了接口声明,我们在使用该模块的时候可以清晰的看到它到底有哪些接口,方法的入参是什么,返回值是什么。同时也有代码的自动提示,提升开发效率,减少拼写错误导致的低级 Bug。
现在流行的库如 React 、Redux 、 Lodash 、 Antd 等等都有 TS 类型声明,加上我们自己业务代码完善的类型定义,整个代码库的健康度可以较好的保持住。这在传统的大规模 JS 应用里面是很难得的。以前总感觉 JS 一复杂,就感觉质量难以保证,线上运行也有点虚,说不定什么时候就爆出一个 's' is undefined, 'b' is not a function 之类的错误。现在有了静态检查,心里更有底了。
上面章节已经就 TS 对面向对象设计能力的增强做了描述。设计的增强,是可以提升代码质量的。良好的设计面对需求迭代的不断冲击,可以保持代码的可维护性和可扩展性,也就提升了代码的质量和健康度。
现在的 Web 应用很多都是复杂的单页应用,尤其是一些工具类的产品,复杂度慢慢接近甚至不亚于一些传统的桌面软件,如 Word、Excel、SQL 客户端、数据集成分析工具等等,这些 Web 应用都可以说是大规模 JS 应用,需要一个团队来协作迭代开发。那么协作效率、代码可读性、可维护性、健壮性等等都是我们应该重点专注的点。
协作效率本身比较难以衡量,但可以从阅读、维护别人代码的难度和效率这一点来说明。如果有有完善的类型定义,再加上智能的 TS IDE(如 VS Code),从我们项目的实践经验来看可以显著的提升代码的可读性、可维护性。很多时候都是看看源代码就能较容易的发现一些问题和 Bug。随着产品的不断迭代,一来一回之间就提升了整个团队的协作效率。
我们项目的 TS 校验规则是很严格的,意味着代码中基本没有 any ,都需要定义具体类型。对于一些公共的模块,我们也会严格定义 private、protected、public 等访问控制符,这意味着代码即文档,代码就能说明哪些是对外的接口,哪些是内部使用的。
一些常用的第三方库和框架都有完善的 TS 类型定义,同时整个项目业务代码都有完善的 TS 类型定义,静态检查出错的都会在 IDE 中提示出来,甚至可以阻断构建流程,这样可以减少 Bug 的产生,代码质量更为可控,代码健康度更高,心里也更有底了。