GrapesJS Angular 集成

GrapesJS Angular:用 Angular 构建一个可视化编辑器

将开源的 GrapesJS 可视化编辑器集成到 Angular 应用程序中,构建拖放式页面构建器、CMS 编辑器、着陆页构建器、电子邮件编辑器以及白标视觉编辑体验。

开源自托管HTML 与 CSS 输出可扩展Angular 兼容

26k+

GitHub 星标

1.4M+

npm 下载量/月

100+

GJS.Market 上的插件

$0

许可费

实时编辑器

试试GrapesJS应用中的Angular应用

在新标签页中打开

原版GrapesJS版本——你在添加任何插件之前,从'GrapesJS.init()'中获得的编辑器,带有标准预设。从右侧面板拖动一个块,选中它,然后在样式管理器中编辑其排版和间距。

grapesjs.com/demo.html免费

从grapesjs.com加载第三方页面。点击之前不会有请求。

这些是实时的 GrapesJS 构建,不是 Angular 演示——这正是它的意义所在。GrapesJS 渲染成它完全拥有的容器元素,因此画布、块、样式管理器、响应式预览、资源选择器和撤销历史在 Angular 组件托管该容器后表现相同。

  1. Angular application
  2. GrapesJS editor
  3. HTML + CSS + project data
  4. Your backend / CMS / SaaS

Angular 驱动你的应用。GrapesJS 驱动视觉编辑体验。输出出来的是 HTML、CSS 和你后端存储的项目 JSON 文档。

直接回答

你能和Angular一起使用GrapesJS吗?

是的

GrapesJS 与框架无关,可以集成到 Angular 应用中。你通过 Angular 生命周期钩子初始化编辑器,保持编辑器实例在你控制之下,将 GrapesJS 事件连接到 Angular 应用逻辑,并通过自己的后端持久化项目数据。

  • GrapesJS 是一个没有框架绑定的普通 JavaScript 库——它只需要一个 DOM 元素,别无他物。
  • 在 ngAfterViewInit 中初始化容器元素后,DOM 中容器元素实际存在。
  • 在ngOnDestroy中ngOnDestroy它,这样监听器、DOM节点和撤销堆栈都会随组件一起释放。
  • 如果需要多个组件与编辑器实例通信,就保留在服务中。
  • 编辑器事件是普通的发射点事件——根据你的应用需求,将它们桥接到信号、可观测量或NgZone。
  • 存储和资源归你所有:GrapesJS调用你的终端,它不提供后端。

没有官方的 Angular 封装器。GrapesJS 项目维护的是 React 封装器,没有 Angular 对应的封装,因此 Angular 集成意味着编写组件——大约是下面示例中的三十行——或根据第三方包本身的优劣进行评估。

跳转到工作示例
劳动分工

Angular的GrapesJS是什么?

GrapesJS 是视觉编辑引擎。Angular 是应用框架。几乎所有集成问题都是基于期望其中一方完成另一方的工作,因此明确区分两者之间的界限是值得的。

Angular

Angular 拥有该应用

画布周围的一切:用户是谁,他们被允许做什么,以及结果会走向何方。

  • 路由
  • 认证
  • 应用 UI
  • 用户
  • 权限
  • 账单
  • API 层
  • 应用状态
GrapesJS

GrapesJS 拥有编辑界面

画布内部的所有内容:可以拖拽、选择、样式和导出的内容。

  • 画布
  • 组件
  • 区块
  • 样式管理器
  • 资源管理器
  • 命令
  • 存储管理器
  • 设备管理器

Angular 是你的应用。GrapesJS 是你的视觉编辑引擎。

设置

在Angular中安装GrapesJS

一个依赖和一个样式表。GrapesJS 自带 CSS,没有它编辑器会挂载,但渲染为无样式标记——这是首次集成看起来出问题的最常见原因。

1 — 添加依赖关系

npm install grapesjs

2 — 注册样式表

angular.jsonjson
// angular.json → projects.<app>.architect.build.options
{
  "styles": [
    "src/styles.css",
    "node_modules/grapesjs/dist/css/grapes.min.css"
  ]
}

在angular.json注册样式表后,会全局应用样式表。如果编辑器存在于一个懒惰加载的路由上,而你又不想在各处都为CSS付费,那就把它导入编辑器组件自己的样式表里。

为什么是ngAfterViewInit?

GrapesJS 需要一个真实元素来附加,并且在初始化时测量该元素。在 ngOnInit 中,模板尚未渲染,因此@ViewChild引用仍未定义,调用会直接失败。ngAfterViewInit 在 Angular 创建组件视图后运行,这也是容器首次出现的时刻。同样的推理也解释了下面样式表中的高度规则:没有解析高度的元素会生成一个编辑器,初始化时不会出错且不显示任何内容。

在你写组件之前

这个容器不是你的

GrapesJS 替换你递交给它的元素的内容。不要在该元素中渲染 Angular 模板内容,也不要绑定到该元素下的任何内容。

给它一个高度

主机元素在初始化前需要一个已解析的高度。零高度的容器会挂载一个你看不到的编辑器。

每个组件一个实例

保留一个对编辑器的引用,然后用ngOnDestroyngOnDestroy它。一条路线上两个实时实例会争夺键盘快捷键。

包含多种类型

grapesjs 包自带 TypeScript 定义,因此 Editor、Component 和 ProjectData 可以导入而无需单独的 @types 包。

简约工作示例

完整的 Angular 编辑器组件

三个文件,没有生成,也没有隐藏。将它们复制到Angular应用程序中,你就有一个可用的可视化编辑器在路由上。本页后面的所有内容——存储、资源、自定义组件、插件——都是对该组件的补充,而非重写。

editor.component.tsts
// editor.component.ts
import {
  AfterViewInit,
  Component,
  ElementRef,
  OnDestroy,
  ViewChild,
} from '@angular/core';
import grapesjs, { type Editor } from 'grapesjs';

@Component({
  selector: 'app-editor',
  templateUrl: './editor.component.html',
  styleUrl: './editor.component.css',
})
export class EditorComponent implements AfterViewInit, OnDestroy {
  // Resolved by the time ngAfterViewInit runs — that is the whole reason
  // initialisation lives there and not in ngOnInit.
  @ViewChild('editorHost') private host!: ElementRef<HTMLDivElement>;

  private editor?: Editor;

  ngAfterViewInit(): void {
    this.editor = grapesjs.init({
      container: this.host.nativeElement,
      height: '100%',
      width: 'auto',
      fromElement: false,
      // No persistence yet. Wire your own API before shipping — see Storage.
      storageManager: false,
      blockManager: {
        blocks: [
          {
            id: 'section',
            label: 'Section',
            category: 'Layout',
            content: '<section class="section"><h2>Headline</h2></section>',
          },
          {
            id: 'text',
            label: 'Text',
            category: 'Basic',
            content: { type: 'text', content: 'Edit me' },
          },
          { id: 'image', label: 'Image', category: 'Basic', content: { type: 'image' } },
        ],
      },
    });
  }

  ngOnDestroy(): void {
    // GrapesJS owns DOM nodes, document listeners and an undo stack. Angular
    // removes the host element; only destroy() releases the rest.
    this.editor?.destroy();
    this.editor = undefined;
  }
}
editor.component.htmlhtml
<!-- editor.component.html -->
<!-- GrapesJS takes this element over completely. Do not render Angular
     content inside it — the editor owns everything below #editorHost. -->
<div class="editor-shell">
  <div #editorHost class="editor-host"></div>
</div>
editor.component.csscss
/* editor.component.css */
.editor-shell {
  display: block;
  block-size: 100dvh;
}

/* GrapesJS measures its container at init. A host with no resolved height
   produces an editor that mounts successfully and renders nothing. */
.editor-host {
  block-size: 100%;
}

这就是整个集成过程。值得重新阅读的是两个生命周期钩子:ngAfterViewInit 中的初始化,因为容器必须先存在;而 ngOnDestroy 中的 destroy(),因为 Angular 移除了主元素后,文档中附带的 GrapesJS 不会释放。

把编辑器单独加载到自己的路由上

编辑器是大多数包含编辑器的应用中最重的部分。把它放在加载得很懒散的路由后面,可以避免每个从未打开它的用户进入初始捆包。

