本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
此篇为上一遍文章的续篇 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介绍。
 
- 提示下载完但解压或打开不了?
 
- 找不到素材资源介绍文章里的示例图片?
 
- 模板不会安装或需要功能定制以及二次开发?
 
                    
    
发表评论
还没有评论,快来抢沙发吧!