阅读 1056

(下)手摸手教你大厂都在用 React+TS+Antd 快速入门到NodeJS全栈项目实战(附源码)

前言

上一篇:(上)手摸手教你大厂都在用 React+TS+Antd 快速入门到NodeJS全栈项目实战(附源码)

上一篇主要介绍项目用到的前端框架及UI组件库 react+typescript+antd,目录结构说明、用到技术栈、功能模块清单、快速构建项目、开发环境搭建、webpack配置等等。这一篇主要讲解以下内容:React全家桶的组合使用、有状态组件和无状态组件的使用、组件间通信、封装工具函数库、前端性能优化、RESTFUL API接口开发。

基于 Vue.js 前端框架开发的开源项目如下👉:

React 全家桶最新组合

  • react(核心组件库)
  • redux/react-redux(状态管理)
  • react-router/react-router-dom(路由)
  • axios(http请求库)
  • antd(React UI库)

redux 使用

redux 相当于一个数据库,可以当成一个本地的数据库使用,react-redux可以完成数据订阅,redux-thunk可以实现异步的action,redux-logger是redux的日志中间件。

redux 的核心概念:将需要修改的state都存入到store里,发起一个action用来描述发生了什么,用reducers描述action如何改变state tree 。创建store的时候需要传入reducer,真正能改变store中数据的是store.dispatch API。

了解redux思想

  • action(行为):它是一个对象里面必有type来指定其类型,这个类型可以理解为你要做什么,reducer要根据action的type来返回不同的state,每个项目有且可以有多个action 。

  • reducer(状态处理函数):可以理解为一个专门处理state的工厂,给他一个旧数据它会根据不同action.type返回新的数据,也就是:旧state + action = 新state,每个项目有且可以有多个reducer 。

  • store(状态管理器):store本质上是一个状态树,保存了所有对象的状态。任何UI组件都能直接从store访问特定对象的状态。每个项目有且只能有一个store 。

1) 使用官方脚手架

上一篇已介绍,这里就不多讲,使用官方脚手架会自动安装react和react-dom核心组件库。

2) 安装redux和react-redux

    yarn add redux react-redux -S
复制代码

进入到项目文件夹,把我们用不到的全部咔咔删掉。

在src目录新建store文件夹,在store文件夹下新建actions、reducers文件夹,actions存放分发的actions函数,reducers存放单个的reducer。

在src/store文件夹下新建actionTypes.js文件,用于存放分发action的type常量,代码如下:

 // 用户信息
 export const SET_USER_TOKEN = "SET_USER_TOKEN";
 export const SET_USER_INFO = "SET_USER_INFO";
 export const MODIFY_USER_INFO = "MODIFY_USER_INFO";
 export const CLEAR_USER_INFO = "CLEAR_USER_INFO";

 // todo增删改
 export const CHANGE_INPUT_VALUE = 'CHANGE_INPUT_VALUE';
 export const ADD_TODO_ITEM = 'ADD_TODO_ITEM';
 export const DELETE_TODO_ITEM = 'DELETE_TODO_ITEM';
复制代码

在reducers中创建单个reducer文件user.js,设置用户信息初始状态和函数,代码如下:

import * as types from '../actionTypes';

const initUserInfo = {
    data: {},
    isLogined: false
}

export default function user(state = initUserInfo, action) {
    switch (action.type) {
        case types.SET_USER_INFO:
            return {
                ...state,
                data: action.data,
                isLogined: true
            };
        case types.MODIFY_USER_INFO:
            return {
                ...state,
                data: Object.assign(state.data, action.data)
            };
        case types.CLEAR_USER_INFO:
            return {
                data: {},
                isLogined: false
            };
        default:
            return state;
    }
}
复制代码

在reducers中创建index.js文件,用来组合单个reducer,输出根state,代码如下:

 import { combineReducers } from 'redux';
 import user from './user';

 export default combineReducers({
    user
 })
复制代码

在actions中创建一个action文件user.js,声明函数用户信息保存和删除,代码如下:

import * as types from '../actionTypes';

export const saveUserInfo = (data) => {
    return {
        type: types.SET_USER_INFO,
        data
    }
}

export const clearUserInfo = () => {
    return {
        type: types.CLEAR_USER_INFO
    }
}
复制代码

在actions中创建一个action文件auth.js,声明函数用户登录、注册、退出,代码如下:

import { saveUserInfo, clearUserInfo } from './user';
import { loginUser, registerUser } from '@/utils/api';

export const login = (username, password) => (dispatch) => {
    return new Promise((resolve, reject) => {
        loginUser({ username: username.trim(), password: password })
        .then(res => {
            console.log('登录===', res)
            if (res.code === 0) {
                dispatch(saveUserInfo(res.data));
                resolve(res);
            } else {
                reject(res.msg);
            }
        })
    })
}

export const register = (username, password) => (dispatch) => {
    return new Promise((resolve, reject) => {
        registerUser({ username: username.trim(), password: password })
        .then(res => {
            console.log('注册===', res)
            if (res.code === 0) {
                dispatch(saveUserInfo(res.data));
                resolve(res);
            } else {
                reject(res.msg);
            }
        })
    })
}

export const logout = () => (dispatch) => {
    console.log('logout')
    dispatch(clearUserInfo());
    window.location.href = '/login';
}
复制代码

在actions中创建index.js,用来组合多个action,输出定义的函数,代码如下:

import { login, register, logout } from './auth';
import { saveUserInfo, clearUserInfo } from './user';

export {
    login,
    register,
    logout,
    saveUserInfo,
    clearUserInfo
}
复制代码

在src/store文件夹下创建store状态管理仓库文件index.js,含注释,代码如下:

