# Vue 接入

在现有组件树内挂载编辑器，保留主题、依赖注入和视图生命周期。

## 创建与挂载

`useMsfeEditor()` 返回浅引用 `editor` 和组件宿主 `host`。在模板中渲染 `VueNodeViewHost`，组件视图便能保留当前应用的 `provide/inject` 上下文。

```vue
<script setup lang="ts">
import { shallowRef } from 'vue'
import { useMsfeEditor, VueNodeViewHost } from '@quirekit/msfe-vue'
import { StarterKit } from '@quirekit/msfe-starter-kit'

const parent = shallowRef<HTMLElement | null>(null)
const { editor, host } = useMsfeEditor(parent, () => ({
  markdown: '## 写作开始',
  mode: 'live',
  extensions: [StarterKit],
}))
</script>

<template>
  <div ref="parent" />
  <VueNodeViewHost :host="host" />
</template>
```

hook 在挂载后创建实例，在卸载时清理编辑器与组件宿主。不要另外创建独立 Vue app 来渲染节点。

`options.onError` 既接收运行时错误，也接收 `createMsfeEditor()` 创建失败的原始异常。
创建失败时 `editor.value` 保持 `undefined`，组件宿主会释放；宿主可展示错误提示和原文，
修正配置后再明确重新挂载。未提供 `onError` 时，创建异常继续交给 Vue 的错误处理机制。
配置 factory 自身抛出的异常仍由 Vue 处理。hook 不自动重试，也不在运行错误后重建文档。

## 更新内容和模式

初始化 options 不会被深度监听。挂载后的变化使用 `setMode()`、`setReadOnly()`、`applyChanges()` 或 `replaceDocument()`。不要以全文 `v-model` 的方式在每次输入后回写编辑器。

### basic/editor.vue

```vue
<script setup lang="ts">
import { computed, ref, shallowRef } from "vue";
import { useMsfeEditor, VueNodeViewHost } from "@quirekit/msfe-vue";
import { StarterKit } from "@quirekit/msfe-starter-kit";
import { QuireButton, QuireSegmented, QuireProseSource } from "@quirekit/ui/vue";
import { Appearance } from "../shared/appearance";
import { exampleMessages } from "../shared/messages";
import type { ExampleLocale } from "../shared/types";

const props = withDefaults(defineProps<{ /** Language supplied by the application. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const text = computed(() => exampleMessages[props.locale]);
const initial = text.value.initial;
const parent = shallowRef<HTMLElement | null>(null);
const mode = ref("live");
const markdown = ref<string>();
const items = computed(() => [{ value: "live", label: text.value.live }, { value: "source", label: text.value.source }, { value: "locked", label: text.value.locked }]);

// One editor instance owns source, selection and history for all three modes.
const { editor, host } = useMsfeEditor(parent, () => ({ markdown: initial, mode: "live",
  extensions: [StarterKit, Appearance], label: text.value.editor }));

/** @param value - Mode selected in the shared control. */
function changeMode(value: string | null | undefined): void {
  if (value === "source" || value === "live" || value === "locked") { mode.value = value; editor.value?.setMode(value); }
}
/** Serializes only for an explicit save/read action; typing never mirrors the entire source. */
function readMarkdown(): void { markdown.value = editor.value?.getMarkdown(); }
/** Starts a fresh document and history after the user explicitly resets this example. */
function reset(): void { editor.value?.replaceDocument(initial, "example-reset"); markdown.value = undefined; }
</script>
<template>
  <div class="grid gap-4 p-qk-surface">
    <div class="flex flex-wrap items-center justify-between gap-3">
      <QuireSegmented :model-value="mode" :items="items" :label="text.modes" size="sm" @update:model-value="changeMode" />
      <div class="flex gap-2">
        <QuireButton size="sm" variant="ghost" :disabled="mode === 'locked'" @click="editor?.undo()">{{ text.undo }}</QuireButton>
        <QuireButton size="sm" variant="ghost" @click="reset">{{ text.reset }}</QuireButton>
        <QuireButton size="sm" variant="outline" @click="readMarkdown">{{ text.readMarkdown }}</QuireButton>
      </div>
    </div>
    <div ref="parent" class="overflow-hidden rounded-qk-control border border-qk-border-subtle" />
    <VueNodeViewHost :host="host" />
    <QuireProseSource v-if="markdown !== undefined" :code="markdown" language="markdown" filename="document.md" :max-height="260" />
  </div>
</template>

```

