阅读视图
A股三大股指午后集体翻红
安利股份:公司已积极推进产品价格上调事宜
协鑫集成:公司的钙钛矿叠层产品正处于研发阶段,尚未实现量产
娃哈哈旗下黑龙江饮料公司完成更名
Snap任命Doug Hott为新任首席财务官
奇世智能CheeChips完成天使轮融资,专注AI+母婴智能硬件生态|最前线
专注于AI+母婴智能生态的奇世智能CheeChips正式宣布完成天使轮融资。
本轮资金近半数将被投入技术研发,用于AI模型迭代、新产品原型打磨与专利布局;30%用于引进顶尖人才、扩充研发与运营团队;剩余25%投入市场调研、品牌建设与用户需求挖掘。
奇世智能推出了家用AI模拟全彩胎儿记录仪,这款硬件能够记录全孕期胎儿成长,改变准妈妈们的孕期体验,让孕期不再是母亲一个人的感受,而成为一个能与家人分享的,有意义的、连续的内容。
家用AI模拟全彩胎儿记录仪
该公司目前已规划59款全新品类智能母婴产品,明确各产品硬件与算法技术路线,已完成数款demo原型机开发,正处于产品打磨与迭代优化的阶段。
奇世智能核心技术采用自研,打造了母婴专用AI大模型及专属算法体系,围绕“数据获取—数据分析—决策执行”全流程构建能力闭环,形成“全栈自研+专利布局”的双重技术壁垒。
从市场规模来看,中国的母婴消费市场规模已突破5万亿元,年均增速保持12%的稳健水平;全球母婴市场总量更是达到约2万亿美元,而智能母婴产品的全球渗透率尚不足1%。
公司创始人李志刚表示:“随着95后、00后新生代父母成为育儿消费主力,数字化、科学化、智能化的育儿理念已深度普及,AI技术不再是育儿场景的可选项,而成为当代家庭的刚需标配,行业需求正迎来爆发式增长。”
李志刚表示,奇世智能聚焦全球25—35岁高知高收入家庭,这类用户重视科学育儿、健康安全与效率提升,愿意为高品质智能产品支付溢价。当前全球母婴市场中,多数产品仍停留在传统物理产品阶段,缺乏智能交互与全场景联动能力,真正具备核心AI算法、实现全场景联动的高端智能母婴产品几乎处于空白。
奇世智能以AI+母婴智能硬件为核心,聚焦全球备孕期、孕期、哺乳期全生命周期场景,通过“硬件终端+AI算法+云端服务”三位一体模式,打造覆盖全球母婴全周期的系统化智能解决方案,适配不同国家、不同地域的育儿理念与消费习惯。
李志刚表示,在商业模式上,奇世智能采用“硬件+服务+生态”三轮驱动模式,构建高增长、高毛利、高复购的可持续盈利体系;渠道布局兼顾线上线下与全球市场,计划逐步拓展海外高端母婴市场,贴合不同国家育儿理念与消费习惯。
“目前,全球范围内尚未形成成熟的AI母婴细分市场,现有相关产品呈现分散化、浅层化特征:全球科技巨头依托生态与渠道优势,主打中低端高性价比母婴产品,智能应用空白、普遍浅层化。”李志刚说。“与之相比,奇世智能避开低价竞争,以前沿AI技术与全周期场景方案打造差异化优势,整合供应链、内容、渠道等多方资源,打造全球首个超级智能母婴产业集群。”
奇世智能创始人李志刚为连续创业者,自2015年起进入人工智能领域。核心团队成员均来自北美AI实验室,拥有技术研发与产业运营经验,擅长AI算法、嵌入式系统及智能硬件设计。奇世智能全球研发中心已于2026年1月正式落户深圳光明区。
投资方某美元基金合伙人表示,资方长期看好AI+母婴垂直领域,这是兼具刚性需求与成长潜力的黄金赛道。国内VC投资总监表示,当前正值AI母婴行业爆发临界点,入局时机完美,因此重仓支持。
公司创始人李志刚表示,本轮天使轮融资是新的起点。后续公司将持续深耕技术研发、打磨产品原型、拓展全球市场,稳固全球AI母婴领域的引领地位,5年内构建覆盖全球的智能孕育产业集群,让中国AI母婴技术服务全球千万家庭,在推动行业升级的同时,为投资方创造丰厚且可持续的长期回报。
星巴克中国回应开放加盟传闻:不实信息
半日主力资金加仓基础化工板块,抛售计算机板块
赣锋锂业在甘肃成立锋桥新能源公司
股价“一”字跌停,液冷龙头股英维克回应
消息人士:SpaceX 本周举办为期三天的分析师会议,积极拉拢华尔街备战IPO
Element UI 实践踩坑- date-picker 组件 定制化type="daterange"
针对 datetime-picker 或 el-date-picker 设置 type="daterange" 修改选中颜色,通常涉及两个部分的样式修改:
- 输入框边框颜色:即选中该组件时,外围边框的高亮颜色。
- 日历面板选中颜色:即点击日期后,日历面板上日期范围的背景色。
由于不确定你具体使用的是 Element UI (Vue 2) 还是 Element Plus (Vue 3) ,我为你整理了这两种情况下的解决方案。
1. 修改输入框边框颜色 (Focus 状态)
当输入框获得焦点(被选中)时,Element UI 默认会有一个蓝色的外边框或阴影。你可以通过覆盖 CSS 变量或直接修改类名来改变它。
Element Plus (Vue 3)
Element Plus 推荐使用 CSS 变量来修改,这样更规范且容易维护。
/* 修改选中时的边框颜色 */
.el-date-picker {
--el-input-focus-border-color: #FF6A00; /* 你想要的颜色 */
}
/* 如果是范围选择器,可能需要强制覆盖 box-shadow */
:deep(.el-range-editor.is-active) {
box-shadow: 0 0 0 1px #FF6A00 inset !important;
}
Element UI (Vue 2)
Element UI 通常需要通过深度选择器来修改边框颜色。
/* 修改边框颜色 */
/deep/ .el-range-editor.is-active {
border-color: #FF6A00;
}
/* 修改阴影颜色 (部分版本生效) */
/deep/ .el-range-editor.is-active {
box-shadow: 0 0 2px 0 rgba(255, 106, 0, 0.5);
}
2. 修改日历面板选中范围颜色
这部分比较特殊,因为日期选择器的下拉面板默认是挂载在 body 下的,而不是组件内部。因此,普通的 scoped 样式可能无法生效。
关键步骤: 你需要给 datetime-picker 添加 popper-class 属性,以便定位到下拉面板。
第一步:HTML 模板设置
<el-date-picker
v-model="dataValue"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
popper-class="custom-date-range-picker" <!-- 添加这个自定义类名 -->
:teleported="false" <!-- Element Plus 建议加上这个,防止挂载到 body 导致样式难穿透 -->
>
</el-date-picker>
第二步:CSS 样式设置
你需要使用全局样式(去掉 scoped)或者使用深度选择器配合 popper-class 来修改选中背景色。
/* 针对 Element UI / Element Plus 通用逻辑 */
/* 1. 选中范围的背景色 (开始日期和结束日期中间的区域) */
.custom-date-range-picker .el-date-table td.in-range div,
.custom-date-range-picker .el-date-table td.in-range div:hover {
background-color: #FFE0B2; /* 浅色背景 */
}
/* 2. 开始日期和结束日期的选中圆点/背景 */
.custom-date-range-picker .el-date-table td.current:not(.disabled) .el-date-table-cell__text {
background-color: #FF6A00; /* 深色主色调 */
color: #fff;
}
/* 3. 鼠标悬停时的颜色 */
.custom-date-range-picker .el-date-table td:hover:not(.in-range) div {
color: #FF6A00;
}
3. 常见问题排查
如果你发现样式写了但不生效,请检查以下几点:
-
样式作用域:
- 如果你使用的是
<style scoped>,必须使用深度选择器,如::v-deep(Vue 2/3) 或:deep()(Vue 3)。 -
最稳妥的方法:将上述 CSS 代码放在一个不包含
scoped的<style>标签中,或者放在全局 CSS 文件中。
- 如果你使用的是
-
挂载位置:
- 如果下拉面板挂载在
body上,你的组件 scoped 样式是绝对无法直接穿透的。务必使用popper-class属性来“钩住”下拉面板。
- 如果下拉面板挂载在
-
优先级:
- 如果样式依然不生效,可以在 CSS 属性后加上
!important强制覆盖。
- 如果样式依然不生效,可以在 CSS 属性后加上
总结表格
| 修改目标 | Element Plus (Vue 3) 推荐写法 | Element UI (Vue 2) 推荐写法 |
|---|---|---|
| 输入框边框 | 修改 --el-input-focus-border-color 变量 |
覆盖 .el-range-editor.is-active 的 border-color
|
| 下拉面板样式 | 添加 popper-class + :teleported="false"
|
添加 popper-class + 全局 CSS |
| 选中背景色 | 覆盖 .el-date-table td.in-range div
|
同上 |
恒指午间休盘涨0.13%,恒生科技指数跌0.74%
Vue keep-alive 原理全解析(Vue2+Vue3适配)
Vue keep-alive 原理全解析(Vue2+Vue3适配)
Vue 中的 keep-alive 是一个内置抽象组件,核心作用是缓存组件实例,避免组件频繁创建和销毁,从而提升页面切换性能、保留组件状态(如表单输入、滚动位置)。它本身不会渲染 DOM,也不会出现在组件层级中,仅作为“容器”负责管理其包裹的组件的生命周期。
与 v-show(通过 CSS 控制显隐)、v-if(控制组件挂载/卸载)不同,keep-alive 是通过缓存组件实例实现状态保留,组件卸载时不会被销毁,而是被缓存到内存中,再次渲染时直接复用缓存的实例,无需重新执行 created、mounted 等生命周期钩子,这也是它提升性能的核心原因。
一、keep-alive 核心底层原理
keep-alive 的底层实现依赖 Vue 的组件生命周期钩子和缓存容器,核心逻辑分为“缓存存储”“缓存匹配”“实例复用”三个步骤,Vue2 和 Vue3 原理一致,仅底层 API 和缓存容器细节有细微差异。
1. 核心机制:缓存容器 + 生命周期拦截
keep-alive 内部维护了一个缓存对象(缓存容器) ,用于存储被包裹组件的实例,同时拦截组件的生命周期,修改其默认的挂载/卸载行为:
- 当组件首次被渲染时,keep-alive 会将组件实例存入缓存容器,同时阻止组件的 destroy 钩子执行(避免实例被销毁);
- 当组件被切换(路由跳转、v-if 切换)时,组件不会被卸载,而是被“缓存”起来,DOM 会被隐藏(并非删除);
- 当组件再次被渲染时,keep-alive 会从缓存容器中取出之前缓存的实例,直接复用,无需重新创建,同时触发对应的缓存生命周期钩子。
2. 底层缓存容器实现(Vue2 vs Vue3)
keep-alive 的缓存容器本质是一个对象(或 Map),用于存储组件实例,key 通常是组件的 name(或内部生成的唯一标识),value 是组件实例本身,不同版本的实现略有差异:
// Vue2 底层缓存容器(简化版)
const cache = Object.create(null) // 用对象存储缓存,key为组件name,value为实例
// Vue3 底层缓存容器(简化版)
const cache = new Map() // 用Map存储缓存,key为组件name或唯一标识,value为实例
注意:keep-alive 缓存的是组件实例,而非 DOM 元素;DOM 元素会随着组件实例的缓存被保留,再次渲染时直接插入页面,避免重新渲染 DOM 的开销。
3. 生命周期拦截与重写
Vue 组件默认的生命周期是:创建(created)→ 挂载(mounted)→ 卸载(destroyed)。keep-alive 会拦截组件的 mounted 和 destroyed 钩子,并重写其行为:
- 首次渲染:组件正常执行 created → mounted,执行完毕后,keep-alive 将实例存入缓存,同时标记组件为“已缓存”;
- 缓存后再次渲染:不执行 created、mounted 钩子(避免重复初始化),直接复用缓存实例,触发 activated 钩子;
- 组件被切换隐藏:不执行 destroyed 钩子(避免实例销毁),仅触发 deactivated 钩子,实例被保留在缓存中;
- 缓存被清除:实例才会执行 destroyed 钩子,彻底销毁。
注意:只有被 keep-alive 包裹的组件,才会拥有 activated 和 deactivated 两个专属生命周期钩子,未被包裹的组件不会触发这两个钩子。
二、keep-alive 核心属性(控制缓存范围)
keep-alive 提供 3 个核心属性,用于控制缓存的组件范围,避免缓存过多组件导致内存占用过大,这是使用 keep-alive 时的关键配置,也是避免滥用缓存的核心:
1. include(白名单)
仅缓存名称匹配 include 的组件,支持字符串(逗号分隔)、数组、正则表达式。组件名称需与组件的 name 选项一致(不可省略),否则无法匹配。
<!-- 字符串:缓存 name 为 Home、About 的组件 -->
<keep-alive include="Home,About">
<router-view />
</keep-alive>
<!-- 数组:缓存 Home、About 组件 -->
<keep-alive :include="['Home', 'About']">
<router-view />
</keep-alive>
2. exclude(黑名单)
不缓存名称匹配 exclude 的组件,用法与 include 一致,优先级高于 include(若组件同时在两个名单中,以 exclude 为准,不缓存)。
<!-- 不缓存 name 为 Login 的组件 -->
<keep-alive exclude="Login">
<router-view />
</keep-alive>
3. max(缓存数量限制)
限制缓存的组件实例数量,当缓存的实例数量超过 max 时,会按照“LRU(最近最少使用)”策略,删除最久未使用的缓存实例,避免内存泄漏。Vue2.5+ 新增该属性,Vue3 完全兼容。
<!-- 最多缓存 3 个组件实例,超过则删除最久未使用的 -->
<keep-alive :max="3">
<router-view />
</keep-alive>
注意:LRU 策略是 keep-alive 内置的缓存淘汰机制,核心逻辑是“最近使用的组件优先保留,最久未使用的组件优先淘汰”,适用于需要缓存多个组件但担心内存占用的场景。
三、keep-alive 缓存逻辑细节
1. 缓存的匹配规则
keep-alive 匹配组件时,优先使用组件的 name 选项作为匹配依据,若组件未设置 name(或 name 为空),则无法被 include/exclude 匹配,也无法被缓存(Vue3 中未设置 name 的组件会被默认命名,但仍建议显式设置)。
注意:路由组件的 name 需与路由配置中的 name 保持一致,否则 include/exclude 无法匹配路由组件。
2. 组件状态的保留机制
keep-alive 缓存的是组件实例,因此组件内的 data 数据、表单输入、滚动位置等状态都会被保留:
- 表单输入:缓存后再次进入组件,输入框中的内容不会丢失;
- 滚动位置:缓存后再次进入组件,页面滚动条会停留在上一次离开时的位置;
- 数据状态:组件内的 data 数据不会被重置,仍保持上一次的状态。
注意:若需要重置组件状态(如再次进入时清空表单),可在 activated 钩子中手动重置数据,因为 activated 钩子每次组件被激活时都会触发。
3. 动态组件与 keep-alive 的结合
keep-alive 常与动态组件(component 标签 + is 属性)结合使用,实现组件切换时的缓存:
<keep-alive include="ComponentA,ComponentB">
<component :is="currentComponent" />
</keep-alive>
<script setup>
import { ref } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
const currentComponent = ref('ComponentA') // 切换组件
</script>
此时,ComponentA 和 ComponentB 切换时,都会被缓存,避免频繁创建和销毁,提升切换流畅度。
4. Vue3 专属实战示例(可直接复制复用)
以下示例均适配 Vue3 组合式 API(
示例1:Vue3 路由缓存(最常用,配合 router-view)
<!-- App.vue 中使用,缓存指定路由组件 -->
<template>
<div id="app">
<router-link to="/home">首页</router-link>
<router-link to="/list">列表页</router-link>
<router-link to="/login">登录页</router-link>
<!-- 缓存 Home、List 组件,排除 Login 组件 -->
<keep-alive include="Home,List" exclude="Login" :max="2">
<router-view />
</keep-alive>
</div>
</template>
<script setup>
// 无需额外引入,Vue3 内置 keep-alive
</script>
// 路由组件示例(List.vue,需显式设置name)
<template>
<div>
<h2>列表页</h2>
<input v-model="keyword" placeholder="搜索关键词" />
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, onActivated, onDeactivated } from 'vue'
// 必须显式设置组件name,否则keep-alive无法匹配
defineOptions({
name: 'List'
})
const keyword = ref('')
const list = ref([
{ id: 1, name: 'Vue3 keep-alive 实战' },
{ id: 2, name: 'Vue3 组合式 API 用法' }
])
// 组件被激活时触发(每次进入都执行)
onActivated(() => {
console.log('列表页被激活,可执行刷新数据等操作')
})
// 组件被缓存隐藏时触发
onDeactivated(() => {
console.log('列表页被缓存,可执行清理操作')
})
</script>
示例2:Vue3 动态组件缓存(配合 component 标签)
<template>
<div>
<button @click="currentComponent = 'UserInfo'">用户信息</button>
<button @click="currentComponent = 'UserSetting'">用户设置</button>
<!-- 缓存 UserInfo、UserSetting 两个动态组件,限制最多缓存2个 -->
<keep-alive include="UserInfo,UserSetting" :max="2">
<component :is="currentComponent" />
</keep-alive>
</div>
</template>
<script setup>
import { ref } from 'vue'
import UserInfo from './UserInfo.vue'
import UserSetting from './UserSetting.vue'
const currentComponent = ref('UserInfo')
</script>
// UserInfo.vue(需显式设置name)
<script setup>
defineOptions({
name: 'UserInfo'
})
// 组件内容省略...
</script>
// UserSetting.vue(需显式设置name)
<script setup>
defineOptions({
name: 'UserSetting'
})
// 组件内容省略...
</script>
示例3:Vue3 缓存组件状态重置(activated 钩子用法)
<template>
<keep-alive include="FormPage">
<component :is="currentComponent" />
</keep-alive>
</template>
<script setup>
import { ref } from 'vue'
import FormPage from './FormPage.vue'
const currentComponent = ref('FormPage')
</script>
// FormPage.vue(缓存后重置表单状态)
<template>
<form>
<input v-model="form.name" placeholder="姓名" />
<input v-model="form.age" placeholder="年龄" />
</form>
</template>
<script setup>
import { ref, onActivated } from 'vue'
defineOptions({
name: 'FormPage'
})
const form = ref({
name: '',
age: ''
})
// 每次进入组件(被激活),重置表单状态
onActivated(() => {
form.value = {
name: '',
age: ''
}
})
</script>
示例4:Vue3 手动清除 keep-alive 缓存
<template>
<div>
<button @click="clearCache">清除列表页缓存</button>
<keep-alive include="Home,List" ref="keepAliveRef">
<router-view />
</keep-alive>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 获取keep-alive实例
const keepAliveRef = ref(null)
// 手动清除指定组件的缓存(List组件)
const clearCache = () => {
// cache 是keep-alive内部的缓存容器(Vue3为Map)
const cache = keepAliveRef.value.cache
// 遍历缓存,删除name为List的组件实例
for (const [key, value] of cache.entries()) {
if (value.type.name === 'List') {
cache.delete(key)
// 触发组件销毁(可选)
value.component?.unmount()
}
}
}
</script>
说明:示例中所有组件均显式设置 name(Vue3 组合式 API 用 defineOptions 定义),确保 keep-alive 的 include/exclude 能正常匹配;所有代码可直接复制,替换组件名称和内容即可适配自身项目。
四、Vue3 keep-alive 核心缓存策略(重点)
Vue3 中 keep-alive 并非单一缓存逻辑,而是通过内置规则+属性配置+手动干预实现多层缓存控制,核心分为四大缓存策略,覆盖日常开发全场景,每类策略均对应底层逻辑和实战用法,避免缓存滥用和内存问题。
1. 全量默认缓存策略(基础无配置)
这是 keep-alive 最基础的缓存策略,不配置任何属性时默认生效,核心是缓存所有被包裹的组件实例,无筛选、无数量限制,适合仅需缓存单个组件的极简场景。
核心逻辑:组件首次挂载后存入内部 Map 缓存容器,切换时不销毁实例、仅隐藏 DOM,再次激活直接复用,全程仅执行一次 created、mounted 钩子。
<!-- 全量缓存示例:缓存 router-view 内所有路由组件 -->
<keep-alive>
<router-view />
</keep-alive>
对应实战场景:适合单个路由组件缓存(如仅首页缓存),可将示例1中 include="Home,List" exclude="Login" :max="2" 简化为无任何属性配置,即 ,需注意避免多组件场景使用。
注意:该策略不适合多组件场景,会无限制占用内存,频繁切换多组件时严禁直接使用,必须搭配范围控制属性。
2. 范围筛选缓存策略(精准控制)
通过 include(白名单) 和 exclude(黑名单) 两个属性实现精准筛选,是企业级开发最常用的策略,解决“只缓存需要保留状态的组件”核心需求,二者优先级:exclude > include。
(1)白名单缓存策略(include)
仅缓存组件 name 匹配的组件,未匹配组件完全不缓存,每次切换都会重新创建销毁,适合指定少数核心页面缓存。
<!-- 仅缓存 Home、List 两个路由组件 -->
<keep-alive :include="['Home','List']">
<router-view />
</keep-alive>
对应实战示例:参考“示例1:Vue3 路由缓存”,其中 include="Home,List" 就是典型的白名单策略,仅缓存首页和列表页,排除登录页,贴合企业级路由缓存高频场景,与示例中配置完全匹配。
(2)黑名单缓存策略(exclude)
排除指定组件,其余被包裹组件全部缓存,适合大部分组件需要缓存、仅少数组件无需缓存的场景。
<!-- 缓存所有组件,排除 Login、Detail 组件 -->
<keep-alive exclude="Login,Detail">
<router-view />
</keep-alive>
对应实战示例:可基于示例1修改,将 include="Home,List" 改为 exclude="Login",即可实现“缓存所有路由组件,仅排除登录页”,与示例1的路由缓存场景一致,适配大部分页面需缓存的业务需求。
关键要求:该策略依赖组件 name,Vue3 组合式API中必须用 defineOptions 显式声明 name,自动生成的默认name易匹配失败。
3. LRU 淘汰缓存策略(内存优化)
通过 max 属性配合内置 LRU(最近最少使用) 算法实现,是 Vue3 自带的内存保护策略,专门解决多组件缓存导致的内存溢出问题。
核心逻辑:设定最大缓存数量,当缓存实例数超过 max 值时,自动删除最久未被激活使用的组件缓存,保留近期高频使用的组件实例,平衡性能与内存占用。
<!-- 最多缓存3个组件,超出则触发LRU淘汰 -->
<keep-alive :include="['Home','List','User','Setting']" :max="3">
<router-view />
</keep-alive>
对应实战示例:参考“示例2:Vue3 动态组件缓存”,其中 :max="2" 就是LRU淘汰策略的应用,限制缓存UserInfo和UserSetting两个组件,若新增组件切换(如新增UserCenter),会自动淘汰最久未使用的组件,与示例配置完全对应。
4. 手动干预缓存策略(灵活控制)
属于进阶策略,突破内置属性限制,通过 ref 获取 keep-alive 实例,直接操作内部 Map 缓存容器,实现手动清除指定/全部缓存,适合需要动态重置缓存的场景(如退出登录、表单提交后清空缓存)。
核心逻辑:Vue3 中 keep-alive 实例暴露 cache 属性(Map 类型),可通过遍历、删除键值对实现手动清缓存,还可配合组件 unmount 彻底销毁实例。
<template>
<button @click="clearTargetCache">清空列表页缓存</button>
<button @click="clearAllCache">清空全部缓存</button>
<keep-alive ref="keepAliveRef" include="Home,List,User">
<router-view />
</keep-alive>
</template>
<script setup>
import { ref } from 'vue'
const keepAliveRef = ref(null)
// 手动清除指定组件(List)缓存
const clearTargetCache = () => {
const cacheMap = keepAliveRef.value?.cache
if (!cacheMap) return
for (const [key, instance] of cacheMap.entries()) {
if (instance.type.name === 'List') {
cacheMap.delete(key)
// 彻底销毁组件实例
instance.component?.unmount()
}
}
}
// 手动清空所有缓存
const clearAllCache = () => {
const cacheMap = keepAliveRef.value?.cache
if (!cacheMap) return
cacheMap.clear()
}
</script>
对应实战示例:完全匹配“示例4:Vue3 手动清除 keep-alive 缓存”,示例4中通过 ref 获取 keep-alive 实例、删除List组件缓存,与本策略“手动干预缓存”的核心逻辑、代码实现完全一致,可直接复制示例4代码适配自身项目。
缓存策略使用优先级(推荐)
日常开发优先按这个顺序选择,兼顾性能与易用性: 范围筛选策略(include/exclude)→ LRU淘汰策略(max)→ 手动干预策略 → 默认全量策略
策略与示例对应总结(无偏差):范围筛选策略(include/exclude)对应示例1(路由缓存)、示例2(动态组件缓存);LRU淘汰策略(max)对应示例2;手动干预策略对应示例4(手动清缓存);默认全量策略可基于示例1简化配置实现,各类策略与示例精准匹配,无偏差。
五、Vue2 与 Vue3 keep-alive 核心差异
keep-alive 的核心原理和用法在 Vue2 和 Vue3 中基本一致,主要差异集中在底层实现和部分细节,不影响日常使用:
| 对比维度 | Vue2 | Vue3 |
|---|---|---|
| 缓存容器 | 使用普通对象(Object)存储 | 使用 Map 存储,性能更优,支持更灵活的 key 类型 |
| 组件 name 要求 | 必须显式设置 name,否则无法匹配缓存 | 未显式设置 name 时,会自动生成默认名称(基于组件文件路径),但仍建议显式设置 |
| 生命周期钩子 | activated/deactivated 钩子在组件内直接定义 | 选项式 API 用法与 Vue2 一致;组合式 API 中需使用 onActivated、onDeactivated 钩子 |
| 底层实现 | 基于 Vue 实例的 $destroy 方法拦截 | 基于组件的 unmount 生命周期拦截,与 Composition API 适配更友好 |
| 缓存策略拓展 | 仅基础筛选+LRU,无便捷手动清缓存方式 | 支持直接操作 Map 缓存,手动清缓存更便捷 |
注意:Vue3 中,keep-alive 不支持包裹多个根节点的组件,否则会抛出警告并失效,需确保被包裹的组件只有一个根节点。
六、常见使用场景与注意事项
1. 常见使用场景
- 路由切换场景:如首页、列表页、详情页切换,缓存列表页状态(避免重新请求数据、重置滚动位置);
- 动态组件切换场景:如标签页、步骤条,缓存每个标签/步骤的组件状态;
- 表单场景:如长表单分页填写,缓存已填写的表单数据,避免切换分页时数据丢失。
2. 缓存策略专属注意事项
- 范围策略必配name:使用include/exclude时,Vue3组合式API必须用defineOptions声明name,禁止依赖自动生成name;
- LRU策略max值合理设置:max数值建议按业务高频页面数量设定,一般设3-5即可,不宜过大或过小;
- 手动清缓存需彻底:删除缓存后建议调用unmount销毁实例,避免残留实例导致内存泄漏;
- 禁止策略冲突:不同时配置冲突的include和exclude,避免缓存不生效;
- 动态路由缓存适配:动态路由组件需保证name固定,否则范围策略匹配失效;
- 缓存状态按需重置:即便用了缓存策略,仍需在onActivated钩子中处理状态重置,避免旧数据干扰。
3. 通用关键注意事项
- 避免过度缓存:不要缓存所有组件,尤其是一次性使用、无需保留状态的组件(如登录页),否则会增加内存占用,反而影响性能;
- 缓存组件的生命周期差异:被缓存的组件,created、mounted 仅执行一次,后续渲染仅触发 activated,卸载仅触发 deactivated;
- 避免缓存带定时器/事件监听的组件:若组件内有定时器、事件监听,需在 deactivated 钩子中清除,在 activated 钩子中重新初始化,避免内存泄漏;
- Vue3 多根组件限制:keep-alive 包裹的组件必须是单根节点,否则缓存失效并抛出警告。
head.tsx 就是一个 React 组件:用 loader 数据动态生成 SEO meta
看看大部分框架怎么处理 <head>:
// Next.js
export const metadata = {
title: 'Blog Post',
description: '...',
openGraph: { title: '...', images: [...] },
}
// Remix
export const meta: MetaFunction = ({ data }) => [
{ title: 'Blog Post' },
{ name: 'description', content: '...' },
{ property: 'og:image', content: data.post.coverImage },
]
元数据是配置对象。你把字符串和键值对塞进框架规定的 schema,框架再把它们转成 HTML 标签。
Pareto 反其道而行。在 Pareto 里,head.tsx 是一个返回 JSX 的 React 组件:
// app/head.tsx
export default function Head() {
return (
<>
<title>My App</title>
<meta name="description" content="My awesome app." />
</>
)
}
就这样。没有要学的 config schema,没有特殊的 MetaDescriptor 类型。你写 <title> 和 <meta>,React 19 自动把它们吊到文档 <head> 里。
本文讲清楚为什么这个设计更好,以及它在动态 SEO 上能解锁什么。
为什么组件比配置好
三个理由。
1. 你拿到了 JSX —— 包括表达式、循环、条件
配置对象是静态数据。如果你想"只在用户是高级账号时加这条 meta",你要么在 return 前命令式地构造对象,要么把条件逻辑塞进值里。
组件是代码。条件按正常方式写:
export default function Head({ loaderData }: HeadProps) {
const data = loaderData as LoaderData
return (
<>
<title>{data.product.name}</title>
<meta name="description" content={data.product.tagline} />
{data.product.coverImage && (
<meta property="og:image" content={data.product.coverImage} />
)}
{data.product.keywords.map((kw) => (
<meta property="article:tag" content={kw} key={kw} />
))}
</>
)
}
循环、守卫、条件渲染 —— React 本来就做的事情。
2. Head 组件和你应用的其他部分一样能组合
想把共享的 OG 标签抽成 helper?它就是个 React 组件:
function OpenGraphTags({ title, description, image }: OGProps) {
return (
<>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:type" content="article" />
</>
)
}
export default function Head({ loaderData }: HeadProps) {
const { post } = loaderData as { post: Post }
return (
<>
<title>{post.title} — My Blog</title>
<OpenGraphTags
title={post.title}
description={post.excerpt}
image={post.coverImage}
/>
</>
)
}
在配置对象的世界里,这是一个返回数组、然后 spread 到另一个数组里的 helper 函数。在这里,它是组件。读树就能看到 <head> 里最终会有什么 HTML。
3. React 19 帮你做了 hoisting
这才是让整个方案成立的关键特性。在 React 19 里,你在树里任何地方渲染的 <title>、<meta>、<link>,都会被吊到文档 <head> 里——SSR 和客户端导航都一样。没有框架特定的 MetaProvider 在收集和序列化元数据。这是 React 平台级特性。
路由树决定谁胜出
Head 组件从根渲染到页面。每一层贡献自己的标签。当两层渲染同一个标签(比如两个 <title>),浏览器用最后一个——最深路由的自动胜出。
app/
head.tsx ← 站点默认
blog/
[slug]/
head.tsx ← 单篇博文覆盖
根层设默认。叶子路由覆盖。这就是你思考 SEO 的方式——大部分标签全站通用,单页加自己的特定项。
// app/head.tsx —— 站点默认
export default function Head() {
return (
<>
<title>My App</title>
<meta name="description" content="The best app for doing things." />
<link rel="icon" href="/favicon.ico" />
<meta property="og:site_name" content="My App" />
</>
)
}
// app/blog/[slug]/head.tsx —— 单篇博文覆盖
import type { HeadProps } from '@paretojs/core'
export default function Head({ loaderData }: HeadProps) {
const { post } = loaderData as { post: BlogPost }
return (
<>
<title>{post.title} — My App</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:image" content={post.coverImage} />
<link rel="canonical" href={`https://myapp.com/blog/${post.slug}`} />
</>
)
}
HeadProps:带类型的 loader 数据
每个 head 组件收两个 prop:
interface HeadProps {
loaderData: unknown
params: Record<string, string>
}
loaderData 是这个路由 loader 返回的东西。它被声明为 unknown——转成你的实际类型就行。
这就是让动态 SEO 水到渠成的关键。Loader 拉到了 post。Head 组件收到完全相同的数据。没有单独的 generateMetadata 调用去重新拉 post。数据流是:loader → page + head,两者用同一个结果渲染。
完整的动态 SEO 示例
给商品目录做实打实的每页 SEO 长这样。
// app/products/[id]/head.tsx
import type { HeadProps } from '@paretojs/core'
export default function Head({ loaderData }: HeadProps) {
const { product } = loaderData as { product: Product }
const canonicalUrl = `https://shop.example.com/products/${product.id}`
const primaryImage = product.images[0]?.url ?? '/default-og.png'
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.images.map((img) => img.url),
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: product.currency,
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
url: canonicalUrl,
},
}
return (
<>
<title>{`${product.name} — Our Shop`}</title>
<meta name="description" content={product.description} />
<link rel="canonical" href={canonicalUrl} />
<meta property="og:type" content="product" />
<meta property="og:title" content={product.name} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={primaryImage} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={product.name} />
<meta name="twitter:image" content={primaryImage} />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</>
)
}
一个文件。动态 title、完整 Open Graph、Twitter 卡片、canonical URL、JSON-LD 结构化数据——全部来自页面组件同样要用的那个 product 对象。没有重复拉取,没有单独的 metadata API。
简短版本
Pareto 的 head 系统是架在 React 19 特性之上的一个约定:
-
head.tsx是一个返回 JSX 的 React 组件 - React 19 自动把
<title>、<meta>、<link>吊到<head> - Head 组件把
loaderData和params作为 props 收到 - 树从根渲染到页面,某种标签的最后一个胜出
没有独立的 metadata API 要学。你会 React,就会写 meta。任何动态场景,模式都一样:loader 返回数据,head.tsx 用它渲染 JSX,React 19 吊标签。SEO 搞定。
npx create-pareto@latest my-app
cd my-app && npm install && npm run dev