webpack-react-ts 构建前端项目

1,329 阅读12分钟

学习参考 视频1

1搭建开发环境

  • 项目初始化
mkdir react-ts

cd react-ts

npm init -y
  • 安装相关依赖
yarn add typescript webpack webpack-cli webpack-dev-server ts-loader cross-env webpack-merge clean-webpack-plugin html-webpack-plugin -D

2-生成ts配置文件

此时,可以使用 tsc 命令,生成 tsconfig 文件

yarn add typescript -g

tsc --init

文件如下:

{
  "compilerOptions": {
    "target": "es5", /** 编译后的版本 */
    "module": "commonjs", /** 编译后模块的写法 */
    "jsx": "react",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  },
}

配置文件含义参考:

3-配置 webpack

  • 安装依赖
yarn add typescript webpack webpack-cli webpack-dev-server ts-loader cross-env webpack-merge clean-webpack-plugin html-webpack-plugin -D

yarn add react @types/react react-dom @types/react-dom -D

yarn add redux react-redux @types/react-redux redux-logger redux-promise redux-thunk @types/redux-logger @types/redux-promise -D

yarn add react-router-dom @types/react-router-dom connected-react-router antd -D

yarn add eslint @typescript-eslit/eslint-plugin @typescript-eslit/parser -D

yarn add @types/jest ts-jest -D
  • 编写 /config/webpack.base.config.js
/*
 * @Description: 
 * @Date: 2020-12-11 15:34:09
 * @Author: Jsmond2016 <jsmond2016@gmail.com>
 * @Copyright: Copyright (c) 2020, Jsmond2016
 */

// 清理产出目录的插件
 const { CleanWebpackPlugin } = require('clean-webpack-plugin')

//  产出 html 的插件
const HtmlWebpackPlugin = require('html-webpack-plugin')

const path = require('path')

module.exports = {
  entry: './src/index.tsx',
  output: {
    // 输出目录
    path: path.resolve(__dirname, '../dist'),
    filename: 'main.js'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx']
  },
  devServer: {
    contentBase: '../dist'
  },
  module: {
    rules: [
      {
        test: /\.(j|t)sx?/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: ['./dist']
    }),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}
  • 编写 webpack.dev.config.js
const { smart } = require('webpack-merge')
const base = require('./webpack.base.config')

module.exports = smart(base, {
  mode: 'development',
  devtool: 'inline-soruce-map'
})
  • 编写 webpack.prod.config.js
const { smart } = require('webpack-merge')
const base = require('./webpack.base.config')

module.exports = smart(base, {
  mode: 'production',
})
  • 新建 src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>react-ts</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
  • 新建 src/index.tsx
console.log('hello')
  • 配置 package.json 中的 dev, build 命令
{
  "name": "react-typeScript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack-dev-server --config ./config/webpack.dev.config.js",
    "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.config.js",
    "eslint": "eslint src --ext .js,.ts,.tsx",
    "test": "jest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/jest": "^24.9.1",
    "@types/react-redux": "^7.1.7",
    "@types/react-router-dom": "^5.1.3",
    "@types/redux-logger": "^3.0.7",
    "@types/redux-promise": "^0.5.28",
    "clean-webpack-plugin": "^3.0.0",
    "cross-env": "^7.0.0",
    "css-loader": "^3.4.2",
    "html-webpack-plugin": "^3.2.0",
    "jest": "^25.1.0",
    "react-redux": "^7.1.3",
    "redux": "^4.0.5",
    "redux-logger": "^3.0.6",
    "redux-promise": "^0.6.0",
    "redux-thunk": "^2.3.0",
    "style-loader": "^1.1.3",
    "ts-jest": "^25.0.0",
    "ts-loader": "^6.2.1",
    "typescript": "^3.7.5",
    "webpack": "^4.41.5",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1",
    "webpack-merge": "^4.2.2"
  },
  "dependencies": {
    "@types/react": "^16.9.19",
    "@types/react-dom": "^16.9.5",
    "antd": "^3.26.7",
    "connected-react-router": "^6.6.1",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-router-dom": "^5.1.2"
  }
}
  • 启动验证
yarn dev
# http://localhost:8080/

yarn build
  • 可能报错
    • 错误1:smart is not a function
    • 错误2:webpack-cli/bin/config-yargs...
  • 解决办法:使用上面的 package.json 文件,锁定依赖版本

