知乎日报项目下--React

利用redux存储数据

在baseReducer问中,对派发标识进行处理,负责用户数据的初始化和删除

  switch (action.type) { // action={type:'',info:''}格式
    case TYPE.BASE_INFO:
      state.info = action.info //初始化或删除操作
      break;
  }

在baseAction中实现派发的具体代码,需要使用到redux-thunk中间件

import * as TYPE from '../actionTypes'
import { queryUserInfo } from '../../api/userApi'

export const baseAction = {
  /**
   * 获取用户信息
   */
  queryUserInfoAsync() {
    return async (dispatch) => {
      let info = null
      try {
        let { data: userData } = await queryUserInfo()
        let { code, data } = userData
        if (+code === 0) {
          info = data
        }
      } catch {
        console.log('初始化获取用户信息失败');
      }
      dispatch({
        type: TYPE.BASE_INFO,
        info
      })
    }
  },

  /**
   * 删除用户信息
   */
  removeUserInfo() {
    // 普通处理直接return,会被包装为dispatch格式
    return {
      type: TYPE.BASE_INFO,
      info: null //代表删除
    }
  }
}

然后在Login组件中引入react-redux提供的connect方法使用store中的数据,最终props的内容如图

// 该组件不需要获取数据显示,因此mapStateToProps可以不写
export default connect(null, action.base)(Login) 

知乎日报项目下--React_第1张图片
解构出需要的内容

let { navigate, queryUserInfoAsync } = props

//存储token后,获取用户信息
_.storage.set('tk', res.data.token) 
await queryUserInfoAsync() //异步处理
navigate('/')

控制台成功输出,state中获取到了用户数据
知乎日报项目下--React_第2张图片

在登录成功后,需要处理跳转的逻辑有几种情况。

  1. 页面一加载,手动输入地址跳转,那么在登录成功后可以跳转到主页或上一页
  2. 假设在首页/,像很多页面,需要登录后才能放行的,需要处理。假设没有登录信息,那么点击个人中心,从首页/跳转到登录页/login,然后在登录页成功后处理如何跳转问题,这个时候如果采样跳转上一级那么就会返回的主页,这不符合正常操作,应该是跳转到个人中心才行。也就是说,需要再登录页知道登录成功后需要调整到哪里。因此我们可以在首页/跳转登录页的时候携带参数/login?to=/person,每次登录成功的时候,根据to属性决定跳转到哪里,如果没有就默认跳转首页也行。但是还需要处理细节问题,就是路由跳转模式是push还是replace,因为登录页跳转到个人中心后,用户就不能通过历史返回箭头回退到登录页了,因此在登录页跳转采样replac方式
  3. 在知乎日报中,需要额外处理收藏按钮的跳转逻辑,点击按钮的时候判断是否登录,如果登录了就可以操作收藏按钮,如果没有就跳转登录页,需要将当前新闻的id传递过去,即/login?to=/detail/123123,也需要在登录页采用replace的方式处理跳转,让用户不能使用历史回退箭头跳转到登录页。在detail组件中采样replace跳转
    知乎日报项目下--React_第3张图片

在登录页的props属性中结构出地址中的search参数,修改原先登录的跳转代码

  let {... ,urlSearchParams } = props //urlSearchParams 基于useSearchParams函数
  // to==> /login?to=/person
  let to = urlSearchParams.get('to')
  to ? navigate(to, { replace: true }) : navigate(-1)

然后需要单独处理登录页的返回功能,该返回是通过二次封装的NavBar实现。在其他组件中,都是从来哪里,点击该返回按钮就回哪里去。但是需要额外处理详情页到登录页,该返回按钮的逻辑
在这里插入图片描述
知乎日报项目下--React_第4张图片

// NavBarAgain组件代码
  let navigate = useNavigate()
  let location = useLocation()
  let { pathname } = location
  let [searchParams] = useSearchParams()
    const handleBack = () => {
    //处理返回逻辑
    let to = searchParams.get('to') //to==> /login?to=/detail/1231
    if (pathname === '/login' && /^\/detail\/\d+/.test(to)) {
      navigate(to, { replace: true })
      return
    }
    navigate(-1)
  }

知乎日报项目下--React_第5张图片

