完美!现在让我为你讲解第五阶段:工作流完成与数据导出系统。这是把低代码平台的成果转化为可用产品的关键阶段。
🎉 第五阶段深入:从编辑到发布的完整工作流
第一部分:工作流的三个阶段
低代码平台的生命周期分为三个阶段:
┌─────────────────────────────────────────────────────────────┐
│ 阶段 1️⃣ 编辑阶段(Editing) │
│ 路由:/ │
│ 组件:Center.vue(完整编辑器) │
│ 特点:可拖拽、可修改、支持撤销/重做 │
└─────────────────────────────────��───────────────────────────┘
↓ 用户点击"发布"
┌─────────────────────────────────────────────────────────────┐
│ 阶段 2️⃣ 服务器存储阶段(Upload) │
│ 接口:POST /api/register │
│ 请求:{ id: nanoid(), result: JSON.stringify(views) } │
│ 响应:{ success: true, link: '/release/:id' } │
└─────────────────────────────────────────────────────────────┘
↓ 用户分享链接或点击预览
┌─────────────────────────────────────────────────────────────┐
│ 阶段 3️⃣ 发布阶段(Release) │
│ 路由:/release/:id │
│ 组件:Release.vue(只读预览) │
│ 特点:只读、不可编辑、快速加载 │
└─────────────────────────────────────────────────────────────┘
第二部分:发布流程的完整代码路径
步骤 1:用户点击"发布"按钮
<!-- Top.vue -->
<button @click="release" type="primary">发 布</button>
methods: {
release() {
this.$bus.emit("release") // 发送事件
}
}
步骤 2:centerEvent.js 接收事件并处理
// centerEvent.js
function release(that) {
that.$bus.on("release", () => {
// 生成唯一 ID
let id = nanoid()
// 构建 FormData(用于 multipart/form-data 上传)
let form = new FormData()
form.append('id', id)
form.append('result', JSON.stringify(that.views))
// 调用后端 API
that.$axios(register(form))
.then((res) => {
// 生成可访问的链接
that.src = window.location.origin + '/release/' + id
// 显示预览/分享窗口
that.sendVisiable = true
})
})
}
步骤 3:后端存储与返回链接
// src/api/sendOut.js
export function register(data) {
return {
url: 'http://47.95.23.74:3001/user/register', // 后端 API
method: 'post',
data: data
}
}
后端接口期望:
// 请求
POST /user/register
Content-Type: multipart/form-data
id: "abc123..."
result: "[{component: 'TextCom', ...}, ...]"
// 响应
{
success: true,
message: "发布成功",
data: {
id: "abc123...",
link: "/release/abc123...",
url: "http://example.com/release/abc123..."
}
}
步骤 4:展示预览窗口(ShowDialog.vue)
<!-- Center.vue -->
<ShowDialog v-model:sendVisiable="sendVisiable" :src="src" />
<!-- ShowDialog.vue -->
<template>
<el-dialog
:model-value="sendVisiable"
@update:model-value="val => $emit('update:sendVisiable', val)"
title="页面已发布"
width="400px"
>
<div>
<p>您的页面已成功发布!</p>
<p>访问链接:<a :href="src" target="_blank">{{ src }}</a></p>
<p>二维码:<img src="..." /></p>
</div>
</el-dialog>
</template>
第三部分:Release.vue - 发布后的预览页面
工作原理
Release.vue 是一个只读的预览器,根据路由参数 :id 加载对应的页面配置。
<template>
<div class="center" :class="{phoneSize: isPhone}">
<!-- 使用 PowerfulDynamicDraw 渲染,但 edit=false(只读) -->
<PowerfulDynamicDraw
:edit="false" ⬅️ 关键:禁用编辑
:views="views" ⬅️ 从服务器加载的配置
></PowerfulDynamicDraw>
</div>
</template>
<script>
import { getOne } from '@/api/sendOut.js'
export default {
data() {
return {
edit: false, // ⬅️ 强制只读模式
isPhone: false,
views: []
}
},
created() {
// ① 从 URL 参数中获取 ID
const id = this.$route.params.id
// ② 先查询本地缓存(加快速度)
const cache = localStorage.getItem(String(id))
if (cache) {
this.views = JSON.parse(cache)
return
}
// ③ 缓存不存在则从服务器获取
const form = new FormData()
form.append('id', id)
this.$axios(getOne(form))
.then((res) => {
// 解析响应中的 views 数据
this.views = JSON.parse(res.data.result.result)
// ④ 缓存到本地(下次快速加载)
localStorage.setItem(String(id), JSON.stringify(this.views))
})
.catch(() => {
// 获取失败时,使用最后保存的状态
this.views = JSON.parse(localStorage.getItem('1'))
})
}
}
</script>
关键特性:
缓存策略:
① localStorage[id] 存在?→ 直接使用(最快)
② 不存在?→ 向服务器请求(网络请求)
③ 服务器不可用?→ 降级到 localStorage['1'](最后保存的编辑状态)
Release.vue 与 Center.vue 的对比
| 特性 | Center.vue(编辑器) | Release.vue(预览) |
|---|---|---|
| 路由 | / | /release/:id |
edit 值 | true | false |
| 可拖拽 | ✅ 是 | ❌ 否 |
| 可编辑 | ✅ 是 | ❌ 否 |
| 显示边框 | ✅ 是 | ❌ 否 |
| 数据来源 | localStorage | 服务器 API |
| 组件 | 完整编辑界面 | 仅渲染组件 |
第四部分:数据导出系统
三种导出格式
导出
├─ HTML:独立 HTML 文件(可直接在浏览器打开)
├─ Vue:Vue 单文件组件(可集成到其他 Vue 项目)
└─ JSON:原始数据格式(可再次导入编辑)
导出 HTML / Vue 的工作流
// exportHtml.js
export function exportHtmlHandle(views, isVue) {
// 1. 创建空 DOM 容器
let bodyRes = document.createElement('div')
bodyRes.classList.add('canvas')
// 2. 递归遍历 views,将每个组件转换为 HTML DOM
for (let i = 0; i < views.length; i++) {
bodyRes.appendChild(getHtmlRes(views[i]))
}
// 3. 提取所有样式为 CSS 字符串
let styleStr = ''
// 4. 调用 jsonToHtml 转换为完整 HTML/Vue 文件
let res = jsonToHtml['getMainStr'](bodyRes, styleStr, isVue)
// 5. 使用 file-saver 下载文件
let blob = new Blob([res], { type: '' })
let fileName = isVue ? 'page.vue' : 'page.html'
FileSaver.saveAs(blob, fileName)
}
转换流程图:
views 数组(JSON)
↓
遍历并转换为 HTML DOM
↓
提取所有样式为 CSS
↓
组合成完整的 HTML / Vue 文件
↓
下载为文件
完整代码深入分析
第 1 步:遍历 views 并转换为 DOM
function getHtmlRes(view) {
// ① 检查组件类型是否支持导出
let set = new Set(['FlexBox', 'ButtonCom', 'TextCom', 'VideoCom', 'LinkCom', 'ImgCom'])
if (!set.has(view.component)) return null
// ② 调用对应的转换函数(来自 jsonToHtml.js)
// 例如:对于 TextCom,调用 jsonToHtml['TextCom'](view, index)
let tempDom = jsonToHtml[view.component](view, baseIndex)
let childDom = tempDom.cloneNode(true)
// ③ 转换外层样式为 CSS 类
let tempStr = styleToStr(view.style)
styleStr += `.block-${baseIndex++} { ${tempStr} }`
// ④ 对于基础组件,还要转换内层样式
if (view.component != 'FlexBox') {
let sonStr = styleToStr(view[view.sonStyle])
styleStr += `.block-${baseIndex++} { ${sonStr} }`
}
// ⑤ 递归处理子组件(用于 FlexBox)
if (view.children) {
for (let i = 0; i < view.children.length; i++) {
let newDom = getHtmlRes(view.children[i])
if (newDom) childDom.appendChild(newDom)
}
}
return childDom
}
第 2 步:jsonToHtml.js 中的组件转换函数
// 每个组件都有对应的转换函数
function getTextComDom(view, index) {
// 1. 创建 HTML 结构
let basicDom = strToDom(HtmlData['TextHtml'])[0]
// 2. 添加 CSS 类名(用于样式)
basicDom.classList.add(`block-${index}`)
basicDom.childNodes[0].classList.add(`block-${index + 1}`)
// 3. 填充实际内容
basicDom.childNodes[0].innerHTML = view.content
return basicDom
}
function getButtonComDom(view, index) {
let basicDom = strToDom(HtmlData['ButtonHtml'])[0]
basicDom.classList.add(`block-${index}`)
basicDom.childNodes[0].classList.add(`block-${index + 1}`)
basicDom.childNodes[0].innerHTML = view.props.content
return basicDom
}
function getImgComDom(view, index) {
let basicDom = strToDom(HtmlData['ImgHtml'])[0]
basicDom.classList.add(`block-${index}`)
basicDom.childNodes[0].classList.add(`block-${index + 1}`)
basicDom.childNodes[0].src = view.src // ⬅️ 填充图片 URL
return basicDom
}
function getFlexBoxDom(view, index) {
let basicDom = strToDom(HtmlData['FlexBoxHtml'])[0]
basicDom.classList.add(`block-${index}`)
basicDom.innerHTML = view.props.content
return basicDom
}
第 3 步:生成完整 HTML 文件
// htmlData.js
function BasicHtml(bodyStr, styleStr) {
return `<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lowcode Export</title>
</head>
<body>
<style>
/* 全局样式 */
html, body {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
body {
display: flex;
position: relative;
width: 100%;
height: 100%;
}
.myflexbox {
position: relative !important;
display: flex !important;
overflow: hidden !important;
}
.canvas {
margin: 5px 10px 0 10px;
position: relative;
width: 100%;
display: flex;
flex-direction: column;
}
/* 组件特定样式 */
${styleStr}
</style>
<!-- 生成的页面内容 -->
${bodyStr}
</body>
</html>`
}
导出流程示例
假设用户有这样的 views:
views = [
{
component: 'TextCom',
id: 'text_1',
content: 'Hello World',
style: { width: '300px', height: '100px' },
textStyle: { fontSize: '20px', color: 'red' }
}
]
导出后的 HTML:
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<style>
.block-1 {
width: 300px;
height: 100px;
}
.block-2 {
font-size: 20px;
color: red;
}
</style>
<div class="canvas">
<div class="block-1">
<p class="block-2">Hello World</p>
</div>
</div>
</body>
</html>
导出 Vue 组件
function BasicVue(bodyStr, styleStr) {
return `<template>
${bodyStr}
</template>
<script>
export default {
data() {
return {}
}
}
</script>
<style scoped>
.myflexbox {
position: relative !important;
display: flex !important;
overflow: hidden !important;
}
.canvas {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
${styleStr}
</style>`
}
这样可以生成一个完整的 Vue 单文件组件,可以直接在其他 Vue 项目中使用:
// 在其他项目中
import MyPage from './page.vue'
export default {
components: { MyPage }
}
导出 JSON
// PageHeader.vue
sendSaveJsonEvent() {
this.$bus.emit('saveJson')
}
// centerEvent.js
function saveJson(that) {
that.$bus.on("saveJson", () => {
that.getJson()
})
}
// Center.vue
getJson() {
// 直接序列化 views 数组
const data = JSON.stringify(this.views)
const blob = new Blob([data], { type: '' })
FileSaver.saveAs(blob, 'page.json')
}
导出的 JSON 可以稍后导入重新编辑:
// 在编辑器中导入
const ImportJSON = JSON.parse(fileContent)
this.views = ImportJSON // 恢复原状态
第五部分:SetExample.vue - 示例加载器
作用
SetExample.vue 是一个临时跳转组件,用于从示例库加载预定义的页面配置。
<script>
export default {
created() {
// 从 URL 参数中获取示例 ID
const id = this.$route.params.num
// 从服务器获取该示例的配置
const form = new FormData()
form.append('id', id)
this.$axios({
url: 'http://47.95.23.74:3001/user/getOne',
method: 'post',
data: form
}).then(res => {
// 将示例配置保存到 localStorage
localStorage.setItem('views', res.data.result.result)
// 跳转回编辑器主页
this.$router.replace({ path: '/' })
})
}
}
</script>
使用场景:
用户访问 /example/123
↓
SetExample.vue 在 created 中执行
↓
从服务器获取示例 ID=123 的配置
↓
保存到 localStorage['views']
↓
自动跳转到 /(编辑器)
↓
编辑器加载 localStorage['views'],显示示例配置
↓
用户可以在此基础上修改或创建新页面
🎯 完整端到端流程(可背诵版本)
场景:用户完整地使用低代码平台
┌─────────────────────────────────────────────────────────────┐
│ 1️⃣ 编辑阶段 │
├─────────────────────────────────────────────────────────────┤
│ 用户访问 http://example.com/
│ → 页面加载编辑器
│ → 从 localStorage 恢复之前保存的页面(如果有)
│ → 或从空白开始创建新页面
│ → 用户拖拽组件、修改属性、撤销/重做
│ → 点击"保存",将状态存到 localStorage
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2️⃣ 导出阶段(可选) │
├─────────────────────────────────────────────────────────────┤
│ 用户可以选择三种导出方式:
│ ① 导出 HTML:下载 page.html(可直接在浏览器打开)
│ ② 导出 Vue:下载 page.vue(可集成到项目中)
│ ③ 导出 JSON:下载 page.json(可稍后重新导入)
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3️⃣ 发布阶段 │
├─────────────────────────────────────────────────────────────┤
│ 用户点击"发布"按钮
│ → Center.vue 发送 release 事件
│ → centerEvent.js 生成唯一 ID
│ → 将 views 数据 POST 到 /api/register
│ → 后端存储并返回 /release/:id 链接
│ → 显示 ShowDialog,可分享链接或生成二维码
│ → 用户复制链接分享给他人
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4️⃣ 预览/分享阶段 │
├─────────────────────────────────────────────────────────────┤
│ 其他用户访问 http://example.com/release/abc123
│ → Release.vue created() 获取 URL 参数 :id
│ → 查询 localStorage[id](有缓存则直接用)
│ → 无缓存则 GET /api/getOne?id=abc123
│ → 后端返回该 ID 对应的 views 数据
│ → Release.vue 用 PowerfulDynamicDraw 渲染(edit=false)
│ → 用户看到最终的只读预览页面
└─────────────────────────────────────────────────────────────┘
📋 架构对比:编辑器 vs 发布器
编辑器(Center.vue) 发布器(Release.vue)
═══════════════════════════════════════════════════════════
路由:/ 路由:/release/:id
edit: true edit: false
左侧面板:✅ 显示 左侧面板:❌ 隐藏
右侧面板:✅ 显示 右侧面板:❌ 隐藏
顶部工具:✅ 显示 顶部工具:❌ 隐藏
可拖拽:✅ 是 可拖拽:❌ 否
可编辑:✅ 是 可编辑:❌ 否
数据来源:localStorage 数据来源:localStorage + 服务器
主要用途:创建和编辑 主要用途:预览和分享
📝 可背诵的核心表述
表述 1:三阶段工作流
低代码平台的工作流分为三阶段:
- 编辑阶段(/):用户在完整编辑器中创建和修改页面
- 服务器存储:发布时生成唯一 ID,将 views 数据存到服务器
- 发布阶段(/release/:id):其他用户访问该链接,看到只读的预览页面
表述 2:缓存策略
Release.vue 采用两层缓存:
- 第一层:localStorage[id](快)
- 第二层:服务器 API /api/getOne(慢但可靠) 如果两者都不可用,降级到 localStorage['1'](最后保存的编辑状态)
表述 3:数据导出
支持三种导出格式:
- HTML:独立文件,可直接在浏览器打开
- Vue:单文件组件,可集成到其他 Vue 项目
- JSON:原始数据,可重新导入编辑
表述 4:导出原理
将 views JSON 数据递归转换为 HTML DOM,同时收集所有样式为 CSS 字符串,最后组合成完整的 HTML/Vue 文件。通过 file-saver 库实现浏览器下载。
表述 5:SetExample 的作用
SetExample.vue 是一个示例加载器。用户访问 /example/:id 时,它从服务器获取该示例的配置,保存到 localStorage,然后自动跳转到编辑器,用户即可基于示例进行修改。
🤔 关键追问清单
-
"为什么 Release.vue 要使用本地缓存?"
- 答:加快加载速度。用户第一次访问时从服务器获取,之后访问时直接从本地缓存读取,无需网络请求。
-
"如果服务器中间宕机了,用户还能访问已发布的页面吗?"
- 答:如果用户之前访问过(缓存在 localStorage),可以继续访问。新用户则无法访问。
-
"导出的 HTML 文件能直接在浏览器中打开吗?"
- 答:可以。因为导出的是标准 HTML,包含所有必要的样式和内容,不依赖服务器。
-
"为什么导出 Vue 组件需要特殊处理?"
- 答:因为需要生成符合 Vue 单文件组件规范的代码(
<template>,<script>,<style>),而不是普通 HTML。
- 答:因为需要生成符合 Vue 单文件组件规范的代码(
-
"SetExample.vue 中的 localStorage.setItem('views', ...) 是什么意思?"
- 答:将示例配置保存到浏览器本地存储的 'views' 键,这样回到编辑器时会加载这个示例配置。
🚀 实战建议
现在你可以尝试:
练习 1:完整发布流程
- 在编辑器中创建一个简单页面(几个文本和按钮)
- 点击"保存"
- 点击"发布"
- 复制生成的链接
- 在新标签页打开该链接,验证页面是否能正确加载
练习 2:导出数据
- 在编辑器中创建一个页面
- 点击"导出 HTML",检查下载的文件
- 在浏览器中直接打开该 HTML 文件,验证显示效果
- 点击"导出 JSON",检查导出的数据结构
练习 3:重新导入
- 导出一个页面为 JSON
- 修改编辑器中的页面(删除组件等)
- 点击"导入 Json",选择之前导出的文件
- 验证页面是否恢复到导出时的状态
最后的架构总结
现在你已经学完了低代码平台的五个阶段!
第一阶段:项目概览
↓ 理解项目是什么、为什么要做
第二阶段:项目结构
↓ 理解代码如何组织:Left、Center、Right、Top 四大模块
第三阶段:渲染引擎 & 组件系统
↓ 理解组件如何创建、修改、渲染:PowerfulDynamicDraw、基础组件、属性面板
第四阶段:事件系统 & 工作流
↓ 理解组件间如何通信:事件总线、撤销/重做、快捷键
第五阶段:发布 & 导出
↓ 理解如何把编辑成果转化为可用产品:发布、预览、导出
🎓 你现在可以:
✅ 完整理解低代码平台的工作原理 ✅ 从零开始添加新的组件��型 ✅ 修改或扩展属性面板 ✅ 添加新的导出格式 ✅ 优化和改进编辑器功能 ✅ 在简历中详细描述这个项目的每个细节
你是否还有其他想深入学习的地方?
比如:
- 如何添加一个完全新的组件类型?
- 如何实现拖拽排序(而不仅仅是添加)?
- 如何添加动画或交互效果?
- 如何优化性能(处理大量组件)?
- 如何部署这个项目到生产环境?
或者,你准备好用可背诵的表述来描述整个项目了吗?🎉