标签: 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 3 | 3.5.x | 前端框架,Composition API |
| TypeScript | 6.0.x | 类型安全 |
| Vite | 8.x | 构建工具 |
| Pinia | 3.x | 状态管理 |
| Naive UI | 2.44.x | UI 组件库 |
| Tailwind CSS | 4.x | 原子化 CSS |
| Tiptap | 3.23.x | 富文本编辑器 |
| Vue Router | 4.x | 路由管理 |
| vue-i18n | 11.x | 国际化 |
| @vueuse/core | 13.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的组合,构建了一套类型安全、逻辑清晰、易于维护的桌面应用前端体系。核心亮点包括:
- Composables 逻辑复用:将编辑器、主题、字数统计等功能封装为可复用的组合式函数
- Tiptap 深度定制:通过自定义 Extension 和 Plugin 实现智能符号、敏感词高亮等高级功能
- 前后端类型安全通信:利用 TypeScript 泛型 + Tauri invoke 实现类型安全的命令调用
- 完善的多语言支持:前后端语言状态同步,开发环境翻译缺失检测
这套架构模式不仅适用于小说写作工具,也可为其他基于 Tauri + Vue 3 的桌面应用开发提供参考。
相关阅读: iNovel 项目地址 | Tauri 2 官方文档 | Vue 3 官方文档