2023年【考勤项目】Vue3+React18 + TS4 入门到实战

根据本人需要,目前只记录React18+TS4,Vue3+TS4可参考课程

后续笔记暂且在本地未整理上传,需要者可私信

“*标题”代表基础知识回顾

目录

基础准备

项目展示

软件说明

版本说明

编辑器及插件说明

浏览器及插件说明

TS4安装及入门

React18安装及入门

初始化工程与整体架构

A.后端接口文档说明及在线数据访问

B.本地接口启动及数据访问

脚手架安装及第三方模块安装与配置

脚手架安装

第三方模块安装

用编辑器打开初始代码

封装axios

拷贝全局样式和图片

创建项目页面

架构项目路由系统及对应页面

配置路由

测试

二级路由

扩展meta元信息接口与全局守卫

扩展meta元信息接口与指定类型

全局守卫组件

权限判定

架构项目状态管理系统

*Redux状态管理

创建状态管理的模块users、 signs、news、checks

配置数据持久化功能并测试后端接口

使用redux-persist模块进行持久化

处理useSelector和useDispatch

调用登录接口和更新token接口

登录界面和首页实现

搭建登录界面

请求头发送token和获取用户信息

拆分首页头部组件和侧边栏组件


基础准备

项目展示

1.http://vue.h5ke.top

2.http://react.h5ke.top

数据仅为测试使用,可能会被重置,

软件说明

React是用于构建用户界面的JavaScript库,起源于Facebook。

vue是轻量级框架,起源于Google

全球趋势肯定是React使用的多一些,毕竟是大公司研发的,有保障,有排面。但在国内的话,其实Vue和React是不相上下的,基本上是两足鼎立的趋势,毕竟Vue使用起来更简单,且中文社区也比较好。

目前在一线公司和一些高级团队中都开始采用Vue+TS或React+TS的开发形式,也慢慢成为了未来的趋势。

版本说明

React框架:18.2.03.

TypeScript:4.7.44.

nodejs:16.13.1

编辑器及插件说明

VSCode编辑器,可官方直接下载(下载最新版或更新到最新版)

插件:

1.VueVSCodeSnippets

2.VueLanguageFeatures(Volar)

3.TypeScriptVuePlugin(Volar)

4.ES7+React/Redux/React-Nativesnippets

Redux应用数据流框架

React Native 将原生开发的最佳部分与 React 相结合

浏览器及插件说明

Chrome浏览器,可官方直接下载(下载最新版或更新到最新版)

插件:

1.Vue.jsdevtools

2.ReactDeveloperTools

3.ReduxDevTools

4.TalendAPITester

Talend API Tester是一款Chrome浏览器上交互和调试REST、SOAP和HTTP接口的客户端插件,类似于Postman。

TS4安装及入门

React18安装及入门

初始化工程与整体架构

A.后端接口文档说明及在线数据访问

Talend API Tester插件 API测试工具

B.本地接口启动及数据访问

数据库采用mongodb3.6的版本,robo3t可视化工具

MongoDB数据库安装时不动怎么办?MongoDB数据库安装教程 - 优草派

mongodb安装好后,可以把软件写入到全局环境变量中,这样就可以在全局地址下进行访问了,操作流程如下:

  1. 找到安装路径:C:\Program Files\MongoDB\Server\3.6\bin\

  2. 此电脑->属性->高级系统设置->环境变量->系统变量->Path

  3. 把第一步中的地址,添加到Path中,就可以设置全局成功

解压压缩包app-server,并执行npm install

后端mvc架构:


启动后端命令: npm start

包含两步:第一步启动数据库,第二个启动后端服务。然后就可以测试接口了。首先先注册两个测试账号,接口调用如下:

```shell
# request POST
http://localhost:3000/users/register
```

```json
// response
{"errcode":0}
```

看到`{"errcode":0}`就表示注册账号成功,会有两个账号数据,即:黄蓉和洪七公。

