react小项目知识总结

270 阅读6分钟

本文已参与[新人创作礼]活动,一起开启掘金创作之路

本文是小白初步接触react做的小项目的主要知识点及bug处理。部分没什么特别突出特点的功能就没有列出。此文仅用于巩固自身,不喜勿喷。

本文使用包:

image.png

css作用域

js

const Login = () => {
  return (
    <div className={styles.root}>
      <Card className="login-container">
        <img className="login-logo" src={logo} alt="" />
        {/* 登录表单 */}
      </Card>
    </div>
  )
}

src/pages/Login/index.module.scss

.root {
  width: 100%;
  height: 100%;
  position: absolute;
  left: 0;
  top: 0;
  background: center/cover url(~@/assets/login.png);
  :global {
    .login-logo {
    ...
    }
    }
 }

配置redux

下包

yarn add redux react-redux redux-thunk redux-devtools-extension

目录:

├─store                # redux根目录
│  ├─actionTypes.js    # 
│  ├─reducers          # 
│  │  ├─index.js       # 
│  │  └─login.js       #  
│  ├─actions           # 
│  │  └─login.js       # 
│	 └─index.js    # 创建store并导出

store/index.js

使用reduxjs

import { legacy_createStore,applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer  from './reducers/index'
import { composeWithDevTools } from "redux-devtools-extension";
//根据开发环境判断是否开启调试工具
let middlewares
if(process.env.NODE_ENV==='production'){
  middlewares=applyMiddleware(thunk)
}else{
//开发环境即挂载thunk中间件也挂载调试工具
  middlewares=composeWithDevTools(applyMiddleware(thunk))
}
const store = legacy_createStore(rootReducer,middlewares)
export default store

使用reduxjs/toolkit,这里没添加中间件

import reducer from './reducer'
import {  configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
  reducer:{
    counter:reducer,
  }
})
export default store

store/reducers/index.js

import login from './login'
import user from './user'
import { combineReducers } from 'redux'
import channels from './channels.js'
import article from './article'
const rootReducer = combineReducers({
  login,
  user,
  channels,
  article
})
export default rootReducer 

举例:store/reducers/login.js

const initValue = {
  token: ''
}
export default function login(state = initValue, action) {
  return state
}

src/index.js

import React from "react";
import ReactDOM from "react-dom/client";
import "@/index.scss";
import App from "@/App";
import { Provider } from "react-redux";
import store from '@/store'
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />  
</Provider>
);

Redux-action实现登录

思路

  1. 用户在组件中 dispatch 异步 action

  2. actions 提供封装好的异步 action(涉及actionType):

    1. 在action中发ajax
    2. dispatch 普通 action 来发起状态更新
  3. reducers 处理状态更新(涉及actionType)

1.业务,pages/Login/Login.jsx

import { login } from '@/store/actions/login.js'
const dispatch = useDispatch()
// 当表单校验通过,就会执行onFinished,并且会携带数据
const onFinish = () => {
  const onFinish = (values) => {
    console.log('Success:', values)
    // 发送请求,进行登录
    dispatch(login(values))
  }
}

2.异步,store/actions/login.js

import request from '@/utils/request'
import { LOGIN } from '../actionType'
export const login = (payload) => {
  return async (dispatch) => {
    const res = await request({
      method: 'post',
      url: '/authorizations',
      data: payload
    })
    dispatch({
      type: LOGIN,
      payload: res.data.token
    })
  }
}

3.统一存储action,store/actionTypes.js

export const LOGIN = 'LOGIN'

4. reducers,store/reducers/login.js

import { LOGIN } from '../constants'

const initValue = {
  token: ''
}
export default function login(state = initValue, action) {
  if (action.type === LOGIN) {
    return {
      ...state,
      token: action.payload
    }
  }
  return state
}

redux-thunk发请求路线图

image.png

token存储到本地

能够统一处理 token 的持久化相关操作

设置持久处理工具

utils/storage.js分别提供 getToken/setToken/removeToken/hasToken四个工具函数并导出

// 消除   魔法字符串
const TOKEN_KEY = 'itcast_geek_h5'

export function getToken () {
  return JSON.parse(localStorage.getItem(TOKEN_KEY) || '{}')
}

export function setToken (val) {
  localStorage.setItem(TOKEN_KEY, JSON.stringify(val))
}

export function removeToken () {
  localStorage.removeItem(TOKEN_KEY)
}

export function hasToken () {
  return !!getToken().token
}
}

登录获取用户信息及存储token

src\store\actions\login.js

