【iNovel 前端架构深度解析:基于 Vue 3 + TypeScript + Tauri 的跨端小说写作工具】
2026/5/16 17:19:21 网站建设 项目流程

标签: Vue 3, TypeScript, Tauri, Pinia, Naive UI, Tailwind CSS, Tiptap, 前端架构
分类: 前端开发
难度: 进阶


一、引言

iNovel 是一款基于 Tauri 2 框架构建的跨平台桌面小说写作工具,其前端采用Vue 3 + TypeScript + Vite 8技术栈,结合Naive UI组件库和Tailwind CSS原子化 CSS 框架,构建了一套完整的写作工作流。本文将深入剖析 iNovel 前端的技术架构、核心设计模式、状态管理方案以及编辑器实现细节,为读者提供一套可参考的桌面应用前端开发范式。

技术栈一览

技术版本用途
Vue 33.5.x前端框架,Composition API
TypeScript6.0.x类型安全
Vite8.x构建工具
Pinia3.x状态管理
Naive UI2.44.xUI 组件库
Tailwind CSS4.x原子化 CSS
Tiptap3.23.x富文本编辑器
Vue Router4.x路由管理
vue-i18n11.x国际化
@vueuse/core13.x组合式工具函数

二、项目架构总览

2.1 目录结构

src/ ├── assets/ # 静态资源(图片、字体等) ├── components/ # 可复用 Vue 组件 │ ├── MarkdownEditor.vue # 核心编辑器组件 │ ├── SmartSymbolsExtension.ts # 智能符号扩展 │ ├── MentionExtension.ts # @提及扩展 │ ├── SensitiveHighlightPlugin.ts # 敏感词高亮插件 │ ├── TemplateSelector.vue # 模板选择器 │ └── ... ├── composables/ # 组合式函数(逻辑复用) │ ├── useEditor.ts # 编辑器核心逻辑 │ ├── useTheme.ts # 主题切换 │ ├── useWordCount.ts # 字数统计 │ ├── useTextBeautify.ts # 文本美化 │ ├── useEditorLayout.ts # 编辑器布局 │ ├── useGlobalShortcuts.ts # 全局快捷键 │ └── ... ├── stores/ # Pinia 状态管理 │ ├── editor.ts # 编辑器状态 │ ├── project.ts # 项目状态 │ └── ... ├── i18n/ # 国际化配置 │ ├── index.ts # i18n 初始化 │ └── composables/ # i18n 组合式函数 ├── locales/ # 语言文件 │ ├── zh-CN.ts # 简体中文 │ ├── en-US.ts # 英文 │ └── zh-TW.ts # 繁体中文 ├── router/ # 路由配置 ├── config/ # 应用配置 ├── services/ # 前端服务层 ├── views/ # 页面级组件 │ ├── WelcomePage.vue # 欢迎页 │ ├── EditorPage.vue # 编辑器主页 │ ├── SettingsPage.vue # 全局设置 │ ├── WorldbuildingPage.vue # 世界观构建 │ └── ... ├── App.vue # 根组件 ├── main.ts # 应用入口 └── style.css # 全局样式

2.2 架构分层

┌──────────────────────────────────────────────────────┐ │ Views(页面层) │ │ WelcomePage / EditorPage / SettingsPage / ... │ ├──────────────────────────────────────────────────────┤ │ Components(组件层) │ │ MarkdownEditor / TemplateSelector / ... │ ├──────────────────────────────────────────────────────┤ │ Composables(逻辑层) │ │ useEditor / useTheme / useWordCount / ... │ ├──────────────────────────────────────────────────────┤ │ Stores(状态层) │ │ useProjectStore / useEditorStore / ... │ ├──────────────────────────────────────────────────────┤ │ Services(服务层) │ │ invoke() → Tauri Commands │ └──────────────────────────────────────────────────────┘

三、核心设计模式

3.1 Composition API + Composables 模式

iNovel 全面采用 Vue 3 的Composition API,通过Composables(组合式函数)实现逻辑复用。这是项目中最核心的设计模式。

