chrome插件开发-自动化脚本(5)

2,421 阅读2分钟

已实现的功能

  1. 全屏点击事件 √
  2. <input>,<textarea>输入事件 √
  3. 滚动事件 √
  4. <input>,<textarea>自定义key √
  5. 脚本和事件可编辑 √
  6. 脚本和事件的可视化 √
  7. 导入导出 √
  8. 批量导入导出 √
  9. tab传参调用另一tab中的脚本 √

社保网站的坑

之前说了做这个插件是给人事办理人员用的,然后我自认为做的差不多可以给别人用了,就问人事要了Ukey进行实际操作

但是万万没想到政府网站竟然是jsp页面,还用了iframe,有的还是动态加载和动态创建的,还有使用了html原生的select标签,我真是日了狗了.没办法只能硬着头皮啃了.

image.png

需要实现的功能

  1. 一层<iframe>中的上述事件
  2. <select>选择事件

一层<iframe>中的上述事件的录制和执行

首先怎么绑定iframe中的事件,有2种方法都要用到

  1. jquery的第2个参数,指定在哪个元素下找input
$('input', doc).on('input', input)
  1. 绑定this contentWindow为iframe的window对象 doc为iframe的document对象
doc.addEventListener('click', onclick.bind(contentWindow), true)

再获取真实的鼠标坐标


/**
 * this = contentWindow
 * @param ev
 */
function onclick (ev) {
  let {x, y} = getPosition_Iframe(ev, this) // 获取真实坐标
  let delay = new Date().getTime() - window.startTime
  if (delay > 5 && !window.running) {
    window.startTime += delay

    let event = {
        x,
        y,
        clientX: ev.clientX,
        clientY: ev.clientY,
        type: 'click',
        tagName: ev.target.tagName,
        time: delay
      }
      chrome.runtime.sendMessage({
        type: 'add-event',
        event: event
      })
    
    
    console.log('onclick', event, ev)
  }
}


/**
 * 网上找的方法, 自己改了下能用
 * 获取最底层iframe页面中鼠标点击的坐标
 */
function getPosition_Iframe (event = {}, contentWindow) {
  var parentWindow = contentWindow.parent;
  var tmpLocation = contentWindow.location;
  var target = null;
  var left = 0;
  var top = 0;
  while (parentWindow != null && typeof (parentWindow) != 'undefined' && tmpLocation.pathname != parentWindow.location.pathname) {
    for (var x = 0; x < parentWindow.frames.length; x++) {
      if (tmpLocation.pathname == parentWindow.frames[x].location.pathname) {
        target = parentWindow.frames[x].frameElement;
        break;
      }
    }
    do {
      left += target.offsetLeft || 0;
      top += target.offsetTop || 0;
      target = target.offsetParent;
    } while (target)
    tmpLocation = parentWindow.location;
    parentWindow = parentWindow.parent;
  }
  let xy =  {x: left + (event.clientX || 0), y: top + (event.clientY || 0)}
  return xy
}

修改event对象

event对象前后区别

// 之前xy就是鼠标点击的坐标
event = {
    x: ev.clientX,
    y: ev.clientY,
    type: 'click',
    tagName: ev.target.tagName,
    time: delay
  }
  
  
// 修改后的event
// xy表示在主窗口的鼠标点击坐标
// clientX,clientY表示 在iframe中的鼠标点击坐标
event = {
    x,
    y,
    clientX: ev.clientX,
    clientY: ev.clientY,
    type: 'click',
    tagName: ev.target.tagName,
    time: delay
  }

iframe绑定事件

通用的绑定方法

function bindEvent (doc, contentWindow) {
// 不用 jquery on方法, useCapture设置为true在捕获时就触发, 是为了避免stopPropagation的情况
  doc.addEventListener('click', onclick.bind(contentWindow), true)

  $('input', doc).on('keyup', onkeyup)
  $('input', doc).on('keydown', onkeydown)
  $('input', doc).on('input', input)
  $('input', doc).on('compositionstart', compositionstart)
  $('input', doc).on('compositionend', compositionend)

  $('textarea', doc).on('keyup', onkeyup)
  $('textarea', doc).on('keydown', onkeydown)
  $('textarea', doc).on('input', input)
  $('textarea', doc).on('compositionstart', compositionstart)
  $('textarea', doc).on('compositionend', compositionend)

  // 绑定鼠标移动事件
  doc.addEventListener('mousemove', throttle(setScrollWatcher.bind(contentWindow)), true)
}

调用绑定方法

