html 表单 设计编辑器,可视化页面编辑器的架构设计
前言
前不久開發歷時半年的可視化搭建項目終于上線[手動撒花 🌹🌹🌹],產品功能上和市面上常見的可視化編輯器其實并沒有很大區別,功能細節處略有不同而已。本文主要是記錄開發過程中遇到的問題以及解決思路。
產品演示圖
需求分析
前期的準備工作還是比較重要的, 尤其是前端項目, 如果整個項目搭建好之后發現某個功能交互邏輯實現起來異常困難, 工作量大概率要成倍增加。哎,就不多說了,懂得都懂。
物料區, 目前支持 5 種組件, 要求可復用, 可支持擴展
可視化拖拽, 物料區拖拽至預覽區, 預覽區頁面內部拖拽排序,
預覽區,流式排版, 點擊可打開組件配置, 跟隨組件位置
實時預覽, 即配置改動需要立馬反映到預覽區
配置區, 大量調用業務相關的彈窗功能
配置區, 需要實現自定義校驗邏輯, 并支持單獨保存
技術棧
系統使用到技術棧如下
react typescript mobx scss antd
數據結構定義
第一步當然是和找后端小哥定義接口頁面存儲的數據結構, 這部分應該沒什么爭議。
interface Page {
id: number // 頁面id
siteName: string // 頁面名稱
description: string // 描述
createdAt: number // 創建時間
operatorName: string // 操作人名稱
modules: [
// 頁面組件配置
{
id: number // 組件id
name: string // 組件名稱
type: number // 組件類型
configuration: JSON.stringify({ // 序列化后的配置, 以內容列表為例
displayRowNum: 8,
subPageConfiguration: {...},
title: "暖心夜話",
contentType: 3,
columnId: 6747,
columnName: "99%的成年人都會患上的情緒綜合癥,你中槍了么",
}
status: number // 上架狀態
}
]
}
復制代碼
具體使用時只需要按順序解析 modules 字段中的 configration 配置展示即可, 編輯過后再按原有的數據結構回傳回后端。注意這里有很多組件配置字段僅存儲了索引關系, 具體展示信息仍需要運行時獲取。
目錄結構
├── @types # 聲明文件
├── store # 數據相關操作, 統一集中在這里
├── constant # 常量相關
├── service # 遠程服務
├── common # 調用的相關組件
├── Editor # 編輯器
│?? ├── BasicModules # 基礎組件區
│?? ├── Empty # 空數據
│?? ├── FormContainer # 配置區
│?? ├── PreviewComponent # 預覽組件
│?? ├── PreviewContainer # 預覽區
│?? ├── UIModules # 擴展組件區
│?? ├── index.tsx
├── Modules # 編輯器組件, 以List組件為例
│?? ├── List
│?? ├── ├── index.tsx # 渲染組件
│?? ├── ├── Form.tsx # 表單組件
│?? └── index.ts
復制代碼
組件
組價設計是這個系統中最重要的部分, 所有的操作都是通過組件解耦串聯到一起, 并且串聯到一起的
數據結構
下面是運行時需要用到的數據結構, 我們將后端給到的 configration 封裝在了 data 中, 并擴展了一些字段, 比如 UI 狀態和校驗屬性等。
interface CmsModule {
id: number // uuid
name: string // 組件名稱
component: any // 展示組件
form: any // 表單組件
type: number // 組件類型
selected: boolean // 是否選中
error: boolean // 是否有錯誤
untouched?: boolean // 是否是初始化狀態, 只有新增的組件會有此狀態
data: { id?: number } & Record // 組件的configration
}
復制代碼
初始化
初始化的操作統一在 store 中編寫, 下面是代碼示例, 解析服務端數據生成本地模型
import { BASIC_MODULE_LIST } from 'Modules'
// modules是后端傳入的數據結構
store.deserialize = (modules) => {
this.value = modules.map((module) => {
// 根據類型篩選出靜態屬性
const staticInfo = BASIC_MODULE_LIST.find(
(item) => item.type === module.type
)
const component: CmsModule = {
...staticInfo,
id: module.id,
type: module.type,
name: module.name,
selected: false,
error: false,
data: {
id: module.id,
...(() => {
try {
return JSON.parse(module.configuration)
} catch (e) {}
})(),
},
}
return component
})
}
復制代碼
組件注冊
上述代碼中的BASIC_MODULE_LIST 相當于一個組件的注冊列表, 通過 BASIC_MODULE_LIST 我們將組件的靜態屬性注入到運行時中, 同理新增一個組件也只需要添加如下條件即可。 當然如果你希望使用遠程組件也都是可以的
import Search from './Search'
import SearchForm from './Search/Form'
export const BASIC_MODULE_LIST = [
{
type: 20,
component: Search,
name: '搜索',
form: SearchForm,
},
]
復制代碼
// 加載遠程組件, 可采用 require.js 加載或者直接加載
init() {
const script = document.createElement('script')
script.src = 'https://demo.umd.component.js'
script.onload = () => {
BASIC_MODULE_LIST.push([
{
type: 31,
component: window.Search,
name: '遠程組件示例',
form: window.Search.Form,
},
])
}
document.body.appendChild(sciprt)
}
復制代碼
最后來看一下我們是如何使用組件的數據的
PreviewComponent.tsx
render() {
const Module = module.component
const Form = module.form
return
className={classnames(
style.preview,
module.selected && style.selected,
module.error && style.error
)}
onClick={this.handleSelect}>
{}
{data.selected &&
{module.name}}
}
復制代碼
配置
組件的配置
先來聊聊組件的配置, 回顧一下需求, 組件的配置需要支持實時錯誤校驗, 調用業務資源相關的彈窗, 以及單獨保存。當然最重要的需要實現控制反轉, 也就是說配置文件只描述表單規則, 而實際的表單則需要由編輯器創建。本系統用到了 antd 的 Form 組件創建表單, 組件實現下面的接口即可
import { WrappedFormUtils } from 'antd/lib/form/Form'
interface ModuleFormProps {
form: WrappedFormUtils // antd 的 form 的實例, 由外部編輯器傳入
initialValue?: any // 表單默認值, 通常是是從 configration 獲取
layout?: {
// 布局配置
labelCol: { span: number }
wrapperCol: { span: number }
}
}
// Form組件簽名
type FormComponent =
| React.Component
| React.FC
// 示例表單組件
import SourceModal from '../common/SourceModal' // 引入業務相關的資源彈窗
const BannerForm: React.Component = (props) => {
return (
{getFieldDecorator('title', {
initialValue: this.props.initialValue?.title, // 默認值
rules: [{ required: true, message: '請輸入標題' }], // 校驗
})()}
)
}
復制代碼
同理, 上述組件如果需要從遠程調用, 只需要把 SourceModal 像 form 對象一樣將依賴注入, 簡單改造即可, 外部調用也比較簡單
FormContainer.tsx
import { Form as AntForm } from 'antd'
render() {
const { data, Form } = this.props
return (
form={this.props.form}
initialValue={data}
layout={...}
/>
)
}
復制代碼
配置同步
前面提到了我們創建了全局唯一的 store 用于統一處理數據, 原則上我們需要將所有的數據及修改數據的方法都封裝在 store 中, 以防萬一需要實現 undo/redo 棧。下面的代碼演示了如何將 Form 表單字段變更同步到 store 中
FormContainer.tsx
import { Form as AntForm } from 'antd'
import store from 'store'
export default Form.create({
onValuesChange: (props, changedFields, allValues) => {
store.updateComponent(this.props.data.id, allValues)
},
})(FormContainer)
復制代碼
在 react 中將 store 數據反應在 UI 上的方法有很多, 因為項目本身采用了 mobx, 故將PreviewComponent組件用 observer 包裹即可
錯誤處理
前面我們只定義了單個組件的表單錯誤校驗, 所以我們需要監控每一個組件的錯誤狀態, 否則當保存頁面時我們只能獲取當前組件的錯誤狀態。當前利用了Form組件的渲染鉤子函數,在切換選中時同步當前表單狀態
FormContainer.tsx
import store from 'store'
// 切換選中組件時, 上一個組件的 Form 的銷毀鉤子
componentWillUnmount() {
const { form, data } = this.props
form.validateFields((err, values) => {
store.updateComponent(data.id, values)
store.changeComponentError(data.id, Boolean(err))
})
}
復制代碼
同時CmsModule還有一個字段 untouched 用來標識組件是否被選中過(只有新增組件會有這個字段), untouched 為 true 時組件表單數據為空, 也無法保存
其他
拖拽
拖拽采用了知名的第三方庫 react-dnd, 具體使用方法可參考文檔, 這里就不贅述了
體驗上有幾處定制優化, 一是從左側物料區拖拽入預覽區有一個中間預覽狀態, 二是拖拽排序時會自動開啟頁面滾動, 在長頁面排序時會比較友好。
性能
渲染性能
因為采用了 mobx, 所以在列表數據量極大的情況下也可以做到精準更新, 不做優化的情況下也不會出現卡頓
數據獲取
前文提到很多組件只保存了資源索引 id, 只有在組件渲染時才會去請求接口數據, 想象一下如果配置了 100 個組件那么頁面初始化的時候就會同時發送 100 個請求。 類似于圖片懶加載, 組件的數據加載也可以優化
List/index.tsx
if (!window.IntersectionObserver) {
this.fetchData()
} else {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
this.fetchData()
observer.unobserve(this.listRef.current)
}
})
observer.observe(this.listRef.current)
}
復制代碼
交互
推薦一個庫react-flip-move, 快速實現動態列表插入、刪除、排序動畫, 零配置接入, 代碼入侵也很小, 推薦使用。
規劃
更多物料組件實現
將組件替換為遠程組件
undo/redo
懶加載做到不依賴組件具體實現
總結
以上是生活随笔為你收集整理的html 表单 设计编辑器,可视化页面编辑器的架构设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaScript大师必须掌握的12个
- 下一篇: MFC项目复制界面