Du gibst
Alles, worüber der Editor keine Meinung hat.
- Konten, Sitzungen und Berechtigungen
- Miet- und Tarifgrenzen
- Die Datenbank und ihre Migrationen
- Veröffentlichung, Domains und Hosting
- Abrechnung und Nutzung
PageKit — der selbst gehostete GrapesJS-Website-Builder, als Quellcode. Early Access sichern
Lerne, GrapesJS mit TypeScript zu verwenden: mit typisierten Editor-APIs und Events arbeiten, eigene Komponenten und Plugins erstellen, Storage anbinden und GrapesJS in React-, Next.js-, Vue- oder Angular-Anwendungen integrieren.
GrapesJS 0.23.6 veröffentlicht seine eigene Deklarationsdatei unter dist/index.d.ts, referenziert aus dem eigenen Typenfeld des Pakets. Die Installation von grapesjs liefert den Editor und seine Typen in einer Abhängigkeit, sodass das Importieren von Editor aus 'grapesjs' ohne zusätzliche Einrichtung und ohne zweites Paket aufgelöst wird.
Was die Typen abdecken, ist die API-Oberfläche des Editors – die Editor-Instanz, deren Manager, Komponenten, Blöcke, Ereignisse und Projektdaten. Sie beschreiben nicht das eigene Modell Ihres Produkts, und dieser Leitfaden dient hauptsächlich dazu, diese beiden Dinge voneinander zu trennen.
Installiere es und starteZwölf Schritte, in der Reihenfolge. Jeder führt zu dem Abschnitt, der ihn behandelt, sodass du dort starten kannst, wo dein Projekt tatsächlich steht.
Es gibt ein Paket. Die Typen kommen mit, sodass es keine zweite Installation und keinen @types Eintrag in deinem devDependencies gibt.
npm install grapesjs@types/grapesjs ist weder veraltet, ersetzt noch optional – es fehlt im npm-Register. Das Ausführen dieses Tutorials gibt einen 404 zurück, und wenn man es in einem älteren Tutorial sieht, ist das ein zuverlässiges Signal, dass der Rest dieses Tutorials auch älter ist als die mitgelieferten Typen.
# Don't. This package is not published — npm returns E404.
npm install --save-dev @types/grapesjsgrapesjs selbst ist ein Wert: Sie rufen grapesjs.init() auf. Editor, Component und Block sind Typen: Sie existieren nur während der Kompilierung. Wenn Sie sie mit import type markieren, wird das explizit und garantiert, dass der Import gelöscht wird und nicht in Ihr Bundle gezogen wird – was im Framework-Code am wichtigsten ist, wo ein zufälliger Laufzeit-Import des Editors ihn in ein Server-Rendering ziehen kann.
// Runtime import: the value you actually call.
import grapesjs from 'grapesjs';
// Type-only import: erased at compile time, ships nothing to the bundle.
import type { Editor, Component, Block } from 'grapesjs';
// Editor styles. Without them the canvas renders unstyled.
import 'grapesjs/dist/css/grapes.min.css';Du brauchst keine spezielle Konfiguration für GrapesJS. Du musst vier Optionen richtig eingestellt haben – der Rest deiner tsconfig kann das bleiben, was dein Projekt bereits nutzt.
{
"compilerOptions": {
// GrapesJS's bundled .d.ts uses const type parameters, a TypeScript 5.0
// feature. On 4.9 and below the file fails to *parse*, and every import
// from 'grapesjs' reports "Cannot find module".
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
// The editor manipulates real DOM nodes: container elements, iframes,
// drag events. Without the DOM lib none of that type-checks.
"lib": ["ES2020", "DOM", "DOM.Iterable"],
// strict is what makes the typings worth having. In particular
// strictNullChecks is what forces you to handle "editor not created yet",
// which is the single most common GrapesJS runtime crash.
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true
}
}Das sind die Namen, die Sie tatsächlich importieren werden. Jeder wurde mit der Deklarationsdatei von grapesjs 0.23.6 überprüft, anstatt von der Dokumentationsseite kopiert zu werden, die JavaScript API beschreibt und nicht immer die gleichen Namen verwendet.
| Typ | Repräsentiert | Gebräuchliche Verwendung |
|---|---|---|
EditorKlasse | Die Editor-Instanz | Alles: Lebenszyklus, Manager, Export, Events |
EditorConfigInterface | Das Objekt wurde an init weitergegeben | Aufbau einer Konfiguration abseits des Aufrufstandorts |
ComponentKlasse | Ein Knoten im Canvas-Baum | Lesen und Aktualisieren eines ausgewählten Elements |
ComponentDefinitionInterface | Eine deklarierte Komponente, keine Instanz | Kinder einer Komponente; Inhalt eines Blocks |
ComponentPropertiesInterface | Die Modellfelder einer Komponente | Die Defaults typisieren, die du an addType übergibst |
AddComponentTypeOptionsInterface | Das Argument addType nimmt tatsächlich | Registrierung eines benutzerdefinierten Komponententyps |
BlockKlasse | Ein Block im Block Manager | Der Rücklaufwert von Blocks.add |
BlockPropertiesInterface | Erklärung eines Blocks | Blöcke in einem eigenen Modul deklarieren |
TraitKlasse | Ein Feld im Einstellungsfeld | Benutzerdefinierte Eigenschaftstypen und Eigenschaftsbetreuer |
ProjectDataInterface | Der Editor hat JSON gespeichert | Speicher, Laden und Speichern; Ihre Datenbankspalte |
Plugin<T>Interface | Eine Plugin-Funktion mit typisierten Optionen | Ein Plugin typisieren, das du schreibst oder verwendest |
PluginOptionsTypalias | Die Einschränkung der Optionen eines Plugins | Generische Plugin-Helfer und Wrapper |
Alle diese sind mit Namen importierbar: import type { Editor, Component, BlockProperties } von 'grapesjs'. Nur grapesjs selbst benötigt einen Laufzeit-Import.
Die Manager-Klassen werden in der Datei deklariert, aber nie exportiert, sodass das Importieren nach Namen mit TS2614 fehlschlägt – ein verwirrender Fehler, da die Klasse offensichtlich existiert, wenn man danach sucht. Indexiere stattdessen in Editor: Der Rückgabetyp des Getters ist dieselbe Klasse, und der Alias ist versionsübergreifend stabil.
BlockManagerEditor['Blocks']StorageManagerEditor['Storage']ComponentManagerEditor['Components']grapesjs.init() gibt einen Editor zurück. Das Annotieren der Variablen ist optional – die Schlussfolgerung macht es bereits richtig – aber die Benennung des Typs ermöglicht es dir, den Editor über Modulgrenzen hinweg zu führen, ohne ihn auf irgendwelche zu erweitern.
import grapesjs from 'grapesjs';
import type { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
const editor: Editor = grapesjs.init({
container: '#gjs',
height: '100vh',
storageManager: false,
});
// Both return values are typed, and they are not the same shape:
// getHtml() always returns a string, getCss() can return undefined.
const html: string = editor.getHtml();
const css: string | undefined = editor.getCss();In einem Framework existiert der Editor beim ersten Rendern noch nicht und darf nach dem Unmount nicht mehr existieren. Die Variable, die ihn hält, als Editor | null zu typisieren zwingt den Compiler, an jeder Aufrufstelle nach beiden Momenten zu fragen. Sie als Editor zu typisieren und das per Assertion wegzuräumen verschiebt diese Frage in die Produktion.
import type { Editor } from 'grapesjs';
// Not `let editor: Editor` — before init there is no editor, and the type
// should say so. Every call site is then forced to handle the empty case.
let editor: Editor | null = null;
export function exportHtml(): string {
if (!editor) throw new Error('Editor is not initialised yet');
return editor.getHtml(); // narrowed to Editor here
}
export function destroy(): void {
editor?.destroy();
editor = null;
}Jeder Manager hängt am Editor ab – editor.Blocks, editor.Components, editor.Storage, editor.Commands – und jeder Getter wird typisiert, sodass Autocomplete von der Instanz nach unten funktioniert. Was du nicht tun kannst, ist, die Manager-Klassen nach Namen zu importieren; sie stattdessen über Editor alias.
import type { Editor } from 'grapesjs';
// These names are NOT exported from 'grapesjs' — importing them by name is a
// compile error. Index into Editor instead and you get the same classes.
type BlockManager = Editor['Blocks'];
type StorageManager = Editor['Storage'];
type ComponentManager = Editor['Components'];
export function countBlocks(blocks: BlockManager): number {
return blocks.getAll().length;
}editor.on ist generisch über dem Ereignisnamen, sodass die Callback-Signatur aus dem String, den du passierst, abgeleitet wird. In der Praxis bedeutet das, dass du fast nichts annotieren solltest – die Inferenz liefert dir die richtigen Parametertypen, und eine Annotation, die nicht übereinstimmt, ist ein Kompilierungsfehler und kein stilles Fehlmatch.
import type { Editor } from 'grapesjs';
export function wireEditorEvents(editor: Editor): void {
// No annotation needed. `component` is inferred as Component and
// `options` carries `action`, which tells add from move from clone.
editor.on('component:add', (component, options) => {
console.log(component.get('type'), options.action);
});
// A different event, a different payload — the callback signature changes
// with the event name, so a wrong parameter list is a compile error.
editor.on('component:selected', (component) => {
console.log(component.getId());
});
editor.on('storage:end:store', () => {
console.log('Project saved');
});
}Der Ereignisname ist eine offene String-Union, absichtlich: Plugins definieren ihre eigenen Kanäle, die sich ständig kompilieren müssen. Der Nachteil ist, dass ein Tippfehler im Kern-Ereignisnamen weiterhin gültig ist als TypeScript – der Rückruf fällt einfach auf eine lose Signatur zurück und wird nie ausgelöst. Wenn ein Event Handler auf mysteriöse Weise nichts tut, überprüfen Sie die Rechtschreibung, bevor Sie die API überprüfen.
import type { Editor } from 'grapesjs';
export function wireCustomEvents(editor: Editor): void {
// Your own events are allowed — the event name is a string union that stays
// open, so plugins can define their own channels.
editor.on('my-plugin:published', (...args: unknown[]) => {
console.log(args);
});
// Which is also the trade-off: this typo compiles. The callback simply falls
// back to (...args: any[]) and never fires.
// editor.on('component:selcted', (component) => { ... });
}Ein Komponententyp registriert neues Verhalten auf der Canvas: ein Tag, sein traits, was es als Kinder akzeptiert, wie es erkannt wird, wenn HTML wieder eingelesen wird. Hier findet die meiste Arbeit in benutzerdefinierten Editoren statt, und dort muss die Grenze zwischen den GrapesJS-Typen und deinen gezogen werden.
editor.Components.addType (Typ, Optionen) nimmt AddComponentTypeOptions – model, view, isComponent, extend – nicht ein ComponentDefinition. ComponentDefinition beschreibt einen deklarierten Knoten innerhalb eines Baums: die Kinder einer Komponente oder den Inhalt eines Blocks. Tutorials, die einen ComponentDefinition an addType weitergeben, zitieren eine ältere Form, und der Fehler, den man daraus erhält, ist nicht offensichtlich.
import type { Editor, Component } from 'grapesjs';
// YOUR domain model. GrapesJS knows nothing about it, and that is the point:
// this is the shape your API, your database and your React props agree on.
export interface HeroContent {
headline: string;
subheadline?: string;
ctaLabel: string;
ctaHref: string;
}
const HERO_DEFAULTS: HeroContent = {
headline: 'Your headline',
ctaLabel: 'Get started',
ctaHref: '#',
};
// addType takes AddComponentTypeOptions — model / view / isComponent — not a
// ComponentDefinition. Tutorials that pass a ComponentDefinition here are
// describing an API that no longer exists.
export function registerHero(editor: Editor): void {
editor.Components.addType('hero', {
isComponent: (el) => el.dataset?.gjsType === 'hero',
model: {
defaults: {
tagName: 'section',
droppable: false,
attributes: { 'data-gjs-type': 'hero' },
traits: [
{ type: 'text', name: 'headline', label: 'Headline' },
{ type: 'text', name: 'ctaLabel', label: 'Button label' },
{ type: 'text', name: 'ctaHref', label: 'Button link' },
],
...HERO_DEFAULTS,
},
},
});
}
// The bridge back to your model. component.get() is intentionally loose —
// this function is where that looseness stops and HeroContent begins.
export function readHero(component: Component): HeroContent {
return {
headline: component.get('headline') ?? HERO_DEFAULTS.headline,
subheadline: component.get('subheadline'),
ctaLabel: component.get('ctaLabel') ?? HERO_DEFAULTS.ctaLabel,
ctaHref: component.get('ctaHref') ?? HERO_DEFAULTS.ctaHref,
};
}GrapesJS-Typen beschreiben den Editor API. Deine eigenen Schnittstellen sollten das Modell deines Produkts beschreiben. Sie zu mischen fühlt sich etwa eine Woche lang effizient an: Dann hat ein Feld, das deine Datenbank braucht, keinen Platz mehr außer einem Komponentenattribut, und die Internen einer Komponente werden Teil deines Schemas. Behalte eine Funktion wie readHero oben als einzige Treffpunkt, an dem sich die beiden begegnen – alles nachgelagert nimmt deine Schnittstelle, nicht ein Component.
Ein Block ist das, was im linken Regal erscheint und was ein Nutzer zieht. Es ist keine Komponente – es ist eine Erklärung, was eingefügt werden soll. Es lohnt sich, die beiden frühzeitig im Gleichgewicht zu halten, da ihre Typen nicht austauschbar sind und die Fehlermeldung dies nicht angibt.
import type { Editor, Block, BlockProperties } from 'grapesjs';
// BlockProperties is exported, so the block can be declared away from the
// editor and checked on its own — label, category, media, content.
const heroBlock: BlockProperties = {
label: 'Hero',
category: 'Sections',
media: '<svg viewBox="0 0 24 24"><rect width="24" height="24" /></svg>',
// A block's content can be a component definition rather than an HTML
// string, which is how a block and a custom component type stay in sync.
content: { type: 'hero' },
};
export function addHeroBlock(editor: Editor): Block {
// Blocks.add(id, props) returns the created Block.
return editor.Blocks.add('hero', heroBlock);
}Blocks.add gibt das erstellte Block zurück, sodass Sie es später erfassen und das Regal anpassen können – also durch Neuordnung, das Ausstecken von Blöcken, die der Nutzerplan nicht enthält, oder das Tauschen eines Kategorielabels zur Laufzeit.
Als Nächstes: Paketiere alles als PluginEin GrapesJS-Plugin ist eine Funktion, die den Editor und ein Options-Objekt empfängt. Das ist der ganze Vertrag – weshalb Plugins die natürliche Einheit für alles sind, was man über Editoren hinweg wiederverwenden, an ein anderes Team verschicken oder verkaufen möchte.
Eine Datei pro Registrierungsart. Der Grund ist nicht die Ordentlichkeit: blocks.ts exportiert BlockProperties-Objekte, die ohne Editor im Umfang typprüfen, sodass sie unit-getestet und wiederverwendet werden können, ohne überhaupt einen Editor zu starten.
my-grapesjs-plugin/
├── src/
│ ├── index.ts # the Plugin<Options> function, and only that
│ ├── types.ts # the exported Options interface
│ ├── blocks.ts # BlockProperties, one per block
│ ├── components.ts # editor.Components.addType calls
│ └── commands.ts # editor.Commands.add calls
├── tsconfig.json
├── package.json # "types": "dist/index.d.ts"
└── README.mdimport type { Editor, Plugin } from 'grapesjs';
// Export the options type. A consumer cannot configure your plugin safely if
// the shape of `options` lives only inside your implementation.
export interface SectionsPluginOptions {
category?: string;
blockPrefix?: string;
}
// Plugin<T> is (editor: Editor, config: T) => PluginResult. Typing the
// function as Plugin<SectionsPluginOptions> checks both parameters for you.
const sectionsPlugin: Plugin<SectionsPluginOptions> = (editor, options) => {
// Defaults belong here, not in the type — an optional field plus a
// destructured default is what makes the call site free to omit them.
const { category = 'Sections', blockPrefix = 'sec' } = options;
editor.Blocks.add(`${blockPrefix}-hero`, {
label: 'Hero',
category,
content: { type: 'hero' },
});
editor.Commands.add(`${blockPrefix}:reset`, {
run(ed: Editor) {
ed.setComponents('');
},
});
};
export default sectionsPlugin;Eine Closure zu übergeben hält deine Optionen an der Aufrufstelle typisiert. Die Alternative – das Plugin in plugins und seine Einstellungen in pluginsOpts aufzulisten – typisiert diese Einstellungen als loses Record, sodass ein falsch geschriebener Schlüssel kompiliert und stillschweigend nichts tut.
import grapesjs from 'grapesjs';
import sectionsPlugin, { type SectionsPluginOptions } from './my-grapesjs-plugin';
const options: SectionsPluginOptions = { category: 'Marketing' };
grapesjs.init({
container: '#gjs',
// Passing a closure keeps the options typed at the call site. The alternative
// — plugins: [sectionsPlugin] with pluginsOpts — types options as
// Record<string, any>, so a misspelled key compiles and silently does nothing.
plugins: [(editor) => sectionsPlugin(editor, options)],
});Ein Design-System-Abschnitt ist keine einzelne Komponente – es ist ein kleiner Baum mit einer Form, für die Ihr Produkt bereits einen Namen hat. Diese Form einmal als Schnittstelle zu beschreiben, ist das, was verhindert, dass Editor, API und Renderer auseinanderdriften.
Hero ← one component type, one interface
├── Heading ← extends 'text'
├── Description ← extends 'text'
└── Button ← extends 'link', traits: label + hrefimport type { Editor, ComponentDefinition } from 'grapesjs';
// The composed shape, described once. Every layer below — the editor default,
// the API payload, the renderer — is checked against this one interface.
export interface HeroContent {
headline: string;
description: string;
ctaLabel: string;
ctaHref: string;
}
// ComponentDefinition is what goes *inside* a tree: the children of a
// component, or the `content` of a block. It is not what addType takes.
const heroChildren = (content: HeroContent): ComponentDefinition[] => [
{ type: 'text', tagName: 'h1', content: content.headline },
{ type: 'text', tagName: 'p', content: content.description },
{
type: 'link',
content: content.ctaLabel,
attributes: { href: content.ctaHref },
},
];
export function registerDesignSystem(
editor: Editor,
defaults: HeroContent
): void {
editor.Components.addType('hero', {
model: {
defaults: {
tagName: 'section',
droppable: false,
// Children are declared, not hand-written as an HTML string, so a
// renamed field is a compile error rather than a silently stale block.
components: heroChildren(defaults),
traits: [
{ type: 'text', name: 'headline', label: 'Headline' },
{ type: 'text', name: 'ctaHref', label: 'Button link' },
],
},
},
});
}Der Vorteil ist heute nicht weniger Fehler – sondern dass sechs Monate später das Hinzufügen eines Feldes zu HeroContent eine Liste aller Stellen erzeugt, die geändert werden müssen, anstatt im gesamten Code nach dem String 'Headline' zu suchen.
Als Nächstes: Bringen Sie es in Ihre Datenbank ein und aus Ihrer DatenbankSpeicher ist der Ort, an dem sich die beiden Typen von Systemen auf die folgenschwerste Weise treffen, weil dies die Grenze ist, die in Ihrer Datenbank landet. Fehler zu machen ist später teuer; Richtig zu machen sind etwa zwanzig Zeilen.
ProjectData ist das eigene JSON des Editors. Seine interne Struktur gehört zu GrapesJS, ändert sich zwischen Versionen und ist nicht etwas, das man manuell migrieren kann. Dein ProjectRecord ist eine Zeile: eine ID, ein Besitzer, ein Name, eine Version, Zeitstempel – plus dieser undurchsichtige Blob in einer Spalte. Speichere es, lade es und lasse das Innere in Ruhe.
import type { ProjectData } from 'grapesjs';
// The editor's own JSON. ProjectData is deliberately open — its internal shape
// is GrapesJS's business and changes between versions, so treat it as opaque:
// store it, load it, never reach into it or migrate it by hand.
// YOUR row. This is the type your API returns and your database stores, and it
// is not a GrapesJS type. Keeping the two apart is what lets you add a column,
// change a version scheme or move providers without touching editor code.
export interface ProjectRecord {
id: string;
name: string;
userId: string;
version: number;
updatedAt: string;
projectData: ProjectData;
}Der Editor erzeugt Projektdaten. Dein Speicheradapter ist der einzige Code, der beide Seiten berührt.
import grapesjs from 'grapesjs';
import type { Editor, ProjectData } from 'grapesjs';
import type { ProjectRecord } from './types/projects';
export function createEditor(projectId: string): Editor {
const editor = grapesjs.init({
container: '#gjs',
storageManager: {
// The id of the storage you register below.
type: 'remote-api',
autosave: true,
stepsBeforeSave: 5,
},
});
editor.Storage.add('remote-api', {
async load(): Promise<ProjectData> {
const res = await fetch(`/api/projects/${projectId}`);
// Throwing here is what makes the editor emit storage:error. Returning
// an empty object instead loses the reader's work without telling them.
if (!res.ok) throw new Error(`Load failed: ${res.status}`);
const record = (await res.json()) as ProjectRecord;
return record.projectData;
},
async store(data: ProjectData): Promise<void> {
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}`);
},
});
editor.on('storage:error', (error) => console.error(error));
return editor;
}Es gibt einen offiziellen React-Wrapper – @grapesjs/react, MIT-lizenziert, derzeit 2.0.0 – und er liefert eine eigene Deklarationsdatei. Er rendert keine React-Komponenten innerhalb der Canvas; er mountet den Editor und übergibt dir die Instanz.
'use client';
import { useRef } from 'react';
import grapesjs from 'grapesjs';
import type { Editor, ProjectData } from 'grapesjs';
import GjsEditor from '@grapesjs/react';
import 'grapesjs/dist/css/grapes.min.css';
interface PageEditorProps {
projectId: string;
onSave: (projectId: string, data: ProjectData) => void;
}
export default function PageEditor({ projectId, onSave }: PageEditorProps) {
const editorRef = useRef<Editor | null>(null);
return (
<GjsEditor
// Required. The wrapper does not import grapesjs itself — you pass the
// module (or a CDN URL), which is what lets you control the version.
grapesjs={grapesjs}
options={{ height: '100vh', storageManager: false }}
onEditor={(editor) => {
editorRef.current = editor;
}}
// projectData is typed as ProjectData, so it lines up with the record
// type your save endpoint expects.
onUpdate={(projectData) => onSave(projectId, projectData)}
/>
);
}Das grapesjs-Requisit ist erforderlich. Der Wrapper importiert absichtlich den Editor selbst nicht, sodass die Version in deinem Bundle die in deinem package.json bleibt – und du kannst sie stattdessen auf einen CDN-Build verweisen.
Der Wrapper ist eine Bequemlichkeit, keine Voraussetzung. Ein useEffect mit Ref erledigt denselben Zweck in etwa fünfzehn Zeilen und ist auch dann verständlich, wenn man den Wrapper verwendet, weil er die beiden Regeln explizit macht: den Ref bewachen und beim Aufräumen zerstören.
'use client';
import { useEffect, useRef } from 'react';
import grapesjs from 'grapesjs';
import type { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
export default function PageEditor() {
const containerRef = useRef<HTMLDivElement | null>(null);
const editorRef = useRef<Editor | null>(null);
useEffect(() => {
// strictNullChecks forces this guard, and it is not ceremony: the ref is
// null on the first render, before React has attached the div.
if (!containerRef.current) return;
const editor = grapesjs.init({
container: containerRef.current,
height: '100vh',
storageManager: false,
});
editorRef.current = editor;
// Without destroy(), React 18's development StrictMode double-mount leaves
// two editors bound to one container.
return () => {
editor.destroy();
editorRef.current = null;
};
}, []);
return <div ref={containerRef} />;
}Die ganze Next.js-Frage ist eine einzige Grenze. grapesjs.init benötigt ein echtes DOM-Element, plus Dokument und Fenster; ein Server Component hat keine davon. Der Editor legt also einen Client Component, und alles darüber kann auf dem Server bleiben.
'use client' ist oben in der Komponente, die den Editor erstellt, und nirgendwo höher. Die Seite darüber bleibt ein Server Component: Es wartet auf Params, überprüft die Sitzung, lädt das Projekt und gibt einfache Requisiten weiter. Diese Aufteilung ist es wert, geschützt zu werden, denn wenn man 'use client' um eine Datei nach oben verschiebt, wird das Datenladen leise zu Client-Code.
// app/editor/[id]/editor-client.tsx
'use client';
import { useEffect, useRef } from 'react';
import grapesjs from 'grapesjs';
import type { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
export default function EditorClient({ projectId }: { projectId: string }) {
const containerRef = useRef<HTMLDivElement | null>(null);
const editorRef = useRef<Editor | null>(null);
// grapesjs.init needs a real element, document and window. Calling it in a
// module body — or in a Server Component — runs it during the server render,
// where none of those exist. useEffect only runs in the browser, which is
// the whole requirement.
useEffect(() => {
if (!containerRef.current) return;
const editor = grapesjs.init({
container: containerRef.current,
height: '100vh',
});
editorRef.current = editor;
return () => {
editor.destroy();
editorRef.current = null;
};
}, [projectId]);
return <div ref={containerRef} />;
}// app/editor/[id]/page.tsx — a Server Component, no 'use client'
import EditorClient from './editor-client';
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function EditorPage({ params }: PageProps) {
const { id } = await params;
// Auth, data loading and permissions stay on the server, fully typed.
// Only the editor itself crosses into the client.
return <EditorClient projectId={id} />;
}Beide funktionieren und folgen der gleichen Form wie die manuelle React-Version: eine Vorlagenreferenz, Initialisierung nach dem Vorhandensein des Elements, Zerstörung beim Zerlegen. Die framework-spezifischen Mechaniken haben hier eigene Leitfäden, nicht eine komprimierte Version.
Eine Vorlagenreferenz plus onMounted / onBeforeUnmount, wobei der Editor außerhalb des reaktiven Zustands gehalten wird – das Wrappen eines Editor in ref() macht den Vue-Proxy zu einem Objekt, das seine eigenen Internen verwaltet. Es gibt keinen offiziellen Vue-Wrapper.
GrapesJS + Vue Leitfaden@ViewChild für den Container, ngAfterViewInit zum Initialisieren, ngOnDestroy zum Zerlegen – und runOutsideAngular, damit die eigene Ereignisschleife des Editors nicht die Änderungserkennung steuert. Es gibt keinen offiziellen Angular-Wrapper.
GrapesJS + Angular LeitfadenIn beiden Fällen sind die Typen dieselben, die diese Seite verwendet hat: Editor, Component, ProjectData. Das Framework ändert, wo init() genannt wird, nicht das, was es zurückgibt.
Alles oben liegt in einer Datei. Es passt nicht mehr bei etwa der dritten benutzerdefinierten Komponente, und entscheidend darüber, ob das nächste Jahr angenehm wird, ist, wo du die Naht zwischen deiner Anwendung und dem Editor platzierst.
src/
├── editor/ # everything that touches the Editor instance
│ ├── createEditor.ts # grapesjs.init, one place, returns Editor
│ ├── plugins.ts # Plugin<T> registrations
│ ├── components.ts # Components.addType calls
│ ├── blocks.ts # BlockProperties definitions
│ ├── commands.ts # Commands.add calls
│ └── storage.ts # Storage.add, ProjectData in and out
│
├── types/
│ ├── editor.ts # aliases over GrapesJS types you use a lot
│ ├── components.ts # HeroContent and friends — YOUR model
│ ├── projects.ts # ProjectRecord — your database row
│ └── api.ts # request/response shapes
│
└── app/ # imports from types/, never from editor/ internalsAnwendungscode sollte nicht direkt von 'grapesjs' importiert werden. Gib ihm ein einziges Modul, das die wenigen Editor-Typen, die dein Produkt wirklich kennt, erneut exportiert, die Manager, die nicht exportiert werden, und die enge Schnittstelle deklariert, auf die dein UI tatsächlich angewiesen ist. Dann nimmt ein Toolbar-Button diese Schnittstelle ein, nicht ein ganzes Editor – und kann nicht einmal versehentlich in die Editor-Internen eindringen.
// src/types/editor.ts — the single seam between your app and the editor.
import type { Editor, ProjectData } from 'grapesjs';
// Re-export what your application is allowed to know about.
export type { Editor, ProjectData };
// Manager classes are not exported by name; alias them here once so no other
// file has to remember that.
export type BlockManager = Editor['Blocks'];
export type StorageManager = Editor['Storage'];
// The surface your UI actually depends on. Application code takes this, not a
// full Editor, so a toolbar button cannot quietly reach into editor internals.
export interface EditorFacade {
getHtml(): string;
getCss(): string;
save(): Promise<void>;
destroy(): void;
}Deine Anwendung oben, deine Typen in der Mitte, der Editor darunter. Abhängigkeiten zeigen nach unten und gehen nie zurück.
Deins. Weiß nichts über GrapesJS.
Die Naht. Der einzige Ort, an dem beide Welten benannt sind.
Deiner. Der einzige Code, der aus dem Editor importiert wird.
Der Lektor. Nach dem Paket typisiert.
Ein Page-Builder-Produkt ist meist kein Page Builder. GrapesJS deckt ein Feld in dieser Kette ab; der Rest ist eine Anwendung, die du sowieso schreiben wolltest, und das Tippen der Joins zwischen ihnen ist das, worauf dieser Guide hinausgearbeitet hat.
GrapesJS besitzt die Bearbeitungsfläche. Authentifizierung, Speicherung, Mietverhältnisse, Abrechnung und Veröffentlichung gehören Ihnen.
GrapesJS besitzt die Bearbeitungsfläche. Authentifizierung, Speicherung, Mietverhältnisse, Abrechnung und Veröffentlichung gehören Ihnen.
Alles, worüber der Editor keine Meinung hat.
Alles innerhalb der Canvas, typisiert durch das Paket.
Der Editor ist ein Bestandteil deines Produkts, kein Ersatz dafür.
Elf Fehlschläge, die wiederholt auftreten, die meisten sind spezifisch für dieses Paar und nicht für TypeScript im Allgemeinen. Jeder ist ein Symptom, gegen das du dich abgleichen kannst, und die Änderung, die es behebt.
Symptom
npm install fails mit E404 oder ein Tutorial sagt dir, es hinzuzufügen, und du gehst davon aus, dass dein Register defekt ist.
Behebung
Entfernen Sie es. Die Typen befinden sich innerhalb von grapesjs selbst, referenziert durch das Typ-Feld des Pakets. Es gibt kein separates Typ-Paket und es gab keines für die aktuelle Leitung.
Symptom
Let Editor: Any, meist hinzugefügt, um einen Fehler während der Einrichtung auszuschalten, und nie entfernt.
Behebung
Editor von 'grapesjs'. One at the root propagiert an jeden Manager, jede Event-Payload und jeden Exportaufruf – du behältst die Kompilierungszeit-Kosten von TypeScript und verlierst den gesamten Nutzen.
Symptom
Ein Plugin nimmt opts: any oder Record<string, unknown>; Konsumenten raten Feldnamen und Tippfehler bewirken nichts.
Behebung
Deklarieren und exportieren Sie eine Optionsschnittstelle und geben Sie dann die Funktion als Plugin<YourOptions> ein. Beide Parameter werden dann überprüft, auch am Aufrufstandort.
Symptom
Ein Block zu passieren, wo ein Component erwartet wird, oder einen Block zu stylen und festzustellen, dass er keine Stile hat.
Behebung
Ein Block ist ein Regaleintrag, der beschreibt, was eingefügt werden soll. Eine Komponente ist ein Knoten in der Canvas. Blocks.add nimmt BlockProperties; Components.addType nimmt AddComponentTypeOptions.
Symptom
Ein Komponententyp registriert sich, verhält sich aber wie ein normaler Div – kein traits, keine Einschränkungen.
Behebung
addType nimmt AddComponentTypeOptions: model, view, isComponent, extend. ComponentDefinition beschreibt einen Knoten innerhalb eines Baums – die Kinder einer Komponente oder des Inhalts eines Blocks.
Symptom
Ein für component:add geschriebener Handler wird für component:remove wiederverwendet, und das zweite Argument ist undefiniert.
Behebung
Die Nutzlasten unterscheiden sich je nach Ereignis, und die Typen geben das bereits an. Lassen Sie die Inferenz Ihnen die Parameter angeben, anstatt sie von einem anderen Handler zu annotieren.
Symptom
Dein ProjectRecord hat Spalten, die Felder im JSON des Editors spiegeln, und ein GrapesJS-Upgrade bedeutet eine Migration.
Behebung
Behandle ProjectData als undurchsichtig. Eine Spalte enthält es; alles, worauf du suchst – Besitzer, Name, Version, Zeitstempel – befindet sich daneben, nicht darin.
Symptom
"Null-Eigenschaften können nicht lesen" bei einer schnellen Navigation, einem Hot-Reload oder dem ersten Rendering einer Route.
Behebung
Editor | null, und die Null zu übernehmen. Nicht-null Assertions auf einer Ref verschieben das Problem von deinem Terminal auf deinen Fehlertracker.
Symptom
Benannte Importe, die nicht aufgelöst werden, Manager-Methoden, die nicht existieren, eine Editor-Version, die nie veröffentlicht wurde.
Behebung
Vergleichen Sie die API mit der Deklarationsdatei in Ihrem node_modules und nicht mit einem Blogbeitrag. Es gibt keine GrapesJS v1.x; die aktuelle Version ist 0.23.6.
Symptom
Der Editor des Wrappers und dein Editor sehen identisch aus, sind aber strukturell inkompatibel, und der Fehler nennt zwei Wege.
Behebung
Ein grapesjs im Baum. Überprüfe npm ls grapesjs – eine verschachtelte Kopie unter einem Plugin ist die übliche Ursache.
Symptom
Eine Schnittstelle, die die Hälfte des Editors neu als API deklariert, damit du es weitergeben kannst und bei jeder Veröffentlichung von den echten Typen abweichst.
Behebung
Alias das, was existiert – Editor['Blocks'] – und deklariere nur die enge Fassade, die dein eigenes UI braucht. Das API des Editors in deinen eigenen Typen zu wiederholen, ist eine Wartung, für die du dich nicht anmelden musst.
Keines davon sind TypeScript-Probleme. Es sind Stellen, an denen das Modell des Editors und das Modell deines Produkts miteinander verwechselt werden, und das Typsystem ist nur das, was die Verwirrung frühzeitig sichtbar macht.
Vier Fehlerformen decken fast alles ab. In jedem Fall ist der nützliche Schritt, die Deklarationsdatei in node_modules zu betrachten, anstatt nach einer zu greifen – die Antwort ist dort enthalten, und jede verschiebt nur die Frage.
Du siehst
TS2307 auf der Importzeile, obwohl das Paket eindeutig installiert ist und der Editor zur Laufzeit einwandfrei läuft.
Prüfe
Zuerst deine TypeScript-Version. Unter 5.0 kann die Deklarationsdatei nicht geparst werden und der Fehler wird als fehlendes Modul gemeldet – eine wirklich irreführende Nachricht. Dann moduleResolution: Es muss bundler, node16 oder nodenext sein, nicht klassisch. Das Hinzufügen von @types/grapesjs hilft nicht; dieses Paket existiert nicht.
Du siehst
TS2614 auf einem Namen, den du in der Deklarationsdatei sehen kannst, meistens StorageManager oder BlockManager.
Prüfe
Die Manager-Klassen werden deklariert, aber nicht exportiert. Verwenden Sie das indexierte Zugriffsalias — Editor['Storage'], Editor['Blocks'], Editor['Components'] — das sich auf dieselbe Klasse auflöst. Wenn der Name etwas anderes ist, grep node_modules/grapesjs/dist/index.d.ts: Wenn er nicht vorhanden ist, gehört er zu einer älteren Version.
Du siehst
Ein Ereignishandler, ein addType-Aufruf oder eine Blockdeklaration, die exakt mit einem Tutorial übereinstimmt und trotzdem nicht kompiliert.
Prüfe
Die Signatur, in der Deklarationsdatei. Ereignisse tragen unterschiedliche Payloads pro Name; addType nimmt AddComponentTypeOptions statt ComponentDefinition. Fähre die Methode in deinem Editor mit der Maus – die echte Signatur ist direkt vorhanden, und sie hat meist eine andere Form als der Artikel, von dem du kopiert hast.
Du siehst
Ein React- oder Next.js-Build, bei dem die Editor des Wrappers und deine sich weigern, sich zu vereinheitlichen, und die Nachricht zwei node_modules-Pfade nennt.
Prüfe
Doppelte Installationen. npm ls grapesjs zeigt die zweite Kopie an, die üblicherweise von einem Plugin mit einem schmalen Peer-Bereich geladen wird. Deduplizieren oder ausrichten Sie die Versionen; der eigene Peer-Bereich des Wrappers ist ^0.22.5.
Für framework-spezifische Typfehler gehen die React- und Next.js-Anleitungen tiefer als diese Seite.
Exakte Versionen, die mit dem Register und der installierten Deklarationsdatei überprüft wurden. Der TypeScript-Floor wurde insbesondere gemessen, indem er mit jeder Version kompiliert wurde und nicht aus einem Changelog abgeleitet wurde.
| Paket | Verifiziert | Was es bedeutet |
|---|---|---|
grapesjs | 0.23.6 | Versandt eigene Typen auf dist/index.d.ts. Es existiert kein separates Typenpaket. |
typescript | >= 5.0 | Die Etage. 4,9 und darunter können die Deklarationsdatei nicht analysieren und als fehlendes Modul melden. |
typescript | 7.0.2 | Die aktuelle Version und die Version, mit der die Beispiele dieses Leitfadens kompiliert wurden. Alles ab Version 5.0 funktioniert. |
@grapesjs/react | 2.0.0 | Der offizielle React-Wrapper, MIT-lizenziert, mit eigenen gebündelten Typen. |
react | ^18.0.0 || ^19.0.0 | Der React-Peer-Bereich des Wrappers. Auf React 17 initialisieren Sie den Editor manuell mit useEffect. |
grapesjs (peer) | ^0.22.5 | Der grapesjs-Peer-Bereich des Wrappers – breit genug, dass der aktuelle Kern sie erfüllt. |
node | >=20.9.0 | GrapesJS ist eine Browser-Bibliothek und deklariert kein Engine-Feld. Der Boden, den du tatsächlich erreichst, stammt aus deinem Framework; Next.js 16.3.4 verlangt das. |
Verifizierte 2026-09-03 mit registry.npmjs.org und node_modules/grapesjs/dist/index.d.ts. Jeder Codebeispiel auf dieser Seite wurde vor der Veröffentlichung im strengen Modus gegen diese Versionen kompiliert.
Es gibt hier kein "funktioniert mit jeder TypeScript-Version", denn das stimmt nicht: 5.0 ist ein harter Untergrund und der darunterliegende Fehlermodus ist verwirrend genug, um genau genannt zu werden.
Sobald du den Kern von API verstehst, können Plugins zusätzliche Funktionen bieten, ohne dass du jedes Feature von Grund auf neu bauen musst. Dies sind aktuelle Einträge auf GJS.Market, gruppiert nach dem Teil der typisierten Oberfläche, die jeder berührt.
Wo ProjectData auf deine Datenbank trifft. Die Schicht, um die sich der Speicherabschnitt dieses Leitfadens handelt.
Kategorie ansehenEin Speicheradapter für ein headless CMS-Backend – derselbe Load/Store-Vertrag, den man von Hand implementieren würde.
Browser-lokale Persistenz. Nützlich als Entwurfsschicht vor einem entfernten Adapter.
Firestore als Projektspeicher, wobei das Dokument für dein ProjectRecord steht.
Firebase-gestützter Speicher für Projekte, verkabelt über den Storage Manager.
Component-Typen, die du sonst bei addType registrieren würdest.
Kategorie ansehenEin tabierter Komponententyp mit eigenem traits – ein ausgearbeitetes Beispiel für addType, das man lesen kann.
Eine Icon-Komponente mit einem Picker-Merkmal, das zeigt, wie ein Merkmal ein Komponentenfeld steuert.
Wiederverwendbare verknüpfte Instanzen, sodass eine Bearbeitung über Seiten hinweg verbreitet wird.
Ein Schreibmaschineneffekt-Komponenten-Wrapping Typed.js – eine Drittanbieterbibliothek, die als Komponententyp offengelegt wird.
Codebearbeitung, Skriptbearbeitung und Projektmanagement im Editor.
Kategorie ansehenBearbeiten Sie die HTML und CSS eines Bauteils, das ist der schnellste Weg, um zu sehen, was Ihre Bauteiltypen tatsächlich produzieren.
Fügen Sie Komponentenskripte aus dem Editor an und bearbeiten Sie sie.
Eine Reihe von Editor-Tools, die sich an Menschen richten, die auf GrapesJS aufbauen, und nicht an Endnutzer.
Multiprojekt-Handling im Editor, angrenzend an die Speicherschicht darüber.
React-basierter Editor UI rund um die GrapesJS-Canvas.
Eine funktionierende React-Integration kannst du zusammen mit dem React-Abschnitt dieses Leitfadens lesen.
Eine hero-Komponente für React-basierte Stacks – der Abschnitt, den dieser Guide von Hand erstellt.
Ein Preset, das sich an React-Entwickler richtet und Blöcke und Komponenten bündelt.
Eine Sache wird diese Seite dir nicht sagen: Keines dieser Einträge wirbt mit gebündelten TypeScript-Deklarationen, also behandle Typunterstützung als unverifiziert und prüfe die eigene README des Plugins. Was unabhängig davon zutrifft, ist, dass der Editor, den ein Plugin erhält, vom Kernpaket typisiert wird – also wird dein Integrationscode um jedes Plugin herum überprüft, selbst wenn das Plugin selbst nur JavaScript ist.
Die Zeile bezieht sich nicht auf die Schwierigkeit. Es geht darum, ob das Verhalten spezifisch für Ihr Produkt ist – denn das entscheidet, wer es in zwei Jahren warten muss.
| Anforderung | Selbst bauen | Verwende ein Plugin |
|---|---|---|
| Verhalten, das auf Ihr Unternehmen zugeschnitten ist | Ja – niemand sonst wird es bauen | — |
| Gemeinsame Editor-Funktionalität | — | Ja – schon gelöst |
| Volle Kontrolle über den Code | Ja | Ja, für Open-Source-Plugins |
| Versuch zur ersten funktionierenden Version | Höher | Unterer Ausgangspunkt |
| Wer hält sie | Dein Team | Plugin-Autor, plus deine Integration |
| Wie weit du es ändern kannst | So weit wie du willst | Kommt auf das Plugin an |
In der Praxis sind die meisten Editoren beides: eine Handvoll Plugins für die Teile, die jeder Editor benötigt, und eigene typisierte Komponententypen für die Teile, die das Produkt zu Ihrem machen.
Die Typen auf dieser Seite sind überall gleich. Was sich ändert, ist, wo man init() nennt und wie man aufräumt.
Kein Framework, oder ein Framework, das diese Liste nicht nennt. Alles oben Genannte gilt direkt.
Leitfaden öffnenDie offizielle Verpackung, Referenzen, Aufräumen und StrictMode.
Leitfaden öffnenClient-Grenze, Serverdaten laden, App Router.
Leitfaden öffnenVorlagenreferenzen, onMounted und das Verhindern des Reaktivitätszustands.
Leitfaden öffnenViewChild, Lebenszyklus-Hooks und Änderungserkennung.
Leitfaden öffnenFang mit dem GrapesJS-Tutorial an und komm dann für die Typen zurück.
Leitfaden öffnenWohin als Nächstes, je nachdem, ob du den Editor noch lernst, ihn in ein Framework einbindest oder ein Produkt darum herum baust.
Wenn der Architekturbereich der eigentliche Standort Ihres Projekts ist, besteht die verbleibende Arbeit meist aus Integration und nicht aus Editor-Funktionen. Das ist es, was wir tun.
Beginne mit dem typisierten GrapesJS API, baue eigene Komponenten und Plugins, verbinde deine Anwendungsinfrastruktur und erweitere den Editor, wenn dein Produkt mehr Funktionalität benötigt.
Installiere das Paket, typisiere den Editor und hab in wenigen Minuten ein funktionierendes, typisiertes Setup.
Starte das TutorialSpeicheradapter, Bauteiltypen und Entwickler-Tools wurden bereits für GrapesJS entwickelt.
Plugins entdeckenGetypte Plugins, Framework-Integration und Produktionsarchitektur, mit dir gebaut.
Individuelle Entwicklung anfragenTypeScript macht GrapesJS nicht sicherer. Es macht einen individuell angepassten GrapesJS-Editor wartbar, sobald er eine Datei überwachsen ist.