4-配置 eslint

  • 配置 .eslintrc.json 文件
{
    "parser": "@typescript-eslint/parser",
    "plugins": [
        "@typescript-eslint/eslint-plugin"
    ],
    "extends": [
        /** 使用推荐配置 */
        "plugin:@typescript-eslint/recommended"
    ],
    "rules": {
        /** 配置规则 */
        "@typescript-eslint/no-unused-vars": "off",
        "@typescript-eslint/no-var-requires": "off"
    }
}
  • 配置 package.json ,新增 eslint 命令

这里可以安装 vs code 的 eslint 插件

5-单元测试

  • 安装 jest 测试工具
yarn add @types/jest ts-jest -D
  • 新建 jest.config.js 配置
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node'
}
  • 编写测试文件
// src/calc.tsx

function sum (a: number, b: number) {
  return a + b
}
function minus (a: number, b: number) {
  return a - b
}

module.exports = {
  sum,
  minus
}

// src/calc.test.jsx
let calc = require('./calc')

describe('测试calc', () => {
  test('1+1', () => {
    expect(calc.sum(1,1)).toBe(2)
  })
  
  test('111', () => {
    expect(calc.minus(1,1)).toBe(0)
  })
})
  • 配置 package.json 中 测试命令
"scripts": {
    /** ... */
    "test": "jest"
  },
  • 运行测试命令: npm run test

6-支持 React

  • 安装 react
yarn add react @types/react react-dom @types/react-dom -D
  • 编写 src/index.tsx
import React from 'react';
import ReactDom from 'react-dom'

const Index = () => {
  return (
    <div>hello, world</div>
  )
}

ReactDom.render(<Index />, document.getElementById("root"))
  • 这里可能会标红语法问题,需要配置 tsconfig.json
{
  "compilerOptions": {
      /** ... 新加这个 */
    "jsx": "preserve", /** 'preserve' | 'react-native' | 'react' */
     /** 'preserve' 表示保留 jsx 语法 和 tsx 后缀  */
     /** 'react-native' 表示 保留 jsx 语法但会把后缀改为 js  */
     /**  'react' 表示不保留 jsx 语法,直接编译成 es5  */
  }
}

  • 启动测试:
yarn dev

// http://localhost:8080/

可以看到预览效果

  • 其他 ts 相关:
    • Element 是指原生 DOM 对象元素,不是 React 里的东西,而是 DOM 里面的类型
// React.tsx
    // DOM Elements
    // ReactHTMLElement
function cloneElement<P extends HTMLAttributes<T>, T extends HTMLElement>(
element: DetailedReactHTMLElement<P, T>,
 props?: P,
 ...children: ReactNode[]): DetailedReactHTMLElement<P, T>;
  • 关系图如下:

继承关系

7-定义 函数组件和类组件

import React from 'react';
import ReactDom from 'react-dom'

interface Props {
  className: string
}
interface State {
  id: string
}

const props: Props = {
  className: 'title'
}

const Index = (props: Props) => {
  const { className } = props
  return (
    <div className={className}>hello, world</div>
  )
}

class Hello extends React.Component<Props, State> {
  state = {
    id: '11'
  }
  render() {
    return React.createElement<Props, HTMLHeadingElement>('h1', props, 'hello')
  }
}

ReactDom.render(<Index {...props} />, document.getElementById("root"))

9-使用 redux

  • 安装依赖:
yarn add redux react-redux @types/react-redux redux-logger redux-promise redux-thunk @types/redux-logger @types/redux-promise -D
  • 创建文件 /src/store/index.tsx
import { createStore, applyMiddleware, StoreEnhancer, StoreEnhancerStoreCreator, Store } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducers'

let storeEnhancer: StoreEnhancer = applyMiddleware(thunk)
let storeEnhancerStoreCreator: StoreEnhancerStoreCreator = storeEnhancer(createStore)
let store: Store = storeEnhancerStoreCreator(reducer)

export default store
  • 创建 /src/store/acton-types.tsx
export const ADD1 = 'ADD1'
export const ADD2 = 'ADD2'
  • 创建 /src/store/reducers/counter1.tsx
import * as types from '../action-types'
import { AnyAction } from 'redux'

export interface Counter1State {
  number: number
}

let initialState: Counter1State = {
  number: 0
}

export default function (state: Counter1State = initialState, action: AnyAction): Counter1State {
  switch (action.type) {
    case types.ADD1:
      return { number: state.number + 1 }
    case types.ADD2:
      return { number: state.number + 2 }
    default: 
      return state
  }
}
  • 创建 /src/store/reducers/counter2.tsx
import * as types from '../action-types'
import { AnyAction } from 'redux'

export interface Counter2State {
  number: number
}

let initialState: Counter2State = {
  number: 0
}