// 创建一个store管理仓库,从redux库中引入一个createStore函数和applyMiddleware方法
import { createStore, applyMiddleware } from 'redux';
// 引入thunk中间件,实现异步action、打印日志、错误报告等功能,放入applyMiddleware方法之中
import thunk from 'redux-thunk';
import reducer from './reducers';

// 引入redux-persist库,全局数据持久化存储
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';

const persistConfig = {
    key: 'root',
    storage: storage,
    stateReconciler: autoMergeLevel2
}

const myPersistReducer = persistReducer(persistConfig, reducer)

// 引入createStore后,store并没有创建,需要调用createStore()后才有store
const store = createStore(
    myPersistReducer,
    applyMiddleware(thunk),
    // window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)

export const persistor = persistStore(store)

export default store
复制代码

3)引入react-redux的Provider和connect

了解react-redux思想

  • Provider:它是react-redux提供的一个 React 组件,作用是把state传给它的所有子组件,也就是说当你用Provider传入数据后,下面的所有子组件都可以共享数据,十分的方便。

    Provider的使用方法是:把Provider组件包裹在最外层的组件,即把整个APP组件给包裹住,然后在Provider里面把store传过去。注意:一定是在Provider中传store,不能在APP组件中传store。

  • connect:它是一个高阶组件 所谓高阶组件就是你给它传入一个组件,它会给你返回新的加工后的组件,注重用法倒简单,深究其原理就有点难度。这里不做connect的深究,主要是学会它的用法,毕竟想要深究必须先会使用它。首先它有四个参数([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]),后面两个参数可以不写,不写的话它是有默认值的。我们主要关注下前两个参数mapStateToProps和mapDispatchToProps 。

    connect的使用方法是:把指定的state和指定的action与React组件连接起来,后面括号里面写UI组件名。

    在src/index.tsx文件中引入react-redux及store,代码如下:

import * as React from 'react';
import ReactDOM from 'react-dom';
import 'antd/dist/antd.less';
import '@/styles/base.less';
import App from './App';
import store, { persistor } from './store';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/lib/integration/react';

ReactDOM.render(
  <Provider store={ store }>
    <PersistGate persistor={ persistor }>
      <App />
    </PersistGate>
  </Provider>,
  document.getElementById('root')
);
复制代码

在login组件里做connect,代码如下:

import * as React from 'react';
import { Input, Button, Checkbox, message } from 'antd';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { login, register } from '@/store/actions';
import logo from "@/assets/logo_2.png";
import '@/styles/login.less';
import { validUserName, validPass } from '@/utils/valid';
import DocumentTitle from 'react-document-title';

interface IProps {
    login: any,
    register: any,
    history: any
}

interface IState {
    formLogin: {
        userName: string,
        userPwd: string
    },
    formRegister: {
        userName?: string,
        userPwd2?: string,
        userPwd?: string,
    },
    typeView: number,
    checked: boolean,
    isLoading: boolean
}

class Login extends React.Component<IProps, IState> {
    constructor(props: any) {
        super(props);
        this.state = {
            formLogin: {
                userName: '',
                userPwd: '',
            },
            formRegister: {
                userName: '',
                userPwd2: '',
                userPwd: '',
            },
            typeView: 0,
            checked: false,
            isLoading: false
        }
    }

    // 设置cookie
    setCookie = (user_name: string, user_pwd: string, exdays: number) => {
        // 获取时间
        let exdate = new Date(); 
        // 保存的天数
        exdate.setTime(exdate.getTime() + 24 * 60 * 60 * 1000 * exdays); 
        // 字符串拼接cookie
        document.cookie = `userName=${user_name};path=/;expires=${exdate.toUTCString()}`;
        document.cookie = `userPwd=${user_pwd};path=/;expires=${exdate.toUTCString()}`;
    }
  
    // 读取cookie
    getCookie = () => {
        const { formLogin } = this.state;
        if (document.cookie.length > 0) {
          // 这里显示的格式需要切割一下自己可输出看下
          let arr = document.cookie.split('; '); 
          console.log(arr) 
          for (let i = 0; i < arr.length; i++) {
            // 再次切割
            let arr2 = arr[i].split('='); 
            // 判断查找相对应的值
            if (arr2[0] === 'userName') {
              // 保存数据并赋值
              this.setState({
                formLogin: {
                    userName: arr2[1],
                    userPwd: formLogin.userPwd
                }
              })
            } else if (arr2[0] === 'userPwd') {
                this.setState({
                    formLogin: {
                        userName: formLogin.userName,
                        userPwd: arr2[1]
                    }
                })
            } else {

            }
          }
        }
    }
  
    //清除cookie
    clearCookie = () => {
        // 修改前2个值都为空,天数为负1天就好了
        this.setCookie('', '', -1); 
    }

    // 立即登录
    handleLogin = () => {
        const { login, history } = this.props; 
        const { formLogin, checked } = this.state;

        if (!validUserName(formLogin.userName)) {
            message.error('请输入正确的邮箱/手机号');
            return false;
        }
    
        if (!validPass(formLogin.userPwd)) {
            message.error('密码应为8到20位字母或数字!');
            return false;
        }
        
        // 判断复选框是否被勾选,勾选则调用配置cookie方法
        if (checked) {
            // 传入账号名,密码,和保存天数3个参数
            this.setCookie(formLogin.userName, formLogin.userPwd, 7);
        } else {
            // 清空Cookie
            this.clearCookie();
        }

        login(
            formLogin.userName, 
            formLogin.userPwd
        )
        .then((res: any) => {
            console.log('login===', res);
            if (res.code === 0) {
                this.clearInput();
                message.success('登录成功');
                history.push('/');
            }
        })
        .catch((error: any) => {
            message.error(error);
        })
    }

