背景介绍

使用 Webpack 定制我们的脚手架,babel 编译 TS,支持类型检查和代码分离。

使用 React Hooks 编写我们的组件。

webpack 篇

一般的话,还是用 babel ,如果不用 babel ,使用 typescript 编译你的代码。如果用 babel ,使用 typescript 做类型检查,babel 做代码转换。

用 babel

参考这个 https://github.com/Microsoft/TypeScript-Babel-Starter#readme

依赖

1 babel的依赖

  1. "devDependencies": {
  2. "@babel/cli": "^7.8.3",
  3. "@babel/core": "^7.8.3",
  4. "@babel/plugin-proposal-class-properties": "^7.8.3",
  5. "@babel/preset-env": "^7.8.3",
  6. "@babel/preset-typescript": "^7.8.3",
  7. "typescript": "^3.7.5"
  8. }

2 react 过程需要安装下面的依赖

  1. npm install --save react react-dom @types/react @types/react-dom
  2. npm install --save-dev @babel/preset-react

3 webpack 的依赖

npm install —save-dev webpack webpack-cli babel-loader

tsconfig.json

使用下面的命令生成 tsconfig.json

  1. tsc --init --declaration --allowSyntheticDefaultImports --target esnext --outDir lib

修改 tsconfig.json 的 jsx 选项,改为 react

webpack.config.js

  1. var path = require('path');
  2. module.exports = {
  3. // Change to your "entry-point".
  4. entry: './src/index',
  5. output: {
  6. path: path.resolve(__dirname, 'dist'),
  7. filename: 'app.bundle.js'
  8. },
  9. resolve: {
  10. extensions: ['.ts', '.tsx', '.js', '.json']
  11. },
  12. module: {
  13. rules: [{
  14. // Include ts, tsx, js, and jsx files.
  15. test: /\.(ts|js)x?$/,
  16. exclude: /node_modules/,
  17. loader: 'babel-loader',
  18. }],
  19. }
  20. };

更改 .babelrc

  1. {
  2. "presets": [
  3. "@babel/preset-env",
  4. "@babel/react",
  5. "@babel/preset-typescript"
  6. ],
  7. "plugins": [
  8. "@babel/plugin-proposal-class-properties"
  9. ]
  10. }

build 命令

ts 只做类型检查

  1. "scripts": {
  2. "type-check": "tsc --noEmit",
  3. "type-check:watch": "npm run type-check -- --watch",
  4. "build": "npm run build:types && npm run build:js",
  5. "build:types": "tsc --emitDeclarationOnly",
  6. "build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline"
  7. }

代码分离

需要引入一个新的插件

  1. @babel/plugin-syntax-dynamic-import
  1. {
  2. "presets": ["@babel/preset-react"],
  3. "plugins": ["@babel/plugin-syntax-dynamic-import"]
  4. }

类型检查

babel 做代码转换,typescirpt 做类型检查

不用 babel

缺点:

  1. 不支持代码分离,如果想要结合路由懒加载,需要修改 module 为 ESnext,遇到低版本的浏览器,就gg了
  2. babel 的一些好用的插件不能用
  3. 垫片无法按需加载、antd 无法按需引入 ```javascript “jsx”: “react”, / Specify JSX code generation: ‘preserve’, ‘react-native’, or ‘react’. /

{ test: /.(t|j)sx?$/, use: { loader: ‘ts-loader’ }, exclude: /node_modules/ },

  1. <a name="GmRqx"></a>
  2. # 后面的内容 - Hooks + TS 的一些实践经验
  3. <a name="l9f6F"></a>
  4. # 组件
  5. <a name="U42da"></a>
  6. ## fc 函数组件
  7. 有2中写法,一种是函数声明,另一种是函数扩展式,
  8. 要求是,保证统一的写法,不管是第一种也好,还是第二种。
  9. ```typescript
  10. import React from 'react'
  11. // 函数声明式写法
  12. function Heading(): React.ReactNode {
  13. return <h1>My Website Heading</h1>
  14. }
  15. // 函数扩展式写法
  16. const OtherHeading: React.FC = () => <h1>My Website Heading</h1>

无状态组件 stateless

https://www.yuque.com/cashgw/blm6hv/whqtf8