export default function (state: Counter2State = initialState, action: AnyAction): Counter2State {
  switch (action.type) {
    case types.ADD1:
      return { number: state.number + 1 }
    case types.ADD2:
      return { number: state.number + 2 }
    default: 
      return state
  }
}
  • 创建 /src/store/reducers/index.tsx
import { combineReducers, ReducersMapObject, Reducer, AnyAction } from 'redux';
import counter1, { Counter1State } from './counter1';
import counter2, { Counter2State } from './counter2';

export interface CombinedState {
  counter1: Counter1State
  counter2: Counter2State
}


let reducers: ReducersMapObject<CombinedState, AnyAction> = {
  counter1,
  counter2
}

// export type CombineState = {
//  [key in keyof typeof reducers]:  ReturnType<typeof reducers[key]>
// }


let reducer: Reducer<CombinedState, AnyAction>  = combineReducers(reducers)
export default reducer
  • 使用 redux
  • 新建 /src/components/Counter1.tsx
import React from 'react'

class Counter1 extends React.Component {

  render () {
    return <>Counter1</>
  }
}

export default Counter1
  • 新建 /src/components/Counter2.tsx
import React from 'react'

class Counter2 extends React.Component {
  
  render () {
    return <>Counter2</>
  }
}

export default Counter2
  • 修改 /src/index.tsx
import React from 'react';
import ReactDom from 'react-dom'
import Counter1 from './components/Counter1'
import Counter2 from './components/Counter2'
import { Provider } from 'react-redux'
import store from './store'


ReactDom.render(
  <Provider store={store}>
    <Counter1 />
    <Counter2 />
  </Provider>
  , document.getElementById("root"))
  • 启动项目验证
yarn dev

// http://localhost:8080

可以看到 Counter1Counter2 正确显示

接下来,开始连接 Redux

  • 修改 /src/components/Counter1.tsx
import React from 'react'
import { Dispatch } from 'redux'
import { connect } from 'react-redux';
import { CombinedState } from '../store/reducers/index';
import { Counter1State } from '../store/reducers/counter1';
import * as types from '../store/action-types';

let mapStateToProps = (state: CombinedState): Counter1State => state.counter1 
let mapDispatchToProps = (dispatch: Dispatch) => ({
  add1(amount: number) {dispatch({type: types.ADD1, payload: amount })},
  add2() {dispatch({type: types.ADD2})}
})

type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>

class Counter1 extends React.Component<Props> {

