目录

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} = data
                    if (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} = data
                  if (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 yupYup 文档),导入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} = userInfo
    const 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);
                                                  // 移除 token
                                                  removeToken()
                                                  setIsLogin(isAuth)
    
                                                  // 清除用户信息
                                                  setUserInfo({
                                                      avatar: '',
                                                      nickname: ''
                                                  })
                                              })
                                          }
                                      }
                                  ]
                              })
                          } else {
                              navigate('/login')
                          }
                      }}>{isLogin ? '退出' : '去登录'}</span>

获取用户信息

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

复制代码
    useEffect(() =>  {
        let ignore = false
        if (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} = config
    Toast.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.data
    if (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 = setIsLogin
    
    return <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} = data
                  if (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}>
                    <img
                    src={
                        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'}})
                                  }
                              ]
                          })
                      }
                  }}>
                      <img
                      src={
                          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 = null

export 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 = v
                        setInfo(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 = v
                        setInfo(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 = v
                        setInfo(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 = v
                        console.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 = v
                        setInfo(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:8080
 REACT_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.js
  10.47 kB   build/static/css/main.ef78ebb8.css
  1.79 kB    build/static/js/453.b9229bd0.chunk.js

The 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 serve
  serve -s build

Find 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 = setIsLogin

    return <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 模块等)。将路径传递给代理函数允许在路径上使用通配符和/或模式匹配,这比快速路由匹配更灵活。

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
掘金一周2 分钟前
掘金的广告越来越烦人了,悄悄把它隐藏起来🤫 | 掘金一周 4.23
前端·人工智能·后端
Go_going_19 分钟前
【解决 el-table 树形数据更新后视图不刷新的问题】
前端·javascript·vue.js
进取星辰30 分钟前
10、Context:跨维度传音术——React 19 状态共享
前端·react.js·前端框架
wfsm31 分钟前
react使用01
前端·javascript·react.js
小小小小宇42 分钟前
Vue 3 的批量更新机制
前端
阳光普照世界和平1 小时前
从单点突破到链式攻击:XSS 的渗透全路径解析
前端·web安全·xss
MrsBaek1 小时前
前端笔记-AJAX
前端·笔记·ajax
前端Hardy1 小时前
第4课:函数基础——JS的“魔法咒语”
前端·javascript
孟陬1 小时前
如何检测 Network 请求异常 - PerformanceObserver
前端
前端小棒槌1 小时前
vue .sync 和 v-model 区别
前端