app.routes.tsts
// app.routes.ts — the editor is the heaviest route in the app. Load it last.
import type { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', loadComponent: () => import('./home/home.component').then((m) => m.HomeComponent) },
  {
    path: 'pages/:id/edit',
    loadComponent: () => import('./editor/editor.component').then((m) => m.EditorComponent),
  },
];
生命周期

将GrapesJS与Angular生命周期集成

编辑器的生命周期必须绑定到组件的生命周期。四个部件负责这项工作,每一个缺失时都会以可识别的方式失败。

@ViewChild

获取容器元素的引用。通过模板引用变量查询,而不是通过CSS选择器,这样标记重构就无法无声地破坏查找。

ngAfterViewInit

组件视图之后运行的第一个钩子存在。在这里初始化。提前初始化会得到一个未定义的元素引用。

编辑器实例

把它放在私有字段,或者如果工具栏组件需要触发命令,就把它保存在服务里。之后你做的所有操作都经过这个对象。

ngOnDestroy

呼叫editor.destroy()。它会移除编辑器的DOM,分离文档级监听器,并丢弃撤销堆栈。跳过它会导致泄漏,但能通过导航存活下来。

在编辑器容器尚未存在之前,不要初始化GrapesJS。

这也是大多数“GrapesJS 在 Angular 中无法工作”报告的原因。在 ngOnInit 以及任何构造函数中,视图尚未被创建——容器引用未定义,初始化会抛出。如果容器位于 @if 块或延迟视图内,ngAfterViewInit 也不够:等该分支实际渲染完后再初始化。

变化检测

GrapesJS 和 Angular 变化检测

GrapesJS 将大量监听器附加到画布上,并在组件被拖拽或调整大小时持续触发监听器。在基于区域的 Angular 应用中,每个监听器都会安排一次变更检测,即使你的组件树中没有任何变化。

editor.component.ts(节选)ts
// editor.component.ts (excerpt)
import { Component, NgZone, inject, signal } from '@angular/core';

export class EditorComponent {
  private readonly zone = inject(NgZone);

  readonly selectedTag = signal('');
  readonly savedAt = signal<Date | null>(null);

  private mount(host: HTMLElement): void {
    // The canvas fires pointer events continuously while a block is being
    // dragged. Creating the editor outside the zone keeps those from
    // scheduling a change-detection pass on every frame.
    this.zone.runOutsideAngular(() => {
      const editor = grapesjs.init({ container: host, height: '100%' });

      // Re-enter only where editor state has to become Angular state.
      editor.on('component:selected', (component) => {
        this.zone.run(() => this.selectedTag.set(component.get('tagName') ?? ''));
      });

      editor.on('storage:end:store', () => {
        this.zone.run(() => this.savedAt.set(new Date()));
      });

      this.editor = editor;
    });
  }
}

// Zoneless applications (provideZonelessChangeDetection) have no zone to
// leave. Set signals from the same editor events and drop NgZone entirely.
  • 在 NgZone.runOutsideAngular() 内创建编辑器,这样画布活动就不会驱动变更检测。
  • 仅在编辑状态必须变为Angular状态的处理程序中重新输入NgZone.run(),即选择、保存时间戳、验证错误。
  • 如果你的UI里没有显示编辑器状态,你根本不需要重新输入。
  • 在无区域应用(provideZonelessChangeDetection)中,没有区域可离开:写入同一编辑器事件的信号,完全丢弃NgZone。

一个经验法则

初始化时离开该区域一次。在接口监听的特定事件处狭窄地重新进入。在 NgZone.run() 中封装每个编辑器回调,会准确返回你离开区域以避免的变更检测流失。

服务器端渲染

GrapesJS和Angular SSR兼容吗?

编辑器运行在浏览器中。GrapesJS 基于真实的 DOM 构建画布——它测量元素,创建 iframe 并附加文档监听器——因此初始化必须在浏览器上下文中进行,且不能在服务器渲染时运行。仅显示已发布输出的路由不受影响:即普通的 HTML 和 CSS,在服务器上像其他渲染一样进行渲染。

