form表单字段级悬浮提示,在子应用中监听主应用下所有子应用

232 阅读5分钟

大家好,我是前端小菜,希望多多指教,一起讨论学习!!! 本文旨在少量或不改动客户代码的前提下,实现操作页面元素的功能!

项目背景

客户项目是主应用下集成很多子应用,我们的项目是以小助手的形式集成在客户项目中,承担着一级二级菜单相应提示引导,和知识搜索等功能。

新的客户需求,需要更细颗粒度的提示功能,比如三级四级菜单的页面提示和引导,页签级的提示引导,其中最细颗粒度的提示是页面中form表单的字段级提示。

今天我们讨论的重点就是最细颗粒度的form表单字段级的悬浮提示。

需求

在不动主应用代码或者最少动主应用代码的前提下,实现所有子应用下form表单字段级提示,form表单在各个子应用中,子应用都是按需加载,form表单可能出现在子应用的各个页面,页面中的TAB页签,弹窗,抽屉等,form表单中哪些字段需要悬浮提示可后台配置,提示内容可后台配置,提示内容的展示形式也可配置,比如:是否有标题,是否有图片表格等。

流程

  • 在主应用中拉起某个子应用,实时监听主应用下渲染的子应用
  • 查询当前页面中是否有form表单
  • 如果有form表单,则查询当前页面是否有配置提示内容
  • 如果有配置内容,则将提示内容渲染到DOM,并且实现鼠标悬浮时,展示提示内容
  • 如果没有配置内容,则获取form表单的DOM,将form表单的DOM发送给后台,以便后台根据DOM配置哪些字段需要悬浮提示和提示内容
  • 如果没有form表单,则不处理

逻辑分析

  1. 获取提示的字段: 在需要提示的字段处调用插件组件 调用组件时传入参数,字段名称、字段key、callback方法,用户接口返回字段提示内容
  2. 初始渲染,尚未配置时不展示提示
  3. 组件生命周期函数中调用接口,将字段信息存入数据库
  4. 后台配置字段提示内容
  5. 组件中同步调用查询接口,将提示内容返回到上述callback方法
  6. 将提示内容通过组件tooltip展示在页面
  7. 鼠标悬浮提示内容

核心技术

VUE,jquery,css,web api,js

功能点分析

1.不动主应用代码或者最少动主应用代码的前提,所以功能逻辑只能写在子应用中

新建无页面的子应用,子应用挂在在主应用下,保证子应用可以实时主应用下的子应用加载

2.实时监听主应用下子应用的渲染

这里我们用到了MutationObserver监听DOM变化,这里网上有很多教程,我就不多说了。

关键代码:

let observer = new MutationObserver(this.observerCallback)
window.parent.document.querySelector(".frame-parent") && 
observer.observer(window.parent.document.querySelector(".frame-parent"), {
  "characterData": true,
  "attributes": true,
  "childList": true,
  "subtree":true,
  "attributeFilter": ["style", "src"] // 由于系统中有其他逻辑会给.frame-parent添加属性,所以加个过滤
})

3.进入页面监听之后,由于在子应用中无法监听主应用下其他子应用的生命周期,所以加了个定时器,去获取子应用下的form表单,一次3s,5次获取不到就说明该页面下没有form表达,就不做处理,定时器要随时清除。

关键代码:

        this.times = 0
        clearInterval(this.inter)
        this.inter = null
        this.inter = setInterval(() => {
          this.times += 1
          // 当前激活的iframe路由做处理,这里要根据实际url做处理
          let url = val.startsWith('/#/') ? val.slice(3) : val
          // iframe的父元素,当前激活的iframe与所有iframe的src做匹配
          // 表单,同个页面存在多个表单,不同页签存在表单
          let currentIframe = Array.from($(".frame-content").contents()).find(el => el.src && el.src.split('#/')[1].split('?')[0] === url)
          console.log(this.times)
          // 判断当前iframe页面有没有表单元素
          if (currentIframe && Array.from($(currentIframe.contentWindow.document).find('form'))[0]) {
            // 如果找到form表单就继续下面的逻辑
            clearInterval(this.inter)
            this.inter = null
            this.getFormInfo(url)
          } else {
            // 如果5次没有找到表单,就停止
            if (this.times > 5) {
              clearInterval(this.inter)
              this.inter = null
            }
          }
        }, 3000);

4.获取页面form表单,并传参

关键代码:

      let url = this.requestParams.url
      // iframe的父元素,当前激活的iframe与所有iframe的src做匹配
      let currentIframe = Array.from($(".frame-content").contents()).find(el => el.src && el.src.split('#/')[1].split('?')[0] === url)
      // 表单,同个页面存在多个表单,不同页签存在表单
      let html = ''
      Array.from($(currentIframe.contentWindow.document).find('form').parent()).forEach(el => {
        html += el.innerHTML
      })
      let requestParams = {
        ...this.requestParams,
        htmlResult: html
      }
      axios.post(url, requestParams)

5.获取到提示内容后,渲染DOM