    // 立即注册
    handleRegister = () => {
        console.log(this.props)
        const { register, history } = this.props; 
        const { formRegister } = this.state;

        if (!validUserName(formRegister.userName)) {
            message.error("请输入正确的邮箱/手机号");
            return false;
        } else if (!validPass(formRegister.userPwd)) {
            message.error("密码应为8到20位字母或数字!");
            return false;
        } else if (!validPass(formRegister.userPwd2)){
            message.error("确认密码有误");
            return false;
        } else if (formRegister.userPwd2 !== formRegister.userPwd){
            message.error("两次密码不一致");
            return false;
        }

        register(
            formRegister.userName,
            formRegister.userPwd2
        )
        .then((res: any) => {
            if (res.code === 0) {
                this.clearInput();
                message.success('注册成功');
                history.push('/');
            }
        })
        .catch((error: any) => {
            message.error(error);
        })
    }

    // 登录/注册tab切换
    handleTab = (type: number) => {
        // console.log('type===',type);
        this.setState({
            typeView: type
        })
        this.clearInput();
    }

    // 是否勾选记住密码
    checkChange = (e: any) => {
        console.log(e.target.checked);
        this.setState({
            checked: e.target.checked
        })
    }

    // 清空输入框
    clearInput = () => {
        this.setState({
            formLogin: {
                userName: '',
                userPwd: '',
            },
            formRegister: {
                userName: '',
                userPwd2: '',
                userPwd: '',
            }
        })
    }

    // 忘记密码界面
    forgetPwd = () => {
        message.info('忘记密码,请联系客服');
    }

    // 监听输入登录信息
    handleChangeInput = (e: any, type: number) => {
        const { formLogin } = this.state;
        this.setState({
            formLogin: {
                userName: type === 1 ? e.target.value : formLogin.userName,
                userPwd: type === 2 ? e.target.value : formLogin.userPwd
            }
        })
    }

    // 监听输入注册信息
    handleChangeRegister = (e: any, type: number) => {
        const { formRegister } = this.state;
        this.setState({
            formRegister: {
                userName: type === 1 ? e.target.value : formRegister.userName,
                userPwd: type === 2 ? e.target.value : formRegister.userPwd,
                userPwd2: type === 3 ? e.target.value : formRegister.userPwd2
            }
        })
    }

    // 判断点击的键盘keyCode是否为13,是就调用登录函数
    handleEnterKey = (e: any, type: number) => {
        const { formLogin, formRegister } = this.state;
        if (type === 1) {
            if (!formLogin.userName || !formLogin.userPwd) {
                return;
            } else {
                if(e.nativeEvent.keyCode === 13){ //e.nativeEvent获取原生的事件对像
                    this.handleLogin();
                }
            }
        } else {
            if (!formRegister.userName || !formRegister.userPwd || !formRegister.userPwd2) {
                return;
            } else {
                if(e.nativeEvent.keyCode === 13){ //e.nativeEvent获取原生的事件对像
                    this.handleRegister();
                }
            } 
        }
    }

    render () {
        const { formLogin, formRegister, typeView, checked } = this.state;
        return (
            <DocumentTitle title={'用户登陆'}>
                <div className="login-container">
                <div className="pageHeader">
                    <img src={ logo } alt="logo" />
                    <span>后台管理模板</span>
                </div>
                <div className="login-box">
                    <div className="login-text">
                        <span className={ typeView === 0 ? 'active' : '' } onClick={ () => this.handleTab(0) }>登录</span>
                        <b>·</b>
                        <span className={ typeView === 1 ? 'active' : '' } onClick={ () => this.handleTab(1) }>注册</span>
                    </div>

                { typeView === 0 ? 
                    <div className="right-content">
                        <div className="input-box">
                            <Input
                                type="text"
                                className="input"
                                value={ formLogin.userName }
                                onChange={ (e: any) => this.handleChangeInput(e, 1) }
                                placeholder="请输入登录邮箱/手机号"
                            />
                            <Input
                                type="password"
                                className="input"
                                maxLength={ 20 }
                                value={ formLogin.userPwd }
                                onChange={ (e: any) => this.handleChangeInput(e, 2) }
                                onPressEnter={ (e: any) => this.handleEnterKey(e, 1) }
                                placeholder="请输入登录密码"
                            />
                        </div>
                        <Button className="loginBtn" type="primary" onClick={ this.handleLogin } disabled={ !formLogin.userName || !formLogin.userPwd }>立即登录</Button>
                        <div className="option">
                            <Checkbox className="remember" checked={ checked } onChange={ this.checkChange }>
                                <span className="checked">记住我</span>
                            </Checkbox>
                            <span className="forget-pwd" onClick={ this.forgetPwd }>忘记密码?</span>
                        </div>
                    </div>
                    :
                    <div className="right_content">
                        <div className="input-box">
                            <Input
                                type="text"
                                className="input"
                                value={ formRegister.userName }
                                onChange={ (e: any) => this.handleChangeRegister(e, 1) }
                                placeholder="请输入注册邮箱/手机号"
                            />
                            <Input
                                type="password"
                                className="input"
                                maxLength={ 20 }
                                value={ formRegister.userPwd }
                                onChange={ (e: any) => this.handleChangeRegister(e, 2) }
                                placeholder="请输入密码"
                            />
                            <Input
                                type="password"
                                className="input"
                                maxLength={ 20 }
                                value={ formRegister.userPwd2 }
                                onChange={ (e: any) => this.handleChangeRegister(e, 3) }
                                onPressEnter={ (e: any) => this.handleEnterKey(e, 2) }
                                placeholder="请再次确认密码"
                            />
                        </div>
                        <Button className="loginBtn" type="primary" onClick={ this.handleRegister } disabled={ !formRegister.userName || !formRegister.userPwd || !formRegister.userPwd2 }>立即注册</Button>
                    </div>
                }
                </div>
            </div>
            </DocumentTitle>
        )
    }
}