处理登录态校验

针对/person,/store,/update这三个页面需要进行登录校验,那么登录校验放在哪里进行。需要在每次路由跳转前,进行登录校验,因此需要在我们的路由index文件中处理,并且是在手写的Element函数组件中处理,因为该组件就是实际返回的组件信息,在返回组件前进行处理。

  // 登录态校验 /person,/store,/update
  let { pathname: path } = location // path存放当前路径
  let checkArr = ['/person', '/store', '/update'] //需要登录校验的路径

其次需要根据redux中是否有用户信息决定是否校验,如果存在用户信息就不需要进行校验规则,代表已经登录了且无刷新redux的情况,直接跳转渲染即可。
在路由中获取redux的信息,只能使用最原始的方法,因为路由文件不是被react-redux控制的,无法使用便捷方法。

import { store } from '../store/index'
import { action } from '../store/actions/index'

let { info } = store.getState().base //当前用户信息

代码如下

const Element = async (props) => {
  let { component: Component, meta } = props
  let location = useLocation()
  let navigate = useNavigate()
  let param = useParams()
  let [urlSearchParams] = useSearchParams()
  // 设置对应路由的页面标题
  let { title = '知乎日报' } = meta || {}//防止meta没传,或title没有
  document.title = title

  // 登录态校验 /person,/store,/update
  let { pathname: path } = location // path存放当前路径
  let checkArr = ['/personal', '/store', '/update'] //需要登录校验的路径
  let { info } = store.getState().base //当前用户信息
  if (!info && checkArr.includes(path)) {
    // 进行行为派发,根据当前携带的token判断是否是登录了,但是刷新了的情况,
    //解构出获取用户信息的action,但是该函数非受react-redux控制的,不会实现自动派发(需要使用redux-promise)
    let infoAction = await action.base.queryUserInfoAsync() // {type:TYPE.BASE_INFO,info}
    let { info } = infoAction //刚派发完获取的用户信息info
    if (!info) {
      // 手动派发获取的用户信息失败了为null。代表当前获取信息的时候没有携带token,即未登录状态
      Toast.show({
        icon: 'fail',
        content: '请先登录!'
      })
      // 跳转登录页
        return <Navigate to={{
          pathname: '/login',
          search: `?to=${path}`
        }}></Navigate>
    }
    //已登录, 修改redux的用户信息
    store.dispatch(infoAction)
  }

  return <Component
    location={location}
    navigate={navigate}
    param={param}
    urlSearchParams={urlSearchParams} />
}


element={<Element meta={meta} component={component} />}

在这里插入图片描述
但是这么写就会出现问题,因为Element 函数组件使用了async处理了,因此默认返回一个Pormise状态,而在element中渲染是不被允许的。

如果采用这种方式,外部不能使用,那么在内部使用自定义函数实现异步处理,会如何。结果就是任何页面都无法实现登录校验功能,都是直接进入页面渲染了。

  (async () => {
    if (!info && checkArr.includes(path)) {
      // 进行行为派发,根据当前携带的token判断是否是登录了,但是刷新了的情况,
      //解构出获取用户信息的action,但是该函数非受react-redux控制的,因此调用只会返回派发对象,尽管里面有dispatch
      let infoAction = await action.base.queryUserInfoAsync() // {type:TYPE.BASE_INFO,info}
      let { info } = infoAction //刚派发完获取的用户信息info
      if (!info) {
        // 手动派发获取的用户信息失败了为null。代表当前获取信息的时候没有携带token,即未登录状态
        Toast.show({
          icon: 'fail',
          content: '请先登录!'
        })
        return <Navigate to={{
          pathname: '/login',
          search: `?to=${path}`
        }}></Navigate>
      }
      //已登录, 修改redux的用户信息
      store.dispatch(infoAction)
    }
  })()

知乎日报项目下--React_第6张图片
这个时候发现,无论如何在内部使用async和await处理都会有一点问题,那么就需要修改代码了。
这个时候换个角度思考,在一个函数组件中,什么地方可以使用异步操作,可以联想到useEffect

创建一个isShow变量,该变量如果为true,代表不需要登录校验。并且isShow初始状态值不能直接写死,需要动态根据当前页面确定,比如/person,/store,/update这写页面就为false。因此写一个函数处理每次isShow的值,不使用useState处理。

let [isShow, setIsShow] = useState(false) //isShow为真代表不需要登录校验

根据isShow变量的值,动态决定返回的Element函数组件的内容。如果需要登录校验,就返回遮罩蒙层,代表正在校验,后续根据isShow的值改变,渲染最终的页面。那么核心做法该如何处理。

  return <>
    {
      isShow ? <Component
        location={location}
        navigate={navigate}
        param={param}
        urlSearchParams={urlSearchParams} /> :
        <Mask visible={true} opacity='thick' >
          <div className="maskLoadingContent">
            <DotLoading></DotLoading>加载中
          </div>
        </Mask>
    }
  </>
const isCheckLogin = (path) => {
  let { info } = store.getState().base; //当前用户信息
  // 登录态校验 /person,/store,/update
  let checkArr = ['/personal', '/store', '/update'] //需要登录校验的路径
  // redux有值,代表登录了
  return !info && checkArr.includes(path)
}

const Element = (props) => {
  let { component: Component, meta } = props
  let location = useLocation()
  let navigate = useNavigate()
  let param = useParams()
  let [urlSearchParams] = useSearchParams()

  let isShow = !isCheckLogin(location.pathname)  //注意取反,isShow为真代表不需要登录校验
  let [_, setRandom] = useState(0) //更新视图的方法

  // 设置对应路由的页面标题
  let { title = '知乎日报' } = meta || {}//防止meta没传,或title没有
  document.title = title

  useEffect(() => {
    if (isShow) return //redux有值,代表登录了,无需校验
    // 处理异步
    (async () => {
      let infoAction = await action.base.queryUserInfoAsync() //{type: 'BASE_INFO', info: {…}} ,thunk中间件返回的是函数,函数未调用,无法直接处理
      let { info } = infoAction //刚派发完获取的用户信息info
      if (!info) {
        // 手动派发获取的用户信息失败了为null。代表当前获取信息的时候没有携带token,即未登录状态
        Toast.show({
          icon: 'fail',
          content: '请先登录!'
        })
        // 实现跳转
        navigate({
          pathname: '/login',
          search: `?to=${location.pathname}`
        })
        return
      }
      //已登录, 修改redux的用户信息
      store.dispatch(infoAction) //这里需要处理完毕后,让视图重新执行,重新执行
      setRandom(+new Date())// 重新获取store中的用户信息,这个时候一定是有的,因此跳过登录校验
    })()
  }) //初始化即更新都会执行

  return ....
}

同时将原先基于redux-thunk实现的异步处理,修改为redux-promise版本的。因为需要使用其返回值

  async queryUserInfoAsync() {
    let info = null
    try {
      let { data: userData } = await queryUserInfo()
      let { code, data } = userData
      if (+code === 0) {
        info = data
      }
    } catch {
      console.log('初始化获取用户信息失败');
    }
    return {
      type: TYPE.BASE_INFO,
      info
    }
  },

知乎日报项目下--React_第7张图片

以下是示例图
无token的情况,不需要进行登录校验的页面可以直接进入

知乎日报项目下--React_第8张图片
知乎日报项目下--React_第9张图片
登录后,从跳转到刚才需要登录的页面去,贴合了用户的使用。
知乎日报项目下--React_第10张图片
当有token页面刷新的时候,会自动获取用户信息更新到redux中,然后不再进入登录页。

处理的细节点,如果首页跳转到personal页面,但是没有登录,会从personal页面进入登录页,历史记录中会多一条personal,这需要清除,
知乎日报项目下--React_第11张图片

        navigate({
          pathname: '/login',
          search: `?to=${location.pathname}`
        }, { replace: true })

搭建其余几个页面

知乎日报项目下--React_第12张图片
知乎日报项目下--React_第13张图片
知乎日报项目下--React_第14张图片
知乎日报项目下--React_第15张图片

首页头像显示和跳转功能

首页顶部头像,首先会判断redux中是否有值,如果没有值就会派发一次,请求用户数据,如果当前没有登录,没token的情况下,则获取的数据会更新redux失败,则redux中存储的用户信息就为null。以此判断显示头像或默认。
在HomeHeader组件中代码如下

