前言
目录
一、顶部导航组件封装
1.1 基础布局
实现步骤:
1、封装NavHeader组件实现城市选择,地图找房页面的复用
2、在components目录中创建组件 NavHeader/index.js
3、在该组件中封装 antd-mobile 组件库中的 NavBar组件
代码示例:
在src/components/NavHeader/index.js中添加如下代码:
import React from 'react';
import {
NavBar} from 'antd-mobile'
export default class extends React.Component {
render() {
return (
<NavBar className="navbar"
// 模式 默认值是 dark
mode="light"
// 左侧小图片
icon={
<i className='iconfont icon-back' />}
// 左侧按钮的点击事件
onLeftClick={
() => this.props.history.go(-1)}
// 标题内容不定的,所以我们通过外界来传入
>{
this.props.children}</NavBar>
)
}
}
1.2 样式调整
在src/components下的NavHeader文件夹中创建 index.scss 文件
把之前城市列表写过的样式,复制到这个文件下
1.3 功能处理
注意:默认情况下,只有路由 Route 直接渲染的组件才能够获取到路由信息,如果需要在其他组件中获取到路由信息,可以通过 withRouter 高阶组件来获取
实现步骤:
1、从 react-router-dom 中导入 withRouter 高阶组件
2、使用 withRouter 高阶组件包装 NavHeader 组件
3、从 props 中就能获取history对象,调用history对象的 go() 方法就能实现返回上一页功能了
4、由于头部的左侧按钮不一定是返回上一个页面的功能,所以我们需要把左侧点击逻辑处理一下,改成需要通过父组件传递进来,如果说外界传递了,那么我们就直接使用外界的行为,如果没有传递,那么就用默认的行为
代码示例:
在src/components/NavHeader/index.js中修改如下代码:
import React from 'react';
import {
NavBar } from 'antd-mobile'
import './index.scss'
import {
withRouter } from 'react-router-dom'
class NavHeader extends React.Component {
render() {
let defaultHandler = () => {
this.props.history.go(-1)}
return (
<NavBar
className="navbar"
// 模式 默认值是 dark
mode="light"
// 左侧小图片
icon={
<i className='iconfont icon-back' />}
// 左侧按钮的点击事件
onLeftClick={
this.props.onLeftClick || defaultHandler}
>{
this.props.children}</NavBar>
)
}
}
// 通过withRouter 包装一层后,返回的还是一个组件
export default withRouter(NavHeader)
1.4 添加props校验
我们可以通过添加props校验,来提示使用者,应该怎样正确的传递props
使用步骤:
1、安装 yarn add prop-types
2、导入 PropTypes
3、给NavHeader组件的 children 和 onLeftClick添加props校验
代码示例:
在src/components/NavHeader/index.js中添加如下代码:
import PropTypes from 'prop-types'
NavHeader.propTypes = {
children: PropTypes.string.isRequired,
onLeftClick:PropTypes.func
}
1.5 在城市找房页面使用NavHeader组件
在src/pages/Citylist/index.js中添加如下代码:
// 导入 NavHeader 组件
import NavHeader from '../../components/NavHeader'
{
/* 顶部导航栏 */}
<NavHeader>城市选择</NavHeader>
二、使用CSS Modules解决组件之间样式覆盖的问题
2.1 组件间样式覆盖问题概述
1、问题:CityList组件的样式,会影响Map组件的样式
2、原因:在配置路由的时候,CityList组件与Map组件都会被导入到路由中,组件被导入,相关的样式也会被导入进来,如果两个组件的样式名称相同,那么就会影响另外一个组件的样式
3、小结:默认情况下,只要导入了组件,不管组件有没有显示在页面中,组件的样式就会生效
4、解决方式:1、写不同的类名 2、CSS IN JS
5、CSS IN JS
CSS IN JS 是使用JavaScript 编写 CSS 的统称,用来解决CSS样式冲突,覆盖等问题;
CSS IN JS 的具体实现有50多种,比如:CSS Modules、styled-components等
推荐使用CSS Modules,因为React脚手架已经集成进来了,可以直接使用
2.2 CSS Modules
概念:
1、CSS Modules 通过对CSS类名重命名,保证每一个类名的唯一性,从而避免样式冲突问题
2、实现方式:webpack的css-loader 插件
3、命名采用:BEM(Block块、Element元素、Modifier三部分组成)命名规范。比如: .list_item_active
4、在React脚手架中演化成:文件名、类名、hash(随机)三部分,只需要指定类名即可
使用方式:
1、创建名为[name].module.css 的样式文件(React脚手架中的约定,与普通CSS区分开)
示例:
在 CityList 组件中创建的样式文件名称:index.module.css
2、组件中导入样式文件(注意语法)
示例:
在 CityList 组件中导入样式文件: import styles from './index.module.css'
3、通过styles对象访问对象中的样式名来设置样式
示例:
className={
styles.navBar}
2.3 使用CSS Modules修改 NavHeader 样式
使用步骤:
1、在NavHeader目录中创建 index.module.css 的样式文件
2、在样式文件中修改当前组件的样式
3、对于组件库中已经有的全局样式,需要使用:global() 来指定
代码示例:
在src/components/NavHeader/index.module.css中添加如下代码:
.navbar {
color: #333;
background-color: #f6f5f6;
}
.navbar :global(.am-navbar-title) {
color: #333;
}
三、地图找房模块-根据定位展示当前城市
实现步骤:
1、获取当前定位城市
2、使用 地址解析器 解析当前城市坐标
3、调用 centerAndZoom() 方法在地图中展示当前城市,并设置缩放级别为11
4、在地图中添加比例尺和平移缩放控件
代码示例:
在src/pages/Map/index.js中添加如下代码:
// 解决脚手架中全局变量访问的问题
const BMap = window.BMap
export default class Map extends React.Component {
componentDidMount() {
this.initMap()
}
// 初始化地图
initMap() {
// 获取当前定位城市
const {
label, value } = JSON.parse(localStorage.getItem('hkzf_city'))
// 初始化地图实例
// 注意:在 react 脚手架中全局对象需要使用 window 来访问,否则,会造成 ESLint 校验错误
const map = new BMap.Map('container')
// 创建地址解析器实例
const myGeo = new BMap.Geocoder()
// 将地址解析结果显示在地图上,并调整地图视野
myGeo.getPoint(
label,
point => {
if (point) {
// 初始化地图
map.centerAndZoom(point, 11)
// 添加常用控件
map.addControl(new BMap.NavigationControl())
map.addControl(new BMap.ScaleControl())
}
},
label
)
}
render() {
return (
<div className={
styles.map}>
{
/* 顶部导航栏组件 */}
<NavHeader>地图找房</NavHeader>
{
/* 地图容器元素 */}
<div id="container" className={
styles.container} />
</div>
)
}
}
四、地图找房模块-房源信息在地图中展示
4.1 房源信息在地图中展示效果图
这些房源信息其实就是用文本覆盖物来实现的,所以我们先查看百度地图开发文档,学习如何创建文本覆盖物
4.2 创建文本覆盖物
创建步骤:
1、打开百度地图添加文字标签DEMO
2、创建 Label 实例对象
3、调用 setStyle() 方法设置样式
4、在 map 对象上调用 addOverlay() 方法,将文本覆盖物添加到地图中。
代码示例:
在src/pages/Map/index.js中添加如下代码:
const opts = {
position: point
}
const label = new BMap.Label('文本覆盖物', opts)
// 设置样式
label.setStyle({
color: 'green'
})
// 添加覆盖物到地图中
map.addOverlay(label)
4.3 绘制房源信息覆盖物
绘制步骤:
1、由于默认提供的本文覆盖物与我们效果不符合,所以我们需要进行重新的绘制
2、调用Label的setContent方法,传入html结构,修改HTML的内容样式
3、调用setStyle方法修改覆盖物样式
4、给覆盖物添加点击事件
代码示例:
在src/pages/Map/index.js中添加如下代码:
// 覆盖物样式
const labelStyle = {
cursor: 'pointer',
border: '0px solid rgb(255, 0, 0)',
padding: '0px',
whiteSpace: 'nowrap',
fontSize: '12px',
color: 'rgb(255, 255, 255)',
textAlign: 'center'
}
const opts = {
position: point,
offset: new BMap.Size(-35, -35)
}
// 说明:设置 setContent 后,第一个参数中设置的文本内容就失效了,因此,直接清空即可
const label = new BMap.Label('', opts)
// 设置房源覆盖物内容
label.setContent(`
<div class="${styles.bubble}">
<p class="${styles.name}">浦东</p>
<p>99套</p>
</div>
`)
// 设置覆盖物样式
label.setStyle(labelStyle)
// 添加单击事件
label.addEventListener('click', () => {
console.log('房源覆盖物被点击了')
})
// 添加覆盖物到地图中
map.addOverlay(label)
五、地图找房模块-业务逻辑实现
5.1 业务逻辑分析
地图找房模块需要实现以下功能:
1、获取房源数据,渲染覆盖物
2、点击覆盖物后放大地图,并获取数据,渲染下一级覆盖物
3、区、镇覆盖物点击后,清除现有的覆盖物,获取下一级数据,创建新的覆盖物
4、小区覆盖物点击后,不清除现有覆盖物,移动地图,展示该小区下的房源信息
5.2 获取所有区的信息并显示在地图中
实现步骤:
1、发送请求获取房源数据
2、遍历数据,创建覆盖物,给每一个覆盖物添加唯一标识
3、给覆盖物添加点击事件
4、在单击事件中,获取到当前单击项的唯一标识
5、放大地图(级别为13),调用clearOverlays()方法清除当前覆盖物
代码示例:
在src/pages/Map/index.js中添加如下代码:
// 请求接口,获取房源数据
let res = await axios.get(`http://localhost:8080/area/map?id=${
value}`)
// 遍历房源信息,创建对应的覆盖物
res.data.body.map(item => {
// 给每一条数据添加覆盖物
// 得到返回的经纬度信息
let {
coord: {
longitude, latitude }, label: areaName, count, value } = item
// 创建覆盖物
let label = new window.BMap.Label('', {
position: new window.BMap.Point(longitude, latitude),
offset: new window.BMap.Size(-35, -35)
})
// 设置覆盖物内容
label.setContent(`<div class="${
styles.bubble}">
<p class="${
styles.name}">${
areaName}</p>
<p>${
count}套</p>
</div>`)
// 设置样式
label.setStyle(labelStyle)
// 添加点击事件
label.addEventListener('click', function () {
// 当点击了覆盖物,要以当前点击的覆盖物为中心来放大地图
map.centerAndZoom(this.K.position, 13);
// 解决清除覆盖物的时候,百度地图js报错问题
setTimeout(function () {
map.clearOverlays()
}, 0)
})
// 给label添加唯一标识
label.id = value
// 添加到地图上
map.addOverlay(label)
})
5.3 业务逻辑封装
经过分析业务逻辑我们发现,区,镇,小区要实现的业务逻辑是相似的,为了提高代码的复用性,可以对此进行一层封装
封装流程图如下图所示:
1、renderOverlays() 作为入口
* 接收区域id参数,获取该区域下的房源数据
* 获覆盖物类型以及下级地图缩放级别
2、createOverlays() 方法
* 根据传入的类型,调用对应方法,创建覆盖物,到底是创建区镇的覆盖物还是小区覆盖物
3、createCircle() 方法
* 根据传入的数据创建覆盖物,绑定事件(放大地图,清除覆盖物,渲染下一级房源数据)
4、createReact() 方法
* 根据传入的数据创建覆盖物,绑定事件(移动地图,渲染房源列表)
5.4 业务逻辑封装-renderOverlays 方法的封装
实现步骤:
1、接收区域 id 参数,获取该区域下的房源数据
2、获取房源类型以及下级地图缩放级别
代码示例:
在src/pages/Map/index.js中添加如下代码:
/**
* 根据id获取对应的房源信息
*/
async renderOverlays(id) {
// 请求,拿到对应房源数据
let res = await axios.get(`http://localhost:8080/area/map?id=${
id}`)
let data = res.data.body
let {
type,nextZoom} = this.getTypeAndZoom()
// 遍历,调用createOverlays创建覆盖物
data.map(item => {
this.createOverlays(item,type,nextZoom)
})
}
/**
* 获取对应要绘制的类型和缩放的比例
*/
getTypeAndZoom() {
// 调用地图的 getZoom() 方法,来获取当前缩放级别
const zoom = this.map.getZoom()
let nextZoom, type
if (zoom >= 10 && zoom < 12) {
// 区
// 下一个缩放级别
nextZoom = 13
// circle 表示绘制圆形覆盖物(区、镇)
type = 'circle'
} else if (zoom >= 12 && zoom < 14) {
// 镇
nextZoom = 15
type = 'circle'
} else if (zoom >= 14 && zoom < 16) {
// 小区
type = 'rect'
}
return {
nextZoom,
type
}
}
最后,在初始化地图方法initMap中调用:
// 初始化地图
initMap() {
// 获取当前定位城市
const {
label, value } = JSON.parse(localStorage.getItem('hkzf_city'))
// 初始化地图实例
const map = new BMap.Map('container')
// 作用:能够在其他方法中通过 this 来获取到地图对象
this.map = map
// 创建地址解析器实例
const myGeo = new BMap.Geocoder()
// 将地址解析结果显示在地图上,并调整地图视野
myGeo.getPoint(
label,
async point => {
if (point) {
// 初始化地图
map.centerAndZoom(point, 11)
// 添加常用控件
map.addControl(new BMap.NavigationControl())
map.addControl(new BMap.ScaleControl())
// 调用 renderOverlays 方法
this.renderOverlays(value)
}
},
label
)
}
5.5 业务逻辑封装-createOverlays 方法的封装
实现步骤:
1、这个方法主要是逻辑判断,然后根据不同条件调用不同渲染的方法
代码示例:
在src/pages/Map/index.js中添加如下代码:
// 创建覆盖物
createOverlays(data, zoom, type) {
const {
coord: {
longitude, latitude },
label: areaName,
count,
value
} = data
// 创建坐标对象
const areaPoint = new BMap.Point(longitude, latitude)
if (type === 'circle') {
// 区或镇
this.createCircle(areaPoint, areaName, count, value, zoom)
} else {
// 小区
this.createRect(areaPoint, areaName, count, value)
}
}
5.6 业务逻辑封装-createCircle 方法的封装
实现步骤:
1、复用之前的创建覆盖物的代码逻辑
2、在覆盖物的单击事件中,调用 renderOverlays(id)方法,重新渲染该区域的房屋数据
代码示例:
在src/pages/Map/index.js中添加如下代码:
// 创建区、镇覆盖物
createCircle(point, name, count, id, zoom) {
// 创建覆盖物
const label = new BMap.Label('', {
position: point,
offset: new BMap.Size(-35, -35)
})
// 给 label 对象添加一个唯一标识
label.id = id
// 设置房源覆盖物内容
label.setContent(`
<div class="${
styles.bubble}">
<p class="${
styles.name}">${
name}</p>
<p>${
count}套</p>
</div>
`)
// 设置样式
label.setStyle(labelStyle)
// 添加单击事件
label.addEventListener('click', () => {
// 调用 renderOverlays 方法,获取该区域下的房源数据
this.renderOverlays(id)
// 放大地图,以当前点击的覆盖物为中心放大地图
this.map.centerAndZoom(point, zoom)
// 解决清除覆盖物时,百度地图API的JS文件自身报错的问题
setTimeout(() => {
// 清除当前覆盖物信息
this.map.clearOverlays()
}, 0)
})
// 添加覆盖物到地图中
this.map.addOverlay(label)
}
5.7 业务逻辑封装-createRect 方法的封装
实现步骤:
1、创建Label、设置样式、设置html内容,绑定事件
代码示例:
在src/pages/Map/index.js中添加如下代码:
// 创建小区覆盖物
createRect(point, name, count, id) {
// 创建覆盖物
const label = new BMap.Label('', {
position: point,
offset: new BMap.Size(-50, -28)
})
// 给 label 对象添加一个唯一标识
label.id = id
// 设置房源覆盖物内容
label.setContent(`
<div class="${
styles.rect}">
<span class="${
styles.housename}">${
name}</span>
<span class="${
styles.housenum}">${
count}套</span>
<i class="${
styles.arrow}"></i>
</div>
`)
// 设置样式
label.setStyle(labelStyle)
// 添加单击事件
label.addEventListener('click', () => {
console.log('小区被点击了')
})
// 添加覆盖物到地图中
this.map.addOverlay(label)
}
现在只是封装了创建小区覆盖物的方法,点击获取小区下的所有房源数据和渲染房源列表还未实现