// 页面上的iframe集合
let iframes = new Set()
function bind () {
  window.startTime = new Date().getTime()

  // 绑定顶层页面
  bindEvent(document, window) 

  $('iframe').each((index, iframe) => {
    iframes.add(iframe)
    // 给当前有的iframes添加事件
    let iframeContentWindow = iframe.contentWindow
    let doc = iframeContentWindow.document
    doc.removeEventListener('click', onclick.bind(iframeContentWindow), true)
    bindEvent(doc, iframeContentWindow)

    // 给变化后的iframes添加事件
    $(iframe).on('load', function (event) {
      iframes.add(iframe)
      iframeContentWindow = iframe.contentWindow
      doc =  iframeContentWindow.document
      doc.removeEventListener('click', onclick.bind(iframeContentWindow), true)
      bindEvent(doc, iframeContentWindow)
    })
  })

  // 给动态生成的iframe绑定事件
  $(document).bind('DOMNodeInserted', throttle(() => {
    $('iframe').each((index, iframe) => {
      if (!iframes.has(iframe)) {
        let iframeContentWindow = iframe.contentWindow
        let doc = iframeContentWindow.document
        doc.removeEventListener('click', onclick.bind(iframeContentWindow), true)
        bindEvent(doc, iframeContentWindow)
      }
    })
  }, 500))
  console.log('bind.js 已运行')
}

执行event对象

let focusTarget = null
function startEvent (item, i) {
  let target = null
  switch (item.type) {
    case 'click':
      let click
      target = document.elementFromPoint(item.x, item.y)
      // 通过真实的xy获取到的可能是iframe元素
      if (target.tagName === 'IFRAME') {
        // iframe的document用clientX,clientY获取一次
        target = target.contentWindow.document.elementFromPoint(item.clientX, item.clientY)
        click = new MouseEvent('click', {
          clientX: item.clientX,
          clientY: item.clientY,
          bubbles: true,
          cancelable: true
        })
      } else {
        click = new MouseEvent('click', {
          clientX: item.x,
          clientY: item.y,
          bubbles: true,
          cancelable: true
        })
      }
      target.dispatchEvent(click)
      // 为下一个输入事件做准备
      if (item.tagName === 'INPUT' || item.tagName === 'TEXTAREA') {
        target.focus && target.focus()
        focusTarget = target
      } else if (item.tagName === 'SELECT') {
        focusTarget = target
      } else {
        focusTarget = null
      }
      break
  }
  console.log(`自动化-${i}-${item.type} 后等待${item.time}毫秒`)
}

高亮功能和这个类似

<select>选择事件

绑定select事件

select真的是一个奇葩的dom, 网上的所有方式都不能完美实现功能, 要么打不开下拉框, 要么打开的位置不对. 但是我通过测试找到了实现的方法. 我发现点击option后返回的坐标是负的! 这样就能在click事件中做做文章了~ 修改后的onclick方法

/**
 * this = contentWindow
 * @param ev
 */
function onclick (ev) {
  let {x, y} = getPosition_Iframe(ev, this)
  let delay = new Date().getTime() - window.startTime
  if (delay > 5 && !window.running) {
    window.startTime += delay

    let event

    // 原生<select>事件太奇葩, 所以把设置select值得时机放在点击<option>中,目前支持单选
    if (ev.target.tagName === 'SELECT' && ev.clientX < 0 && ev.clientY < 0) {
      if (ev.target.selectedOptions) {
        // 获取到select选中的值
        let val = []
        for (let option of ev.target.selectedOptions) {
          val.push(option.value)
        }
        if (val.length === 0) {
          val = ''
        } else if (val.length === 1) {
          val = val[0]
        }
        event = {
          type: 'set-select-value', // 新增一个事件类型
          key: '',
          value: val,
          time: delay
        }
        chrome.runtime.sendMessage({
          type: 'add-event',
          event: event
        })
      }
    } else {
      event = {
        x,
        y,
        clientX: ev.clientX,
        clientY: ev.clientY,
        type: 'click',
        tagName: ev.target.tagName,
        time: delay
      }
      chrome.runtime.sendMessage({
        type: 'add-event',
        event: event
      })
    }
    console.log('onclick', event, ev)
  }
}

执行事件

用了和input一样的方式模拟InputEvent事件

// 亲测有效 
  case 'set-select-value':
      if (focusTarget) {
        // 具体看 https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js
        let prototype = window.HTMLSelectElement.prototype
        let nativeInputValueSetter = Object.getOwnPropertyDescriptor(prototype, "value").set;
        nativeInputValueSetter.call(focusTarget, item.value);
        let inputEvent = new InputEvent('input', {bubbles: true})
        focusTarget.dispatchEvent(inputEvent)
      }
      break

演示效果

www.bilibili.com/video/BV1aU…