需求
- 完成前端界面开发
- 实现列表滚动加载、图片懒加载效果
-
模块概览
开发
底部导航
引入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 />参数:```javascriptconst 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 = undefinedexport 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<divid="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) returnobserver.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 = undefinedexport 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<divid="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 dataif (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 observerconst 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.targetconst 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-ignoreif (!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 falsereturn 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`实现修改路由等操作```javascriptexport 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 = truemark &&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>))) : (<divstyle={{margin: '50% auto',width: '10%',height: '10px',}}><SpinLoading color="primary" /></div>)}
- 渲染页面时要还要判断当前页面的 Lists ,如果有就不要重新渲染了 而是接着下面添加
- 发现是因为每次整个页面都重新更新了——渲染了一个loading组件

