@nova_voyager/article-editor-vue3

Protocol-driven Vue 3 article editor based on Tiptap
Install
npm install @nova_voyager/article-editor-vue3
Latest Version:0.5.0
Vue:3
License:MIT
Last Updated:Sep 3, 2026
Links: GitHub  ·  npm
Maintainer: nova_voyager

Vue 3 Article Editor

一个基于 Vue 3、TypeScript、Tiptap 3 和 ProseMirror 的协议驱动文章编辑器。组件只接收和输出 ProseMirror 风格 JSON。

内置 Article Content Protocol v1,并提供协议注册、版本切换、显式迁移、Schema 校验和自定义工具栏能力。

安装与运行

npm install
npm run dev

验证组件库:

npm run check

构建产物位于 dist/:

  • article-editor.js:ESM。
  • article-editor.umd.cjs:UMD/CommonJS。
  • article-editor.css:独立样式。
  • index.d.ts:TypeScript 声明入口。

基础用法

<script setup lang="ts">
import { ref } from 'vue'
import {
  ArticleEditor,
  ARTICLE_PROTOCOL_V1_ID,
  type JSONContent,
} from '@nova_voyager/article-editor-vue3'
import '@nova_voyager/article-editor-vue3/style.css'

const content = ref<JSONContent>({ type: 'doc', content: [] })

function save(payload: { content: JSONContent; protocol: string; valid: boolean }) {
  // 保存 payload.content
}
</script>

<template>
  <ArticleEditor
    v-model:content="content"
    :protocol="ARTICLE_PROTOCOL_V1_ID"
    :editable="true"
    @change="save"
  />
</template>

update:content 在有效编辑 transaction 后同步触发;change 默认防抖 200ms,适合连接保存逻辑。

配置

<ArticleEditor
  v-model:content="content"
  protocol="article-content@1"
  :config="{
    validationMode: 'strict',
    changeDebounce: 300,
    layout: {
      contentHeight: 480,
    },
    link: {
      defaultTarget: '_blank',
      allowedProtocols: ['http', 'https'],
    },
    image: {
      allowedProtocols: ['https'],
      accept: 'image/png,image/jpeg,image/webp',
      maxFileSize: 10 * 1024 * 1024,
    },
    table: {
      defaultRows: 3,
      defaultColumns: 3,
      maxRows: 20,
      maxColumns: 20,
      resizable: false,
    },
    codeBlock: {
      languages: [
        { value: '', label: '纯文本' },
        { value: 'typescript', label: 'TypeScript' },
      ],
    },
    toolbar: {
      image: false,
      textAlign: true,
      articleButton: true,
    },
  }"
/>

config.layout.contentHeight 设置正文滚动区域的高度,数字按像素处理,也可以传入 "60vh"、"calc(100vh - 240px)" 等 CSS 长度。默认值为 480px。工具栏位于滚动区域之外,正文滚动时会固定在编辑器顶部。

配置只能隐藏或收紧协议已有能力。若需要增加 Node、Mark 或属性,应注册新的协议版本。

普通链接与自定义链接

V1 协议的 link mark 支持 href 和 custom 两种类型。工具栏的“链接类型”可以在两者之间切换。

href 链接

type: "href" 要求提供 href 和 target,不能包含 id 或 title。为了兼容旧文章,省略 type 时也按 href 链接处理:

{
  "type": "link",
  "attrs": {
    "type": "href",
    "href": "#section",
    "target": "_self"
  }
}

href 支持不带协议的 URI 引用、配置中允许的绝对 URL 协议,以及 fragment-only 页面锚点。以下值都可以通过严格模式校验:

#
#section
example.com/article
https://example.com/article#section

普通文本链接和 articleButton 链接都会保留输入的 href,不会为不带协议的地址自动补充 https://。例如输入 example.com/article,协议 JSON 中仍保存为 example.com/article。 如果输入中显式包含协议,则该协议仍必须位于 config.link.allowedProtocols 白名单中。

fragment-only 例外只适用于 link.href,不适用于图片 src。javascript:、data: 等未配置的协议仍会被严格模式拒绝。

