组件
刀刀
1/2/2025
0 字
0 分钟
组件封装
封装思路
在项目中,组件包括以下几种:
- 普通业务组件:SPA 中一个个页面,一般放置在
src/views
下 - 通用业务组件:好多页面中都需要的,我们提取为公共的组件,一般放置在
src/components
下,进行封装和抽取,保证更好的复用性,如属性、插槽 - 通用功能组件:一般 UI 开源组件中都有,如果没有或者不支持才需要自主封装。如大文件上传或断点续传。
一般情况下,我们会对 UI 组件库的组件做二次封装:
- 统一处理复杂业务
- 统一处理样式
- 几个组件作为一个整体的组件
封装组件
头部导航栏封装
antd-modile
组件库提供了头部导航栏组件 NavBar
,但是每次使用都需要引入、设置标题、设置 onBack
事件,代码重复,效率不高。把它抽离出来作为公共组件,父组件只需要传递标题即可。
子组件通过父组件传参动态渲染子组件的内容到页面上,因此父组件的传参需要添加判断,给使用的父组件提供校验,避免传了错误的类型导致报错。
在 React 中,子组件的 props
参数类型通过 props-type
来设置,语法为 函数名.PropTypes
。代码如下:
import React from "react";
import PropTypes from "prop-types";
import { NavBar } from "antd-mobile";
function MyNavBar(props) {
const { title } = props;
// 点击返回
const handleBack = () => {};
return <NavBar onBack={handleBack}>{title}</NavBar>;
}
// 类型
MyNavBar.propTypes = {
title: PropTypes.string,
};
export default MyNavBar;
也可以设置默认参数,父组件没传参时显示默认莫参数,代码如下:
MyNavBar.defaultProps = {
title: "个人中心",
};
点击返回上一页时需要判断,如果是从某些页面执行操作跳转来的登录页,则要返回上一页,否则直接返回首页。代码如下:
const whiteList = [/^\/detail\/\d+$/]
const handleBack = () => {
let to = usp.get('to')
if(location.pathname === '/login' && whiteList.some(item => item.test(to))) {
navigate(to, { replace: true })
return
}
navigate(-1)
};
按钮封装
按钮需要怎么样的封装?
有部分场景比如支付、登录、获取验证码等,如果页面没有相应的效果,则有可能造成用户多次点击发送多次请求。因此需要给按钮添加 loading
效果直到事件结束,或者添加防抖节流等操作。
而按钮其他属性,则需要原封不动渲染到 <Button />
按钮组件上,如下:
<MyButton size="small" type="primary" onClick={handle}>click me</MyButton>
封装成组件后需要父组件传递 size
、type
、onClick
方法和 click me
按钮文本给子组件,子组件需要获取并绑定到按钮组件上去。下面依次来判断:
size
、type
等变量属性子组件可以通过props
形参直接获取到,可以直接使用对象解构的方式获取设置click me
内容文本子组件也能获取到,形参props
中有一个属性children
,值则为父组件传递的文本onClick
函数方法也是通过形参props
获取,但是由于需要为按钮添加额外的设置,也需要添加onClick
事件,因此通过props
获取的点击事件函数需要设置别名
需要注意的是,后续按钮子组件在获取父组件传递过来的 props
可能需要做某些改动,而 props
是 只读属性 ,因此需要赋值给一个新的变量,从而操作那个变量。
代码
import React, { useState } from "react";
import { Button } from "antd-mobile";
export default function MyButton(props) {
// props包含<Button></Button>组件里的所有属性
let options = { ...props };
let { children, onClick: handleClick } = options;
delete options.children;
delete options.handleClickFn;
// 点击事件
const handleClickFn = async () => {
try {
await handleClick();
} catch (error) {
console.log(error);
}
};
return (
<Button {...options} onClick={handleClickFn}>
{children}
</Button>
);
}
上方代码中,父组件传过来的数据通过对象点语法简便的全部赋到 <Button />
组件上。由于 onClick
事件与文本内容是独立设置,因此还需要把这两个属性方法从 options
对象中去除。
接下来就是处理这个点击事件。子组件按钮点击事件触发后,先让按钮开启 loading
加载状态,然后执行父组件传递过来的事件函数。执行完毕后关闭 loading
状态。代码如下所示:
export default function MyButton(props) {
// ...
// 状态
let [loading, setLoading] = useState(false);
// 点击事件
const handleClickFn = async () => {
setLoading(true);
try {
await handleClick(); // handleClick方法是父组件传过来的方法,等待它执行完毕再取消loading
} catch (error) {
console.log(error);
}
setLoading(false);
};
return (
<Button {...options} loading={loading} onClick={handleClickFn}>
{children}
</Button>
);
}
这么写已经能够实现业务了,但是还是会有一点潜在的小 BUG 需要优化。如果父组件没传点击事件函数,则使用该子组件时就会报错。该怎么办呢?
此时不能直接给按钮绑定点击事件,而是事先判断父组件有没有传点击事件,传递了点击事件后再把 onClick
事件绑定到 options
函数中。由于 options
函数通过对象的点语法赋值给 <Button />
按钮组件,因此也能最终把点击事件赋值上去。
最终代码
import React, { useState } from "react";
import { Button } from "antd-mobile";
import "./MyButton.scss";
export default function MyButton(props) {
// props包含<Button></Button>组件里的所有属性
let options = { ...props };
let { children, onClick: handleClick } = options;
delete options.children;
// 注意这里不能把点击事件删除了
// 状态
let [loading, setLoading] = useState(false);
// 点击事件
const handleClickFn = async () => {
setLoading(true);
try {
await handleClick(); // handleClick方法是父组件传过来的方法,等待它执行完毕再取消loading
} catch (error) {
console.log(error);
}
setLoading(false);
};
// 如果父组件没传点击事件,则需要把我们的点击事件传进去;没有传点击事件,则不给他绑定点击事件。
// 由于下面通过解构options,因此onClick方法也能被绑定进去
if (handleClick) options.onClick = handleClickFn;
return (
<Button {...options} loading={loading}>
{children}
</Button>
);
}
组件页面
首页
头部组件
静态图片引入
在引入静态图片资源时,不能使用相对路径的方法,如下:
<img src="../assets/images/logo512.png" alt="" />
保存运行后会发现图片没有渲染到页面上。这是因为打包处理后,项目的结构目录会被改变,而图片的资源路径还是原来的资源路径。
而 CSS 通过相对路径使用静态资源图片生效的原因是 CSS 打包后会处理css的图片导入,所以可以使用相对路径。其主要做以下操作:
- 把需要的图片打包
- 把打包后的地址重新覆盖css中写的地址
因此解决图片引入不生效的办法有两个:
- 使用绝对路径或网址路径
- 基于ES6Module模块的方式导入图片
代码修改为以下形式:
import React from "react";
import timg from "../assets/images/logo512.png";
export default function HomeHead() {
return (
<div className="home-head-box">
<div className="photo">
<img src={timg} alt="" />
</div>
</div>
);
}
现在运行页面,可以看到图片已经生效。
后续判断 redux
中是否保存到用户信息,如果没有则尝试调用接口获取最新的用户信息。图片标签则通过三元表达式判断渲染对应的图片。
代码
import React, { useEffect } from "react";
import timg from "../assets/images/logo512.png";
import Api from '@/api'
import { connect } from 'react-dom'
import action from '@/store/action'
function HomeHead(props) {
const {info, infoAsync} = props
useEffect(() => {
if(!info) {
infoAsync()
}
}, [])
return (
<div className="home-head-box">
<div className="photo">
<img src={info ? info.img : timg} alt="" />
</div>
</div>
);
}
export default connect(
state => state.base,
action.base
)(HomeHead)
计算月日
计算年月我们封装成一个函数方法,该方法使用了以下两个知识点:
useMemo
:计算属性,缓存数据,优化性能,第一个参数表示一个 回调函数 ,第二个表示依赖的数据。其在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用。
在 React 中,还有一个属性
useCallback
也有同样的作用,二者的区别是:useMemo
缓存的结果是回调函数中return
回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态useCallback
缓存的结果是函数,主要用于缓存函数,应用场景如需要缓存的函数,因为函数式组件每次任何一个state发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费;另外还需要注意的是,useCallback
应该和React.memo
配套使用,缺了一个都可能导致性能不升反而下降。
示例代码
jsimport React, { useState, useMemo } from 'react'; function Info(props) { let [personalInfo , setPersonalInfo] = useState({ name: 'kevin kang', gender: 'male' }) function formatGender(gender) { console.log('---调用了翻译性别的方法---') return gender === 'male' ? '男' : '女' } // BAD // 不使用useMemo的情况下,修改其他属性,也会重新调用formatGender方法,浪费计算资源 // let gender = formatGender(personalInfo.gender) // GOOD let gender = useMemo(()=>{ return formatGender(personalInfo.gender) }, [personalInfo.gender]) return ( <> <div> 姓名: {personalInfo.name} -- 性别: { gender } <br/> <button onClick={ ()=> { setPersonalInfo({ ...personalInfo, name: 'Will Kang' }) } }> 点击修改名字</button> </div> </> ) } export default Info
match
match
一般都要结合RegExp
的全局g属性来使用的,这样返回的结果是:匹配到的所有结果的数组;jslet str1 = "12345678"; let reg1 = /\d{1,3}(\d{3})*/g console.log(str1.match(reg1)) // ["123456", "78"]
如果不结合g使用,则在没有分组的情况下,只会返回一个匹配结果(或者没有匹配结果时返回null);
jslet str0 = "12345678"; let reg0 = /\d{1,3}/ console.log(str0.match(reg0)) // ['123', index: 0, input: '12345678', groups: undefined] 1234 // 这个例子的结果是没有使用全局匹配的正则表达式的匹配结果。说白了,就是正则表达式的末尾没跟g。由于不适用全局匹配,所以match()方法只找到源字符串中首次匹配的子串后,就立刻得到返回结果,不再比较之后剩余的部分是否还有能匹配上的内容。 // 我们可以看到,match()的结果是一个数组,该数组一共有4项。各项代表的意思如下: // 第0项:匹配到字符串 // 第1项:index表示首次匹配上的子串的起始下标。 // 第2项:input,表示源字符串 // 第3项:groups:undefined,这表示当前的正则表达式没使用分组 // 第4项:length,表示匹配到的结果个数,由于这里不使用全局匹配,只找到首次匹配项就结束了,所以匹配结果只有1个,length也就是1。
如果不结合g使用,在有分组的情况下(还要没有开始(^)和结束($)符),则还会返回分组匹配到的内容,例如:
jsvar str = 'Today is the 286th day of 2018, the 108th Thanksgiving Day.'; var results = str.match(/\d+(t)(h)/); //匹配str中首个以数字开头,并且以th结尾的子串 console.log(results); // ['286th', 't', 'h', index: 13, input: 'Today is the 2
前置知识点盘点完毕后,接下来开始实现计算月日的方法。步骤如下:
useMemo()
监听当前时间,时间变量发生才执行方法,减少性能消耗match()
方法找出去符合条件的月和日,返回的是一个数组,用数组解构的方式分别获取- 创建一个月份字典数组,通过月的值作为索引获取对应的月份
代码示例
// ...
export default function HomeHead(props) {
const { today } = props;
// 计算时间中的月和日
let times = useMemo(() => {
let a = today.match(/^\d{4}(\d{2})(\d{2})$/);
console.log(a);
let [, months, day] = today.match(/^\d{4}(\d{2})(\d{2})$/),
area = [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
];
return {
month: area[parseInt(months) - 1],
day,
};
}, [today]);
return (
<div className={HomeHeadStyle["home-head-box"]}>
<div className={HomeHeadStyle.info}>
<div className={HomeHeadStyle.time}>
<span>{times.day}</span>
<span>{times.month}</span>
</div>
<div className={HomeHeadStyle.title}>知乎日报</div>
</div>
);
}
新闻列表
新闻列表希望做到用户点击后字体颜色变灰,表明用户已经点击过该条新闻。
想要实现这个功能,只需要依靠 CSS 中的 :visited
伪类,复习一下 CSS 提供的几个伪类:
:link
:未访问的链接:visited
:已访问的链接:hover
:鼠标悬停链接:active
:已选择的链接
⚠ 注意
a:hover
必须在 CSS 定义中的 a:link
和 a:visited
之后,才能生效!a:active
必须在 CSS 定义中的 a:hover
之后才能生效!伪类名称对大小写不敏感。
最后通过 CSS 把字体样式改的浅一点即可。代码如下:
&:visited {
.title {
color: #999;
}
.author {
color: #ccc;
}
}
加载更多
控制组件显隐的方式有两种:
- 控制其是否渲染,没有数据可以不渲染,也获取不到 DOM 元素,有数据再渲染
- 控制元素的样式,无论是否有值都会渲染,只不过
display: none
隐藏元素
由于我们需要获取到节点判断其是否显示在可视区域内(即是否滚动到底部),因此我们要选择第二种方法。
判断元素是否出现在可视区域内可通过 new IntersectionObserver()
方法实现,该方法在 小兔鲜 有介绍,因此这里不做过多描述。代码如下所示:
// 第一次渲染完毕,设置监听器,实现触底加载
useEffect(() => {
let ob = new IntersectionObserver(async (changes) => {
let { isIntersecting } = changes[0];
if (isIntersecting) {
// 已经触底,获取新的数据
try {
let times = newList.length > 0 && newList[newList.length - 1]["date"];
const res = await Api.queryNewsLatest(times);
let newArr = newList;
newArr.push({
date: res.date,
stories: res.stories,
});
setNewList([...newArr]);
} catch (error) {
console.log(error);
}
}
});
ob.observe(loadmore.current);
// 销毁时loadmore.current已经没了,而通过变量保存的值还在,因此需要用变量保存
let loadmoreRef = loadmore.current;
// 事件销毁,手动销毁监听器
return () => {
ob.unobserve(loadmoreRef);
ob = null;
};
}, []);
⚠ 注意
在组件释放的时候, React 内部会移除:
- 虚拟 DOM
- 真实 DOM
- 合成事件绑定
- ......
不会移除的东西:
- 设置的定时器
- 设置的监听器
- 基于
addEventListener
手动做的事件绑定 - ...
因此为了减少不必要的性能消耗,需要手动销毁监听器!
详情
详情模块需要渲染富文本内容,直接通过 {}
方法渲染会把标签原原本本也渲染到页面上。
有一个 API ,作用和 v-html
类似,用于渲染富文本的内容,名称为 dangerouslySetInnerHTML
。复制代码保存运行,代码如下:
<div className="content" dangerouslySetInnerHTML={info.body}></div>
发现报错,报错信息如下所示:
翻译报错信息,我们需要提供一个 __html
属性,因此需要传递一个对象过去,修改代码为以下形式:
<div className="content" dangerouslySetInnerHTML={{ __html: info.body }}></div>
效果实现,但是没有样式。查看后端返回的数据,发现其样式与结构是分离的,样式返回了一个 CSS 链接,需要我们动态通过 <link href="" style="" />
标签设置。
代码
export default function Detail(props) {
// 在配置路由组件时已经把路由相关信息传参过来了
const { navigate, params } = props;
const [info, setInfo] = useState(null)
// ...
// 处理样式、图片
let link;
const handleImg = () => {
let imgPlaceHolder = document.querySelector(".img-place-holder");
if (!imgPlaceHolder || !info) return;
let tempImg = new Image();
tempImg.src = info.image;
tempImg.onload = () => {
imgPlaceHolder.appendChild(tempImg);
};
tempImg.onerror = () => {
let parent = imgPlaceHolder.parentNode;
parent.parentNode.removeChild(parent);
};
};
const handleStyle = () => {
if (info) {
// 确保css存在
if (!Array.isArray(info.css)) return;
let css = info.css[0];
if (!css) return;
// css存在再设置样式引入
link = document.createElement("link");
link.rel = "stylesheet";
link.href = css;
// 添加到document中
document.head.appendChild(link);
}
};
useEffect(() => {
try {
handleImg();
handleStyle();
} catch (error) {
console.log(error);
}
return () => {
// 移除添加的样式
if (link) document.head.removeChild(link);
};
}, [info]);
// ...
}
登录
表单数据收集
React 是 MVC
结构的框架,因此无法做到数据双向绑定,用户输入内容时动态获取用户输入的内容,需要手动获取。
在 Vue
中,v-model
数据双休绑定实际上是一个语法糖,他的本质是 :value
显示数据、@change
在用户输入时实时获取数据。因此在 React
中我们也可以从这里入手:
- 定义一个变量方法接收修改表单数据
- 输入框组件通过
value
显示对应数据 - 通过
onChange
事件实时获取用户输入的最新内容
代码如下:
export default function Test() {
const [phone, setPhone] = useState('')
return (
<input value={phone} onChange={
(e) => {
setPhone(e.target.value)
}
} />
)
}
⚠ 注意
如果是表单组件则不需要使用 onChange
实时获取,表单组件会获取到用户输入的数据。
校验
表单校验时实际上做了以下事情:
客户端:
- 手机规则校验
- 防止未必要的请求
- 防止 SQL 注入
- 向服务器发送请求
- 客户端接收服务器返回的内容
- 失败给出提示
- 成功开启倒计时
服务器:
手机号格式的再次校验
创建一个验证码
存储到数据库中,格式【手机号 验证码】,方便后期登录校验
编辑短信内容
调用第三方平台,把短信发送到用户手机上【付费操作】
服务器把发送的结果返回给客户端
本项目使用的是 Antd-Mobile
组件库,查看 Form
表单组件的文档,看表单校验规则的设置方式以及如何判断校验。
表单校验规则有两种方法:
直接使用校验字段做校验规则,在
<Form.Item>
标签通过rules
标签设置校验规则数组即可。代码如下:jsx<Form.Item label="验证码" name="code" rules={[ { required: true, message: "验证码必填" }, { pattern: /^\d{6}$/, message: "验证码6位数字" }, ]} extra={ <MyButton color="primary" disabled={disabled} size="small" onClick={sendCode} > {sendText} </MyButton> } > <Input placeholder="请输入验证码" /> </Form.Item>
设置自定义校验规则。注意,使用该方法不仅需要设置返回的错误提示,也需要返回相对应正确的提示,否则校验成功后无法往下执行
jsxconst validate = { phone(_, value) { value = value.trim(); let reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/; if (value.length === 0) return Promise.reject(new Error("手机号必填")); if (!reg.test(value)) return Promise.reject(new Error("手机号格式不正确")); return Promise.resolve(); }, };
再通过
rules
设置,代码如下:jsx<Form.Item name="phone" label="手机号" rules={[{ validator: validate.phone }]} > <Input placeholder="请输入手机号" /> </Form.Item>
登录
执行登录操作时实际上做了以下事情:
客户端:
- 表单校验
- 向服务器发送请求(传输手机号 / 验证码)
- 客户端接收服务器返回的结果
- 成功:存储 token 方便后续某些操作(本地存储、redux),跳转页面
- 失败:提示
服务器:
- 获取传输的内容做二次校验
- 在数据库中匹配手机号与验证码是否一致
- 失败:返回登录失败
- 成功:看手机号是否被注册,没注册过注册 + 登录,注册过直接登录
- 返回一个 Token 令牌,根据登录者信息 + 时间 + 密钥创建 token
对于本地存储的事件,我们可以做二次封装,前端实现 token 有效期的判断以及处理。思路如下:
- 保存数据时,不仅保存 token,还保存保存 token 时的时间戳,后面用于判断
- 获取数据时,判断当前的时间戳与保存的时间戳,如果二者相差预设的值,则表示过期,返回空
- 删除数据时,直接删除该本地存储的数据即可
代码如下:
const storage = {
set(key, value) {
localStorage.setItem(key, JSON.stringify({
time: +new Date(),
value
}))
},
get(key, cycle) {
cycle = +cycle
if(isNaN(cycle)) cycle = 2592000000
let data = localStorage.getItem(key)
if(!data) return null
let { time, value } = JSON.parse(data)
if((+new Data() - time) > cycle) {
storage.remove(key)
return null
}
return value
},
remove(key) {
localStorage.removeItem(key)
}
}
接下来重点看登录成功后的处理,首先梳理一下前往登录页的三种方式:
- 地址栏直接输入登录的路由,此时登录成功后直接返回首页
- 在新闻详情页点击收藏发现没登录,跳转到登录页,登录成功后要返回详情页
- 点击前往个人中心页时发现没登录,前往登录页,登录成功后继续前往个人中心页
代码如下:
const { usp, navigate } = props
let to = usp.get('to')
to ? navigate(to, { replace: true }) : navigate(-1)
to
为要跳转的路由,如果有值则说明是第三种情况,跳转到对应页面;没值则是第一第二种情况,直接返回上一页。
ReactRouter6 与 ReactRouter5 的区别
在 5 版本中,返回上一页通过
history.go(-1)
实现,6 版本中通过navigate(-1)
实现5 版本中
push
传参代码如下:jshistory.push({ pathname: '/xxx', search: '', state: '' })
6 版本代码如下:
jsnavigate.push({ pathname: '/xxx', search: '' }, { state: '' })
第三方组件的思考
组件支持很多属性,第三方组件如图片上传组件,都是对原生 <input type="file" accept />
的设置和封装。例如:
accpet
:控制上传的文件类型,为原生组件accept
属性beforeUpload
:自己封装的,在选择图片上传之前触发,在这个函数中可以限制上传的大小maxCount
:设置允许上传图片的数量,原生组件没有提供,需要自己封装判断逻辑multiple
:是否支持多选,原生组件也有该方法,默认为不支持
以上只是部分属性方法,可以获取 input
的真实 DOM 元素,并通过 console.dir()
方法打印,在控制台上可以看到更多更详细的数据。
部分原生组件就有的数据,可以作为组件属性来封装,父组件需要使用的时候就传值,不需要则给默认值;没有提供的方法属性则通过自主代码封装设置。
代码如下:
export default function UploadImg(props) {
const [pic, setPic] = useState([{ url: props.pic }])
// 上传前的判断
const limitImage = (file) => {
// 如果返回 null 则不会往下走执行图片上传
let limit = 1024 * 1024
if(file.isze > limit) {
Toast.show({
icon: 'fail',
content: '图片必须在1MB内'
})
return null
}
// 成功则返回原本的文件
return file
}
// 图片上传
const uploadImage = async (file) => {
let temp
try {
let formData = new FormData()
formData.append('file', file)
let { code, data } = await Api.uploadImg(formData)
if(code === 200) {
temp = data
setPic([{
url: data
}])
} else {
Toast.show({
icon: 'fail',
content: '图片必须在1MB内'
})
}
} catch(err) {}
// 这里需要返回一个对象,否则会报错
return { url: temp }
}
return (
<ImageUploader value={pic} maxCount={1} beforeUpload={limitImage} upload={uploadImage} onDelete={
() => {
setPic([])
}
} />
)
}
⚠ 注意
antd-mobile
组件库的图片上传组件不支持自动上传,需要手动封装。
组件缓存
没有做组件缓存时,从首页进入详情页,返回后并没有在之前的滚动位置,而是返回顶部,探讨一下底层做了什么。
从首进入详情页页实际上做了以下处理:
- 首页组件:释放真实 DOM、虚拟 DOM 释放
- 详情组件:加载第一次渲染的逻辑
从详情页返回首页实际上做了以下处理:
- 详情组件:释放真实 DOM、虚拟 DOM 释放
- 首页组件:加载第一次渲染的逻辑
正常情况下,每一次路由跳转,都是原有组件释放,新组建加载的过程!
有些需求中,是需要对某个或者某几个组件做 “缓存” 的。做了缓存的组件:
- 每次路由跳转时,组件不销毁
- 再次跳转回来,直接渲染即可
组件缓存机制是 KeepAlive
,和网络中的 connect:keep-alive
不是同一个东西。
在 Vue
中默认就写好了 <keep-alive></keep-alive>
这样的处理机制。React
中没有现成的,需要自己去实现。
思路
主流思想上:
不是标准的组件缓存,只是数据缓存
A -> B
在 A 组件路由跳转时,把 A 组件中需要的数据或 A 组件的全部虚拟 DOM 存储到localStorage
或redux
中!然后 A 组件释放, B 组件加载。
当从 B 回到 A 时,A开始加载。
接着判断是否存储了数据或虚拟 DOM,如果没有,则说明是第一次加载逻辑处理;如果有存储,则把存储的数据拿来渲染。
修改路由的跳转机制
在路由跳转的时候,把指定的组件不销毁,只是控制
display: none
隐藏;后期从 B 回到 A 时,直接让 Adisplay:block
。缺点是比较麻烦,需要深知源码才能修改。
缓存真实信息
把 A 组件的真实 DOM 等信息,直接缓存起来;从 B 跳转回 A 的时候,直接把 A 之前缓存的信息拿出来用
第三方库
NPM 上有第三方库 keepalive-react-component
处理了这个事情,官网指路:keepalive-react-component 。
下载完之后,使用方式如下:
在根组件
App.js
中按需导入组件jsimport { KeepAliveProvider } from "keepalive-react-component";
用组件包裹路由组件
<RouterView />
jsxfunction App() { return ( <> <KeepAliveProvider> <RouterView /> </KeepAliveProvider> </> ); }
去往路由表,按需引入
withKeepAlive
方法,把需要缓存的组件用该方法包裹起来。其需要两个参数:- 参数一:对应的组件
- 参数二:一个对象,其中有两个属性。属性
cacheId
是该组件唯一的 ID,属性scroll
表示是否要缓存其滚动位置
jsimport { lazy } from "react"; import { withKeepAlive } from "keepalive-react-component"; // ... const routes = [ { path: "/", name: "home", component: withKeepAlive(Home, {cacheId: 'Home', scroll:true}), meta: { title: "知乎日报-WebApp", }, }, // ... ]; export default routes;
底层原理如下: