Next.js + GrapesJSNext.js 页面构建器,基于 GrapesJS 在你的 Next.js 应用内构建一个可视化拖放页面构建器,使用GrapesJS、自定义组件、持久项目数据和你自己的发布流程。
Next.js App Router React 拖放 视觉编辑 HTML 与 CSS 可扩展
your-app.com/editor
Next.js 块面板、画布、图层、样式管理器和响应式设备——编辑器表面 GrapesJS 在你写一行之前就已经给你了。 your-app.com/editor
Next.js 块面板、画布、图层、样式管理器和响应式设备——编辑器表面 GrapesJS 在你写一行之前就已经给你了。 在本页
GrapesJS 数据于 2026-09-03 通过 npm 注册表与 GitHub API 核实。核心库为 BSD-3-Clause;官方 React 封装器为 MIT。
构建与采用的问题
在不构建编辑器引擎的情况下,构建一个 Next.js 页面构建器 GrapesJS 提供视觉编辑引擎。Next.js 提供围绕其的应用架构。以下是重复阅读的同一份列表:编辑器的组成部分,以及在你采用 GrapesJS 后,谁负责构建每个部分。
拖放 — GrapesJS 核心 画布 — GrapesJS 核心 组件树 — GrapesJS 核心 选择与悬停 — GrapesJS 核心 区块 — GrapesJS 核心 样式 — GrapesJS 核心 响应式编辑 — GrapesJS 核心 撤销/重做 — GrapesJS 核心 资源管理 — GrapesJS 核心 序列化 — GrapesJS 核心 命令 — GrapesJS 核心 插件系统 — GrapesJS 核心 存储 — GrapesJS 核心 多页项目 — 插件或扩展 模板库 — 插件或扩展 用户与权限 — 你的Next.js应用 发布 — 你的Next.js应用 谁来建造它 你的Next.js应用 GrapesJS 核心 插件或扩展
十七张中有两张是你的。其余的要么随编辑器附带,要么作为插件安装。
页面构建器看起来像是一个功能,实际上是一个小型产品。可见的部分——一块块、画布、样式侧边栏——都建在十几个子系统之上,这些子系统都必须全部运行起来,才能让它们看起来可用。
与其重建编辑器引擎,不如用GrapesJS专注于你的产品。
结果
用Next.js页面构建器能构建什么? 同一个编辑器引擎支持着非常不同的产品。它们之间的区别在于你注册的区块、谁被允许发布以及输出的去向。
试试 Next.js 页面构建器 这是一个真正的 GrapesJS 编辑器,就运行在本页面上。从右侧拖入一个区块,选中画布上的任意元素改样式,或把画布切换到手机宽度——这就是你的用户会看到的编辑界面。
Drag & drop Blocks Layers Style Manager Responsive devices Assets Undo / redo React 入门示例 免费 定制 React UI 免费 shadcn UI Premium 经典演示 免费
React 入门集成:GrapesJS 从一个 React 组件挂载,配有标准的块、图层和样式面板。
grapesjs-react-demo.netlify.app 免费
your-app.com/editor
Next.js 加载编辑器 在iframe上加载了grapesjs-react-demo.netlify.app的第三方演示。
目录中
React 入门示例 可在 GJS.Market 上使用
用户拖拽方块时会发生什么
Next.js application ↓ Client Component ↓ GrapesJS ↓ Project data ↓ Next.js API ↓ Database ↓ Publish 三项责任,三位主人。
Next.js 负责路由、认证、数据获取和部署表面。GrapesJS 拥有画布内的所有内容。你的 API 拥有项目内容、编辑权以及项目何时上线。下面的每个部分都是打开的三个盒子之一。
Next.js 负责应用程序。GrapesJS 负责视觉编辑。你的后端负责持久化和发布。这条链中没有任何要求你放弃现有架构。
建筑
GrapesJS如何融入Next.js应用 GrapesJS 是一个层,而不是框架。 它会把画布渲染成你给它的DOM节点,并为画布中的所有内容暴露一个API。它对路由没有意见,对你的数据库没有意见,也没有与应用其他部分的运行时关系,除了它被交付的元素。
NEXT.JS
├── App Router ├── Authentication ├── Users ├── Permissions ├── API ├── Database ├── Billing └── Publishing ↓
CLIENT EDITOR
↓
GRAPESJS
├── Canvas ├── Components ├── Blocks ├── Style Manager ├── Assets ├── Commands └── Storage 这就是为什么集成规模很小。编辑器是一个客户端小部件,带有丰富的 API——形状与代码编辑器或图表库相同,而非竞争的应用框架。所有让你的产品属于你的部分都留在 Next.js 端。
GrapesJS + React 积分 GrapesJS 并不能取代 Next.js。它只是添加你的应用所需的可视化编辑层。
快速入门
用GrapesJS配合Next.js App Router GrapesJS 需要一个浏览器。 它测量元素,附加监听器,并在 DOM 初始化时进行变异,所以编辑器属于 Client Component,其设置属于效果。这就是全部约束——其他都是普通的 React。
npm install grapesjs
components/GrapesEditor.tsx tsx
'use client';
import { useEffect, useRef } from 'react';
import grapesjs, { type Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
export default function GrapesEditor() {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<Editor | null>(null);
useEffect(() => {
if (!containerRef.current) return;
// Runs only in the browser: effects never execute during SSR.
const editor = grapesjs.init({
container: containerRef.current,
height: '100vh',
storageManager: false,
blockManager: {
blocks: [
{
id: 'section',
label: 'Section',
content: '<section class="py-16"><h2>Headline</h2></section>',
},
{ id: 'text', label: 'Text', content: '<p>Edit me</p>' },
],
},
});
editorRef.current = editor;
// Strict Mode mounts twice in development; without this you get two editors.
return () => {
editor.destroy();
editorRef.current = null;
};
}, []);
return <div ref={containerRef} />;
}应用/编辑器/页面.tsx tsx
import GrapesEditor from '@/components/GrapesEditor';
// A Server Component. It renders the Client Component; it never touches
// the editor instance, and no 'use client' is needed here.
export default function EditorPage() {
return <GrapesEditor />;
}为什么这个例子会这样
'use client' 将模块标记为Client Component,以便React钩子可用,代码发送到浏览器。
useRef,不是useState 编辑器实例不是渲染数据。把它放在状态状态会对每个更改都重新渲染,但没有任何好处。
useEffect 效果在服务器渲染过程中从不运行,因此初始化保证只有在DOM存在后才会进行。
editor.destroy() React Strict Mode 在开发过程中会挂载两次组件。没有清理的话,你会看到两个编辑器堆放在一个节点上。
那是一个可用的编辑器。本页剩余部分讲述了接下来添加的四个内容:项目存储在哪里,用户可以拖拽什么,发布时会发生什么,以及哪些内容你不必自己编写。
布线
App Router 与 Pages Router 两者都有效。它们在客户端边界划定的位置不同,这也是为什么网络上许多GrapesJS + Next.js的建议在现代项目中失败的原因。
推荐 App Router Client Component 保留编辑器;绕过它的路由保持 Server Component。不需要动态导入,因为编辑器已经在浏览器中初始化。
tsx
// components/GrapesEditor.tsx
'use client';
// …useRef + useEffect + grapesjs.init()
// app/editor/page.tsx — stays a Server Component
import GrapesEditor from '@/components/GrapesEditor';
export default function Page() {
return <GrapesEditor />;
} 'use client'指令标记了边界——所有低于边界的信号都发送到浏览器。 路由文件保持Server Component状态,可以等待认证、参数和数据后再渲染编辑器。 Client Component 默认仍是服务器预渲染的。特效则不是,这也是 grapesjs.init() 保持浏览器功能的原因。 next/dynamic与ssr: false在Server Component内被排斥——Next.js告诉你将其移入Client Component。旧版 Pages Router 每个页面都是客户端的入口点,所以通常的做法是动态导入并关闭预渲染。
pages/editor.tsx tsx
import dynamic from 'next/dynamic';
// In the Pages Router every page is a client entry point, so ssr: false
// is allowed here — and skips the prerender pass entirely.
const GrapesEditor = dynamic(() => import('@/components/GrapesEditor'), {
ssr: false,
loading: () => <p>Loading editor…</p>,
});
export default function EditorPage() {
return <GrapesEditor />;
} 这里允许使用ssr: false,并且完全跳过服务器渲染通行证。 加载选项会给你一个占位符,同时编辑器片段正在下载。 GrapesEditor 组件的同一个功能保持不变——只是导入方式不同。如果你在迁移,不要携带动态()包装器。在App Router中,它要么直接在Server Component内部失败,要么重复了'use client'已经完成的工作。
服务器组件
GrapesJS 和 React Server Components Server Components 才是编辑器周边工作的最佳位置。 它们获取数据,运行服务器端逻辑,并提供应用壳。Client Component 初始化 GrapesJS,拥有其生命周期并处理所有浏览器交互。prop 跨越边界;编辑器实例则从不跨越。
边界所在位置
Server Component ↓ Page / data ↓ Client Component ↓ GrapesJS editor app/projects/[projectId]/editor/page.tsx tsx
import GrapesEditor from '@/components/GrapesEditor';
export default async function EditorPage({
params,
}: {
params: Promise<{ projectId: string }>;
}) {
const { projectId } = await params;
// Server side: auth, permissions and data fetching stay here.
const res = await fetch(`${process.env.API_URL}/projects/${projectId}`, {
cache: 'no-store',
});
const initialProject = await res.json();
// The Client Component receives plain, serialisable props.
return <GrapesEditor projectId={projectId} initialProject={initialProject} />;
} 认证和权限检查在服务器上运行,编辑器包才值得下载。 项目的初始数据会从服务器端获取,并以普通、可序列化的道具形式传递下去。 编辑器实例留在Client Component内部。它没有任何可序列化的内容,所以没有任何东西会越过边界。 Server Actions 可以从客户端组件调用存档——它们只是边界客户端的函数。编辑器保留客户端,同时使用 Server Components 来管理周边应用。
SSR
GrapesJS和Next.js SSR兼容吗? 是的,但编辑器本身应该在客户端上初始化,因为它依赖浏览器的APIs和DOM。
这个答案的有用版本更具体,因为这两部分的失败方式不同。
本地复制
runtime Node 20, no DOM
grapesjs 0.23.6
import import('grapesjs') → resolves
init grapesjs.init() → ReferenceError: document is not defined 2026-09-02
在无 DOM 的运行时导入库是可以的。调用 init() 则不行。所以规则不是“让 GrapesJS 远离服务器”,而是“让 init() 远离渲染路径”,而效果已经保证了这一点。
导入是安全的。将GrapesJS捆绑到服务器也评估的模块中不会丢弃。 初始化则不是。grapesjs.init() 读取文档,因此必须在挂载后运行——这正是 useEffect 的含义。 'use client' 与“仅客户端”不同。Client Components 是在服务器上预渲染的;效果是哪些不在服务器上运行。 因此,dynamic(..., { ssr: false }) 在 App Router 中是可选的。你拿它是为了让编辑器块不进入初始有效载荷,而不是为了修复崩溃。 渲染已发布页面则是完全不同的问题——见下文。该页面根本不需要编辑器运行时间,因此可以像其他内容一样静态生成。写作与服务
你的编辑不必是你已发布的页面 这是最需要早期做好、最容易出错的部分。编辑器是一个创作环境。发布页面可以使用你自己的 Next.js 渲染架构——静态生成、流式传输、重新验证、边缘缓存,所有这些。
著作
GrapesJS ↓ Project data ↓ Database 服务
Publish ↓ HTML + CSS ↓ Public page 两条管道,一条伪影穿过它们。只有第一条需要GrapesJS。 app/p/[slug]/page.tsx tsx
// The public route. GrapesJS is not imported here, so the editor
// bundle never reaches a visitor.
export const revalidate = 3600;
export default async function PublishedPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const res = await fetch(`${process.env.API_URL}/published/${slug}`, {
next: { revalidate: 3600 },
});
// Already sanitised on the way in — see the publish route below.
const { html, css } = await res.json();
return (
<>
<style dangerouslySetInnerHTML={{ __html: css }} />
<div dangerouslySetInnerHTML={{ __html: html }} />
</>
);
}GrapesJS 永远不必在访客打开的页面上运行。把编辑器发给少数编辑者,其他人则发普通的 HTML 和 CSS。
持久性
在你的Next.js应用中保存GrapesJS项目 GrapesJS 提供本地和远程存储适配器,并允许你注册自己的适配器。定制适配器通常是正确的选择,因为它会把所有读写都放在你控制且可认证的路由后面。
GrapesJS ↓ → Project data ↓ → Next.js API ↓ → Your backend ↓ → Database 编辑器从不和你的数据库通信。它只和一条路由通信,而这条路由又和你实际使用的路径通信。 components/GrapesEditor.tsx — 存储 ts
const editor = grapesjs.init({
container: containerRef.current,
storageManager: {
type: 'nextjs-api',
autosave: true,
stepsBeforeSave: 5,
},
plugins: [
// Registered as a plugin so the adapter exists before the first load.
(editor) => {
editor.Storage.add('nextjs-api', {
async load() {
const res = await fetch(`/api/projects/${projectId}`);
return res.ok ? res.json() : {};
},
async store(project) {
await fetch(`/api/projects/${projectId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(project),
});
},
});
},
],
});app/api/projects/[projectId]/route.ts ts
import { NextResponse } from 'next/server';
// Your own persistence layer — Postgres, MySQL, Mongo, S3, a headless CMS.
// GrapesJS never talks to it; it only ever talks to this route.
import { loadProject, saveProject } from '@/lib/projects';
import { requireProjectAccess } from '@/lib/auth';
export async function GET(
_request: Request,
{ params }: { params: Promise<{ projectId: string }> },
) {
const { projectId } = await params;
await requireProjectAccess(projectId);
return NextResponse.json(await loadProject(projectId));
}
export async function PUT(
request: Request,
{ params }: { params: Promise<{ projectId: string }> },
) {
const { projectId } = await params;
await requireProjectAccess(projectId);
const project = await request.json();
if (typeof project !== 'object' || project === null) {
return NextResponse.json({ error: 'Invalid project' }, { status: 400 });
}
await saveProject(projectId, project);
return NextResponse.json({ ok: true });
} 加载:适配器的load()在初始状态下运行,恢复用户最后处理的项目。 保存:store() 以 JSON 的身份接收整个项目。返回被拒绝的承诺,以在编辑器中发现失败。 自动保存:用stepsBeforeSave自动保存批量修改,这样你就不会每次按键都写入。 草稿:将草稿项目和发布的成果分列,避免编辑改变访客看到的内容。 版本管理:项目数据是 JSON 文档——仅附录版本表只需一次插入,并购买回滚费用。 发布:一个独立的端点,有自己的权限检查,而不是在保存路径上设置标志。示例:使用 Supabase 存储项目 自由/projects.ts ts
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // server only — never NEXT_PUBLIC_
);
export async function loadProject(projectId: string) {
const { data } = await supabase
.from('projects')
.select('project')
.eq('id', projectId)
.single();
return data?.project ?? {};
}
export async function saveProject(projectId: string, project: unknown) {
await supabase
.from('projects')
.upsert({ id: projectId, project, updated_at: new Date().toISOString() });
}这实现了 loadProject 和 saveProject 边界,路径上方导入。将主体替换为 Prisma、Drizzle、Mongo、DynamoDB 或对现有后端的 REST 调用,页面上的其他内容不变。
示例:将项目存储连接到 Vercel/Next.js 部署 发布写入HTML,CSS则读取你的公用路由。重新验证、缓存和渲染策略仍是Next.js的常规关注点——编辑器不在请求路径中。
查看已发布页面的路由 你可以把GrapesJS连接到任何后端或数据库。编辑器里根本不知道你选了哪个。
扩展画布
为您的Next.js页面构建器创建自定义组件 开箱即用,画布为用户提供了通用的HTML。自定义组件类型是编辑器开始生成产品标记的方式——也就是你Next.js应用已经渲染的部分,只包含你决定暴露的属性。
注册组件类型 ts
// Your design system, expressed as something users can drop in,
// edit and style — but not break.
editor.Components.addType('pricing-table', {
isComponent: (el) =>
el.tagName === 'SECTION' && el.classList.contains('pricing'),
model: {
defaults: {
tagName: 'section',
classes: ['pricing'],
// Structure the user cannot accidentally dismantle.
droppable: false,
// Fields exposed in the Settings panel.
traits: [
{ name: 'plan', label: 'Plan name', type: 'text' },
{ name: 'price', label: 'Price', type: 'text' },
{ name: 'billing', label: 'Billing period', type: 'select',
options: [
{ id: 'month', label: 'Monthly' },
{ id: 'year', label: 'Yearly' },
] },
],
components: [
{ type: 'text', tagName: 'h3', content: 'Starter' },
{ type: 'text', tagName: 'p', content: '$19 / month' },
{ type: 'link', content: 'Choose plan', attributes: { href: '#' } },
],
},
},
});常见的自定义组件
Hero — 标题、副标题、背景及一个行动号召 定价表——计划、价格和计费期间作为可编辑字段 产品卡——绑定到真实的产品ID,而非打字文字 特征网格——一种固定布局,子节点数量可变 表单——用户可以安排的字段,连接到你的提交端点 导航——从你的路线中拉取,避免链接过时 应用组件——图表、预订小部件,或者你SaaS已经发货的设备自定义组件是编辑器匹配你的设计系统和商业模式的方式。droppable: false 阻止用户拆解结构;traits 则决定他们可以更改哪些属性。
构建一个模块库 块是用户拖拽时出现在面板上的。一旦组件类型存在,注册块只需几行。
注册区块 ts
// A block is what the user drags. A component is what it becomes.
editor.Blocks.add('pricing-table', {
label: 'Pricing table',
category: 'Marketing',
media: '<svg viewBox="0 0 24 24" width="24" height="24">' +
'<rect x="3" y="4" width="18" height="16" rx="2" fill="currentColor"/></svg>',
content: { type: 'pricing-table' },
});方块不是组件 块是一个起点——一个标签、一个图标以及它插入的内容。它存在于面板中,而非页面中。 组件是编辑器内部的一个结构化元素,拥有自己的模型、traits 和规则。一旦插入,组件就存在于页面中。 一个组件类型可以支持多个模块(两列和三列定价表),一个模块可以插入整棵组件树。可复用区块 —— Hero、功能区、客户评价、页脚 营销块——定价表、标志墙、行动号召 布局块——列、间隔、容器 表格——联系、注册、多步骤 电商区块——产品网格、购物车摘要、结账提示 应用块——无论你的产品做什么,页面应该能显示出来 管理图片和资源 Asset Manager负责选择和插入。文件存放在哪里、谁可以上传以及允许通过什么,都是你合同上的规定。
Asset Manager — 自定义上传 ts
assetManager: {
// The library the user already has, loaded from your backend.
assets: initialAssets,
uploadFile: async (event) => {
const files = event.dataTransfer
? event.dataTransfer.files
: (event.target as HTMLInputElement).files;
if (!files) return;
const body = new FormData();
Array.from(files).forEach((file) => body.append('files', file));
// Your route authenticates the user and validates type and size
// before anything is written to storage.
const res = await fetch('/api/assets', { method: 'POST', body });
const { urls } = await res.json();
editor.AssetManager.add(urls);
},
},GrapesJS Asset Manager ↓ Next.js API ↓ Your storage ↓ CDN 上传的邮件只需离开浏览器一次,进入你自己写的路由。 通过你自己的路由处理程序上传,这样会话 cookie、速率限制和审计日志都能生效。 允许已经托管图片的团队使用外部网址。 预加载用户现有库,这样拣选器会在他们的资产上打开,而不是在空面板上。 验证服务器上的MIME类型和大小。编辑器的文件输入是建议,不是控制。 从CDN服务器提供,并将CDN URL存储在项目中,这样发布的页面就不会到达你的媒体来源。 分页大库——资产面板会很乐意渲染一万个缩略图。数据模型
项目数据与HTML和CSS 编辑器生成两种不同的内容,且它们不能互换。存储错误的代码是将“编辑您的页面”变成“重新开始”的错误。
项目数据 一份描述组件、样式、页面和资源的JSON文档。这是可编辑的源代码——唯一能完全恢复用户离开时编辑会话的伪物。
HTML 画布渲染的内容。非常适合为访客服务,但作为源头有损:组件类型、traits 和编辑器状态都消失了。
CSS 编辑器生成的样式表,包括每个断点的规则。与HTML一起运行,发布时重新生成。
这两件文物均来自一位编辑 ts
// Project data — the editable source. This is what you store.
const project = editor.getProjectData();
// HTML and CSS — the rendered output. Generate this when you publish.
const html = editor.getHtml();
const css = editor.getCss();
// Reopening an editing session needs the project data, not the HTML:
editor.loadProjectData(project);剪辑
User edits ↓ Project data ↓ Database 渲染
Project data ↓ HTML + CSS ↓ Preview / publish 存储项目数据以便编辑。需要发布或渲染内容时生成HTML/CSS。
工作流程
从草稿到已发布页面
1
项目数据中会不断积累变化。访客看到的任何东西都没有移动。
2
你的存储适配器会把项目的JSON通过去退回或计步数发送到你的API。
3
一条私有路由,从草稿项目生成 HTML 和 CSS——发布时使用的代码路径相同,但无需写入。
4
这是可选的,值得在第一次有人发布破损定价表时补充。
5
生成HTML和CSS,消毒,存储为新版本,然后用活弹头对准它。
6
如果每次发布都是插入而非更新,那就太便宜了。以后添加也太贵了。
应用/API/发布/route.ts ts
import { NextResponse } from 'next/server';
import sanitizeHtml from 'sanitize-html';
import { publishPage } from '@/lib/projects';
import { requireProjectAccess } from '@/lib/auth';
export async function POST(request: Request) {
const { projectId, html, css } = await request.json();
// Permissions are enforced here, not by hiding a button in the editor.
const user = await requireProjectAccess(projectId);
if (typeof html !== 'string' || typeof css !== 'string') {
return NextResponse.json({ error: 'Invalid payload' }, { status: 400 });
}
// Sanitise on the way in, once — not on every render.
const page = await publishPage({
projectId,
html: sanitizeHtml(html),
css,
publishedBy: user.id,
});
return NextResponse.json({ url: `/p/${page.slug}` });
}目录
用插件扩展你的Next.js页面构建器 GrapesJS 提供编辑器引擎。插件增加了特定产品所需的专用功能——而页面构建器在第一周后仍然缺乏的大部分功能已经存在。
浏览GrapesJS插件 市场
Next.js 构建插件 GJS.Market目录中的实时列表,按Next.js团队在编辑器安装后提出的问题分组。
React 与 Next.js 集成 为从React组件安装GrapesJS的队伍提供起始和UI层。
浏览类别 存储与恢复 从“用户编辑了某件事”到“明天它还在”之间的各种情况。
浏览类别 区块、页面与模板 用户拖拽了哪些页面,页面在哪里,编辑器打开了什么。
浏览类别 资源、导出与邮件 媒体处理以及页面构建器发展的输出格式。
浏览类别 瞄准镜控制
不要自己做所有功能 插件并不能消除开发工作。它们改变了工作类型,这种差异会随着产品生命周期的增长而累积。
自己编写 自己组装吧 你写的每一项能力都会成为永久的一行。
来自插件市场 安装插件 有人已经解决了你编辑器的通用一半问题。
编辑器用GrapesJS核心,只添加产品真正需要的功能。
判决
从零开始构建一个Next.js页面构建器,还是用GrapesJS? 根据能力逐一说明你要写的内容。“可扩展”意味着库提供接口和默认值,并期望你指向自己的后端。
能力 打造自己 GrapesJS 画布 自己实现 已内置 拖拽 自己实现 已内置 组成部分 自己实现 已内置 方块 自己实现 已内置 造型 自己实现 已内置 响应式剪辑 自己实现 已内置 资产 自己实现 可扩展 存储 自己实现 可扩展 指挥 自己实现 已内置 插件 自建生态 插件架构
已在2026-09-02上与已发布的GrapesJS API进行验证。
Next.js 提供应用框架。GrapesJS 提供视觉编辑引擎。
商业产品
用Next.js构建一个SaaS页面构建器 SaaS 页面构建器是你的应用程序,里面有一个编辑器。 几乎所有让它成为企业的东西——账户、团队、限额、计费、域名——你本来就会做Next.js的工作。编辑器就是其中一条路。
NEXT.JS
├── Authentication ├── Organizations ├── Users ├── Permissions ├── Billing └── Editor ↓
GRAPESJS
↓
多用户:多人编辑不同的项目,最终是同一个项目。 权限:谁可以编辑,谁可以发布,谁只能查看。服务器端强制执行。 组织和团队:项目属于账户,而非个人。 计费:计划限制,以项目数量、页数、座位数或已发布域名数表示。 发布:你的产品负责发布内容的步骤。 白标:你的分镜、图标、颜色——编辑不应该显得死板。编辑器只是你申请中的一条路径。围绕它的所有内容才是你真正卖的东西。
在你发货之前
性能与安全考虑 这两份清单值得在第一个真正用户打开编辑器之前阅读,而不是之后。
性能 让编辑别走捷径 GrapesJS 是一个相当庞大的客户端库。这在创作方面没问题,其他地方价格较高。
✓ 用 Lazy load GrapesJS 让它的块通过编辑器路由获取,而不是应用壳。✓ 只在真正需要编辑器时初始化——而不是仅仅链接到编辑器的仪表盘上。✓ 保持编辑器实例在参考中,不要进入React状态。✓ 避免重新渲染拥有编辑器的组件;GrapesJS 的生命周期不是 React 的。✓ 懒惰加载重插件,而不是在初始化时注册所有插件。✓ 用分页方式处理大型资源集合,而不是把整个库加载到面板里。编辑器是一个创作工具。你通常不需要对每个发布页面都配备完整的编辑器运行时间。
安全性 将编辑器输出视为用户输入 因为它确实如此。画布生成的任何内容都是通过你无法控制的浏览器通过网络到达的。
✓ 在存储前,无论在哪里以标记形式渲染,都要对生成或用户提供的 HTML 进行净化处理。✓ 在服务器端验证上传的资源:MIME类型、大小以及你愿意回馈的金额。✓ 认证所有存储端点。URL中的项目ID不等同于授权。✓ 服务器端强制执行权限;隐藏的发布按钮是UI的偏好设置,而不是控制。✓ 在写入项目数据前验证其形状,渲染前也要再验证一次。✓ 保护发布端点不被保存——它们有不同的爆炸半径。✓ 为渲染发布标记的路由设置Content Security Policy。永远不要相信客户端编辑器的状态。编辑器是作者的便利,而非界限。
故障排除
常见的Next.js + GrapesJS错误 几乎所有报告的该栈集成失败都属于以下七种。
✕ 错误
在Server Component内部初始化GrapesJS Server Components 没有钩子、没有效果,也没有 DOM。导入可能会解决,但编辑器没有地方可以挂载。
✓ 修复
用Client Component——把'use client'放在拥有编辑器的模块顶部。
✕ 错误
在DOM存在之前初始化 在渲染时调用init(),或者对仍为空的引用调用失败,因为容器元素尚未被创建。
✓ 修复
安装后初始化,在useEffect内部,并在ref出现时进行守卫。
✕ 错误
在SSR期间运行GrapesJS 任何访问 init() 的调用都会抛出 ReferenceError: 文档未定义。仅导入本身无害。
✓ 修复
编辑器初始化保持在客户端。一个效果就足够了;动态导入是优化,不是修复。
✕ 错误
将编辑器实例置于React状态 编辑器是一个大且可变的对象,不断变化。将其存储在状态中,调度会重新渲染,但这些都没有任何效果。
✓ 修复
使用参考文献。保留UI实际渲染的内容状态,比如保存指示器。
✕ 错误
不必要的重新渲染编辑器 如果是父重渲染,重建道具或重新安装容器,则会拆除并重建整个画布。
✓ 修复
保持GrapesJS生命周期与正常的React渲染分开——先挂载一次,然后通过自己的API驱动。
✕ 错误
仅存储HTML HTML 是输出,不是源。从 HTML 重新打开项目会丢失组件类型、traits 和编辑器状态,所以下一次编辑从扁平化的页面开始。
✓ 修复
如果用户需要继续编辑,请持久化项目数据。发布时生成 HTML。
✕ 错误
在每个公共页面上运行GrapesJS 将编辑器运行时发给无法编辑内容的访客,需要支付捆绑包尺寸、内存和Core Web Vitals费用,且无回报。
✓ 修复
将编辑器运行时间与已发布内容分离——将已发布页面渲染为纯 HTML 和 CSS。
路线图
先从小处开始,然后逐步扩大 三个示波器,每个相辅相成。大多数队伍在第一个导向镜上会超标,第三个则会被惊讶。
1 第一周
MVP 在围绕它做任何东西之前,先证明编辑体验是正确的。
GrapesJS 安装在客户端组件中 几个基础方块 本地或单项目存储 简单发布到一条路由从这里开始 2 首批真正用户
生产环境 所有让演示变成客户可以依赖的东西的一切。
每个编辑器路由的认证 后端存储放在你自己的API后面 资源上传与媒体库 与你的设计系统匹配的定制组件 服务器端强制执行的权限 版本管理与发布工作流程详见存储部分 3 商业
SaaS 就是围绕编辑器的产品层,这才是你实际收费的部分。
组织与团队 计费和套餐限额 白标编辑器 UI 为用户精心策划的插件集 模板库 已发布页面的分析SaaS 页面构建器 从原型到商业产品的路径是通过你的应用程序,而不是编辑器。
FAQ
常见问题解答 我可以和Next.js一起使用吗? 是的。GrapesJS 是一个客户端库,可以挂载到 DOM 元素中,因此它可以运行在任何 React 应用程序中,包括 Next.js。安装 grapesjs,在 Client Component 内初始化效果,卸载时销毁它。
GrapesJS能和Next.js App Router一起使用吗? 是的,App Router 是推荐的集成。把 'use client' 放在拥有编辑器的组件顶部,然后在 useEffect 中初始化 GrapesJS。路由文件本身可以保持 Server Component。
GrapesJS和React Server Components兼容吗? 它与它们协同工作。编辑器本身不能是 Server Component——它需要钩子、效果和 DOM。Server Components 负责认证、数据获取和页面壳,然后将可序列化的道具传递给拥有编辑器的 Client Component。
GrapesJS 支持 Next.js SSR 吗? 包装编辑器的组件可以实现服务器渲染;编辑器本身必须在浏览器中初始化。grapesjs.init() 读取文档并插入 Node 运行时,因此将调用保留在一个效果中。已发布的页面完全不需要编辑器运行时,可以静态生成。
为什么GrapesJS需要Client Component? 因为它直接与 DOM 交互——测量元素、连接监听器并渲染 iframe 画布——而且需要 React 钩子来管理生命周期。Server Component 中没有这两种功能。
我如何在Next.js中初始化GrapesJS? 创建一个带有容器 div 参考的 Client Component,在 useEffect 内部调用 grapesjs.init({ container }),依赖数组为空,将返回的编辑器放在第二个参考中,并在清理函数中调用 editor.destroy()。
我应该用dynamic(..., { ssr: false })吗? 在 Pages Router 中,是的——这是跳过预渲染的标准方式。在 App Router 中,预渲染是可选的,不能在 Server Component 中使用:Next.js 拒绝 ssr: false,要求你把它移到 Client Component 中。在 App Router 中使用它,只是为了避免编辑器块进入初始负载。
我如何在Next.js中保存GrapesJS项目? 用editor.Storage.add()注册一个自定义存储适配器,其load()和store()调用Next.js路由处理程序。路由认证请求,并在数据库中读写项目JSON。用stepsBeforeSave开启自动保存,这样写入会被批量处理。
我可以和GrapesJS、Next.js一起使用吗? 是的,这个页面上有一个示例。Supabase 是加载/保存边界的一个实现——Postgres、MySQL、Mongo、S3 或现有的 REST API 工作方式相同,因为编辑器只会与你的路由通信。
我能用Next.js和GrapesJS构建一个SaaS页面构建器吗? 是的。GrapesJS 是 BSD-3-Clause 授权且自托管的,没有按座位付费,也没有托管服务,所以你可以将其嵌入商业产品中。工作主要是 SaaS 层——组织、权限、计费、发布——这也是普通的 Next.js 工作。
我可以创建自定义方块吗? 是的。editor.Blocks.add() 会注册一个带有标签、类别、图标和插入内容的块。块可以插入原始的 HTML,或者实例化你自己的组件类型。
我可以创建自定义组件吗? 是的。editor.Components.addType() 定义了一个组件类型,拥有自己的模型、traits、规则和子结构——这是一种在画布内暴露设计系统同时防止用户破坏它的机制。
我可以导出HTML和CSS吗? 是的。editor.getHtml() 和 editor.getCss() 随时返回画布输出,getProjectData() 返回可编辑的 JSON 源。存储项目数据以便编辑;发布时生成 HTML 和 CSS。
我可以在Next.js上使用GrapesJS插件吗? 是的。插件是接收编辑器实例的函数,所以它们在 Next.js 中和其他地方一样注册——通过 init 时的插件选项,或者通过你的 onEditor 处理器调用编辑器 API。
我可以用 Next.js 构建一个 CMS 编辑器吗? 是的,而且这是最常见的用途之一。将GrapesJS项目数据映射到你现有的内容模型上,限制块集为Next.js模板能渲染的组件,并让编辑者在不触及仓库的情况下可视化工作。
我可以自架Next.js页面构建器吗? 是的。GrapesJS 是一个 npm 包,没有托管后端、许可服务器或遥测,所以整个构建器——编辑器、存储和发布页面——运行在你的 Next.js 应用运行的任何地方。
用GrapesJS构建你的Next.js页面构建器 应用架构使用Next.js,视觉编辑引擎使用GrapesJS。从核心编辑器开始,扩展产品,加入所需的插件、模块和集成。
用Next.js构建应用。用GrapesJS添加视觉编辑。用GJS.Market扩展。