再进行登录接口测试即可。

```shell
# request POST { "email":"[email protected]", "pass":"huangrong" }
http://localhost:3000/users/login
```

```json
// response
{"errcode":0,"errmsg":"ok","token":"..."}
```

测试接口: http://localhost:3000/users/register
 

刷新

脚手架安装及第三方模块安装与配置

脚手架安装

facebook出了 基于webpack+ES6创建的create-react-app命令,用于react项目开发环境的构建,也是用 React 创建新的单页应用的最佳方式。

安装Node >= 14.0.0 和 npm >= 5.6。

# 安装命令
npx create-react-app my-app
cd my-app
npm start

主要开发代码在src目录下。App.js为根组件,index.js为入口模块,index.css为全局样式文件。

安装命令:

npx create-react-app 项目名称 --template typescript                     

第三方模块安装

axios sass antd ant-design/icons react-router-dom

axios样式预编译sass   ant-design/icons的图标,路由

redux react-redux redux-persist @reduxjs/toolkit
状态管理redux,持久化redux-persist,RTK

安装命令:

npm i axios sass antd @ant-design/icons react-router-dom redux react-redux redux-persist @reduxjs/toolkit

React + Ts项目搭建_react+ts项目搭建_iam671的博客-CSDN博客

react项目中引入typescript_2422400672的博客-CSDN博客_react 引入typescript

用编辑器打开初始代码

命令:

项目文件夹中:code .

封装axios

调整:

引用模块时import后面加上{}和不加{}的区别_Time-Traveler的博客-CSDN博客

import axios from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'


const instance = axios.create({
  baseURL: 'http://localhost:3000/',
  timeout: 5000
});

instance.interceptors.request.use(function (config) {

  return config;
}, function (error) {
  return Promise.reject(error);
});

instance.interceptors.response.use(function (response) {
  return response;
}, function (error) {
  return Promise.reject(error);
});

interface Data {
  [index: string]: unknown
}

interface Http {
  get: (url: string, data?: Data, config?: AxiosRequestConfig) => Promise<AxiosResponse>
  post: (url: string, data?: Data, config?: AxiosRequestConfig) => Promise<AxiosResponse>
  put: (url: string, data?: Data, config?: AxiosRequestConfig) => Promise<AxiosResponse>
  patch: (url: string, data?: Data, config?: AxiosRequestConfig) => Promise<AxiosResponse>
  delete: (url: string, data?: Data, config?: AxiosRequestConfig) => Promise<AxiosResponse>
}

const http: Http = {
  get(url, data, config){
    return instance.get(url, {
      params: data,
      ...config
    })
  },
  post(url, data, config){
    return instance.post(url, data, config)
  },
  put(url, data, config){
    return instance.put(url, data, config)
  },
  patch(url, data, config){
    return instance.patch(url, data, config)
  },
  delete(url, data, config){
    return instance.delete(url, {
      data,
      ...config
    })
  }
}

export default http;

拷贝全局样式和图片


创建项目页面

//输入rfc 快捷创建初始结构,react from react
import React from 'react'
import styles from './Login.module.scss'
export default function Home() {
  return (
    <div> Login </div>
  )
}

解决Cannot find module ‘./index.module.scss‘ or its corresponding type declarations.ts(2307)_不吃萝卜不吃菜的博客-CSDN博客

架构项目路由系统及对应页面

配置路由