import {LOGIN} from '../actionTypes'
import  response from '@/utils/request.js'
import { setToken } from '@/utils/storage'
export const login=(payload)=>{
  return async(dispatch)=>{
    const res =await  response({
      method:'post',
      url:'/authorizations',
      data:payload
    })
    if (res.message === 'OK') {
    setToken(res.data)
    // console.log(res);
    dispatch ({type:LOGIN,payload:res.data})      
    }

  }
}

存储信息到仓库中,及判断是否已持久存储token

src\store\reducers\login.js

import {LOGIN,LOGOUT} from '../actionTypes'
import { getToken } from '@/utils/storage'
const initValue = getToken()||{
  token: '123'
}
export default function login(state = initValue, action) {
  // console.log(action);
  switch (action.type) {
    case LOGIN:
      
      return {...state,...action.payload} 
      case LOGOUT:
      
      return initValue
      case 'loginUser':
      
        return {...state,...action.payload} 
    default:
      return state
  }
}

loading登录消息提示

  const [load,setLoad]=useState(false)
  。。。
  let onFinish=(values)=>{
  setLoad(true)
   try{  
    dispatch(login(values))
    // values会是一个对象,键名就是表单项的name,键值就是表单元素的内容
    // console.log(values);
    message.success('登陆成功',1,()=>{
    //提示信息,时间,回调函数
    setLoad(false)
    dispatch(getUserInfo())
    //  props.history.push('/layout') 
     props.history.push((history.location.state&&history.location.state.from)||'/layout') 
    }) 
  // history.push('/layout')
   }catch{
    message.error('登陆失败')
   }

  }
  。。。
{/* htmlType原生,检测所有带有name的表单属性 */}
<Button loading={load} type="primary" htmlType="submit" size='large' block>
 登录
</Button>

重定向

          //初始设置默认二级
        <Redirect exact from='/' to='/login'></Redirect>
          //主页设置默认二级
        <Redirect exact from="/layout" to="/layout/home"></Redirect>

自定义history对象实现跳转

新建src\utils\history.js文件

// 自定义history对象
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
export default history

引入路由

image.png

使用src\utils\request.js

import history from '@/utils/history'
/* history====可以获取非组建中获取路由对象信息 */
 history.push('/login')

Menu控制左侧菜单栏

const items = [
  {
    label: <Link to="/layout/home">数据概览</Link>,
    key: '/layout/home',
    icon: <HomeOutlined />,
  },
  {
    label: <Link to="/layout/article">内容管理</Link>,
    key: '/layout/article',
    icon: <DiffOutlined />,
  },
  {
    label: <Link to="/layout/publish">发布文章</Link>,
    icon: <EditOutlined />,
    key: '/layout/publish',
  },
]
<Menu
onClick={onClick}
defaultSelectedKeys={[history.location.pathname]}
//初始选中的菜单项 key 数组
selectedKeys={[pathname]}
//该属性可解决除了点击左侧菜单跳转到其他页面高亮不跟随问题
theme="dark"
//主题颜色
mode="vertical"
//菜单类型,垂直
items={items}
//菜单内容
 />

配置请求拦截器

utils/request.js

// 添加请求拦截器
instance.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    const token = getToken()
    if (token) {
      // 添加token
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)

处理请求异常token失效

utils/request.js

// 添加响应拦截器
instance.interceptors.response.use(
  function (response) {
    // 对响应数据做点什么
    return response
  },
  function (error) {
    console.log('error')
    console.dir(error)
    if (!error.response) {
      message.error('网络异常')
    } else if (error.response.status === 401) {
      message.error('身份过期,请重新登录')
      // 1. 清空信息
      store.dispatch(logout())
      // 2. 跳转页面 - login
      //hash
      //window.location.href = '/#/login'
      //但是页面会刷新,用户体验不好
      window.location.href = '/login'
      // useHistory
      // history.push('/login')
    }

    return Promise.reject(error)
  }
)

登录权限管理

初始版本

          <Route  path="/layout" 
                render={()=>{ 
                  return token?<Layout></Layout>:<Redirect  to="/login"></Redirect>
                  }}></Route>  

之所以下面提出抽离是因为初始版本,如果每个都需要判断token的话过于麻烦了,就封装了一个组件专门处理,使用时仅需携带路径名与组件名即可

抽离版本

app.js

import AuthRoute from "./utils/authRoute";
<AuthRoute path="/Layout" component={Layout}></AuthRoute>

authRoute.js

import {Route , Redirect} from 'react-router'
import { hasToken } from './storage'
export default function AuthRoute(props){
   const Com=props.component
   const token=hasToken()
   return (
        <Route  path="/layout" 
   render={()=>{ 
     return token?<Com></Com>:<Redirect  to="/login"></Redirect>
     }}></Route>  
   )
}
  //  <Route  path="/layout" 
  //  render={()=>{ 
  //    return token?<Layout></Layout>:<Redirect  to="/login"></Redirect>
  //    }}></Route>  

