初学者
核心循环:让剪辑师出现在屏幕上,把东西放进去。
一步步学习GrapesJS。构建你的第一个可视化编辑器,添加模块和自定义组件,配置样式和资源,保存项目,导出HTML/CSS,安装插件,并将GrapesJS与React、Vue、Angular或Next.js集成。
GrapesJS 核心以 BSD-3-Clause 许可证发布;官方的 React 包装 @grapesjs/react 是 MIT。两者都允许商业使用。以下所有示例均与 GrapesJS 0.23.6 进行了核对。
完成本教程后,你将拥有一个可运行的拖放可视化编辑器,可以创建页面、编辑组件、管理样式和资源、保存项目,并导出生成的 HTML 和 CSS。
非常少。如果你能手写一页,你就能跟着写。
你应该能识别标签、类和CSS属性。GrapesJS编辑HTML和CSS——没有更特殊的事情。
足够读取一个对象的字面和一个函数。这里的每个示例都是纯JavaScript。
只有在你从 npm 安装时才会有。第一步的 CDN 路线只需要一个文本编辑器和一个浏览器。
你不需要任何之前的GrapesJS经验,也不需要框架。React、Vue、Angular和Next.js在第11步会涵盖,核心内容通过后。
如果你有任何构建步骤,这就是应该采取的路径。它会把编辑器和样式表安装到你的项目中;运行时不会从第三方主机抓取任何东西。
npm install grapesjs完全不用构建工具:在HTML文件里放两个标签,你就有编辑器了。非常适合做初次展示或原型。无论你发货什么,都钉住一个完全相同的版本,而不是跟踪最新版本。
<link
rel="stylesheet"
href="https://unpkg.com/grapesjs/dist/css/grapes.min.css"
/>
<script src="https://unpkg.com/grapesjs"></script>
<div id="gjs"></div>
<script>
// The UMD build puts the library on window.grapesjs
const editor = grapesjs.init({ container: '#gjs' });
</script>编辑器核心
画布、组件树、拖放、撤销/重做、面板以及块、样式、资源、traits、图层和存储管理器。
样式表
grapesjs/dist/css/grapes.min.css — 编辑器自己的Chrome。没有它,编辑器看起来很坏。
没有任何阻碍
核心会附带一个空的块调色板。熟悉的列/文本/图片块来自插件,这是步骤3。
没有后端
没有账户,没有数据库,没有托管。GrapesJS 运行在浏览器中,将数据传递给你的应用,这是第7步。
它是客户端组件。你的应用程序认证用户,决定打开哪个项目,将编辑器挂载到DOM元素中,保存后会接收项目数据。编辑器上下的所有内容都是你的。
<!-- The editor takes over this element completely.
Do not render anything inside it yourself. -->
<div id="gjs"></div>import grapesjs from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
const editor = grapesjs.init({
// Where the editor mounts: a selector or an HTMLElement.
container: '#gjs',
height: '100vh',
width: 'auto',
// Do not adopt the markup already inside #gjs...
fromElement: false,
// ...load this instead. Strings are parsed into components.
components: `
<section class="hero">
<h1>Hello GrapesJS</h1>
<p>Drag a block from the panel on the right.</p>
</section>`,
style: `
.hero { padding: 64px 32px; font-family: system-ui, sans-serif; }
.hero h1 { margin: 0 0 12px; font-size: 40px; }`,
// Storage is ON by default and writes to localStorage.
// Turn it off until you have decided where projects really live.
storageManager: false,
});区块
一个调色板条目。它只存在于面板中,包含一个制作配方。它有标签、分类、图标和内容。
组成部分
画布中的一个节点。它有一个类型、属性、样式、子节点,这些节点会被导出和保存。
一个块是用户拖入画布的。一旦丢弃,它就会在编辑器内创建组件。一个块可以创建完整的组件子树——而丢弃两个相同的块则会形成两个独立的子树。
// A Block is a palette entry. Dropping it creates Components.
editor.Blocks.add('hero-section', {
label: 'Hero',
category: 'Sections',
// Shown in the palette. Any HTML string works; an inline SVG keeps it sharp.
media: '<svg viewBox="0 0 24 24" width="22"><rect x="3" y="5" width="18" height="6" rx="1" fill="currentColor"/><rect x="3" y="13" width="11" height="3" rx="1" fill="currentColor" opacity=".5"/></svg>',
content: `
<section class="hero">
<h1>Headline</h1>
<p>Supporting copy.</p>
<a href="#" class="btn">Call to action</a>
</section>`,
});
// The same block, expressed as a component definition instead of HTML.
// Use this form once you have your own component types (step 10).
editor.Blocks.add('product-card', {
label: 'Product card',
category: 'Commerce',
content: { type: 'product-card' },
});这几乎让所有人都感到惊讶。开箱即用的GrapesJS编辑器有一个空白调色板——每个截图中出现的熟悉“1列/2列/文本/图片”集合,来自grapesjs-blocks-basic或某个预设。你要么像上面一样写自己的块,要么添加预设。
import grapesjs, { usePlugin } from 'grapesjs';
import blocksBasic from 'grapesjs-blocks-basic';
// The core ships no blocks at all. The familiar
// "1 column / 2 columns / text / image" palette is a plugin.
grapesjs.init({
container: '#gjs',
plugins: [usePlugin(blocksBasic, { flexGrid: true })],
});典型子树
// Every node in the canvas is a Component, and the canvas is a tree.
const wrapper = editor.getWrapper();
wrapper.components().forEach((component) => {
console.log(
component.get('type'), // 'text' | 'image' | 'link' | your own type
component.getName(), // label shown in the Layer manager
component.components().length // number of children
);
});
// React to what the user selects — the hook most custom UI hangs off.
editor.on('component:selected', (component) => {
console.log('selected', component.getId(), component.get('type'));
});// Traits are the settings panel for a component.
// By default a trait writes an HTML attribute.
editor.Components.addType('cta-button', {
extend: 'link',
model: {
defaults: {
name: 'CTA button',
attributes: { class: 'btn' },
components: 'Call to action',
traits: [
{ name: 'href', label: 'Link' },
{ name: 'title', label: 'Title' },
{
type: 'select',
name: 'target',
label: 'Opens in',
options: [
{ id: '', name: 'Same tab' },
{ id: '_blank', name: 'New tab' },
],
},
],
},
},
});grapesjs.init({
container: '#gjs',
styleManager: {
// Sectors are the collapsible groups in the right-hand panel.
// Listing them yourself is how you stop the editor offering
// 100+ CSS properties to a non-technical author.
sectors: [
{
id: 'typography',
name: 'Typography',
open: true,
properties: [
'font-family',
'font-size',
'font-weight',
'line-height',
'color',
'text-align',
],
},
{ id: 'spacing', name: 'Spacing', properties: ['margin', 'padding'] },
{
id: 'dimension',
name: 'Dimension',
properties: ['width', 'max-width', 'height'],
},
{
id: 'decorations',
name: 'Decorations',
properties: ['background-color', 'border-radius', 'border', 'box-shadow'],
},
],
},
});组件结构
存在什么,包含什么,可以添加或删除什么。通过组件类型 droppable、draggable 和 removable 控制。
组件样式
组件可能长什么样。通过Style Manager配置控制,组件本身则用stylable / unstylable控制。
// Structure and styling are separate concerns. A component can accept
// children while refusing to be restyled beyond a fixed allowance.
editor.Components.addType('brand-heading', {
extend: 'text',
model: {
defaults: {
name: 'Brand heading',
// Only these properties reach the Style manager for this component.
stylable: ['color', 'text-align'],
// Everything else stays on the class in your own stylesheet.
attributes: { class: 'brand-h2' },
},
},
});grapesjs.init({
container: '#gjs',
assetManager: {
// Seed the panel with images you already host.
assets: [
'https://cdn.example.com/hero.jpg',
{ src: 'https://cdn.example.com/team.jpg', name: 'Team', category: 'People' },
],
// Your upload endpoint. Set `upload: false` to disable uploading entirely.
upload: 'https://api.example.com/uploads',
uploadName: 'files',
headers: { Authorization: 'Bearer <token>' },
multiUpload: true,
// Add the response's assets to the panel automatically. Your endpoint must
// answer with { data: [ ...assets ] }.
autoAdd: true,
},
});// Full control: upload wherever you like, then hand the URLs back.
grapesjs.init({
container: '#gjs',
assetManager: {
async uploadFile(event) {
const files = event.dataTransfer
? event.dataTransfer.files
: event.target.files;
const urls = await uploadToYourStorage(files); // S3, R2, Cloudinary…
editor.AssetManager.add(urls);
},
},
});
// Without `upload` or `uploadFile`, dropped images are embedded as base64
// straight into the project — convenient in a demo, painful in production.在没有上传和 uploadFile 配置的情况下,GrapesJS 会将丢弃的图像嵌入项目中,格式为 base64。它即时生效,并且会让存储的项目膨胀,直到加载缓慢且移动成本高昂。在任何人开始制作内容之前,先将 Asset Manager 连接到真实存储。
grapesjs.init({
container: '#gjs',
storageManager: {
type: 'remote',
autosave: true,
autoload: true,
// Batch changes: save after N edits rather than after every keystroke.
stepsBeforeSave: 5,
options: {
remote: {
urlLoad: '/api/projects/42',
urlStore: '/api/projects/42',
headers: { 'X-CSRF-Token': csrfToken },
credentials: 'include',
// Shape the request body to match your API…
onStore: (data) => ({ project: data }),
// …and pull the project back out of your response.
onLoad: (result) => result.project,
},
},
},
});// When `remote` does not fit — GraphQL, a queue, an offline-first cache —
// register a storage of your own and select it by name.
editor.Storage.add('my-api', {
async load() {
const res = await fetch('/api/projects/42');
const { project } = await res.json();
return project; // the object you previously stored
},
async store(data) {
await fetch('/api/projects/42', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project: data }),
});
},
});
// storageManager: { type: 'my-api' }
// You can also drive it by hand, with no storage configured at all:
const project = editor.getProjectData(); // plain JSON — store it anywhere
editor.loadProjectData(project); // and put it backGrapesJS 提供编辑器和项目数据层,但你的应用决定生产数据存储在哪里。哪个用户拥有项目,属于哪个租户,谁可以打开项目,保留多少版本,何时备份——这些都不在库里,也不应该存在。
const html = editor.getHtml();
const css = editor.getCss();
// Two things surprise everyone on their first export:
//
// 1. getHtml() returns the canvas wrapped in <body> … </body>.
// Strip or template around it before you save a fragment.
// 2. getCss() includes GrapesJS's own canvas reset unless you opt out:
const pageCss = editor.getCss({ avoidProtected: true });
// Export one branch instead of the whole page:
const selected = editor.getSelected();
const partial = editor.getHtml({ component: selected });
// The editable project — NOT the same thing as the exported page.
const project = editor.getProjectData();getHtml() 包裹在 <body> 中
包装组件作为主体元素导出。如果你保存片段,可以围绕它模板或剥离它——不要假设你能拿回一个裸部分。
getCss() 包含编辑器的重置功能
GrapesJS 附带了一个小的受保护样式表。通过 avoidProtected:当你只想要阅读器实际创建的 CSS 时,是正确的。
典型的出版流程
getHtml() 和 getCss() 给你访问者看到的内容。getProjectData() 给你作者可以继续编辑的内容。存储项目;从它重新生成页面。只存储 HTML 意味着下一次编辑是从解析标记开始,而不是从作者构建的文档开始。
import grapesjs, { usePlugin } from 'grapesjs';
import blocksBasic from 'grapesjs-blocks-basic';
import forms from 'grapesjs-plugin-forms';
grapesjs.init({
container: '#gjs',
plugins: [
usePlugin(blocksBasic, { flexGrid: true }),
usePlugin(forms, {}),
],
});// A plugin is just a function that receives the editor.
// Anything you can do at init you can do inside one.
export default function dividerPlugin(editor, options = {}) {
const category = options.category ?? 'Basic';
editor.Blocks.add('divider', {
label: 'Divider',
category,
content: '<hr class="divider" />',
});
editor.Commands.add('clear-canvas', {
run: (ed) => ed.Components.clear(),
});
}
// Then: plugins: [usePlugin(dividerPlugin, { category: 'Layout' })]需要基础编辑器中没有的功能吗?探索通过GJS.Market提供的插件和扩展。以下是目录中的真实列表,按所属步骤分组。
这些是 GJS.Market 目录中的真实列表,按教程部分分组。这里没有任何东西替代核心——每个链接都填补了核心故意留下的缝隙。
产品卡,作为组件类型
editor.Components.addType('product-card', {
// Lets GrapesJS recognise the type when parsing saved HTML.
isComponent: (el) => el.dataset?.gjsType === 'product-card',
model: {
defaults: {
name: 'Product card',
attributes: { 'data-gjs-type': 'product-card', class: 'product-card' },
// Author-visible settings. `changeProp` writes to the model
// instead of to an HTML attribute.
traits: [
{ name: 'sku', label: 'SKU', changeProp: true },
{
type: 'checkbox',
name: 'showPrice',
label: 'Show price',
changeProp: true,
},
],
sku: '',
showPrice: true,
// Fixed structure: the author edits the parts, not the layout.
components: [
{ type: 'image', attributes: { class: 'product-card__image' } },
{ type: 'text', name: 'Name', components: 'Product name' },
{ type: 'text', name: 'Price', attributes: { class: 'product-card__price' }, components: '$0.00' },
{ type: 'cta-button', components: 'Add to cart' },
],
// Locks that make the card a card and not a free-form div.
droppable: false,
stylable: ['background-color', 'border-radius', 'box-shadow'],
},
init() {
this.on('change:showPrice', this.togglePrice);
},
togglePrice() {
const price = this.components().at(2);
price?.addStyle({ display: this.get('showPrice') ? 'block' : 'none' });
},
},
});SaaS 应用可以暴露产品特定组件——连接真实计划的定价表、绑定 SKU 的产品卡、预订小组件——而非通用的无限制 HTML 编辑器。作者获得的选择更少,结果更好,你的支持队列也不会看到有人用零散浮动点破坏页面。
步骤3、5和10结合成了你在GrapesJS中能做的最有用的事情:用“这七件事,只要做得好”,替代“一切皆有可能”。每个机制都是你已经遇到过的。
自定义区块
调色板就是菜单。如果没有放在架子上,没人能添加。
自定义组件
每个模块的结构固定,应该可编辑的部分标记为可编辑。
特征
作者获得的设置——标题、链接、变体——而不是原始的标记。
Style Manager 配置
扇区只列出系统实际允许的属性。
允许的风格
每个组件的stylable和unstylable,所以卡片可以变色但不会变成浮点。
可重复使用组件
共享的作品保持同步,而不是逐页复制。
模板
每页类型起始文档,所以没人会从空白画布开始。
定制UI
GrapesJS的面板是可以更换的。产品编辑器很少看起来像默认的。
你的SaaS设计系统
把GrapesJS从一个通用编辑器变成专门为你的产品设计的编辑器。
有两本指南进一步说明,分为两个方向:
GrapesJS 渲染成一个普通的 DOM 元素,因此“将其与框架集成”归结为一个问题:哪个生命周期钩子调用 init(),哪个调用 destroy()。下面的模式是 React;专门的指南涵盖了其余部分,包括真正针对框架的部分。
import { useEffect, useRef } from 'react';
import grapesjs, { type Editor } from 'grapesjs';
export function GjsEditor() {
const ref = useRef<HTMLDivElement>(null);
const editorRef = useRef<Editor | null>(null);
useEffect(() => {
if (!ref.current) return;
editorRef.current = grapesjs.init({
container: ref.current,
height: '100vh',
storageManager: false,
});
// GrapesJS owns this node now — React must never render into it again.
return () => {
editorRef.current?.destroy();
editorRef.current = null;
};
}, []);
return <div ref={ref} />;
}有一个需要注意的警告:GrapesJS 在模块加载时会触摸窗口,所以编辑器必须只在客户端导入。Next.js 指南正好涵盖了这一点。
制作编辑器通常需要的不仅仅是grapesjs.init()。并不是因为库不完整——而是编辑器是产品的一层,而其周围的层就是你的。
典型的生产堆栈
以下内容并非对GrapesJS的批评——它是一个编辑器框架,这里才是它该停下来的正确地点。在开始之前知道具体内容,是避免三周制作变成九个月制作的关键。
其中八个完全属于你。这就是作品的真实形态,无论你选哪个视觉编辑,这个形态都是一样的。
这些核心都是一样的。不同的是包围它的层次——而且每个层都有自己的指引。
这些都源自同一个地方:把剪辑师当作产品,而不是其中一层。
你最终会给调色板条目添加行为,却不明白为什么物品一旦放到画布上就什么都没有。
块只负责创建东西。所有行为——traits、锁、渲染、验证——都属于它创建的组件类型。
默认存储是写入localStorage。看起来就像一直保存到读者切换设备、清空浏览器,或者在两个标签页中打开同一个项目。
在有人制作内容之前,先确定项目的真实位置,并在确定之前将storageManager: false设置为假。
工作会默默地存在于你从未设计过的商店里,而一旦有多个项目存在,加载顺序就变得不可预测。
配置远程存储,注册自定义存储,或者存档并加载getProjectData()和loadProjectData()。
四十种几乎一模一样的类型,每种都有自己的traits,还有一个没人能导航的调色板。
优先选择带有traits的一种类型,而不是五种颜色差异的类型。扩展内置类型,而不是重建它们。
作者们追求浮动、绝对位置和13像素边距,每一页都离设计系统越来越远。
明确列出你的扇区,并每个组件使用 stylable / unstylable。控制更少,页面质量更好。
花了好几周时间寻找那些从未存在过的用户、角色、工作流程和发布功能。
先读第12步的责任分工。GrapesJS是编辑层;它周围的CMS是你的产品。
如果没有上传配置,图片会以base64形式嵌入,存储的项目会不断增长,直到加载缓慢、移动变得尴尬。
第一天就把Asset Manager接到真实存储上,即使那个存储是磁盘上的文件夹。
你有一个编辑器会保存,却没有回答“这怎么会变成访客可以打开的页面?”。
在构建编辑器之前,先从第8步草绘出流程。这通常会改变你存储的内容。
一个2000行的单一文件,用于注册块、类型、面板和命令,且不能分段重复使用或测试。
每个项目一个插件。他们会作曲,每个插件都可以被赋予选项。
页面在桌面画布上看起来很顺畅,手机上却会破碎,数百条桌面专用规则已经写好。
在构建过程中切换设备。样式是按设备写的,所以按一个宽度创作时,这个宽度会被烘焙进去。
第一次构建时最容易出错的五个问题,以及每项需要注意的事项。
通常是安装问题,而不是GrapesJS的问题。
检查项
涉及两种不同的样式表,且都不容易加载。
检查项
注意storage:error——GrapesJS报告的是故障,而不是吞噬它们。
检查项
几乎总是生命周期问题,不是GrapesJS的问题。
检查项
等到上面的编辑器说清楚后,大致按难度顺序去哪里。
核心循环:让剪辑师出现在屏幕上,把东西放进去。
让编辑器成为你的:自定义类型、插件、存储和工具。
编辑周围的一切:架构、租约、出版和制作。
大部分教程都是一天的工作。底层——存储、租赁、发布、与设计系统匹配的编辑器——是项目最长的地方。GJS.Market可以承担这部分。
先从核心编辑器开始,针对你的产品进行定制,需要更多功能时再加插件和集成扩展。
本页所有样本均与GrapesJS、0.23.6、2026-09-03对比。