React十案例下

代码下载

登录模块

用户登录

页面结构

新建 Login 组件,对应结构:

    export default function Login() {return (<div className={styles.root}><NavHeader className={styles.header}>账号登录</NavHeader><form className={styles.form}><input placeholder="请输入账号" className={styles.account}></input><input type="password" placeholder="请输入密码" className={styles.password}></input><Button color='success' className={styles.login} type="submit">登 录</Button></form><Link className={styles.backHome} to='/registe'>还没有账号,去注册~</Link></div>)}
功能实现

1、添加状态 username和password,获取表单元素值:

        const [username, setUsername] = useState('')const [password, setPassword] = useState('')……<input placeholder="请输入账号" className={styles.account} onChange={(v) => setUsername(v)}>{username}</input><input type="password" placeholder="请输入密码" className={styles.password} onChange={(v) => setPassword(v)}>{password}</input>

2、在form表单的 onSubmit 方法中实现表单提交,通过username和password获取到账号和密码,使用API调用登录接口,将 username 和 password 作为参数,登录成功后,将token保存到本地存储中(hkzf_token)并返回登录前的页面:

        const navigate = useNavigate()……<form className={styles.form} onSubmit={(e) => {// 阻止默认行为e.preventDefault()// 请求登录接口instance.post('/user/login', {username, password}).then((data) => {console.log('login data: ', data);const {status, description, body} = dataif (status !== 200) {// 登录成功localStorage.setItem('hkzf_token', body.token)navigate(-1)} else {// 登录失败Toast.show({content: description})}})}}>

表单验证说明

表单提交前,需要先进性表单验证,验证通过后再提交表单:

  • 方式一:antd-mobile 组件库的方式(需要 Form.Item 文本输入组件)
  • 方法二(推荐):使用更通用的 formik,React中专门用来进行表单处理和表单校验的库
formik
  • Github地址:formik文档
  • 场景:表单处理,表单验证
  • 优势:轻松处理React中的复杂表单,包括:获取表单元素的值,表单验证和错误信息,处理表单提交,并且将这些内容放在一起统一处理,有利于代码阅读,重构,测试等
  • formik 具体使用

使用 formik 实现表单校验

  • 安装 npm i formikyarn add formik
  • 导入 Formik 组件,根据 render props 模式,使用 Formik 组件包裹Login组件
  • 为 Formik 组件提供配置属性 initialValues,这是初始数据;onSubmit 表单提交的执行函数,通过values获取到表单元素值,完成登录逻辑
  • Formik 的 children 属性是一个函数,通过函数参数获取到values(表单元素值对象),handleSubmit,handleChange
  • 使用 values 提供的值设置为表单元素的 value,使用 handleChange 设置为表单元素的 onChange,使用handleSubmit设置为表单的onSubmit
        <Formik initialValues={{'username': '', password: ''}}onSubmit={(values) => {console.log('login values: ', values);// 请求登录接口instance.post('/user/login', {'username': values.username, 'password': values.password}).then((data) => {console.log('login data: ', data);const {status, description, body} = dataif (data.status === 200) {// 登录成功localStorage.setItem('hkzf_token', body.token)navigate(-1)} else {// 登录失败Toast.show({content: description})}})}}>{({values, handleSubmit, handleChange}) => {return <form className={styles.form} onSubmit={handleSubmit}><input name="username" placeholder="请输入账号" className={styles.account} onChange={handleChange} value={values.username}></input><input name="password" type="password" placeholder="请输入密码" className={styles.password} onChange={handleChange} value={values.password}></input><Button color='success' className={styles.login} type="submit">登 录</Button></form>}}</Formik>

两种表单验证方式:

  • 通过给 Formik 组件 配置 validate 属性手动校验
  • 通过给 Formik 组件 validationSchema 属性配合Yup来校验(推荐)
给登录功能添加表单验证

1、安装npm i yupyarn add yup (Yup 文档),导入Yup

2、在 Formik 组件中添加配置项 validationSchema,使用 Yup 添加表单校验规则

        validationSchema={Yup.object().shape({username: Yup.string().required('账号为必填项').matches(/^\w{5,8}/, '5~8位的数字、字母、下划线'),password: Yup.string().required('密码为必填项').matches(/^\w{5,12}/, '5~8位的数字、字母、下划线')})}

3、在 Formik 组件中,通过 children 函数属性获取到 errors(错误信息)和 touched(是否访问过,注意:需要给表单元素添加 handleBlur 处理失焦点事件才生效!),在表单元素中通过这两个对象展示表单校验错误信

            {({values, handleSubmit, handleChange, errors, touched}) => {return <form className={styles.form} onSubmit={handleSubmit}><input name="username" placeholder="请输入账号" className={styles.account} onChange={handleChange} value={values.username}></input>{errors.username && touched.username && <div className={styles.error}>{errors.username}</div>}<input name="password" type="password" placeholder="请输入密码" className={styles.password} onChange={handleChange} value={values.password}></input>{errors.password && touched.password && <div className={styles.error}>{errors.password}</div>}<Button color='success' className={styles.login} type="submit">登 录</Button></form>}}

其他处理:

  • 导入 Form 组件,替换 form 元素,去掉onSubmit
  • 导入 Field 组件,替换 input 表单元素,去掉onChange,onBlur,value
  • 导入 ErrorMessage 组件,替换原来的错误消息逻辑代码
            {({values, handleSubmit, handleChange, errors, touched}) => {return <Form className={styles.form}><Field name="username" placeholder="请输入账号" className={styles.account}></Field><ErrorMessage name="username" className={styles.error} component='div'></ErrorMessage><Field name="password" type="password" placeholder="请输入密码" className={styles.password}></Field><ErrorMessage name="password" className={styles.error} component='div'></ErrorMessage><Button color='success' className={styles.login} type="submit">登 录</Button></Form>}}

我的页面

登录判断

查询本地缓存中是否有 token 信息来判断是否登录,新建 utils/auth.js 文件,为方便使用封装如下内容:

// localStorage 中存储 token 的键
const token_key = 'hkzf_token'// 获取 token
const getToken = () => localStorage.getItem(token_key)// 设置 token
const setToken = (token) => localStorage.setItem(token_key, token)// 删除 token
const removeToken = () => localStorage.removeItem(token_key)// 判断是否登录
const isAuth = !!getToken()export { getToken, setToken, removeToken, isAuth }

页面结构

我的页面根据是否登录展示有所不同:
请添加图片描述

import styles from "./Profile.module.css";
import { baseUrl } from "../utils/constValue";
import { useState } from "react";
import { isAuth } from "../utils/auth";
import { Grid } from "antd-mobile";
import { useNavigate } from "react-router-dom";// 默认头像
const DEFAULT_AVATAR = baseUrl + '/img/profile/avatar.png'
// 菜单数据
const menus = [{ id: 1, name: '我的收藏', iconfont: 'icon-coll', to: '/favorate' },{ id: 2, name: '我的出租', iconfont: 'icon-ind', to: '/rent' },{ id: 3, name: '看房记录', iconfont: 'icon-record' },{ id: 4, name: '成为房主', iconfont: 'icon-identity'},{ id: 5, name: '个人资料', iconfont: 'icon-myinfo' },{ id: 6, name: '联系我们', iconfont: 'icon-cust' }
]export default function Profile() {const [isLogin, setIsLogin] = useState(isAuth)const [userInfo, setUserInfo] = useState({avatar: '',nickname: ''})const {avatar, nickname} = userInfoconst navigate = useNavigate()return (<div className={styles.root}>{/* 个人信息 */}<div className={styles.title}><img className={styles.bg} src={baseUrl + '/img/profile/bg.png'}></img><div className={styles.info}><div className={styles.myIcon}><img className={styles.avater} src={avatar || DEFAULT_AVATAR} alt="头像"></img></div><div className={styles.user}><div className={styles.name}>{nickname || '游客'}</div><span className={isLogin ? styles.logout : styles.login}>{isLogin ? '退出' : '去登录'}</span></div><div className={styles.edit}>编辑个人资料<span className={styles.arrow}><i className="iconfont icon-arrow" /></span></div></div></div>{/* 九宫格菜单 */}<Grid columns={3} gap={8} className={styles.grid}>{menus.map((item) => {console.log('item: ', item);return <Grid.Item key={item.id} onClick={() => {if (item.to) navigate(item.to)}}><div className={styles.menusItem}><span className={'iconfont ' + item.iconfont}></span><span>{item.name}</span></div></Grid.Item>})}</Grid>{/* 加入我们 */}<div className={styles.add}><img src={baseUrl + '/img/profile/join.png'} alt=""></img></div></div>)
}

添加两个状态 isLogin(是否登录)和 userInfo(用户信息),从 utils/auth 中导入 isAuth 来判断是否登录。如果没有登录,渲染未登录信息;如果已登录,就渲染个人资料数据。

去登录与退出

  • 给去登录、退出按钮绑定点击事件
  • 点击去登录按钮,直接导航到登录页面
  • 点击退出按钮,弹出 Modal 对话框,提示是否确定退出。先调用退出接口(让服务器端退出),再移除本地token(本地退出)、把登录状态 isLogin 设置为 false、清空用户状态对象。
                    <span className={isLogin ? styles.logout : styles.login} onClick={() => {if (isLogin) {Modal.show({title: '提示',content: '是否确定退出?',closeOnAction: true,actions: [{key: 'cancel',text: '取消'},{key: 'confirm',text: '退出',primary: true,onClick: () => {instance.post('/user/logout').finally((data) => {console.log('logout data: ', data);// 移除 tokenremoveToken()setIsLogin(isAuth)// 清除用户信息setUserInfo({avatar: '',nickname: ''})})}}]})} else {navigate('/login')}}}>{isLogin ? '退出' : '去登录'}</span>

获取用户信息

如果已登录,就使用 useEffect 根据接口发送请求,获取用户个人资料

    useEffect(() =>  {let ignore = falseif (isLogin) {instance.get('/user', {headers: {authorization: getToken()}}).then((data) => {if (!ignore) {if (data.status === 200) {setUserInfo(data.body)}}})}return () => ignore = true}, [isLogin])

登录访问控制

项目中的两种类型的功能和两种类型的页面:

两种功能:

  • 登录后才能进行操作(比如:获取个人资料)
  • 不需要登录就可以操作(比如:获取房屋列表)

两种页面:

  • 需要登录才能访问(比如:发布房源页)
  • 不需要登录即可访问(比如:首页)

对于需要登录才能操作的功能使用 axios 拦截器 进行处理(比如:统一添加请求头 authorization等);对于需要登录才能访问的页面使用 路由控制

功能处理-使用axios拦截器统一处理token

api.js 中,添加请求拦截器 instance.interceptors.request.user(),获取到当前请求的接口路径(url),判断接口路径,是否是以/user 开头,并且不是登录或注册接口(只给需要的接口添加请求头),如果是,就添加请求头Authorization:

// 请求拦截器
instance.interceptors.request.use((config) => {// 统一打印接口请求参数日志console.log('request url: ', config.baseURL + config.url);console.log('request params: ', config.params);console.log('request headers: ', config.headers);console.log('request data: ', config.data);// 统一显示加载提示const {url} = configToast.show({icon: 'loading', duration: 0, content: '加载中…', maskClickable: false})// 统一判断请求url路径添加请求头if (url.startsWith('/user') && !url.startsWith('/user/login') && !url.startsWith('/user/registered')) {config.headers.Authorization = getToken()}return config
})

添加响应拦截器 instance.interceptors.response.use(),判断返回值中的状态码如果是 400 表示 token 失效,如果 data 中的状态码是 400 表示接口没有传递 token,则直接移除 token 并更新 isLogin 状态:

// 响应拦截器
instance.interceptors.response.use((res) => {// 统一打印接口响应数据日志console.log('response data: ', res);// 清除加载提示Toast.clear()// 统一判断 token 是否失效或者被清除const {status} = res.dataif (status === 400 || res.data.status === 400) {if (isAuth) {removeToken()}if (instance.setIsLogin) {instance.setIsLogin(isAuth())}}return res.data
}, (error) => {// 统一打印接口响应错误日志console.log('response error: ', error);// 清除加载提示Toast.clear()return Promise.reject(error)
})

页面处理-路由鉴权

限制某个页面只能在登陆的情况下访问,但是在React路由中并没有直接提供该该功能,需要手动封装来实现登陆访问控制(类似与Vue路由的导航守卫)。

react-router-dom的文档,实际上就是通过 Context 对原来路由系统做了一次包装,来实现一些额外的功能。

1、在 utils/auth.js 中添加 是否登录 和 设置是否登录 的 Context,并将它们导出:

// 是否登录 Context
const isLoginContext = createContext(isAuth())// 设置是否登录 Context
const setIsLoginContext = createContext(null)

2、在components目录中创建 AuthRoute.js 文件,创建组件AuthRoute并导出,添加状态 isLogin 并将 setIsLogin 函数传递给网络层,让后将原来的路由系统包裹在 isLoginContext 和 setIsLoginContext 中:

export default function AuthRoute() {const [isLogin, setIsLogin] = useState(useContext(isLoginContext))// 将修改登录状态函数传递给网络层instance.setIsLogin = setIsLoginreturn <isLoginContext.Provider value={isLogin}><setIsLoginContext.Provider value={setIsLogin}><Router><Routes>{/* 路由重定向 */}<Route path='/' element={<Navigate to='/home' replace></Navigate>}></Route>{/* 父路由 */}<Route path='/' element={<App></App>}>{/* 子路由 */}<Route path='/home' element={<Home></Home>}></Route><Route path='/house' element={<House></House>}></Route><Route path='/news' element={<News></News>}></Route><Route path='/profile' element={<Profile></Profile>}></Route></Route><Route path='/cityList' element={<CityList></CityList>}></Route><Route path='/map' element={<Map></Map>}></Route><Route path='/detail/:id' element={<HouseDetail></HouseDetail>}></Route><Route path='/login' element={<Login></Login>}></Route></Routes></Router></setIsLoginContext.Provider></isLoginContext.Provider>
}

3、在components目录中创建 AuthRoute.js 文件中定义一个 loginRoute 函数,根据是否登录来返回对应的 Rute 组件(如果没有登陆,就重定向到登陆页面,并指定登陆成功后腰跳转的页面路径,并使用 replace 模式):

function loginRoute(isLogin) { return (route) => {if (isLogin) {return route}return <Route path={route.props.path} element={<Navigate to='/login' state={{from: {pathname: route.props.path}}} replace></Navigate>}></Route>}}

4、将启动文件 index.js 中的路由系统删除,由新的 <AuthRoute></AuthRoute> 组件代替:

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<React.StrictMode><AuthRoute></AuthRoute></React.StrictMode>
);
修改登录成功跳转
  • 登陆成功后使用 setToken 清除 token并使用 Context 更新登录状态,判断是否需要跳转到用户想要访问的页面(使用 useLocation 获取路由的 state 参数, 判断 state.form 是否有值)
  • 如果不需要,则直接调用history.go(-1) 返回上一页
  • 如果需要,就跳转到 from.pathname 指定的页面(推荐使用replace方法模式)
    const {state} = useLocation()const setIsLogin = useContext(setIsLoginContext)……// 请求登录接口instance.post('/user/login', {'username': values.username, 'password': values.password}).then((data) => {console.log('login data: ', data);const {description, body} = dataif (data.status === 200) {// 登录成功setToken(body.token)setIsLogin(isAuth())if (state && state.form) {navigate(state.form.pathname, {replace: true})} else {navigate(-1)}} else {// 登录失败Toast.show({content: description})}})
修改我的页面的登录状态

用 Context 将原来的 isLogin 状态替换:

    // const [isLogin, setIsLogin] = useState(isAuth())const isLogin = useContext(isLoginContext)const setIsLogin = useContext(setIsLoginContext)

收藏模块

检查房源是否收藏

  • 在 HouseDetail 页面中添加状态 isFavorite(表示是否收藏),默认值是false
  • 使用 useEffect,在进入房源详情页面时执行
  • 先调用isAuth方法,来判断是否登陆
  • 如果未登录,直接return,不再检查是否收藏
  • 如果已登陆,从路由参数中,获取当前房屋id
  • 使用API调用接口,查询该房源是否收藏
  • 如果返回状态码为200,就更新isFavorite;否则,不做任何处理
    // 收藏const [isFavorite, setIsFavorite] = useState(false)useEffect(() => {let ignore = false// 登录才获取收藏数据if (isAuth()) {instance.get('/user/favorites/' + routerParams.id).then((data) => {if (!ignore) {console.log('favorite data: ', data);if (data.status === 200) {setIsFavorite(data.body.isFavorite)}}})}return () => ignore = true}, [])

在 HouseDetail 页面结构中,通过状态isFavorite修改收藏按钮的文字和图片内容:

            {/* 底部栏工具栏 */}<div className={styles.footer}><div className={styles.favorite}><imgsrc={baseUrl + (isFavorite ? '/img/star.png' : '/img/unstar.png')}className={styles.favoriteImg}alt="收藏"/><span className={styles.favoriteTxt}>{isFavorite ? '已收藏' : '收藏'}</span></div><div className={styles.consult}>在线咨询</div><div className={styles.telephone}><a href="tel:400-618-4000" className={styles.telephoneTxt}>电话预约</a></div></div>

收藏房源

  • 给收藏按钮绑定点击事件,调用isAuth方法,判断是否登陆
  • 如果未登录,则使用 Toast.show 提示用户是否去登陆;如果点击取消,则不做任何操作;如果点击去登陆,就跳转到登陆页面,同时传递state(登陆后,再回到房源收藏页面)
  • 如果已登录则根据 isFavorite 判断当前房源是否收藏,如果未收藏,就调用添加收藏接口,添加收藏;如果收藏了,就调用删除接口,删除收藏
                <div className={styles.favorite} onClick={async () => {if (isAuth()) {// 已登录if (isFavorite) {// 已收藏const deleteData = await instance.delete('/user/favorites/' + routerParams.id)console.log('delete data: ', deleteData);if (deleteData.status === 200) {Toast.show('已取消收藏')setIsFavorite(false)} else {Toast.show('登录超时,请重新登录')}} else {// 未收藏const postData = await instance.post('/user/favorites/' + routerParams.id)console.log('post data: ', postData);if (postData.status === 200) {Toast.show('已收藏')setIsFavorite(true)} else {Toast.show('登录超时,请重新登录')}}} else {// 未登录Modal.show({title: '提示',content: '登录后才能收藏房源,是否去登录?',closeOnAction: true,actions: [{key: 'cancel',text: '取消'},{key: 'confirm',text: '去登录',primary: true,onClick: async () => navigate('/login', {state: {from: 'location'}})}]})}}}><imgsrc={baseUrl + (isFavorite ? '/img/star.png' : '/img/unstar.png')}className={styles.favoriteImg}alt="收藏"/><span className={styles.favoriteTxt}>{isFavorite ? '已收藏' : '收藏'}</span></div>

房源发布模块

主要功能为获取房源的小区信息,房源图片上传,房源发布等。
请添加图片描述

前期准备

1、将 House 页面中没有找到房源时显示的内容封装成一个公共组件 NoHouse,并将其children属性校验为设置为 node(任何可以渲染的内容):

import styles from "./NoHouse.module.css";
import PropTypes from "prop-types";export function NoHouse(props) {return <div className={styles.noData}><img className={styles.img} src={baseUrl + '/img/not-found.png'} alt="暂无数据"/><p className={styles.msg}>{props.children}</p></div>
}NoHouse.propTypes = {//  node(任何可以渲染的内容)children: PropTypes.node.isRequired
}

2、将 HouseDetail 页面中房屋配置的内容封装成一个公共组件 HousePackage,并为 onSelect 属性设置默认值:

export default function HousePackage({supporting = [], onSelect = () => {}}) {// 选中的配套名称console.log('supporting: ', supporting);const [selectedNames, setSelectedNames] = useState(supporting)return (<>{/* 房屋配套 */}<div className={styles.aboutList}>{HOUSE_PACKAGE.map((item, i) => {const si = selectedNames.indexOf(item.name)return <div className={styles.aboutItem + (si > -1 ? ' ' + styles.aboutActive : '')} key={item.id} onClick={() => {console.log('si: ', si);const newNames = [...selectedNames]if (si > -1) {newNames.splice(si, 1)} else {newNames.push(item.name)}// 修改选中setSelectedNames(newNames)// 将值传递给父组件onSelect(newNames)}}><p className={styles.aboutValue}><i className={`iconfont ${item.icon} ${styles.icon}`} /></p><div>{item.name}</div></div>})}</div></>)
}HousePackage.propTypes = {supporting: PropTypes.string,onSelect: PropTypes.func
}

3、创建了三个页面组件 Rent(已发布房源列表)、RentAdd(发布房源)、RentSearch(关键词搜索校区信息),并为 Rent 组件构建如下布局:

function renderNoHouse() {return <NoHouse>您还没有房源,<Link to='/rent/add' className={styles.link}>去发布房源</Link>吧~</NoHouse>
}
function renderList(list, navigate) {return list.map((value) => {return <HouseItem key={value.houseCode} item={value} onClick={() => {navigate('/detail/' + value.houseCode)}}></HouseItem>})
}
export default function Rent() {// 获取已发布房源数据const {data: rentData} = useData.get('/user/houses')console.log('rent data: ', rentData);const list = rentData && rentData.body ? rentData.body : []const navigate = useNavigate()return (<div className={styles.root}><NavHeader className={styles.navigate}>房屋管理</NavHeader>{list.length > 0 ? renderList(list, navigate) : renderNoHouse()}</div>)
}

4、在 AuthRoute 中导入 Rent、RentAdd、RentSearch 3个页面组件,使用 loginRoute 函数配置3个对应的路由规则:

                    {loginRoute(isLogin)(<Route path='/rent' element={<Rent></Rent>}></Route>)}{loginRoute(isLogin)(<Route path='/rent/add' element={<RentAdd></RentAdd>}></Route>)}{loginRoute(isLogin)(<Route path='/rent/search' element={<RentSearch></RentSearch>}></Route>)}

搜索小区

关键词搜索小区信息分析:

  • 给 SearchBar 组件添加 onChange 事件获取文本框的值,判断当前文本框的值是否为空(如果为空,清空列表,然后return,不再发送请求;如果不为空,使用API发送请求,获取小区数据),使用定时器来延迟搜索,提升性能
  • 给搜索列表项添加点击事件,使用 useNavigate 跳转到发布房源页面,将被点击的校区信息作为数据一起传递过去

防抖:搜索栏中每输入一个值,就发一次请求,这样对服务器压力比较大,用户体验不好。解决方式:使用定时器来进行延迟执行(关键词:JS文本框输入 防抖)

import { SearchBar } from "antd-mobile";
import styles from "./RentSearch.module.css";
import { useState } from "react";
import { instance } from "../utils/api";
import useCurrentCity from "../utils/useCurrentCity";
import { replace, useNavigate } from "react-router-dom";let timer = nullexport function RentSearch() {const [searchText, setSearchText] = useState('')const {currentCity} = useCurrentCity()const [list, setList] = useState([])console.log('current city: ', currentCity);const navigate = useNavigate()return <div className={styles.root}>{/* 搜索栏 */}<SearchBar className={styles.searchBar} placeholder="请输入小区名称" showCancelButton value={searchText} onChange={(value) => {console.log('searchText: ', value);if (value) {console.log('timer: ', timer);if (timer) {clearTimeout(timer)timer = null}timer = setTimeout(() => {instance.get('/area/community', {params: {name: value, id: currentCity.value}}).then((data) => {console.log('search data: ', data);setList(data.body)})}, 500);} else {setList([])}setSearchText(value)}}></SearchBar>{/* 搜索项 */}<ul className={styles.list}>{list.map((v) => <li key={v.community} className={styles.item} onClick={() => {navigate('/rent/add', {replace: true, state: {community: v}})}}>{v.communityName}</li>)}</ul></div>
}

发布房源

使用 ImageUploader, Input, List, Modal, Picker, TextArea 等组件搭建页面结构并实现功能:

// 房屋类型
const roomTypeData = [{ label: '一室', value: 'ROOM|d4a692e4-a177-37fd' },{ label: '二室', value: 'ROOM|d1a00384-5801-d5cd' },{ label: '三室', value: 'ROOM|20903ae0-c7bc-f2e2' },{ label: '四室', value: 'ROOM|ce2a5daa-811d-2f49' },{ label: '四室+', value: 'ROOM|2731c38c-5b19-ff7f' }
]// 朝向:
const orientedData = [{ label: '东', value: 'ORIEN|141b98bf-1ad0-11e3' },{ label: '西', value: 'ORIEN|103fb3aa-e8b4-de0e' },{ label: '南', value: 'ORIEN|61e99445-e95e-7f37' },{ label: '北', value: 'ORIEN|caa6f80b-b764-c2df' },{ label: '东南', value: 'ORIEN|dfb1b36b-e0d1-0977' },{ label: '东北', value: 'ORIEN|67ac2205-7e0f-c057' },{ label: '西南', value: 'ORIEN|2354e89e-3918-9cef' },{ label: '西北', value: 'ORIEN|80795f1a-e32f-feb9' }
]// 楼层
const floorData = [{ label: '高楼层', value: 'FLOOR|1' },{ label: '中楼层', value: 'FLOOR|2' },{ label: '低楼层', value: 'FLOOR|3' }
]// 获取数据列表中 value 对应的 label 值
const labelForValue = (data, value) => {for (let index = 0; index < data.length; index++) {const element = data[index];if (element.value === value) {return element.label}}return null
}export default function RentAdd() {const {state} = useLocation()const navigate = useNavigate()const [info, setInfo] = useState({community: state ? state.community : {}})return <div className={styles.root}><NavHeader className={styles.navHeader}>发布房源</NavHeader><div className={styles.content}><List className={styles.header} header='房源信息'><List.Item prefix={<label>小区名称</label>} extra={info.community.communityName || '请选择'} clickable onClick={() => navigate('/rent/search')}></List.Item><List.Item prefix={<label htmlFor='price'>租&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;金</label>} extra='¥/月'><Input id="price" placeholder="请输入租金/月" value={info.price} onChange={(v) => {const newInfo = {...info}newInfo.price = vsetInfo(newInfo)}}></Input></List.Item><List.Item prefix={<label htmlFor='size'>建筑面积</label>} extra='m²'><Input id="size" placeholder="请输入建筑面积" value={info.size} onChange={(v) => {const newInfo = {...info}newInfo.size = vsetInfo(newInfo)}}></Input></List.Item><Picker columns={[roomTypeData]} value={[info.roomType]} onConfirm={(v) => {console.log('room type value: ', v)const newInfo = {...info}newInfo.roomType = v[0]setInfo(newInfo)}}>{(_, actions) => {return <List.Item prefix={<label>户&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;型</label>} extra={labelForValue(roomTypeData, info.roomType) || '请选择'} clickable onClick={actions.open}></List.Item>}}</Picker><Picker columns={[floorData]} value={[info.floor]} onConfirm={(v) => {console.log('floor value: ', v)const newInfo = {...info}newInfo.floor = v[0]setInfo(newInfo)}}>{(_, actions) => {return <List.Item prefix={<label>所在楼层</label>} extra={labelForValue(floorData, info.floor) || '请选择'} clickable onClick={actions.open}></List.Item>}}</Picker><Picker columns={[orientedData]} value={[info.oriented]} onConfirm={(v) => {console.log('oriented value: ', v)const newInfo = {...info}newInfo.oriented = v[0]setInfo(newInfo)}}>{(_, actions) => {return <List.Item prefix={<label>朝&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;向</label>} extra={labelForValue(orientedData, info.oriented) || '请选择'} clickable onClick={actions.open}></List.Item>}}</Picker></List><List header='房屋标题'><List.Item><Input placeholder="请输入标题(例如:整租 小区名 2室 5000元)" value={info.title} onChange={(v) => {const newInfo = {...info}newInfo.title = vsetInfo(newInfo)}}></Input></List.Item></List><List header='房屋图像'><List.Item><ImageUploader value={info.houseImg} multiple maxCount={9} showUpload={info.houseImg ? info.houseImg.length < 9 : true} onCountExceed={(exceed) => Toast.show(`最多选择 9 张图片,您多选了 ${exceed} 张`)} onChange={(v) => {console.log('temp slides value: ', v);const newInfo = {...info}newInfo.houseImg = vconsole.log('new info: ', newInfo);setInfo(newInfo)}} upload={async (file) => {console.log('file: ', file);const fd = new FormData()fd.append('file', file)const data = await instance.post('/houses/image', fd, {headers: {'Content-Type': 'multipart/form-data'}})console.log('image data: ', data);if (data.status === 200) {const url = data.body[0]return {url: baseUrl + data.body[0]}}}}></ImageUploader></List.Item></List><List header='房屋配置'><List.Item><HousePackage onSelect={(names) => {const newInfo = {...info}newInfo.supporting = names.join('|')setInfo(newInfo)}}></HousePackage></List.Item></List><List header='房屋描述'><List.Item><TextArea placeholder="请输入房屋描述信息" value={info.description} rows={5} onChange={(v) => {const newInfo = {...info}newInfo.description = vsetInfo(newInfo)}}></TextArea></List.Item></List><div className={styles.bottom}><div className={styles.cancel} onClick={() => {Modal.show({title: '提示',content: '放弃发布房源?',closeOnAction: true,actions: [{key: 'cancel',text: '放弃',onClick: () => {navigate(-1)}},{key: 'edit',text: '继续编辑',primary: true}]})}}>取消</div><div className={styles.confirm} onClick={() => {console.log('confirm info: ', info);const params = {...info}if (info.community) {params.community = info.community.community}if (info.houseImg) {const imgs = info.houseImg.map((v) => v.url.replace(baseUrl, ''))params.houseImg = imgs.join('|')}console.log('confirm params: ', params);instance.post('/user/houses', params).then((data) => {console.log('add houses data: ', data);if (data.status === 200) {navigate('/rent')} else {Toast.show('服务开小差,请稍后再试!')}})}}>提交</div></div></div></div>
}

说明:

  • 上传房屋图片,创建 FormData 对象,调用图片上传接口传递 form 参数,并设置请求头 Content-Type 为 multipart/form-data,通过接口返回值获取到图片路径
  • 发布房源,从 state 里面获取到所有的房屋数据,调用发布房源接口传递所有房屋数据。

项目打包

create-react-app 脚手架的 打包文档说明。

简易打包

1、在根目录创建 .env.production 文件,配置生产环境变量:

 REACT_APP_URL = http://localhost:8080REACT_APP_TIME_OUT = 10000

2、打开终端进入项目根目录,输入命令 npm run buildyarn build进行项目打包,生成build文件夹(打包好的项目内容),将build目录中的文件内容,部署到都服务器中即可。

出现以下提示,就代表打包成功,在根目录中就会生成一个build文件夹:

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.File sizes after gzip:209.02 kB  build/static/js/main.b6b1c41b.js10.47 kB   build/static/css/main.ef78ebb8.css1.79 kB    build/static/js/453.b9229bd0.chunk.jsThe project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.The build folder is ready to be deployed.
You may serve it with a static server:npm install -g serveserve -s buildFind out more about deployment here:https://cra.link/deployment

3、也可以通过终端中的提示,使用 serve-s build 来本地查看(需要全局安装工具包 serve)

脚手架的配置说明

create-react-app 中隐藏了 webpack的配置,隐藏在react-scripts包中,两种方式来修改:

  • 运行命令 npm run eject 释放 webpack配置(注意:不可逆)
  • 通过第三方包重写 webpack配置(比如:react-app-rewired 等)

eject 说明:
如果对构建工具和配置选择不满意可以eject随时进行。此命令将从项目中删除单个构建依赖项。
相反,它会将所有配置文件和传递依赖项(Webpack,Babel,ESLint等)作为依赖项复制到项目中package.json。从技术上讲,依赖关系和开发依赖关系之间的区别对于生成静态包的前端应用程序来说是非常随意的。此外,它曾经导致某些托管平台出现问题,这些托管平台没有安装开发依赖项(因此无法在服务器上构建项目或在部署之前对其进行测试)。可以根据需要自由重新排列依赖项 package.json
除了 eject 仍然可以使用所有命令,但它们将指向复制的脚本,以便可以调整它们。在这一点上是独立的。
不必使用eject,策划的功能集适用于中小型部署,不应觉得有义务使用此功能。但是我们知道如果准备好它时无法自定义此工具将无用。

antd-mobile 按需加载

1、安装 npm install react-app-rewired customize-cra --save-dev 用于脚手架重写配置

2、修改package.json 中的 scripts:

  "scripts": {"start": "react-app-rewired start","build": "react-app-rewired build","test": "react-scripts test","eject": "react-scripts eject"}

3、安装 npm install babel-plugin-import --save-dev 插件,用于按需加载组件代码和样式

4、在项目根目录创建文件 config-overrides.js 用于覆盖脚手架默认配置:

const { override, fixBabelImports } = require('customize-cra')module.exports = override(fixBabelImports('import', {libraryName: 'antd-moble',style: 'css'})
)

5、重新打包,发现两次打包的体积并没有变化

打开 Ant Design 按需加载文档,会发现 antd 默认支持基于 ES modules 的 tree shaking,直接引入 import { Button } from 'antd'; 就会有按需加载的效果。

基于路由代码分割(路由懒加载)

将代码按照路由进行分割,只在访问该路由的时候才加载该组件内容,提高首屏加载速度。通过 React.lazy() 方法 + import() 方法、Suspense组件来实现,(React Code-Splitting文档)。

  • React.lazy() 作用: 处理动态导入的组件,让其像普通组件一样使用
  • import(‘组件路径’),作用:告诉webpack,这是一个代码分割点,进行代码分割
  • Suspense组件:用来在动态组件加载完成之前,显示一些loading内容,需要包裹动态组件内容

AuthRoute.js 文件中做如下调整:

import App from '../App.js'
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
import { isLoginContext, setIsLoginContext } from "../utils/auth.js";
import React, { useContext, useState } from "react";
import { instance } from "../utils/api.js";// import Home from '../pages/Home';
// import House from '../pages/House.js';
// import News from '../pages/News.js';
// import Profile from '../pages/Profile.js';
// import CityList from '../pages/CityList';
// import Map from '../pages/Map.js';
// import HouseDetail from "../pages/HouseDetail.js";
// import Login from '../pages/Login.js';
// import Rent from "../pages/Rent.js";
// import RentAdd from "../pages/RentAdd.js";
// import RentSearch from "../pages/RentSearch.js";
// import FormikLearn from "../pages/FormikLearn.js";const Home = React.lazy(() => import('../pages/Home'))
const House = React.lazy(() => import('../pages/House.js'))
const News = React.lazy(() => import('../pages/News.js'))
const Profile = React.lazy(() => import('../pages/Profile.js'))
const CityList = React.lazy(() => import('../pages/CityList'))
const Map = React.lazy(() => import('../pages/Map.js'))
const HouseDetail = React.lazy(() => import('../pages/HouseDetail.js'))
const Login = React.lazy(() => import('../pages/Login.js'))
const Rent = React.lazy(() => import('../pages/Rent.js'))
const RentAdd = React.lazy(() => import('../pages/RentAdd.js'))
const RentSearch = React.lazy(() => import('../pages/RentSearch.js'))
const FormikLearn = React.lazy(() => import('../pages/FormikLearn.js'))function loginRoute(isLogin) { return (route) => {console.log('AuthRoute isLogin: ', isLogin);if (isLogin) {return route}return <Route path={route.props.path} element={<Navigate to='/login' state={{from: {pathname: route.props.path}}} replace></Navigate>}></Route>}
}export default function AuthRoute() {const [isLogin, setIsLogin] = useState(useContext(isLoginContext))// 将修改登录状态函数传递给网络层instance.setIsLogin = setIsLoginreturn <isLoginContext.Provider value={isLogin}><setIsLoginContext.Provider value={setIsLogin}><React.Suspense><Router><Routes>{/* 路由重定向 */}<Route path='/' element={<Navigate to='/home' replace></Navigate>}></Route>{/* 父路由 */}<Route path='/' element={<App></App>}>{/* 子路由 */}<Route path='/home' element={<Home></Home>}></Route><Route path='/house' element={<House></House>}></Route><Route path='/news' element={<News></News>}></Route><Route path='/profile' element={<Profile></Profile>}></Route></Route><Route path='/cityList' element={<CityList></CityList>}></Route><Route path='/map' element={<Map></Map>}></Route><Route path='/detail/:id' element={<HouseDetail></HouseDetail>}></Route><Route path='/login' element={<Login></Login>}></Route>{loginRoute(isLogin)(<Route path='/rent' element={<Rent></Rent>}></Route>)}{loginRoute(isLogin)(<Route path='/rent/add' element={<RentAdd></RentAdd>}></Route>)}{loginRoute(isLogin)(<Route path='/rent/search' element={<RentSearch></RentSearch>}></Route>)}<Route path='/formik' element={<FormikLearn></FormikLearn>}></Route></Routes></Router></React.Suspense></setIsLoginContext.Provider></isLoginContext.Provider>
}

其他性能优化

1、react-virtualized只加载用到的组件 文档,如果只使用少量的组件并增加应用程序的包大小,可以直接导入需要的组件,像这样:

// import { List, AutoSizer, InfiniteLoader } from "react-virtualized";
import AutoSizer from "react-virtualized/dist/commonjs/AutoSizer";
import List from 'react-virtualized/dist/commonjs/List';
import InfiniteLoader from 'react-virtualized/dist/commonjs/InfiniteLoader';

2、脚手架配置解决跨域问题,代理 API 请求 文档,首先,使用 npm 或 Yarn 安装 http-proxy-middleware:

npm install http-proxy-middleware --save
$ # or
$ yarn add http-proxy-middleware

接下来,创建 src/setupProxy.js 现在可以根据需要注册代理:

const { createProxyMiddleware } = require('http-proxy-middleware');module.exports = function (app) {app.use('/api',createProxyMiddleware({target: 'http://localhost:5000',changeOrigin: true,}));
};

注意:不需要将此文件导入到任何地方。当启动开发服务器时,它会自动注册。该文件仅支持 Node 的 JavaScript 语法。确保仅使用受支持的语言功能(即不支持 Flow、ES 模块等)。将路径传递给代理函数允许在路径上使用通配符和/或模式匹配,这比快速路由匹配更灵活。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/77648.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

100道C#高频经典面试题带解析答案——全面C#知识点总结

100道C#高频经典面试题带解析答案 以下是100道C#高频经典面试题及其详细解析&#xff0c;涵盖基础语法、面向对象编程、集合、异步编程、LINQ等多个方面&#xff0c;旨在帮助初学者和有经验的开发者全面准备C#相关面试。 &#x1f9d1; 博主简介&#xff1a;CSDN博客专家、CSD…

机动车号牌管理系统设计与实现(代码+数据库+LW)

摘 要 在如今社会上&#xff0c;关于信息上面的处理&#xff0c;没有任何一个企业或者个人会忽视&#xff0c;如何让信息急速传递&#xff0c;并且归档储存查询&#xff0c;采用之前的纸张记录模式已经不符合当前使用要求了。所以&#xff0c;对机动车号牌信息管理的提升&…

VMWare Workstation Pro17.6最新版虚拟机详细安装教程(附安装包教程)

目录 前言 一、VMWare虚拟机下载 二、VMWare虚拟机安装 三、运行虚拟机 前言 VMware 是全球领先的虚拟化技术与云计算解决方案提供商&#xff0c;通过软件模拟计算机硬件环境&#xff0c;允许用户在一台物理设备上运行多个独立的虚拟操作系统或应用。其核心技术可提升硬件…

DeepSeek的神经元革命:穿透搜索引擎算法的下一代内容基建

DeepSeek的神经元革命&#xff1a;穿透搜索引擎算法的下一代内容基建 ——从语义网络到价值共识的范式重构 一、搜索引擎的“内容饥渴症”与AI的基建使命 2024年Q1数据显示&#xff0c;百度索引网页总数突破3500亿&#xff0c;但用户点击集中在0.78%的高价值页面。这种“数据…

docker安装nginx,基础命令,目录结构,配置文件结构

Nginx简介 Nginx是一款轻量级的Web服务器(动静分离)/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器。其特点是占有内存少&#xff0c;并发能力强. &#x1f517;官网 docker安装Nginx &#x1f433; 一、前提条件 • 已安装 Docker&#xff08;dock…

Python Lambda表达式详解

Python Lambda表达式详解 1. Lambda是什么&#xff1f; Lambda是Python中用于创建匿名函数&#xff08;没有名字的函数&#xff09;的关键字&#xff0c;核心特点是简洁。它适用于需要临时定义简单函数的场景&#xff0c;或直接作为参数传递给高阶函数&#xff08;如map()、f…

基础知识补充篇:什么是DAPP前端连接中的provider

专栏:区块链入门到放弃查看目录-CSDN博客文章浏览阅读352次。为了方便查看将本专栏的所有内容列出目录,按照顺序查看即可。后续也会在此规划一下后续内容,因此如果遇到不能点击的,代表还没有更新。声明:文中所出观点大多数源于笔者多年开发经验所总结,如果你想要知道区块…

P1115 最大子段和

P1115 最大子段和 - 洛谷 题目描述 给出一个长度为 n 的序列 a&#xff0c;选出其中连续且非空的一段使得这段和最大。 输入格式 第一行是一个整数&#xff0c;表示序列的长度 n。 第二行有 n 个整数&#xff0c;第 i 个整数表示序列的第 i 个数字 aᵢ。 输出格式 输出一…

用实体识别模型提取每一条事实性句子的关键词(实体),并保存到 JSON 文件中

示例代码&#xff1a; # Generate Keywords import torch import os from tqdm import tqdm import json import nltk import numpy as npfrom span_marker import SpanMarkerModelmodel SpanMarkerModel.from_pretrained("tomaarsen/span-marker-mbert-base-multinerd&…

E8流程多行明细行字符串用I分隔,赋值到主表

需求&#xff1a;明细行摘要字段赋值到主表隐藏字段&#xff0c;隐藏摘要字段在标题中显示 代码如下&#xff0c;代码中的获取字段名获取方式&#xff0c;自行转换成jQuery("#fieldid").val()替换。 //1:参数表单id 2:流程字段名 3:0代表主表&#xff0c;1代表明细…

优化你的 REST Assured 测试:设置默认主机与端口、GET 请求与断言

REST Assured 是一个功能强大的 Java 库&#xff0c;用于测试 RESTful Web 服务。它简化了 API 测试流程&#xff0c;提供了一整套用于高效验证响应的工具。在本篇博客中&#xff0c;我们将深入探讨几个核心概念&#xff0c;包括如何设置默认主机和端口、如何发起 GET 请求以及…

3.1.3.4 Spring Boot使用使用Listener组件

在Spring Boot中&#xff0c;使用Listener组件可以监听和响应应用中的各种事件。首先&#xff0c;创建自定义事件类CustomEvent&#xff0c;继承自ApplicationEvent。然后&#xff0c;创建事件监听器CustomEventListener&#xff0c;使用EventListener注解标记监听方法。接下来…

【 vue + js 】引入图片、base64 编译显示图片

一、引入普通图片 1、代码示例&#xff1a; <div class"question"><!-- 错误写法 --><el-empty image"../assets/noinformation.svg" description"暂无问卷"><el-button type"primary">按钮</el-button&…

JVM 之 String 引用机制解析:常量池、堆内存与 intern 方法

关于常量池中的String类型的数据&#xff0c;在JDK6中只可能是对象&#xff0c;在JDK7中既可以是对象也可以是引用 案例一&#xff1a; String s1 new String("1"); String s2 "1"; System.out.println(s1 s2);s1: 执行 new String("1")&am…

数据库管理-第313期 分布式挑战单机,OceanBase单机版试玩(20250411)

数据库管理313期 2025-04-11 数据库管理-第313期 分布式挑战单机&#xff0c;OceanBase单机版试玩&#xff08;20250411&#xff09;1 环境说明2 操作系统配置2.1 关闭防火墙2.2 关闭SELinux2.3 配置hosts文件2.4 配置本地yum源2.5 配置sysctl.conf2.6 配置limits.conf2.7 创建…

AI 之 LLM(大语言模型)是如何生成文本的!

你是不是也曾在朋友面前自信满满地说&#xff1a;“AI我可太熟了&#xff01;”然后随便丢一句“写篇短文”给大模型&#xff0c;坐等它秒出结果&#xff1f;但你有没有想过&#xff0c;那几秒钟里&#xff0c;AI到底干了什么&#xff1f;从你敲下的几个字&#xff0c;到屏幕上…

STL之序列式容器(Vector/Deque/List)

序列式容器 序列式容器包括&#xff1a;静态数组 array 、动态数组 vector 、双端队列 deque 、单链表 forward_ list 、双链表 list 。这五个容器中&#xff0c;我们需要讲解三个 vector 、 deque 、 list 的使 用&#xff0c;包括&#xff1a;初始化、遍历、尾部插入与删除、…

swift菜鸟教程6-10(运算符,条件,循环,字符串,字符)

一个朴实无华的目录 今日学习内容&#xff1a;1.Swift 运算符算术运算符比较运算符逻辑运算符位运算符赋值运算区间运算符其他运算符 2.Swift 条件语句3.Swift 循环4.Swift 字符串字符串属性 isEmpty字符串常量let 变量var字符串中插入值字符串连接字符串长度 String.count使用…

泛微ECOLOGY9 记 数据展现集成 自定义开窗测试中对SQL 的IN语法转换存在BUG

背景 搭建流程时&#xff0c;需将明细表1中的合同字段 供明细表2中的合同开窗查询使用。 最终实现如下图&#xff1a; 选择 发票号时&#xff0c;自动带出明细表1中的采购合同号清单&#xff0c;然后在明细表2中开窗采购合同号时&#xff0c;只跳出明细表1中有的采购合同号&am…

(自用)蓝桥杯准备(需要写的基础)

要写的文件 led_app lcd_app key_app adc_app usart_app scheduler LHF_SYS一、外设引脚配置 1. 按键引脚 按键引脚配置如下&#xff1a; B1&#xff1a;PB0B2&#xff1a;PB1B3&#xff1a;PB2B4&#xff1a;PA0 2. LCD引脚 LCD引脚配置如下&#xff1a; GPIO_Pin_9 /* …