记录退出前页面登录后跳转到该页面

layout记录

image.png

login登录

image.png

给内容添加滚轮

        <Content
              className="site-layout-background"
              style={{
                padding: 24,
                margin: 0,
                minHeight: 280,
                overflow:'auto'
              }} >

表单初始化

image.png

筛选表单

1.表单绑定数据

image.png

image.png

2.点击事件种获取表单数据

给表单的筛选按钮注册事件

image.png

const onFinish=(value)=>{
  // console.log(value);
}

3.创建对象用于存储需要提交的变量名来保存表单数据

const onFinish=(value)=>{
  // console.log(value);
  const params={}
  params.status=value.status
  params.channel_id=value.channel_id
  if(value.date){
    params.begin_paubdate=value.date[0]
    .startOf('day')
    .format('YYYY-MM-DD HH:mm:ss')
    params.end_paubdate=value.date[1]
    .endOf('day')
    .format('YYYY-MM-DD HH:mm:ss')
  }
    // console.log(params);
}

4.发送请求重新获取表单数据

dispatch(getArticleList(params))

筛选之后点击分页获取数据不对

分析

筛选条件点击之后获取到数据后再点击分页,导致重新走了一遍函数组件,导致重新定义了一次条件{},使之前的条件清空了,然后再发送请求获取的就是原本的所有数据的当前页数据

解决

使用useRef注册变量,然后使用变量.current储存筛选条件

具体实施

  • 删除储存条件的params={}

  • 给每个事件添加 const params = ref.current

在表单中封装组件

1.创建组件

src\components\Channel.js

import React from 'react'
import { Select } from 'antd'
import {useChannels} from '@/hooks/useChannel'
export default function Channel(props) {
const channels=useChannels()
  return (
    <Select
    {...props}
      style={{ width: 200 }}
      allowClear
      placeholder="请选择频道"
      
    >
      {channels.map((item) => (
        <Select.Option  value={item.id} key={item.id}>
          {item.name}
        </Select.Option>
      ))}
    </Select>
  )
}

2.在表单中使用组件

注意

image.png 可以使用props解构出相应数据,也可以使用{...props}

引入并使用

import Channel from '@/components/Channel'

 <Form.Item label="频道" name='channel_id'>
 <Channel></Channel>
</Form.Item>

上传图片(需要重点掌握)

基本使用

<Form.Item wrapperCol={{ offset: 4, span: 20 }}>
  {/* 
    action: 上传的地址
    name: 上传的文件的名字 默认file
    fileList: 显示控制上传的图片, 做回填 */}
  <Upload listType="picture-card" fileList={fileList}>
    <PlusOutlined />
  </Upload>
</Form.Item>


const [fileList, setFileList] = useState([])

form.item的数据可以自行收集,但是,Upload组件中的数据,Form表单不会自动收集,需要我们自行处理

实现上传

  •  为 Upload 组件添加 action 属性和name,指定封面图片上传接口地址和参数名
    
  •  创建状态 fileList 存储已上传封面图片地址,并设置为 Upload 组件的 fileList 属性值
    
  •  为 Upload 添加 onChange 属性,监听封面图片上传、删除等操作
    
  •  在 change 事件中拿到当前图片数据,并存储到状态 fileList 中
    

限制数量

  • 自己控制type属性

  • 根据type属性控制上传的数量(Upload组件有个maxCount属性)

选项与图片组件间联动

  • 创建 ref 对象,用来存储已上传图片

  • 如果是单图,只展示第一张图片

  • 如果是三图,展示所有图片

图片效验

 rules={[
    {
      validator(_, value) {
        if (fileList.length !== value) {
          return Promise.reject(new Error(`请上传${value}张图片`))
        } else {
          return Promise.resolve()
        }
      }
    }
  ]}

核心代码

  // 控制type属性
const [type, setType] = useState(1)
  // 上传图片数据
  const [fileList, setFileList] = useState([
  ])
    // 储存图片
    const refFileList=useRef(fileList)
    const formRef=useRef(null)
    // 上传图片
  const onChange1=(value)=>{
    // console.log(value);
    // 存入数据
    refFileList.current=value.fileList
    // console.log(value.fileList,'ref');
    // 显示图片
    setFileList(value.fileList)
   //手动触发表单项的校验
    formRef.current.validateFields(['type'])
  }