editor.component.tsts
// editor.component.ts — browser-only initialisation
import {
  AfterViewInit,
  Component,
  ElementRef,
  OnDestroy,
  PLATFORM_ID,
  ViewChild,
  inject,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import type { Editor } from 'grapesjs';

export class EditorComponent implements AfterViewInit, OnDestroy {
  @ViewChild('editorHost') private host!: ElementRef<HTMLDivElement>;

  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
  private editor?: Editor;

  async ngAfterViewInit(): Promise<void> {
    if (!this.isBrowser) return;

    // A dynamic import keeps the editor out of the server bundle and off the
    // critical path of every route that never opens it.
    const { default: grapesjs } = await import('grapesjs');
    this.editor = grapesjs.init({ container: this.host.nativeElement });
  }

  ngOnDestroy(): void {
    this.editor?.destroy();
  }
}
  • Guard初始化,只在浏览器中运行——平台检查和下面的渲染钩子都能做到这一点。
  • 动态导入库,这样它就永远不会进入服务器捆绑包。
  • 在服务器传递过程中渲染相同大小的占位符,以避免编辑器出现时布局发生变化。
  • 发布的路由只渲染存储的 HTML 和 CSS 完全不需要防护。

渲染钩子的替代方案

最近的 Angular 版本提供了渲染钩子,其回调仅在浏览器平台上运行,绝不会在服务器上运行。在可用的情况下,它表达了同样的保证,无需平台检查,并且自然地与基于信号的视图查询匹配。

editor.component.ts(渲染钩子)ts
// The same guard, expressed with the render hook instead of a platform check.
import { Component, ElementRef, afterNextRender, viewChild } from '@angular/core';
import type { Editor } from 'grapesjs';

@Component({ selector: 'app-editor', template: '<div #editorHost></div>' })
export class EditorComponent {
  private readonly host = viewChild.required<ElementRef<HTMLDivElement>>('editorHost');
  private editor?: Editor;

  constructor() {
    // afterNextRender callbacks run on browser platforms only — they never
    // execute on the server, so no isPlatformBrowser check is needed.
    afterNextRender(async () => {
      const { default: grapesjs } = await import('grapesjs');
      this.editor = grapesjs.init({ container: this.host().nativeElement });
    });
  }
}

在选择之前,请先确认你的Angular版本支持哪种。这两种模式的存在都是为了执行同一规则——编辑器在浏览器中初始化,其他地方都不做。 angular.dev

解剖学

用GrapesJS构建一个Angular页面构建器

页面构建器不仅仅是一个功能。它包含九个子系统,必须相互协调,而GrapesJS发布了全部九个。这是概述;产品层级决策——定价、角色、模板、发布流程——都属于下面链接的专用指南。

  • GrapesJS

    画布

    可编辑的表面,在自己的 iframe 中渲染,这样页面样式就不会泄漏到你的管理员 UI 里。

  • GrapesJS

    组件

    每个元素背后的类型模型:它是什么,接受什么,以及它暴露哪些设置。

  • GrapesJS

    区块管理器

    用户拖曳的调色板。块是映射到组件类型的命名内容。

  • GrapesJS

    样式管理器

    CSS 编辑面板,可以配置到每个用户允许更改的属性。

  • 插件

    资源管理器

    图片和媒体选择。它背后的上传端点是你的,所以这里的插件大多是连接到服务提供商。

  • 你的Angular应用

    存储管理器

    加载并保存。GrapesJS 定义了契约并调用你的 API——持久化本身就是你的应用程序。

  • GrapesJS

    命令

    有命名、可调用的操作——撤销、预览、导出,以及你自己注册的工具栏按钮。

  • GrapesJS

    设备管理器

    画布上的响应断点可以预览和样式化。

  • GrapesJS

    面板

    编辑器自有的Chrome——按钮、工具栏和区域,如果编辑器置于现有壳体中,这些都可以替换。

谁来构建它GrapesJS插件你的Angular应用

九款中有八款随编辑器或插件一起发布。不可避免的是存储——它也必须了解你的用户、权限和数据库。

构建一个完整的Angular页面构建器
它所在的位置

Angular 应用程序中的编辑器

从外部往内读,这就是整个集成过程。GrapesJS实例以上的都是普通的Angular;下面的是编辑器在iframe中做自己的工作,你根本不需要动用。
  1. Angular application
  2. Editor route (lazy-loaded)
  3. EditorComponent
  4. GrapesJS instance
  5. Canvas iframe

如果编辑器需要从应用的其他地方访问——比如shell的工具栏、页面列表、预览窗格——那么将初始化转移到可注入服务中,让组件与该服务通信,而不是直接与编辑器沟通。

画布是真正的iframe。这是刻意为之:页面样式不能泄漏到你的管理界面,应用样式也不会渗透到被编辑页面,这正是你想要的隔离,因为两者由不同人设计。

输出

GrapesJS 产生什么?

三个伪影,混用会导致实际数据丢失。项目数据是可编辑文档;HTML 和 CSS 是发布结果。它们不可互换,因为导出设计上是有损的。

项目数据

一个包含组件、样式、页面和资源的JSON文档。这是你存储的内容,以便编辑者可以完全按照原来的状态重新打开页面。把它当作真实的来源。

HTML

页面将被提供时的标记。将其与项目数据一起存储,这样发布时无需启动编辑器,即使编辑器不可用,页面也能渲染。

CSS

编辑器为该页面生成的样式,根据其管理的规则进行范围限制。用 HTML 提供——这两个输出是分成两个文件的输出。

output.tsts
// Three outputs, three different jobs. Do not use one where you need another.

const projectData = editor.getProjectData(); // editable state → store this
const html = editor.getHtml();               // markup         → publish this
const css = editor.getCss();                 // styles         → publish this

// Reopening a saved project restores components, styles, pages and assets.
// Re-parsing exported HTML does not — it loses everything not in the markup.
editor.loadProjectData(projectData);

将导出的 HTML 重新解析回编辑器并不能替代项目数据:它会丢失组件类型、traits、页面结构以及标记中无法表达的其他内容。GrapesJS 是一个编辑器,不是 CMS 或后端——伪影去向以及它们之后发生什么,都是你的应用程序自行决定。

持久性

将GrapesJS项目保存到你的Angular应用中

GrapesJS 定义了契约;你提供端点。 用两个异步方法注册一个命名存储,编辑器会按需调用它们,或者随着编辑数量自动调用。所有关于身份验证、租户和权限的都留在你的Angular服务和API中。
  1. Angular
  2. GrapesJS Storage
  3. HttpClient → REST API
  4. Your backend
  5. Database
project-storage.service.tsts
// project-storage.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import type { Editor, ProjectData } from 'grapesjs';

@Injectable({ providedIn: 'root' })
export class ProjectStorage {
  private readonly http = inject(HttpClient);

  /** Registers a named storage the editor can then autosave into. */
  register(editor: Editor, projectId: string): void {
    editor.Storage.add('gjs-api', {
      load: () =>
        firstValueFrom(this.http.get<ProjectData>(`/api/projects/${projectId}`)),

      store: async (data) => {
        await firstValueFrom(
          this.http.put(`/api/projects/${projectId}`, {
            project: data,          // what the editor reopens
            html: editor.getHtml(), // what the site renders
            css: editor.getCss(),
          })
        );
      },
    });
  }
}
editor.component.ts(节选)ts
// Point the editor at the storage you just registered.
this.storage.register(editor, projectId);

grapesjs.init({
  container: host,
  storageManager: {
    type: 'gjs-api',      // the name passed to Storage.add()
    autosave: true,
    autoload: true,
    stepsBeforeSave: 5,   // batch edits instead of a request per keystroke
  },
});

编辑器从不与你的数据库通信。它调用的是你注册的两个方法,分别调用 HttpClient,HttpClient 调用你的 API,使用与应用中其他请求相同的拦截器和认证。

持久性检查表

在第一次救援前需要做出的五个决定

  1. 1
    JSON 计划

    存储可编辑文档

    将 getProjectData() 输出作为用户重新打开的记录保持。将导出的 HTML 和 CSS 存储在同一写入中,这样发布就没有第二次往返。

  2. 2
    自动保存

    批量写入,不要流式处理

    用stepsBeforeSaveautosave可以把一连串编辑变成一个请求。把步数调到你的API上,而不是演示默认。

  3. 3
    负载

    自动加载,或明确加载

    autoload让编辑器在开始时获取。显式加载则给你一个展示自己旋转器的地方,并且将403作为应用程序错误处理,而不是编辑器错误。

  4. 4
    版本

    历史保存在服务器端

    撤销栈存在浏览器,随着标签页的关闭而消失。如果用户需要恢复昨天的页面,那就是你数据库中的版本表,在商店里写的。

  5. 5
    发布

    保存与发布分开

    存档会更新草稿。发布会将特定版本推广到上线路线——一个独立的终点,带有自己的权限检查。

媒体

管理图片和资源

资源管理器是拣选者,不是文件服务。把它指向你的Angular应用中的某个端点,它会把文件发布在那里;之后的所有操作——验证、存储、调整大小、交付——都由你来实现,这也是你能重用已有媒体库的原因。

editor.component.ts(节选)ts
// Assets go to your endpoint, under your auth — GrapesJS only POSTs files.
grapesjs.init({
  container: host,
  assetManager: {
    upload: `/api/projects/${projectId}/assets`,
    uploadName: 'files',
    multiUpload: true,
    credentials: 'include',
    headers: { 'X-Requested-With': 'XMLHttpRequest' },
    // Off by default is the safer choice: base64 images end up inside saved
    // markup and make every page row grow without limit.
    embedAsBase64: false,
    // Returning false aborts the upload before the request leaves the browser.
    beforeUpload: (files: File[]) =>
      files.every((file) => file.size <= MAX_UPLOAD_BYTES && ALLOWED_TYPES.has(file.type)),
  },
});

editor.on('asset:upload:start', () => this.uploading.set(true));
editor.on('asset:upload:end', () => this.uploading.set(false));
editor.on('asset:upload:error', (error) => this.uploadError.set(String(error)));
  • 上传文件会带着你的头部和凭证一起发送到你的端点,所以应用的认证和应用其他部分相同。
  • 在请求离开浏览器前验证,并在服务器端再次验证——客户端检查是方便,而非控制。
  • 从你的端点返回存储的URL;编辑器会把这些URL放到页面里,所以它们必须是你打算服务的。
  • 除非你想让图片内嵌到保存的标记中,否则关闭 base64 嵌入,因为它们会让每个存储页面无限增长。
  • 从CDN或对象存储中提供资源,并保持编辑器指向公共URL,而不是你的应用起点。
  • 通过分页端点加载大型库,而不是在初始化时向编辑器交付成千上万的资源。

这些都不规定特定的后端。S3、Cloudinary、Laravel磁盘或服务器上的文件夹都满足同一个契约:接受文件,返回URL。

扩展编辑器

创建自定义组件

注册组件类型是编辑了解产品的方式。自定义类型拥有自己的设置面板、拖拽规则和调色板块——这样内容团队就能使用你领域的概念,而不是原始的 div。

pricing-table.type.tsts
// pricing-table.type.ts — an application-specific building block.
import type { Editor } from 'grapesjs';

export function registerPricingTable(editor: Editor): void {
  editor.Components.addType('pricing-table', {
    // Lets the editor recognise the type again when a saved page is reparsed.
    isComponent: (el) => el.classList?.contains('pricing-table'),
    model: {
      defaults: {
        tagName: 'section',
        attributes: { class: 'pricing-table' },
        droppable: false,
        // Traits become the right-hand settings panel for this component.
        traits: [
          { type: 'text', name: 'plan', label: 'Plan name' },
          { type: 'number', name: 'price', label: 'Price' },
          { type: 'checkbox', name: 'featured', label: 'Highlight' },
        ],
        components: `
          <h3 data-gjs-type="text">Team</h3>
          <p data-gjs-type="text">$29 / month</p>
        `,
      },
    },
  });

  // A block is how the component reaches the user: one entry in the palette.
  editor.Blocks.add('pricing-table', {
    label: 'Pricing table',
    category: 'Commerce',
    content: { type: 'pricing-table' },
  });
}

典型的应用特定组件

  • Hero 部分
  • 价格表
  • 产品卡
  • 表单
  • 导航
  • 客户评价
  • 功能网格
  • 任何针对你SaaS的具体问题

自定义类型有两个原因。isComponent 让编辑在重新解析保存页面时能再次识别你的类型,而 traits 则成为非技术用户实际编辑的设置面板。即使是简单的组件,这两者都值得编写。

延长它

用插件扩展GrapesJS

插件是一个接收编辑器实例的函数,所以这里没有任何针对Angular的特殊内容——任何发布给GrapesJS的内容都可以在Angular组件中工作。这是编辑器挂载后,团队首先处理的四个空白点。

构建还是采用

自己做编辑器还是用GrapesJS?

在Angular中构建可视化编辑器是可能的——这是已知的工作量,而非不可能。问题是你想在整个产品生命周期内拥有哪些部分。

功能自己构建GrapesJS
画布自行开发内置
拖拽自行开发内置
组件自行开发内置
区块自行开发内置
样式自行开发内置
资源自行开发内置
存储自行开发可扩展——你提供端点
插件构建生态系统插件架构
Angular 集成原生于你的应用通过组件集成
许可证归你所有BSD-3-Clause — $0 to use

“构建”不是批评——这些都是可以构建的。它是对事后维护者的声明。
本表中的事实重新验证了2026-09-02。

Angular 提供应用框架。GrapesJS 提供视觉编辑引擎。

集成选择

我需要用Angular封装器来做GrapesJS吗?

不,值得理解为什么会出现这个问题。GrapesJS 发布了官方的 React 封装器,所以开发者合理期待有类似的 Angular 包装。但没有。这就剩下三种方法,对大多数团队来说,第一种是正确答案。

推荐阅读

直接集成

你自己编写组件——本页上的组件大约有三十行。你拥有生命周期、变更检测策略和升级路径,应用程序和编辑器API之间没有第三方。

  • 没有额外的依赖来跟上Angular版本的更新
  • 完全访问所有编辑器选项和活动
  • 升级到GrapesJS后立即着陆
第三方

社区包装器

社区包存在,可以缩短第一个小时。像评估依赖一样评估一个:查看谁维护它,声明了哪些Angular和GrapesJS版本,以及发布时间。

  • 非由GrapesJS项目维护
  • 对 Angular 和 GrapesJS 都增加了版本限制
  • 在采用之前,值得对照当前的核心版本进行核对
  • ngx-grapesjs 21.0.0 · 2026-01-15 · peer grapesjs ^0.22.4
对于较大的应用

你自己的服务包装

一旦多个组件需要编辑器——工具栏、页面列表、预览面板——就将初始化迁移到可注入服务中。它与接缝直接集成,并且将编辑器排除在组件树之外。

  • 一个拥有初始和摧毁的机构
  • 编辑器状态以信号或可观测量的形式到达组件
  • 无需挂载编辑器即可测试

GrapesJS 组织维护的是 React 封装器,没有 Angular 包。你找到的任何 Angular 库都是第三方的,尽管非常有用——请将其视为依赖选择,而非 GrapesJS 的一部分。

架构

GrapesJS + Angular 的生产架构

演示是一个组件。产品由四个层组成,编辑器只是其中之一。当编辑器背后有真实用户支持后,大多数团队都会趋向于这种形态。

  1. Angular 应用

    shell、router,还有一个懒散加载的路由,它拥有编辑器。

    • 应用壳
    • 路由器
    • 编辑器路由(懒加载)
  2. GrapesJS 实例

    初始化时配置一次:哪些组件存在,哪些可以拖拽,哪些可以样式化。

    • 组件
    • 区块
    • 样式
    • 资源
    • 命令
  3. 应用服务

    Angular层是编辑器和API之间的一层——也是唯一在客户端做出授权决策的地方。

    • 认证
    • API 客户端
    • 权限
    • 发布
  4. 后端

    文物存在的地方,所有许可都被真正执行。

    • 项目
    • 资源
    • 用户
    • 版本

图中除了第二层外,没有任何内容是GrapesJS特有的。这就是重点:编辑器是普通Angular应用中的一个组件,其余部分是你本来就会构建的架构。

看看团队如何将此打包成产品
性能

性能考量

编辑器是运行在你应用中的一个大型软件。这些地方实际上需要花费。

只有在需要时才初始化

编辑器应在编辑路径打开时创建,而不是在应用开始时。从不编辑的用户绝不应该为此付费。

拉泽加载路线

动态导入将GrapesJS排除在初始捆绑包之外,而初始捆绑包是本列表中最大的杠杆。

别进入那个区域

创建编辑器时使用runOutsideAngular,这样画布指针事件不会在每一帧触发变更检测。

绑定资源库

不要在初始化时给编辑器完整的媒体库。在 API 中分页,按选区要求加载素材。

注意组件树

非常深或非常重复的页面结构会让选择、样式和撤销变慢。更喜欢组件而不是深度嵌套的包装。

批量保存

使用stepsBeforeSave,这样一次编辑的突发发送变成一个请求,而不是每次按键请求一次。

这些都没有承诺的数字。在编辑前后测量一下自己的编辑路线——每个效果的大小取决于你的块、页面和API。

安全性

安全考量

可视化编辑器会把用户变成HTML的作者,然后你的应用再为他们服务。把编辑器里输出的所有内容都当作用户输入,因为它就是用户输入。

Sanitise 发布了 HTML

编辑器输出可以包含任意标记,包括如果启用自定义代码块则包含脚本。在发送给除作者以外的任何人之前,先对服务器端进行净化处理。

验证上传的资源

检查服务器上的类型和规模,而不仅仅是在beforeUpload中。上传内容存储在网页root之外,并从不同的来源发送。

认证存储端点

加载和保存路由是普通的 API 端点。它们需要和应用中其他所有东西相同的认证。

服务器端强制执行权限

在Angular UI中隐藏发布按钮是展示。重要的检查是端点上的。

永远不要相信项目数据

存储的项目JSON可能会被篡改。加载时验证它,而不是假设那就是你写的那个文档。

显式的范围租户

在多租户产品中,项目所有者应从服务器会话中推导出——绝不能从请求体中的 id 中获得。

在你发货之前

Angular + GrapesJS 生产清单

十二点可以区分一个工作演示和真实用户的路径。

  1. 安装GrapesJS

    添加依赖并注册样式表,无论是全局还是在编辑器组件中。

    请阅读该节
  2. 创建视图后初始化

    grapesjs.init() 运行于 ngAfterViewInit,对应一个具有解析高度的容器。

    请阅读该节
  3. 正确销毁编辑器

    在ngOnDestroy中使用editor.destroy(),通过导航无泄漏验证。

    请阅读该节
  4. 处理仅浏览器初始化

    服务器渲染时会被保护,导致任何程序都无法运行,并用占位符代替。

    请阅读该节
  5. 连接存储

    一个命名存储已注册、autosave并与HTML和CSS并存的项目JSON。

    请阅读该节
  6. 配置资源

    上传指向你的端点,关闭 base64 嵌入,双方都进行验证。

    请阅读该节
  7. 添加自定义组件

    你产品自身的构建单元被注册为类型,带有traits和调色板条目。

    请阅读该节
  8. 添加你需要的插件

    区块、富文本和导出都是有意选择的,而不是累积的。

    请阅读该节
  9. 实现权限

    谁可以编辑,谁可以发布——这些都被服务器强制执行,反映在UI中。

    请阅读该节
  10. 净化已发布内容

    服务器端的页面保存与向访客提供之间的净化处理。

    请阅读该节
  11. 监视器编辑器错误

    编辑器异常报告到与你Angular其他错误相同的地方。

  12. 测试响应式编辑

    设备管理器在触摸设备上运行,而不仅仅是在桌面浏览器窗口中。

    请阅读该节
FAQ

GrapesJS 与 Angular:常见问题

下一步

用GrapesJS构建你的Angular可视化编辑器

从开源的可视化编辑器开始,将其连接到你的Angular应用,然后用产品所需的插件和集成进行扩展。

开发者

开始

安装包,复制组件,并让编辑器在今天下午运行 Angular 路由。

开始
延伸

浏览插件

块、存储、富文本、资源、导出和电子邮件——这些都是你本来会自己写的部分。

浏览插件
团队

规划产品

将编辑器作为应用中的一个功能范围:角色、模板、发布和版本管理。

开始写简报

用Angular构建应用程序。用GrapesJS构建可视化编辑器。用GJS.Market插件扩展。