Repository Wiki
LYOfficial/OneDocs

React、Tauri 与应用启动

本页说明 OneDocs 前端应用的 React 启动壳、页面装配、响应式导航与 Tauri 宿主边界。重点放在 App 组件实际承担的启动后 UI 初始化职责,以及应用中已确认存在的 Tauri API 使用方向;PDF 图片提取、分析工作流和各业务页面的内部实现属于其他专题。

Purpose and Scope

本页覆盖:

  • React 应用进入 App 后如何建立默认页面和全局 UI 状态;
  • 桌面端 TitleBar 与移动端 MobileNav 的分流;
  • useAppStore 如何参与页面子标签、主题和外观设置同步;
  • 主题、字体、背景和窗口尺寸变化的生命周期处理;
  • React UI 与 Tauri 能力之间的边界,包括已在代码中出现的 invoke、文件系统、对话框、路径和 opener 插件。

本页不展开具体 Analysis、Archive、Discover、Settings 页面业务,也不替代独立的文档处理、PDF 图片提取或分析工作流文档。源代码中未提供完整的 Rust tauri::Builder 启动配置片段,因此本文不会推断 Rust 侧命令、窗口配置或插件注册细节。

Overview

应用启动后的首个 React 页面是 App。它使用 currentPage 将应用划分为五个顶层页面,初始值为 analysis;同时根据 window.innerWidth 判断是否进入移动布局,断点为 768 像素。桌面布局显示 TitleBar,移动布局显示 MobileNav,两者都通过回调改变同一个 currentPage 状态,因此页面内容不会因导航外观不同而产生两套路由状态。

App 还负责一组跨页面的显示策略:

  1. 页面切换到移动模式时,为当前页面选择合理的子标签默认值;
  2. 子标签变化时,把值同步回 useAppStore 中对应的分析、关于或设置状态;
  3. 将 theme 解析为浅色、深色或跟随系统,并写入根元素 class 与 data-theme;
  4. 将字体、字体缩放、背景色和背景图转为 CSS 自定义属性;
  5. 在卸载时清理 resize 和系统深色模式媒体查询监听器。

Tauri 并不是 App 组件直接调用的路由层,而是被业务服务和组件按需使用:代码中可确认 src/services/api.ts 使用 @tauri-apps/api/core 的 invoke,文件导出组件使用 Tauri dialog、filesystem 和 opener 插件,文档/PDF 相关工具还使用 appDataDir、mkdir、writeFile 与 convertFileSrc。这说明启动壳保持了 UI 编排职责,桌面能力通过边界模块按功能注入。

Architecture

Loading diagram...

图中实线表示 App 源码中的直接装配关系;虚线表示通过已搜索到的服务或组件代码确认的 Tauri 能力边界,而不是 App 自身直接调用这些插件。这样的分层避免把桌面 API 细节散落到启动壳中,同时允许页面在需要导出、文件访问或后端命令时按需使用宿主能力。

Source: App.tsx

应用启动壳的实现

默认页面与响应式模式

currentPage 的类型被限制为五个字面量页面名,初始值是 analysis。这使应用首次渲染时直接进入分析页,不依赖异步路由解析。isMobile 在状态初始化函数中读取窗口宽度,随后由 resize 监听器持续更新。监听器在 effect 清理函数中移除,避免热更新或组件重新挂载造成重复订阅。

tsx
1const [currentPage, setCurrentPage] = useState<Page>("analysis"); 2const [isMobile, setIsMobile] = useState( 3 () => window.innerWidth < MOBILE_BREAKPOINT, 4); 5 6useEffect(() => { 7 const handleResize = () => { 8 setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 9 }; 10 window.addEventListener("resize", handleResize); 11 return () => window.removeEventListener("resize", handleResize); 12}, []);

Source: App.tsx

这里的设计意图是让桌面/移动切换成为同一 React 状态机的一部分,而不是依靠 CSS 单独隐藏导航。页面组件仍接收 isMobile,因此页面本身可以针对移动布局调整参数;导航选择则保持单一状态来源。

页面装配与导航分流

渲染时,isMobile 决定使用 MobileNav 还是 TitleBar。随后 page-content 通过条件渲染挂载当前页面。五个页面都在同一个组件中明确列出,说明这里承担的是轻量级页面协调器,而不是基于 URL 的路由器。

  • about 渲染 About;
  • analysis 渲染 Analysis;
  • archive 渲染 Archive,并传递移动子标签回调;
  • discover 渲染 Discover,并传递移动子标签回调;
  • settings 渲染 Settings;
  • Toast 和 OnboardingTour 始终位于应用壳内,跨页面存在。

