ice.js 实战:基于 React 18 `<Activity />` 的 KeepAlive 页面保活示例(with-keep-alive-react)
2026/9/21 1:35:18 网站建设 项目流程
  • 前端
  • Web框架
  • SSR
  • 前端构建
  • 插件系统
  • 微前端
  • 跨平台

【免费下载链接】ice

🚀 ice.js: The Progressive App Framework Based On React(基于 React 的渐进式应用框架)

项目地址:https://gitcode.com/gh_mirrors/ice1/ice
点击查看免费下载

导读

KeepAlive(页面保活)是现代前端应用中非常实用的能力:它能让用户从 A 页面切到 B 页面时,A 页面的滚动位置、表单输入、组件内部状态等不因卸载而丢失,返回时瞬间恢复。ice.js 在examples/with-keep-alive-react示例中演示了一种基于 React 18 实验性<Activity />API 的 KeepAlive 实现方案。本文以该示例为骨架,完整解析其页面结构、核心 API(KeepAliveOutletuseActive)、底层实现原理以及基于 yalc 的本地调试流程,帮助你快速在自己的 ice.js 项目中落地页面保活能力。

示例定位:Experimental keep-alive with React 18<Activity />

仓库中的examples/with-keep-alive-react是一个独立可运行的 ice.js 应用,其 README 开宗明义地给出了它的定位:

Experimental keep-alive with React 18<Activity />.

也就是说,这是一个实验性示例,用来验证 ice.js 运行时对 React 18 新引入的<Activity />能力(即页面/组件级可见性控制原语)的封装。它没有引入任何第三方 KeepAlive 库,而是直接复用了 React 实验版本内置的unstable_Activity,并把"哪些路由出口要被保活、保活多少个、保活哪些路径"这类策略问题封装成了 ice.js 运行时自带的KeepAliveOutlet组件(见 KeepAliveOutlet.tsx)。

从示例的package.json可以看到,该示例强依赖 React 实验版本:

{ "dependencies": { "react": "0.0.0-experimental-0cdfef19b-20231211", "react-dom": "0.0.0-experimental-0cdfef19b-20231211" }, "resolutions": { "react": "0.0.0-experimental-0cdfef19b-20231211", "react-dom": "0.0.0-experimental-0cdfef19b-20231211" } }

这里通过resolutions强制锁定 React 版本,确保实验性 API 行为一致。这是运行该示例前需要知晓的一个前提:KeepAlive 示例需要 experimental 版本的 React

示例目录结构与页面拓扑

examples/with-keep-alive-react/ ├── src/ │ ├── components/ │ │ └── Counter.tsx # 保活效果演示用的计数器组件 │ ├── pages/ │ │ ├── index.tsx # 首页 /(含 Counter 与跳转链接) │ │ ├── layout.tsx # 根布局:渲染 <KeepAliveOutlet /> │ │ └── about/ │ │ ├── index.tsx # 子页面 /about │ │ ├── layout.tsx # 子路由布局:渲染普通 <Outlet /> │ │ └── me.tsx # 子页面 /about/me(含 input) │ ├── app.ts # defineAppConfig 入口 │ └── document.tsx # HTML 模板 ├── ice.config.mts # defineConfig 空配置 ├── package.json └── tsconfig.json

页面拓扑关系如下:

  • 根布局src/pages/layout.tsx渲染<KeepAliveOutlet />,所有被保活的页面都在它内部;
  • 首页/index.tsx)挂载一个Counter计数器组件;
  • /aboutabout/index.tsx)同样挂载一个Counter
  • /about/meabout/me.tsx)包含一个<input />输入框;
  • /about自身还有一个布局about/layout.tsx,内部使用普通的<Outlet />

这样一个拓扑故意制造了多种典型场景:组件级状态(Counter)、表单输入(input)、嵌套路由布局(layout + Outlet),从而可以完整地验证保活效果。

核心代码逐层拆解

1. 根布局:用KeepAliveOutlet替换普通Outlet

普通 ice.js 应用的路由出口通常写成:

import { Outlet } from 'ice';

而保活示例的根布局(layout.tsx)换成了:

import { KeepAliveOutlet } from 'ice'; export default function Layout() { return ( <> <h1>I'm Keep Alive</h1> <KeepAliveOutlet /> </> ); }

这是整个保活能力接入的唯一入口改动:把<Outlet />换成<KeepAliveOutlet />,ice.js 运行时便会接管路由出口的渲染与缓存。

2. 页面组件:保持普通写法,无需任何侵入式改造

被保活的页面组件本身不需要任何额外代码。首页(index.tsx):

import { Link } from 'ice'; import Counter from '@/components/Counter'; export default function Home() { return ( <main> <h2>Home</h2> <Counter /> <Link to="/about">About</Link> </main> ); } export function pageConfig() { return { title: 'Home', }; }

/about(about/index.tsx)与/about/me(about/me.tsx)同样保持常规写法。这种"零侵入"正是 KeepAlive 方案易用性的关键:业务代码无需感知保活的存在,只需要在布局层替换一个组件。

Counter组件(Counter.tsx)用useState保存计数:

import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> count: {count} <button onClick={() => setCount((count) => count + 1)}>add</button> </div> ); }