3.1.1 编辑器 Composables 示例
// src/composables/useEditor.tsimport{ref,watch,toRef,typeRef}from"vue";import{useEditor,EditorContent}from"@tiptap/vue-3";importStarterKitfrom"@tiptap/starter-kit";importPlaceholderfrom"@tiptap/extension-placeholder";exportinterfaceUseEditorOptions{modelValue:string|Ref<string>;projectId?:number|null|Ref<number|null|undefined>;editorMode?:EditorMode|Ref<EditorMode|undefined>;smartSymbolsEnabled?:boolean;onContentChange?:(html:string)=>void;onWordCountUpdate?:(count:number)=>void;onMentionClick?:(id:string)=>void;}exportfunctionuseEditorComposable(options:UseEditorOptions){constmodelValueRef=toRef(options,"modelValue");constprojectIdRef=toRef(options,"projectId");consteditorModeRef=toRef(options,"editorMode");// 创建 Tiptap 编辑器实例consteditor=useEditor({content:modelValueRef.value,extensions:[StarterKit,Placeholder.configure({placeholder:"开始写作..."}),// ... 更多扩展],onUpdate:({editor})=>{consthtml=editor.getHTML();options.onContentChange?.(html);},});// 暴露编辑器操作方法return{editor,EditorContent,toggleBold:()=>editor.value?.chain().focus().toggleBold().run(),toggleItalic:()=>editor.value?.chain().focus().toggleItalic().run(),// ...};}
3.1.2 主题切换 Composables
// src/composables/useTheme.tsimport{computed}from"vue";import{useDark,useToggle}from"@vueuse/core";import{darkTheme}from"naive-ui";importtype{GlobalTheme}from"naive-ui";constisDark=useDark({selector:"html",attribute:"class",valueDark:"dark",valueLight:"",});consttoggleDark=useToggle(isDark);consttheme=computed<GlobalTheme|undefined>(()=>isDark.value?darkTheme:undefined,);exportfunctionuseTheme(){return{isDark,toggleDark,theme};}

3.2 Pinia Store 模式

项目使用Pinia进行全局状态管理,采用Setup Store语法(与 Composition API 风格一致)。

// src/stores/project.tsimport{defineStore}from"pinia";import{ref}from"vue";import{invoke}from"@tauri-apps/api/core";exportconstuseProjectStore=defineStore("project",()=>{// ===== 状态 =====constrecentProjects=ref<ProjectMeta[]>([]);constcurrentProject=ref<ProjectMeta|null>(null);constisLoading=ref(false);consterror=ref<string|null>(null);// ===== 分页状态 =====constcurrentPage=ref(1);consttotalPages=ref(0);constpageSize=ref(5);// ===== 操作 =====asyncfunctionfetchRecentProjects(page:number=1){isLoading.value=true;error.value=null;try{constresult=awaitinvoke<PaginatedProjects>("get_recent_projects",{page,page_size:pageSize.value,});recentProjects.value=result.items;currentPage.value=result.page;totalPages.value=result.total_pages;}catch(e){error.value=String(e);}finally{isLoading.value=false;}}asyncfunctioncreateProject(params:CreateProjectParams){isLoading.value=true;error.value=null;try{constproject=awaitinvoke<ProjectMeta>("create_project",{params});recentProjects.value.unshift(project);currentProject.value=project;returnproject;}catch(e){error.value=String(e);returnnull;}finally{isLoading.value=false;}}return{// 状态recentProjects,currentProject,isLoading,error,currentPage,totalPages,pageSize,// 操作fetchRecentProjects,createProject,openProject,removeProjectFromList,updateProject,};});

3.3 组件设计模式