### shared/appearance.ts

```ts
import { Extension } from "@quirekit/msfe-core";
import { EditorView } from "@quirekit/cm-view";
import { Prec } from "@quirekit/cm-state";
import { HighlightStyle, syntaxHighlighting } from "@quirekit/cm-language";
import { tags } from "@lezer/highlight";
import { themeVariableReferences as token } from "@quirekit/ui";

/** Consumer-owned theme; source semantics stay in the installed feature packages. */
export const Appearance = Extension.create({ name: "documentationAppearance",
  /** @returns Scoped typography, surfaces and syntax colors from the host design system. */
  addCodeMirrorExtensions() {
    return Prec.high([
      EditorView.theme({
        "&": { ...token.typography.body, color: token.color.text.primary, backgroundColor: token.color.surface.page, minHeight: "180px" },
        ".cm-scroller": { fontFamily: "inherit", lineHeight: "1.8" },
        ".cm-content": { padding: token.spacing.surface, caretColor: token.color.text.primary },
        ".cm-line": { padding: "0" },
        ".cm-gutters": { color: token.color.text.tertiary, backgroundColor: token.color.surface.subtle, border: "none" },
        ".cm-activeLine, .cm-activeLineGutter": { backgroundColor: "transparent" },
        ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { backgroundColor: token.color.selection.document },
        ".cm-cursor": { borderLeftColor: token.color.text.primary },
        ".msfe-h1": { ...token.typography.pageTitle },
        ".msfe-h2, .msfe-h3": { ...token.typography.sectionTitle },
        ".msfe-heading": { paddingBlock: token.spacing.inline },
        ".msfe-link": { color: token.color.text.accent },
        ".msfe-code-line, .msfe-inline-code": { ...token.typography.code },
        ".consumer-status": { verticalAlign: "middle" },
        "&.cm-focused": { outline: "none" },
      }),
      syntaxHighlighting(HighlightStyle.define([
        { tag: tags.heading, color: token.color.text.primary, fontWeight: "600" },
        { tag: tags.strong, fontWeight: "700" },
        { tag: tags.emphasis, fontStyle: "italic" },
        { tag: [tags.link, tags.url], color: token.color.text.accent },
        { tag: tags.processingInstruction, color: token.color.text.tertiary },
        { tag: tags.monospace, color: token.color.text.secondary },
      ])),
    ]);
  },
});

```

### shared/messages.ts

```ts
const chinese = {
  editor: "Markdown 示例编辑器", modes: "编辑器模式", source: "源码", live: "实时预览", locked: "只读",
  undo: "撤销", redo: "重做", reset: "重置", readMarkdown: "读取 Markdown", readingPreview: "阅读预览", insertStatus: "插入状态", done: "已完成", todo: "待处理",
  nativeEditor: "原生 DOM 示例编辑器", statusEditor: "自定义状态编辑器",
  initial: "## 把想法写下来\n\n在这里编辑 **Markdown**，也可以切换到源码。\n\n- [x] 保留 Markdown 原文\n- [ ] 试试修改这一行\n",
  nativeInitial: "## 原生 DOM 接入\n\n这个实例直接使用 `createMsfeEditor()`。\n",
  statusInitial: "## 自定义状态\n\n设计评审：[[todo]]\n\n点击状态按钮，然后读取 Markdown 或撤销。\n",
};
const english: { [Key in keyof typeof chinese]: typeof chinese[Key] } = {
  editor: "Markdown example editor", modes: "Editor mode", source: "Source", live: "Live", locked: "Locked",
  undo: "Undo", redo: "Redo", reset: "Reset", readMarkdown: "Read Markdown", readingPreview: "Reading preview", insertStatus: "Insert status", done: "Done", todo: "Pending",
  nativeEditor: "Native DOM example editor", statusEditor: "Custom status editor",
  initial: "## Write your ideas\n\nEdit **Markdown** here, or switch to source.\n\n- [x] Preserve the Markdown source\n- [ ] Try editing this line\n",
  nativeInitial: "## Native DOM integration\n\nThis instance uses `createMsfeEditor()` directly.\n",
  statusInitial: "## Custom status\n\nDesign review: [[todo]]\n\nClick the status, then read the Markdown or undo.\n",
};
export const exampleMessages = { "zh-CN": chinese, en: english };

```