export default withRouter(connect((state: any) => state.user, { login, register })(Login))
复制代码

4)react-thunk使用

1、中间件的概念

dispatch一个action之后,到达reducer之前,进行一些额外的操作,就需要用到middleware。你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

换言之,中间件都是对store.dispatch()的增强。redux-thunk就是用来异步操作,比如接口请求等。

2、引入redux-thunk,代码如下:

import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
  reducers,
  applyMiddleware(thunk)
);
复制代码

3、这样就可以在单个action中创建一个带异步函数的方法了,代码如下:

export const login = (username, password) => (dispatch) => {
    return new Promise((resolve, reject) => {
        loginUser({ username: username.trim(), password: password })
        .then(res => {
            console.log('登录===', res)
            if (res.code === 0) {
                dispatch(saveUserInfo(res.data));
                resolve(res);
            } else {
                reject(res.msg);
            }
        })
    })
}
复制代码

react-router 使用

React Router 是专为 React 设计的路由解决方案。它利用HTML5 的history API,来操作浏览器的 session history (会话历史)。

react-router包含3个库,react-router、react-router-dom和react-router-native。

react-router提供最基本的路由功能,实际使用的时候我们不会直接安装react-router,而是根据应用运行的环境选择安装react-router-dom(在浏览器中使用)或react-router-native(在rn中使用)。

react-router-dom和react-router-native都依赖react-router,所以在安装时,react-router也会自动安装,创建web应用。

1、安装依赖

yarn add react-router-dom -S
复制代码

2、基本使用

react-router中奉行一切皆组件的思想,路由器-Router、链接-Link、路由-Route、独占-Switch、重定向-Redirect 都以组件形式存在。

Route渲染优先级:children > component > render 。

BrowserRouter与HashRouter对比:

  • HashRouter 最简单,不需要服务器渲染,靠浏览器的#来区分path就可以,BrowserRouter 需要服务器端对不同的URL返回不同的HTML。

    后端配置可参考:react-guide.github.io/react-route…

  • BrowserRouter 使用HTML5历史API(pushState,replaceState 和 propstate 事件),让页面的UI同步与URL

  • HashRouter 不支持 location.key 和location.state,动态路由跳转需要通过?传递参数

  • Hash 不需要服务器任何配置就可以运行,如果你刚刚入门,那就使用它吧。但是我们不推荐在实际生产环境中用到它,因为每一个web应用都应该渴望使用 browserHistory 。

3、配置路由

在src目录新建router文件夹,在router中创建index.js文件,作为路由入口文件,代码如下:

import { routerConfig } from './config';
import { PermissionAuth } from './permissionAuth';

 class Routes extends React.Component {
    render () {
        return (
            <Switch>
                <PermissionAuth config={ routerConfig } />
            </Switch>
        )
    }
}

export default Routes
复制代码

在src/router中创建config.js文件,作为路由组件配置,代码如下:

import Login from '../views/Login.tsx';
import Home from '../views/Home.tsx';
import NotFound from '../components/404.tsx';

export const routerConfig = [
    {
        path: '/',
        component: Home,
        auth: true
    },
    {
        path: '/login',
        component: Login
    },
    {
        path: '/404',
        component: NotFound,
        auth: true
    }
]
复制代码

在src/router中创建路由守卫permission.js文件,作为组件包装Route使其具有权限判断功能,代码如下:

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import store from '@/store';

export class PermissionAuth extends React.Component {
    render () {
        const { location, config } = this.props;
        const { pathname } = location;
        const isLogin = store.getState().user.data.token;

        // 如果该路由不用进行权限校验,登录状态下登陆页除外
        // 因为登陆后,无法跳转到登陆页
        // 这部分代码,是为了在非登陆状态下,访问不需要权限校验的路由
        const targetRouterConfig = config.find(v => v.path === pathname);
        if (targetRouterConfig && !targetRouterConfig.auth && !isLogin) {
            const { component } = targetRouterConfig;
            return <Route exact path={ pathname } component={ component } />
        }

        if (isLogin) {
            // 如果是登陆状态,想要跳转到登陆,重定向到主页
            if (pathname === '/login') {
                return <Redirect to='/' />
            } else {
                // 如果路由合法,就跳转到相应的路由
                if (targetRouterConfig) {
                    return <Route path={ pathname } component={ targetRouterConfig.component } />
                } else {
                    // 如果路由不合法,重定向到 404 页面
                    return <Redirect to='/404' />
                }
            }
        } else {
            // 非登陆状态下,当路由合法时且需要权限校验时,跳转到登陆页面,要求登陆
            if (targetRouterConfig && targetRouterConfig.auth) {
                return <Redirect to='/login' />
            } else {
                // 非登陆状态下,路由不合法时,重定向至 404
                return <Redirect to='/404' />
            }
        }

        
    }
}
复制代码

在src/views中创建两个界面分别是:login.tsx(登录)、home.tsx(首页),代码如下:

// 首页组件
import * as React from 'react';
import DocumentTitle from 'react-document-title';
import { Drawer, Button, Table, Space, Pagination, message, Select, Form, Input, DatePicker } from 'antd';
import { StarOutlined, StarTwoTone, PlusOutlined  } from '@ant-design/icons';
import { ColumnsType } from 'antd/es/table';
import moment from 'moment';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import '@/styles/home.less';
import { 
  queryTaskList,
  addTask,
  editTask,
  updateTaskStatus,
  updateMark,
  deleteTask
} from '@/utils/api';
import { formatDate } from '@/utils/valid';