import React,{lazy} from "react";
import { createBrowserRouter } from "react-router-dom";
//指定类型
import type {RouteObject} from "react-router-dom";
//通过懒加载映射Home
const Home=lazy(()=>import('../views/Home/Home'))
const Sign = lazy(()=> import('../views/Sign/Sign'))
const Exception = lazy(()=> import('../views/Exception/Exception'))
const Apply = lazy(()=> import('../views/Apply/Apply'))
const Check = lazy(()=> import('../views/Check/Check'))
const Login = lazy(()=> import('../views/Login/Login'))
//创建路由表
const routes:RouteObject[]=[
    {
        //根路径
        path:'/',
        //页面模块
        element: React.createElement(Home),
        children: [
            {
              path: 'sign',
              element: React.createElement(Sign),
             
            },
            {
              path: 'exception',
              element: React.createElement(Exception),
              
            },
            {
              path: 'apply',
              element: React.createElement(Apply),
             
            },
            {
              path: 'check',
              element: React.createElement(Check),
             
            }
        ]
    },
    {
        path: '/login',
        element: React.createElement(Login)
    }
];
//创建router对象
const router=createBrowserRouter(routes);
//对外路由表
export default router;

端口3000改成8080

测试

npm start 

  ctrl+c 终止运行

二级路由

//输入rfc 快捷创建初始结构,react from react
import React from 'react'
import styles from './Home.module.scss'
import { Outlet } from 'react-router-dom'

export default function Home() {
  return (
    <div> Home 
      <Outlet/>
    </div>
  )
}

扩展meta元信息接口与全局守卫

扩展meta元信息接口与指定类型

一般情况下,不同的路由获取到的信息是不一样的,可以通过自定义元信息来完成操作。

//index.ts

//引入图标
import {
  CopyOutlined,
  CalendarOutlined,
  WarningOutlined,
  FileAddOutlined,
  ScheduleOutlined,
} from '@ant-design/icons'

//扩展RouteObject
declare module 'react-router' {
  interface IndexRouteObject {
    meta?: {
      menu?: boolean
      title?: string
      icon?: React.ReactNode 
      auth?: boolean
    }
  }
  interface NonIndexRouteObject {
    meta?: {
      menu?: boolean
      title?: string
      icon?: React.ReactNode  
      auth?: boolean
    }
  }
}

export const routes: RouteObject[] = [
  {
    path: '/',
    element: React.createElement(BeforeEach, null, React.createElement(Home)),
    meta: {
      menu: true,
      title: '考勤管理',
      icon: React.createElement(CopyOutlined),
      auth: true
    },
    children: [
      {
        path: 'sign',
        element: React.createElement(Sign),
        meta: {
          menu: true,
          title: '在线打卡签到',
          icon: React.createElement(CalendarOutlined),
          auth: true
        }
      },
      {
        path: 'exception',
        element: React.createElement(Exception),
        meta: {
          menu: true,
          title: '异常考勤查询',
          icon: React.createElement(WarningOutlined),
          auth: true,
        }
      },
      {
        path: 'apply',
        element: React.createElement(Apply),
        meta: {
          menu: true,
          title: '添加考勤审批',
          icon: React.createElement(FileAddOutlined),
          auth: true,
        }
      },
      {
        path: 'check',
        element: React.createElement(Check),
        meta: {
          menu: true,
          title: '我的考勤审批',
          icon: React.createElement(ScheduleOutlined),
          auth: true,
        }
      }
    ]
  },
  {
    path: '/login',
    element: React.createElement(BeforeEach, null, React.createElement(Login))
  }
];

图标 Icon - Ant Design

 引入图标,以组件方式调用React.ReactNode(React中),vue是string,


全局守卫组件<BeforeEach />

auth: true,根据是否有权限进行拦截

//index.ts
//懒加载

const BeforeEach = lazy(()=> import('../components/BeforeEach/BeforeEach'))

