虽然 TypeScript 主要用于客户端,而数据模型的设计主要是服务端来做的。 但是要写出优雅的代码,也还是有不少讲究的。
让我们从一个简单的我的文章列表 api 返回的数据开始,返回的文章列表的信息如下:
{
"id": 2018,
"title" : "TypeScript 数据模型层的编程最佳实践",
"created" : 1530321232,
"last_modified" : 1530320620,
"status": 1
}
同时服务端告诉我们说:
status 各值的意思 0/未发布,1/已发布,2/已撤回
对于 status
这种可枚举的值,为了避免写出 status === 1
这种跟一个魔法常量的比较的代码,最佳的做法是写一个枚举,并配套一个格式化为字符串表示的函数,如下:
/**
* 文章状态
*/
const enum PostStatus {
/**
* 草稿
*/
draft = 0,
/**
* 已发布
*/
published = 1,
/**
* 已撤回
*/
revoked = 2
}
function formatPostStatus(status: PostStatus) {
switch (status) {
case PostStatus.draft:
return "草稿";
case PostStatus.published:
return "已发布";
case PostStatus.revoked:
return "已撤回";
}
}
如果 PostStatus
状态比较多的话,根据喜好可以写成下面的这样。
function formatPostStatus(status: PostStatus) {
const statusTextMap = {
[PostStatus.draft]: "草稿",
[PostStatus.published]: "已发布",
[PostStatus.revoked]: "已撤回"
};
return statusTextMap[status];
}
考虑到返回的 created
是时间戳值,我们还需要添加一个格式化时间戳的函数:
const enum TimestampFormatterStyle {
date,
time,
datetime
}
function formatTimestamp(
timestamp: number,
style: TimestampFormatterStyle = TimestampFormatterStyle.date
): string {
const millis = timestamp * 1000;
const date = new Date(millis);
switch (style) {
case TimestampFormatterStyle.date:
return date.toLocaleDateString();
case TimestampFormatterStyle.time:
return date.toLocaleTimeString();
case TimestampFormatterStyle.datetime:
return date.toLocaleString();
}
}
一开始的时候,由于之前的编程经验的影响,我一上来就搞一个数据类。如下:
class Post {
id: number;
title: string;
created: number;
last_modified: number;
status: number;
constructor(
id: number,
title: string,
created: number,
last_modified: number,
status: number
) {
this.id = id;
this.title = title;
this.created = created;
this.last_modified = last_modified;
this.status = status;
}
}
这可谓分分钟就写了 20 行代码。 然后如果你想到了 TS 提供了简写的方式的话,可以将上面的代码简写如下。
class Post {
constructor(
readonly id: number,
readonly title: string,
readonly created: number,
readonly last_modified: number,
readonly status: number
) {}
}
也就是说在构造函数中的参数前面添加如 readonly
,public
,private
等可见性修饰符的话,即可自动创建对应字段。 因为我们是数据模型,所以我们选择使用 readonly
。
一般再在 Post
添加几个 Getter,用于返回格式化好的要显示的属性值。
如下:
class Post{
// 构造函数同上
get createdDateString(): string {
return formatTimestamp(this.created, TimestampFormatterStyle.date);
}
get lastModifiedDateString(): string {
return formatTimestamp(this.last_modified, TimestampFormatterStyle.date);
}
get statusText(): string {
return formatPostStatus(this.status);
}
}
好了现在数据类写好,准备请求数据,绑定数据了。 一开始我们写出如下代码:
const posts:Post[] = resp.data
然后 TS 报如下错误:
[ts]
Type '{ id: number; title: string; created: number; last_modifistatic fromJson(json: JsonObject): Post {
return new Post(
json.id,
json.title,
json.created,
json.last_modified,
json.status
);
}ed: number; status: number; }[]' is not assignable to type 'Post[]'.
Type '{ id: number; title: string; created: number; last_modified: number; status: number; }' is not assignable to type 'Post'.
Property 'createdDateString' is missing in type '{ id: number; title: string; created: number; last_modified: number; status: number; }'.
此时我们开始意识到,请求回来的json
的 data
列表是普通的 object
不能直接给 Post
赋值。 由于一些编程惯性,我们开始想着,是不是反序列化一下,将json
对象反序列化成 Post
. 于是我们在 Post
类中添加如下的反序列化方法。
type JsonObject = { [key: string]: any };
class Post{
// 其他代码同上
static fromJson(json: JsonObject): Post {
return new Post(
json.id,
json.title,
json.created,
json.last_modified,
json.status
);
}
}
然后在请求结果处理上增加一过 map
用于反序列化的转换。如下:
const posts: Post[] = resp.data.map(Post.fromJson);
代码写到这里,思考一下,原来 json
就是一个原生的 JavaScript 对象了。但是我们又再一步又用来构造出 Post
类。这一步显得多余。
另外虽然一般我们的模型代码比如 Post
其实可以根据 api 文档自动生成,
但是也还是增加不少代码。
怎么改进呢? 既然我们的 json
已经是 JavaScrit 对象了,我们只是缺少类型声明。 那我们直接加上类型声明的,而且 TS 中的类型声明,编译成 js
代码之后会自动清除的,这样可以减少代码量。这对于小程序开发来说还是很有意义的。
自然我们写出如下代码。
interface Post {
id: number;
title: string;
created: number;
last_modified: number;
status: number;
}
此时,为了 UI 模板数据上的绑定。
我们双增加了一个叫 PostInfo
的接口。然后将代码修改如下:
interface PostInfo {
statusText: string;
createdDateString: string;
post: Post;
}
function getPostInfoFromPost(post: Post): PostInfo {
const statusText = formatPostStatus(post.status);
const createdDateString = formatTimestamp(post.created);
return { statusText, createdDateString, post };
}
const postInfos: PostInfo[] = (resp.data as Post[]).map(getPostInfoFromPost);
其实我想说的是,我们上面的代码中 Post
接口是多余的。
直接看代码:
const postDemo = {
id: 2018,
title: "TypeScript 数据模型层的编程最佳实践",
created: 1530321232,
last_modified: 1530320620,
status: 1
};
type Post = typeof postDemo;
当把鼠标放到 Post
上时,可以看到如下类型提示:
所以在开发开始时,可以先直接用 API 返回的数据结构当作一个数据模型实例。然后使用 typeof
来得到对应的类型。
PostInfo
这样包装其实挺丑陋的,
因为在我们心里这里其实应该是一个 Post
列表,但是为了格式化一些数据显示,我们弄一个 PostInfo
的包装,这样在使用上带来很多不方便。因为当你要使用 Post
的其他的值时,你总需要多一次间接访问比如这样 postInfo.post.id
。
这就PostInfo
是我们在使用 Post
实例时的一个枷锁,一个套,
现在我们来将这个套去掉。而去掉这个套的方法使用了两项技术。
一个是 TS 中接口的继承,一个是 Object.assign
这个方法。
直接用代码说话:
interface PostEx extends Post {
statusText: string;
createdDateString: string;
}
function getPostExFromPost(post: Post): PostEx {
const statusText = formatPostStatus(post.status);
const createdDateString = formatTimestamp(post.created);
return Object.assign(post, { statusText, createdDateString });
}
const posts: PostEx[] = (resp.data as Post[]).map(getPostExFromPost);
即保证了类型安全,使用上又方便,代码也不失优雅。
1
nl101531 2018-06-30 17:24:11 +08:00 via Android
看了一些代码,现在貌似都这样写,但是有个问题,在服务端有 domain 层概念,我想在这个实体类中加属于他的方法该怎么做? ts 的 class 加了这个会出问题,interface 又不能加,有什么好的解决办法吗?
|
2
banxi1988 OP |
3
nl101531 2018-07-01 08:38:18 +08:00
@banxi1988
比如有下面一个类,里面定义了一个 getFilePath()方法。 export class FileItem { fileName: string; md5Name: string; frontRoute: string; passwd: string; fileType: string; fileContent: string; getFilePath(): string { switch (this.fileType) { case 'md': return `${this.frontRoute}/${this.md5Name}/${this.passwd}/`; } } } 我用的是 Angular,有时候使用对象调用这个 item.getFilePath(),会得到 getFilePath is not funcition 这个错误,不知道为什么,还是说不支持这种写法。 |
4
banxi1988 OP @nl101531 #3 这种写法肯定是支持的。 具体到 Angular,出现错误的时候,你将 item 输出调试看看。
看这个对象是不是 FileItem 的对象. 有一种可能是 FileItem 对象被序列化之后,比如 ```js const fileItem = new FileItem(略); const jsonStr = JSON.stringify(fileItem); const fileItem2 = JSON.parse(jsonStr); // 此时 fileItem2 反序列化之后 `getFilePath` 是不会再存在的。 或者. ``` |