interface Task {
    id: number,
    title: string,
    content: string,
    gmt_expire: number,
    status: number,
    is_major: any
}

interface Values {
    id?: number,
    title: string,
    date: any,
    content: string
}

interface IState {
    total: number,
    pageNo: number,
    pageSize: number,
    loading: boolean,
    textBtn: string,
    title: string,
    visible: boolean,
    currentRowData: Values,
    status: any,
    columns: ColumnsType<Task>,
    dataSource: Task[]
}

interface IProps {
    title: string,
    textBtn: string,
    visible: boolean,
    currentRowData: Values,
    onSubmitDrawer: (values: Values, type: number) => void,
    onCloseDrawer: () => void
}

const AddEditTaskForm: React.FC<IProps> = ({
    title,
    textBtn,
    visible,
    currentRowData,
    onSubmitDrawer,
    onCloseDrawer
}) => {
    const [form] = Form.useForm();

    console.log('currentRowData===', currentRowData)
    setTimeout(() => {
        form.setFieldsValue(currentRowData);
    }, 100)

    const onSubmit = () => {
        form.validateFields()
        .then((values: any) => {
            if (title === '添加任务') {
                onSubmitDrawer(values, 1);
            } else {
                onSubmitDrawer(values, 2);
            }
        })
        .catch(info => {
            console.log('Validate Failed:', info);
        })
    }

    const onReset = () => {
        form.resetFields();
    }

    const onClose = () => {
        form.resetFields();
        onCloseDrawer();
    }
    
    return (
        <Drawer
            forceRender
            title={ title }
            width={ 600 }
            onClose={ onClose }
            visible={ visible }
            bodyStyle={{ paddingBottom: 80 }}
            maskClosable={ false }
            footer={
                <div style={{display: 'flex', justifyContent: 'space-around'}}>
                    <Button onClick={ onSubmit } type="primary">{ textBtn }</Button>
                    <Button onClick={ onReset }>重置</Button>
                    <Button onClick={ onClose } danger>取消</Button>
                </div>
            }
        >
            <Form
                form={ form }
                layout="vertical"
                name="form_in_modal"
            >
                <Form.Item
                    label="任务名称"
                    name="title"
                    rules={[{ required: true, message: '请输入任务名称' }]}
                >
                    <Input placeholder="请输入任务名称" />
                </Form.Item>
                <Form.Item 
                    label="截止日期"
                    name="date"
                    rules={[{ required: true, message: '请选择截止日期' }]}
                >
                    <DatePicker inputReadOnly={ true } placeholder="请选择截止日期" style={{ width: '100%' }} />
                </Form.Item>
                <Form.Item 
                    label="任务内容"
                    name="content"
                    rules={[{ required: true, message: '请输入任务内容' }]}
                >
                    <Input.TextArea rows={ 7 } placeholder="请输入任务内容" className="textarea" />
                </Form.Item>
            </Form>
        </Drawer>

    )
}

class Home extends React.Component<any, IState> {
    constructor(props: any) {
        super(props);
        this.state = {
            total: 0,
            pageNo: 1,
            pageSize: 10,
            loading: false,
            textBtn: '提交',
            title: '添加任务',
            currentRowData: {
                id: -1,
                title: '',
                date: '',
                content: ''
            },
            visible: false,
            dataSource: [],
            status: null,  // 0:待办 1:完成 2:删除
            columns: [
                {
                    title: '序号',
                    key: 'id',
                    align: 'center',
                    render: (text: any, record: any, index: number) => {
                        let num = (this.state.pageNo - 1) * 10 + index + 1;
                        return num;
                    }
                },
                {
                    title: '任务名称',
                    dataIndex: 'title',
                    key: 'title',
                    render: (text: any, record: any, index: number) => {
                        const fav = this.state.dataSource[index].is_major;
                        const style = {
                            cursor: 'pointer',
                            fontSize: '16px'
                        }

                        const icon = fav === 0 ? <StarOutlined style={ style } /> : <StarTwoTone style={ style } twoToneColor="#f50" />;

                        return <div><span onClick={ () => this.toggleFav(record, index) }>{ icon }</span> { record.title }</div>;
                    }
                },
                {
                    title: '任务内容',
                    dataIndex: 'content',
                    key: 'content'
                },
                {
                    title: '截止日期',
                    dataIndex: 'gmt_expire',
                    key: 'gmt_expire',
                    render: (text: any, record: any) => formatDate(record.gmt_expire)
                },
                {
                    title: '任务状态',
                    dataIndex: 'status',
                    key: 'status',
                    width: 120,
                    render: (text: any, record: any) => {
                        const txt = record.status === 0 ? '待办' : record.status === 1 ? '完成' : '删除';
                        return txt;
                    }
                },
                {
                    title: '操作',
                    key: 'action',
                    width: 300,
                    align: 'center',
                    render: (text: any, record: any, index: number) => (
                        <Space size="middle">
                            <Button style={{marginRight: '10px', display: record.status !== 2 ? '' : 'none'  }} onClick={ () => this.editTask(record, index) }>编辑</Button>
                            <Button type="primary" ghost style={{marginRight: '10px', display: record.status !== 2 ? '' : 'none' }} onClick={ () => this.completeTask(record) }>
                                { record.status === 0 ? '完成' : record.status === 1 ? '待办' : null }
                            </Button>
                            <Button danger style={{ display: record.status !== 2 ? '' : 'none'  }} onClick={ () => this.removeTask(record.id) }>删除</Button>
                        </Space>
                    )
                }
            ]
        }
    }

    componentDidMount () {
        console.log('componentDidMount===')
        this.getTaskList();
    }

    componentWillUnmount () {
        console.log('componentWillUnmount===')
    }