import { connect } from 'react-redux'
import { action } from '../../store/actions/index'
...
export default connect(state => state.base, action.base)(HomeHeader) // 基于属性传递给组件使用
let { ..., info, queryUserInfoAsync } = props
let navigate = useNavigate()

  // 组件初次渲染获取用户头像
  useEffect(() => {
    if (!info) { // 获取用户信息可能成功或不成功
      // 这里不需要执行其他逻辑,不写await也可以
      queryUserInfoAsync() // 基于react-redux,会实现自动派发更新状态,自动更新视图
    }
  }, [])

    <div className="avatar" onClick={() => navigate('/personal')}>
      <img src={!info ? timgImageUrl : info.pic} alt="" />
    </div>

详情页收藏按钮逻辑

详情页的收藏按钮也需要判断用户信息是否存在,因此也可以在进入详情页的时候就进行一次用户数据获取,其次还需要获取收藏列表的状态信息。因此在该组件中需要用到两个状态的信息和派发。

export default connect(state => state, dispatch => {
  return { //返回对象中的每一个属性都必须是函数格式
    async queryUserInfoAsync() {
      let res = await action.base.queryUserInfoAsync() // {type:'',info:''}
      dispatch(res)
    },
    removeUserInfo() {
      dispatch(action.base.removeUserInfo())
    }
    // 因为store中暂无数据,所以不写
  }
})(Detail)

编写一段代码测试,查看用户信息是否成功获取

  console.log(props);
  useEffect(() => {
    (async () => {
      await props.queryUserInfoAsync()
    })()
  }, [])

知乎日报项目下--React_第16张图片

但是这样子写完会发现在dispatch中的槽点,多个状态需要派发解决就需要手写多个函数。那么如何在多个状态之间采用简化的形式书写。

直接修改如下,只要确保mapDispatchToProps中最后处理的属性是一个函数即可

export default connect(state => state, { ...action.base, ...action.store })(Detail)
// 如果是一个状态中的派发标识,那么就是 action.base 其本质就是一个对象 {queryUserInfoAsync,removeUserInfo},每一个属性均是是函数

然后点击收藏按钮的时候,代码如下

  // 收藏按钮
  let { location, queryUserInfoAsync, base,navigate } = props

  // 判断是否又登录信息
  useEffect(() => {
    if (!base.info) {
      queryUserInfoAsync()
    }
  }, [])

  // 点击收藏的逻辑
  const handleStore = () => {
    if (!base.info) {
      Toast.show({
        icon: 'fail',
        content: '请先登录!'
      })
      navigate(`/login?to=${location.pathname}`, { replace: true }) //注意跳转模式
    }
  }

编写跟收藏相关的接口

// 收藏新闻
export const store = (newsId) => { //新闻id
  return http({
    url: '/store',
    method: 'POST',
    headers: {
      "Content-Type": "application/x-www-form-urlencoded"
    },
    data: {
      newsId
    }
  })
}

// 取消某项收藏
export const removeStoreItem = (id) => { //收藏的id
  return http({
    url: '/store_remove',
    method: 'GET',
    params: {
      id
    }
  })
}

// 获取收藏列表
export const getStoreList = () => {
  return http({
    url: '/store_list'
  })
}

搭建完善收藏相关的store

// 收藏   actionType
export const STORE_LIST = `STORE_LIST`
export const STORE_REMOVE = 'STORE_REMOVE' //删除某项收藏
// storeReducer中代码  action =>{ type:'',list或id:''}  list也可能为id
  switch (action.type) {
    case TYPE.STORE_LIST:
      state.list = action.list //初始化或删除
      break;
    case TYPE.STORE_REMOVE: //删除某项,根据收藏id
            if (Array.isArray(state.list)) {
                state.list = state.list.filter(item => {
                    return +item.id !== +action.id;
                });
            }
      break;
  }
