低代码phase5

7 阅读9分钟

完美!现在让我为你讲解第五阶段:工作流完成与数据导出系统。这是把低代码平台的成果转化为可用产品的关键阶段。


🎉 第五阶段深入:从编辑到发布的完整工作流

第一部分:工作流的三个阶段

低代码平台的生命周期分为三个阶段:

┌─────────────────────────────────────────────────────────────┐
│ 阶段 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
edittruefalse
可拖拽✅ 是❌ 否
可编辑✅ 是❌ 否
显示边框✅ 是❌ 否
数据来源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,然后自动跳转到编辑器,用户即可基于示例进行修改。


🤔 关键追问清单

  1. "为什么 Release.vue 要使用本地缓存?"

    • 答:加快加载速度。用户第一次访问时从服务器获取,之后访问时直接从本地缓存读取,无需网络请求。
  2. "如果服务器中间宕机了,用户还能访问已发布的页面吗?"

    • 答:如果用户之前访问过(缓存在 localStorage),可以继续访问。新用户则无法访问。
  3. "导出的 HTML 文件能直接在浏览器中打开吗?"

    • 答:可以。因为导出的是标准 HTML,包含所有必要的样式和内容,不依赖服务器。
  4. "为什么导出 Vue 组件需要特殊处理?"

    • 答:因为需要生成符合 Vue 单文件组件规范的代码(<template>, <script>, <style>),而不是普通 HTML。
  5. "SetExample.vue 中的 localStorage.setItem('views', ...) 是什么意思?"

    • 答:将示例配置保存到浏览器本地存储的 'views' 键,这样回到编辑器时会加载这个示例配置。

🚀 实战建议

现在你可以尝试:

练习 1:完整发布流程

  1. 在编辑器中创建一个简单页面(几个文本和按钮)
  2. 点击"保存"
  3. 点击"发布"
  4. 复制生成的链接
  5. 在新标签页打开该链接,验证页面是否能正确加载

练习 2:导出数据

  1. 在编辑器中创建一个页面
  2. 点击"导出 HTML",检查下载的文件
  3. 在浏览器中直接打开该 HTML 文件,验证显示效果
  4. 点击"导出 JSON",检查导出的数据结构

练习 3:重新导入

  1. 导出一个页面为 JSON
  2. 修改编辑器中的页面(删除组件等)
  3. 点击"导入 Json",选择之前导出的文件
  4. 验证页面是否恢复到导出时的状态

最后的架构总结

现在你已经学完了低代码平台的五个阶段!

第一阶段:项目概览
  ↓ 理解项目是什么、为什么要做
  
第二阶段:项目结构
  ↓ 理解代码如何组织:Left、Center、RightTop 四大模块
  
第三阶段:渲染引擎 & 组件系统
  ↓ 理解组件如何创建、修改、渲染:PowerfulDynamicDraw、基础组件、属性面板
  
第四阶段:事件系统 & 工作流
  ↓ 理解组件间如何通信:事件总线、撤销/重做、快捷键
  
第五阶段:发布 & 导出
  ↓ 理解如何把编辑成果转化为可用产品:发布、预览、导出

🎓 你现在可以:

✅ 完整理解低代码平台的工作原理 ✅ 从零开始添加新的组件��型 ✅ 修改或扩展属性面板 ✅ 添加新的导出格式 ✅ 优化和改进编辑器功能 ✅ 在简历中详细描述这个项目的每个细节


你是否还有其他想深入学习的地方?

比如:

  • 如何添加一个完全新的组件类型?
  • 如何实现拖拽排序(而不仅仅是添加)?
  • 如何添加动画或交互效果?
  • 如何优化性能(处理大量组件)?
  • 如何部署这个项目到生产环境?

或者,你准备好用可背诵的表述来描述整个项目了吗?🎉