在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)
let { navigate, queryUserInfoAsync } = props
//存储token后,获取用户信息
_.storage.set('tk', res.data.token)
await queryUserInfoAsync() //异步处理
navigate('/')
在登录成功后,需要处理跳转的逻辑有几种情况。
/
,像很多页面,需要登录后才能放行的,需要处理。假设没有登录信息,那么点击个人中心,从首页/
跳转到登录页/login
,然后在登录页成功后处理如何跳转问题,这个时候如果采样跳转上一级那么就会返回的主页,这不符合正常操作,应该是跳转到个人中心才行。也就是说,需要再登录页知道登录成功后需要调整到哪里。因此我们可以在首页/
跳转登录页的时候携带参数/login?to=/person
,每次登录成功的时候,根据to
属性决定跳转到哪里,如果没有就默认跳转首页也行。但是还需要处理细节问题,就是路由跳转模式是push还是replace,因为登录页跳转到个人中心后,用户就不能通过历史返回箭头回退到登录页了,因此在登录页跳转采样replac方式/login?to=/detail/123123
,也需要在登录页采用replace的方式处理跳转,让用户不能使用历史回退箭头跳转到登录页。在detail组件中采样replace跳转在登录页的props属性中结构出地址中的search参数,修改原先登录的跳转代码
let {... ,urlSearchParams } = props //urlSearchParams 基于useSearchParams函数
// to==> /login?to=/person
let to = urlSearchParams.get('to')
to ? navigate(to, { replace: true }) : navigate(-1)
然后需要单独处理登录页的返回功能,该返回是通过二次封装的NavBar实现。在其他组件中,都是从来哪里,点击该返回按钮就回哪里去。但是需要额外处理详情页到登录页,该返回按钮的逻辑
// 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)
}
针对/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)
}
})()
这个时候发现,无论如何在内部使用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
}
},
以下是示例图
无token的情况,不需要进行登录校验的页面可以直接进入
登录后,从跳转到刚才需要登录的页面去,贴合了用户的使用。
当有token页面刷新的时候,会自动获取用户信息更新到redux中,然后不再进入登录页。
处理的细节点,如果首页跳转到personal页面,但是没有登录,会从personal页面进入登录页,历史记录中会多一条personal,这需要清除,
navigate({
pathname: '/login',
search: `?to=${location.pathname}`
}, { replace: true })
首页顶部头像,首先会判断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()
})()
}, [])
但是这样子写完会发现在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'
})
}
// 收藏 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中实现组件缓存的方法有几种。
借助yarn add keepalive-react-component
插件实现。
从该库中引入KeepAliveProvider
组件使用,该组件必须放在Router
组件中,即HashRouter
或BrowserRouter
中使用。
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
属性:用于保存已上传的文件列表。它的值的格式是一个数组,数组中的每一项是一个对象,一个对象代表一个文件。也就是说可以有多个文件存放在数组对象中。(可以上传多个文件)
<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' }
]}
/>
可以通过accept
属性设置文件的类型:在组件库中默认是所有图片格式都可以,例如:指定多个文件图片上传的类accept='image/jpg,image/jpeg,image/png'
beforeUpload
属性指定文件上传之前需要做什么事情,可以在这里做文件图片大小校验。
maxCount
属性控制能够上传多少文件。默认0代表不做限制上传。在该按钮中编辑头像只需要一个文件上次即可,设置为1。
deletable
,deleteIcon
,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([])}}/>
接下来处理图片上传前校验大小和上传服务器的操作。
搭建接口
// 文件上传接口,需要传入文件
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
属性代表了当前文件的大小,以字节为单位。
可以根据文件的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
指定的函数中。
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为假设置默认显示图片这里不做处理了
}
}
成功显示新上传的图片,并且可以预览,且服务器的文件中会多出一个图片文件。
当文件部分处理完毕,就可以处理提交按钮的逻辑了,在提交按钮中需要再次对两个区域进行二次校验。校验通过后,将用户的信息传递给服务器更改,最后通过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进行反向代理。