有状态组件 statefull

  1. 私用方法加private
  2. 用React.createRef创建一个ref
  1. import * as React from 'react'
  2. interface Props {
  3. handleSubmit: (value: string) => void
  4. }
  5. interface State {
  6. itemText: string
  7. }
  8. export class TodoInput extends React.Component<Props, State> {
  9. constructor(props: Props) {
  10. super(props)
  11. this.state = {
  12. itemText: ''
  13. }
  14. }
  15. }

props

可以使用 interface 或者 type 来定义 props 的类型

你的 props 建议加上 readonly,可以使用工具类 ReadOnly

  • 无论你为组件 Props 使用 type 还是 interfaces ,都应始终使用它们。
  • 始终使用 TSDoc 标记为你的 Props 添加描述性注释 /* comment /。
    • 这一条考虑一下
  1. import React from 'react'
  2. interface Props {
  3. readonly name: string;
  4. readonly color: string;
  5. }
  6. type Props2 = {
  7. /** color to use for the background */
  8. color?: string;
  9. /** standard children prop: accepts any valid React Node */
  10. children: React.ReactNode;
  11. /** callback function passed to the onClick handler*/
  12. onClick: () => void;
  13. }
  14. type OtherProps = {
  15. name: string;
  16. color: string;
  17. }
  18. // Notice here we're using the function declaration with the interface Props
  19. function Heading({ name, color }: Props): React.ReactNode {
  20. return <h1>My Website Heading</h1>
  21. }
  22. // Notice here we're using the function expression with the type OtherProps
  23. const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
  24. <h1>My Website Heading</h1>

事件

处理表单事件

  1. import React from 'react'
  2. const MyInput = () => {
  3. const [value, setValue] = React.useState('')
  4. // 事件类型是“ChangeEvent”
  5. // 我们将 “HTMLInputElement” 传递给 input
  6. function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  7. setValue(e.target.value)
  8. }
  9. return <input value={value} onChange={onChange} id="input-example"/>
  10. }

HOC

使用 type

  1. import React from 'react';
  2. type ButtonProps = {
  3. /** the background color of the button */
  4. color: string;
  5. /** the text to show inside the button */
  6. text: string;
  7. }
  8. type ContainerProps = ButtonProps & {
  9. /** the height of the container (value used with 'px') */
  10. height: number;
  11. }
  12. const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  13. return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
  14. }

使用 interface

  1. import React from 'react';
  2. interface ButtonProps {
  3. /** the background color of the button */
  4. color: string;
  5. /** the text to show inside the button */
  6. text: string;
  7. }
  8. interface ContainerProps extends ButtonProps {
  9. /** the height of the container (value used with 'px') */
  10. height: number;
  11. }
  12. const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  13. return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
  14. }

Hooks

Hooks 对 TS 的支持很好,一般来说没啥问题。

Hooks 最佳实践

空值

  1. type User = {
  2. email: string;
  3. id: string;
  4. }
  5. // the generic is the < >
  6. // the union is the User | null
  7. // together, TypeScript knows, "Ah, user can be User or null".
  8. const [user, setUser] = useState<User | null>(null);

useReducer

  1. type AppState = {};
  2. type Action =
  3. | { type: "SET_ONE"; payload: string }
  4. | { type: "SET_TWO"; payload: number };
  5. export function reducer(state: AppState, action: Action): AppState {
  6. switch (action.type) {
  7. case "SET_ONE":
  8. return {
  9. ...state,
  10. one: action.payload // `payload` is string
  11. };
  12. case "SET_TWO":
  13. return {
  14. ...state,
  15. two: action.payload // `payload` is number
  16. };
  17. default:
  18. return state;
  19. }
  20. }