  render () {
    return (
      <div>
        <p>{this.props.number}</p>
        <button onClick={() => this.props.add1(5)}>+5</button>
        <button onClick={() => this.props.add2()}>+2</button>
      </div>
    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter1)
  • 修改 /src/components/Counter2.tsx
import React from 'react'
import { Dispatch } from 'redux'
import { connect } from 'react-redux';
import { CombinedState } from '../store/reducers/index';
import { Counter2State } from '../store/reducers/counter2';
import * as types from '../store/action-types';

let mapStateToProps = (state: CombinedState): Counter2State => state.counter2
let mapDispatchToProps = (dispatch: Dispatch) => ({
  add3() {dispatch({type: types.ADD3 })},
  add4() {dispatch({type: types.ADD4 })},
  
})

type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>

class Counter2 extends React.Component<Props> {

  render () {
    return (
      <div>
        <p>{this.props.number}</p>
        <button onClick={() => this.props.add3()}>+1</button>
        <button onClick={() => this.props.add4()}>+10</button>
      </div>
    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter2)
  • 修改 src/store/action-types.tsx
export const ADD1 = 'ADD1'
export const ADD2 = 'ADD2'
export const ADD3 = 'ADD3'
export const ADD4 = 'ADD4'
  • 修改 src/store/reducers/counter1.tsxtypes.ADD1, types.ADD2
import * as types from '../action-types'
import { AnyAction } from 'redux'

export interface Counter1State {
  number: number
}

let initialState: Counter1State = {
  number: 0
}

export default function (state: Counter1State = initialState, action: AnyAction): Counter1State {
  switch (action.type) {
    case types.ADD1:
      // 每次点击新增传入的参数
      return { number: state.number + (action.payload || 1) }
    case types.ADD2:
      return { number: state.number + 2 }
    default: 
      return state
  }
}
  • 修改 src/store/reducers/counter2.tsxtypes.ADD3, types.ADD4
import * as types from '../action-types'
import { AnyAction } from 'redux'

export interface Counter2State {
  number: number
}

let initialState: Counter2State = {
  number: 0
}

export default function (state: Counter2State = initialState, action: AnyAction): Counter2State {
  switch (action.type) {
    case types.ADD3:
      return { number: state.number + 1 }
    case types.ADD4:
      return { number: state.number + 10 }
    default: 
      return state
  }
}
  • 启动验证
yarn dev

// http://localhost:8080/

10-支持路由

  • 安装路由相关依赖
yarn add react-router-dom @types/react-router-dom connected-react-router antd -D
  • 修改 src/index.tsx
import React from "react";
import ReactDom from "react-dom";
import Counter1 from "./components/Counter1";
import Counter2 from "./components/Counter2";
import { Provider } from "react-redux";
import store from "./store";
import { Route, Link, Redirect, Switch } from "react-router-dom";
import { ConnectedRouter } from "connected-react-router";
import history from "./history";

ReactDom.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <ul>
        <li>
          <Link to="counter1">counter1</Link>
        </li>
        <li>
          <Link to="counter2">counter2</Link>
        </li>
      </ul>
      <Switch>
        <Route path="/counter1" component={Counter1} />
        <Route path="/counter2" component={Counter2} />
        <Redirect to="counter1" />
      </Switch>
    </ConnectedRouter>
  </Provider>,
  document.getElementById("root")
);

  • 新增 src/history.tsx 文件
import { createHashHistory } from 'history'

const history = createHashHistory()

export default history
  • 修改 /src/store/index.tsx 文件
import {
  createStore,
  applyMiddleware,
  StoreEnhancer,
  StoreEnhancerStoreCreator,
  Store,
} from "redux";
import thunk from "redux-thunk";
import reducer from "./reducers";
import { routerMiddleware } from 'connected-react-router'
import history from '../history';

// 在中间件中使用 routerMiddleware(history)
const storeEnhancer: StoreEnhancer = applyMiddleware(thunk, routerMiddleware(history));
const storeEnhancerStoreCreator: StoreEnhancerStoreCreator = storeEnhancer(
  createStore
);
const store: Store = storeEnhancerStoreCreator(reducer);

export default store;

  • 修改 /src/store/reducers/index.tsx
import { combineReducers, ReducersMapObject, Reducer, AnyAction } from "redux";
import counter1, { Counter1State } from "./counter1";
import counter2, { Counter2State } from "./counter2";
// 引入 下面的文件
import { connectRouter, RouterState } from "connected-react-router";
import history from "../../history";

export interface CombinedState {
  counter1: Counter1State;
  counter2: Counter2State;
   // 新增 router 在 store 的 state 中
  router: RouterState;
}

// 这里因为 RouterState 的类型和 AnyAction 不一致没有交集,使用 any
const reducers: ReducersMapObject<CombinedState, any> = {
  counter1,
  counter2,
  router: connectRouter(history),
};

// export type CombineState = {
//  [key in keyof typeof reducers]:  ReturnType<typeof reducers[key]>
// }

const reducer: Reducer<CombinedState, AnyAction> = combineReducers(reducers);
export default reducer;

  • 页面使用 dispatch 的方式跳转路由
import React from 'react'
import { Dispatch } from 'redux'
import { connect } from 'react-redux';
import { CombinedState } from '../store/reducers/index';
import { Counter1State } from '../store/reducers/counter1';
import * as types from '../store/action-types';
// 引入依赖
import { LocationDescriptorObject, LocationState } from 'history'
import { push } from 'connected-react-router'

const mapStateToProps = (state: CombinedState): Counter1State => state.counter1 
const mapDispatchToProps = (dispatch: Dispatch) => ({
  add1(amount: number) {dispatch({type: types.ADD1, payload: amount })},
  add2() {dispatch({type: types.ADD2})},
  // 新增 跳转路由方法 
  goTo(location: LocationDescriptorObject<LocationState>) {
    dispatch(push(location))
  }
})

type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>

class Counter1 extends React.Component<Props> {

  render () {
    return (
      <div>
        <p>{this.props.number}</p>
        <button onClick={() => this.props.add1(5)}>+5</button>
        <br/>
        <button onClick={() => this.props.add2()}>+2</button>
        <br/>
         {/** 新增 跳转路由方法  */}
        <button onClick={() => this.props.goTo({pathname: '/counter2'})}>跳转页面</button>
      </div>
    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter1)

参考资料:

11-使用AntDesign

  • 安装依赖:
yarn add antd @types/antd -D

yarn add style-loader css-loader @types/react-router-dom -D

yarn add axios -D
  • 修改 src/index.tsx
import React from "react";
import ReactDom from "react-dom";
import Counter1 from "./components/Counter1";
import Counter2 from "./components/Counter2";
import { Provider } from "react-redux";
import store from "./store";
import { Route, Link, Redirect, Switch } from "react-router-dom";
import { ConnectedRouter } from "connected-react-router";
import history from "./history";
import "antd/dist/antd.css";
// 使用 antd
import { Layout } from "antd";
import NavBar from "./components/NavBar";
import User from "./components/User";
const { Content } = Layout;

ReactDom.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Layout>
        <NavBar />
        <Content style={{ padding: "20px" }}>
          <Switch>
            <Route path="/counter1" component={Counter1} />
            <Route path="/counter2" component={Counter2} />
            <Route path="/user" component={User} />
            <Redirect to="counter1" />
          </Switch>
        </Content>
      </Layout>
    </ConnectedRouter>
  </Provider>,
  document.getElementById("root")
);

  • 新增 src/components/NavBar.tsx
import React from 'react';
import { Link, RouteComponentProps, withRouter } from 'react-router-dom'
import { Layout, Menu } from 'antd'

type Props = RouteComponentProps


class NavBar extends React.Component<Props> {
  render() {
    return (
      <Layout.Header>
        <Menu
          theme="dark"
          style={{lineHeight: '64px'}}
          mode="horizontal"
          selectedKeys={[this.props.location.pathname]}
        >
          <Menu.Item>
            <Link to="/counter1">counter1</Link>
          </Menu.Item>
          <Menu.Item>
            <Link to="/counter2">counter2</Link>
          </Menu.Item>
          <Menu.Item>
            <Link to="/user">user</Link>
          </Menu.Item>
        </Menu>
      </Layout.Header>
    )
  }
}

export default withRouter(NavBar)
  • 以下文件因为是 逐步增加的 需求代码,这里只放最后的代码
  • 新增 src/components/User.tsx 用户模块
import React from "react";
import { Link, RouteComponentProps, withRouter, Route } from "react-router-dom";
import { Layout, Menu } from "antd";
import UserAdd from './UserAdd'
import UserList from './UserList'
import UserDetail from './UserDetail'

type Props = RouteComponentProps;

const { Sider, Content } = Layout;

class User extends React.Component<Props> {
  render() {
    return (
      <Layout>
        <Sider>
          <Menu
            theme="dark"
            mode="inline"
            selectedKeys={[this.props.location.pathname]}
          >
            <Menu.Item>
              <Link to="/user/add">添加用户</Link>
            </Menu.Item>
            <Menu.Item>
              <Link to="/user/list">用户列表</Link>
            </Menu.Item>
          </Menu>
        </Sider>
        <Content style={{padding: '20px'}}>
          <Route path="/user/add" component={UserAdd} />
          <Route path="/user/list" component={UserList} />
          <Route path="/user/detail/:id" component={UserDetail} />
        </Content>
      </Layout>
    );
  }
}

export default withRouter(User);
  • 新增 src/components/UserAdd.tsx 新增用户模块
import React, { useState, useEffect } from 'react'
import { message, Form, Button, Layout, Input, Menu } from 'antd'
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
import { User, UserAddResponse } from '../typings/api'
import http, { AxiosResponse } from '../api/request'

type Props = RouteComponentProps

const UserAdd = (props: Props) => {
  const [user, setUser] = useState<User>({} as User)
  
  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault()
    http.post<UserAddResponse>('/user', user).then((res: AxiosResponse) => {
       const { data, code } = res.data
       if (code === 0) {
         props.history.push('/user/list')
       }else {
         message.error('添加失败')
       }
    })
  }

  const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setUser({
      ...user,
      name: event.target.value
    })
  }

  return (
    <Form>
      <Form.Item>
        <Input
          placeholder="用户名"
          style={{width: 120}}
          value={user.name}
          onChange={handleNameChange}
        />
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit" onClick={handleSubmit}>添加</Button>
      </Form.Item>
    </Form>
  )
}

export default UserAdd
  • 新增 src/components/UserList.tsx 用户列表模块
import React, { useState, useEffect } from 'react'
import { message, Table } from 'antd'
import { ColumnProps } from 'antd/lib/table'
import { Link } from 'react-router-dom'
import { User, UserListResponse } from '../typings/api'
import httpInstance, { AxiosResponse } from '../api/request'

const columns: ColumnProps<User>[] = [
  {
    title: '用户名',
    dataIndex: 'name',
    key: 'name'
  },
  {
    title: '跳转详情页',
    dataIndex: 'jump',
    key: 'jump',
    render: (val, record) => (<Link to={`/user/detail/${record._id}`} >跳转</Link>)
  }
]

const UserList = () => {
  const [users, setUsers] = useState<User[]>([])
  
  useEffect(() => {
    (async function () {
     const res: AxiosResponse<UserListResponse> =  await httpInstance.get<UserListResponse, AxiosResponse<UserListResponse>>('/users')
     const { data, code } = res.data
     if (code === 0) {
       setUsers(data)
     } else {
       message.error('获取用户列表失败')
     }
    })()
  }, [])


  return (
    <Table columns={columns} dataSource={users} rowKey={row => row._id} />
  )
}

export default UserList
  • 新建 src/typings/api.ts 定义接口类型
/*
 * @Description: 
 * @Date: 2020-12-12 12:11:11
 * @Author: Jsmond2016 <jsmond2016@gmail.com>
 * @Copyright: Copyright (c) 2020, Jsmond2016
 */

 export interface User {
   _id: string
   name: string
 }

 export interface UserListResponse {
   code: number
   data: User[]
 }


 export interface UserAddResponse {
   code: number
   data: User
 }
  • 新建 src/api/request.ts 定义请求方法
/*
 * @Description: 
 * @Date: 2020-12-12 13:51:31
 * @Author: Jsmond2016 <jsmond2016@gmail.com>
 * @Copyright: Copyright (c) 2020, Jsmond2016
 */

 import axios from 'axios'

 const httpInstance = axios.create({
   timeout: 2000,
   baseURL: '/api/'
 })

 export * from 'axios'
 export default httpInstance
  • 连接 store
  • 修改 src/store/action-types.tsx
export const ADD1 = 'ADD1'
export const ADD2 = 'ADD2'
export const ADD3 = 'ADD3'
export const ADD4 = 'ADD4'


export const SET_USER_LIST = 'SET_USER_LIST'
  • 新建 src/store/reducers/user.tsx
import * as types from "../action-types";
import { AnyAction } from "redux";
import { User } from '../../typings/api'


export interface UserState {
  list: User[]
}

const initialState: UserState = {
  list: [],
};

export default function (
  state: UserState = initialState,
  action: AnyAction
): UserState {
  switch (action.type) {
    case types.SET_USER_LIST:
      return { list: action.payload };
    default:
      return state;
  }
}

  • 修改 src/store/reducers/index.tsx
import { combineReducers, ReducersMapObject, Reducer, AnyAction } from "redux";
import counter1, { Counter1State } from "./counter1";
import counter2, { Counter2State } from "./counter2";
// 新增 user
import user, { UserState } from './user'
import { connectRouter, RouterState } from "connected-react-router";
import history from "../../history";
export interface CombinedState {
  counter1: Counter1State;
  counter2: Counter2State;
  // 新增 user
  user: UserState;
  router: RouterState;
}

const reducers: ReducersMapObject<CombinedState, any> = {
  counter1,
  counter2,
  // 新增 user
  user,
  router: connectRouter(history),
};

// export type CombineState = {
//  [key in keyof typeof reducers]:  ReturnType<typeof reducers[key]>
// }

const reducer: Reducer<CombinedState, AnyAction> = combineReducers(reducers);
export default reducer;

  • 修改 src/components/UserList.tsx
import React, { useState, useEffect } from 'react'
import { message, Table } from 'antd'
import { ColumnProps } from 'antd/lib/table'
import { Link } from 'react-router-dom'
import { User, UserListResponse } from '../typings/api'
import httpInstance, { AxiosResponse } from '../api/request'
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { CombinedState } from '../store/reducers/index';
import { UserState } from '../store/reducers/user';
import * as types from '../store/action-types';


const mapStateToProps = (state: CombinedState): UserState => state.user
const mapDispatchToProps = (dispatch: Dispatch) => ({
  setUserList(list: User[]) {
    dispatch({ type: types.SET_USER_LIST, payload: list })
  }
})

const columns: ColumnProps<User>[] = [
  {
    title: '用户名',
    dataIndex: 'name',
    key: 'name'
  },
  {
    title: '跳转详情页',
    dataIndex: 'jump',
    key: 'jump',
    render: (val, record) => (<Link to={`/user/detail/${record._id}`} >跳转</Link>)
  }
]


type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>

const UserList = (props: Props ) => {

  // const [users, setUsers] = useState<User[]>([])
  const users = props.list
  useEffect(() => {
    (async function () {
      const res: AxiosResponse<UserListResponse> =  await httpInstance.get<UserListResponse, AxiosResponse<UserListResponse>>('/users')
      const { data, code } = res.data
      if (code === 0) {
        // setUsers(data)
        props.setUserList(data)
      } else {
        message.error('获取用户列表失败')
      }
     })()
  }, [])


  return (
    <Table columns={columns} dataSource={users} rowKey={row => row._id} />
  )
}

export default connect(mapStateToProps, mapDispatchToProps)(UserList)

12-后台接口

mkdir server
cd server
cnpm init -y
cnpm i @types/node express @types/express body-parser cors @types/cors mongoose @types/mongoogse shelljs -S
  • tsconfig.json
{
  "compilerOptions": {
    "target": "es5", 
    "module": "commonjs", 
    "lib": [
      "ES2015",
       "DOM"
    ],
    "outDir": "./dist",
    "strict": true, 
     "baseUrl": "./",
     "paths": {
       "paths": {
         "*": [
         "node_modules/*",
         "typings/*"
       ]
     },
    "esModuleInterop": true,
  }
}
  • servet.ts
/*
 * @Description: 
 * @Date: 2020-12-12 14:11:23
 * @Author: Jsmond2016 <jsmond2016@gmail.com>
 * @Copyright: Copyright (c) 2020, Jsmond2016
 */

 import express, {Express, Request, Response } from 'express'
 import bodyParser from 'body-parser'
 import cors from 'cors'
 import Models from './db'
 import config from './config'
 import path from 'path'

 const app: Express = express()

 app.use(cors({
   origin: config.origin,
   credentials: true,
   allowedHeaders: "Content-Type, Authorization",
   methods: "GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS"
 }))
 app.use(express.static(path.resolve(__dirname, 'public')))
 app.use(bodyParser.urlencoded({extended: false }))
 app.use(bodyParser.json())

 app.get('/api/users', async (req: Request, res: Response) => {
   const user = await Models.UserModel.find()
   res.json({
     code: 0,
     data: user
   })
 })

 app.post('/api/user', async (req: Request, res: Response) => {
   let user = req.body
   user = await Models.UserModel.create(user)
   res.json({
     code: 0,
     data: user
   })
 })

 app.listen(4000, () => {
   console.log('服务器在 http://localhost:4000 端口启动')
 })
  • 编写 src/db.ts
/*
 * @Description: 
 * @Date: 2020-12-12 14:46:27
 * @Author: Jsmond2016 <jsmond2016@gmail.com>
 * @Copyright: Copyright (c) 2020, Jsmond2016
 */

 import mongoose, { Schema, Connection, Model} from 'mongoose'
 import config from './config';
 

 const conn: Connection = mongoose.createConnection(config.dbUrl, {
   useNewUrlParser: true,
   useUnifiedTopology: true
 })
 const UserModel = conn.model("User", new Schema({
   usename: {
     type: String
   }
 }))

 export default { UserModel }
  • 编写 src/config.ts
/*
 * @Description: 
 * @Date: 2020-12-12 14:20:51
 * @Author: Jsmond2016 <jsmond2016@gmail.com>
 * @Copyright: Copyright (c) 2020, Jsmond2016
 */

 interface IConfig {
   secret: string
   dbUrl: string
   origin: string []
 }

 const config: IConfig = {
   secret: 'webpack-react-ts-test',
   dbUrl: 'mongodb://localhost:27017/webpack-ts',
   origin: ['http://localhost:8080']
 }

 export default config
  • 编写 src/copy.ts
/*
 * @Description: 
 * @Date: 2020-12-12 14:58:55
 * @Author: Jsmond2016 <jsmond2016@gmail.com>
 * @Copyright: Copyright (c) 2020, Jsmond2016
 */

 import shelljs  from 'shelljs'

 shelljs.cp("-R", "./public/", "./dist/")
  • 编写 src/typings/shelljs/index.d.ts
/*
 * @Description: 
 * @Date: 2020-12-12 14:58:27
 * @Author: Jsmond2016 <jsmond2016@gmail.com>
 * @Copyright: Copyright (c) 2020, Jsmond2016
 */

 declare module 'shelljs'
  • 修改 package.json 文件
{
  "name": "server-webpack-react-ts",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "ts-node ./src/server.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/mongoose": "^5.10.2",
    "mongoose": "^5.11.6"
  },
  "devDependencies": {
    "@types/cors": "^2.8.8",
    "@types/express": "^4.17.9",
    "@types/node": "^14.14.12",
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "shelljs": "^0.8.4"
  }
}

  • 打开 Robo 3T 数据库预览工具,开启连接
  • 创建数据库 webpack-ts
  • 创建表 users
  • 新增 log 进行测试
// src/server.ts
app.get('/api/users', async (req: Request, res: Response) => {
   const user = await Models.UserModel.find()
   console.log('GET /api/users: ', user)
   res.json({
     code: 0,
     data: user
   })
 })

 app.post('/api/user', async (req: Request, res: Response) => {
   let user = req.body
   console.log('POST /api/user: ', JSON.stringify(user))
   user = await Models.UserModel.create(user)
   res.json({
     code: 0,
     data: user
   })
 })
  • 使用 postman 测试接口,查看 log 信息
get localhost:4000/api/users

post localhost:4000/api/user
  • 修改 前端 请求配置
// src/api/request.ts 
import axios from 'axios'

 const httpInstance = axios.create({
   timeout: 2000,
   // 如果你这里的代码和我的不一致,参考修改
   baseURL: '/api/'
 })

 export * from 'axios'
 export default httpInstance

// 其他文件所有请求都只请求后面部分,如 '/users'
//const res: AxiosResponse<UserListResponse> =  await httpInstance.get<UserListResponse, AxiosResponse<UserListResponse>>('/users')

  • 前端设置代理 webpack.base.config.js
devServer: {
    contentBase: '../dist',
    proxy: [  
      // webpack 关于跨域的配置,参考资料 https://www.cnblogs.com/zwhbk/p/13364931.html    
    // 例如将'localhost: 8080/api/xxx'代理到'http:www.baidu.com/api/xxx
      {
          context: ['/api'],
          target: 'http://localhost:4000/', //接口域名
          changeOrigin: true, //如果是https需要配置该参数
          secure: false, //如果接口跨域需要进行该配置
      },
    ]
  },
  • 前端项目启动,测试数据是否成功
yarn dev

// localhost:8080

13-拓展知识:异步 Dispatch

因为 Redux 自带的 Dispatch 没有异步 Dispatch ,因此需要自己定义

  • 看代码:
import { Middleware, Action, AnyAction } from 'redux';
type MiddlewareExt = Middleware & {
    withExtraArgument: typeof createThunkMiddleware
}
export type ThunkAction<R, S, E, A extends Action> = (
    dispatch: ThunkDispatch<S, E, A>,
    getState: () => S,
    extraArgument: E
) => R;
// 特点:异步 dispatch 可以接受一个 异步函数
export interface ThunkDispatch<S, E, A extends Action> {
    <T extends A>(action: T): T;
    <R>(asyncAction: ThunkAction<R, S, E, A>): R;
}
function createThunkMiddleware<S = Record<string, unknown>, A extends Action = AnyAction, E = undefined>(extraArgument?: any): Middleware {
    const middleware: Middleware<ThunkDispatch<S, E, A>, S, ThunkDispatch<S, E, A>> = ({ dispatch, getState }) => next => action => {
        if (typeof action === 'function') {
            return action(dispatch, getState, extraArgument);
        }

        return next(action);
    };
    return middleware;
}

const thunk: MiddlewareExt = createThunkMiddleware() as MiddlewareExt;
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

  • 使用
import React from 'react'
import { Dispatch, AnyAction } from 'redux';
import { connect } from 'react-redux';
import { CombinedState } from '../store/reducers/index';
import { Counter1State } from '../store/reducers/counter1';
import * as types from '../store/action-types';
import { LocationDescriptorObject, LocationState } from 'history'
import { push } from 'connected-react-router'
// 异步 dispatch 
import { ThunkDispatch } from '../redux-thunk';


const mapStateToProps = (state: CombinedState): Counter1State => state.counter1 
// 异步 dispatch - ThunkDispatch<CombinedState, Record<string, unknown>, AnyAction>
const mapDispatchToProps = (dispatch: ThunkDispatch<CombinedState, Record<string, unknown>, AnyAction>) => ({
  add1(amount: number) {dispatch({type: types.ADD1, payload: amount })},
  add2() {dispatch({type: types.ADD2})},
  goTo(location: LocationDescriptorObject<LocationState>) {
    dispatch(push(location))
  },
   // 异步 dispatch
  asnycAdd(amount: number) {
    dispatch((dispatch: ThunkDispatch<CombinedState, Record<string, unknown>, AnyAction>, getState: any) => {
      setTimeout(() => {
        dispatch({type: types.ADD1, payload: amount})
      }, 1000)
    })
  }
})

type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>

class Counter1 extends React.Component<Props> {

  render () {
    return (
      <div>
        <p>{this.props.number}</p>
        <button onClick={() => this.props.add1(5)}>+5</button>
        <br/>
        <button onClick={() => this.props.add2()}>+2</button>
        <br/>
        <button onClick={() => this.props.goTo({pathname: '/counter2'})}>跳转页面</button>
        <br/>
        <button onClick={() => this.props.asnycAdd(5)}>异步thunk</button>
      </div>
    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter1)

14-Mock 数据

参考资料:

15-webpack-tsconfig 配置优化

参考学习资料: