大家好,我是前端小菜,希望多多指教,一起讨论学习!!! 本文旨在少量或不改动客户代码的前提下,实现操作页面元素的功能!
项目背景
客户项目是主应用下集成很多子应用,我们的项目是以小助手的形式集成在客户项目中,承担着一级二级菜单相应提示引导,和知识搜索等功能。
新的客户需求,需要更细颗粒度的提示功能,比如三级四级菜单的页面提示和引导,页签级的提示引导,其中最细颗粒度的提示是页面中form表单的字段级提示。
今天我们讨论的重点就是最细颗粒度的form表单字段级的悬浮提示。
需求
在不动主应用代码或者最少动主应用代码的前提下,实现所有子应用下form表单字段级提示,form表单在各个子应用中,子应用都是按需加载,form表单可能出现在子应用的各个页面,页面中的TAB页签,弹窗,抽屉等,form表单中哪些字段需要悬浮提示可后台配置,提示内容可后台配置,提示内容的展示形式也可配置,比如:是否有标题,是否有图片表格等。
流程
- 在主应用中拉起某个子应用,实时监听主应用下渲染的子应用
- 查询当前页面中是否有form表单
- 如果有form表单,则查询当前页面是否有配置提示内容
- 如果有配置内容,则将提示内容渲染到DOM,并且实现鼠标悬浮时,展示提示内容
- 如果没有配置内容,则获取form表单的DOM,将form表单的DOM发送给后台,以便后台根据DOM配置哪些字段需要悬浮提示和提示内容
- 如果没有form表单,则不处理
逻辑分析
- 获取提示的字段: 在需要提示的字段处调用插件组件 调用组件时传入参数,字段名称、字段key、callback方法,用户接口返回字段提示内容
- 初始渲染,尚未配置时不展示提示
- 组件生命周期函数中调用接口,将字段信息存入数据库
- 后台配置字段提示内容
- 组件中同步调用查询接口,将提示内容返回到上述callback方法
- 将提示内容通过组件tooltip展示在页面
- 鼠标悬浮提示内容
核心技术
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()
})
})
回顾
本文旨在抛砖引玉,随着客户的需求越来越奇葩,明明在客户代码中几句代码搞定的事,到我们手里就得想破脑袋去实现。 本次功能实现过程中也是伴随着客户需求的一直变更,反反复复,比如提示内容中增加鼠标交互要实现表单内容回填等,这边主要以思路为主,如果大家有更好的思路,可以一起讨论学习。