利用高级类型解决默认属性报错

  1. import * as React from 'react'
  2. // 定义 state 接口
  3. interface State {
  4. itemText: string
  5. }
  6. // 定义一个类型
  7. // Partial 泛型类型
  8. // & 类型合并
  9. type Props = {
  10. handleSubmit: (value: string) => void
  11. children: React.ReactNode
  12. } & Partial<typeof todoInputDefaultProps>
  13. const todoInputDefaultProps = {
  14. inputSetting: {
  15. maxlength: 20,
  16. placeholder: '请输入todo',
  17. }
  18. }
  19. // 重点是这个函数
  20. export const createPropsGetter = <DP extends object>(defaultProps: DP) => {
  21. return <P extends Partial<DP>>(props: P) => {
  22. type PropsExcludingDefaults = Omit<P, keyof DP>
  23. type RecomposedProps = DP & PropsExcludingDefaults
  24. return (props as any) as RecomposedProps
  25. }
  26. }
  27. const getProps = createPropsGetter(todoInputDefaultProps)
  28. export class TodoInput extends React.Component<Props, State> {
  29. public static defaultProps = todoInputDefaultProps
  30. constructor(props: Props) {
  31. super(props)
  32. this.state = {
  33. itemText: ''
  34. }
  35. }
  36. public render() {
  37. const { itemText } = this.state
  38. const { updateValue, handleSubmit } = this
  39. const { inputSetting } = getProps(this.props)
  40. return (
  41. <form onSubmit={handleSubmit} >
  42. <input maxLength={inputSetting.maxlength} type='text' value={itemText} onChange={updateValue} />
  43. <button type='submit' >添加todo</button>
  44. </form>
  45. )
  46. }
  47. private updateValue(e: React.ChangeEvent<HTMLInputElement>) {
  48. this.setState({ itemText: e.target.value })
  49. }
  50. private handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  51. e.preventDefault()
  52. if (!this.state.itemText.trim()) {
  53. return
  54. }
  55. this.props.handleSubmit(this.state.itemText)
  56. this.setState({itemText: ''})
  57. }
  58. }

使用 Redux

定义 state 的形状

在 types 文件夹下新建一个文件

  1. // src/types/index.tsx
  2. export interface StoreState {
  3. languageName: string;
  4. enthusiasmLevel: number;
  5. }

actions

使用 constants 管理你的 reducer type

  1. // src/constants/index.tsx
  2. export const INCREMENT_ENTHUSIASM = 'INCREMENT_ENTHUSIASM';
  3. export type INCREMENT_ENTHUSIASM = typeof INCREMENT_ENTHUSIASM;
  4. export const DECREMENT_ENTHUSIASM = 'DECREMENT_ENTHUSIASM';
  5. export type DECREMENT_ENTHUSIASM = typeof DECREMENT_ENTHUSIASM;
  6. // src/actions/index.tsx
  7. import * as constants from '../constants';
  8. export interface IncrementEnthusiasm {
  9. type: constants.INCREMENT_ENTHUSIASM;
  10. }
  11. export interface DecrementEnthusiasm {
  12. type: constants.DECREMENT_ENTHUSIASM;
  13. }
  14. export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;
  15. export function incrementEnthusiasm(): IncrementEnthusiasm {
  16. return {
  17. type: constants.INCREMENT_ENTHUSIASM
  18. }
  19. }
  20. export function decrementEnthusiasm(): DecrementEnthusiasm {
  21. return {
  22. type: constants.DECREMENT_ENTHUSIASM
  23. }
  24. }

reducers

immutable

  1. // src/reducers/index.tsx
  2. import { EnthusiasmAction } from '../actions';
  3. import { StoreState } from '../types/index';
  4. import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index';
  5. export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState {
  6. switch (action.type) {
  7. case INCREMENT_ENTHUSIASM:
  8. return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
  9. case DECREMENT_ENTHUSIASM:
  10. return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1) };
  11. }
  12. return state;
  13. }

废弃 - connect

现在可以使用 useSelector 和 useDiaptch, react-redux 提供的 hooks API。

  1. // src/containers/Hello.tsx
  2. import Hello from '../components/Hello';
  3. import * as actions from '../actions/';
  4. import { StoreState } from '../types/index';
  5. import { connect, Dispatch } from 'react-redux';
  6. export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
  7. return {
  8. enthusiasmLevel,
  9. name: languageName,
  10. }
  11. }
  12. export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
  13. return {
  14. onIncrement: () => dispatch(actions.incrementEnthusiasm()),
  15. onDecrement: () => dispatch(actions.decrementEnthusiasm()),
  16. }
  17. }
  18. export default connect(mapStateToProps, mapDispatchToProps)(Hello);

创建 store

  1. import { createStore } from 'redux';
  2. import { enthusiasm } from './reducers/index';
  3. import { StoreState } from './types/index';
  4. const store = createStore<StoreState>(enthusiasm, {
  5. enthusiasmLevel: 1,
  6. languageName: 'TypeScript',
  7. });

导入非代码资源

需要定义一个声明文件

  1. declare module "*.svg" {
  2. const content: any;
  3. export default content;
  4. }

项目地址

https://github.com/bhaltair/ts-react-webpack-starter

参考

三千字讲清TypeScript与React的实战技巧