    // 重要或不重要
    toggleFav = (record: any, index: number) => {
        if (record.status === 2) {
          message.error('数据已删除');
        } else {
            // is_major: 0:不重要 1:重要
            let data = {
                id: record.id,
                is_major: this.state.dataSource[index].is_major === 0 ? 1 : 0
            }
  
            updateMark(data)
            .then((res: any) => {
                console.log('操作标记===', res);
                if (res.code === 0) {
                this.setState({
                    pageNo: 1
                }, () => {
                    this.getTaskList();
                    message.success('更新标记成功');
                })
                } else {
                message.error(res.msg);
                }
            })
        }
    }

    // 获取任务列表数据
    getTaskList = () => {
        const { pageNo, pageSize, status } = this.state;
        this.setState({
            loading: true
        })
  
        let params = {
          pageNo: pageNo,
          pageSize: pageSize,
          status: status
        }
  
        queryTaskList(params)
        .then((res: any) => {
            console.log('任务列表===', res);
            this.setState({
                loading: false
            })

            if (res.code === 0 && res.data) {
                this.setState({
                    dataSource: res.data.rows,
                    total: res.data.total
                })
            } else {
                this.setState({
                    dataSource: [],
                    total: 0
                })
            }
        })
        .catch(() => {
            this.setState({
                loading: false
            })
        })
    }

    // 添加任务对话框
    addTask = () => {
        console.log('添加任务===');
        this.setState({
            title: '添加任务',
            textBtn: '提交',
            visible: true,
            currentRowData: {
                id: -1,
                title: '',
                date: '',
                content: ''
            }
        })
    }

    // 编辑任务对话框
    editTask = (record: any, index: number) => {
        console.log('编辑任务===', record);
        this.setState({
            title: '编辑任务',
            textBtn: '保存',
            visible: true,
            currentRowData: {
                id: record.id,
                title: record.title,
                date: moment(record.gmt_expire),
                content: record.content
            }
        })
    }

    // 删除任务
    removeTask = (id: number) => {
        console.log('删除任务===');
        let data = {
            id: id,
            status: 2
        }

        deleteTask(data)
        .then((res: any) => {
            console.log('删除任务===', res);
            if (res.code === 0) {
                this.setState({
                    pageNo: 1
                }, () => {
                    this.getTaskList();
                    message.success('任务删除成功');
                })
            } else {
                message.error(res.msg);
            }
        })
    }

    // 完成/待办任务
    completeTask = (record: any) => {
        console.log('完成/待办任务===');
        let status = record.status === 0 ? 1 : record.status === 1 ? 0 : null;

        let data = {
            id: record.id,
            status: status
        }

        updateTaskStatus(data)
        .then((res: any) => {
            console.log('操作状态===', res);
            if (res.code === 0) {
                this.setState({
                    pageNo: 1
                }, () => {
                    this.getTaskList();
                    message.success('更新任务状态成功');
                })
            } else {
               message.error(res.msg);
            }
        })
    }

    // 提交添加或编辑表单
    onSubmit = (values: Values, type: number) => {
        console.log('表单提交===', values);
        const { currentRowData } = this.state;
        if (type === 1) {
            let data = {
                title: values.title,
                gmt_expire: moment(values.date).valueOf(),
                content: values.content
            }

            addTask(data)
            .then((res: any) => {
              console.log('添加任务===', res)
              this.setState({
                  visible: false
              })
              if (res.code === 0) {
                this.setState({
                    pageNo: 1
                }, () => {
                    this.getTaskList();
                    message.success(`新增任务 <${values.title}> 成功`);
                })
              } else {
                message.error(res.msg);
              }
            })
            .catch(() => {
                this.setState({
                    visible: false
                })
            })

        } else if (type === 2) {
            let data = {
                id: currentRowData.id,
                title: values.title,
                gmt_expire: moment(values.date).valueOf(),
                content: values.content
            }
  
            editTask(data)
            .then((res: any) => {
                console.log('编辑任务===', res)
                this.setState({
                    visible: false
                })
                if (res.code === 0) {
                    this.setState({
                        pageNo: 1
                    }, () => {
                        this.getTaskList();
                        message.success(`更新任务 <${values.title}> 成功`);
                    })
                } else {
                    message.error(res.msg);
                }
            })
            .catch(() => {
                this.setState({
                    visible: false
                })
            })
        }
    }

    // 关闭任务对话框
    onClose = () => {
        this.setState({
            visible: false,
            currentRowData: {
                id: -1,
                title: '',
                date: '',
                content: ''
            }
        })
    }

    // 页码改变的回调,返回改变后的页码
    changePage = (pageNo: number) => {
        console.log('pageNo=', pageNo)
        this.setState({
            pageNo
        }, () => {
            this.getTaskList();
        })
    }

    // 筛选任务状态
    handleChange = (value: number) => {
        console.log('任务状态筛选===', typeof value === 'string')
        this.setState({
            status: typeof value === 'string' ? null : value,
            pageNo: 1
        }, () => {
            this.getTaskList();
        })
    }

