【iNovel 前端架构深度解析:基于 Vue 3 + TypeScript + Tauri 的跨端小说写作工具】
本文介绍了基于Tauri 2框架构建的跨平台桌面写作工具iNovel的前端技术架构。该工具采用Vue 3 + TypeScript + Vite 8技术栈,结合Naive UI组件库和Tailwind CSS框架,使用Pinia进行状态管理,Tiptap实现富文本编辑功能。文章详细剖析了项目的目录结构、分层架构和核心设计模式,重点展示了基于Composition API的组合式函数(Composa
标签: 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.ts
import { ref, watch, toRef, type Ref } from "vue";
import { useEditor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
export interface UseEditorOptions {
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;
}
export function useEditorComposable(options: UseEditorOptions) {
const modelValueRef = toRef(options, "modelValue");
const projectIdRef = toRef(options, "projectId");
const editorModeRef = toRef(options, "editorMode");
// 创建 Tiptap 编辑器实例
const editor = useEditor({
content: modelValueRef.value,
extensions: [
StarterKit,
Placeholder.configure({ placeholder: "开始写作..." }),
// ... 更多扩展
],
onUpdate: ({ editor }) => {
const html = 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.ts
import { computed } from "vue";
import { useDark, useToggle } from "@vueuse/core";
import { darkTheme } from "naive-ui";
import type { GlobalTheme } from "naive-ui";
const isDark = useDark({
selector: "html",
attribute: "class",
valueDark: "dark",
valueLight: "",
});
const toggleDark = useToggle(isDark);
const theme = computed<GlobalTheme | undefined>(() =>
isDark.value ? darkTheme : undefined,
);
export function useTheme() {
return { isDark, toggleDark, theme };
}
3.2 Pinia Store 模式
项目使用 Pinia 进行全局状态管理,采用 Setup Store 语法(与 Composition API 风格一致)。
// src/stores/project.ts
import { defineStore } from "pinia";
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
export const useProjectStore = defineStore("project", () => {
// ===== 状态 =====
const recentProjects = ref<ProjectMeta[]>([]);
const currentProject = ref<ProjectMeta | null>(null);
const isLoading = ref(false);
const error = ref<string | null>(null);
// ===== 分页状态 =====
const currentPage = ref(1);
const totalPages = ref(0);
const pageSize = ref(5);
// ===== 操作 =====
async function fetchRecentProjects(page: number = 1) {
isLoading.value = true;
error.value = null;
try {
const result = await invoke<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;
}
}
async function createProject(params: CreateProjectParams) {
isLoading.value = true;
error.value = null;
try {
const project = await invoke<ProjectMeta>("create_project", { params });
recentProjects.value.unshift(project);
currentProject.value = project;
return project;
} catch (e) {
error.value = String(e);
return null;
} 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";
export const sensitiveKey = new PluginKey("sensitiveWords");
export function createSensitivePlugin() {
return new Plugin({
key: sensitiveKey,
state: {
init() {
return DecorationSet.empty;
},
apply(tr, oldSet) {
// 在文档变更时重新计算高亮
const matches = findSensitiveMatches(tr.doc);
return buildDecorations(tr.doc, matches);
},
},
props: {
decorations(state) {
return sensitiveKey.getState(state);
},
},
});
}
4.3 Markdown 自动检测与转换
// 检测内容是否为 Markdown 格式
function isMarkdownContent(content: string): boolean {
if (!content) return false;
const markdownPatterns = [
/^#+\s/m, // 标题
/^\s*[-*+]\s/m, // 无序列表
/^\s*\d+\.\s/m, // 有序列表
/```[\s\S]*?```/g, // 代码块
/\[.+?\]\(.+?\)/g, // 链接
/\*\*[^*]+\*\*/g, // 加粗
];
const matchCount = markdownPatterns.filter((pattern) =>
pattern.test(content),
).length;
return matchCount >= 2;
}
// Markdown 转 HTML
function markdownToHtml(markdown: string): string {
if (!markdown) return "";
try {
return marked.parse(markdown) as string;
} catch (error) {
console.error("Markdown parsing failed:", error);
return markdown;
}
}
五、国际化方案
5.1 多语言架构
iNovel 支持 简体中文、英文、繁体中文 三种语言,采用 vue-i18n 实现。
// src/i18n/index.ts
import { createI18n } from "vue-i18n";
import zhCN from "../locales/zh-CN";
import enUS from "../locales/en-US";
import zhTW from "../locales/zh-TW";
const i18n = 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,
});
// 前后端语言同步
export async function initializeLocale(): Promise<void> {
try {
const locale = await invoke<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 (示例结构)
export default {
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";
// 带类型的命令调用
const result = await invoke<ProjectMeta>("create_project", {
params: {
name: "我的小说",
author: "作者名",
description: "简介",
path: "/path/to/project",
},
});
// 分页查询
const projects = await invoke<PaginatedProjects>("get_recent_projects", {
page: 1,
page_size: 10,
});
6.2 错误处理模式
async function fetchData() {
isLoading.value = true;
error.value = null;
try {
const result = await invoke<DataType>("command_name", params);
// 处理成功结果
return result;
} catch (e) {
error.value = String(e);
console.error("操作失败:", e);
return null;
} finally {
isLoading.value = false;
}
}
七、路由设计
// src/router/index.ts
const router = 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 官方文档
更多推荐




所有评论(0)