背景介绍
使用 Webpack 定制我们的脚手架,babel 编译 TS,支持类型检查和代码分离。
使用 React Hooks 编写我们的组件。
webpack 篇
一般的话,还是用 babel ,如果不用 babel ,使用 typescript 编译你的代码。如果用 babel ,使用 typescript 做类型检查,babel 做代码转换。
用 babel
参考这个 https://github.com/Microsoft/TypeScript-Babel-Starter#readme
依赖
1 babel的依赖
"devDependencies": {
"@babel/cli": "^7.8.3",
"@babel/core": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/preset-env": "^7.8.3",
"@babel/preset-typescript": "^7.8.3",
"typescript": "^3.7.5"
}
2 react 过程需要安装下面的依赖
npm install --save react react-dom @types/react @types/react-dom
npm install --save-dev @babel/preset-react
3 webpack 的依赖
npm install —save-dev webpack webpack-cli babel-loader
tsconfig.json
使用下面的命令生成 tsconfig.json
tsc --init --declaration --allowSyntheticDefaultImports --target esnext --outDir lib
修改 tsconfig.json 的 jsx 选项,改为 react
webpack.config.js
var path = require('path');
module.exports = {
// Change to your "entry-point".
entry: './src/index',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.bundle.js'
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json']
},
module: {
rules: [{
// Include ts, tsx, js, and jsx files.
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
loader: 'babel-loader',
}],
}
};
更改 .babelrc
{
"presets": [
"@babel/preset-env",
"@babel/react",
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-class-properties"
]
}
build 命令
ts 只做类型检查
"scripts": {
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch",
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline"
}
代码分离
需要引入一个新的插件
@babel/plugin-syntax-dynamic-import
{
"presets": ["@babel/preset-react"],
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
类型检查
babel 做代码转换,typescirpt 做类型检查
不用 babel
缺点:
- 不支持代码分离,如果想要结合路由懒加载,需要修改 module 为 ESnext,遇到低版本的浏览器,就gg了
- babel 的一些好用的插件不能用
- 垫片无法按需加载、antd 无法按需引入 ```javascript “jsx”: “react”, / Specify JSX code generation: ‘preserve’, ‘react-native’, or ‘react’. /
{ test: /.(t|j)sx?$/, use: { loader: ‘ts-loader’ }, exclude: /node_modules/ },
<a name="GmRqx"></a>
# 后面的内容 - Hooks + TS 的一些实践经验
<a name="l9f6F"></a>
# 组件
<a name="U42da"></a>
## fc 函数组件
有2中写法,一种是函数声明,另一种是函数扩展式,
要求是,保证统一的写法,不管是第一种也好,还是第二种。
```typescript
import React from 'react'
// 函数声明式写法
function Heading(): React.ReactNode {
return <h1>My Website Heading</h1>
}
// 函数扩展式写法
const OtherHeading: React.FC = () => <h1>My Website Heading</h1>
无状态组件 stateless
https://www.yuque.com/cashgw/blm6hv/whqtf8
有状态组件 statefull
- 私用方法加private
- 用React.createRef创建一个ref
import * as React from 'react'
interface Props {
handleSubmit: (value: string) => void
}
interface State {
itemText: string
}
export class TodoInput extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
itemText: ''
}
}
}
props
可以使用 interface 或者 type 来定义 props 的类型
你的 props 建议加上 readonly,可以使用工具类 ReadOnly
- 无论你为组件 Props 使用 type 还是 interfaces ,都应始终使用它们。
- 始终使用 TSDoc 标记为你的 Props 添加描述性注释 /* comment /。
- 这一条考虑一下
import React from 'react'
interface Props {
readonly name: string;
readonly color: string;
}
type Props2 = {
/** color to use for the background */
color?: string;
/** standard children prop: accepts any valid React Node */
children: React.ReactNode;
/** callback function passed to the onClick handler*/
onClick: () => void;
}
type OtherProps = {
name: string;
color: string;
}
// Notice here we're using the function declaration with the interface Props
function Heading({ name, color }: Props): React.ReactNode {
return <h1>My Website Heading</h1>
}
// Notice here we're using the function expression with the type OtherProps
const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
<h1>My Website Heading</h1>
事件
处理表单事件
import React from 'react'
const MyInput = () => {
const [value, setValue] = React.useState('')
// 事件类型是“ChangeEvent”
// 我们将 “HTMLInputElement” 传递给 input
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
}
return <input value={value} onChange={onChange} id="input-example"/>
}
HOC
使用 type
import React from 'react';
type ButtonProps = {
/** the background color of the button */
color: string;
/** the text to show inside the button */
text: string;
}
type ContainerProps = ButtonProps & {
/** the height of the container (value used with 'px') */
height: number;
}
const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}
使用 interface
import React from 'react';
interface ButtonProps {
/** the background color of the button */
color: string;
/** the text to show inside the button */
text: string;
}
interface ContainerProps extends ButtonProps {
/** the height of the container (value used with 'px') */
height: number;
}
const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}
Hooks
Hooks 对 TS 的支持很好,一般来说没啥问题。
Hooks 最佳实践
空值
type User = {
email: string;
id: string;
}
// the generic is the < >
// the union is the User | null
// together, TypeScript knows, "Ah, user can be User or null".
const [user, setUser] = useState<User | null>(null);
useReducer
type AppState = {};
type Action =
| { type: "SET_ONE"; payload: string }
| { type: "SET_TWO"; payload: number };
export function reducer(state: AppState, action: Action): AppState {
switch (action.type) {
case "SET_ONE":
return {
...state,
one: action.payload // `payload` is string
};
case "SET_TWO":
return {
...state,
two: action.payload // `payload` is number
};
default:
return state;
}
}
利用高级类型解决默认属性报错
import * as React from 'react'
// 定义 state 接口
interface State {
itemText: string
}
// 定义一个类型
// Partial 泛型类型
// & 类型合并
type Props = {
handleSubmit: (value: string) => void
children: React.ReactNode
} & Partial<typeof todoInputDefaultProps>
const todoInputDefaultProps = {
inputSetting: {
maxlength: 20,
placeholder: '请输入todo',
}
}
// 重点是这个函数
export const createPropsGetter = <DP extends object>(defaultProps: DP) => {
return <P extends Partial<DP>>(props: P) => {
type PropsExcludingDefaults = Omit<P, keyof DP>
type RecomposedProps = DP & PropsExcludingDefaults
return (props as any) as RecomposedProps
}
}
const getProps = createPropsGetter(todoInputDefaultProps)
export class TodoInput extends React.Component<Props, State> {
public static defaultProps = todoInputDefaultProps
constructor(props: Props) {
super(props)
this.state = {
itemText: ''
}
}
public render() {
const { itemText } = this.state
const { updateValue, handleSubmit } = this
const { inputSetting } = getProps(this.props)
return (
<form onSubmit={handleSubmit} >
<input maxLength={inputSetting.maxlength} type='text' value={itemText} onChange={updateValue} />
<button type='submit' >添加todo</button>
</form>
)
}
private updateValue(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({ itemText: e.target.value })
}
private handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (!this.state.itemText.trim()) {
return
}
this.props.handleSubmit(this.state.itemText)
this.setState({itemText: ''})
}
}
使用 Redux
定义 state 的形状
在 types 文件夹下新建一个文件
// src/types/index.tsx
export interface StoreState {
languageName: string;
enthusiasmLevel: number;
}
actions
使用 constants 管理你的 reducer type
// src/constants/index.tsx
export const INCREMENT_ENTHUSIASM = 'INCREMENT_ENTHUSIASM';
export type INCREMENT_ENTHUSIASM = typeof INCREMENT_ENTHUSIASM;
export const DECREMENT_ENTHUSIASM = 'DECREMENT_ENTHUSIASM';
export type DECREMENT_ENTHUSIASM = typeof DECREMENT_ENTHUSIASM;
// src/actions/index.tsx
import * as constants from '../constants';
export interface IncrementEnthusiasm {
type: constants.INCREMENT_ENTHUSIASM;
}
export interface DecrementEnthusiasm {
type: constants.DECREMENT_ENTHUSIASM;
}
export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;
export function incrementEnthusiasm(): IncrementEnthusiasm {
return {
type: constants.INCREMENT_ENTHUSIASM
}
}
export function decrementEnthusiasm(): DecrementEnthusiasm {
return {
type: constants.DECREMENT_ENTHUSIASM
}
}
reducers
immutable
// src/reducers/index.tsx
import { EnthusiasmAction } from '../actions';
import { StoreState } from '../types/index';
import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index';
export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
case DECREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1) };
}
return state;
}
废弃 - connect
现在可以使用 useSelector 和 useDiaptch, react-redux
提供的 hooks API。
// src/containers/Hello.tsx
import Hello from '../components/Hello';
import * as actions from '../actions/';
import { StoreState } from '../types/index';
import { connect, Dispatch } from 'react-redux';
export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
return {
enthusiasmLevel,
name: languageName,
}
}
export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
return {
onIncrement: () => dispatch(actions.incrementEnthusiasm()),
onDecrement: () => dispatch(actions.decrementEnthusiasm()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Hello);
创建 store
import { createStore } from 'redux';
import { enthusiasm } from './reducers/index';
import { StoreState } from './types/index';
const store = createStore<StoreState>(enthusiasm, {
enthusiasmLevel: 1,
languageName: 'TypeScript',
});
导入非代码资源
需要定义一个声明文件
declare module "*.svg" {
const content: any;
export default content;
}
项目地址
https://github.com/bhaltair/ts-react-webpack-starter