前言
之前看 antd 的源码,已经使用 TypeScript 重写了。对于像我这种喜欢通过实际项目学习技术的人,非常的友好。
一段时间内,我都是通过 antd 的源码来学习 TypeScript 的,但是纸上得来终觉浅,虽然自我感觉上,已经对 TypeScript 掌握的不错了,但是总觉得写起来没有自己想的这么简单。
空想不如实干,我的小程序需要做一个文章管理系统,正好可以使用 TypeScript 开发作为练手。
纸上得来终觉浅,绝知此事要躬行。
带着问题去寻找答案
项目开始之前,我并没有问题,写了一个页面之后,我就开始怀疑人生了。
......
列出这些问题的时候,也许我还不能完全能解答,希望整个知识重拾结束之后,我能找到答案。
基础往往不可或缺
TS官网对基础类型的介绍是下面这样一段话
为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。
从描述中不难提取的几个关键点
数据类型
// 声明布尔类型
let isDone: boolean = false;
// 声明数字类型
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d; // 支持十六进制、二进制、八进制字面量
// 声明字符串类型
let name: string = "bob";
// 声明数组类型
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3]; // 也可以使用数组泛型,Array<元素类型>:
// 声明元组类型 元组类型允许表示一个已知元素数量和类型的数组
let x: [string, number];
// 初始化变量
x = ['hello', 10];
// 声明枚举类型
enum Color {Red, Green, Blue}
let c: Color = Color.Green; // 打印结果是1,因为默认情况下,从0开始为元素编号。也可以手动的指定成员的数值。
// 声明any类型
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
// 声明void类型
function warnUser(): void {
console.log("This is my warning message");
}
// 声明undefined类型
let u: undefined = undefined;
// 声明null类型
let n: null = null;
// 声明never类型
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
// 声明object类型
declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
复制代码
\
类型断言
用途
一段话,你就明白它的用途了。
有时候,你会比 TypeScript 更了解某个值的详细信息。 比如它的确切类型。通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 这个时候 TypeScript 会假设你,程序员,已经进行了必须的检查。
写法
两种写法
“尖括号”语法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
复制代码
as 语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
复制代码
小结
原始类型包括:number,string,boolean,symbol,null,undefined。非原始类型包括:object,any,void,never;
any 类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查;因为有些时候编程阶段还不清楚类型的变量指定一个类型,不能一直卡着不动,所以可以使用 any 类型声明这些变量。同样的,需要尽量避免全部声明成 any 类型,不然使用 TS 就没有太大意义了;
声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefined 和 null;
undefined 和 null,它们的本身的类型用处不是很大,默认情况下 null 和 undefined 是所有类型的子类型。但是,当指定了--strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自。 这能避免很多常见的问题;
FAQ
注:以下所有问题的解答,并不是唯一的答案,大多是我根据开发经验总结出来的,所以见仁见智。
所有的变量都需要加类型注释吗?
问:
刚开始上手 TS,不自觉的就按照 JS 的写法,很多变量没有做类型注释,但是代码能编译通过,功能可以正常运行。怎么书写才是规范的?
答:
上面这个问题,正是我最初使用 TS 开发功能的一个困扰。我阅读了一些文章,结合自己的理解,我个人建议,能加类型注释的都加上。尤其是大型的多人协作的项目,添加类型注释,更有利于增强代码的可读性,也能有利于减少出错率。
比如下面的代码,通过类型注释我们能清除的了解到 checked 变量是布尔类型,但是 checkedEmail 变量却不能确定数据类型。
const [checked, setChecked] = useState<boolean>(false);
const [checkedEmail, setCheckedEmail] = useState(null);
复制代码
当为 checked 变量赋值其他类型的时候就会报错
setChecked(1); // TypeScript error: Argument of type '1' is not assignable to parameter of type 'SetStateAction<boolean>'
复制代码
所以我更推荐尽可能的添加类型注释。
类型注释之后取值时报错,很想使用 any 类型,怎么克服?
问:
有时候根据业务需要会声明比较复杂的嵌套对象,像登录/注册的切换功能,展示中按钮文案不同,我将展示内容提炼成一个公共方法,通过切换的 type 值区分当前展示的具体内容,但是实际使用 formObj[type]时会报错。如果将 formObj 声明成 any 类型,报错就会消失,很想一劳永逸的使用 any,怎么克服?
答:
可以分析一下导致报错的原因,上面的问题的原因是 TypeScript 不知道 type 的类型,所以出现了报错。可以通过类型断言的方式告诉 TypeScript 我很确定这个变量的数据类型是什么,就能解决问题了。
any 类型虽然能解决问题,但是治标不治本。一味的使用 any 类型,TS 的意见就不大了。
interface formItemInter {
btnName: string;
}
interface formInter {
login: formItemInter;
register: formItemInter;
}
const getFormTypeItem = (type: string) => {
const formObj: formInter = {
login: {
btnName: '立即登录',
},
register: {
btnName: '立即注册',
},
};
// let formItem = formObj[type]; // 报错:Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'formInter'.No index signature with a parameter of type 'string' was found on type 'formInter'.
let formItem = formObj[type as keyof typeof formObj]; // OK
return formItem;
};
复制代码
interface 和 type 两兄弟
之前学习的时候,interface和type这两个,我有点分不清底用哪个。
介绍对比
interface(接口)
在 TypeScript 里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
type(类型别名)
类型别名会给一个类型起个新名字。起别名不会新建一个类型,它创建了一个新名字来引用这个类型。
用法对比
interface(接口)
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj); // Size 10 Object
复制代码
type(类型别名)
type LabelledValue = {
label: string;
};
const printLabel = (labelledObj: LabelledValue) => {
console.log(labelledObj.label);
};
let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj); // Size 10 Object
复制代码
细微差别
类型别名可以像接口一样;然而,仍有一些细微差别。
type Name = string; // 基本类型
type NameUnion = string | number; // 联合类型
type NameTuple = [string, number]; // 元组
复制代码
注:可能有疑问的地方在于,interface 不是也可以声明联合类型吗?如下官方的示例,其实不是一个 interface 可以声明联合类型,而是 Bird 和 Fish 两个不同的 interface 联合定义类型,和 type 是不一样的。
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
复制代码
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
复制代码
FAQ
interface 和 type 怎么选择更加合理?
问:
interface 和 type,有时候用哪个都可以,那我怎么确定使用哪个呢?
答:
结合上面的对比,首先可以确定一个能用的两种情况:
其他类型定义能使用 interface,使用 interface 即可。
文章管理系统
React+TS+antd
此次开发的文章管理系统基于 React+TS+antd 的技术栈完成。
tsconfig.json
TS编辑选项官网很详情,可以根据需要进行设置。
{
"compilerOptions": {
"target": "esnext", // 指定ECMAScript目标版本 "esnext"
"lib": [
"dom",
"dom.iterable",
"esnext"
], // 编译过程中需要引入的库文件的列表。
"allowJs": true, // 允许编译javascript文件
"skipLibCheck": true, // 忽略所有的声明文件( *.d.ts)的类型检查。
"allowSyntheticDefaultImports": true, // 许从没有设置默认导出的模块中默认导入。
"strict": true, // 启用所有严格类型检查选项。
"forceConsistentCasingInFileNames": true, // 禁止对同一个文件的不一致的引用。
"module": "esnext", // 指定生成哪个模块系统代码
"moduleResolution": "node", // 决定如何处理模块。 "Node"对于Node.js/io.js
"resolveJsonModule": true, // 导入 JSON Module
"isolatedModules": true, // 将每个文件作为单独的模块
"noEmit": true, // 不生成输出文件
"jsx": "react", // 在 .tsx文件里支持JSX: "React"或 "Preserve"。
"sourceMap": true, // 生成相应的 .map文件。
"outDir": ".", // 重定向输出目录。
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错。
"esModuleInterop": true // 支持使用import d from 'cjs'的方式引入commonjs包。
},
"extends": "./paths.json",
"include": [
"src"
],
"exclude": [
"node_modules",
"dist"
]
}
复制代码
基础组件
正式开发页面之前,我首先完成的是基础组件的开发。后台系统的基础组件主要有布局组件、列表组件、按钮权限组件等。因为目前没有涉及到按钮权限,所以我首先实现的是前两个。
布局组件
文件路径:src/components/layout
index.tsx
/**
* @description 公共布局
*/
import React from 'react';
import { NO_LAYOUT } from '@/constants/common';
import BasicLayout from './Basic';
import BlankLayout from './Blank';
function Layout({ ...props }) {
const pathname = window.location.pathname;
/** @name 不需要布局页面的索引值 */
const noLayoutIndex = NO_LAYOUT.indexOf(pathname);
return noLayoutIndex === -1 ? <BasicLayout {...props} /> : <BlankLayout {...props} />;
}
export default Layout;
复制代码
Blank.tsx
/**
* @description 纯页面展示 不含头、底、导航菜单
*/
import React from 'react';
import './index.less';
import Page from './page';
function BlankLayout({ ...props }) {
return (
<div className='layout'>
<Page>{props.children}</Page>
</div>
);
}
export default BlankLayout;
复制代码
Basic.tsx
/**
* @description 包含公共头、底、导航菜单的基础布局
*/
import React from 'react';
import Page from './page';
import Sidebar from './sidebar';
import Header from './header';
import Content from './content';
import Main from './main';
function BasicLayout({ ...props }) {
return (
<Page>
<Header />
<Main>
<Sidebar />
<Content>{props.children}</Content>
</Main>
</Page>
);
}
export default BasicLayout;
复制代码
列表组件
文件路径:src/components/list
index.tsx
/**
* @description 通用列表组件
*/
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Table } from 'antd';
function List({ ...props }) {
const { columns, autoQuery, http } = props;
const [list, setList] = useState([]);
const [total, setTotal] = useState<number>(0);
const [page, setPage] = useState<number>(1);
const [size, setSize] = useState<number>(20);
const query = (page: number, size: number) => {
const params = { page, size };
http(params, (res: any) => {
setList(res.list);
setTotal(res.total);
});
};
// 分页、排序、筛选变化时回调函数
const paginationChange = (pages: number, sizes: number) => {
setPage(pages);
setSize(sizes);
query(pages, sizes);
};
useEffect(() => {
if (autoQuery) {
query(page, size);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<>
<Table
dataSource={list}
rowKey={record => record['id']}
columns={columns}
scroll={{ x: '100%' }}
pagination={{
total,
current: page,
pageSize: size,
onChange: paginationChange,
showQuickJumper: true,
showSizeChanger: true,
showTotal: total => `共 ${total} 条`,
}}
/>
</>
);
}
List.propTypes = {
http: PropTypes.func.isRequired, // 请求
columns: PropTypes.array, // 表格项列表
autoQuery: PropTypes.bool, // 是否第一次加载就进行查询,默认为true
};
List.defaultProps = {
columns: [],
autoQuery: true,
};
export default List;
复制代码
常量管理
将前端需要维护的内容统一在一处管理,有利于提升开发效率和可维护性。这些内容包括网站公共的 logo、icon 或者其他信息,某些数据枚举值、表格列的配置描述等。
除了公共常量,其他基本根据页面模块管理常量。
公共常量
文件路径:src/constants/common.js
common.js
/**
* @description 全局公共常量
*/
/** @name 网站公共信息 */
export const COMMON_SYSTEM_INFO = {
avatar: 'https://p6-passport.byteacctimg.com/img/user-avatar/c6c1a335a3b48adc43e011dd21bfdc60~300x300.image', // 头像
};
复制代码
用户常量管理
文件路径:src/constants/user.js
user.js
/**
* @description 用户常量管理
*/
import { util } from '@/utils';
/** @name 用户列表 */
export const USER_COLUMNS = [
{
title: '用户ID',
dataIndex: 'id',
key: 'id',
},
{
title: '姓名',
dataIndex: 'userName',
key: 'userName',
},
{
title: '创建时间',
dataIndex: 'creatAt',
key: 'creatAt',
render(val) {
return util.dateFormatTransform(val);
},
},
];
复制代码
API 管理
除了基础的 api,其他基本根据页面模块管理 api。
因为后端部分还没有开发,所以目前 api 均由模拟实现。
用户 API 管理
文件路径:src/api/user.js
user.js
import { util } from '@/utils';
// 首页列表
export const getUserList = function (requestData, successCallback) {
const { page, size } = requestData;
const total = 24;
let numList = new Array(total);
let list = [];
for (var i = 0; i < numList.length; i++) {
const index = i + 1;
list[i] = {
id: index,
name: '花狐狸' + index,
creatAt: 1652172686000,
};
}
let res = {
total: total,
list: [],
};
if (total !== 0) {
res.list = util.getListByPageAndSize(total, page, size, list);
}
successCallback && successCallback(res);
};
复制代码
页面
目前规划的四个部分:用户中心、游记管理、城市数据管理、活动中心。
首页
文件路径:src/pages/home/index.tsx
展示当前用户、文章的增长数据。
index.tsx
/**
* @description 首页
*/
import React, { useState, useEffect } from 'react';
import { Statistic, Row, Col, Card } from 'antd';
import './index.less';
import { getHomeData } from '@/api/home';
interface topListInter {
title: string;
value: number;
}
export default function Home() {
const [topList, setTopList] = useState<Array<topListInter>>([]);
useEffect(() => {
getHomeData({}, (res: Array<topListInter>) => {
setTopList(res);
});
}, []);
return (
<div className='home'>
<Row gutter={16}>
{topList.map((item, index) => {
return (
<Col span={4} key={index}>
<Card className='home-card'>
<Statistic title={item.title} value={item.value} />
</Card>
</Col>
);
})}
</Row>
</div>
);
}
复制代码
UI
用户列表
文件路径:src/pages/user/index.tsx
因为已提炼了 List 公共组件,所以列表页面代码非常简洁。
index.tsx
/**
* @description 用户列表
*/
import React from 'react';
import { getUserList } from '@/api/user';
import List from '@/components/list';
import { USER_COLUMNS } from '@/constants/user';
export default function UserList() {
const columns = USER_COLUMNS;
return (
<div>
<List columns={columns} http={getUserList} />
</div>
);
}
复制代码
UI
心得体会
本次项目总结开始之前先回答上面的一个问题
FAQ
问:
项目中真的有必要使用 TS 吗?
答:
以我的实际工作经验,我推荐使用 TS 的原因之一,在团队协作项目中,代码可读性不高的原因之一是代码规范不统一,尽管我们做了辅助工作比如命名规范、添加必要注释、Code Review
等,但是这些都是人为干预,远远不如代码干预的效率高且准确性好。TS 在编写层面已经严格约束了代码规范,比如通过类型注释约束了变量类型等,进而增加了代码的可读性。
总结
目前,文章管理系统的基础组件和页面已经基本完成了,后续会随着功能设计内容逐渐丰富。而对 TS 的学习也会随着实践逐步积累经验。
github 源码: travel-cms-ts
评论