// storeAction中的代码
export const storeAction = {
  async queryStoreListAsync() {
    let list = null;
    try {
      let { data: resData } = await getStoreList()
      let { code, data } = resData
      if (+code === 0) {
        list = data
      }
    } catch { }
    return {
      type: TYPE.STORE_LIST,
      list
    }
  },
  clearStoreList() {
    return {
      type: TYPE.STORE_LIST, //代表清空
      list: null
    }
  },
  removeStoreListItemById(id) {
    return {
      type: TYPE.STORE_REMOVE,
      id
    }
  }
}

然后在之前的函数中添加收藏代码的逻辑

  // 收藏按钮
  let { location, queryUserInfoAsync, base, store, queryStoreListAsync, removeStoreListItemById } = props

  // 判断是否登录,同时判断是否可以获取收藏列表
  useEffect(() => {
    (async () => {
      let userInfo = null //存放登录信息,和store中的用户信息保持一致
      // 校验登录
      if (!base.info) {
        let { info } = await queryUserInfoAsync() //{type: 'BASE_INFO', info: {…}}
        userInfo = info //info是服务器获取的用户信息
      }
      // 用户登录了,并且收藏列表为空才需要获取收藏列表信息
      if (userInfo && !store.list) {
        queryStoreListAsync()
      }
    })()
  }, [])

设置收藏标志位,绑定样式

  // 收藏标志,依赖于收藏列表和当前新闻id,标志用于绑定样式
  const isStore = useMemo(() => {
    if (!store.list) return false // 没有收藏列表,代表当前新闻未被收藏(未登录的时候也为空,所有新闻的样式均为未收藏)
    return store.list.some(item => {
      return +item.news.id === +param.id //当前页面的新闻存在收藏列表
    })
  }, [store.list, param.id])
className={isStore ? 'active' : ''}

在点击收藏按钮处理的逻辑中继续添加如下代码

const handleStore = async () => {
	。。。。。
    // 已登录,收藏或移除收藏
    if (isStore) { //处理点击从收藏到移除的过程
      // 获取当前的收藏id,根据当前新闻id到收藏列表匹配,匹配成功后拿到给新闻id绑定的收藏id
      let storeItem = store.list.find(item => {
        return +item.news.id === +param.id
      })
      if (!storeItem) return //防止出错
      let res = await removeStoreItemAPI(storeItem.id)
      if (+res.data.code !== 0) {
        Toast.show({
          icon: 'fail',
          content: '移除失败!'
        })
        return
      }

      Toast.show({
        icon: 'success',
        content: '移除成功!'
      })

      // 同步redux
      removeStoreListItemById(storeItem.id)
      return
    } else {
      // 处理未收藏到收藏的过程
      try {
        let res = await storeAPI(param.id) //名字冲突,接口重命名
        if (+res.data.code !== 0) {
          Toast.show({
            icon: 'fail',
            content: '收藏失败!'
          })
          return
        }

        Toast.show({
          icon: 'success',
          content: '收藏成功!'
        })
        // 发送请求同步redux
        queryStoreListAsync()
      } catch {
        console.log('收藏失败');
      }
    }
 }

个人中心

退出登录

// 只获取action中某个方法使用
let { navigate, removeUserInfo, clearStoreList, info } = props

export default connect(state => state.base, {
  removeUserInfo: action.base.removeUserInfo,
  clearStoreList: action.store.clearStoreList
})(Personal)
  const handleLogout = () => {
    // 退出登录需要将redux中所以板块信息清空,同时清空本地token,跳转登录页
    removeUserInfo()
    clearStoreList()
    _.storage.remove('tk')
    navigate('/login?to=/personal', { replace: true })
  }

<div className="logout" onClick={handleLogout}>退出登录<RightOutline /></div>

跳转编辑

  // 已经在个人中心,跳转收藏页,信息页面,不需要校验
  // 跳转修改信息页面
  const handleToUpdate = () => {
    navigate('/update')
  }

    <div className="info" onClick={handleToUpdate}>
      <div className="avatar">
        <img src={info.pic ? info.pic : imgUrl} alt="" />
      </div>
      <p>{info.name ? info.name : '知乎日报'}</p>
    </div>

跳转收藏

  const handleToStore = () => {
    navigate('/store')
  }

<div className="store" onClick={handleToStore}>我的收藏<RightOutline /></div>

收藏列表