3. 嵌套路由:子布局继续使用普通Outlet

注意/about下的子布局(about/layout.tsx)仍然使用普通Outlet

import { Outlet } from 'ice'; export default function AboutLayout() { return ( <> <h2>About Layout </h2> <Outlet /> </> ); }

这说明 KeepAlive 的粒度在示例中被设置在顶层路由出口:保活的对象是//about/about/me这些顶层路由条目对应的出口,而子路由内部继续用原生路由机制渲染。

4. 应用入口与配置

应用入口(app.ts)是一个标准的defineAppConfig

import { defineAppConfig } from 'ice'; export default defineAppConfig(() => ({}));

构建配置(ice.config.mts)为空配置:

import { defineConfig } from '@ice/app'; export default defineConfig(() => ({}));

即:KeepAlive 能力不需要任何构建期配置,纯运行时提供。

KeepAliveOutlet源码实现解读

KeepAliveOutlet由 ice.js 运行时包@ice/runtime提供,实现在 packages/runtime/src/KeepAliveOutlet.tsx,并从 packages/runtime/src/index.ts 统一对外导出(useActiveKeepAliveOutlet)。

// @ts-ignore const Activity = React.unstable_Activity || ActivityComponent;

这里有一个重要的降级策略:优先使用 React 实验版本提供的React.unstable_Activity;如果当前 React 版本不支持(即非实验版),则回退到运行时内置的ActivityComponent(见 Activity.tsx)。如果两者都不可用,组件会直接抛出明确错误:

if (!Activity) { throw new Error('`<KeepAliveOutlet />` now requires react experimental version. Please install it first.'); }

保活队列管理

组件内部用useState维护一个"出口队列",队列元素包含outletkeypathname三个字段:

interface ActivityItem { outlet: React.ReactElement | null; key: string; pathname: string; }

useEffect中,每当路由变化(location.pathname/location.key变化)时,将当前的useOutlet()结果追加进队列,并用.slice(-outletLimit)保证队列长度不超过上限,从而实现"只保留最近 N 个页面"的 LRU 式裁剪:

const OUTLET_LIMIT = 5; ... const outletLimit = props.limit || OUTLET_LIMIT;

默认保活上限为5 个出口OUTLET_LIMIT = 5),可通过limitprop 覆盖。

支持 props

KeepAliveOutlet支持两个可选 props(定义见 KeepAliveOutlet.tsx):

Prop类型默认值说明
limitnumber5保活的出口(outlet)数量上限,超出后最旧的出口会被淘汰
pathsstring[]未配置(全部保活)仅对指定路径启用保活,未匹配的路径不缓存

paths的实现逻辑是:在追加新出口之前,先对现有队列执行过滤currentOutlets.filter(o => keepAlivePaths.includes(o.pathname)),从而只保留命中列表的页面。这适用于"只想缓存首页、详情页,不缓存表单页"之类的精细化控制场景。

仓库中另一个 KeepAlive 示例 examples/with-keep-alive/src/pages/layout.tsx 就同时用到了这两个参数:

<KeepAliveOutlet limit={2} paths={['/home']} />

SSR 水合兼容

源码中对 SSR 场景做了专门处理:用一个useRef保存首个出口(首屏渲染时的outlet),在outlets为空时直接渲染outletRef.current,避免客户端水合(hydration)时额外触发setOutlets造成重复渲染:

const outletRef = useRef({ key: location.key, pathname: location.pathname, outlet, }); ... const renderOutlets = outlets.length === 0 ? [outletRef.current] : outlets;

这也是为什么示例同时保留了 document.tsx 这样的 SSR 文档模板——KeepAlive 在服务端渲染/水合链路中同样可用。

可见性切换:mode="visible" | "hidden"

队列中的每个出口都会被包一层<Activity>,并根据当前路由决定可见性:

<Activity key={o.key} mode={location.pathname === o.pathname ? 'visible' : 'hidden'}> {o.outlet} </Activity>

当前路径的出口以visible模式渲染,其余保活页面以hidden模式渲染。

ActivityuseActive:保活页面的可见性语义

当 React 实验版本不可用时,运行时内置的降级实现位于 packages/runtime/src/Activity.tsx:

export default function Activity({ mode, children }: ActivityProps) { const active = mode === 'visible'; return ( <ActivityProvider value={{ active }}> {/* Additional wrapper for hidden elements */} <div style={{ display: active ? 'block' : 'none' }}> {children} </div> </ActivityProvider> ); }