关键代码:

      let _this = this
      // 当前激活的iframe路由做处理
      let url = this.requestParams.url
      // iframe的父元素,当前激活的iframe与所有iframe的src做匹配
      let currentIframe = Array.from($(".frame-content").contents()).find(el => el.src && el.src.split('#/')[1].split('?')[0] === url)
      // 表单,同个页面存在多个表单,不同页签存在表单
      let doms = Array.from($(Array.from($(currentIframe.contentWindow.document).find('form'))).find("label")).filter(v => this.showInfoLabels.includes(v.innerText))
      console.log(doms)
      doms.forEach(ele => {
        // 将原有页面字段的悬浮提示清空,保证字段添加图标的悬浮提示
        ele.title = ''
        // 当前字段的信息
        let currentInfo = _this.labelsInfo.find(v => v.filedName === ele.innerText)
        // 根据接口返回的字段的tipPos添加提示按钮的位置,0在字段前,1在字段后,图标用UI组件在页面渲染后的代码,不能直接用icon组件
        ele.innerHTML = currentInfo.tipPos === 0 ? '<i data-v-05d524fb="" class="test" style="vertical-align: baseline; margin: 0 4px" aria-label="图标: edit" tabindex="-1" class="table-opt icon-blue opt-cursor anticon anticon-edit"><svg viewBox="64 64 896 896" data-icon="edit" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 0 0 0-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 0 0 9.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"></path></svg></i>' + ele.innerText
          : ele.innerText + '<i data-v-05d524fb="" class="test" style="vertical-align: baseline; margin: 0 4px" aria-label="图标: edit" tabindex="-1" class="table-opt icon-blue opt-cursor anticon anticon-edit"><svg viewBox="64 64 896 896" data-icon="edit" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 0 0 0-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 0 0 9.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"></path></svg></i>'
        // 提示图标的鼠标移入移出事件
        $($(ele).find('.test')[0]).hover(
          function(e) {
            // 如果两次悬浮的字段不一样,则之前是否点击图标要置为false
            if (_this.current !== ele.innerText) {
              _this.isClickIcon = false
            }
            _this.knowledgeTitle = ''
            _this.current = ele.innerText
            _this.currentInfo = _this.labelsInfo.find(v => v.filedName === ele.innerText)
            // 鼠标移入时,定位提示框的位置
            var top = e.pageY + 80;
            var left = e.pageX + 80;
            $(".form-label-new-tip").css({
              'top': top + 'px',
              'left': left + 'px',
              'width': '280px',
              'height': '140px',
            })
            $(".form-label-content").css({
              'width': '280px',
              'height': _this.currentInfo && _this.currentInfo.tipAnsType === 1 ? '64px' : '108px',
            })
            // 移入时,提示框内容重置为loading,loading用UI组件渲染成的dom
            $(".form-label-content")[0].innerHTML = '<div class="loading" style="min-height: 108px;display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;background: rgba(255,255,255,.5)"><div data-v-7e74327e="" class="ant-spin ant-spin-spinning"><span class="ant-spin-dot ant-spin-dot-spin"><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i></span></div></div>'
            
            $(".form-label-new-tip").stop().show(() => {
              $(".form-label-new-tip").css({
                'width': 'auto',
                'height': 'auto',
              })
              $(".form-label-content").css({
                'width': 'auto',
                'height': 'auto',
              })
              let requestParams = {
                id: _this.currentInfo.id,
                knowledgeId: _this.currentInfo.knowledgeId,
                webId: _this.currentInfo.webId,
              }
              let data = {
                tipAnsType: 1,
                tipTitle: '这是标题',
                tipAnsContent: '这是内容'
              }
              _this.currentInfo.htmlInfo = data.tipAnsContent
                  _this.knowledgeTitle = data.tipAnsType === 1 ? data.tipTitle : ''
                  $(".form-label-content")[0].innerHTML = data.tipAnsContent
            })
          },
          function() {
            if (_this.isClickIcon) return
            clearTimeout(_this.timmer)
            _this.timmer = null
            
            // 鼠标移出加个延迟,保证鼠标移出提示框提示内容不消失,随时清楚定时器
            _this.timmer = setTimeout(() => {
              if (_this.mouseStatus) return
              $(".form-label-new-tip").css({
                'width': '280px',
                'height': '140px',
              })
              $(".form-label-content").css({
                'width': '280px',
                'height': _this.currentInfo && _this.currentInfo.tipAnsType === 1 ? '64px' : '108px',
              })
              $(".form-label-new-tip").stop().hide(500)
              clearTimeout(_this.timmer)
              _this.timmer = null
            }, 300)
          }
        )
        // 提示图标增加点击事件,点击之后提示框鼠标移出不消失,点击提示框右上角X号,提示框消失
        $($(ele).find('.test')[0]).click(() => {
          // 如果提示框loading,则无需点击事件
          _this.isClickIcon = true
          $(".form-label-new-tip").show()
        })
      })

回顾

本文旨在抛砖引玉,随着客户的需求越来越奇葩,明明在客户代码中几句代码搞定的事,到我们手里就得想破脑袋去实现。 本次功能实现过程中也是伴随着客户需求的一直变更,反反复复,比如提示内容中增加鼠标交互要实现表单内容回填等,这边主要以思路为主,如果大家有更好的思路,可以一起讨论学习。