### shared/types.ts

```ts
/** Example host language; independent of the documentation router. */
export type ExampleLocale = "zh-CN" | "en";

```

## 将编辑器状态接入 Vue

在 `setup()` 中使用 `useMsfeState()`，无需自己编写 `watch(editor)`、初始化快照或取消订阅：

```ts
import { useMsfeState } from '@quirekit/msfe-vue'

const state = useMsfeState(editor) // 可直接传给 MdProse 的 state
const cursor = useMsfeState(editor, {
  select: state => state.selection.main.head,
})
```

`editor` 可为实例、ref 或 getter（例如 `() => props.editor`）。返回只读的浅引用；创建前、
创建失败、实例被清空或销毁时为 `undefined`，模板可用 `v-if="state"` 等待可用状态。
Vue 作用域结束或切换实例会自动清理订阅；借用实例时不会替调用方销毁编辑器。

内部连接 MSFE 的 `subscribeState()`，只观察已接受的 CodeMirror 状态。快照引用同步更新，
不复制状态、重新解析或序列化全文，也不会向编辑器回写。未调用这个 hook 的组件不增加订阅。
`select` 和可选的 `equal` 沿用核心订阅合同：选择器必须纯且有界，每次已接受的更新求值一次，
仅当结果变化时通知 Vue；默认用 `Object.is` 比较。返回新对象时可提供比较函数，例如
`equal: (a, b) => a.from === b.from && a.to === b.to`。

selector 在创建 hook 时确定；它只依赖传入的不可变状态，不自动追踪其中读取的其他 Vue ref。
需要与语言等响应式信息组合时，在返回值之外使用 `computed()`。完整状态供阅读器使用，
工具栏等小型控件优先选择需要的字段。纯选区变化是否能复用阅读内容仍由 `MdProse` 判断。

## Nuxt 与服务端渲染

编辑器依赖浏览器 DOM。在 Nuxt 中使用客户端组件或 `ClientOnly` 隔离编辑器挂载；文档说明和导航仍可服务端渲染。占位区域预留合理高度，避免页面在加载时跳动。

## 自定义组件

使用 `VueNodeViewRenderer()` 替换源码节点的 UI，或使用 `VueComponentViewRenderer()` 渲染搜索面板等通用控件。完整字段修改流程见[自定义节点](/extensions/custom-node)。

## 业务 Provider 与 NodeView

应用可以通过 provide 注入语言、权限或业务服务，NodeView 和 ComponentView 通过同一 Host 继续读取。示例 `shared/locale.ts` 就演示了本地化注入。不要把服务实例或响应式 UI 对象写到 Markdown 字段中。

需要重新配置整个扩展集合时，先保存源码和需要保留的宿主状态，再明确重建实例。`options` factory 是创建入口，不是深度响应式配置器；修改 props 并不等于扩展热插拔。

## Nuxt 项目检查

1. 客户端组件中创建 DOM 编辑器，服务端只渲染占位和说明。
2. 避免同时打包多份 Vue、CodeMirror state/view；链接工作区源码时在打包器中 dedupe 对应包。
3. 自定义组件的依赖必须存在于消费应用，不能依赖文档站私有 alias。
4. 路由离开前处理未保存状态；卸载本身只能负责资源清理，不能代替可靠保存。

本文的可运行示例使用 QuireKit；更复杂的组件见 [节点](/extensions/custom-node) 和 [图表](/extensions/diagrams)。
