技术学习ReactReactreact好租客项目Day03-城市选择功能&react-virtualized组件使用
Ramsayi城市选择模块
目标
- 能够实现顶部导航栏
- 能够获取城市列表数据,热门城市数据,当前定位城市数据,并且对数据进行重新封装
- 知道什么是长列表,以及带来的缺陷
- 说出长列表性能优化的两种方式
- 能够使用 react-virtualized 进行城市列表的渲染
- 能够渲染右侧索引列表
功能分析
- 切换城市,查看该城市下的房源信息
- 功能
- 顶部导航栏
- 城市列表展示
- 使用索引快速切换城市
- 点击城市名称切换城市
顶部导航栏(★★★)
- 打开 antd-mobile 组件库的 NavBar 导航栏组件 文档
- 从文档中拷贝组件示例代码到项目中,让其正确运行
- 修改导航栏样式和结构
示例
1
| import {NavBar, Icon} from 'antd-mobile'
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <div> <NavBar // 模式 默认值是 dark mode="light" // 左侧小图片 icon={<Icon type="left" />} // 左侧按钮的点击事件 onLeftClick={() => console.log('onLeftClick')} // 右侧按按钮图标 rightContent={[ <Icon key="0" type="search" style={{ marginRight: '16px' }} />, <Icon key="1" type="ellipsis" />, ]} >城市列表</NavBar> </div>
|
1 2 3 4 5 6 7 8 9 10
| <div> <NavBar // 模式 默认值是 dark mode="light" // 左侧小图片 icon={<i className='iconfont icon-back' />} // 左侧按钮的点击事件 onLeftClick={() => this.props.history.go(-1)} >城市列表</NavBar> </div>
|
1 2 3 4 5 6 7 8 9 10
| .citylist { .navbar { color: #333; background-color: #f6f5f6; } .am-navbar-title { color: #333; } }
|
获取处理数据(★★★)
- 页面加载时候,根据接口获取到城市列表数据
- 分析当前数据格式以及该功能需要的数据格式
- 转换当前数据格式为所需要的数据格式‘
获取数据
- 根据接口文档提供的 url 进行网络请求
- 获取到相应的数据信息
1 2 3 4 5 6 7 8
| // 当组件被挂载的时候调用 componentDidMount() { this.getCityList() } async getCityList() { let {data:res} = await axios.get('http://localhost:8080/area/city?level=1') console.log(res); }
|
处理数据格式
我们需要把服务器返回的数据进行格式化处理,我们可以通过首字母来进行城市的定位,所以我们需要把格式转换成以下格式
- 我们需要遍历 list 数组
- 获取到每一个城市的首字母
- 判断我们定义的数组中是否有这个分类,如果有,那么直接 push 数据进来,如果没有,添加这个分类
- 当城市列表数据按照首字母分好类了之后,还需要实现热门城市数据和定位城市数据
- 获取热门城市数据,添加到
cityList
列表数据中,将索引数据添加到 cityIndex
索引数据中
- 获取当前城市数据,添加到
cityList
列表数据中,将索引数据添加到 cityIndex
索引数据中
封装一个函数,来处理这个数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| /** * 格式化返回的数据 * @param {*} list */ function formatCityData(list) { // 键是首字母,值是一个数组:对应首字母的城市信息 let cityList = {} list.forEach(item => { // 通过简写获取到第一个首字母 let first = item.short.substr(0, 1) // 判断对象中是否有这个key 我们可以利用对象取值的第二种方式 中括号的方式 if(cityList[first]){ // 如果进入if 代表有这个值,我们只需要直接push进去 cityList[first].push(item) }else{ // 如果进入else 代表没有这个值,我们初始化这个属性,并且把当前数据设置进去 cityList[first] = [item] } }) // 接下来我们需要把 cityList里面所有的key取出来,放在数组中,充当城市列表右侧的首字母导航条 let cityIndex = Object.keys(cityList).sort() return { cityList, cityIndex } }
|
在getCityList()
方法中调用这个函数,来格式化数据
1 2 3 4 5
| async getCityList() { let { data: res } = await axios.get('http://localhost:8080/area/city?level=1') // 格式化返回的数据 let { cityList, cityIndex } = formatCityData(res.body) }
|
获取热门数据,并且添加到 cityList
和cityListIndex
中,注意,对象里面的属性是无序的,可以直接插入,但是数组是有序的,我们需要添加到前面
1 2 3 4 5 6
| // 获取热门城市数据 let {data: hotRes} = await axios.get('http://localhost:8080/area/hot') // 将热门数据添加到 cityList cityList['hot'] = hotRes.body // 将热门数据添加到 cityIndex cityIndex.unshift('hot')
|
获取当前城市信息,我们将获取定位城市的代码封装到一个函数中,哪个页面需要获取定位城市,直接调用该方法即可
- 在 utils 目录中,创建一个文件,在这个文件中进行封装
- 创建并且导出获取定位城市的函数 getCurrentCity
- 判断 localStorage 中是否有定位信息
- 如果没有,我们通过获取定位信息来获取当前定位城市,获取完了需要存到本地存储中
- 如果有,直接使用就好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import axios from 'axios' export const getCurrentCity = () => { // 获取本地存储中是否有 let localCity = JSON.parse(localStorage.getItem('localCity')) if (!localCity) { // 如果没有,就需要获取当前定位城市 // 利用 promis 来解决异步数据的返回 return new Promise((resolve, reject) => { try { // 获取当前城市信息 var myCity = new window.BMap.LocalCity(); myCity.get(async res => { // 当获取到对应的城市信息了后,我们需要请求我们自己的服务器 const { data: infoRes } = await axios.get('http://localhost:8080/area/info', { params: { name: res.name } }) if (infoRes.status != 200) { console.error(infoRes.description) return } console.log(infoRes);
// res.data.body // 保存在本地存储中 localStorage.setItem('localCity', JSON.stringify(infoRes.body)) // 返回城市的数据 resolve(infoRes.body) }); } catch (error) { // 进入到catch代码块 说明调用失败了 reject(error) } })
} // 如果有,我们直接返回城市信息就好,返回一个成功的promis对象即可 return Promise.resolve(localCity) }
|
- 将定位的城市信息添加到
cityList
和cityIndex
中
1 2 3 4 5 6
| // 获取当前城市定位信息 let curCity = await getCurrentCity() // 将当前城市数据添加到 cityList cityList['#'] = curCity // 将当前城市数据添加到 cityIndex cityIndex.unshift('#')
|
长列表性能优化(★★)
概述
在展示大型列表和表格数据的时候(城市列表、通讯录、微博等),会导致页面卡顿,滚动不流畅等性能问题,这样就会导致移动设备耗电加快,影响移动设备的电池寿命
产生性能问题的元素:大量 DOM 节点的重绘和重排
优化方案:
懒渲染
- 懒加载,常见的长列表优化方案,常见于移动端
- 原理:每次只渲染一部分,等渲染的数据即将滚动完时,再渲染下面部分
- 优点:每次渲染一部分数据,速度快
- 缺点:数据量大时,页面中依然存在大量 DOM 节点,占用内存过多,降低浏览器渲染性能,导致页面卡顿
- 使用场景:数据量不大的情况下
可视区渲染(React-virtualized)
原理: 只渲染页面可视区域的列表项,非可视区域的数据 完全不渲染(预加载前面几项和后面几项) ,在滚动列表时动态更新列表项
使用场景: 一次性展示大量数据的情况
react-virtualized(★★★)
概述
- 在项目中的应用:实现城市选择列表页面的渲染
- react-virtualized 是 React 组件,用来高效渲染大型列表和表格数据
- GitHub 地址: react-virtualized
基本使用
- 安装: yarn add react-virtualized
- 在项目入口文件 index.js 中导入样式文件
- 打开 文档, 点击 List 组件,进入 List 的文档中
- 拷贝示例代码到我们项目中,分析示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import React from 'react'; import ReactDOM from 'react-dom'; import { List } from 'react-virtualized';
// 列表数据 const list = [ 'Brian Vaughn' // And so on... ]; // 渲染每一行的内容 function rowRenderer ({ key, // Unique key within array of rows index, // 索引号 isScrolling, // 当前项是否正在滚动中 isVisible, // 当前项在List中是可见的 style // 重点属性:一定要给每一个行数添加该样式 }) { return ( <div key={key} style={style} > {list[index]} </div> ) }
// 渲染list列表 ReactDOM.render( <List // 组件的宽度 width={300} // 组件的高度 height={300} rowCount={list.length} // 每行的高度 rowHeight={20} rowRenderer={rowRenderer} />, document.getElementById('example') );
|
渲染城市列表(★★★)
让 List 组件占满屏幕
- 利用
AutoSizer
组件来调整子元素的宽高
- 导入
AutoSizer
组件
- 通过 render-props 模式,获取到
AutoSizer
组件暴露的 width 和 height 属性
- 设置 List 组件的 width 和 height 属性
- 设置城市选择页面根元素高度 100%,让 List 组件占满整个页面
1 2 3 4 5 6 7 8 9 10 11 12 13
| .citylist { height: 100%; padding-top: 45px; .navbar { margin-top: -45px; color: #333; background-color: #f6f5f6; } .am-navbar-title { color: #333; } }
|
渲染城市列表
- 将获取到的 cityList 和 cityIndex 添加为组建的状态数据
1 2 3 4
| state = { cityList: null, cityIndex: [] }
|
- 修改 List 组件的 rowCount 为 cityIndex 数组的长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| {/* 城市列表 */} <AutoSizer> { ({ width, height }) => { return <List // 组件的宽度 width={width} // 组件的高度 height={height} rowCount={this.state.cityIndex.length} // 每行的高度 rowHeight={this.getRowHeight} rowRenderer={this.rowRenderer} /> } } </AutoSizer>
|
- 修改 List 组件的 rowRender 方法中渲染的结构和样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| // 渲染每一行的内容 rowRenderer({ key, // Unique key within array of rows index, // 索引号 isScrolling, // 当前项是否正在滚动中 isVisible, // 当前项在List中是可见的 style // 重点属性:一定要给每一个行数添加该样式 }) { let letter = this.state.cityIndex[index] let citys = this.state.cityList[letter] return ( <div key={key} style={style} className="city" > <div className="title">{this.formatCityIndex(letter)}</div> {citys.map(item => { return ( <div className="name" key={item.value}>{item.label}</div> ) })}</div> ) }
|
- 修改 List 的 rowHeight 为函数,动态计算每行的高度
1 2 3 4 5 6 7
| // 动态计算高度 getRowHeight = ({ index }) => { // 索引的高度 + 数量 * 每个城市的高度 let { cityIndex, cityList } = this.state;
return cityList[cityIndex[index]].length * NAME_HEIGHT + TITLE_HEIGHT; }
|
渲染右侧索引列表
- 封装
renderCityIndex
方法,用来渲染城市索引列表
- 在方法中,获取到索引数组
cityIndex
,遍历cityIndex
,渲染索引列表
- 将索引 hot 替换成 热
- 在 state 中添加状态 activeIndex,用来指定当前高亮的索引
- 在遍历 cityIndex 时,添加当前字母索引是否是高亮
结构代码
1 2 3 4 5 6
| {/* 右侧索引列表 */} <ul className="city-index"> { this.renderCityIndex() } </ul>
|
样式代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| .city-index { position: absolute; display: flex; flex-direction: column; right: 5px; z-index: 1; height: 90%; box-sizing: border-box; padding-top: 20px; text-align: center; list-style: none; .city-index-item { flex: 1; } .index-active { color: #fff; background-color: #21b97a; border-radius: 100%; display: inline-block; font-size: 12px; width: 15px; height: 15px; line-height: 15px; } }
|
渲染右侧索引的函数
1 2 3 4 5 6 7 8 9 10 11 12
| renderCityIndex() { return this.state.cityIndex.map((item,index) => { console.log(item,index);
return ( <li className="city-index-item" key={item}> {/*判断一下,如果高亮状态的索引等于当前索引,那么就设置高亮样式*/} <span className={this.state.activeIndex == index? 'index-active' : ''}>{item == 'hot' ? '热' : item.toUpperCase()}</span> </li> ) }) }
|