//给Home和Login这两大部分套上全局守卫BeforeEach
//提供并创建路由表
export const routes:RouteObject[]=[
    {
        //根路径
        path:'/',
        //页面模块,属性暂定null,子项为Home
        element: React.createElement(BeforeEach,null,React.createElement(Home)),
        children: [
           ...
    },
    {
        path: '/login',
        element: React.createElement(BeforeEach,null,React.createElement(Login))
    }
];

权限判定

解决:在主模块中加入suspense,以配合lazy

//index.tsx
import React,{Suspense} from 'react';
...
root.render(
  <React.StrictMode>
    <Suspense>
    <RouterProvider router={router}></RouterProvider>
    </Suspense>
  </React.StrictMode>
);

架构项目状态管理系统

*Redux状态管理

Redux就像Vue中的Vuex或Pinia是一样的,专门处理状态管理的。

只不过Redux比较独立,可以跟很多框架结合使用,不过主要还是跟React配合比较好,也是最常见的React状态管理的库。

  • State:用于存储共享数据

  • Reducer:用于修改state数据的方法

  • Middleware:用于扩展一些插件来完成异步的操作

  • Dispatch:用于触发Reducer或Middleware

因为Redux是一个独立的库,所以和React结合还是不够方便,因此就诞生了react-redux这个库, 第三方模块react-redux来简化对Redux的使用,属于Redux的一个辅助模块。

<Provider>组件主要是注册状态管理与React结合,并且可以自动完成重渲染的操作。

useSelector,useDispatch都是react-redux库提供的use函数,可以获取共享状态以及修改共享状态。

Redux在使用上还是有很多不方便的地方,所以提供了Redux-Toolkit(RTK)这个模块,通过这么模块可以更方便的处理Redux的操作,下面列举一些RTK的好处:

  • 可以自动跟redux devtools结合,不需要再下载模块进行生效

  • 数据不需要再通过返回值进行修改,像Vue一样可以直接修改

  • 内置了 redux-thunk 这个异步插件

  • 代码风格更好,采用选项式编写程序

创建状态管理的模块users、 signs、news、checks

创建文件夹,为了不报错并加载为模块(export{}),其中index.ts为主模块

//users.ts
import http from "../../utils/http";
import { createSlice,createAsyncThunk} from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit/dist/createAction";
import http from "../../utils/http";
//指定类型,首字母大写
type Token=string;
type Infos={
    [index:string]:unknown
}
type UsersState={
    token:Token
    infos:Infos
}
//账号和密码
type Login={
    email:string
    pass:string
}
//参数:'users/loginAction'命名空间,异步方法;
export const loginAction=createAsyncThunk('users/loginAction',async(payload:Login)=>{
    //调用封装好的axios,参数:接口。调用
    const ret=await http.post('/users/login',payload)
    return ret;
})
//获取用户信息
export const infosAction=createAsyncThunk('users/infosAction',async()=>{
    const ret=await http.get('/users/infos')
    return ret;
})

const usersSlice=createSlice({
    //命名空间
    name:'users',
    //断言共享状态
    initialState:{
        token:'',
        infos:{}
    }as UsersState,
    //同步方法
    reducers:{
        updateToken(state,action:PayloadAction<Token>){
            state.token=action.payload;
        },
        updateInfos(state,action:PayloadAction<Infos>){
            state.infos=action.payload;
        },
        clearToken(state){
            state.token='';
        }
    }
})
//将方法结构出来,提供给外部使用
export const {updateInfos,updateToken,clearToken}=usersSlice.actions

export default usersSlice.reducers;
import { configureStore } from "@reduxjs/toolkit/dist/configureStore";
import { useReducer } from "react";
//index.ts
import usersReducer from './modules/users'

const store=configureStore({
    reducer:{
        users:useReducer
    }
})

export default store;

配置主模块,让状态管理生效

...

import { Provider } from 'react-redux';
import store from './store'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Suspense>
      <Provider store={store}>
      <RouterProvider router={router}></RouterProvider>
      </Provider>
    </Suspense>
    
  </React.StrictMode>
);


配置数据持久化功能并测试后端接口

使用redux-persist模块进行持久化

redux-persist模块是对状态管理进行持久化处理的,默认数据是不会被保存下来的,需要长期存储改变的共享数据就需要使用持久化模块。基本配置参考RTK官网即可。

通过本地token

