
在前端开发中,GrapesJS 作为一款可视化页面搭建工具,可以说是非常"务实"的存在:界面拖拽、实时预览、源码编辑等功能都很完备,极大地降低了开发和维护静态页面的成本。然而,GrapesJS 在处理 动态内容 时却往往让人陷入困境:用它做静态页面得心应手,可一旦遇到需要根据交互逻辑动态维护内容的需求,转而就需要大量二次开发,才能让动态部分和静态页面完美结合。
今天分享的这篇文章,就来"现身说法"------如何在 GrapesJS 中开发一个动态渲染的 Drawer 弹框组件 。我们会通过一个多页面的设计思路,讲解如何巧妙分离"静态设计"与"动态交互"这两个互相矛盾又息息相关的需求。希望可以帮助更多的小伙伴快速上手。
一、为什么需要动态 Drawer ?
1.1 项目背景和痛点
想象一个实际业务场景:我们需要在页面中加入一个右侧弹出的信息填写弹窗(Drawer),用于展示或编辑一份动态获取的数据表单。对于前端和 UI 设计师来说,大多喜欢直接在页面设计中预览这个 Drawer 的大致样式,最好还能在拖拽组件时就能看到它的最终效果。但现实是,Drawer 中的内容是要根据后端接口返回的信息进行动态渲染的,GrapesJS 并不直接支持这种"动态可视化"。
那怎么办?假如我在已有的 GrapesJS 设计器中强行塞入这个 Drawer,要么 我得提前写好占位的静态内容,要么在视觉设计时完全展示不出来------都很别扭,也很容易导致后续联调或维护上的混乱。
1.2 多页面模式的思路
正如开始所说,前端可视化搭建往往离不开额外的配置或二次开发,来处理那些"静态 + 动态"的场景冲突。我们在项目中找出的一个巧妙思路 是:将这个 Drawer 的静态结构和需要动态渲染的表单视图------拆分到不同的页面进行开发。
- PageA:主页面(GrapesJS 设计完的正常功能页)。在这个页面中,我们只需要留出一个按钮或者链接,告诉系统:"当我点击这个按钮时,会呼出一个 Drawer"。
- PageB:Drawer 组件所在的页面。在这个页面上,设计师可以独立使用 GrapesJS(或其他工具)进行表单、文本、样式的可视化设计。因为这一块内容是像一个完整的"小页面",就更容易在 GrapesJS 里独立制作。在实际运行时再通过 Node.js 渲染成 HTML,再动态插入到应用中。
如此,通过"多页面"的方式,就让主页面和弹窗页面各司其职:主页面只负责静态排版和调用逻辑,而弹窗页面则专注于动态样式和内容的可视化。最终再通过一段加载逻辑,将弹窗与主页面串联起来。
二、核心思路 + 代码原理
下面是我们项目中的关键代码片段,给大家做参考。该组件基于 Vue3 + Ant Design Vue 构建,将 Drawer 组件的调用和渲染拆分到两个部分:
- 入口函数 :
showDrawer
,负责将我们真正的 Drawer 组件(AndDrawer.vue
)动态挂载到页面上。 - Drawer 组件 :
AndDrawer.vue
,内部会根据后端返回的 HTML 和 CSS,在运行时进行插入渲染。
2.1 入口:showDrawer(opts: DrawerProps)
const showDrawer = (opts: DrawerProps) => {
Promise.all([
import('@/views/system/function/editor/components/AndDrawer.vue'),
]).then(([vueModule]) => {
let app: App | null = null;
let mountNode: HTMLDivElement | null = null;
const drawerProps = reactive({
...opts,
onClose: () => {
app?.unmount(); // 通过可选链安全访问
mountNode?.remove(); // 更安全的 DOM 移除方式
}
});
app = createApp({
render() {
return h(ConfigProvider, { locale: zhCN }, () =>
h(vueModule.default, { drawerProps, onClose: drawerProps.onClose })
);
},
});
mountNode = document.createElement('div');
document.body.appendChild(mountNode);
app.use(Antd);
app.mount(mountNode);
}).catch((err) => {
console.error('Failed to mount Iterator component:', err);
});
};
从代码可以看出,这段逻辑在需要的时候,才会异步请求并挂载我们真正的 AndDrawer.vue
组件(这也是为什么可以把它放在另外一个"PageB"里,或独立的模块中)。这样,在主页面中只需在合适的时机调用 showDrawer()
,就能瞬时展示一个右侧弹窗。
2.1.1 为什么要 Promise + 动态 import?
因为我们期望 Drawer 组件只有在真正需要时才被加载,而不是主页面一加载就把所有依赖都打包进来。也方便后续独立维护和版本管理。
2.2 动态 Drawer 组件:AndDrawer.vue
<template>
<a-drawer v-model:open="open" :title="drawerProps.title" placement="right" :width="drawerProps.width">
<div ref="container"></div>
</a-drawer>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { type DrawerProps } from '@/views/system/function/common/CommonTypes';
import { defHttp } from '/@/utils/http/axios';
import { appendHTMLToFirstElementByClass } from '@/views/system/function/utils/Base';
const open = ref<boolean>(true);
// 组件属性
const props = withDefaults(defineProps<{
drawerProps: DrawerProps;
}>(), {
drawerProps: () => (
{ width: 600, title: '默认标题', placement: 'right' }
)
});
const container = ref<HTMLElement>();
onMounted(() => {
query();
});
const query = () => {
let params = {
module_name: props.drawerProps.moduleName,
comp_id: props.drawerProps.compId,
clone_num: 1
}
// 请求后端 Node.js 的生成接口
defHttp.post({ url: `/generate_ui/ui/generate_html`, params }, { isTransformResponse: false })
.then(r => {
if (r.success) {
appendElement(r);
}
})
.catch(e => {
console.log(e);
})
.finally(() => {
});
};
const appendElement = (r) => {
let containerElement = container.value;
appendHTMLToFirstElementByClass(
containerElement as HTMLElement,
r.html,
r.css,
{ preserveExistingContent: false }
)
};
</script>
<style scoped></style>
简要来看,这个组件所做的事情就是:
- 先借助
props.drawerProps
中的参数,来决定 Drawer 的基本属性(宽度、标题、placement 等)。 onMounted
阶段调用query()
接口,去 Node.js 后端 生成或获取该弹窗要动态渲染的 HTML 片段和对应的 CSS。- 把后端返回的内容插进
div ref="container"
里,实现 "页面设计" + "实时数据" 的融合。
至于 generate_html
接口背后是如何实现的,就不在本文过多展开。大致来说:这是一个在 Node.js 端"无头"环境里,将 PageB 通过类似 puppeteer / ejs / handleBars 等模板技术进行渲染处理,然后把结果打包成 HTML + CSS,由 Vue
接管并注入到页面中。
三、解决了哪些问题?
3.1 前后端标准分离 + 动态渲染
痛点:如果把所有对动态内容的处理都写死在 GrapesJS 中,后续维护非常困难,而且 GrapesJS 也并不适合做复杂的数据交互逻辑。
解决:通过"多页面"+"动态挂载"的思路,我们把 Drawer 的业务逻辑、可视化布局、以及动态接口调用都分散到更合适的地方。让 GrapesJS 只负责静态部分的设计,实现高度解耦。
3.2 静态可视化 & 动态展示 "两全其美"
痛点:在 GrapesJS 上直接预览动态数据是不现实的,或者需要值班开发手动写 mock。 这样维护量大,还不一定跟真正的后端接口对得上。
解决:把需要动态加载的区域放到另一个可视化页面设计中,分别各自专注于"静态"和"动态"部分。需要展示时,通过"动态 drawer"拉过来即可,避免前期搭建时的臃肿,也让"设计师"或"运营"只需要在对应页面专心做可视化设计。
3.3 模块化 & 复用
痛点:很多时候我们需要的 Drawer 组件不仅是一个页面用,还可能多个业务场景公用,或者在不同项目里复用。
解决 :一个"动态挂载"的通用 Drawer 组件,只需传入对应的参数,比如大小、标题、内容生成 API 地址......其余都由我们统一封装。这样就像"装配线"一样,无论在哪个页面,只要调用了 showDrawer
,就能快速拉起同样的 Drawer 结构。
四、背后的挑战 & 实践心得
不得不说,在最初做这套方案时,也遇到过不少痛点和"坑":
- 动态 import 兼容性:早期某些低版本浏览器或打包工具对动态 import 不太友好,需要我们配置 Babel 或 Vite/webpack 的相应设置。
- Node.js 模板渲染:如何在 Node.js 里无头地渲染出我们想要的页面片段,需要一定的插件或技术栈,比如 puppeteer、ejs 或其他 SSR 方案。要保证渲染出的 HTML/CSS 能在前端畅通无阻地插入。
- 样式冲突:在把样式插入到主页面时,可能会遇到命名冲突或层级冲突问题,需要借助类似 CSS Modules、Scoped CSS 或者最简单的 BEM 命名规范来避免。
- 通信与交互 :如果 Drawer 内部还牵涉到一些业务交互,比如数据回调到主页面,需要借助事件总线、或者把通信事件都封装到
DrawerProps
里,避免耦合。
但解决完这一系列的问题后,随之而来的,是模块化 、可复用 、解耦程度都有了大幅的提升。尤其是当项目发展到一定规模时,这种"多页面 + 动态弹窗合并"的模式变得很有价值。
五、总结 & 展望
这篇文章从一个比较具体的场景出发,介绍了如何让 GrapesJS + Vue3 也能轻松处理"动态 Drawer"需求的思路。通过拆分到多页面的方式,借助 Node.js 在后台对页面进行无头渲染,然后在需要的场景中再用一段通用的挂载脚本去"拉起"这个动态 Drawer。看似复杂,实则为后续的功能扩展提供了相对清晰的思路与实施路径。
回顾一下,这样做的核心好处就是:
- 将静态与动态部分分开,降低对 GrapesJS 的侵入;
- 让主页面的设计过程保持简单;
- Drawer 部分的动态更新逻辑也能专门维护,升级更自由。
如果你也在使用 GrapesJS 或者其它低代码工具,并苦于如何处理动态渲染、不想写很多 mocked 占位,那么不妨试试这种"多页面 + 动态挂载"的玩法。既能不破坏已有的可视化体验,又能让"真实"逻辑"后期注入",十分灵活。
希望这次的分享能帮助到你 ,让开发者与设计师在同一个环境里更愉快地合作,也让你的前端项目在面对动静结合的场景时游刃有余。
如果你也有其他关于 GrapesJS 动态内容渲染或二次开发的经验,欢迎留言、交流。让我们一起在可视化搭建领域持续探索与进步!