在收藏列表中,只要能进入该组件,就一定是用户登录的情况,因此直接初次渲染的时候派发一次任务获取收藏列表的数据,每次进入的收藏列表就显示骨架屏,如果存在收藏列表就实现渲染。但是这里需要注意使用的是NewsItem组件显示新闻项,之前封装好的,内部数据已经固定好,但是收藏列表中对新闻数据进行了处理,如没有作者信息,和images数组项变成了iamge字符串,这些都是需要处理。建议在当前收藏列表中将这些数据统一处理,如将image字符串转换为iamges数组传递。当然也可以在NewsItem中处理。这里推荐前者

在store组件中处理代码如下

const Store = (props) => {
  let { list, removeStoreListItemById, queryStoreListAsync } = props
  useEffect(() => {
    (async () => {
      if (!list) queryStoreListAsync()
    })()
  }, [])

  const handleDeleteStoreItem = async (id) => {
    try {
      let { data } = await removeStoreItem(id)
      if (+data.code !== 0) {
        Toast.show({
          icon: 'fail',
          content: '移除失败!'
        })
        return 
      }
      // 同步redux
      removeStoreListItemById(id)
    } catch (error) {

    }
  }
  return <StoreBox>
    <NavBarAgain title='我的收藏'></NavBarAgain>
    {
      // 骨架屏
      !list ?
        <SkeletonAgain />
        :
        <>
          {/* 遍历收藏列表生成新闻项 */}
          {
            list.map(item => {
              let { id, news } = item // id是收藏id,新闻id在news内部
              //滑块删除组件
              return <SwipeAction key={id} rightActions={[{
                key: 'delete',
                text: '删除',
                color: 'danger',
                // onClick配置项可以有多种写法
                onClick: () => handleDeleteStoreItem(id)
                // onClick() {
                //   handleDeleteStoreItem(id)
                // }
                // onClick: handleDeleteStoreItem.bind(null, id)
              }]}>
                <div className="store-list">
                  {/* 注意这里的新闻项和首页的新闻项存在区别,需要在组件中进行判断显示 */}
                  <HomeItem newsItem={news}></HomeItem> //news => { id,title,image }
                </div>
              </SwipeAction>
            })
          }
        </>
    }
  </StoreBox >
}

export default connect(state => state.store, action.store)(Store)

HomeItem组件如下,修改部分内容即可

  let { newsItem } = props
  let { hint, images, title, id, image } = newsItem
  // images为数组,图片地址存放在数组中的首位,主要是为了防止报错,images[0],如果是一个undefined访问[0]那么一定会报错
  if (!images) {
    images = [] //将images从undefined转换为数组
    images[0] = image
  }
  if (!Array.isArray(images)) return

组件缓存

组件缓存是针对某些组件实现路由跳转的时候,组件不销毁,再次跳转回来的时候直接渲染即可。

在React中不像vue一样提供好了组件缓存的方法直接使用需要自己实现。那么在react中实现组件缓存的方法有几种。

  1. 非标准的组件缓存,只是组件的数据或虚拟DOM缓存起来,然后存储到redux或sessionStorage等存储介质中。然后从别的组件再次跳转换来的时候,需要判断是否是缓存的组件,然后从存储介质中将数据取出重新渲染即可。如果没有缓存数据,就重新执行渲染逻辑。
  2. 修改路由源码机制,在路由跳转的时候,指定的组件不进行销毁。控制其显示或隐藏即可。(该方法不推荐使用)
  3. 将组件的真实DOM信息全部缓存起来。从其他组件跳转回来的时候,直接渲染缓存的信息即可。

借助yarn add keepalive-react-component插件实现。

从该库中引入KeepAliveProvider 组件使用,该组件必须放在Router组件中,即HashRouterBrowserRouter中使用。

import { KeepAliveProvider } from 'keepalive-react-component'

  return <HashRouter>
    <KeepAliveProvider>
      <RotuerView></RotuerView>
    </KeepAliveProvider>
  </HashRouter>

同时还需要在配置routes的时候使用withKeepAlive 高阶组件处理我们要渲染的组件。其中cacheId属性代表缓存的名字,scroll属性代表是否记录滚动条的位置。如果是lazy懒加载处理的组件,也可以使用该函数处理。