3.3.1 Props 与 Emits 类型定义
<script setup lang="ts"> import { ref, toRef } from "vue"; // 使用 TypeScript 接口定义 Props const props = defineProps<{ modelValue: string; chapterId: number | null; projectId?: number | null; volumeWordCount?: number; totalWordCount?: number; isDark?: boolean; editorMode?: EditorMode; }>(); // 使用 TypeScript 函数签名定义 Emits const emit = defineEmits<{ (e: "update:modelValue", value: string): void; (e: "requestSave"): void; (e: "exitSpecialMode"): void; (e: "mention-click", id: string): void; (e: "show-history"): void; (e: "create-snapshot"): void; (e: "word-count-updated", count: number): void; }>(); // 使用 toRef 将 props 转为响应式引用 const modelValueRef = toRef(props, "modelValue"); const projectIdRef = toRef(props, "projectId"); </script>
3.3.2 组件组合模式
<!-- App.vue - 根组件中的 Provider 嵌套 --> <template> <n-config-provider :theme="theme" :theme-overrides="themeOverrides" :locale="naiveLocale" :date-locale="naiveDateLocale" > <n-loading-bar-provider> <n-dialog-provider> <n-message-provider> <GlobalPasswordOverlay /> <RouterView /> </n-message-provider> </n-dialog-provider> </n-loading-bar-provider> </n-config-provider> </template>

四、富文本编辑器实现

4.1 Tiptap 编辑器架构

iNovel 的核心是Tiptap 3(基于 ProseMirror)富文本编辑器,支持以下功能:

功能实现方式
基础编辑StarterKit 扩展包
占位符Placeholder 扩展
智能符号自定义 SmartSymbolsExtension
@提及自定义 MentionExtension
敏感词高亮自定义 SensitiveHighlightPlugin
Markdown 导入marked 库解析
打字机模式CSS + 编辑器状态控制
专注模式段落级高亮控制

4.2 自定义 ProseMirror 插件

// 敏感词高亮插件核心逻辑import{Plugin,PluginKey}from"prosemirror-state";import{Decoration,DecorationSet}from"prosemirror-view";exportconstsensitiveKey=newPluginKey("sensitiveWords");exportfunctioncreateSensitivePlugin(){returnnewPlugin({key:sensitiveKey,state:{init(){returnDecorationSet.empty;},apply(tr,oldSet){// 在文档变更时重新计算高亮constmatches=findSensitiveMatches(tr.doc);returnbuildDecorations(tr.doc,matches);},},props:{decorations(state){returnsensitiveKey.getState(state);},},});}

4.3 Markdown 自动检测与转换

// 检测内容是否为 Markdown 格式functionisMarkdownContent(content:string):boolean{if(!content)returnfalse;constmarkdownPatterns=[/^#+\s/m,// 标题/^\s*[-*+]\s/m,// 无序列表/^\s*\d+\.\s/m,// 有序列表/```[\s\S]*?```/g,// 代码块/\[.+?\]\(.+?\)/g,// 链接/\*\*[^*]+\*\*/g,// 加粗];constmatchCount=markdownPatterns.filter((pattern)=>pattern.test(content),).length;returnmatchCount>=2;}// Markdown 转 HTMLfunctionmarkdownToHtml(markdown:string):string{if(!markdown)return"";try{returnmarked.parse(markdown)asstring;}catch(error){console.error("Markdown parsing failed:",error);returnmarkdown;}}

五、国际化方案

5.1 多语言架构

iNovel 支持简体中文、英文、繁体中文三种语言,采用vue-i18n实现。

// src/i18n/index.tsimport{createI18n}from"vue-i18n";importzhCNfrom"../locales/zh-CN";importenUSfrom"../locales/en-US";importzhTWfrom"../locales/zh-TW";consti18n=createI18n({legacy:false,locale:getStoredLocale(),fallbackLocale:"zh-CN",messages:{"zh-CN":zhCN,"en-US":enUS,"zh-TW":zhTW,},missing:import.meta.env.DEV?handleMissing:undefined,});// 前后端语言同步exportasyncfunctioninitializeLocale():Promise<void>{try{constlocale=awaitinvoke<string>("get_locale");if(isValidLocale(locale)){i18n.global.locale.value=locale;localStorage.setItem("inovel_locale",locale);document.documentElement.setAttribute("lang",locale);}}catch(error){console.warn("Failed to load locale from backend:",error);}}