custom 链接

type: "custom" 要求提供非空的 id、title 和 target,并且不能包含 href:

{
  "type": "link",
  "attrs": {
    "type": "custom",
    "id": "article-42",
    "title": "查看相关文章",
    "target": "_blank"
  }
}

custom 链接渲染为没有 href 的 <a>,对应属性为 data-link-type="custom"、data-link-id、title 和 target。宿主展示文章时可根据 data-link-id 或协议 JSON 中的 id 实现自己的跳转逻辑。

协议注册与切换

协议结构:

interface ArticleProtocolDefinition {
  id: string
  version: string
  label: string
  capabilities: ProtocolCapabilities
  createExtensions(config): Extensions
  validate(content, config): ValidationResult
  toEditorJSON(content): JSONContent
  fromEditorJSON(content): JSONContent
  migrations?: ProtocolMigration[]
}

注册并切换到新协议:

import {
  articleProtocolV1,
  cloneJSON,
  type ArticleProtocolDefinition,
} from '@nova_voyager/article-editor-vue3'

const articleProtocolV2: ArticleProtocolDefinition = {
  ...articleProtocolV1,
  id: 'article-content@2',
  version: '2.0.0',
  label: 'Article Content Protocol v2',
  // 实际项目应替换为 v2 Extension、校验器和 JSON 适配器。
  migrations: [
    {
      from: articleProtocolV1.id,
      migrate: cloneJSON,
    },
  ],
}
<ArticleEditor
  v-model:content="content"
  :protocol="currentProtocol"
  :protocols="[articleProtocolV2]"
  @protocol-changed="handleProtocolChanged"
  @protocol-change-error="handleProtocolError"
/>

协议切换会先迁移并校验目标 JSON,再创建目标 Schema。校验或创建失败时,原编辑器实例和内容保持不变。Tiptap Schema 不支持安全的原地替换,因此成功切换时会原子化重建编辑器。

图片接入

点击默认工具栏的图片按钮后,可选择“上传图片”或“网络图片”:“网络图片”会打开 URL 表单;“上传图片”会调用宿主项目传入的 imageUploader(file)。编辑器不绑定具体上传服务,只把上传成功后的 URL 写入协议 JSON。

图片插入后,点击正文中的图片即可在工具栏看到“图片居左”“图片居中”“图片居右”三个按钮。对齐结果保存到图片节点的 attrs.imageAlign,允许值为 left、center、right;旧数据没有该属性时按居中显示。

上传图片

下面是一个完整的 Vue 3 接入示例:

<script setup lang="ts">
import { ref } from 'vue'
import {
  ArticleEditor,
  type ArticleEditorConfig,
  type ImageUploader,
  type JSONContent,
} from '@nova_voyager/article-editor-vue3'
import '@nova_voyager/article-editor-vue3/style.css'

const content = ref<JSONContent>({ type: 'doc', content: [] })

const editorConfig: ArticleEditorConfig = {
  image: {
    allowedProtocols: ['https'],
    accept: 'image/png,image/jpeg,image/webp',
    maxFileSize: 10 * 1024 * 1024,
  },
}

const uploadImage: ImageUploader = async (file) => {
  const body = new FormData()
  body.append('file', file)

  const response = await fetch('/api/article-images', {
    method: 'POST',
    body,
  })

  if (!response.ok) {
    throw new Error('图片上传失败')
  }

  const result = await response.json() as {
    url: string
    width?: number
    height?: number
  }

  return {
    src: result.url,
    alt: file.name,
    width: result.width,
    height: result.height,
    imageAlign: 'center',
  }
}

function handleEditorError(error: Error) {
  console.error(error.message)
}
</script>

<template>
  <ArticleEditor
    v-model:content="content"
    :config="editorConfig"
    :image-uploader="uploadImage"
    @error="handleEditorError"
  />
</template>

imageUploader 可以返回以下结果:

  • URL 字符串,例如 https://cdn.example.com/image.png。
  • 图片属性对象 { src, alt?, title?, width?, height?, imageAlign? }。
  • null,表示用户取消,不插入图片。