import { configureStore } from "@reduxjs/toolkit/dist/configureStore";
import { useReducer } from "react";
//index.ts
import usersReducer from './modules/users'
//引入
import {
    persistStore,
    persistReducer,
    // FLUSH,
    // REHYDRATE,
    // PAUSE,
    // PERSIST,
    // PURGE,
    // REGISTER,
  } from 'redux-persist'
  import storage from 'redux-persist/lib/storage'
//配置  
  const persistConfig = {
    key: 'root',
    version: 1,
    storage,
    //白名单
    whitelist: ['token']
  }
  

const store=configureStore({
    reducer:{
        users:persistReducer(persistConfig, usersReducer) 
    },
    //复制官网的中间件
    middleware: (getDefaultMiddleware) =>
      //为了持久化默认调用了一些任务,为序列化产生了冲突
      // serializableCheck: {
      //   ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      // },
    //序列化做成false
      serializableCheck: false
    }),
})
//配置持久化
persistStore(store)

export default store;

处理useSelector和useDispatch


//index.ts
...
  import { useDispatch } from 'react-redux'
...

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch
...

调用登录接口和更新token接口

//Login.tsx
import React from 'react'
import styles from './Login.module.scss'
import { Button, message } from 'antd'
import { useSelector } from 'react-redux'
import { useAppDispatch } from '../../store'
import type { RootState } from '../../store'
import { loginAction, updateToken } from '../../store/modules/users'
export default function Login() {
  const token = useSelector((state: RootState)=> state.users.token)
  const dispatch = useAppDispatch()
  const handleLogin = () => {
    dispatch(loginAction({ email: '[email protected]', pass: 'huangrong' })).then((action)=>{
      const {errcode, token} = (action.payload as {[index: string]: unknown}).data as {[index: string]: unknown}
      if( errcode === 0 && typeof token === 'string' ){
        dispatch(updateToken(token))
        message.success('登录成功');
      }
      else{
        message.error('登录失败');
      }
    })
  }
  return (
    <div>
      Login
      <br />
      <Button onClick={handleLogin}>登录</Button>
      { token }
    </div>
  )
}

1.当持久化后,会推断不出来类型

2. 那么写错时就不会提醒

 3.观察users推断出来的是什么类型

 4.断言类型


//index.ts
import type { Reducer, AnyAction } from '@reduxjs/toolkit'
import usersReducer from './modules/users'
import type { UsersState } from './modules/users'
import type { PersistPartial } from 'redux-persist/es/persistReducer'
...

const store=configureStore({
    reducer:{
        users:persistReducer(persistConfig, usersReducer)  as Reducer<UsersState & PersistPartial, AnyAction>
    }
    ...
})
...

export type RootState = ReturnType<typeof store.getState>
...

5.结果

登录界面和首页实现

搭建登录界面

//Login.module.scss
.login {
    width: 100vw;
    height: 100vh;
    background: url('../../assets/images/login-bg.svg') no-repeat center 110px;
    background-size: 100%;
    .header {
      height: 44px;
      line-height: 44px;
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 34px;
      padding-top: 100px;
      .header-logo {
        .icon-react,
        .icon-icon-test,
        .icon-typescript {
          margin-right: 5px;
          font-size: inherit;
        }
        .icon-react {
          color: #61dafb;
        }
        .icon-icon-test {
          color: #deb887;
        }
        .icon-typescript {
          color: blue;
        }
      }
      .header-title {
        margin-left: 30px;
        font-weight: 700;
        font-size: 30px;
      }
    }
    .desc {
      text-align: center;
      padding-top: 30px;
      color: rgba(0, 0, 0, 0.45);
      font-size: 16px;
    }
    .main {
      width: 500px;
      margin: 0 auto;
      padding-top: 50px;
    }
    .users{
      width: 500px;
      margin: 60px auto;
      color: rgba(0,0,0,.65);
      h3{
        font-size: 16px;
      }
      p{
        margin: 20px;
      }
    }
  }

Ant Design表单组件:<Form>

字体图标

