直接积分
导入葡萄,调用grapesjs.init()在onMounted(),调用editor.destroy()在onUnmounted()。没有额外的依赖,你和编辑器API之间没有任何关联。
将开源的 GrapesJS 可视化编辑器集成到 Vue 3 和 Nuxt 应用程序中,构建可视化页面构建器、CMS 编辑器、着陆页构建器、电子邮件编辑器以及白标编辑体验。
这个分割值得在任何代码之前先说明。你的 Vue 应用拥有路由、用户、权限和数据。GrapesJS 拥有画布以及用户在其中操作的所有东西。两者在一个 DOM 节点和一个存档调用处相连。
这条链中没有任何东西需要 Vue 来知道编辑器的工作原理,也不需要 GrapesJS 才能知道它在 Vue 应用里。这就是为什么集成时间很短。
是的。GrapesJS 与框架无关,可以直接集成到 Vue 3 应用中。你可以用 Vue 生命周期钩子初始化编辑器,将编辑器实例保留在可组合或组件中,将 GrapesJS 事件连接到你的 Vue 应用,并通过自己的后端持久化项目数据。
导入葡萄,调用grapesjs.init()在onMounted(),调用editor.destroy()在onUnmounted()。没有额外的依赖,你和编辑器API之间没有任何关联。
将生命周期包裹在你自己的组合软件或社区包中,使组件获得类似Vue的API而非命令式API。当多个屏幕挂载一个编辑器时,这非常有用。
GrapesJS 发布了一个官方框架包装器 @grapesjs/react,用于 React。GrapesJS 项目没有对应的 Vue 包,因此你在 npm 上找到的任何 Vue 包装器都是社区项目。在采用之前,请查看其 Vue 和 GrapesJS 的对等范围。
Vue 3 是一个应用框架。GrapesJS 是一个可视化编辑引擎。它们解决不同的问题,集成之所以干净,正是因为它们没有重叠。
其中五个是你的。其余的已经存在,剩下的两个是配置而非施工。
一个依赖。GrapesJS 发布了自己的 TypeScript 定义,因此没有单独的 @types 包需要添加。
npm install grapesjs没有样式表,编辑器是无样式的。在Vite或Nuxt项目中,你可以从创建编辑器的组件或组合器导入;而在有全局样式表的应用中,只需导入一次。
把它复制到组件中,它就能运行。本节之后的内容都是对它的补充,而不是重写。
<template>
<div ref="editorContainer" class="editor-shell"></div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import grapesjs, { type Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
// A template ref: the name matches the ref="" attribute above.
const editorContainer = ref<HTMLDivElement | null>(null);
// shallowRef, not ref — see "Performance" further down the page.
const editor = shallowRef<Editor | null>(null);
onMounted(() => {
if (!editorContainer.value) return;
editor.value = grapesjs.init({
container: editorContainer.value,
height: '100%',
fromElement: false,
storageManager: false,
});
});
onUnmounted(() => {
editor.value?.destroy();
editor.value = null;
});
</script>
<style scoped>
.editor-shell {
height: 100vh;
}
</style>模板的ref名称与ref=“”属性匹配——这就是Vue在<script setup>中绑定它们的方式。
编辑器存在于 shallowRef,而非 ref。否则 Vue 会走遍整个编辑器对象图,使其变得被动。
fromElement 是假的,所以 GrapesJS 是从你通过的组件开始的,而不是容器内部的标记。
整个积分是一个生命周期映射。Vue 告诉你 DOM 节点何时存在,何时即将消失;GrapesJS 正好需要这两个时刻。
从头到尾读,这就是两个图书馆之间的全部合同。
onMounted和onUnmounted都不在服务器上运行,这也是为什么普通的Vue SPA不需要额外保护。服务器渲染的应用需要多一个保护——Nuxt部分涵盖了这一点。
一旦第二个屏幕需要编辑器,就把生命周期迁移到可组合界面。组件才会说它想要什么,而不是编辑器的连接方式。
// composables/useGrapesJS.ts
import { onMounted, onUnmounted, shallowRef, type Ref } from 'vue';
import grapesjs, { type Editor, type EditorConfig } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
type EditorOptions = Omit<EditorConfig, 'container'>;
export function useGrapesJS(
container: Ref<HTMLElement | null>,
options: EditorOptions = {},
) {
const editor = shallowRef<Editor | null>(null);
const init = (): Editor | undefined => {
if (editor.value || !container.value) return;
editor.value = grapesjs.init({
container: container.value,
height: '100%',
fromElement: false,
storageManager: false,
...options,
});
return editor.value;
};
const destroy = (): void => {
editor.value?.destroy();
editor.value = null;
};
onMounted(init);
onUnmounted(destroy);
return { editor, init, destroy };
}<template>
<div ref="editorContainer" class="editor-shell"></div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useGrapesJS } from '~/composables/useGrapesJS';
const editorContainer = ref<HTMLElement | null>(null);
const { editor } = useGrapesJS(editorContainer, {
blockManager: {
blocks: [
{
id: 'hero',
label: 'Hero',
category: 'Sections',
content: '<section>…</section>',
},
],
},
});
</script>不要让它过度扩张。一个将每个GrapesJS模块包裹在反应式界面的组合文件,既是第二个需要学习的API,也是保持同步的第二个关键。
GrapesJS 管理编辑器状态。Vue 管理应用状态。事件是接缝:订阅少数几个你的 UI 实际反映的事件,然后写入普通参考。
import { onMounted, ref, shallowRef } from 'vue';
import type { Editor, Component } from 'grapesjs';
const isReady = ref(false);
const isDirty = ref(false);
const saveState = ref<'idle' | 'saving' | 'saved' | 'error'>('idle');
const selectedName = ref<string | null>(null);
function bindEditorEvents(editor: Editor): void {
editor.on('load', () => {
isReady.value = true;
});
editor.on('component:selected', (component: Component) => {
selectedName.value = component?.getName?.() ?? null;
});
editor.on('component:update', () => {
isDirty.value = true;
});
editor.on('storage:start:store', () => {
saveState.value = 'saving';
});
editor.on('storage:end:store', () => {
saveState.value = 'saved';
isDirty.value = false;
});
editor.on('storage:error', () => {
saveState.value = 'error';
});
}| 事件 | 你的界面对它的作用 |
|---|---|
| load | 项目在画布上后隐藏加载状态 |
| component:selected | 在你自己的工具栏中显示选中了哪个元素 |
| component:update | 标记文档为脏,启用保存按钮 |
| storage:start:store | 显示储蓄指示 |
| storage:end:store | 节目得救,清除污点旗 |
| storage:error | 用户可以采取行动的故障表层 |
镜像你显示的内容,而不是编辑器里的存储。把组件树复制到Vue存储会给你两个真实来源和一个同步错误。
GrapesJS 依赖浏览器的 DOM API,所以编辑器初始化应该只在浏览器上下文中进行。在 Nuxt 中,这意味着编辑器不出现在服务器渲染中——其他一切都是你已有的集成。
<!-- pages/editor.vue -->
<template>
<ClientOnly>
<VisualEditor />
<template #fallback>
<p class="editor-placeholder">Loading the editor…</p>
</template>
</ClientOnly>
</template><!-- components/VisualEditor.client.vue -->
<template>
<div ref="editorContainer" class="editor-shell"></div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useGrapesJS } from '~/composables/useGrapesJS';
// Reached only in the browser: the .client suffix keeps this
// component out of the server render, and onMounted never runs
// on the server anyway.
const editorContainer = ref<HTMLElement | null>(null);
const { editor } = useGrapesJS(editorContainer);
</script>把编辑器包裹在使用到的地方。备用槽给服务器一个可以渲染的东西,这样在水合前就不会有空白帧。
Nuxt 自己的命名规范:文件名以 .client 结尾的组件只在浏览器中渲染。当编辑器在多个地方使用时非常有用。
两者都是有效的,并且可以组合。GrapesJS 都不是强制要求的——GrapesJS 要求的只是 init() 在文档存在的地方运行。
SSR 渲染应用壳层。GrapesJS 运行在浏览器中。命名这个边界使得 Nuxt 集成变得可预测,而不是水合漏洞的根源。
编辑器界面不是你为SEO服务器渲染的页面——它是一个认证工具。它生成的页面是你渲染和索引的内容,这些都是纯HTML和CSS。
构建一个完整的Vue页面构建器 →不是。直接集成是使用GrapesJS的完整且受支持的方式,这正是本页示例所做的。封装器是一种便利,而非必需品。
| 方法介绍 | 最佳 | 权衡 |
|---|---|---|
| 直通GrapesJS | 最大控制 | 你自己写了生命周期——大约十五行。 |
| Vue 封装器/集成 | 更偏向 Vue 的 API | 你和编辑器API之间的依赖,依赖别人的发布时间表。 |
| 自定义组合 | 可复用应用集成 | 这是你自己维护的——但这就是上面展示的文件,而且不会变得陈旧。 |
在采用任何东西之前,值得先核实一下,因为这个问题的搜索结果比他们描述的套餐还要早。
我对照了2026-09-02的npm注册表。如果你找到更新的Vue封装,先读它的peerDependencies:那个单一栏能告诉你它是否针对Vue 3和当前的GrapesJS。
对于今天的 Vue 3,上面的组合是封装器。它有二十多行,直接使用 GrapesJS API,且不会落后于你依赖的版本。
挂载编辑器是在第一个下午。剩下的部分工作是用户信任的页面构建器,主要是对GrapesJS已经发布的模块进行配置。
Canvas
可编辑表面及其设备宽度。
Components
用户可以放置的类型,以及他们的特征。
Blocks
他们拖拽的调色板。
Style Manager
哪些 CSS 属性被暴露,以及暴露给谁。
Asset Manager
图片和媒体,来自你的存储。
Storage Manager
项目数据被加载和保存。
Commands
有名称的动作,可以绑定到你自己的UI上。
Device Manager
编辑和输出时的响应式断点。
Plugins
以上所有产品都已包装好且可重复使用。
同一个编辑器核心,但配置不同。每个核心都是不同的块、不同的存储目标和不同的发布步骤。
这三件事很容易混淆,但合并起来却成本高昂。第一天就把这些做对,才让文档一年后依然可以重新编辑。
GrapesJS 项目的可编辑状态——页面、组件、样式、资源。用 editor.getProjectData() 读取,用 editor.loadProjectData() 恢复。这就是要持久保存的东西。
渲染出来的标记,来自editor.getHtml()。每次发布时重新生成。它是输出,不是源。
编辑器生成的样式,来自editor.getCss()。同样规则:生成的,不作为真实的来源存储。
存储可编辑的项目数据。需要渲染或发布时生成HTML和CSS。
只保存渲染后的 HTML 是无法纠正的错误。你总可以从项目数据重新生成 HTML;但你无法可靠地从 HTML 恢复项目数据。
// Editable state — store this, it is what the editor reloads.
const projectData = editor.getProjectData();
// Rendered output — regenerate this whenever you publish.
const html = editor.getHtml();
const css = editor.getCss();
await fetch(`/api/projects/${projectId}/publish`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectData, html, css }),
});
// Sanitize `html` on the server before it is served to anyone.GrapesJS 不在乎你的后端是什么。存储适配器是两个异步函数——一个返回项目数据,一个接受项目数据——所以编辑器会与你已有的 API 通信。
import grapesjs, { type ProjectData } from 'grapesjs';
// Wherever your app keeps it — a route param, a prop, a store.
const projectId = props.projectId;
const editor = grapesjs.init({
container: editorContainer.value!,
fromElement: false,
storageManager: {
type: 'app-api',
autosave: true,
stepsBeforeSave: 10,
},
// Registered as a plugin so the adapter exists before
// GrapesJS performs its first load.
plugins: [
(ed) => {
ed.Storage.add('app-api', {
async load(): Promise<ProjectData> {
const res = await fetch(`/api/projects/${projectId}`);
if (!res.ok) {
throw new Error(`Load failed: ${res.status}`);
}
return await res.json();
},
async store(data: ProjectData) {
const res = await fetch(`/api/projects/${projectId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectData: data }),
});
if (!res.ok) {
throw new Error(`Save failed: ${res.status}`);
}
},
});
},
],
});通过插件数组注册适配器很重要:这保证了存储在 GrapesJS 进行首次加载前就已存在。
资产管理就像一个终端上的界面。把它指向你的,所有现有规则——认证、验证、配额、CDN——都会持续生效。
// Fetched from your own API before the editor is created.
const assets = await fetch('/api/assets').then((res) => res.json());
const editor = grapesjs.init({
container: editorContainer.value!,
assetManager: {
assets,
// Uploads go to your endpoint, with your auth and
// your validation.
upload: '/api/assets',
uploadName: 'files',
multiUpload: true,
autoAdd: true,
headers: { Authorization: `Bearer ${token.value}` },
credentials: 'same-origin',
// Reject what the backend would reject anyway,
// before the round trip.
beforeUpload: (files: File[]) => {
const limit = 5 * 1024 * 1024;
const tooBig = files.some((f) => f.size > limit);
if (tooBig) {
toast.error('Images must be under 5 MB');
return false;
}
},
},
});自定义组件类型是编辑器学习你的产品的方式。它有自己的设置面板、自己的下落规则和调色板中的独立块。
import type { Editor } from 'grapesjs';
export function pricingCardPlugin(editor: Editor): void {
editor.Components.addType('pricing-card', {
isComponent: (el) => el.dataset?.gjsType === 'pricing-card',
model: {
defaults: {
tagName: 'section',
attributes: { 'data-gjs-type': 'pricing-card' },
// Traits become the settings panel for this component.
traits: [
{ type: 'text', name: 'plan', label: 'Plan name' },
{ type: 'text', name: 'price', label: 'Price' },
{ type: 'checkbox', name: 'featured', label: 'Featured' },
],
components: `
<h3 data-gjs-type="text">Starter</h3>
<p data-gjs-type="text">$0 / month</p>
<a href="#">Choose plan</a>
`,
},
},
});
editor.Blocks.add('pricing-card', {
label: 'Pricing card',
category: 'Marketing',
content: { type: 'pricing-card' },
});
}
// Pass it to the editor like any other plugin:
// grapesjs.init({ container, plugins: [pricingCardPlugin] })自定义组件也是限制编辑器的方式。在类型上设置可丢弃或拖拽,是防止用户在导航栏内放置定价卡的关键,而特征则是给用户一个带标签的字段,而不是原始的CSS控件。
这些是 GrapesJS 组件类型,不是 Vue 组件。画布渲染 DOM;你的 Vue 组件渲染你的应用围绕它。
插件是一个接收编辑器的函数——无论它来自npm、GJS.Market还是你自己的仓库,都是相同的函数签名。它们都不在乎主机应用是Vue。
import grapesjs from 'grapesjs';
import basicBlocks from 'grapesjs-blocks-basic';
import { pricingCardPlugin } from '~/editor/pricing-card';
const editor = grapesjs.init({
container: editorContainer.value!,
plugins: [
// Options go through a wrapper function: `pluginsOpts`
// is keyed by string, so it only reaches plugins that
// were passed to `plugins` by name.
(ed) => basicBlocks(ed, { flexGrid: true }),
pricingCardPlugin,
],
});这不是说建筑是错的——而是对建筑含义的清单。每一行都是一个子系统,无论你是否计划好。
| 能力 | 打造自己 | GrapesJS |
|---|---|---|
| Canvas | 建造 | 收录 |
| Drag & drop | 建造 | 收录 |
| Components | 建造 | 收录 |
| Blocks | 建造 | 收录 |
| Style Manager | 建造 | 收录 |
| Asset Manager | 建造 | 可扩展性 |
| Storage Manager | 建造 | 可扩展性 |
| Plugins | 构建生态系统 | 插件架构 |
| Vue integration | 本地 | 框架无关性 |
Vue 提供应用框架。GrapesJS 提供视觉编辑引擎。
因为Vue组件是应用的构建模块,而可视化编辑器还需要在上面加点别的东西:画布管理、带有丢弃规则、选择和悬停状态的组件树、写真实CSS的样式层、设备切换、撤销和重做、资产管理、序列化、命名命令,以及整个项目的扩展点。你可以在Vue里构建这些——这只是一个和你最初想做的项目不同的项目。
Vue + GrapesJS 与单纯 Vue 组分是不同的命题。
你的Nuxt或Vue应用程序:壳体、会话,以及谁被允许打开哪个项目。
编辑器安装在一个屏幕上。它接收一个项目并发出一个项目。
你自己的路由。GrapesJS 通过你写的存储和资产适配器调用它们。
项目、媒体、用户和版本实际上存在——不被上方编辑器更改。
编辑器是那棵树上的一个叶子,不是根节点。它上面的所有内容都是你本来就会写的代码。
不要让编辑变得非常被动
这才是真正会咬人的。shallowRef,或者一个普通的模块作用域变量——绝不会在编辑器实例周围使用ref()。Vue会走动一个非常大的对象图,并对内部流失执行效果。
只有在需要时才初始化
在编辑界面创建编辑器,而不是在每个路由都挂载的布局或存储中。
懒惰加载编辑器
GrapesJS 是一个相当可观的捆绑包。把它加载成独立的异步区块,这样那些从未编辑的路由就不会付费。
将更新排除在全局状态之外
将每个 component:update 推入存储,都会将每个按键都转化为应用范围的渲染。
大型资产收藏的分页
从分页端点为资产管理器做种子,而不是把成千上万的 URL 发送到编辑器里。
保持组件树的管理
非常深的文档渲染、序列化和差异化成本更高。如果你的块能做到,结构自然会促进。
可视化编辑器是一个接受用户自撰HTML的系统。就这样对待它吧。
几乎所有关于该组合的集成问题都属于这七个。
症状
容器是空的,或者编辑器渲染成虚无。
修复
在onMounted()里创建编辑器。模板参考之前没有元素。
症状
记忆会增长,听众在离开路线后会开枪。
修复
用onUnmounted()呼叫editor.destroy()并清除参考。
症状
编辑器感觉迟钝;Vue 开发工具卡顿。
修复
将实例保持在浅层Ref,或者完全不在Vue反应性中。
症状
文档未定义,或者编辑路径上的水合不匹配。
修复
渲染编辑器为仅客户端——<ClientOnly>、.client组件,或两者兼有。
症状
两个真实来源,以及在导航中消失的编辑。
修复
让GrapesJS拥有项目;只镜像你UI显示的少数数值。
症状
编辑器能加载,但面板没有样式且无法使用。
修复
导入 grapesjs/dist/css/grapes.min.css,或者包含在你的全局样式表中。
症状
类型错误,或者基于Vue 2生命周期钩子构建的集成。
修复
在采用任何封装器之前,请先检查peerDependencies。几个流行的例子早于Vue 3。
在Vue 2上,相同的积分使用mounted()和beforeDestroy()代替onMounted()和onUnmounted();GrapesJS侧保持不变。新工作应针对Vue 3——本页所有内容均假设如此。
顺序大致与本页介绍他们的顺序相符。
是的。GrapesJS 是一个与框架无关的库,渲染成 DOM 元素,所以它在 Vue 3 中无需适配器即可工作。在 onMounted() 中初始化它,然后在 onUnmounted() 中销毁它。
运行 npm 安装 grapesjs。该包包含自己的 TypeScript 定义。你还需要加载它的样式表,要么导入 grapesjs/dist/css/grapes.min.css,要么将其包含在全局 CSS 中。
在容器元素上创建一个模板ref,然后在onMounted()中调用grapesjs.init({ container: containerRef.value })。该元素在挂载前不存在,所以初始化任何一个元素后,GrapesJS就没有可渲染的对象。
是的,这是推荐的方法。onMounted() 和 onUnmounted() 直接映射到 grapesjs.init() 和 editor.destroy(),整个集成都装进一个可组合单元里。
是的。本页上的每个示例都使用 <script setup lang="ts">。用 ref() 声明的模板 ref 会自动绑定匹配的 ref=“” 属性。
是的。集成方式和普通的 Vue 3 完全一样;唯一的新增是编辑器不出现在服务器渲染中,使用 <ClientOnly> 或 .client 组件。
是的,编辑器是只渲染客户端的。GrapesJS 需要浏览器 DOM API,所以不能在服务器渲染时运行——但它生成的页面是纯 HTML 和 CSS,这些正是你服务器渲染和索引的内容。
没有。直接集成是一种完整的方法,这里的例子就是这样。GrapesJS 发布了官方的 React 封装器,但没有 Vue 的对应版本,所以任何 Vue 封装器在 npm 上都是社区项目——在采用前请查看其 Vue 和 GrapesJS 的同类范围。
是的。GrapesJS 在包里自带了 TypeScript 定义,所以编辑器类型和配置对象的类型没有额外的 @types 依赖。
是的。用 editor.Storage.add() 注册一个存储适配器,实现 load() 和 store()。这两个都是普通的异步函数,所以可以调用你已有的任何 API。
是的。editor.Components.addType() 定义了一个带有自身特征和下落规则的组件类型,而 editor.Blocks.add() 则将其放入调色板。这就是编辑器学习你产品术语的方式。
是的。GrapesJS 插件是一个接收编辑器实例的函数,因此不受托管框架的影响。将插件传递到 grapesjs.init() 插件数组中。
是的——这是常见的情况。挂载编辑器是最短的部分;剩下的就是围绕它配置块、存储、资源和权限。