5.2 语言文件结构

// src/locales/zh-CN.ts (示例结构)exportdefault{common:{app:{name:"iNovel"},actions:{save:"保存",cancel:"取消",confirm:"确认",},},editor:{toolbar:{bold:"加粗",italic:"斜体",heading:"标题",},placeholder:"开始写作...",},project:{create:"创建项目",open:"打开项目",delete:"删除项目",},};

六、前后端通信

6.1 Tauri invoke 调用模式

import{invoke}from"@tauri-apps/api/core";// 带类型的命令调用constresult=awaitinvoke<ProjectMeta>("create_project",{params:{name:"我的小说",author:"作者名",description:"简介",path:"/path/to/project",},});// 分页查询constprojects=awaitinvoke<PaginatedProjects>("get_recent_projects",{page:1,page_size:10,});

6.2 错误处理模式

asyncfunctionfetchData(){isLoading.value=true;error.value=null;try{constresult=awaitinvoke<DataType>("command_name",params);// 处理成功结果returnresult;}catch(e){error.value=String(e);console.error("操作失败:",e);returnnull;}finally{isLoading.value=false;}}

七、路由设计

// src/router/index.tsconstrouter=createRouter({history:createWebHashHistory(),routes:[{path:"/",name:"Welcome",component:WelcomePage},{path:"/editor/:projectId",name:"Editor",component:EditorPage,},{path:"/editor/:projectId/worldbuilding",name:"Worldbuilding",component:WorldbuildingPage,},{path:"/settings",name:"Settings",component:SettingsPage},{path:"/stats",name:"Stats",component:StatsDashboard},{path:"/tasks",name:"TaskChecklist",component:TaskChecklistPage},{path:"/config",name:"ConfigManager",component:ConfigManagerPage},{path:"/templates",name:"UserTemplates",component:UserTemplatesPage},],});

八、最佳实践总结

8.1 代码组织原则

原则说明
单一职责每个 Composable 只负责一个功能领域
类型优先所有接口、Props、Emits 使用 TypeScript 类型定义
逻辑分离业务逻辑放在 Composables,UI 逻辑放在组件中
状态集中跨组件共享状态使用 Pinia Store

8.2 性能优化策略

策略实现
懒加载路由使用动态 import 实现路由级代码分割
编辑器优化Tiptap 基于 ProseMirror 的增量更新机制
响应式优化使用toRef而非ref包装 props,避免不必要的响应式转换
条件渲染使用v-if而非v-show处理不常切换的组件

8.3 常见问题与解决方案

问题解决方案
编辑器内容与 props 不同步使用toRef将 props 转为 Ref,通过watch监听变化
Tauri invoke 类型丢失使用泛型invoke<T>()指定返回类型
暗色模式闪烁在 HTML 标签上预设 class,通过useDark同步状态
i18n 缺失翻译开发环境启用missing回调,生产环境使用 fallback

九、结语

iNovel 的前端架构充分体现了 Vue 3 Composition API 的优势,通过Composables + Pinia Store + TypeScript的组合,构建了一套类型安全、逻辑清晰、易于维护的桌面应用前端体系。核心亮点包括:

  1. Composables 逻辑复用:将编辑器、主题、字数统计等功能封装为可复用的组合式函数
  2. Tiptap 深度定制:通过自定义 Extension 和 Plugin 实现智能符号、敏感词高亮等高级功能
  3. 前后端类型安全通信:利用 TypeScript 泛型 + Tauri invoke 实现类型安全的命令调用
  4. 完善的多语言支持:前后端语言状态同步,开发环境翻译缺失检测

这套架构模式不仅适用于小说写作工具,也可为其他基于 Tauri + Vue 3 的桌面应用开发提供参考。


相关阅读: iNovel 项目地址 | Tauri 2 官方文档 | Vue 3 官方文档

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

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

立即咨询