1、创建支持 TypeScript
的 React
项目
执行下面的命令新建一个名字为 demo
的 React
项目:
npx create-react-app demo --template typescript
根据 typescript
官网文档的说明,还可以使用下面的命令:
npx create-react-app demo --scripts-version=react-scripts-ts
react-script-ts
是一个在采用了标准的 create-react-app
项目流程 的基础上,混合了 TypeScript
的功能的集合。
我想,原来是采用的第二种方式,后来就整理为了第一种方式,这里采用第一种方式。
创建的项目文件结构:
demo/
|─ node_modules/
|─ public/
| └─ favicon.ico
| └─ index.html
| └─ manifest.json
| └─ ...
|─ src/
| └─ ...
|─ .gitignore
|─ package.json
|─ package-lock.json
|─ README.md
└─ tsconfig.json
执行:
npm start
运行项目,默认服务将运行在 localhost:3000
。
2、tsconfig.json
配置文件详解
如果一个目录下存在一个 tsconfig.json
文件,那么它意味着这个目录是 TypeScript
项目的根目录,tsconfig.json
文件中指定了用来编译这个项目的根文件和编译选项。
一个项目可以通过以下方式之一来编译:
- 不带任何输入文件的情况下调用
tsc
命令,编译器会从当前目录开始去查找tsconfig.json
文件,逐级向上搜索父目录。 - 不带任何输入文件的情况下调用
tsc
命令,且使用命令行参数--project
(或-p
)指定一个包含tsconfig.json
文件的目录。
当命令行上指定了输入文件时,tsconfig.json
文件会被忽略。
一个 tsconfig.json
文件主要有以下配置项:
{
"compilerOptions": {},
"files": [],
"include": [],
"exclude": [],
"extends": "",
"compileOnSave": false,
"typeAcquisition": {}
}
compilerOptions
compilerOptions
:对象类型,用来设置编译选项,若不设置则默认使用上节介绍的默认配置。
下面是一份梳理的常用 compilerOptions
属性配置:
{
"compilerOptions": {
"target": "esnext", /* 指定编译之后的版本目标: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "esnext", /* 指定要使用的模块标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"noImplicitAny": false, /* 是否默认禁用 any */
"removeComments": true, /* 是否移除注释 */
"declaration": true, /* 是否自动创建类型声明文件 */
"strict": true, /* 启动所有类型检查 */
"jsx": "preserve", /* 指定jsx代码用于的开发环境 */
"importHelpers": true, /* 引入tslib里的辅助工具函数*/
"moduleResolution": "node", /* 选择模块解析策略,有'node'和'classic'两种类型 */
"experimentalDecorators": true, /* 启用实验性的装饰器特性 */
"esModuleInterop": true, /* 通过为导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性 */
"allowSyntheticDefaultImports": true, /* 允许从没有默认导出的模块中默认导入 */
"sourceMap": true, /* 是否生成map文件 */
"baseUrl": ".", /* 工作根目录 */
"types": [ /* 指定引入的类型声明文件,默认是自动引入所有声明文件,一旦指定该选项,则会禁用自动引入,改为只引入指定的类型声明文件,如果指定空数组[]则不引用任何文件 */
"webpack-env",
"jest"
],
"paths": { /* 指定模块的路径,和 baseUrl有关联,和 webpack 中 resolve.alias 配置一样 */
"@/*": [
"src/*"
]
},
"lib": [ /* 译过程中需要引入的库文件的列表 */
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
files
,include
和 exclude
iles
是一个数组列表,写入待编译文件的相对或绝对路径,不支持glob
匹配模式。include
是一个数组列表,写入待编译文件的路径,支持glob
匹配模式。exclude
也是一个数组列表,写入排除某些文件路径,这些文件排除于待编译列表,支持glob
匹配模式。
glob
通配符有:
*
匹配 0 或多个字符(不包括目录分隔符)?
匹配一个任意字符(不包括目录分隔符)**/
递归匹配任意子目录
如果 "files"
和 "include"
都没有被指定,编译器默认包含当前目录和子目录下所有的 TypeScript 文件(.ts
, .d.ts
和 .tsx
),排除在"exclude"
里指定的文件。
如果开启了 allowJs
选项,那 .js
和 .jsx
文件也属于编译器包含范围。
{
"files": [
"core.ts",
"index.ts",
"types.ts"
],
"exclude": [
"node_modules",
"lib",
"**/*.test.ts"
],
"include": [
"src/**/*"
],
}
如果没有特殊指定,"exclude"
默认情况下会排除 node_modules,bower_components,jspm_packages
和 <outDir>
目录。
任何被 "files"
或 "include"
指定的文件所引用的文件也会被包含进来。
优先级:命令行配置 > files > exclude > include
extends
extends
:字符串类型,指向另一个要继承文件的路径。例如:
{
"extends": "config/base.json"
}
这个配置项的意思就是我们可以借助 "extends"
属性引入路径为 "config/base.json"
的配置文件中的配置选项。
configs/base.json
:
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
需要注意:
- 如果有同名配置,继承文件里的配置会覆盖源文件里的配置
compileOnSave
compileOnSave
是一个布尔类型的属性,当值为 true 时,设置 compileOnSave
属性到 IDE
,以便 tsconfig.ts
文件在保存时能够重新生成文件。
typeAcquisition
typeAcquisition
:对象类型,用以设置自动引入库类型定义文件(.d.ts
),该属性下面有3个子属性:
enable
: 布尔类型,用以设置是否开启自动引入库类型定义文件include
: 数组类型,允许自动引入的库名列表,如["jquery", "kendo-ui"]
exclude
: 数组类型,排除的库名列表
@types
,typeRoots
和 types
默认情况下,node_modules/@types
文件夹下以及它们子文件夹下的所有包都会在编译过程中被包含进来。
但是如果指定了 typeRoots
,则只有 typeRoots
路径下的包才会被包含进来:
{
"compilerOptions": {
"typeRoots" : ["./typings"]
}
}
这个配置文件会包含所有 ./typings
下面的包,而不包含 ./node_modules/@types
里面的包。
如果指定了 types
,只有被列出来的包才会被包含进来。比如:
{
"compilerOptions": {
"types": ["node", "lodash", "express"]
}
}
如果 types
设置为空数组,则禁止自动引入 @types
包:
{
"compilerOptions": {
"types": []
}
}
注意,自动引入只在你使用了全局的声明(相反于模块)时是重要的。如果你使用 import "foo"
语句,TypeScript
仍然会查找 node_modules
和node_modules/@types
文件夹来获取 foo
包。
3、创建第一个 Hello.tsx
组件
在创建好的项目中,简化 src
目录下的内容,删除 src/index.tsx
之外的所有文件,将 src/index.tsx
文件的内容做精简,示例代码如下:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<div>
</div>
,document.getElementById('root'));
创建 src/Hello.tsx
文件,示例代码如下:
import React from 'react'
interface IProps {
message: string
}
const Hello = (props:IProps) => {
return <h2>{props.message}</h2>
}
export default Hello
把 Hello.tsx
组件引入到 index.tsx
入口文件中,示例代码如下:
import React from 'react';
import ReactDOM from 'react-dom';
import Hello from './Hello';
ReactDOM.render(
<div>
<Hello message="hello world" />
</div>
,document.getElementById('root'));
在终端执行 npm start
启动项目。
对上面代码还可以继续做优化。修改 src/Hello.tsx
组件,示例代码如下:
import React from 'react'
interface IProps {
message?: string
}
const Hello:React.FunctionComponent<IProps> = (props) => {
return <h2>{props.message}</h2>
}
Hello.defaultProps = {
message: 'hello world'
}
export default Hello
在上面代码中,声明函数组件时使用 React.FunctionComponent
接口,该接口接受一个泛型,使用代码中定义的 IProps
接口作为泛型,此时的 Hello
组件中就可以接收 props.children
的属性,用于接收组件实例中传入的子节点。同时,还可以使用 defaultProps
为 props
对象中的属性设置初始化值。
在组件中引入 React.FunctionComponent
单词有点长,可以使用简写的方式引入,修改后的代码如下:
const Hello:React.FC<IProps> = (props) => {
return <h2>{props.message}</h2>
}
在上面代码中,React.FC
就是 React.FunctionComponent
的简写。
4、封装一个 Button
组件
为了方便的拼接 className
的值,本案例中使用了 classnames
工具。
安装 classnames
工具:
$ npm install classnames --save
$ npm install @types/classnames --save
创建 src/components/Button/button.tsx
组件,示例代码如下:
import React from 'react';
import classNames from 'classnames';
import './button.css';
// 声明按钮尺寸枚举
export enum ButtonSize {
Large = 'lg',
Small = 'sm'
}
// 声明按钮样式枚举
export enum ButtonType {
Primary = 'primary',
Default = 'default',
Danger = 'danger',
Link = 'link'
}
// 声明按钮组件的 props 接口
interface BaseButtonProps {
className?: string;
disabled?: boolean;
size?: ButtonSize;
btnType?: ButtonType;
children?: React.ReactNode;
href?: string
}
// 声明按钮与超链接标签的原生事件
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLElement>
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
const Button:React.FC<ButtonProps> = (props) => {
const {
className,
btnType,
disabled,
size,
children,
href,
...restProps // 解构按钮与超链接的原生事件属性
} = props
// 使用 classnames 工具拼接样式的 class 值
let classes = classNames('btn',className,{
[`btn-${btnType}`]: btnType,
[`btn-${size}`]:size
})
if(btnType === ButtonType.Link && href) {
return (
<a
className={classes}
href={href}
{...restProps}
>
{children}
</a>
)
} else {
return (
<button
className={classes}
disabled={disabled}
{...restProps}
>
{children}
</button>
)
}
}
// 定义 props 的默认值
Button.defaultProps = {
disabled: false,
btnType: ButtonType.Default,
size: ButtonSize.Small
}
export default Button
创建 src/components/Button/button.css
样式文件,示例代码如下:
.btn {
width: 80px;
height: 30px;
font-size: 16px;
color: #666;
}
.btn-danger {
color: #f56;
}
.btn-primary {
color: #37f;
}
.btn-lg {
width: 150px;
height: 50px;
font-size: 22px;
}
.btn-sm {
width: 60px;
height: 20px;
font-size: 14px;
}
以上
css
的值仅供参考
在 src/App.tsx
组件中引入 Button
组件,示例代码如下:
import React from 'react'
import Button, {ButtonType,ButtonSize} from './components/Button/button.tsx';
import './app.css'
const App:React.FC = () => {
return (
<>
<Button disabled>Hello</Button>
<Button btnType={ButtonType.Primary} size={ButtonSize.Large}>Hello</Button>
<Button btnType={ButtonType.Danger} size={ButtonSize.Small}>Hello</Button>
<Button btnType={ButtonType.Link} href="http://www.baidu.com">百度一下</Button>
<Button className='my-btn'>hello</Button>
</>
)
}
export default App
创建 src/app.css
样式文件,示例代码如下:
.my-btn{
width: 300px;
height: 80px;
color: 'pink';
font-size: 15px;
}
以上
css
的值仅供参考