    render () {
        const { 
            total, 
            pageNo, 
            pageSize, 
            loading, 
            dataSource, 
            columns, 
            visible, 
            title,
            textBtn,
            currentRowData 
        } = this.state;
        const { Option } = Select;

        return (
            <DocumentTitle title={'首页'}>
                <div className="home-container">
                    <Header curActive={'active'} />

                    <div className="content clearfix">
                        <div className="list">
                            <h2>任务列表</h2>
                            <div className="list-right">
                                <Space size="middle">
                                    <Select size="large" onChange={ this.handleChange } style={{ width: 160 }} allowClear placeholder="请筛选任务状态">
                                        <Option value=''>全部</Option>
                                        <Option value={ 0 }>待办</Option>
                                        <Option value={ 1 }>完成</Option>
                                        <Option value={ 2 }>删除</Option>
                                    </Select>
                                    <Button type="primary" size="large" onClick={ this.addTask }><PlusOutlined /> 添加任务</Button>
                                </Space>
                            </div>
                        </div>
                        
                        <Table 
                            bordered
                            rowKey={ record => record.id  } 
                            dataSource={ dataSource } 
                            columns={ columns }
                            loading={ loading }
                            pagination={ false } 
                        />
                        <Pagination
                            className="pagination"
                            total={ total }
                            style={{ display: loading && total === 0 ? 'none' : '' }}
                            showTotal={total => `共 ${total} 条数据`}
                            onChange={ this.changePage }
                            current={ pageNo }
                            showSizeChanger={ false }
                            defaultPageSize={ pageSize }
                            hideOnSinglePage={ false }
                        />
                    </div>

                    <Footer />

                    <AddEditTaskForm
                        title={ title }
                        textBtn={ textBtn } 
                        visible={ visible }
                        currentRowData={ currentRowData }
                        onSubmitDrawer={ this.onSubmit }
                        onCloseDrawer={ this.onClose }
                    />
                   
                </div>
            </DocumentTitle>
        )
    }
}

export default Home
复制代码

axios 介绍

axios 是基于Promise的用于浏览器和Node.js的http客户端。可以发送get、post等http请求,用来和服务器进行交互的。

antd 介绍

Ant Design of React 是个很好的React UI库,看起来跟我们熟知的bootstrap有点类似,从页面布局到按钮到文字提示框应有尽有。

有状态组件和无状态组件区别

区别

  • 有状态组件是一个类,无状态组件是一个函数;
  • 是否拥有state:有状态组件可以使用state,无状态组件不能使用state;只有继承component这个组件它才能拥有state进行一些数据的存储和管理,仍然可以拥有props;
  • 是否拥有生命周期:如果是有状态组件的话那么你就会拥有生命周期函数,无状态组件就不用有生命周期函数,因为数据的更改都是通过状态进行更改。使用props进行组件间的通信传值,如果要更改某一些数据的话使用的是state,既然数据可以发生变化那么它就会触发对应的生命周期函数;
  • 有状态组件可以通过this接收属性和状态,无状态组件可以通过属性实现数据传递
  • 有状态组件能使用this,无状态组件不能使用this 。

如何选择

如果想要存储一些数据并且想要对这些数据进行增删改查那么就应该使用有状态组件,如果只是单纯的处理一些逻辑就用无状态组件,我们更多应该使用的是无状态组件(因为如果是一个有状态组件的话那么他就一定会触发生命周期定义一些函数,一旦触发这些函数就会影响当前项目的运行,所以在尽可能的情况下使用无状态组件,除非你对当前的组件不是很清晰是否要存储数据,这个时候可能选择使用有状态组件或者确定要拥有一些状态去存储数据,那么就需要使用有状态组件)。状态的东西都会通过父级去传递,比如Persons,Header这些组件如果想用到数据的话我们可以通过传递的形式给它传递过去,即当前的数据能够进行统一的数据管理,比如说通过父级管理数据,其他组件如果想拥有这个组件的话可以通过传值的形式给它。

有状态组件(stateful components)

平时用的大部分是有状态组件,代码如下:

import React from 'react';

class Bottom extends React.Component{
    constructor(props){
        super(props);
    }

    sayHi = () => {
        let { name } = this.props
        console.log(`Hi ${name}`);
    }

    render () {
        let { name } = this.props;
        let { sayHi } = this;

        return (
            <div className="app">
                <h1>{`Hello, ${name}`}</h1>
                <button onClick={ this.sayHi }>Say Hi</button>
            </div>
        )
    }

}

export default Bottom;
复制代码

无状态组件(stateless components)

它是一种函数式组件,没有state, 接收Props,渲染DOM,而不关注其他逻辑。

import React from 'react';

export const Bottom = (props) => {
    let { name } = props;

    const sayHi = () => {
        console.log(`Hi ${name}`);
    }
    
    return (
        <div className="app">
            <h1>Hello, { name }</h1>
            <button onClick={ sayHi }>Say Hi</button>
        </div>
    )
}
复制代码

组件间通信

React的基本组件元素是一个个组件,组件之间可能存在关联、组合等关系。不同的组件之间,经常会发生数据传递或者交换,我们称之为组件间通信。

根据传递的复杂程度,可以分为以下三种情况:

父组件向子组件传值

在父组件中通过props属性传递给子组件,在子组件中通过this.props获取信息,代码如下:

// 父组件
import React from 'react';

class Father extends React.Component {
    // 这里要加super,否则会报错    
    constructor(props) {
        super(props);        
        this.state = {            
            checked: true
        }
    }

    render () {        
        return (            
            <Sub text="Toggle me" checked={ this.state.checked } />
        )
    }
}

// 子组件
class Sub extends React.Component {
    render () {       
        // 接收来自父组件的参数
        let { checked, text } = this.props;
        return (            
            <label>{ text }: <input type="checkbox" checked={ checked } /></label>
        )
    }
}

export default Father;
复制代码

子组件向父组件传值

子组件向父组件传递数据,是通过父级传递进来的props中的函数引用,间接的唤起父级处理函数,并传入参数。

// 父组件
import React from 'react';

class Father extends React.Component {
    constructor(props) {
        super(props);        
        this.state = {            
            checked: false
        }
    }

    onChildChanged = (newState) => {
        this.setState({            
            checked: newState
        })
    }

    render () {
        let isChecked = this.state.checked ? 'yes' : 'no';         
        return (                   
            <div>
                <span>Are you checked: { isChecked }</span>
                <Child text="Toggle me" initChecked={ this.state.checked } cbFather={ this.onChildChanged }></Child>
            </div>
        )
    }
}

