本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
此篇为上一遍文章的续篇 Low code 之从零搭建一个h5可视化平台
之前看到过转转
它们发表过一个此类的博文,不过它们因react-dnd不支持跨iframe便放弃了,自己使用原生H5的拖拽事件封装一个。
其实吧,无论使用哪种它们的核心都是一样的。要认清楚这一点:我们实际拖拽的并不是视图而是数据
什么意思?简单画个图吧。
即,我们通过拖拽要做的真实目的是想要去改变iframe区域的数据源。iframe拿到最新的数据源重新渲染就OK了
来吧,我们还是利用react-dnd,简单实现一下吧
创建一个数据源
首先,在它们的统一父组件内创建一个状态,用于保存iframe中的组件信息
const [currentCacheCopm, setCurrentCacheCopm] = useState([]);
开始设置拖拽目标
这里的拖拽目标就是左侧的每个缩略图了,思考一下要做些什么呢?
答:即在拖拽开始和结束分别对数据源进行更新操作
简单逻辑代码demo如下:拖拽end的时候,通过setCurrentCacheCopm改变数据源信息。
const Thumbnail: FC<ThumbnailProps> = ({
compInfo,
currentCacheCopm,
setCurrentCacheCopm,
}) => {
const [, drag] = useDrag(
{
item: compInfo,
type: 'comp',
end: (item, monitor: DragSourceMonitor) => {
if (monitor.didDrop()) {
// 拖拽结束&处于放置目标,先简单放到最后
setCurrentCacheCopm([...currentCacheCopm,item])
} else {
// 拖拽结束&不处于放置目标
}
},
},
[currentCacheCopm, setCurrentCacheCopm],
);
return (
<div ref={drag}>
<img className="thum-preview" src={compInfo.pic} />
</div>
);
};
export default memo(Thumbnail);
设置放置目标
注意:因为不支持跨iframe拖拽,所以我们不要直接将iframe设置为放置目标,而是应该设给iframe的父亲。
当拖拽行为结束,数据源便已经有了刚才放进去的组件信息。渲染它不是我们编辑器的事情,故我们需要将数据源传给预览项目
首先放置目标代码
const PreView: FC<PreViewProps> = ({
currentCacheCopm,
setCurrentCacheCopm,
}) => {
const [, drop] = useDrop({
accept: 'comp',
});
return (
<div
className="preview"
ref={drop}
>
<iframe
src="http://localhost:3000/#/preview"
width="100%"
scrolling="yes"
frameBorder="0"
id="preview"
/>
</div>
);
};
上一篇我们说了一下,我们在拖拽过程中是需要有一个占位标签的,而且我们也提前说了思路,主要就是在拖拽过程中要拿到当前放置目标组件的索引。现在接着来扩展放置目标代码,即将iframe区域的每一个组件都设为放置目标
图解:
等到新来组件
插入,此时我们要做的操作
- 移动占位元素
- 将新来组件插入指定位置
现在我们给图上标记索引
我们的实际代码逻辑其实只是
- 找到数据源中占位数据,先把它干掉。此时组件索引又发生改变。即当前目标处的索引变为了0
- 那么便在索引为0的下面插入占位数据(即view视图层的移动占位 标签)
- 等待拖拽行为结束,以真正的目标组件信息替换掉占位组件信息
关键代码如下:
const Drop: FC<DropProps> = ({
compInfo,
index, //当前组件索引
setCurrentCacheCopm,
currentCacheCopm,
}) => {
const currentCompRef = useRef(null);
const [, drop] = useDrop(
{
accept: 'comp',
hover: (_, monitor) => {
// 移动占位标签
const occupantsIndex = currentCacheCopm.findIndex(
(compItem) => compItem.name === 'occupants',
);
currentCacheCopm.splice(occupantsIndex, 1);
currentCacheCopm.splice(index, 0, {
name: 'occupants',
description: '放到这里',
});
setCurrentCacheCopm([...currentCacheCopm]);
}
},
[currentCacheCopm, setCurrentCacheCopm, index],
);
return (
<div ref={drop} className={compInfo.name}>
<div ref={currentCompRef}>
<div
style={{ height: `${compInfo.clientHeight}px` }}
className="dropDemo"
>
{compInfo.description}
</div>
</div>
</div>
);
};
这时候可能有细心的大佬发现了问题,这块逻辑应该写到哪呢?编辑器中还是预览项目中呢?因为react-dnd不可以跨iframe
拖拽,到目前为止我的实现中还没有提及任何向iframe子页面传递数据源的逻辑呢。
我其实是在拖拽过程中,占位标签的移动逻辑写在了编辑器项目。其实在整个iframe区域,我是有两块东西的。一个当然就是iframe标签,里面包裹着预览项目,另一个则是和iframe布局一摸一样的盒子。这个盒子的工作就是上面我们写的逻辑。即在拖拽过程中,展示每个组件的名称及其占位元素的位置
为什么这样搞呢?
-
首先拖拽的预览展示逻辑其实都是属于编辑器的。但是呢,我又不想编辑器去读react or vue 的相关代码。这就涉及到一个问题,编辑本身是无法预览的,因为它没有办法进行组件的渲染,所以只能去借助iframe
-
而在预览项目,我又不想加入过多的编辑器操作相关的逻辑
那么到现在位置,当我们处于拖拽过程中,其实其中iframe表面区域是一个克隆了iframe基本布局样式的大盒子。
这样做就又出现了一个新的问题。iframe的克隆盒子怎么保证和真正的iframe有一样的布局呢?
换句话说,虽然我们现在已经有了一个渲染区域的数据源。但是对iframe和iframe克隆者来说两者的渲染怎么能保持一致呢?对于iframe来说,我们只需要将数据源传给它,它就可以依照预览项目的逻辑去渲染。可是iframe的克隆盒子是在编辑器中,无法真正渲染数据源内的相关组件。也就无法得到每个组件的高度。
这就出现了个相当严重的问题,当编辑器发生拖拽时。iframe的克隆盒子显现,但是它里面每个组件位置和iframe中的组件位置完全对应不上。
怎么处理呢?
其实也很简单,我们可以等到组件在预览项目中渲染完毕之后。获取高度反传回编辑器中,编辑器中的iframe盒子此时就可以拿到与iframe一样的尺寸了。
改造拖拽目标代码
要做哪些改造呢?
- 拖拽结束之后向iframe内传入当前最新的数据源
- 拖拽结束之后,以真正的组件数据替换掉占位标签
- 拖拽开始时,显示iframe的克隆盒子
由于两个组件(拖拽目标组件和预览组件)属于远亲,简单起见可以使用eventbus进行两者之间的通信
首先现在的拖拽目标代码逻辑如下:
const Thumbnail: FC<ThumbnailProps> = ({
compInfo,
currentCacheCopm,
setCurrentCacheCopm,
}) => {
const [{ isDragging }, drag] = useDrag(
{
item: compInfo,
type: 'comp',
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
end: (item, monitor: DragSourceMonitor) => {
const occupantsIndex = currentCacheCopm.findIndex(
(compItem) => compItem.name === 'occupants',
);
// 1. 如果成功放入目标容器,则以真正的comp替代占位元素
// 2. 没有放置目标容器中且拖拽结束,删除占位元素
if (monitor.didDrop()) {
currentCacheCopm.splice(occupantsIndex, 1, item);
//@ts-ignore
document.querySelector('#preview').contentWindow.postMessage({ currentCacheCopm }, '*');
} else {
currentCacheCopm.splice(occupantsIndex, 1);
}
eventbus.emit('watchDragState', false);
setCurrentCacheCopm([...currentCacheCopm]);
},
},
[currentCacheCopm, setCurrentCacheCopm],
);
useEffect(() => {
if (isDragging) {
// 开始拖拽时,放入数据源占位元素。并且通知预览组件展示iframe克隆盒子
eventbus.emit('watchDragState', true);
setCurrentCacheCopm([
{
name: 'occupants',
description: '放到这里',
},
...currentCacheCopm,
]);
}
}, [isDragging]);
return (
<div ref={drag}>
<img className="thum-preview" src={compInfo.pic} />
</div>
);
};
预览项目的处理情况
预览项目要做什么呢?
-
接受父页面传过来的数据源
-
根据数据源进行组件的渲染
-
在组件的commit阶段,拿到每块组件所占高度。传会给父页面
代码逻辑如下
const PreView = () => {
const [currentCacheCopm, setCurrentCacheCopm] = useState([]);
/** 获取编辑器中操作中预览组件信息 */
useEffect(() => {
window.addEventListener("message", ({ data }) => {
const { currentCacheCopm } = data;
if (currentCacheCopm) {
setCurrentCacheCopm(data.currentCacheCopm);
}
});
/** 计算每个容器的实际高度,返回编辑器 */
useLayoutEffect(() => {
const contents = document.querySelectorAll(".content");
for (let i = 0; i < contents.length; i++) {
currentCacheCopm[i].clientHeight = contents[i].clientHeight;
}
window.parent.postMessage({ currentCacheCopm }, "*");
}, [currentCacheCopm]);
return (
<div className="preview">
{currentCacheCopm.length > 0 &&
currentCacheCopm.map((comp, index) => {
// 同理 key 忽略diff优化
return (
<div
className="content"
key={id++}
onClick={() => getCurrentOperation(index)}
>
{renderJson(comp)}
</div>
);
})}
</div>
);
};
每个组件的高度信息放到哪了?其实在上一篇的基础部分可能有人就看到了。我是给数据源的每个组件的schema对象增加了一个属性,
即下面的clientHeight
{
"currentCacheCopm": [
{
"name": "button",
"compId": "Button",
"description": "按钮组件",
"pic": "https://img12.360buyimg.com/ddimg/jfs/t1/206278/28/8822/54487/615539f5E4f4cb5ab/49773bdc89799e5c.png",
"config": [
{
"name": "bgcColor",
"label": "按钮颜色",
"type": "string",
"format": "color"
},
{
"name": "btnText",
"label": "按钮文案",
"type": "string",
"format": "text"
}
],
"defaultConfig": { "btnText": "这是一个按钮", "bgcColor": "#333333" },
"clientHeight": 100
},
{
"name": "dialog",
"compId": "Dialog",
"description": "弹窗组件",
"pic": "https://img11.360buyimg.com/ddimg/jfs/t1/97204/11/18195/74905/61553bb8E9ba92a0d/8d59c5db08ccd759.png",
"config": [
{
"name": "dialogText",
"label": "请填写弹框文案",
"type": "string",
"format": "text"
}
],
"defaultConfig": { "dialogText": "默认弹框文案" },
"clientHeight": 100
},
{
"name": "button",
"compId": "Button",
"description": "按钮组件",
"pic": "https://img12.360buyimg.com/ddimg/jfs/t1/206278/28/8822/54487/615539f5E4f4cb5ab/49773bdc89799e5c.png",
"config": [
{
"name": "bgcColor",
"label": "按钮颜色",
"type": "string",
"format": "color"
},
{
"name": "btnText",
"label": "按钮文案",
"type": "string",
"format": "text"
}
],
"defaultConfig": { "btnText": "这是一个按钮", "bgcColor": "#333333" },
"clientHeight": 100
}
]
}
这个时候,iframe的克隆盒子的组件就有了高度。
到这里,我们来梳理一下总流程吧
-
用户触发拖拽,
-
拖拽目标
-
通知预览区域展示iframe克隆盒子
-
向数据源加入一个占位元素
-
-
渲染区域
- 在iframe的克隆盒子中根据数据源信息渲染占位组件
-
-
用户拖拽行为结束
-
拖拽目标
-
判断是否成功放置到目标区域 是 ?以真正数据信息替换条占位元素信息 :删除占位信息
-
向iframe子页面传送最新的数据源信息
-
通知渲染区域隐藏iframe克隆盒子
-
-
预览项目
- 根据最新的数据源,渲染相关组件。并且返回主页面每个组件的高度
-
渲染区域
- 隐藏iframe克隆盒子
-
还有一些小问题——处理滚动
当iframe区域出现滚动条时,iframe的克隆盒子和iframe之间组件位置又不匹配了
问题原因:因为iframe中此时的子页面,由于发生了滚动它的头部已经被卷去了一部分,但是此时的iframe克隆盒子还是完全从top为0开始。
如图:方便查看,我先把iframe和iframe克隆拆开来。本应它两应该是完全重合在一块的,可以看到现在它们组件位置已经完全不匹配了
解决:
其实也很容易解决,主要问题就是iframe内页面被卷去了一部分。那么我们就可以在子页面中监听滚动事件,然后把卷去的部分高度拿到传回编辑器。使得编辑的iframe克隆整个盒子上移相同高度就可以了
改造预览项目
const PreView = () => {
const [currentCacheCopm, setCurrentCacheCopm] = useState([]);
/** 获取编辑器中操作中预览组件信息 */
useEffect(() => {
window.addEventListener("message", ({ data }) => {
const { currentCacheCopm } = data;
if (currentCacheCopm) {
setCurrentCacheCopm(data.currentCacheCopm);
}
});
window.addEventListener(
"scroll",
debounce(() => {
// 获取页面Y轴的滚动距离
const scrollY =
document.documentElement.scrollTop || document.body.scrollTop;
window.parent.postMessage({ scrollY }, "*");
})
);
}, []);
/** 计算每个容器的实际高度,返回编辑器 */
useLayoutEffect(() => {
const contents = document.querySelectorAll(".content");
for (let i = 0; i < contents.length; i++) {
currentCacheCopm[i].clientHeight = contents[i].clientHeight;
}
window.parent.postMessage({ currentCacheCopm }, "*");
}, [currentCacheCopm]);
/** 获取处于操作态的组件 */
const getCurrentOperation = (compIndex) => {
window.parent.postMessage({ compActiveIndex: compIndex }, "*");
};
return (
<div className="preview">
{currentCacheCopm.length > 0 &&
currentCacheCopm.map((comp, index) => {
// 同理 key 忽略diff优化
return (
<div
className="content"
key={id++}
onClick={() => getCurrentOperation(index)}
>
{renderJson(comp)}
</div>
);
})}
</div>
);
};
然后编辑器拿到子页面被卷去的高度之后,再使得整个iframe克隆盒子上移相同高度就?️了
写到最后
最近有些负能量,便出去走了走。昨天在承德一个小山村了,看到了一个站牌上的标语感觉很受触动。分享给大家:人不行,别怨路不平
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!