<Radio.Group 
    value={type}
    action={`${BASEURL}upload`}
    onChange={e=>{
              // console.log(e.target.value);
        setType(e.target.value)
        if(refFileList) setFileList(refFileList.current.slice(0,e.target.value))
 }}>
    <Radio value={1}>单图</Radio>
    <Radio value={3}>三图</Radio>
    <Radio value={0}>无图</Radio>
</Radio.Group>
</Form.Item>
 <Form.Item  wrapperCol={{offset:4 ,span: 22 }}>
 {/* 
    action: 上传的地址
    name: 上传的文件的名字 默认file
    fileList: 显示控制上传的图片, 做回填 */}
<Upload 
    action={`${BASEURL}upload`}
    //限制图片数量
    maxCount={type}
    name="image"
    onChange={onChange1}
    listType="picture-card" fileList={fileList} className='upload'>
    {fileList.length < type&& <PlusOutlined />}
</Upload>
</Form.Item>

表单上传

上传事件

注册 image.png 绑定 image.png 触发 image.png

存入草稿

点击草稿事件

  // 存入草稿
  const addDraft=async()=>{
  //手动触发表单项的校验-----表单.validateFields(['属性名'])
    const form=await formRef.current.validateFields()
    add(form,true)
  }

与表单传统一处理获取数据

注意图片的获取

    const add =async(value,draft)=>{
       const cover = {
      type:value.type,
      images:fileList.map(item=>item.response.data.url)
    }
  const data={
    title:value.title,
    content:value.content,
    channel_id:value.channel_id,
    cover
  }   
  try {
    await dispatch(saveArticle(data,draft))

    message.success(draft?'存入草稿':'发布成功', 1, () => {
      history.push('/layout/article')
    })
  } catch(error) {
    message.error("操作失败");
  }
  console.log(data,'dat');
  console.log(fileList,'fileList');  
    }

代码优化

将数据与提示消息分离成两个函数,便于后期修改

实现编辑效果

1.获取id

publish.js

import { useHistory,useParams } from 'react-router-dom'
const {id}=useParams()

2.使用对象存储并传给form

  •  使用useEffect监听id
    
  •  注意useEffect第一个参数不可以直接放异步函数
    
  •  发请求
    
  •  使用form绑定的useRefform进行传值,动态给表单设置数据,需要调用表单的实例方法:setFieldsValue({属性名:属性值,...})
    
  •   因为获取到的图片无法直接渲染,需要先将其存入上传图片的数组的对象中
    

核心代码

  // 编辑
  useEffect(()=>{
    const getInfo=async(id)=>{
      if(id){
    const res=await dispatch(getArticleInfo(id) ) 
    // console.log(res,'res');
    const useInfo={
      type:res.cover.type,
      content:res.content,
      channel_id:res.channel_id,
      title:res.title
    }
    // console.log(useInfo,'useInfo');

    formRef.current.setFieldsValue(useInfo)
    // console.log(res,'rs');
    //点击编辑时如果图片为3图,删除1个后type为默认,需要重新赋值一边,否则type不对导致fileList长度出错
    setType(res.cover.type)
    setFileList(res.cover.images.map((img) => ({ url: img }))) 
    }
    }
    getInfo(id)
  },[id,dispatch])

3.提交

获取数据时需要注意编辑过的图片与未编辑过的路径不同

  const onFinish=(value)=>{
    // console.log(value,'value');
    add(value,false) }
    const buildAdd =(value)=>{
      console.log(fileList);
      // 后端解析后处理导致点击编辑获取数据方式不同
    const images = fileList.map((item) => {
      if (item.url) {
        return item.url
      } else {
        return item.response.data.url
      }
    })
   const  data = {
      ...value,
      cover: {
        type: type,
        images: images
      }
    }
    return data
    }

提示信息需要与上传分开

  const add =async(value,draft)=>{
      const data = buildAdd(value)
      
      if(id){
        data.id=id
        try {
          await dispatch(saveArticle(data,draft))
       
          message.success(draft?'修改草稿成功':'修改文章成功', 1, () => {
            history.push('/layout/article')
          })
           } catch(error) {
          message.error("修改失败");
        }
      }else{

    上传
   }

发布文章菜单高亮处理

layout.js

1.判断路径来源头部

使用内置api

image.png 注意:如果报错可能是信息没获取到,可使用js可选链操作符( 父?.子)如果没有父则返回undefined而不会因为找不到父亲而导致无法进行子的查找而报错

2.需要将选中状态改为pathname

image.png

发布文章编辑文章切换有数据残留

layout.js

key用于相同组件间的辨别,key值只要不一样具体值无所谓,只是为了辨别不同路由调用相同组件

 <Route path="/layout/publish"  key="add" exact component={Publish}></Route>
<Route path="/layout/publish/:id" key='edit' component={Publish} />