// 子组件
class Child extends React.Component {
    constructor(props) {        
        super(props);        
        this.state = {            
            checked: this.props.initChecked
        }
    }

   onTextChange = () => {        
        let newState = !this.state.checked;
        // 注意:setState 是一个异步方法,state值不会立即改变,回调时要传缓存的当前值        
        this.setState({            
            checked: newState
        });             
        // 也可以利用传递一个函数(以上传的是对象),并传递prevState参数来实现数据的同步更新
        this.props.cbFather(newState);
    }

    render() {        
        let { text } = this.props;        
        let { checked } = this.state;        
        return (            
            <label>{ text }: <input type="checkbox" checked={ checked }  onChange={ this.onTextChange }></label>
        )
    }
}

export default Father;
复制代码

兄弟组件之间传值

两个兄弟组件之间会有一个共同的父组件,我们都是结合父子传值的方式来实现兄弟之间的传值的,即先其中一个子组件(兄弟组件)向父组件传值,然后父组件接收到这个值之后再将值传递给另外一个子组件(兄弟组件)。

// 父组件
import React from 'react';

class Father extends React.Component {
    constructor(props) {
        super(props);        
        this.state = {            
            msg: ''
        }
    }

    // 向子组件1提供的传值方法,参数为获取的子组件传过来的值
    getDatas = (data) => {
        this.setState({
            msg: data
        });
    }

    render() {       
        return (                   
            <React.Fragment>
                父组件中显示按钮并传值:
                <Child1 data={ this.getDatas }></Child1>
                <Child2 msg={ this.state.msg }></Child2>
            </React.Fragment>
        )
    }
}

// 子组件1
class Child1 extends React.Component {
    
    // 按钮点击事件,向父组件传值
    handleClick = () => {
        this.props.data('hello...React...');
    }

    render() {      
        return (            
            <button onClick={ this.handleClick }>child1子组件中获取数据</button>
        )
    }
}

// 子组件2
class Child2 extends React.Component {
    
    render() {      
        return (            
            <div>在child2子组件中展示数据:{ this.props.msg }</div>
        )
    }
}

export default Father;
复制代码

封装工具函数库

在src目录新建工具函数utils文件夹,在utils中创建valid.js文件,代码如下:

export function formatDate(value) {
  if (!value) {
    return '';
  }
  let d = new Date(value);
  let year = d.getFullYear();
  let month = (d.getMonth() + 1) < 10 ? '0' + (d.getMonth() + 1) : (d.getMonth() + 1);
  let day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate();
  return  year + '-' + month + '-' + day;
}

export default {
    formatDate
}
复制代码

在组件中引入valid.js文件,代码如下:

import React from 'react';
import { formatDate } from '@/utils/valid';

class Home extends React.Component {
    constructor(props) {
        super(props);        
        this.state = {            
            msg: ''
        }
    }
    
    componentDidMount () {
        this.setState({
            msg: formatDate(Date.now())
        })
    }
    
    render() {
        return (
            <div>当前时间:{ this.state.msg }</div>
        )
    }
}

export default Home;
复制代码

前端性能优化

图片优化

推荐一款在线图片压缩工具tinypng.com/

推荐一款免费上传图片的CDNimgchr.com/

打包不生成 sourceMap,在config-overrides.js文件添加以下代码即可

const isProduction = process.env.NODE_ENV === 'production';
process.env.GENERATE_SOURCEMAP = isProduction ? 'false' : 'true';
复制代码

开启gzip压缩

// 安装gzip压缩插件
yarn add compression-webpack-plugin -D
复制代码
const webpack = require('webpack');
const CompressionWebpackPlugin = require('compression-webpack-plugin');

const addCustomize = () => (config) => {
    // 添加js、css打包gzip配置
    config.plugins.push(
      new CompressionWebpackPlugin({
        algorithm: 'gzip', // 算法
        test: /\.js$|\.css$/,  // 压缩 js 与 css
        threshold: 1024, // 只处理比这个值大的资源,按字节计算
        // minRatio: 0.8 // 只有压缩率比这个值小的资源才会被处理
      }),
    )
    
    return config;
}

module.exports = {
    webpack: override(
        // 压缩JS、CSS等
        addCustomize()
    )
}
复制代码

然后在 nginx 开启 gzip 配置

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
#gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";
复制代码

生产环境清除 console 语句和 debugger 语句

yarn add uglifyjs-webpack-plugin -D
复制代码
const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const isProduction = process.env.NODE_ENV === 'production';

module.exports = {
    webpack: override(
        // 判断环境,只有在生产环境的时候才去使用这个插件
        isProduction && addWebpackPlugin(new UglifyJsPlugin({
            uglifyOptions: {
                compress: {
                    drop_debugger: true,
                    drop_console: true
                }
            }
        }))
    )
}
复制代码

RESTFUL API接口开发

NodeJS全栈项目开发的API接口,之前已讲过,这里就不多说,请移步到这里👉juejin.im/post/684490…,查看第五条后端部分。

最后

React版本的NodeJS全栈项目实战,就分享到这里,根据自己的实践操作及记录总结,希望能帮助到想要学习或正在学习React+TS+Antd技术栈的小伙伴~🐶

如果大家真心觉得不错的话,就请赏个赞👍或给个收藏💖吧!你们的赞和github star是我编写更多更精彩文章的动力!再次感谢支持!

github地址👉:github.com/jackchen012…

此项目其实还存在有不足的地方,愿接受批评指正,也期望与大家一起交流学习共同进步。😃

推荐阅读相关优质文章

获取更多项目实战经验及各种源码资源,请关注作者公众号:懒人码农