需求
- 完成前端界面开发
- 实现列表滚动加载、图片懒加载效果
-
模块概览
开发
底部导航
引入ui react-icon
- antd-mobile TabBar,最新版是5. ,而umi自带的是2,api有所变化,自行进行了修正
首页开发
创建 less 工具函数.flex()
,因为实际开发中很多 flex 布局 ```less .flex(@direction:row,@justify:center,@align:center) { display: flex; flex-direction: @direction; justify-content: @justify; align-items: @align; }
<a name="kEb7x"></a>
### 首页数据
对首页数据 mock ,方便后期与后端进行联调<br />需要的接口:
- 可选城市
- 热门民宿
**原则:父组件中的各个子组件没有数据交互,那么就将所有数据放到父组件中,通过父组件传递给子组件**
---
<a name="ViQ0G"></a>
#### 数据mock
使用之前开发的<br /> `useHttpHook`:<br />参数:
```javascript
const useHttpHook = ({
url, //请求路径
method = 'post', //请求方式
headers, //请求头
body = {}, //请求体
watch = [], //useEffect 依赖项
}) => {}
返回一个数组,其中 [data, isLoadingFlag]
:分别为请求得到的数据和请求发送结束的标志
搜索界面
点击搜索进行跳转
const history = useHistory()
const handleSearchClick = () => {
if (!times.includes('~')) {
Toast.show({
icon: 'fail',
content: '请选择时间',
})
return
}
history.push({
pathname: '/search',
query: {
code: selectedCity,
startTime: times.split('~')[0],
endTime: times.split('~')[1],
},
})
}
数据同样是 mock 的请求 异步加载,加载时显示 spinLoading
分页加载
useHttpHook + 数据监听
监听页面是否滑倒最底部:Intersection Observer
IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。 当一个IntersectionObserver对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦IntersectionObserver被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。
demo.js
import React, { useEffect } from 'react'
import { useHistory } from 'react-router-dom'
// 因为api 也是消耗性能的,所以只在他需要的时候使用,不需要的时候要停止监听
let observer = undefined
export default function(props) {
const history = useHistory()
useEffect(() => {
// 参数是一个 函数,监听的 DOM 出现或消失在页面中时调用
observer = new IntersectionObserver(entries => {
console.log(entries)
// entries 是一个数组 [IntersectionObserverEntry]
// 每个IntersectionObserverEntry 中有几个属性,常用的如下:
// intersectionRatio:范围0~1 子元素进入范围
// isIntersecting :布尔值 是否可见
})
// 监听 DOM 元素
const listened = document.querySelector('#listened')
observer.observe(listened)
// 离开页面时
return () => {
if (observer) {
// 解除元素绑定
observer.unobserve(listened)
// 停止监听
observer.disconnect()
}
console.log(observer) // 可以自己看看有什么变化
}
}, [])
return (
<div onClick={() => history.push('/')}>
observer
<div
id="listened"
style={{
width: '100px',
height: '100px',
backgroundColor: 'orange',
marginTop: '1000px',
}}
>
loading
</div>
</div>
)
}
然后我们再将其抽离为一个 自定义HookuseObserverHook
分页Hook:useObserverHook
import { useEffect } from 'react'
let observer = undefined
// 传入要监听的DOM元素 ele, 监听元素的回调函数 callback, useEffect 的依赖项
export default function useObserverHook(
selector,
callback,
watch = [],
) {
useEffect(() => {
const listened = document.querySelector(selector)
if (listened) {
observer = new IntersectionObserver(entries => {
callback && callback(entries)
})
observer.observe(listened)
}
return () => {
if (!observer || !listened) return
observer.unobserve(listened)
observer.disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, watch)
}
回到原先的demo进行调用
import React, { useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { useObserverHook } from '@/hooks'
// 因为api 也是消耗性能的,所以只在他需要的时候使用,不需要的时候要停止监听
// let observer = undefined
export default function(props) {
const history = useHistory()
// useEffect(() => {
// // 参数是一个 函数,监听的 DOM 出现或消失在页面中时调用
// observer = new IntersectionObserver(entries => {
// console.log(entries)
// // entries 是一个数组 [IntersectionObserverEntry]
// // 每个IntersectionObserverEntry 中有几个属性,常用的如下:
// // intersectionRatio:范围0~1 子元素进入范围
// // isIntersecting :布尔值 是否可见
// })
// // 监听 DOM 元素
// const listened = document.querySelector('#listened')
// observer.observe(listened)
// // 离开页面时
// return () => {
// if (observer) {
// // 解除元素绑定
// observer.unobserve(listened)
// // 停止监听
// observer.disconnect()
// }
// console.log(observer)
// }
// }, [])
// 监听 DOM 元素
// const listened = document.querySelector('#listened')
useObserverHook('#listened', entries => {
console.log('callback--', entries)
})
return (
<div onClick={() => history.push('/')}>
observer
<div
id="listened"
style={{
width: '100px',
height: '100px',
backgroundColor: 'orange',
marginTop: '1000px',
}}
>
loading
</div>
</div>
)
}
现在回到 搜索页面 在此进行分页加载
- 监听 发送请求后返回的
loading
是否为 true ,确保有数据展示(DOM 节点是否可以看见) - 修改分页数据
- 监听分页数据的修改,发送接口请求下一页的数据
监听
loading
变化,拼装数据(请求返回的loading
,表示请求是否结束)- 根据
loading
状态来进行相应的操作const [page, setPage] = useState({
pageSize: 6, // 一页展示多少
pageNum: 1, // 当前页码
})
另外在mock方面控制const [houses, loading] = useHttpHook({
url: '/houses/search',
body: {},
watch: [page.pageNum], // 监听 pageNum 的变化
})
此时 lightHouse 得分 42 变为 56'post /api/houses/search': (req, res) => {
let data
if (req.body.pageNum < 4) {
data = [...]
} else {
data = []
}
res.json({
status: 200,
data,
})
图片的懒加载
懒加载:当图片进入可视区时,才显示真实的图片;否则就只是一个 填充品
- 根据
监听图片是否进入可视区域
- 进入就将 src 属性的值替换为真实的图片地址
data-src
,一开始是假的 src :fake-src
- 已经替换为真实图片地址了,就停止监听
懒加载 Hook:useImgHook
import { useEffect } from 'react'
/**
*
* @param {DOM元素} ele
* @param {function} callback 回调函数
* @param {数组} watch 监听项
* @returns
*/
let observer
const useImgHook = (ele, callback, watch = []) => {
useEffect(() => {
const nodes = document.querySelectorAll(ele)
if (nodes && nodes.length) {
observer = new IntersectionObserver(entries => {
callback && callback(entries)
entries.forEach(item => {
// console.log(item)
if (item.isIntersecting) {
const itemTarget = item.target
const dataSrc = itemTarget.getAttribute('data-src')
// console.log(dataSrc)
itemTarget.setAttribute('src', dataSrc)
observer.unobserve(itemTarget)
}
})
})
nodes.forEach(item => {
observer.observe(item)
})
}
return () => {
if (nodes && nodes.length && observer) {
observer.disconnect()
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, watch)
}
export default useImgHook
优化:提取公共组件 ShowLoading
当其他页面也需要滚动加载…
优化点:
- 抽离公共部分作为组件
- 优化样式
- 优化 id ```jsx import React from ‘react’ import { SpinLoading } from ‘antd-mobile/es’ import PropTypes from ‘prop-types’
import ‘./index.less’
export default function ShowLoading(props) { return (
ShowLoading.defaultProps = { showLoading: true, id: ‘zhou-loading’, }
ShowLoading.propTypes = { showLoading: PropTypes.bool, id: PropTypes.string, }
<a name="DzZZA"></a>
#### 优化: 建立 enums 专门存放重复出现多次的值
如页码、id、
<a name="wYV4P"></a>
#### 优化:utils 常用函数集锦
type.ts : 全面返回某个东东的类型
```typescript
/**
*
* @param ele {any}元素
* @returns {string} 元素类型的字符串
*/
export default function type(ele: any): string {
const toString = Object.prototype.toString,
map: any = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object',
'[object Map]': 'map',
'[object Set]': 'set',
'[object Symbol]': 'symbol',
}
return map[toString.call(ele)]
}
isEmpty.ts : 判断一个东西是否为空对象 | 空数组 | 空字符串
import type from './type'
/**
* 判断空对象,空数组,空字符串
* @param obj 数组或者对象或者字符串
* @returns boolean
*/
export default function isEmpty(
obj: Array<any> | Object | string,
): boolean {
if (!obj) {
return true
}
if (obj === '') {
return true
}
if (type(obj) === 'array') {
// @ts-ignore
if (!obj.length) {
return true
}
}
if (type(obj) === 'object') {
if (JSON.stringify(obj) === '{}') {
return true
}
}
return false
}
详情页
快速构建轮播图 🐶
借助一个 第三方写好的 swiperyarn add react-awesome-swiper
评论浮窗
评论列表
- 初次渲染
- 分页加载
- 评论功能
- 列表重置
分页加载
数据流+数据监听
- 监听 loading 是否展示
- 触发 reload 修改分页
- 监听 reload 变化,重新请求接口
- 拼装数据
订单页面
ui
const [orders] = useHttpHook({
url: '/order/lists',
body: {
...page,
},
})
const tabs = [
{ title: '未支付', key: 0, orders, type: 0 },
{ title: '已支付', key: 1, orders, type: 1 },
]
不一样的滚动加载
只监听底部 loading ,出现就直接发送请求,而不监听数据
思路:
- 通过伪元素实现骨架样式
- 制作布局组件 添加骨架样式
- 替换默认Loading样式
我的页面
edit
- 添加用户头像
- 设置用户电话
- 设置用户签名
使用 antd-mobile 里面的 ImageUploader
组件
需求:需要点击修改按键后上传数据,这里借助一个 第三方依赖 **rc-form**
来解决给表单每个input 绑定 onchange 的麻烦事
login
表单用到了rc-form
存储信息到cookie
/**
* 验证是否可以被JSON.parse
* @param ele {any} 元素
* @returns {boolean} boolean
*/
export default function isJsonString(ele: any): boolean {
try {
JSON.parse(ele)
} catch (e) {
return false
}
return true
}
import isJsonString from './isJsonString'
interface CONFIG {
hours?: number // 过期时间,单位小时
path?: string // 路径
domain?: string // 域名
secure?: boolean // 安全策略
httpOnly?: boolean // 设置键值对是否可以被 js 访问
sameSite?: 'strict' | 'Strict' | 'lax' | 'Lax' | 'none' | 'None' // 用来限制第三方 Cookie
}
/**
* 操作 cookie
*/
const cookie = {
/**
* 判断cookie是否可用
* @returns {boolean} boolean
*/
support(): boolean {
if (!(document.cookie || navigator.cookieEnabled)) return false
return true
},
/**
* 添加cookie
* @param name {string} cookie 键
* @param value {string | object} cookie 值
* @param config {object} 可选配置项
*
- {
- hours: 过期时间,单位小时,
- path: 路径,
- domain: 域名,
- secure: 安全策略,
- httpOnly: 设置键值对是否可以被 js 访问,
- sameSite: 用来限制第三方 Cookie
- }
``` */ set(name: string, value: string | object, config?: CONFIG): void { if (!this.support()) { console.error( ‘ (Cookie方法不可用):浏览器不支持Cookie,请检查相关设置’, ) return }
let data = name + ‘=’ + encodeURIComponent(JSON.stringify(value))
if (config?.hours) { const d = new Date() d.setHours(d.getHours() + config?.hours) data += ‘; expires=’ + d.toUTCString() }
if (config?.path) { data += ‘; path=’ + config.path }
if (config?.domain) { data += ‘; domain=’ + config.domain }
if (config?.secure) { data += ‘; secure=’ + config.secure }
if (config?.httpOnly) { data += ‘; httpOnly=’ + config.httpOnly }
if (config?.sameSite) { data += ‘; sameSite=’ + config.sameSite }
document.cookie = data },
/**
- 查询 cookie
- @param name {string} Cookie 的键;如果参数为空则获取所有的cookie
@returns {string | object | null} 有参数获取cookie后返回字符串,没有参数获取cookie返回json;获取不到则返回 null */ get(name?: string): string | object | null { if (!this.support()) { console.error( ‘ (Cookie方法不可用):浏览器不支持Cookie,请检查相关设置’, ) return }
let cs = document.cookie, arr = [], obj: any = {} arr = cs.split(‘;’)
if (cs !== ‘’) { for (let i = 0; i < arr.length; i++) { const a = arr[i].split(‘=’) const key = a[0].trim() if (key !== ‘’) {
const val = decodeURIComponent(a[1])
obj[key] = isJsonString ? JSON.parse(val) : val
} }
return name ? obj[name] : obj } else { return null } },
/**
- 删除 cookie
- @param name Cookie 的键;如果参数为空,则清理所有的cookie
@param path 路径,默认为’’ */ remove(name: string, path?: string): void { if (!this.support()) { console.error( ‘ (Cookie方法不可用):浏览器不支持Cookie,请检查相关设置’, ) return }
if (arguments.length === 0) { const all = this.get() Object.keys(all).forEach(item => { this.set(item, ‘’, { hours: -1 }) }) } else { this.set(name, path || ‘’, { hours: -1 }) } }, }
export default cookie
<a name="TPF4Q"></a>
#### 未登录时点击我的页面应该跳转到登录页面
多个页面都需要验证的话就会有重复的代码<br />利用 umi 的运行时配置~<br />`src/app.js`实现修改路由等操作
```javascript
export function onRouteChange(route) {
console.log(route)
}
auto:true
表示需要验证
{
path: '/order',
component: './order/index',
title: '订单',
auth: true,
},
优化
memo
举一个例子
const areaEqual = (preProps, nextProps) => {
console.log(preProps, nextProps)
if (
preProps === nextProps &&
preProps.citysLoading === nextProps.citysLoading
) {
return true
}
return false
}
export default memo(Search, areaEqual)
memo自动对 两次 props 进行对比,但是只是浅层次的对比,需求复杂时,可以自己写一个方法进行精准的判断,作为memo的第二个参数
开发中遇到的问题
- 一开始 umi 默认开启 CSS Moudle,不习惯,我把它关了
export default {
disableCSSModules: true,
}
- umi 中的 useLocation 也有问题,直接从 react-router-dom 中引用就没问题,umi2 bug真多。。
- 分页加载时候发现 滑倒底部引发重复的请求,需要回到 useHttpHook 中进行限制:
节流:
// 节流
let mark = true
mark &&
setTimeout(() => {
return new Promise((resolve, reject) => {
fetch('/api' + url, params)
.then(res => res.json())
.then(res => {
if (res.status === 200) {
resolve(res.data)
setResult(res.data)
} else {
reject(res.errMsg)
}
})
.catch(err => {
console.log(err)
reject(err)
})
.finally(() => {
setLoading(false)
})
})
}, 10)
mark = false
- 然后发现每次分页加载后滚动条都会回到顶部
- 发现是因为每次整个页面都重新更新了——渲染了一个loading组件
- 渲染页面时要还要判断当前页面的 Lists ,如果有就不要重新渲染了 而是接着下面添加
{!loading || housesLists ? (
housesLists?.map(item => (
<div className="item" key={item.id}>
<img alt="img" src={item.img} />
<div className="item-right">
<div className="title">{item.title}</div>
<div className="price">¥{item.price}</div>
</div>
</div>
))
) : (
<div
style={{
margin: '50% auto',
width: '10%',
height: '10px',
}}
>
<SpinLoading color="primary" />
</div>
)}
- 渲染页面时要还要判断当前页面的 Lists ,如果有就不要重新渲染了 而是接着下面添加
- 发现是因为每次整个页面都重新更新了——渲染了一个loading组件