最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • low code 可视化平台之跨iframe拖拽 - 掘金

    正文概述 掘金(宫小白)   2021-10-13   963

    本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

    此篇为上一遍文章的续篇 Low code 之从零搭建一个h5可视化平台

    之前看到过转转它们发表过一个此类的博文,不过它们因react-dnd不支持跨iframe便放弃了,自己使用原生H5的拖拽事件封装一个。

    其实吧,无论使用哪种它们的核心都是一样的。要认清楚这一点:我们实际拖拽的并不是视图而是数据

    什么意思?简单画个图吧。

    low code 可视化平台之跨iframe拖拽 - 掘金

    即,我们通过拖拽要做的真实目的是想要去改变iframe区域的数据源。iframe拿到最新的数据源重新渲染就OK了

    来吧,我们还是利用react-dnd,简单实现一下吧

    low code 可视化平台之跨iframe拖拽 - 掘金

    创建一个数据源

    首先,在它们的统一父组件内创建一个状态,用于保存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区域的每一个组件都设为放置目标

    low code 可视化平台之跨iframe拖拽 - 掘金

    图解:

    low code 可视化平台之跨iframe拖拽 - 掘金

    等到新来组件插入,此时我们要做的操作

    • 移动占位元素
    • 将新来组件插入指定位置

    现在我们给图上标记索引

    low code 可视化平台之跨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布局一摸一样的盒子。这个盒子的工作就是上面我们写的逻辑。即在拖拽过程中,展示每个组件的名称及其占位元素的位置

    为什么这样搞呢?

    1. 首先拖拽的预览展示逻辑其实都是属于编辑器的。但是呢,我又不想编辑器去读react or vue 的相关代码。这就涉及到一个问题,编辑本身是无法预览的,因为它没有办法进行组件的渲染,所以只能去借助iframe

    2. 而在预览项目,我又不想加入过多的编辑器操作相关的逻辑

    那么到现在位置,当我们处于拖拽过程中,其实其中iframe表面区域是一个克隆了iframe基本布局样式的大盒子。

    low code 可视化平台之跨iframe拖拽 - 掘金

    这样做就又出现了一个新的问题。iframe的克隆盒子怎么保证和真正的iframe有一样的布局呢?

    换句话说,虽然我们现在已经有了一个渲染区域的数据源。但是对iframe和iframe克隆者来说两者的渲染怎么能保持一致呢?对于iframe来说,我们只需要将数据源传给它,它就可以依照预览项目的逻辑去渲染。可是iframe的克隆盒子是在编辑器中,无法真正渲染数据源内的相关组件。也就无法得到每个组件的高度。

    这就出现了个相当严重的问题,当编辑器发生拖拽时。iframe的克隆盒子显现,但是它里面每个组件位置和iframe中的组件位置完全对应不上。

    怎么处理呢?

    其实也很简单,我们可以等到组件在预览项目中渲染完毕之后。获取高度反传回编辑器中,编辑器中的iframe盒子此时就可以拿到与iframe一样的尺寸了。

    改造拖拽目标代码

    要做哪些改造呢?

    1. 拖拽结束之后向iframe内传入当前最新的数据源
    2. 拖拽结束之后,以真正的组件数据替换掉占位标签
    3. 拖拽开始时,显示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>
      );
    };
    

    预览项目的处理情况

    预览项目要做什么呢?

    1. 接受父页面传过来的数据源

    2. 根据数据源进行组件的渲染

    3. 在组件的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的克隆盒子的组件就有了高度。

    到这里,我们来梳理一下总流程吧

    1. 用户触发拖拽,

      1. 拖拽目标

        • 通知预览区域展示iframe克隆盒子

        • 向数据源加入一个占位元素

      2. 渲染区域

        • 在iframe的克隆盒子中根据数据源信息渲染占位组件
    2. 用户拖拽行为结束

      1. 拖拽目标

        • 判断是否成功放置到目标区域 是 ?以真正数据信息替换条占位元素信息 :删除占位信息

        • 向iframe子页面传送最新的数据源信息

        • 通知渲染区域隐藏iframe克隆盒子

      2. 预览项目

        • 根据最新的数据源,渲染相关组件。并且返回主页面每个组件的高度
      3. 渲染区域

        • 隐藏iframe克隆盒子

    还有一些小问题——处理滚动

    当iframe区域出现滚动条时,iframe的克隆盒子和iframe之间组件位置又不匹配了

    问题原因:因为iframe中此时的子页面,由于发生了滚动它的头部已经被卷去了一部分,但是此时的iframe克隆盒子还是完全从top为0开始。

    如图:方便查看,我先把iframe和iframe克隆拆开来。本应它两应该是完全重合在一块的,可以看到现在它们组件位置已经完全不匹配了

    low code 可视化平台之跨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克隆盒子上移相同高度就?️了

    写到最后

    最近有些负能量,便出去走了走。昨天在承德一个小山村了,看到了一个站牌上的标语感觉很受触动。分享给大家:人不行,别怨路不平


    下载网 » low code 可视化平台之跨iframe拖拽 - 掘金

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元