你提供
所有编辑都没有意见的内容。
- 账户、会话与权限
- 租户和按套餐限制
- 数据库及其迁移
- 发布、域名与托管
- 计费与使用
学习如何将 GrapesJS 与 TypeScript 结合使用:使用带类型的编辑器 API 和事件,创建自定义组件和插件,连接存储,并把 GrapesJS 集成进 React、Next.js、Vue 或 Angular 应用。
GrapesJS 0.23.6 在 dist/index.d.ts 发布自己的声明文件,引用包自身的类型字段。安装 grapesjs 后,编辑器和类型集成在一个依赖中,因此从 'grapesjs' 导入 Editor 即可解析,无需额外设置和第二个包。
这些类型涵盖的是编辑器的API表面——编辑器实例、管理器、组件、模块、事件和项目数据。它们并不描述你产品自身的模型,本指南主要是为了区分这两者。
安装后开始十二步,按顺序。每个步骤都链接到涵盖该内容的章节,这样你可以从项目实际所在的位置开始。
有一个套装。类型自带,所以不需要第二次安装,也没有 devDependencies 的 @types 条目。
npm install grapesjs@types/grapesjs 既不被弃用、取代也不可选——它不在 npm 注册表中。运行它会返回 404,如果你在较早的教程中看到它,那是可靠信号,说明该教程的其余部分也早于捆绑的类型。
# Don't. This package is not published — npm returns E404.
npm install --save-dev @types/grapesjsgrapesjs 本身是一个值:你调用 grapesjs.init()。Editor、Component 和 Block 是类型:它们只存在于编译过程中。用 import type 标记它们可以明确表示,并确保导入被擦除而不是被拉入你的捆绑包——这在框架代码中最为重要,因为编辑器的零散运行时导入可能会将其拖入服务器渲染。
// 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';GrapesJS 不需要特殊配置。你需要正确设置四个选项——其余的 tsconfig 可以保留项目已经用的配置。
{
"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
}
}这些是你实际会导入的名称。每个名称都是与grapesjs 0.23.6声明文件对照的,而不是从文档网站复制,文档描述JavaScript API,且不总是使用相同的名称。
| 类型 | 代表 | 常见用途 |
|---|---|---|
Editor类 | 编辑器实例 | 一切:生命周期、管理器、导出、事件 |
EditorConfig接口 | 对象传递给初始 | 构建远离呼叫站点的配置 |
Component类 | 画布树中的一个节点 | 读取并更新选定元素 |
ComponentDefinition接口 | 声明的组件,不是实例 | 组件的子节点;块的内容 |
ComponentProperties接口 | 组件的模型字段 | 为传给 addType 的默认值添加类型 |
AddComponentTypeOptions接口 | 参数 addType 实际上取 | 注册自定义组件类型 |
Block类 | Block Manager 中的一个模块 | Blocks.add 的返回值 |
BlockProperties接口 | 区块声明 | 在自己的模块中声明块 |
Trait类 | 设置面板里有一个字段 | 自定义特征类型和特征处理器 |
ProjectData接口 | 编辑器保存的JSON | 存储加载和存储;你的数据库列 |
Plugin<T>接口 | 一个带有类型选项的插件函数 | 为你编写或使用的插件添加类型 |
PluginOptions类型别名 | 插件选项的限制 | 通用插件辅助器和封装器 |
所有这些都可以按名称导入:import type { Editor, Component, BlockProperties },来自'grapesjs'。只有 grapesjs 本身需要运行时导入。
管理器类在文件中声明但从未导出,因此按名称导入时TS2614会失败——这是一个令人困惑的错误,因为当你去查找时,这个类显然存在。改用Editor索引:获取器的返回类型是同一类,且别名在不同版本间稳定。
BlockManagerEditor['Blocks']StorageManagerEditor['Storage']ComponentManagerEditor['Components']grapesjs.init() 返回一个 Editor。注释变量是可选的——推理已经正确了——但命名类型是让你能跨模块边界传递编辑器而不扩大到任何边界的关键。
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();在框架里,编辑器在首次渲染时并不存在,卸载之后也不应存在。把持有它的变量标注为 Editor | null,编译器就会在每个调用点追问这两个时刻。标注成 Editor 再用断言绕开,只是把这个问题推到了线上。
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;
}每个管理器都挂在编辑器上——editor.Blocks、editor.Components、editor.Storage、editor.Commands——每个getter都有类型,所以自动补全从实例开始。你不能用名称导入管理器类;而是通过Editor给它们加别名。
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 是事件名称上的通用,因此回调签名是从你传递的字符串中推导出来的。实际上这意味着你几乎不应该注释——推理给了你正确的参数类型,而与其不一致的注释是编译错误,而不是静默的不匹配。
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');
});
}事件名称是有意为之的开放字符串联合:插件定义自己的通道,通道必须不断编译。代价是核心事件名称中的错别字仍然有效,TypeScript 代码——回调只会退回到一个松散的签名,永远不会触发。当事件处理程序神秘地不做时,检查拼写后再检查 API。
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) => { ... });
}组件类型会在画布中注册新的行为:标签、它的 traits、它接受的子节点、解析回 HTML 时如何识别。这里是大多数自定义编辑器工作的地方,也是必须划定 GrapesJS 类型与你类型界限的地方。
editor.Components.addType(type, options)采用AddComponentTypeOptions——模型、视图、isComponent、扩展——而不是ComponentDefinition。ComponentDefinition描述的是树内声明的节点:组件的子节点,或块的内容。将ComponentDefinition交给addType的教程引用的是较早的形状,因此出现的错误并不明显。
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 类型描述编辑器 API。你自己的接口应该描述产品的模型。混合它们感觉效率大约持续一周:然后数据库需要的字段只剩下组件属性,组件内部结构就成了你的模式的一部分。保留像 readHero 这样的函数作为两者唯一的交汇点——所有下游接口都用你的接口,而不是 Component。
块是显示在左侧书架上的内容,也是用户拖动的内容。它不是组件——而是对插入内容的声明。提前区分两者是值得的,因为它们的类型不可互换,错误信息也没有说明。
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 返回创建的 Block,方便你捕获并稍后调整书架——比如重新排序、隐藏用户计划中未包含的块,或在运行时更换类别标签。
接下来:把所有内容打包成插件GrapesJS 插件是一个接收编辑器和选项对象的函数。这就是整个合同——这就是为什么插件是你想跨编辑器重复使用、发货给其他团队或销售的自然单位。
每个注册类型一个文件。原因不是整洁:blocks.ts导出的BlockProperties对象类型检查时没有编辑器,这样可以进行单元测试和重复使用,完全不启动编辑器。
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;传入一个闭包,可以让你的选项在调用处保持类型。另一种做法——把插件写进 plugins、把它的配置写进 pluginsOpts——会把这些配置的类型放宽成一条松散记录,于是拼错的键也能通过编译,却什么都不做。
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)],
});设计系统部分不是单一组件——它是一棵带有产品已有名称形状的小树。仅仅描述一次该形状作为接口,就能防止编辑器、API 和渲染器分离。
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' },
],
},
},
});
}回报不是今天的bug减少——而是六个月后,在HeroContent中添加字段时,会生成一个必须更改的每个地方的列表,而不是在代码库中搜索字符串“headline”。
接下来:把它从数据库里进出存储是两种类型系统最关键的交汇点,因为这是最终进入数据库的边界。错误后期成本较高;正确大约需要二十行。
ProjectData 是编辑器自己的 JSON。它的内部结构属于 GrapesJS,版本间会变更,不需要手动迁移。你的 ProjectRecord 是一个行:一个 ID、一个所有者、一个名字、一个版本、时间戳——加上一列里那个不透明的斑点。存储它,加载它,内部保持原样。
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;
}编辑器生成项目数据。你的存储适配器是唯一同时处理两侧的代码。
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;
}有一个官方的 React 包装器——@grapesjs/react、MIT 授权,目前为 2.0.0——并且它会发布自己的声明文件。它不会在画布内渲染 React 组件;它会挂载编辑器并交付实例。
'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)}
/>
);
}必须使用 grapesjs prop。包装器故意不导入编辑器本身,所以你包里的版本保持 package.json 版本——这样你就可以指向 CDN 构建。
封装只是方便,不是必需品。带 ref 的 useEffect 在大约十五行内完成同样的工作,即使用 Refper 也值得理解,因为它明确了两条规则:守护 ref,和清理时摧毁。
'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} />;
}整个Next.js的问题就是一个边界。grapesjs.init需要真正的DOM元素,还有文档和窗口;而Server Component则没有这些元素。所以编辑器放在Client Component里,上面的所有内容都可以留在服务器上。
'use client' 位于创建编辑器的组件顶部,且不高。其上方页面仍为 Server Component:等待参数,检查会话,加载项目并传递普通道具。这种分割值得注意,因为将 'use client' 往上移动一个文件会悄悄地将数据加载变成客户端代码。
// 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} />;
}两者都能正常工作,且都遵循与手动React版本相同的结构:模板参考,元素存在后初始化,拆解时销毁。框架专用机制有自己的指南,这里没有浓缩版。
模板参考加上onMounted / onBeforeUnmount,编辑器保持在反应式状态之外——将Editor包裹在ref()中,使Vue代理成为管理自身内部的对象。没有官方的Vue封装器。
GrapesJS + Vue 指南@ViewChild容器,ngAfterViewInit 初始化,ngOnDestroy 拆除——runOutsideAngular 则是编辑器自身的事件循环不驱动变更检测。没有官方的 Angular 封装器。
GrapesJS + Angular 指南在这两种情况下,类型都是本页一直使用的:Editor、Component、ProjectData。框架变的是init()被调用的位置,而不是返回的内容。
以上所有内容都能放进一个文件里。大约在第三个自定义组件时就不再合适了,决定明年是否愉快的是你在应用和编辑器之间的接缝处。
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/ internals应用代码不应直接从“grapesjs”导入。给它一个模块,重新导出你产品合法知道的少数编辑器类型,给未导出的管理器做别名,并声明你的UI实际依赖的狭窄界面。然后工具栏按钮接管那个接口,而不是整个Editor——即使意外也无法进入编辑器内部。
// 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;
}你的应用程序在上面,类型在中间,编辑器在下面。依赖关系指向下方,永远不会回溯。
你的。对GrapesJS一无所知。
那条缝隙。两个世界唯一被命名的地方。
你的。唯一能从编辑器导入的代码。
编辑。由包裹添加类型。
页面构建器产品大多不是页面构建器。GrapesJS 涵盖了这条链中的一个盒子;其余的都是你本来就打算写的应用程序,而输入它们之间的连接正是本指南的目标。
GrapesJS拥有编辑界面。认证、存储、租赁、计费和发布均由你负责。
GrapesJS拥有编辑界面。认证、存储、租赁、计费和发布均由你负责。
所有编辑都没有意见的内容。
画布内的一切,类型由这个包提供。
编辑器是你产品的组成部分,而不是替代品。
十一种反复出现的失败,大多数是这对组合特有的,而不是TypeScript整体。每一个都是你可以匹配的症状和修复它的变革。
症状
npm install fails和E404,或者教程告诉你添加它,你就以为注册表坏了。
修复
删除它。类型都在grapesjs内部,由包的类型字段引用。没有单独的类型包,当前行也没有单独的类型包。
症状
LET Editor: any,通常在设置时用来消除一个错误,从未被删除。
修复
Editor 来自“grapesjs”。根节点上的任意节点会传播到每个管理器、每个事件有效载荷和每次导出调用——你保留了 TypeScript 的编译时成本,但失去了所有的好处。
症状
插件会选择opts:any或Record<string, unknown>;用户猜字段名,错别字也没用。
修复
声明并导出一个选项接口,然后将函数输入为Plugin<YourOptions>。然后检查这两个参数,包括调用站点。
症状
比如通过Block而预期有Component,或者试图样式化一个方块却发现它没有样式。
修复
块是一个描述要插入内容的架子条目。组件是画布中的一个节点。Blocks.add 使用 BlockProperties;Components.addType 使用 AddComponentTypeOptions。
症状
组件类型会注册,但表现得像普通的 div——没有 traits,也没有限制。
修复
addType 取 AddComponentTypeOptions:model、view、isComponent、extend。ComponentDefinition 描述树中的节点——组件的子节点或块内容。
症状
为component:add编写的处理程序被重复用于component:remove,第二个参数未定义。
修复
每个事件的有效载荷不同,类型本身就说明了这一点。让推理给你参数,而不是从其他处理程序中标注。
症状
你的ProjectRecord在编辑器的JSON中有列与字段镜像,GrapesJS升级意味着迁移。
修复
把ProjectData当作不透明。有一列保存它;你查询的所有内容——所有者、名称、版本、时间戳——都存在于它旁边,而不是内部。
症状
在快速导航、热加载或路线首次渲染时,“无法读取空属性”。
修复
Editor | null,并处理空。引用上的非空断言会将问题从终端转移到错误追踪器。
症状
有命名导入无法解析,管理器方法不存在,编辑器版本从未发布。
修复
请将API与你node_modules中的声明文件对照,而不是对照博客文章。没有GrapesJS v1.x;当前版本是0.23.6。
症状
包裹的Editor和你的Editor看起来一样,但结构不兼容,错误中提到了两条路径。
修复
树上有一个grapesjs。查查npm ls grapesjs——插件下嵌套副本通常是原因。
症状
一个界面会重新声明编辑器的一半 API,这样你就可以在每次发布时都与真实类型有偏差地传递。
修复
别名现有的Editor['Blocks'],只声明你自己UI需要的狭窄表象。用你自己的类型重新描述编辑器的API是维护,你不必签约。
这些都不是TypeScript的问题。这些是编辑器模型和你产品模型混淆的地方,类型系统只是让混淆在早期显现。
四个错误形状几乎涵盖了所有内容。在每种情况下,有用的做法是查看node_modules中的声明文件,而不是去找任何一个——答案就在里面,任何错误都只是推迟问题。
你会看到
导入线上出现了TS2307,尽管软件包明显安装了,编辑器运行正常。
检查
先看你的TypeScript版本。低于5.0版本,声明文件无法解析,故障会被报告为缺失模块——这确实是误导性的消息。那么moduleResolution:必须是bundler、node16或nodenext,而不是经典版。添加@types/grapesjs也无济于事;那个包不存在。
你会看到
TS2614出现在声明文件中,最常见的是StorageManager或BlockManager。
检查
管理器类声明但未导出。使用索引访问别名 — Editor['Storage']、Editor['Blocks']、Editor['Components'] — 该别名解析为同一类。如果名称是其他,则 grep node_modules/grapesjs/dist/index.d.ts;如果没有,则属于较旧版本。
你会看到
事件处理程序、addType调用或块声明,完全匹配教程却无法编译。
检查
声明文件中的签名。事件每个名称携带不同的载荷;addType 采用 AddComponentTypeOptions 而不是 ComponentDefinition。将鼠标悬停在编辑器中的方法——真实的签名就在那儿,而且通常和你复制的文章形状不同。
你会看到
一个React或Next.js构建,封装者的Editor和你的Editor拒绝合并,消息中会指定两条node_modules路径。
检查
重复安装。npm ls grapesjs 会显示第二个副本,通常由具有窄对等范围的插件拉入。去重或对齐版本;封装器自身的对等范围是 ^0.22.5。
针对框架特定的类型错误,React 和 Next.js 指南比本页更深入。
精确版本,与注册表和已安装声明文件核对。尤其是TypeScript底线是通过对每个版本编译来测量的,而不是从更新日志推断。
| 包 | 已验证 | 意味着什么 |
|---|---|---|
grapesjs | 0.23.6 | 在dist/index.d.ts上自行发货。没有单独的类型套件。 |
typescript | >= 5.0 | 底层。4.9及以下版本无法解析声明文件,并将其报告为缺失模块。 |
typescript | 7.0.2 | 当前版本,以及本指南采样编译时使用的版本。5.0以上的所有版本都能正常使用。 |
@grapesjs/react | 2.0.0 | 官方的React封装,MIT授权,并附带了自己的类型。 |
react | ^18.0.0 || ^19.0.0 | 封装器的 React 对等范围。在 React 17 时,手动用 useEffect 初始化编辑器。 |
grapesjs (peer) | ^0.22.5 | 封装器的 grapesjs 对等范围——足够宽,当前核心能满足它。 |
node | >=20.9.0 | GrapesJS 是一个浏览器库,声明没有引擎字段。你实际达到的底层来自你的框架;Next.js 16.3.4 需要这个。 |
已验证 2026-09-03 与 registry.npmjs.org 和 node_modules/grapesjs/dist/index.d.ts. 的匹配 本页上的每个代码样本均在发布前以严格模式对这些版本进行编译。
这里没有“所有TypeScript版本都适用”,因为事实并非如此:5.0是一个硬底线,而其下面的故障模式足够复杂,值得准确说明。
一旦你了解了核心的 API,插件就能提供额外功能,而无需你从零构建所有功能。这些是 GJS.Market 当前的列表,按每个插件接触的类型表面部分分组。