去年上線的可視化編輯器 H5-dooring 至今已有一年的時間,期間有很多熱心的網友和大佬提出了非常多寶貴的建議,我們也在一步步實現中,以下是幾個比較典型的低代碼可視化平台需求:
- 出碼能力(即源碼下載功能)
- 組件交互(即組件支持業務中常用的鏈接跳轉,彈窗交互,自定義事件等)
- 數據源管理(即用戶創建的不同頁面擁有共享數據的能力,不同組件之間也有共享數據的能力)
- 組件商店(即用戶可以自主生產組件,定義組件,接入組件數據的能力)
- 布局能力(即用戶可以選擇不同的布局方案來設計頁面)
- 常用功能集成(頁面截圖,微信分享,debug能力)
上面的這些功需求已經在 H5-dooring 陸續實現了,在我之前的文章中也有對應的技術分享。但是為了讓更多的人能低成本的擁有自己的可視化搭建系統,我們團隊的大佬花了非常多的時間研究和沉澱,最近也開源了一款可視化搭建框架 dooringx-lib,我們可以基於它輕鬆製作可視化編輯器,而無需考慮內部的實現細節,接下來我就和大家分享一下這款可視化框架的使用方式和實現思路,同時也非常感謝 dooring可視化團隊 各位大佬們的辛勤付出。
dooringx
可視化搭建框架基本使用和技術實現
為了讓大家更好的理解可視化搭建框架,我這裡舉幾個形象的例子:
- antd —— antd-pro

我們都知道 antd 是流行的前端組件庫,那麼基於它上層封裝的管理後台 antd-pro 就是它的上層應用。
- GrapesJS —— craft.js

GrapesJS 是一款國外的頁面編輯器框架(詳細介紹可參考我之前的文章 這款國外開源框架, 讓你輕鬆構建自己的頁面編輯器) ,那麼 craft.js 就是它的上層應用框架。
- dooringx-lib —— dooringx

dooringx
dooringx-lib 是一款可視化搭建框架,同理 dooringx 就是基於 dooringx-lib 的可視化編輯器。
之所以要介紹它們的區別,是因為之前有很多朋友對這塊概念理解的不是很清晰,在了解了可視化搭建框架 的 “內涵” 之後,我們開始今天的核心內容。
1.技術棧
在分享框架實現思路之前當然要自報家門,框架實現上我們還是採用熟悉的 React 生態,移動端組件庫採用的眾安團隊的 zarm,編輯器應用層採用的 antd,至於其他的比如拖拽,參考線,狀態管理,插件機制等都是我們團隊大佬自研的方案。如果你是 vue 或者其他技術棧為主的團隊,也可以參考實現思路,相信也會對你有一定的啟發。
2.基本使用方式
在開始深入之前我們先看看如何使用這款框架,我們只需要按照如下方式即可安裝使用:
npm/yarn  install dooringx-lib
同時我們還提供了基礎的使用demo,方便大家在自己的工程中快速上手:
# 克隆項目
# cnpmjs
git clone https://github.com.cnpmjs.org/H5-Dooring/dooringx.git
# or
git clone https://github.com/H5-Dooring/dooringx.git
# 進入項目目錄
cd dooringx
# 安裝依賴
yarn install
# 啟動基礎示例
yarn start:example
# 啟動 dooringx-lib
yarn start
# 啟動 dooringx doc 文檔
yarn start:doc
yarn build
demo 的 github 項目如下:

github地址:
https://github.com/H5-Dooring/dooringx
在了解完使用方式之後,我們來看看基本架構和實現思路。
3.dooringx-lib基礎架構和工作機制

上圖就是我根據目前 dooringx-lib 的項目架構梳理的架構圖,基本包含了搭建化編輯框架的大部分必備模塊。為了保證框架的靈活性,我們還可以按需安裝對應的功能組件,開發自定義的組件等。如下是一個基本的導入案例:
import {
    RightConfig,
    Container,
    useStoreState,
    innerContainerDragUp,
    LeftConfig,
    ContainerWrapper,
    Control,
} from 'dooringx-lib';
我們將整個框架拆分成了不同的模塊,這些模塊既相互獨立又可以相互關聯。完整的工作流程如下:

由上圖可以看出,我們只需要擁有基礎的業務研發能力,就可以藉助 dooringx-lib 構建一個屬於自己的搭建平台,就好比任何程序的本質: 數據和邏輯。
4.dooringx-lib插件開發
接下來我會和大家分享 dooringx-lib 的插件開發方式和具體實現(如何導入插件,如何編寫組件,如何註冊函數等),如果大家感興趣的話也可以跟着下面的方式實踐一下。
4.1 如何導入組件

我們在上圖可以看到左側是我們的組件物料區,分為基礎組件,媒體組件,可視化組件,它們的添加會統一放在 LeftRegistMap 數組中來管理,其基本結構如下:
const LeftRegistMap: LeftRegistComponentMapItem[] = [
  {
      type: 'basic', // 組件類別
      component: 'button', // 組件名稱
      img: 'icon-anniu', // 組件icon
      displayName: '按鈕', // 組件中文名
      urlFn: () => import('./registComponents/button'),  // 註冊回調
  },
];
左側組件支持同步導入或者異步導入。
如果需要異步導入組件,則需要填寫 urlFn,需要一個返回 promise 的函數。也可以支持遠程載入組件,只要 webpack 配上即可。
如果需要同步導入組件,則需要將組件放入配置項的 initComponentCache 中,這樣在載入時便會註冊進 componentRegister 里。
initComponentCache: {
  modalMask: { component: MmodalMask },  
},
4.2 如何定製左側面板

左側面板傳入 leftRenderListCategory 即可。
leftRenderListCategory: [
  {
type: 'basic',
icon: <HighlightOutlined />,
displayName: '基礎組件',
  },
  {
type: 'xxc',
icon: <ContainerOutlined />,
custom: true,
customRender: <div>我是自定義渲染</div>,
  },
],
type 是分類,左側組件顯示在哪個分類由該字段決定。icon 則是左側分類小圖標(如上圖所示)。當 custom 為 true 時,可以使用 customRender 自定義渲染。
4.3 開發一個自定義的可視化組件
組件需要導出一個由 ComponentItemFactory 生成的對象:
const MButton = new ComponentItemFactory(
 'button',
 '按鈕',
 {
style: [
 createPannelOptions<FormMap, 'input'>('input', {
  receive: 'text', 
  label: '文字',
 }),
],
animate: [createPannelOptions<FormMap, 'animateControl'>('animateControl', {})],
actions: [createPannelOptions<FormMap, 'actionButton'>('actionButton', {})],
 },
 {
props: {
 ...
 text:'x.dooring'// input配置項組件接收的初始值
},
 },
 (data, context, store, config) => {
return <ButtonTemp data={data} store={store} context={context} config={config}></ButtonTemp>;
 },
 true
);
export default MButton;
其中第一個參數為組件註冊名,第二個參數用來展示使用。
第三個參數用來配置右側面板的配置項組件。其中鍵為右側面板的分類,值為配置項組件數組。
第四個參數會配置組件的初始值,特別注意的是,製作組件必須要有初始寬度高度(非由內容撐開),否則會在適配時全選時產生問題。
這個初始值里有很多有用的屬性,比如fixed代表使用固定定位,可以結合配置項更改該值,使得組件可以fixed定位。
還有 canDrag 類似於鎖定命令,鎖定的元素不可拖拽。
初始值里的 rotate 需要個對象,value 代表旋轉角度,canRotate 代表是否可以操作旋轉。(0.7.0版本開始支持)
第五個參數是個函數,你將獲得配置項中的 receive 屬性(暫且都默認該配置為receive)傳來的配置,比如上例中 receive 的是 text,則該函數中 data 里會收到該字段。
context 一般只有 preview 和 edit,用來進行環境判斷。
config 可以拿到所有數據,用來製作事件時使用。
第六個參數 resize 是為了判斷是否能進行縮放,當為 false 時,無法進行縮放。
第七個參數 needPosition,某些組件移入畫布後會默認採取拖拽的落點,該配置項默認為 true, 就是需要拖拽的位置,為 false 時將使用組件自身 top 和 left 定位來放置。
4.4 事件註冊