import { withKeepAlive } from 'keepalive-react-component'

 {
    path: '/',
    name: 'home',
    component: withKeepAlive(Home, { cacheId: 'home', scroll: true }), //主页不需要懒加载
    meta: {
      title: '知乎日报' //设置页面标题
    }
  },

修改个人信息界面

首先在这里并没有使用form表单搭建界面,因此各种校验规则需要自己实现。并且修改信息这里需要上传文件,需要使用FormData构造函数实现。在这里思路是:将文件上传给服务器,服务器生成一个图片地址返回,然后收到该图片地址后,在和其他修改的信息一起上传实现修改。(也可以在客户端将图片生成一个地址进行处理,然后和信息一起提交给服务器处理

    <div className="formBox">
      <div className="item">
        <div className="label">头像</div>
        <div className="input">
          <ImageUploader />
        </div>
      </div>
      <div className="item">
        <div className="label">姓名</div>
        <div className="input">
          <Input placeholder='请输入账号名称' />
        </div>
      </div>
      <ButtonAgain color='primary' className="submit">
        提交
      </ButtonAgain>
    </div>

// 使用用户信息预览
export default connect(state => state.base, action.base)(Update)

首先处理最简单的昵称预览。从props中结构出需要的数据

  let { navigate, queryUserInfoAsync, info } = props
  // 初始化预览数据,从info中取数据
  let [pic, setPic] = useState(info.pic)
  let [username, setUsername] = useState(info.name)

以下是使用ui组件库中的Input组件实现的效果。如果需要获取表单中的值,react是视图机制是MVC,数据驱动视图,那么视图更新是无法驱动数据的,因此我们需要自己处理。基于change事件,不断获取表单的值,并且不断更新视图。在ui组件库中,已经将change事件重写了,不用再获取事件对象后,在获取元素的值了。可以直接获取表单的内容

<Input placeholder='请输入账号名称' value={username} onChange={val => {setUsername(val)}} /> //直接获取值

如果采用的是原生的input表单,那么需要配合change事件,获取默认的事件对象,获取当前表单元素,这样子就可以获取表单元素的值了

<input type="text" value={username} 
		onChange={e => {let target = e.target setUsername(target.value)}}  // 获取的是事件对象
/>

然后就是针对图片文件上传需要如何处理?
首先需要知道,antd组件中提供的组件实现文件上传,先了解如何使用该组件。

value属性:用于保存已上传的文件列表。它的值的格式是一个数组,数组中的每一项是一个对象,一个对象代表一个文件。也就是说可以有多个文件存放在数组对象中。(可以上传多个文件)
知乎日报项目下--React_第17张图片

<ImageUploader
            value={[
              { url: 'https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF' },
              { url: 'https://t7.baidu.com/it/u=4198287529,2774471735&fm=193&f=GIF' }
            ]}
/>

知乎日报项目下--React_第18张图片
可以通过accept属性设置文件的类型:在组件库中默认是所有图片格式都可以,例如:指定多个文件图片上传的类accept='image/jpg,image/jpeg,image/png'

beforeUpload属性指定文件上传之前需要做什么事情,可以在这里做文件图片大小校验。
maxCount属性控制能够上传多少文件。默认0代表不做限制上传。在该按钮中编辑头像只需要一个文件上次即可,设置为1。
deletabledeleteIcon,onDelete这些属性是和删除有关的,其中onDelete属性用于处理删除已经上传成功文件的回调
upload属性是上传文件的方法。antd-mobile中文件上传需要自己手动处理,不支持自动上传

那么如何实现删除一个图片重新上传?代码如下,本质是将pic图片赋值给url属性,页面可以正常显示预览文件。然后添加一删除事件。

let [pic, setPic] = useState(info.pic)
<ImageUploader maxCount={1} value={[ { url: pic } ]}/>

直接将图片设置为空,但是发现并不是我们想要的效果,并没有实现图片删除,重新显示选中文件的按钮。这是因为文件是根据value属性中数组有多个元素决定的。在这里最终格式是这样子:value={ [ url : ’ ’ ] } ,当预览图片地址的时候失效了显示碎图。

onDelete={() => {setPic('')}}

在这里插入图片描述
解决如下

let [pic, setPic] = useState([{ url: info.pic }])

<ImageUploader maxCount={1} value={pic} onDelete={() => {setPic([])}}/>

知乎日报项目下--React_第19张图片
知乎日报项目下--React_第20张图片

接下来处理图片上传前校验大小和上传服务器的操作。

搭建接口

// 文件上传接口,需要传入文件
export const uploadImgAPI = (file) => {
  let form = new FormData()
  form.append('file', file) //后端要求使用file字段
  return http.post('/upload', form)
}

export const updateUserInfoAPI = ({ username, pic }) => {
  return http({
    url: '/user_update',
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded"
    },
    data: {
      username,
      pic
    }
  })
}