传入 imageUploader 后,也可以直接把一张或多张本地图片拖入可编辑的正文区域。编辑器会:

  • 使用同一个 imageUploader(file) 逐张上传图片,并保持文件原有顺序。
  • 使用鼠标松开时对应的正文位置插入上传结果并立即回显。
  • 在上传前执行 config.image.accept 和 config.image.maxFileSize 校验。
  • 对上传结果执行 URL 协议、宽高和 imageAlign 校验。

拖拽过程中正文区域会显示上传提示。只读模式、未提供 imageUploader 或当前协议不支持 image 节点时,组件不会接管图片拖放。单张图片上传失败不会阻止同一批次中其他有效图片 插入,错误仍通过组件的 error 事件返回。

回调抛出的错误会通过组件的 error 事件传出。config.image.accept 控制文件选择器接受的类型,config.image.maxFileSize 控制上传前的文件大小校验;默认分别为 image/* 和 10 MB。

上传服务返回的 src 必须符合 config.image.allowedProtocols。v1 协议保存的是最终图片 URL,不会把 File、Base64 或临时对象 URL 写入文章 JSON。

网络图片与素材库

“网络图片”不需要配置上传器,用户可以直接填写图片 URL、替代文本、标题和尺寸。

已有素材库可以继续使用 imageResolver。当没有配置 imageUploader、但配置了 imageResolver 时,“上传图片”入口会调用素材选择器:

async function selectImage() {
  const asset = await openAssetPicker()
  return asset ? { src: asset.url, alt: asset.alt } : null
}

文章按钮

v1 协议支持独立的 articleButton 块节点。点击工具栏的“文章按钮”后,可以选择“手动配置”或“从外部选择”。text 和 button 样式需要填写文章 ID;link 样式的文章 ID 为选填。显示文字和样式始终必填,提示文字为选填。

生成的协议 JSON:

{
  "type": "articleButton",
  "attrs": {
    "id": "article-123",
    "text": "查看文章",
    "title": "打开相关文章",
    "style": "button"
  }
}

style 支持 button、text 和 link。text 是文字操作样式;link 会渲染为 <a>,并允许携带 href;其他样式继续渲染为 <button>。title 对应 HTML 的悬停提示,未填写时不会输出该字段。

链接样式示例:

{
  "type": "articleButton",
  "attrs": {
    "text": "查看完整文章",
    "style": "link",
    "href": "https://example.com/articles/456"
  }
}

协议规定 href 只能在 style: "link" 时出现。默认手动配置表单选择“链接”后会把文章 ID 标记为选填,同时显示链接地址并要求填写;支持不带协议的地址、配置允许的绝对 URL,以及 #、#section 等 fragment-only 地址,输入值不会被自动补充 https://。外部选择器返回 link 样式时也应提供 href,可以省略 id。text 和 button 样式仍必须提供非空 id。

接入外部文章选择器

宿主项目通过 articleButtonResolver 接入自己的弹窗、列表或素材库。resolver 可以同步返回,也可以返回 Promise;返回 null 表示用户取消。

<script setup lang="ts">
import { ref } from 'vue'
import {
  ArticleEditor,
  type ArticleButtonResolver,
  type JSONContent,
} from '@nova_voyager/article-editor-vue3'
import '@nova_voyager/article-editor-vue3/style.css'

const content = ref<JSONContent>({ type: 'doc', content: [] })

const selectArticle: ArticleButtonResolver = async ({ mode, current }) => {
  // openArticlePicker 由宿主项目实现,可以打开任意 Vue 弹窗或选择组件。
  const selected = await openArticlePicker({ mode, currentId: current?.id })
  if (!selected) return null

  return {
    id: selected.id,
    text: selected.title,
    title: `打开文章:${selected.title}`,
    style: 'link',
    href: selected.url,
  }
}
</script>

<template>
  <ArticleEditor
    v-model:content="content"
    :article-button-resolver="selectArticle"
    @error="console.error($event)"
  />
</template>

如果当前选中了已有的文章按钮,resolver 会收到 { mode: 'update', current };否则收到 { mode: 'insert', current: undefined }。返回结果只会保留协议允许的 id、text、title、style 和 href 字段,字符串会去除首尾空格。href 仅在 style: 'link' 时保留;link 可省略 id,其他样式缺少 id 时会拒绝插入。

编辑器内点击文章按钮只会选中节点,便于再次编辑,不会执行业务跳转。最终展示页面可读取 data-article-button-id 或协议 JSON 中的 attrs.id,自行实现打开文章等行为。

仓库内置了一个可运行的外部选择器 Demo。执行 npm run dev 后,点击“文章按钮 → 从外部选择”,可以体验文章搜索、列表选择、样式选择、取消,以及编辑已有文章按钮时的数据回填。Promise 桥接代码位于 src/App.vue,独立选择弹窗位于 src/components/DemoArticlePicker.vue。

自定义工具栏

可以替换默认工具栏。工具栏应只调用 Tiptap command,不能直接修改 JSON:

<ArticleEditor v-model:content="content">
  <template #toolbar="{ editor, protocol, config, revision }">
    <MyToolbar
      :editor="editor"
      :protocol="protocol"
      :config="config"
      :revision="revision"
    />
  </template>
</ArticleEditor>

组件也导出 useArticleEditor(),可用于构建完全无默认 UI 的编辑器封装。

Props

名称 类型 默认值 说明
content JSONContent 空文档 协议 JSON
protocol string | ArticleProtocolDefinition article-content@1 当前协议
protocols ArticleProtocolDefinition[] [] 附加协议注册表
config ArticleEditorConfig {} 交互与限制配置
editable boolean true 是否允许编辑
showToolbar boolean true 是否渲染工具栏 slot/default toolbar
imageResolver ImageResolver undefined 外部图片选择器
imageUploader ImageUploader undefined 本地图片上传回调,返回 URL 或图片属性
articleButtonResolver ArticleButtonResolver undefined 外部文章选择器,返回 articleButton 属性或 null

Events

事件 说明
update:content 同步有效协议 JSON
change 防抖后的 { content, protocol, valid }
ready 编辑器实例创建完成
validation-error 输入、输出或切换校验失败
protocol-changed 协议切换成功
protocol-change-error 协议切换失败
error 编辑器或外部解析器运行错误

暴露方法

  • editor:当前 Tiptap Vue Editor。
  • activeProtocol:当前协议定义。
  • setContent(content):校验并静默替换内容。
  • getJSON():取得协议 JSON。
  • validate(content?):执行当前协议校验。
  • focus(position?):聚焦编辑器。

v1 范围

支持 paragraph、H1–H6、blockquote、bullet/ordered list、code block、horizontal rule、URL image、articleButton、table,以及 bold、italic、strike、underline、inline code 和 link。paragraph 与 heading 额外支持 attrs.textAlign,允许 left、center、right、justify;image 支持 attrs.imageAlign,允许 left、center、right;link 支持 href 与 custom 两种类型;articleButton 支持 text、button 和带 href 的 link 样式。

不支持 Highlight、上下标、Video、Mention、Emoji、Embed、AI Block 或协作评论。完整示例见 examples/article-demo.json。

Related Packages

article-content-renderer-vue2

A safe Vue 2.7 renderer for the Article Content Protocol ProseMirror JSON format...

1,994 0
vue-tiptap-editor

A Vue Editor Component based on Tiptap

591
@chiefry/tiptap-editor-vue

Vue 3 + Tiptap v3 富文本编辑器组件

4,973
@tiaohsun/vue-rich-text-editor

A thin, batteries-included Vue 3 wrapper around Tiptap v3 — editor component, to...

1,299 0
@looohz/rich-editor

A rich text editor Vue 3 component with WYSIWYG, Markdown, and split-preview mod...

388

Version History

Version Released Vue License
0.5.0 ^3.3.0 MIT
0.4.0 ^3.3.0 MIT
0.3.0 ^3.3.0 MIT
0.2.1 ^3.3.0 MIT
0.2.0 ^3.3.0 MIT