- 註冊時機
事件可以細分為 註冊時機 和 函數,組件內可以通過 hook 的方式來實現註冊時機:
useDynamicAddEventCenter(pr, `${pr.data.id}-init`, '初始渲染時機'); //註冊名必須帶id 約定!
useDynamicAddEventCenter(pr, `${pr.data.id}-click`, '點擊執行時機');
useDynamicAddEventCenter 第一個參數是 render 的四個參數組成的對象。第二個參數是註冊的時機名,必須跟 id 相關,這是約定,否則多個組件可能會導致名稱衝突,並且方便查找該時機。
註冊完時機後,我們需要將時機放入對應的觸發位置上,比如這個 button 的點擊執行時機就放到 onclick 中:
<Button
    onClick={() => {
eventCenter.runEventQueue(`${pr.data.id}-click`, pr.config);
    }}
>
    x.dooring
</Button> 
其中第一個參數則為註冊的時機名,第二個為 render 函數中最後一個參數 config
- 函數註冊
函數由組件拋出,可以加載到事件鏈上。比如,註冊個改變文本函數,那麼我可以在任意組件的時機中去調用該函數,從而觸發該組件改變文本。
函數註冊需要放入 useEffect 中,在組件卸載時需要卸載函數!否則會導致函數越來越多。
useEffect(() => {
const functionCenter = eventCenter.getFunctionCenter();
const unregist = functionCenter.register(
 `${pr.data.id}+改變文本函數`,
 async (ctx, next, config, args, _eventList, iname) => {
  const userSelect = iname.data;
  const ctxVal = changeUserValue(
   userSelect['改變文本數據源'],
   args,
   '_changeval',
   config,
   ctx
  );
  const text = ctxVal[0];
  setText(text);
  next();
 },
 [
  {
   name: '改變文本數據源',
   data: ['ctx', 'input', 'dataSource'],
   options: {
    receive: '_changeval',
    multi: false,
   },
  },
 ]
);
return () => {
 unregist();
};
}, []);
函數中參數與配置見後面的函數開發。
4.5 右側面板開發

為了開發自定義的右側屬性面板,我們只要將開發的組件配成一個對象放入 initFormComponents 即可。為了良好的開發體驗,需要定義個 formMap 類型:
export interface FormBaseType {
    receive?: string;
}
export interface FormInputType extends FormBaseType {
    label: string;
}
export interface FormActionButtonType {}
export interface FormAnimateControlType {}
export interface FormMap {
    input: FormInputType;
    actionButton: FormActionButtonType;
    animateControl: FormAnimateControlType;
}
formMap 的鍵名就是 initFormComponents 鍵名,formMap 的值對應組件需要收到的值。
以 input 組件為例,FormInputType 此時有2個屬性: label, receive。
那麼在開發該組件時,props 會收到:
interface MInputProps {
    data: CreateOptionsRes<FormMap, 'input'>;
    current: IBlockType;
    config: UserConfig;
}
也就是 data 是 formMap 類型,而 current 是當前點擊的組件,config 就不用說了。
還記得在左側組件開發中的第三個參數嗎?這樣就都關聯起來了:
style: [
    createPannelOptions<FormMap, 'input'>('input', {
        receive: 'text',  
        label: '文字'
    })
],
createPannelOptions 這個函數的泛型里填入對應的組件,將會給收到的配置項良好的提示。
在配置項組件里所要做的就是接收組件傳來的配置項,然後去修改 current 的屬性:
function MInput(props: MInputProps) {
 const option = useMemo(() => {
return props.data?.option || {};
 }, [props.data]);
 return (
<Row style={{ padding: '10px 20px' }}>
 <Col span={6} style={{ lineHeight: '30px' }}>
  {(option as any)?.label || '文字'}:
 </Col>
 <Col span={18}>
            <Input
                value={props.current.props[(option as any).receive] || ''}
                onChange={(e) => {
                        const receive = (option as any).receive;
                        const clonedata = deepCopy(store.getData());
                        const newblock = clonedata.block.map((v: IBlockType) => {
                                if (v.id === props.current.id) {
                                        v.props[receive] = e.target.value;
                                }
                                return v;
                        });
                        store.setData({ ...clonedata, block: [...newblock] });
                }}
            ></Input>
 </Col>
</Row>
 );
}
由於可以很輕鬆的拿到 store,所以可以在任意地方進行修改數據。
將組件的 value 關聯 current 的屬性,onChange 去修改 store,這樣就完成了個雙向綁定。
注意:如果你的右側組件需要用到 block 以外的屬性,可能需要去判斷是否處於彈窗模式。
4.6 自定義右鍵菜單