可以看到降级方案的核心是:被隐藏的页面并不卸载,而是通过display: none保持在 DOM 中,因此组件的useState等内部状态得以保留——这正是 KeepAlive 的本质。

同时,Activity通过 React Context 暴露active状态,并对外提供useActive钩子(同样从 index.ts 导出):

export const useActive = () => { const data = React.useContext(Context); return data?.active; };

业务组件可以在保活页面内部这样使用它,感知自己是否处于前台可见状态:

import { useActive } from 'ice'; function MyPage() { const active = useActive(); // active === true 表示当前页面可见,false 表示被保活隐藏 }

这在"页面被隐藏时暂停轮询/动画、回到前台时恢复"这类场景非常实用。

基于 yalc 的本地调试流程

该示例的 README 给出了一套完整的本地调试流程。由于示例依赖的是仓库内正在开发的@ice/app@ice/runtime(而非 npm 上发布的稳定版本),因此需要先用yalc把这两个包"发布"到本地仓库,再在示例中安装。

第 1 步:将核心包发布到 yalc 仓库

$ cd packages/ice && yalc publish --push $ cd packages/runtime && yalc publish --push
  • yalc publish --push会将当前目录下的包发布到本地 yalc 仓库,并推送(push)到所有已添加该包的本地项目中
  • 顺序上先packages/ice(构建工具链@ice/app),再packages/runtime(运行时@ice/runtime),与依赖方向一致。

第 2 步:进入示例目录并添加本地依赖

$ cd examples/with-keep-alive $ yalc add @ice/app @ice/runtime

yalc add会把本地仓库中的@ice/app@ice/runtime链接进示例项目的node_modules。这样示例运行时使用的是当前仓库源码构建出的最新包,改动packages/下的源码后只需重新yalc publish --push即可热同步。

说明:examples/with-keep-alive-reactexamples/with-keep-alive是仓库内两个并列的 KeepAlive 示例(前者面向 React 18<Activity />实验 API,后者展示limit/paths参数的用法),README 中的调试命令以with-keep-alive目录为例,路径按需替换即可。

第 3 步:安装依赖并启动

$ yarn install $ npm run start

yarn install负责补齐示例自身声明的依赖(包括实验版 React);随后npm run start对应 package.json 中的ice start,启动本地开发服务器。

第 4 步:验证 KeepAlive 效果

启动后打开首页,可以这样验证:

  1. 在首页点击add按钮,把Counter计数加到一个非零值;
  2. 点击About链接跳到/about,再把该页的计数器加几下;
  3. /about/me<input />中输入一些文字;
  4. 依次返回/about/,观察计数器和输入框内容是否完整保留。

如果一切正常,会发现切换页面后状态不丢失——这正是<KeepAliveOutlet />生效的表现。

常见问题与注意事项

  • 必须使用 experimental ReactKeepAliveOutlet依赖React.unstable_Activity。如果 React 版本不满足,会抛出'<KeepAliveOutlet />' now requires react experimental version. Please install it first.错误。示例通过package.jsonresolutions字段锁定了实验版本(0.0.0-experimental-0cdfef19b-20231211)。
  • 保活数量有上限:默认最多保活 5 个出口,超出后最旧的会被淘汰(slice(-outletLimit))。需要更多时通过limitprop 调整。
  • 按路径精细化控制:通过pathsprop 指定需要保活的路径白名单,未命中路径的页面不会被缓存。
  • 页面可见性感知:被保活的页面处于hidden状态时并未卸载(display: none),如有需要可用useActive()判断当前是否可见,据此暂停/恢复定时器等副作用。
  • 实验性功能:该示例在 README 中明确标注为 Experimental,其底层依赖 React 实验性<Activity />能力,API 形态在未来版本中可能演进,生产环境接入前请评估稳定性。

总结

examples/with-keep-alive-react展示了 ice.js 运行时对 React 18<Activity />保活能力的完整封装链路:业务侧只需在布局中把<Outlet />替换为<KeepAliveOutlet />,即可获得页面状态保留能力;运行时侧通过出口队列管理、limit/paths策略参数、SSR 水合兼容与useActive可见性感知,把这套能力打磨成了开箱即用的 API。配合 README 中的 yalc 调试流程,你可以在本地快速复现、修改并验证 KeepAlive 行为,为生产环境中的页面保活方案选型提供直接参考。

  • 前端
  • Web框架
  • SSR
  • 前端构建
  • 插件系统
  • 微前端
  • 跨平台

【免费下载链接】ice

🚀 ice.js: The Progressive App Framework Based On React(基于 React 的渐进式应用框架)

项目地址:https://gitcode.com/gh_mirrors/ice1/ice
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询