ImageUploader组件分别绑定两个方法:beforeUpload={limitImg} upload={uploadImg}beforeUpload指定的回调如果返回null或者不返回都会终止文件上传,那么在upload指定的回调函数中就无法读取到文件。

  // 校验图片大小
  const limitImg = (file) => {
    console.log('beforeUpload', file); //未返回
  }

  // 上传图片
  const uploadImg = (file) => {
    console.log('upload', file);
  }

beforeUpload中为返回结果终止上传,则upload不会接受到结果,如图是在beforeUpload指定函数中获得的默认参数,即文件的信息,在一文件中size属性代表了当前文件的大小,以字节为单位。
知乎日报项目下--React_第21张图片

可以根据文件的size属性进行文件大小限制

  // 校验图片大小
  const limitImg = (file) => {
    let limit = 1 * 1024 * 1024 // 1MB限制,转换为B进行比较
    if (file.size > limit) {
      Toast.show({
        icon: 'fail',
        content: '图片过大!'
      })
      return null //终止文件上传
    }
    return file
  }

当上传的文件符合条件的时候,会执行upload指定的函数中。
知乎日报项目下--React_第22张图片

upload指定的回调函数中需要返回结果,否则报错无法实现预览。

  // 上传图片
  const uploadImg = async (file) => {
    console.log('upload', file);
    let temp = null //创建中间变量接收服务器返回的pic图片地址
    try {
      let { data: { code, pic } } = await uploadImgAPI(file)
      if (+code !== 0) {
        Toast.show({
          icon: 'fail',
          content: '上传失败'
        })
        return
      }
      // // 更新图片预览
      setPic([{ url: pic }])
      temp = pic
    } catch {
      console.log("上传文件失败");
    }
    // upload指定的回调最终需要返回内容
    return {
      url: temp //可以判断temp为假设置默认显示图片这里不做处理了
    }
  }

成功显示新上传的图片,并且可以预览,且服务器的文件中会多出一个图片文件。
知乎日报项目下--React_第23张图片

当文件部分处理完毕,就可以处理提交按钮的逻辑了,在提交按钮中需要再次对两个区域进行二次校验。校验通过后,将用户的信息传递给服务器更改,最后通过redux派发,获取最新的用户信息更新即可。

      <ButtonAgain color='primary' className="submit" onClick={submit}>
        提交
      </ButtonAgain>
  // 提交
  const submit = async () => {
    // 对表单的内容进行校验
     if (pic.length === 0) {
      Toast.show({
        icon: 'fail',
        content: '请选择图片!'
      })
      return
    }
    if (username.trim().length === 0) {
      Toast.show({
        icon: 'fail',
        content: '昵称不能为空!'
      })
      return
    }

    let [{ url }] = pic // [{ url:''}] //对图片进行解构
    try {
      let pic = url
      let { data: { code } } = await updateUserInfoAPI({ username, pic }) //接口解构处理要求是pic形式
      if (+code !== 0) {
        Toast.show({
          icon: 'fail',
          content: '修改信息失败'
        })
        return
      }
      Toast.show({
        icon: 'success',
        content: '修改成功'
      })
      // 同步redux用户信息
      queryUserInfoAsync()
      navigate(-1, { replace: true })
    } catch {
      console.log('修改信息失败');
    }
  }

项目打包

最后使用yarn build打包生成项目文件。但是需要注意,在本地开发环境下采样webpackdevserver,在正式环境中是没有的,还是需要基于nginx进行反向代理。

你可能感兴趣的:(react.js,javascript,前端)