右鍵菜單可以進行自定義:
// 自定義右鍵
const contextMenuState = config.getContextMenuState();
const unmountContextMenu = contextMenuState.unmountContextMenu;
const commander = config.getCommanderRegister();
const ContextMenu = () => {
 const handleclick = () => {
unmountContextMenu();
 };
 const forceUpdate = useState(0)[1];
 contextMenuState.forceUpdate = () => {
forceUpdate((pre) => pre + 1);
 };
 return (
<div
 style={{
            left: contextMenuState.left,
            top: contextMenuState.top,
            position: 'fixed',
            background: 'rgb(24, 23, 23)',
 }}
>
 <div
            style={{ width: '100%' }}
            onClick={() => {
                    commander.exec('redo');
                    handleclick();
            }}
        >
            <Button>自定義</Button>
 </div>
</div>
 );
};
contextMenuState.contextMenu = <ContextMenu></ContextMenu>;
先拿到 contextMenuState,contextMenuState 上有個 unmountContextMenu 是關閉右鍵菜單方法。所以在點擊後需要調用關閉。同時上面的 left 和 top 是右鍵的位置。另外,我們還需要在組件內增加強刷,賦值給 forceUpdate,用於在組件移動時進行跟隨。
4.7 表單驗證提交思路

表單驗證提交有非常多的做法,因為數據全部是聯通的,或者直接寫個表單組件也可以。在不使用表單組件時,簡單的做法是為每個輸入組件做個驗證函數與提交函數。這樣是否驗證就取決於用戶的選取,而拋出的輸入可以讓用戶選擇放到哪,並由用戶去命名變量。
在點擊提交按鈕時,調用所有組件的驗證函數與提交函數,使其拋給上下文,再通過上下文聚合函數聚合成對象,最後可以通過發送函數發送給對應後端,從而完成整個流程。我們可以在 dooringx 中試下這個demo。
另一種方式是可以專門寫個提交按鈕,固定了參數,以及部分規則,比如規定在頁面中的所有表單都會被收集提交。
那麼我們可以利用數據源,將所有表單輸出內容自動提交給數據源,最後的提交按鈕按數據源規定格式的key 提取,發送給後端。
後期規劃
後期我們還會在產品功能方面持續迭代優化,如果大家有好的建議, 也可以隨時和我們交流, 也歡迎在 github 上積極提 issue。
如果大家對可視化搭建或者低代碼/零代碼感興趣,也可以參考我往期的文章或者在評論區交流你的想法和心得,歡迎一起探索前端真正的技術。
github: dooringx | 快速高效搭建可視化拖拽平台
團隊:Dooring可視化團隊
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/209404.html
 
 微信掃一掃
微信掃一掃  支付寶掃一掃
支付寶掃一掃 