移动子标签与全局 store 同步

页面切换后,第一个 effect 仅在 isMobile 为真时为子标签设定默认值。分析页从 selectedFunction 读取当前选择,否则使用 news;关于页和设置页分别回退到 version 与 general;归档和发现页使用 list。这种处理把“当前页面”和“当前页面内部选项”分开,避免页面切换后保留不适用的子标签。

当用户主动切换子标签时,handleMobileSubTabChange 先更新本地 mobileSubTab,再根据当前页面把值写回 store。当前源码对三个 store setter 使用了类型断言 as any,因此扩展新的子标签时,应同时检查 Page 分支、store 的联合类型和对应页面 props,而不能只修改导航显示文本。

tsx
1const handleMobileSubTabChange = (subTab: string) => { 2 setMobileSubTab(subTab); 3 // Sync with store where applicable 4 switch (currentPage) { 5 case "analysis": 6 setSelectedFunction(subTab as any); 7 break; 8 case "about": 9 setAboutActiveSection(subTab as any); 10 break; 11 case "settings": 12 setSettingsActiveSection(subTab as any); 13 break; 14 } 15};

Source: App.tsx

Core Flow

Loading diagram...

实际控制流是同步首屏初始化加上多个独立 effect:页面/子标签 effect 处理移动状态,尺寸 effect 管理窗口监听,主题 effect 管理系统颜色偏好,外观 effect 管理 CSS 变量。它们没有共享异步锁,也没有在源码中显示持久化写入;持久化行为若存在,应在 useAppStore 或设置页面专题中确认。

主题与外观初始化

主题 effect 把 theme === "system" 解析为媒体查询结果,否则直接使用 store 中的主题值。深色模式同时写入 dark class 和 data-theme="dark";浅色模式移除 class 并写入 data-theme="light"。当系统媒体查询发生变化时,只有 theme 仍为 system 才重新执行解析。effect 返回清理函数,移除媒体查询监听器。

外观 effect 把四个 store 值写到根元素:--app-font-family、--app-font-scale、--app-shell-background 和 --app-shell-image。当没有字体或背景色时分别使用 'Noto Serif SC', serif、1 和 var(--background-color);没有背景图时写入 none。背景图值会被包装为 CSS url(...),因此传入值必须能作为 CSS URL 使用。

Tauri 集成边界

已确认的代码引用表明,Tauri 能力分布在业务边界模块,而不是集中写入 App:

  • src/services/api.ts 从 @tauri-apps/api/core 导入 invoke,用于调用宿主侧命令;本次受限源码读取未取得其完整方法签名,因此不对命令名、参数或返回值作推断;
  • src/components/ArchiveResultDisplay.tsx 使用 dialog 的 save、filesystem 的 writeTextFile 和 opener 的 openUrl,并在 Tauri 导出失败时记录错误并回退到浏览器下载;
  • src/services/pdfImageExtractor.ts 使用 invoke、appDataDir、mkdir 和动态导入的 writeFile,说明 PDF 图片落盘需要宿主文件系统能力;
  • src/utils/documentProcessor.ts 使用 appDataDir、mkdir 和 writeFile;
  • src/utils/markdownRenderer.ts 使用 convertFileSrc 将本地资源转换为 Tauri 可渲染的资源 URL。

这些结论来自搜索到的实际 import 和错误处理位置。由于本页的源码读取预算已用尽,Tauri Rust 侧命令注册、权限 capability 和插件初始化细节属于实现细节未在本页源材料中完整找到,不能据此写出具体配置表。

Usage Examples

应用壳中的页面装配

tsx
1return ( 2 <div className={`app-container ${isMobile ? "mobile" : "desktop"}`}> 3 {isMobile ? ( 4 <MobileNav 5 activeTab={currentPage} 6 onTabChange={setCurrentPage} 7 activeSubTab={mobileSubTab} 8 onSubTabChange={handleMobileSubTabChange} 9 /> 10 ) : ( 11 <TitleBar activeTab={currentPage} onTabChange={setCurrentPage} /> 12 )} 13 <div className="page-content"> 14 {currentPage === "about" && <About isMobile={isMobile} mobileSubTab={isMobile ? mobileSubTab : undefined} />} 15 {currentPage === "analysis" && <Analysis isMobile={isMobile} />} 16 {currentPage === "archive" && <Archive isMobile={isMobile} mobileSubTab={isMobile ? mobileSubTab : undefined} onMobileSubTabChange={isMobile ? handleMobileSubTabChange : undefined} />} 17 {currentPage === "discover" && <Discover isMobile={isMobile} mobileSubTab={isMobile ? mobileSubTab : undefined} onMobileSubTabChange={isMobile ? handleMobileSubTabChange : undefined} />} 18 {currentPage === "settings" && <Settings isMobile={isMobile} />} 19 </div> 20 <Toast /> 21 <OnboardingTour 22 currentPage={currentPage} 23 onPageChange={(page) => setCurrentPage(page as Page)} 24 /> 25 </div> 26);

Source: App.tsx

主题和系统偏好监听

tsx
1const effectiveTheme = 2 theme === "system" 3 ? window.matchMedia("(prefers-color-scheme: dark)").matches 4 ? "dark" 5 : "light" 6 : theme; 7 8if (effectiveTheme === "dark") { 9 root.classList.add("dark"); 10 root.setAttribute("data-theme", "dark"); 11} else { 12 root.classList.remove("dark"); 13 root.setAttribute("data-theme", "light"); 14}

Source: App.tsx

Configuration Options

选项类型默认值/回退值说明
MOBILE_BREAKPOINTnumber768window.innerWidth 小于该值时启用移动导航。
currentPagePage"analysis"应用首次渲染的顶层页面。可选值为 analysis、archive、about、discover、settings。
themestore 状态system 分支按系统媒体查询解析控制根元素的 dark class 和 data-theme。具体 store 默认值未在本次读取的源材料中展开。
uiFontFamilystring'Noto Serif SC', serif写入 --app-font-family。
uiFontScalenumber1写入 --app-font-scale。
uiBackgroundColorstringvar(--background-color)写入 --app-shell-background。
uiBackgroundImagestringnone有值时包装成 CSS url(...),否则为 none。

API Reference

App(): JSX.Element

App 是默认导出的 React 函数组件。它不接收 props,内部维护顶层页面、移动模式和移动子标签,并从 useAppStore 读取和更新跨页面 UI 状态。源码没有显式抛出异常或声明异步返回值。

handleMobileSubTabChange(subTab: string): void

组件内部回调。它更新本地移动子标签,并按当前页面调用对应 store setter:分析页调用 setSelectedFunction,关于页调用 setAboutActiveSection,设置页调用 setSettingsActiveSection。归档和发现页不在该 switch 中更新 store。

useEffect 生命周期行为

  • 页面切换 effect:在移动模式下重设移动子标签;
  • resize effect:注册并清理 window.resize;
  • 主题 effect:应用主题并注册/清理 prefers-color-scheme 变化监听;
  • 外观 effect:把 store 外观值写入根元素 CSS 变量。

Failure Modes、边界与并发

  • 窗口环境依赖:初始化直接读取 window.innerWidth,因此该组件不是无条件可在没有 DOM 的环境中执行的纯服务端组件。仓库中是否存在 SSR 启动路径,本次材料未确认。
  • 监听器清理:resize 和媒体查询监听器均有清理函数;因此组件卸载后不会继续更新已卸载实例。
  • 状态竞态:源码没有异步请求或显式并发控制。尺寸、主题和导航事件都通过 React 状态更新;快速切换时最终 UI 由最近一次状态更新决定。
  • 子标签类型安全:as any 会绕过编译期联合类型检查。扩展页面或子标签时,这是最需要额外验证的边界。
  • Tauri 导出失败:搜索结果明确显示归档导出会记录 Tauri export failed 并回退到浏览器下载;具体回退实现和错误类型应在归档页面文档中继续追踪。
  • 本地资源 URL:Markdown 渲染器明确把本地图片路径转换为 Tauri asset URL;不要在分析工作流中提前替换为 Tauri 协议,相关源码注释要求由渲染阶段负责。

Performance、运维与扩展点

App 的 effect 依赖数组限制了主题和外观更新范围;窗口 resize 事件没有 debounce,但处理函数只进行一次布尔状态比较式更新,当前实现未显示额外计算。页面使用条件渲染,因此非当前页面不会在 page-content 中同时挂载。

扩展顶层页面时,应同步修改 Page 联合类型、移动子标签初始化 switch、移动子标签同步 switch、导航组件可接受的 tab 类型和 page-content 条件渲染。扩展 Tauri 能力时,建议沿用现有边界:把 invoke 或文件系统调用放到服务/功能组件中,不把宿主 API 直接塞入 App 的启动协调逻辑。

Sources

(1 files)