//Login.tsx
import React from 'react'
import styles from './Login.module.scss'
import { Button, message, Form, Input, Row, Col } from 'antd'
import { useAppDispatch } from '../../store'
import { loginAction, updateToken } from '../../store/modules/users'
import classNames from 'classnames'
import { useNavigate } from 'react-router-dom'

interface User {
  email: string
  pass: string
}

const testUsers: User[] = [
  {
    email: '[email protected]',
    pass: 'huangrong'
  },
  {
    email: '[email protected]',
    pass: 'hongqigong'
  }
];

export default function Login() {
  const navigate = useNavigate()
  const dispatch = useAppDispatch()
  //拿到表单实例
  const [form] = Form.useForm()

  const onFinish = (values: User) => {
    dispatch(loginAction(values)).then((action)=>{
      const {errcode, token} = (action.payload as {[index: string]: unknown}).data as {[index: string]: unknown}
      if( errcode === 0 && typeof token === 'string' ){
        dispatch(updateToken(token))
        message.success('登录成功');
        navigate('/');
      }
      else{
        message.error('登录失败');
      }
    })
  };
  const onFinishFailed = ({values}: {values: User}) => {
    console.log('Failed:', values);
  };
  const autoLogin = (values: User) => {
    return ()=>{
      form.setFieldsValue(values)  // 设置数据的回显,即测试账号密码显示在输入框,valuse{mail,pass}
      onFinish(values)
    }
  }
  return (
    <div className={styles.login}>
      <div className={styles.header}>
        <span className={styles['header-logo']}>
          <i className={classNames('iconfont icon-react', styles['icon-react'])}></i>
          <i className={classNames('iconfont icon-icon-test', styles['icon-icon-test'])}></i>
          <i className={classNames('iconfont icon-typescript', styles['icon-typescript'])}></i>
        </span>
        <span className={styles['header-title']}>在线考勤系统</span>
      </div>
      <div className={styles.desc}>
        零基础从入门到进阶,系统掌握前端三大热门技术(Vue、React、TypeScript)
      </div>
      <Form
        name="basic"
        labelCol={
   
   { span: 6 }}
        wrapperCol={
   
   { span: 18 }}
        initialValues={
   
   { remember: true }}
        onFinish={onFinish}
        onFinishFailed={onFinishFailed}
        autoComplete="off"
        className={styles.main}
        //将属性绑定在一起
        form={form}
      >
        <Form.Item
          label="邮箱"
          name="email"
          rules={[
            { required: true, message: '请输入邮箱' },
            { type: 'email', message: '请输入正确的邮箱地址' }
          ]}
        >
          <Input placeholder="请输入邮箱" />
        </Form.Item>

        <Form.Item
          label="密码"
          name="pass"
          rules={[{ required: true, message: '请输入密码' }]}
        >
          <Input.Password placeholder="请输入密码" visibilityToggle={false} />
        </Form.Item>
        <Form.Item wrapperCol={
   
   { offset: 6, span: 18 }}>
          <Button type="primary" htmlType="submit">
            登录
          </Button>
        </Form.Item>
      </Form>
      <div className={styles.users}>
        <Row gutter={20}>
          {
            testUsers.map((v)=> (
              <Col key={v.email} span={12}>
                <h3>
                  测试账号,<Button onClick={autoLogin({email: v.email, pass: v.pass})}>一键登录</Button>
                </h3>
                <p>邮箱:{v.email}</p>
                <p>密码:{v.pass}</p>
              </Col>
            ))
          }
        </Row>
      </div>
    </div>
  )
}

请求头发送token和获取用户信息

请求拦截器:config.headers.authorization
用户信息:/users/infos
响应拦截器:统一错误处理


拆分首页头部组件和侧边栏组件

头部组件:<HomeHeader>
侧边栏组件: <HomeAside>
 

猜你喜欢

转载自blog.csdn.net/qq_28838891/article/details/129124014