```
- 在FilterMore组件中,讲获取到的选中值,设置为子组件状态selectedValues的默认值
```react
state = {
selectedValues: this.props.defaultValues
};
```
## 完善FilterTitle高亮功能(★★)
- 在Filter组件的onTitleClick方法中,添加type为more的判断条件
- 当选中值数组长度不为0的时候,表示FilterMore组件中有选中项,此时,设置选中状态高亮
- 点击确定按钮时,根据参数type和value,判断当前菜单是否高亮
```react
// 保存,隐藏对话框
onSave = (type, value) => {
const { titleSelectedStatus } = this.state;
let newTitleSelectedStatus = { ...titleSelectedStatus };
let selectedVal = value;
if (
type === "area" &&
(selectedVal.length !== 2 || selectedVal[0] !== "area")
) {
newTitleSelectedStatus[type] = true;
} else if (type === "mode" && selectedVal[0] !== "null") {
newTitleSelectedStatus[type] = true;
} else if (type === "price" && selectedVal[0] !== "null") {
newTitleSelectedStatus[type] = true;
} else if (type === "more" && selectedVal.length !== 0) {
// 更多选择
newTitleSelectedStatus[type] = true;
} else {
newTitleSelectedStatus[type] = false;
}
this.setState({
openType: "",
titleSelectedStatus: newTitleSelectedStatus,
selectedValues: {
...this.state.selectedValues,
[type]: value
}
});
};
```
- 在关闭对话框时(onCancel),根据type和当前type的选中值,判断当前菜单是否高亮
```react
// 取消
onCancel = type => {
const { titleSelectedStatus, selectedValues } = this.state;
let newTitleSelectedStatus = { ...titleSelectedStatus };
let selectedVal = selectedValues[type];
if (
type === "area" &&
(selectedVal.length !== 2 || selectedVal[0] !== "area")
) {
newTitleSelectedStatus[type] = true;
} else if (type === "mode" && selectedVal[0] !== "null") {
newTitleSelectedStatus[type] = true;
} else if (type === "price" && selectedVal[0] !== "null") {
newTitleSelectedStatus[type] = true;
} else if (type === "more" && selectedVal.length !== 0) {
// 更多选择
newTitleSelectedStatus[type] = true;
} else {
newTitleSelectedStatus[type] = false;
}
// 隐藏对话框
this.setState({
openType: "",
titleSelectedStatus: newTitleSelectedStatus
});
};
```
# 列表找房模块-获取房屋列表数据
## 目标
- 能够封装筛选条件对象
- 能够获取到房屋列表数据
- 能够实现HouseItem组件在map页面和房屋列表页面的复用
- 能够渲染房屋列表数据在页面
## 组装筛选条件(★★★)
- 在Filter组件的onSave方法中,根据最新selectedValues组装筛选的条件数据 filters,以下是数据格式

- 获取区域数据的参数名:area 或 subway(选中值,数组的第一个元素)
- 获取区域数据值(以最后一个value为准)
- 获取方式和租金的值(选中值得第一个元素)
- 获取筛选(more)的值(讲选中值数组转换为以逗号分隔的字符串)
```react
// 组拼数据格式
let newSelectedValues = {
...selectedValues,
[type]: value
};
const { area, mode, price, more } = newSelectedValues;
// 筛选条件数据
const filters = {};
// 区域
const areaKey = area[0];
let areaValue = "null";
if (area.length === 3) {
areaValue = area[2] !== "null" ? area[2] : area[1];
}
filters[areaKey] = areaValue;
// 方式和租金
filters.mode = mode[0];
filters.price = price[0];
// more
filters.more = more.join(",");
```
## 获取房屋数据(★★★)
- 将筛选条件数据filters传递给父组件HouseList
```react
// 保存,隐藏对话框
onSave = (type, value) => {
...
this.props.onFilter(filters)
...
};
```
- HouseList组件中,创建方法onFilter,通过参数接收filters数据,并存储到this中
```react
// 提供给Filter组件调用的函数,接受参数 filters
onFilter = filters => {
this.filters = filters;
this.searchHouseList();
};
```
- 创建方法searchHouseList(用来获取房屋列表数据)
- 根据接口,获取当前定位城市id参数
- 将筛选条件数据与分页数据合并后,作为借口的参数,发送请求,获取房屋数据
```react
// 获取房源列表的函数
async searchHouseList() {
// 获取城市信息
let { value } = JSON.parse(localStorage.getItem("localCity"));
// 请求数据
let res = await instance.get("/houses", {
cityId: value,
...this.filters,
start: 1,
end: 20
});
}
```
## 进入页面时获取数据(★★)
- 在componentDidMount钩子函数中,调用searchHouseList,来获取房屋列表数据
- 给HouseList组件添加属性 filters,值为对象
```react
// 初始化属性
filters = {};
```
- 添加两个状态:list和count(存储房屋列表数据和总条数)
```react
state = {
list: [],
count: 0
};
```
- 将获取到的房屋数据,存储在state中
```react
// 获取房源列表的函数
async searchHouseList() {
// 获取城市信息
let { value } = JSON.parse(localStorage.getItem("localCity"));
// 请求数据
let res = await instance.get("/houses", {
cityId: value,
...this.filters,
start: 1,
end: 20
});
let { list, count } = res.data.body;
this.setState({
list: list,
count: count
});
}
```
## 使用List组件渲染数据(★★★)
- 封装HouseItem组件,实现map和HouseListuemian中,房屋列表项的复用
```react
import React from 'react'
import PropTypes from 'prop-types'
import styles from './index.module.css'
function HouseItem({ src, title, desc, tags, price, onClick }) {
return (
{title}
{desc}
{/* ['近地铁', '随时看房'] */}
{tags.map((tag, index) => {
const tagClass = 'tag' + (index + 1)
return (
{tag}
)
})}
{price} 元/月
)
}
HouseItem.propTypes = {
src: PropTypes.string,
title: PropTypes.string,
desc: PropTypes.string,
tags: PropTypes.array.isRequired,
price: PropTypes.number,
onClick: PropTypes.func
}
export default HouseItem
```
- 使用HouseItem组件改造Map组件的房屋列表项
```react
renderHousesList() {
return this.state.housesList.map(item => (
)
)
}
```
- 使用react-virtualized的List组件渲染房屋列表(参考CityList组件的使用)
```react
// 渲染每一行的内容
renderHouseList = ({
key, // Unique key within array of rows
index, // 索引号
style // 重点属性:一定要给每一个行数添加该样式
}) => {
// 当前这一行的
const { list } = this.state;
const house = list[index];
return (
);
};
render(){
return (
)
}
```
# 列表找房模块-房屋列表
## 目标
- 能够使用windowScroller组件解决整个页面无法滚动的问题
- 能够使用InfiniteLoader组件来实现加载更多逻辑
## 使用`WindowScroller` 跟随页面滚动(★★★)
- **默认:**List组件只让组件自身出现滚动条,无法让整个页面滚动,也就无法实现标题吸顶功能
- **解决方式:**使用`WindowScroller`高阶组件,让List组件跟随页面滚动(为List组件提供状态,同时还需要设置List组件的autoHeight属性)
- **注意:**`WindowScroller`高阶组件只能提供height,无法提供width
- **解决方式:**在WindowScroller组件中使用AutoSizer高阶组件来为List组件提供width

```react
{({ height, isScrolling, scrollTop }) => (
{({ width }) => (
)}
)}
```
## `InfiniteLoader` 组件(★★★)
- 滚动房屋列表时候,动态加载更多房屋数据
- 使用`InfiniteLoader` 组件,来实现无限滚动列表,从而加载更多房屋数据
- 根据 `InfiniteLoader` 文档示例,在项目中使用组件

```react
{({ onRowsRendered, registerChild }) => (
{({ height, isScrolling, scrollTop }) => (
{({ width }) => (
)}
)}
)}
// 判断每一行数据是否加载完毕
isRowLoaded = ({ index }) => {
return !!this.state.list[index];
};
// 用来获取更多房屋列表数据
// 注意,该方法的返回值是一个 Promise 对象,并且,这个对象应该在数据加载完成时,来调用 resolve让 Promise对象的状态变为已完成
loadMoreRows = ({ startIndex, stopIndex }) => {
return new Promise(resolve => {
...
});
};
```
## 加载更多房屋列表数据
- 在loadMoreRows方法中,根据起始索引和结束索引,发送请求,获取更多房屋数据
- 获取到最新的数据后,与当前list中的数据合并,再更新state,并调用Promise的resolve
```react
loadMoreRows = ({ startIndex, stopIndex }) => {
return new Promise(resolve => {
instance
.get("/houses", {
params: {
cityId: value,
...this.filters,
start: startIndex,
end: stopIndex
}
})
.then(res => {
this.setState({
list: [...this.state.list, ...res.data.body.list]
});
// 加载数据完成时,调用resolve即可
resolve();
});
});
};
```
- 在renderHouseList方法中,判断house是否存在
- 不存在的时候,就渲染一个loading元素
- 存在的时候,再渲染HouseItem组件
```react
// 渲染每一行的内容
renderHouseList = ({
key, // Unique key within array of rows
index, // 索引号
style // 重点属性:一定要给每一个行数添加该样式
}) => {
// 当前这一行的
const { list } = this.state;
const house = list[index];
// 如果不存在,需要渲染loading元素占位
if (!house) {
return (
);
}
return (
...
);
};
```
# 列表找房模块-吸顶功能(★★★)
## 目标
- 能够完成吸顶的功能
- 能够封装通用的Sticky组件
## 实现思路
- 在页面滚动的时候,判断筛选栏上边是否还在可视区域内
- 如果在,不需要吸顶
- 如果不在,就吸顶
- 吸顶之后,元素脱标,房屋列表会突然往上调动筛选栏的高度,解决这个问题,我们用一个跟筛选栏相同的占位元素,在筛选栏脱标后,代替它撑起高度
## 实现步骤
- 封装Sticky组件
- 在HouseList页面中,导入Sticky组件
- 使用Sticky组件包裹要实现吸顶功能的Filter组件
```react
```
- 在Sticky组件中,创建两个ref对象(placeholder,content),分别指向占位元素和内容元素
```react
class Sticky extends Component {
// 创建ref对象
placeholder = createRef()
content = createRef()
...
render() {
return (
{/* 占位元素 */}
{/* 内容元素 */}
{this.props.children}
)
}
```
- 在组件中,监听浏览器的scroll事件
```react
// 监听 scroll 事件
componentDidMount() {
window.addEventListener('scroll', this.handleScroll)
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll)
}
```
- 在scroll事件中,通过getBoundingClientRect()方法得到筛选栏占位元素当前位置
- 判断top是否小于0(是否在可视区内)
- 如果小于,就添加需要吸顶样式(fixed),同时设置占位元素高度
- 否则,就移除吸顶样式,同时让占位元素高度为0
```react
// scroll 事件的事件处理程序
handleScroll = () => {
// 获取DOM对象
const placeholderEl = this.placeholder.current
const contentEl = this.content.current
const { top } = placeholderEl.getBoundingClientRect()
if (top < 0) {
// 吸顶
contentEl.classList.add(styles.fixed)
placeholderEl.style.height = '40px'
} else {
// 取消吸顶
contentEl.classList.remove(styles.fixed)
placeholderEl.style.height = '0px'
}
}
```
## 通用性优化
- 现在Sticky组件中占位元素的高度是写死的,这样就不通用了,我们可以把这个高度通过参数来进行传递,组件内部通过props就可以来进行获取了
```react
// HouseList组件
// Sticky 组件
handleScroll = () => {
const { height } = this.props;
...
const { top } = placeholderEl.getBoundingClientRect();
if (top < 0) {
// 吸顶
contentEl.classList.add(styles.fixed);
placeholderEl.style.height = `${height}px`;
} else {
// 取消吸顶
contentEl.classList.remove(styles.fixed);
placeholderEl.style.height = "0px";
}
};
```
# 列表找房模块-优化(★★★)
## 目标
- 学习在已有代码上寻找bug的思路(定位bug原因,思考解决办法)
## 加载提示
- 实现加载房源数据时:加载中、加载完成的提示(需要解决:没有房源数据时,不弹提示框)
- 判断一下count是否为0,如果为0,就不加载提示信息
- 找不到房源数据时候的提示(需要解决:进入页面就展示该提示的问题)
- 判断一下是否是第一次进入,可以用一个变量来进行记录,然后只要进行了数据请求,就把这个标识更改
## 条件筛选栏优化
- 点击条件筛选栏,展示FilterPicker组件时,样式错乱问题(需要解决:样式问题)
- 把FilterPicker组件修改成绝对定位,脱标了,就不会挤下面的结构了
- 使用条件筛选查询数据时,页面没有回到顶部(需要解决:每次重新回到页面顶部)
- 在点击条件查询确定按钮的时候,利用window.scroll(0 ,0) 来回到页面顶部
- 点击条件筛选栏,展示对话框后,页面还会滚动(需要解决:展示对话框后页面不滚动)
- 展示对话框的时候,给body添加 overflow: hidden 的样式,这样页面就不能进行滚动,等到对话框消失的时候,取消body的 overflow: hidden 样式
## 切换城市显示房源优化
- 切换城市或,该页面无法展示当前定位城市名称和当前城市房源数据,刷新页面后才会生效(需要解决:切换城市后立即生效)
- 在组件的 componentDidMount构造函数中,进行获取当前定位城市名称
# react-spring动画库(★★★)
## 目标
- 知道react-spring组件库的优势
- 能够参照官方文档,使用react-spring进行简单的透明度变化的动画
- 能够实现遮罩层动画效果
## 概述
- 场景:展示筛选对话框的时候,实现动画效果,增强用户体验
- react-spring是基于spring-physics(弹簧物理)的react动画库,动画效果更加流畅、自然
- 优势:
- 几乎可以实现任意UI动画效果
- 组件式使用方式(render-props模式),简单易用,符合react的声明式特性,性能高
- [github地址](https://github.com/react-spring/react-spring)
- [官方文档](https://www.react-spring.io/docs/props/spring)
## 基本使用
- 安装: yarn add react-spring
- 打开Spring组件文档
- 导入Spring文档,使用Spring组件包裹要实现动画效果的遮罩层div
- 通过render-props模式,讲参数props设置为遮罩层div的style
- 给Spring组件添加from属性,指定:组件第一次渲染时的动画状态
- 给Spring组件添加to属性,指定:组件要更新的新动画状态
- props就是透明度有0~1中变化的值

## 实现遮罩层动画
- 创建方法 renderMask来渲染遮罩层 div
- 修改渲染遮罩层的逻辑,保证Spring组件一直都被渲染(Spring组件被销毁了,就无法实现动画效果)
- 修改to属性的值,在遮罩层隐藏时为0,在遮罩层展示为1
- 在render-props的函数内部,判断props.opacity是否等于0
- 如果等于0,就返回null,解决遮罩层遮挡页面导致顶部点击事件失效
- 如果不等于0,渲染遮罩层div
```react
renderMask() {
const { openType } = this.state
const isHide = openType === 'more' || openType === ''
return (
{props => {
// 说明遮罩层已经完成动画效果,隐藏了
if (props.opacity === 0) {
return null
}
return (
this.onCancel(openType)}
/>
)
}}
)
```
# 房屋详情模块-准备工作
## 目标
- 看懂模板HouseDetail的结构
- 能够获取到数据,渲染到组件上
- 能够配置通用路由规则,并且获取路由参数
## 模板说明
- 创建房屋详情页面HouseDetail
- 修改NavHeader组件(添加了className和rightContent两个props)
- 创建了HousePackage组件(房屋配套)
- 这些模板已经提供好,可以直接来使用
```react
// 添加 className 和 rightContent(导航栏右侧内容) 属性
function NavHeader({
children,
history,
onLeftClick,
className,
rightContent
}) {
// 默认点击行为
const defaultHandler = () => history.go(-1)
return (
}
onLeftClick={onLeftClick || defaultHandler}
rightContent={rightContent}
>
{children}
)
}
// 添加props校验
NavHeader.propTypes = {
...
rightContent: PropTypes.array
}
// withRouter(NavHeader) 函数的返回值也是一个组件
export default withRouter(NavHeader)
```
## 路由参数(★★★)
- 房源有多个,那么URL路径也就有多个,那么需要多少个路由规则来匹配呢?一个还是多个?
- 使用 一个 路由规则匹配不同的URL路径,同时I获取到URL中不同的内容,利用路由参数来解决
- 让一个路由规则,同时匹配多个符合该规则的URL路径
- 语法:/detail/:id ,其中:id 就是路由参数

- 获取路由参数: props.match.params

## 展示房屋详情(★★★)
- 在找房页面中,给每一个房源列表添加点击事件,在点击时跳转到房屋详情页面
- 在单击事件中,获取到当前房屋id
- 根据房屋详情的路由地址,调用history.push() 实现路由跳转
```react
this.props.history.push(`/detail/${house.houseCode}`)}
// 注意:该组件中应该接收 style,然后给组件元素设置样式!!!
style={style}
src={BASE_URL + house.houseImg}
title={house.title}
desc={house.desc}
tags={house.tags}
price={house.price}
/>
```
- 封装getHouseDetail方法,在componentDidMount中调用该方法
```react
componentDidMount() {
// 获取房屋数据
this.getHouseDetail()
}
```
- 在方法中,通过路由参数获取到当前房屋id
- 使用API发送请求,获取房屋数据,保存到state中
```react
async getHouseDetail() {
const { id } = this.props.match.params
// 开启loading
this.setState({
isLoading: true
})
const res = await API.get(`/houses/${id}`)
this.setState({
houseInfo: res.data.body,
isLoading: false
})
const { community, coord } = res.data.body
// 渲染地图
this.renderMap(community, coord)
}
```
- 使用房屋数据,渲染页面
- 解构出需要的数据
```react
const {
isLoading,
houseInfo: {
community,
title,
price,
roomType,
size,
floor,
oriented,
supporting,
description
}
} = this.state
```
- 渲染小区名称
```react
{/* 导航栏 */}
]}
>
{community}
```
- 渲染轮播图
```react
// 渲染轮播图结构
renderSwipers() {
const {
houseInfo: { houseImg }
} = this.state
return houseImg.map(item => (
))
}
```
- 渲染标签
```react
// 渲染标签
renderTags() {
const {
houseInfo: { tags }
} = this.state
return tags.map((item, index) => {
// 如果标签数量超过3个,后面的标签就都展示位第三个标签的样式
let tagClass = ''
if (index > 2) {
tagClass = 'tag3'
} else {
tagClass = 'tag' + (index + 1)
}
return (
{item}
)
})
}
```
- 渲染价格,房型,面积等
```react
{price}
/月
租金
{roomType}
房型
{size}平米
面积
```
- 渲染装修,楼层,朝向等
```react
装修:
精装
楼层:
{floor}
朝向:
{oriented.join('、')}
类型:普通住宅
```
- 渲染地图
```react
// 渲染地图
renderMap(community, coord) {
const { latitude, longitude } = coord
const map = new BMap.Map('map')
const point = new BMap.Point(longitude, latitude)
map.centerAndZoom(point, 17)
const label = new BMap.Label('', {
position: point,
offset: new BMap.Size(0, -36)
})
label.setStyle(labelStyle)
label.setContent(`
${community}
`)
map.addOverlay(label)
}
```
- 渲染房屋配套
```react
{/* 房屋配套 */}
房屋配套
{/* 判断是否有数据 */}
{supporting.length === 0 ? (
暂无数据
) : (
)}
```
- 渲染房屋概况
```react
房源概况
{description || '暂无房屋描述'}
```
- 渲染推荐,可以复用 HouseItem组件
```react
猜你喜欢
{recommendHouses.map(item => (
))}
```
# 好客租房移动Web(中)小结
- 地图找房模块:百度地图API,地图覆盖物,CSS Modules解决样式覆盖问题,脚手架环境变量,axios公共URL配置
- 列表找房模块:条件筛选组件封装(变化点),房源列表,react-virtualized(InfiniteLoader,WindowScroller),react-spring动画库
- 房屋详情模块:路由参数(/:id 和 props.match.params),展示房屋详情
# 登录模块(★★★)
## 目标
- 能够看懂登录页面的模板结构(/Login/index.js)
- 能够把文本框和密码框设置为受控组件
- 能够给form表单绑定onSubmit事件,取消默认行为
- 能够获取用户名和密码请求服务器,保存token
- 能够说出token的作用
## 功能分析
- 用户登录
- 我的页面
- 封装路由访问控制组件
## 用户登录

对应结构:
```react
{/* 顶部导航 */}
账号登录
{/* 登录表单 */}
还没有账号,去注册~
```
功能实现:
- 添加状态:username和password
```react
state = {
username: '',
password: ''
}
```
- 使用受控组件方式获取表单元素值
```react
getUserName = e => {
this.setState({
username: e.target.value
})
}
getPassword = e => {
this.setState({
password: e.target.value
})
}
render() {
const { username, password } = this.state
return (
)
}
}
```
- 给form表单添加 onSubmit
- 创建方法 handleSubmit,实现表单提交
```react
// 表单提交事件的事件处理程序
handleSubmit = async e => {
// 阻止表单提交时的默认行为
e.preventDefault()
...
}
```
- 在方法中,通过username和password获取到账号和密码
- 使用API调用登录接口,将username和password作为参数
- 判断返回值status为200时候,表示登录成功
- 登录成功后,将token保存到本地存储中(hkzf_token)
- 返回登录前的页面
```react
// 表单提交事件的事件处理程序
handleSubmit = async e => {
// 阻止表单提交时的默认行为
e.preventDefault()
// 获取账号和密码
const { username, password } = this.state
// console.log('表单提交了', username, password)
// 发送请求
const res = await API.post('/user/login', {
username,
password
})
console.log('登录结果:', res)
const { status, body, description } = res.data
if (status === 200) {
// 登录成功
localStorage.setItem('hkzf_token', body.token)
this.props.history.go(-1)
} else {
// 登录失败
Toast.info(description, 2, null, false)
}
}
```
## 表单验证说明
- 表单提交前,需要先进性表单验证,验证通过后再提交表单
- 方式一:antd-mobile 组件库的方式(需要InputItem文本输入组件)
- 推荐:使用更通用的 formik,React中专门用来进行表单处理和表单校验的库

# formik
## 目标
- 知道formik的作用
- 能够参照文档来实现简单的表单校验
- 能够给登录功能添加表单校验
- 能够使用formik中提供的组件:Form, Field, ErrorMessage,来对登录模块进行优化
## 介绍
- Github地址:[formik文档](http://jaredpalmer.com/formik/docs/overview)
- 场景:表单处理,表单验证
- 优势:轻松处理React中的复杂表单,包括:获取表单元素的值,表单验证和错误信息,处理表单提交,并且将这些内容放在一起统一处理,有利于代码阅读,重构,测试等
- 使用两种方式:1. 高阶组件(withFormik) 2. render-props( {}} />)
## formik来实现表单校验(★★★)
### 重构
- 安装: yarn add formik
- 导入 withFormik,使用withFormit 高阶组件包裹Login组件
- 为withFormit提供配置对象: mapPropsToValues / handleSubmit
- 在Login组件中,通过props获取到values(表单元素值对象),handleSubmit,handleChange
- 使用values提供的值,设置为表单元素的value,使用handleChange设置为表单元素的onChange
- 使用handleSubmit设置为表单的onSubmit
- 在handleSubmit中,通过values获取到表单元素值
- 在handleSubmit中,完成登录逻辑

```react
// Login组件中
render() {
// const { username, password } = this.state
// 通过 props 获取高阶组件传递进来的属性
const { values, handleSubmit, handleChange } = this.props
return (
{/* 顶部导航 */}
账号登录
{/* 登录表单 */}
还没有账号,去注册~
)
}
// 使用 withFormik 高阶组件包装 Login 组件,为 Login 组件提供属性和方法
Login = withFormik({
// 提供状态:
mapPropsToValues: () => ({ username: '', password: '' }),
// 表单的提交事件
handleSubmit: async (values, { props }) => {
// 获取账号和密码
const { username, password } = values
// 发送请求
const res = await API.post('/user/login', {
username,
password
})
console.log('登录结果:', res)
const { status, body, description } = res.data
if (status === 200) {
// 登录成功
localStorage.setItem('hkzf_token', body.token)
// 注意:无法在该方法中,通过 this 来获取到路由信息
// 所以,需要通过 第二个对象参数中获取到 props 来使用 props
props.history.go(-1)
} else {
// 登录失败
Toast.info(description, 2, null, false)
}
}
})(Login)
```
### 两种表单验证方式
- 两种方式
- 通过validate 配置手动校验

- 通过 validationSchema 配置项配合Yup来校验

- 推荐: validationSchema配合Yup的方式进行表单校验
### 给登录功能添加表单验证
- 安装: yarn add yup ([Yup 文档](https://github.com/jquense/yup)),导入Yup
```react
// 导入Yup
import * as Yup from 'yup'
```
- 在 withFormik 中添加配置项 validationSchema,使用 Yup 添加表单校验规则

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

示例代码:
```react
// 使用 withFormik 高阶组件包装 Login 组件,为 Login 组件提供属性和方法
Login = withFormik({
...
// 添加表单校验规则
validationSchema: Yup.object().shape({
username: Yup.string()
.required('账号为必填项')
.matches(REG_UNAME, '长度为5到8位,只能出现数字、字母、下划线'),
password: Yup.string()
.required('密码为必填项')
.matches(REG_PWD, '长度为5到12位,只能出现数字、字母、下划线')
}),
...
})(Login)
```
在结构中需要渲染错误信息:
```react
{/* 登录表单 */}
```
### 简单处理
- 导入 Form组件,替换form元素,去掉onSubmit

- 导入Field组件,替换input表单元素,去掉onChange,onBlur,value

- 导入 ErrorMessage 组件,替换原来的错误消息逻辑代码

- 去掉所有 props
示例代码:
```react
// 导入withFormik
import { withFormik, Form, Field, ErrorMessage } from 'formik'
```
# 我的页面
## 目标
- 我的页面能够实现判断用户登录状态来显示不同的效果
- 能够实现退出登录功能
## 结构和样式
- 对应的结构样式可以直接拿过来用,我们最主要要实现里面的代码逻辑,文件在 pages/Profile/index
```react
render() {
return (
{/* 个人信息 */}
{nickname || '游客'}
{/* 登录后展示: */}
{isLogin ? (
<>
退出
编辑个人资料
>
) : (
)}
{/* 未登录展示: */}
{/* 九宫格菜单 */}
item.to ? (
{item.name}
) : (
{item.name}
)
}
/>
{/* 加入我们 */}
)
}
```
## 功能分析
- 判断是否登录(本地缓存中是否有token信息,直接调用isAuth() 方法即可,这里在utils/auth.js文件中已经封装好了)
- 如果登录了,就发送请求获取个人资料,并且在页面中展示个人资料
- 如果没有登录,则不获取个人资料,只在页面中展示未登录信息
- 在页面中展示登录或未登录信息,就要通过state变化来体现,因此,需要一个标示是否登录的状态

## 判断用户是否登陆步骤(★★★)
- 在state中添加两个状态:isLogin(是否登录)和userInfo(用户信息)
```react
state = {
// 是否登录
isLogin: isAuth(),
// 用户信息
userInfo: {
avatar: '',
nickname: ''
}
}
```
- 从utils中导入isAuth(登录状态)、getToken(获取token)
```react
import { BASE_URL, isAuth, getToken, API } from '../../utils'
```
- 创建方法getUserInfo,用户来获取个人资料
```react
async getUserInfo() {
...
}
```
- 在方法中,通过isLogin判断用户是否登录
```react
if (!this.state.isLogin) {
// 未登录
return
}
```
- 如果没有登录,则不发送请求,渲染未登录信息
```react
// 对用结构使用状态来判断显示登录还是未登录UI
{/* 登录后展示: */}
{isLogin ? (
<>
退出
编辑个人资料
>
) : (
)}
```
- 如果已登录,就根据接口发送请求,获取用户个人资料
- 渲染个人资料数据
```react
async getUserInfo() {
if (!this.state.isLogin) {
// 未登录
return
}
// 发送请求,获取个人资料
const res = await API.get('/user', {
headers: {
authorization: getToken()
}
})
if (res.data.status === 200) {
// 请求成功
const { avatar, nickname } = res.data.body
this.setState({
userInfo: {
avatar: BASE_URL + avatar,
nickname
}
})
}
}
// render方法中
render(){
const { history } = this.props
const {
isLogin,
userInfo: { avatar, nickname }
} = this.state
return (
{/* 个人信息 */}
{nickname || '游客'}
...
}
```
## 退出功能(★★★)
- 点击退出按钮,弹出对话框,提示是否确定退出
- 给退出按钮绑定点击事件,创建方法logout作为事件处理函数
- 导入Modal对话框组件(弹出模态框)
```react
import {..., Modal } from 'antd-mobile'
```
- 在方法中,拷贝Modal组件文件高中确认对话框的示例代码
```react
const alert = Modal.alert
alert('Delete', 'Are you sure?', [
{ text: 'Cancel',onPress: () => console.log('cancel) },
{ text: 'Ok', onPress: () => console.log('ok')}
])
```
- 修改对话框的文字提示
```react
alert('提示', '是否确定退出?', [
{ text: '取消'},
{ text: '退出', onPress: () => console.log('ok')}
])
```
- 在退出按钮的事件处理程序中,先调用退出接口(让服务器端退出),再移除本地token(本地退出)
- 把登录状态isLogin设置为false
- 清空用户状态对象
```react
{
text: '退出',
onPress: async () => {
// 调用退出接口
await API.post('/user/logout', null, {
headers: {
authorization: getToken()
}
})
// 移除本地token
removeToken()
// 处理状态
this.setState({
isLogin: false,
userInfo: {
avatar: '',
nickname: ''
}
})
}
}
```
# 登录访问控制
## 目标
- 理解访问控制中的两种功能和两种页面
- 能够说出处理两种功能用什么方式来实现
- 能够写出 axios请求拦截器与响应拦截器,并且能够说出这两种拦截器分别在什么时候触发,有什么作用
- 能够说出处理两种页面用什么方式来实现
- 能够说出AuthRoute 鉴权路由组件实现思路
- 能够参照官网自己封装AuthRoute 鉴权路由组件
- 能够实现修改登录成功后的跳转
## 概述
项目中的两种类型的功能和两种类型的页面:
两种功能:
- 登录后才能进行操作(比如:获取个人资料)
- 不需要登录就可以操作(比如:获取房屋列表)
两种页面:
- 需要登录才能访问(比如:发布房源页)
- 不需要登录即可访问(比如:首页)
对于需要登录才能操作的功能使用 **axios 拦截器** 进行处理(比如:统一添加请求头 authorization等)
对于需要登录才能访问的页面使用 **路由控制**
## 功能处理-使用axios拦截器统一处理token(★★★)
- 在api.js 中,添加请求拦截器 (API.interceptors.request.user())
- 获取到当前请求的接口路径(url)
- 判断接口路径,是否是以/user 开头,并且不是登录或注册接口(只给需要的接口添加请求头)
- 如果是,就添加请求头Authorization
```react
// 添加请求拦截器
API.interceptors.request.use(config => {
const { url } = config
// 判断请求url路径
if (
url.startsWith('/user') &&
!url.startsWith('/user/login') &&
!url.startsWith('/user/registered')
) {
// 添加请求头
config.headers.Authorization = getToken()
}
return config
})
```
- 添加响应拦截器 (API.interceptors.response.use())
- 判断返回值中的状态码
- 如果是400,标示token超时或异常,直接移除token
```react
// 添加响应拦截器
API.interceptors.response.use(response => {
const { status } = response.data
if (status === 400) {
// 此时,说明 token 失效,直接移除 token 即可
removeToken()
}
return response
})
```
## 页面处理-AuthRoute鉴权路由组件(★★★)
### 实现原理
- 限制某个页面只能在登陆的情况下访问,但是在React路由中并没有直接提供该组件,需要手动封装,来实现登陆访问控制(类似与Vue路由的导航守卫)
- 参数 react-router-dom的[鉴权文档](https://reacttraining.com/react-router/web/wxample/auth-workflow)
- AuthRoute 组件实际上就是对原来Route组件做了一次包装,来实现一些额外的功能
- 使用

- render方法:render props模式,指定该路由要渲染的组件内容
- Redirect组件:重定向组件,通过to属性,指定要跳转的路由信息
```react
// 官网封装的核心逻辑代码
// ...rest 把之前的组件中传递的属性原封不动传递过来
function PrivateRoute({ component: Component, ...rest }) {
return (
// 判断是否登陆,如果登陆,跳转配置的component,如果没有登陆,利用 Redirect组件来进行重定向
fakeAuth.isAuthenticated ? (
) : (
)
}
/>
);
}
```
### 封装AuthRoute鉴权路由组件
- 在components目录中创建AuthRoute/index.js 文件
- 创建组件AuthRoute并导出
- 在AuthRoute组件中返回Route组件(在Route基础上做了一层包装,用于实现自定义功能)
- 给Route组件,添加render方法,指定改组件要渲染的内容(类似与component属性)
- 在render方法中,调用isAuth() 判断是否登陆
- 如果登陆了,就渲染当前组件(通过参数component获取到要渲染的组件,需要重命名)
- 如果没有登陆,就重定向到登陆页面,并且指定登陆成功后腰跳转的页面路径
- 将AuthRoute组件接收到的props原样传递给Route组件(保证与Route组件使用方式相同)
- 使用AuthRoute组件配置路由规则,验证是否实现页面的登陆访问控制
```react
const AuthRoute = ({ component: Component, ...rest }) => {
return (
{
const isLogin = isAuth()
if (isLogin) {
// 已登录
// 将 props 传递给组件,组件中才能获取到路由相关信息
return
} else {
// 未登录
return (
)
}
}}
/>
)
}
export default AuthRoute
```
### 修改登录成功跳转
- 登陆成功后,判断是否需要跳转到用户想要访问的页面(判断props.location.state 是否有值)
- 如果不需要,则直接调用history.go(-1) 返回上一页
- 如果需要,就跳转到from.pathname 指定的页面(推荐使用replace方法模式,不是push)
```react
// 表单的提交事件
handleSubmit: async (values, { props }) => {
...
if (status === 200) {
// 登录成功
localStorage.setItem('hkzf_token', body.token)
/*
1 登录成功后,判断是否需要跳转到用户想要访问的页面(判断 props.location.state 是否有值)。
2 如果不需要(没有值),则直接调用 history.go(-1) 返回上一页。
3 如果需要,就跳转到 from.pathname 指定的页面(推荐使用 replace 方法模式,而不是 push)。
*/
if (!props.location.state) {
// 此时,表示是直接进入到了该页面,直接调用 go(-1) 即可
props.history.go(-1)
} else {
// replace: [home, map]
props.history.replace(props.location.state.from.pathname)
}
} else {
// 登录失败
Toast.info(description, 2, null, false)
}
}
```
# 我的收藏模块
## 目标
- 能够实现检测房源是否收藏
- 能够实现收藏房源功能
## 功能分析
- 收藏房源
- 功能:
- 检查房源是否收藏
- 收藏房源
## 检查房源是否收藏(★★)
- 在state中添加状态,isFavorite(表示是否收藏),默认值是false
```react
state= {
// 表示房源是否收藏
isFavorite: false
}
```
- 创建方法 checkFavorite,在进入房源详情页面时调用该方法
- 先调用isAuth方法,来判断是否登陆
- 如果未登录,直接return,不再检查是否收藏
- 如果已登陆,从路由参数中,获取当前房屋id
- 使用API调用接口,查询该房源是否收藏
- 如果返回状态码为200,就更新isFavorite;否则,不做任何处理
```react
async checkFavorite() {
const isLogin = isAuth()
if (!isLogin) {
// 没有登录
return
}
// 已登录
const { id } = this.props.match.params
const res = await API.get(`/user/favorites/${id}`)
const { status, body } = res.data
if (status === 200) {
// 表示请求已经成功,需要更新 isFavorite 的值
this.setState({
isFavorite: body.isFavorite
})
}
}
```
- 在页面结构中,通过状态isFavorite修改收藏按钮的文字和图片内容
```react
{/* 底部收藏按钮 */}
{isFavorite ? '已收藏' : '收藏'}
在线咨询
电话预约
```
## 收藏房源(★★★)
- 给收藏按钮绑定点击事件,创建方法handleFavorite作为事件处理程序
```react
handleFavorite = async () => {
...
}
```
- 调用isAuth方法,判断是否登陆
- 如果未登录,则使用Modal.alert 提示用户是否去登陆
- 如果点击取消,则不做任何操作
- 如果点击去登陆,就跳转到登陆页面,同时传递state(登陆后,再回到房源收藏页面)
```react
const isLogin = isAuth()
const { history, location, match } = this.props
if (!isLogin) {
// 未登录
return alert('提示', '登录后才能收藏房源,是否去登录?', [
{ text: '取消' },
{
text: '去登录',
onPress: () => history.push('/login', { from: location })
}
])
}
```
- 根据isFavorite判断,当前房源是否收藏
- 如果未收藏,就调用添加收藏接口,添加收藏
- 如果收藏了,就调用删除接口,删除收藏
```react
if (isFavorite) {
// 已收藏,应该删除收藏
const res = await API.delete(`/user/favorites/${id}`)
// console.log(res)
this.setState({
isFavorite: false
})
if (res.data.status === 200) {
// 提示用户取消收藏
Toast.info('已取消收藏', 1, null, false)
} else {
// token 超时
Toast.info('登录超时,请重新登录', 2, null, false)
}
} else {
// 未收藏,应该添加收藏
const res = await API.post(`/user/favorites/${id}`)
// console.log(res)
if (res.data.status === 200) {
// 提示用户收藏成功
Toast.info('已收藏', 1, null, false)
this.setState({
isFavorite: true
})
} else {
// token 超时
Toast.info('登录超时,请重新登录', 2, null, false)
}
}
```
- 完整逻辑代码
```react
handleFavorite = async () => {
const isLogin = isAuth()
const { history, location, match } = this.props
if (!isLogin) {
// 未登录
return alert('提示', '登录后才能收藏房源,是否去登录?', [
{ text: '取消' },
{
text: '去登录',
onPress: () => history.push('/login', { from: location })
}
])
}
// 已登录
const { isFavorite } = this.state
const { id } = match.params
if (isFavorite) {
// 已收藏,应该删除收藏
const res = await API.delete(`/user/favorites/${id}`)
// console.log(res)
this.setState({
isFavorite: false
})
if (res.data.status === 200) {
// 提示用户取消收藏
Toast.info('已取消收藏', 1, null, false)
} else {
// token 超时
Toast.info('登录超时,请重新登录', 2, null, false)
}
} else {
// 未收藏,应该添加收藏
const res = await API.post(`/user/favorites/${id}`)
// console.log(res)
if (res.data.status === 200) {
// 提示用户收藏成功
Toast.info('已收藏', 1, null, false)
this.setState({
isFavorite: true
})
} else {
// token 超时
Toast.info('登录超时,请重新登录', 2, null, false)
}
}
}
```
# 房源发布模块
## 目标
- 如何解决JS 文本输入框防抖(用户输入过快导致请求服务器的压力过大)
- 能够完成搜索模块
- 能够获取发布房源的相关信息
- 能够知道图片上传的流程
- 能够完成图片上传功能
- 能够完成房源发布功能
## 前期准备工作
### 功能
- 获取房源的小区信息,房源图片上传,房源发布等

### 模板改动说明
- 修改首页(Index)去出租链接为: /rent/add
- 修改公共组件NoHouse的children属性校验为: node(任何可以渲染的内容)

- 修改公共组件HousePackage,添加onSelect的默认值

- 添加utils/city.js,封装当前定位城市 localStorage的操作

- 创建了三个页面组件:Rent(已发布房源列表)、Rent/Add(发布房源)、Rent/Search(关键词搜索校区信息)

- Rent 模板代码
```react
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { API, BASE_URL } from '../../utils'
import NavHeader from '../../components/NavHeader'
import HouseItem from '../../components/HouseItem'
import NoHouse from '../../components/NoHouse'
import styles from './index.module.css'
export default class Rent extends Component {
state = {
// 出租房屋列表
list: []
}
// 获取已发布房源的列表数据
async getHouseList() {
const res = await API.get('/user/houses')
const { status, body } = res.data
if (status === 200) {
this.setState({
list: body
})
} else {
const { history, location } = this.props
history.replace('/login', {
from: location
})
}
}
componentDidMount() {
this.getHouseList()
}
renderHouseItem() {
const { list } = this.state
const { history } = this.props
return list.map(item => {
return (
history.push(`/detail/${item.houseCode}`)}
src={BASE_URL + item.houseImg}
title={item.title}
desc={item.desc}
tags={item.tags}
price={item.price}
/>
)
})
}
renderRentList() {
const { list } = this.state
const hasHouses = list.length > 0
if (!hasHouses) {
return (
您还没有房源,
去发布房源
吧~
)
}
return {this.renderHouseItem()}
}
render() {
const { history } = this.props
return (
history.go(-1)}>房屋管理
{this.renderRentList()}
)
}
}
```
### 三个路由规则配置
- 在App.js 中导入Rent已发布房源列表页面
- 在App.js 中导入AuthRoute组件
- 使用AuthRoute组件,配置路由规则
- 使用同样方式,配置Rent/Add 房源发布页面,Rent/Search 关键词搜索小区信息页面
```react
{/* 配置登录后,才能访问的页面 */}
```
## 搜索模块(★★★)
### 关键词搜索小区信息
- 获取SearchBar 搜索栏组件的值
- 在搜索栏的change事件中,判断当前值是否为空
- 如果为空,直接return,不做任何处理
- 如果不为空,就根据当前输入的值以及当前城市id,获取该关键词对应的小区信息
- **问题:**搜索栏中每输入一个值,就发一次请求,这样对服务器压力比较大,用户体验不好
- **解决方式:**使用定时器来进行延迟执行(关键词:JS文本框输入 防抖)

#### 实现步骤
- 给SearchBar组件,添加onChange配置项,获取文本框的值
```react
{/* 搜索框 */}
history.go(-1)}
/>
{/* 搜索提示列表 */}
```
- 判断当前文本框的值是否为空
- 如果为空,清空列表,然后return,不再发送请求
```react
handleSearchTxt = value => {
this.setState({ searchTxt: value })
if (!value) {
// 文本框的值为空
return this.setState({
tipsList: []
})
}
}
```
- 如果不为空,使用API发送请求,获取小区数据
- 使用定时器来延迟搜索,提升性能
```react
handleSearchTxt = value => {
this.setState({ searchTxt: value })
if (!value) {
// 文本框的值为空
return this.setState({
tipsList: []
})
}
// 清除上一次的定时器
clearTimeout(this.timerId)
this.timerId = setTimeout(async () => {
// 获取小区数据
const res = await API.get('/area/community', {
params: {
name: value,
id: this.cityId
}
})
this.setState({
tipsList: res.data.body
})
}, 500)
}
```
### 传递校区信息给发布房源页面
- 给搜索列表项添加点击事件
```react
// 渲染搜索结果列表
renderTips = () => {
const { tipsList } = this.state
return tipsList.map(item => (
this.onTipsClick(item)}
>
{item.communityName}
))
}
```
- 在事件处理程序中,调用 history.replace() 方法跳转到发布房源页面
- 将被点击的校区信息作为数据一起传递过去
```react
onTipsClick = item => {
this.props.history.replace('/rent/add', {
name: item.communityName,
id: item.community
})
}
```
- 在发布房源页面,判断history.location.state 是否为空
- 如果为空,不做任何处理
- 如果不为空,则将小区信息存储到发布房源页面的状态中
```react
constructor(props) {
super(props)
// console.log(props)
const { state } = props.location
const community = {
name: '',
id: ''
}
if (state) {
// 有小区信息数据,存储到状态中
community.name = state.name
community.id = state.id
}
}
```
## 发布房源
### 布局结构

- List列表 组件
- InputItem 文本输入组件
- TextareaItem 多行输入组件
- Picker 选择器组件
- ImagePicker 图片选择器组件
- 模板结构
```react
import React, { Component } from 'react'
import {
Flex,
List,
InputItem,
Picker,
ImagePicker,
TextareaItem,
Modal
} from 'antd-mobile'
import NavHeader from '../../../components/NavHeader'
import HousePackge from '../../../components/HousePackage'
import styles from './index.module.css'
const alert = Modal.alert
// 房屋类型
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' }
]
export default class RentAdd extends Component {
constructor(props) {
super(props)
// console.log(props)
const { state } = props.location
const community = {
name: '',
id: ''
}
if (state) {
// 有小区信息数据,存储到状态中
community.name = state.name
community.id = state.id
}
this.state = {
// 临时图片地址
tempSlides: [],
// 小区的名称和id
community,
// 价格
price: '',
// 面积
size: '',
// 房屋类型
roomType: '',
// 楼层
floor: '',
// 朝向:
oriented: '',
// 房屋标题
title: '',
// 房屋图片
houseImg: '',
// 房屋配套:
supporting: '',
// 房屋描述
description: ''
}
}
render() {
const Item = List.Item
const { history } = this.props
const {
community,
price,
roomType,
floor,
oriented,
description,
tempSlides,
title,
size
} = this.state
return (
发布房源
{/* 房源信息 */}
'房源信息'}
data-role="rent-list"
>
{/* 选择所在小区 */}
- history.replace('/rent/search')}
>
小区名称
{/* 相当于 form 表单的 input 元素 */}
租 金
this.getValue('size', val)}
>
建筑面积
-
户 型
- 所在楼层
-
朝 向
{/* 房屋标题 */}
'房屋标题'}
data-role="rent-list"
>
{/* 房屋图像 */}
'房屋图像'}
data-role="rent-list"
>
{/* 房屋配置 */}
'房屋配置'}
data-role="rent-list"
>
{/* 房屋描述 */}
'房屋描述'}
data-role="rent-list"
>
取消
提交
)
}
}
```
### 获取房源数据分析(★★)
- InputItem、TextareaItem、Picker组件,都使用onChange配置项,来获取当前值
- 处理方式:封装一个事件处理函数 getValue 来统一获取三种组件的值
- 创建方法getValue作为三个组件的事件处理函数
- 该方法接受两个参数:1. name 当前状态名;2. value 当前输入值或者选中值
- 分别给 InputItem/TextareaItem/Picker 组件,添加onChange配置项
- 分别调用 getValue 并传递 name 和 value 两个参数(注意:Picker组件选中值为数组,而接口需要字符串,所以,取索引号为 0 的值即可)

示例代码:
```react
/*
获取表单数据:
*/
getValue = (name, value) => {
this.setState({
[name]: value
})
}
// 给相应组件添加 onChange 事件,传递 name 和value
```
### 获取房屋配置数据(★★)
- 给HousePackge 组件, 添加 onSelect 属性
- 在onSelect 处理方法中,通过参数获取到当前选中项的值
- 根据发布房源接口的参数说明,将获取到的数组类型的选中值,转化为字符串类型
- 调用setState 更新状态
```react
/*
获取房屋配置数据
*/
handleSupporting = selected => {
this.setState({
supporting: selected.join('|')
})
}
...
```
### 图片上传(★★★)
#### 分析
- 根据发布房源接口,最终需要的是房屋图片的路径
- 两个步骤: 1- 获取房屋图片; 2- 上传图片获取到图片的路径
- 如何获取房屋图片? ImagePicker图片选择器组件,通过onChange配置项来获取
- 如何上传房屋图片? 根据图片上传接口,将图片转化为FormData数据后再上传,由接口返回图片路径

#### 获取房屋图片
要上传图片,首先需要先获取到房屋图片
- 给ImagePicker 组件添加 onChange 配置项
- 通过onChange 的参数,获取到上传的图片,并且存储到tempSlides中
```react
handleHouseImg = (files, type, index) => {
// files 图片文件的数组; type 操作类型:添加,移除(如果是移除,那么第三个参数代表就是移除的图片的索引)
console.log(files, type, index)
this.setState({
tempSlides: files
})
}
...
```
#### 上传房屋图片
图片已经可以通过 ImagePicker 的 onChange 事件来获取到了,接下来就需要把图片进行上传,然后获取到服务器返回的成功上传图片的路径
- 给提交按钮,绑定点击事件
- 在事件处理函数中,判断是否有房屋图片
- 如果没有,不做任何处理
- 如果有,就创建FormData的示例对象(form)
- 遍历tempSlides数组,分别将每一个图片图片对象,添加到form中(键为:file,根据接口文档获取)
- 调用图片上传接口,传递form参数,并设置请求头 Content-Type 为 multipart/form-data
- 通过接口返回值获取到图片路径
```react
// 上传图片
addHouse = async() => {
const { tempSlides } = this.state
let houseImg = ''
if (tempSlides.length > 0) {
// 已经有上传的图片了
const form = new FormData()
tempSlides.forEach(item => form.append('file', item.file))
const res = await API.post('/houses/image', form, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
// console.log(res)
houseImg = res.data.body.join('|')
}
}
...
提交
```
#### 发布房源
到现在,我们已经可以获取到发布房源的所有信息了,接下来就需要把数据传递给服务器
- 在 addHouse 方法中, 从state 里面获取到所有的房屋数据
- 使用API 调用发布房源接口,传递所有房屋数据
- 根据接口返回值中的状态码,判断是否发布成功
- 如果状态码是200,标示发布成功,就提示:发布成功,并跳转到已发布的房源页面
- 否则,就提示:服务器偷懒了,请稍后再试
```react
addHouse = async () => {
const {
tempSlides,
title,
description,
oriented,
supporting,
price,
roomType,
size,
floor,
community
} = this.state
let houseImg = ''
// 上传房屋图片:
if (tempSlides.length > 0) {
// 已经有上传的图片了
const form = new FormData()
tempSlides.forEach(item => form.append('file', item.file))
const res = await API.post('/houses/image', form, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
houseImg = res.data.body.join('|')
}
// 发布房源
const res = await API.post('/user/houses', {
title,
description,
oriented,
supporting,
price,
roomType,
size,
floor,
community: community.id,
houseImg
})
if (res.data.status === 200) {
// 发布成功
Toast.info('发布成功', 1, null, false)
this.props.history.push('/rent')
} else {
Toast.info('服务器偷懒了,请稍后再试~', 2, null, false)
}
}
```
# 项目打包
## 目标
- 能够配置生产环境的环境变量
- 能够完成简易的打包
- 知道react中如果要配置webpack的两种方式
- 知道 antd-mobile 按需加载的好处
- 知道路由代码分割的好处
- 能够参照笔记来进行 按需加载配置和代码分割配置,然后打包
- 能够知道如何解决react中跨域问题
## 简易打包(★★★)
- 打开 create-react-app 脚手架的 [打包文档说明](https://facebook.github.io/create-react-app/docs/deployment)
- 在根目录创建 .env.production 文件,配置生产环境的接口基础路径

- 在项目根目录中,打开终端
- 输入命令: yarn build,进行项目打包,生成build文件夹(打包好的项目内容)
- 将build目录中的文件内容,部署到都服务器中即可
- 也可以通过终端中的提示,使用 serve-s build 来本地查看(需要全局安装工具包 serve)
**如果出现以下提示,就代表打包成功,在根目录中就会生成一个build文件夹**

## 脚手架的配置说明(★★★)
- create-react-app 中隐藏了 webpack的配置,隐藏在react-scripts包中
- 两种方式来修改
- 运行命令 npm run eject 释放 webpack配置(注意:不可逆)
如果您对构建工具和配置选择不满意,您可以`eject`随时进行。此命令将从项目中删除单个构建依赖项。
相反,它会将所有配置文件和传递依赖项(Webpack,Babel,ESLint等)作为依赖项复制到项目中`package.json`。从技术上讲,依赖关系和开发依赖关系之间的区别对于生成静态包的前端应用程序来说是非常随意的。此外,它曾经导致某些托管平台出现问题,这些托管平台没有安装开发依赖项(因此无法在服务器上构建项目或在部署之前对其进行测试)。您可以根据需要自由重新排列依赖项`package.json`。
除了`eject`仍然可以使用所有命令,但它们将指向复制的脚本,以便您可以调整它们。在这一点上,你是独立的。
你不必使用`eject`。策划的功能集适用于中小型部署,您不应觉得有义务使用此功能。但是,我们知道如果您准备好它时无法自定义此工具将无用
- 通过第三方包重写 webpack配置(比如:**[react-app-rewired](https://mobile.ant.design/docs/react/use-with-create-react-app-cn)** 等)
## antd-mobile 按需加载(★★★)
- 打开 antd-mobile 在create-react-app中的使用文档
- 安装 yarn add react-app-rewired customize-cra(用于脚手架重写配置)
- 修改package.json 中的 scripts

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

- 安装 yarn add babel-plugin-import 插件(用于按需加载组件代码和样式)
- 修改 config-overrides.js 文件,配置按需加载功能
```react
const { override, fixBabelImports } = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd-mobile',
style: 'css',
}),
);
```
- 重启项目(yarn start)
- 移除index.js 中导入的 antd-mobile样式文件
- 将index.css 移动到App后面,让index.css 中的页面背景生效
打完包后,你会发现,两次打包的体积会有变化,这样达到了一个代码体积优化的层面

## 基于路由代码分割(★★★)
- 目的:将代码按照路由进行分割,只在访问该路由的时候才加载该组件内容,提高首屏加载速度
- 如何实现? React.lazy() 方法 + import() 方法、Suspense组件([React Code-Splitting文档](https://reactjs.org/docs/code-splitting.html))
- React.lazy() 作用: 处理动态导入的组件,让其像普通组件一样使用
- import('组件路径'),作用:告诉webpack,这是一个代码分割点,进行代码分割
- Suspense组件:用来在动态组件加载完成之前,显示一些loading内容,需要包裹动态组件内容


项目中代码修改:

## 其他性能优化(★★)
- React.js 优化性能[文档](https://reactjs.org/docs/docs/optimizing-performance.html)
- react-virtualized只加载用到的组件 [文档](https://github.com/bvaughn/react-virtualized#getting-started)
- 脚手架配置 解决跨域问题
- 安装 http-proxy-middleware
```
$ npm install http-proxy-middleware --save
$ # or
$ yarn add http-proxy-middleware
```
- 创建`src/setupProxy.js`并放置以下内容
```react
const proxy = require('http-proxy-middleware');
module.exports = function(app) {
app.use(proxy('/api', { target: 'http://localhost:5000/' }));
};
```
- **注意:**无需在任何位置导入此文件。它在启动开发服务器时自动注册,此文件仅支持Node的JavaScript语法。请务必仅使用支持的语言功能(即不支持Flow,ES模块等)。将路径传递给代理功能允许您在路径上使用通配和/或模式匹配,这比快速路由匹配更灵活

# 好客租房移动Web(下)-总结
- 登录模块:使用Fomik组件实现了表单处理和表单校验、封装鉴权路由AuthRoute和axios拦截器实现登录访问控制
- 我的收藏模块:添加、取消收藏
- 发布房源模块:小区关键词搜索、图片上传、发布房源信息
- 项目打包和优化:antd-mobile组件库按需加载,基于路由的代码分割实现组件的按需加载,提高了首屏加载速度