背景介绍
使用 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-domnpm 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中写法,一种是函数声明,另一种是函数扩展式,要求是,保证统一的写法,不管是第一种也好,还是第二种。```typescriptimport 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 Propsfunction Heading({ name, color }: Props): React.ReactNode {return <h1>My Website Heading</h1>}// Notice here we're using the function expression with the type OtherPropsconst OtherHeading: React.FC<OtherProps> = ({ name, color }) =><h1>My Website Heading</h1>
事件
处理表单事件
import React from 'react'const MyInput = () => {const [value, setValue] = React.useState('')// 事件类型是“ChangeEvent”// 我们将 “HTMLInputElement” 传递给 inputfunction 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) => voidchildren: 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 & PropsExcludingDefaultsreturn (props as any) as RecomposedProps}}const getProps = createPropsGetter(todoInputDefaultProps)export class TodoInput extends React.Component<Props, State> {public static defaultProps = todoInputDefaultPropsconstructor(props: Props) {super(props)this.state = {itemText: ''}}public render() {const { itemText } = this.stateconst { updateValue, handleSubmit } = thisconst { 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.tsxexport interface StoreState {languageName: string;enthusiasmLevel: number;}
actions
使用 constants 管理你的 reducer type
// src/constants/index.tsxexport 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.tsximport * 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.tsximport { 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.tsximport 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
