前言
之前看antd的源码,已经使用TypeScript重写了。对于像我这种喜欢通过实际项目学习技术的人,非常的友好。
一段时间内,我都是通过antd的源码来学习TypeScript的,但是纸上得来终觉浅,虽然自我感觉上,已经对TypeScript掌握的不错了,但是总觉得写起来没有自己想的这么简单。
空想不如实干,我的小程序需要做一个文章管理系统,正好可以使用TypeScript开发作为练手。
纸上得来终觉浅,绝知此事要躬行。
带着问题去寻找答案
项目开始之前,我并没有问题,写了一个页面之后,我就开始怀疑人生了。
- 所有的变量都需要加类型注释吗?
- 类型注释之后取值时报错,很想使用any类型,怎么克服?
- interface和type怎么选择更加合理?
- 项目中真的有必要使用TS吗?
......
列出这些问题的时候,也许我还不能完全能解答,希望整个知识重拾结束之后,我能找到答案。
基础往往不可或缺
TS官网对基础类型的介绍是下面这样一段话
为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。
从描述中不难提取的几个关键点
- 基础数据处理是必不可少的;
- TypeScript和JavaScript的数据类型基本是一致,降低了学习难度;
- 提供了枚举类型,常年做业务开发的经验告诉我枚举类型很实用;
数据类型
代码语言: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会假设你,程序员,已经进行了必须的检查。
写法
两种写法
“尖括号”语法:
代码语言:javascript复制let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
as语法:
代码语言:javascript复制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变量却不能确定数据类型。
代码语言:javascript复制const [checked, setChecked] = useState<boolean>(false);
const [checkedEmail, setCheckedEmail] = useState(null);
当为checked变量赋值其他类型的时候就会报错
代码语言:javascript复制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的意见就不大了。
代码语言:javascript复制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(接口)
代码语言:javascript复制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(类型别名)
代码语言:javascript复制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可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。但是interface不行。
type Name = string; // 基本类型
type NameUnion = string | number; // 联合类型
type NameTuple = [string, number]; // 元组
注:可能有疑问的地方在于,interface不是也可以声明联合类型吗?如下官方的示例,其实不是一个interface可以声明联合类型,而是Bird和Fish两个不同的interface联合定义类型,和type是不一样的。
代码语言:javascript复制interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
• interface可以相互继承,type不可以
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
FAQ
interface和type怎么选择更加合理?
问:
interface和type,有时候用哪个都可以,那我怎么确定使用哪个呢?
答:
结合上面的对比,首先可以确定一个能用的两种情况:
- 如果使用联合类型、元组等类型的时候,用type起一个别名使用;
- 如果需要使用extends进行类型继承时,使用interface;
其他类型定义能使用interface,使用interface即可。
文章管理系统
React TS antd
此次开发的文章管理系统基于React TS antd的技术栈完成。
tsconfig.json
TS编辑选项官网很详情,可以根据需要进行设置。
代码语言:javascript复制{
"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
代码语言:javascript复制/**
* @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
代码语言:javascript复制/**
* @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
代码语言:javascript复制/**
* @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
代码语言:javascript复制/**
* @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
代码语言:javascript复制/**
* @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
代码语言:javascript复制/**
* @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
代码语言:javascript复制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
代码语言:javascript复制/**
* @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
代码语言:javascript复制/**
* @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的学习也会随着实践逐步积累经验。