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='m2'><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.pswp.cn/diannao/77648.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/77648.shtml
英文地址,請注明出處:http://en.pswp.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 /* …