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

2,855 阅读3分钟

参考页面

chrome插件开发官方文档v3版本

【干货】Chrome插件(扩展)开发全攻略v2版本

自动化脚本原理

1. 收集鼠标键盘事件

收集事件还需要一个时间维度, 所以每个事件对象中需要上一个事件到这次事件的延迟时间.需要一个全局的时间,记录每次事件的开始时间. window.startTime = new Date().getTime()

1.1 收集鼠标点击事件

function onclick (ev) {
  const x = ev.clientX;
  const y = ev.clientY;
  let delay = new Date().getTime() - window.startTime // 记录延迟时间
  // 这个5毫秒判断可以不加, window.running 是运行中的flag,运行中是不用记录的
  if (delay > 5 && !window.running) {
    window.startTime += delay
    // 这是插件开发的api 后面会讲到原理, 这里就是记录一个点击事件
    chrome.runtime.sendMessage({
      type: 'add-event',
      event: {
        x,
        y,
        type: 'click',
        tagName: ev.target.tagName,
        time: delay
      }
    })
  }
}

绑定点击事件

// 给整个document加上点击事件
// 不用 jquery on方法, useCapture设置为true在捕获时就触发, 是为了避免stopPropagation的情况
document.addEventListener('click', onclick, true)

1.2 收集键盘输入事件

键盘输入事件比较复杂,只是用英文输入是比较简单的,但是有输入法输入时就很麻烦, 网上找了好多博主写的才找到基本可以用的,如下

// 键盘输入事件逻辑
function onkeyup (ev) {
  let flag = ev.target.isNeedPrevent
  if (flag) return
  sendInputMessage(ev)
  ev.target.keyEvent = false
}
function onkeydown (ev) {
  ev.target.keyEvent = true
}
function input (ev) {
  if(!ev.target.keyEvent){
    sendInputMessage(ev)
  }
}
function compositionstart (ev){
  ev.target.isNeedPrevent = true
}
function compositionend (ev){
  ev.target.isNeedPrevent = false
}

// 发送键盘输入事件
function sendInputMessage (ev) {
  let delay = new Date().getTime() - window.startTime
  if (delay > 5 && !window.running) {
    window.startTime += delay
    chrome.runtime.sendMessage({
      type: 'add-event',
      event: {
        type: 'set-input-value',
        value: ev.target.value,
        time: delay
      }
    })
  }
}
// 键盘输入事件逻辑 end

注意看type: 'set-input-value', value: ev.target.value,这里我做的是直接设置输入框中的值,不是一个一个输入文字.比如用户输入一个字符串我爱中国,他可能一个字一个字打,也可能一次打4个字,前面一种触发了4次sendInputMessage,后面一种只触发一次.

前面一次的逻辑是 value: '我' > value: '我爱' > value: '我爱中' > value: '我爱中国'

后面一次的逻辑是 value: '我爱中国'

这样中的好处是以后还可以优化,这个以后再说.接下来是绑定这些事件

// jquery版本是 jquery-1.8.3
// 使用jquery的方法,是为了给动态生成的那些对象也帮上这些事件
$(document).on('keyup', 'input', onkeyup, )
$(document).on('keydown', 'input', onkeydown)
$(document).on('input', 'input', input)
$(document).on('compositionstart', 'input', compositionstart)
$(document).on('compositionend', 'input', compositionend)

1.3 收集鼠标滚动事件

页面滚动也是比较麻烦的, 它可能有多个可滚动区域,也可能嵌套.下面的方法也是在掘金找到的,但是找不到源地址了,所以没法贴出来.


// 页面滚动事件逻辑
let mouseX = 0;
let mouseY = 0;
let scrollStartEl = null; //用于记录滚动的起始元素,为了保证重现操作时为元素设置scrollTop时不出现偏差
let scrollElementSet = new Set();
function setScrollWatcher (ev) {
  mouseX = ev && ev.clientX || mouseX;
  mouseY = ev && ev.clientY || mouseY;
  scrollStartEl = document.elementFromPoint(mouseX, mouseY); // 这个方法可以根据坐标获取最上层的子元素
  let el = scrollStartEl;
  while (el) {
    if (scrollElementSet.has(el)) {
      el = null;
    } else {
      el.onscroll = throttle(recordScrollInfo);
      scrollElementSet.add(el);
      el = el.parentNode;
    }
  }
}
function recordScrollInfo (ev) {
  let el = scrollStartEl;
  // 单纯的滚动也可能引起鼠标对应的dom的变化,滚动结束也需要setScrollWatcher
  setScrollWatcher();
  let scrollRecordInfo = {
    mouseX: mouseX,
    mouseY: mouseY,
    scrollList: []
  }
  while (el) {
    // 记录子节点到最上层html的每个滚动距离
    scrollRecordInfo.scrollList.push({top: el.scrollTop, left: el.scrollLeft});
    el = el.parentNode;
  }
  let delay = new Date().getTime() - window.startTime
  if (delay > 5 && !window.running) {
    window.startTime += delay
    let message = {
      type: 'add-event',
      event: {
        ...scrollRecordInfo,
        type: 'scroll',
        time: delay
      }
    }
    chrome.runtime.sendMessage(message)
  }
}
// 页面滚动事件逻辑 end

绑定鼠标滚动事件

const mousemove = throttle(setScrollWatcher) // 节流滚动
// 绑定鼠标移动事件
document.addEventListener('mousemove', mousemove, true)

2. 运行这些事件

之后就是看怎么运行这些发出去的事件,每个event对象都有tpye和time字段,根据type来运行相应操作,主要通过模拟事件来运行.

// 正在输入的输入框dom
let focusTarget = null

// item 就是第一步发出去的event对象, i是数组的index
function startEvent (item, i) {
  let target = null
  switch (item.type) {
    case 'click':
      let click = new MouseEvent('click', {
        clientX: item.x,
        clientY: item.y,
        bubbles: true,
        cancelable: true
      })
      target = document.elementFromPoint(item.x, item.y)
      target.dispatchEvent(click)
      // 为下一个输入事件做准备
      if (item.tagName === 'INPUT') {
        target.focus && target.focus()
        focusTarget = target
      } else {
        focusTarget = null
      }
      break
    case 'set-input-value':
      // 直接改变input的value是不能触发vue的双向绑定逻辑的, 使用下面模拟输入方式是可以触发的
      if (focusTarget) {
        // 具体看 https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js
        let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
        nativeInputValueSetter.call(focusTarget, item.value);
        let inputEvent = new InputEvent('input', {bubbles: true})
        focusTarget.dispatchEvent(inputEvent)
      }
      break
    case 'scroll':
      target = document.elementFromPoint(item.mouseX, item.mouseY)
      let el = target
      for (let i =0; i< item.scrollList.length; i++) {
        if (typeof item.scrollList[i].top !== 'undefined') {
          $(el).scrollTop(item.scrollList[i].top)
          $(el).scrollLeft(item.scrollList[i].left)
        }
        el = el.parentNode
      }
      break
  }
  console.log(`自动化-${i}-${item.type} 后等待${item.time}毫秒`, target)
}

完成以上逻辑就可以运行自动化脚本了.可以发送整个eventList数组运行,也可以之后发送单独一个event执行.

async function startEventList (vm) {
  for (let i = 0; i < vm.eventList.length; i++) {
    let item = vm.eventList[i]
    startEvent(item, i)
    await sleep(item.time)
  }
}
function sleep (time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve()
    }, time)
  })
}

chrome插件开发

整体通信逻辑

image.png

配置文件 manifest.json

manifest_version: 3使用的是3版本,它和2的一些api是不一样的,一定要看清楚.

background 指定后台js

content_scripts 指定每个浏览器tab页面加载的js

devtools_page 开发工具面板生成逻辑

{
  "name": "自动化脚本",
  "version": "1.0",
  "manifest_version": 3,
  "action": {},
  "permissions": [
    "scripting",
    "activeTab",
    "contextMenus",
    "storage",
    "tabs"
  ],
  "host_permissions": [ "http://*/", "https://*/" ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["js/jquery-1.8.3.js", "js/bind-unbind.js"]
    }
  ],
  "devtools_page": "devtools.html"
}

这个项目目前就只用到了这3个模块,其他的模块可以之后再添加优化.

常驻后台的 background

这个js是插件的心脏,消息就像血液一样流过这里再到达每个模块.

展示内容和发起脚本的 devtools_page

使用vue开发,需要使用csp环境开发,因为v3版本是有这个限制的,怎么使用vue的csp环境开发看这里cn.vuejs.org/v2/guide/in…

绑定事件,解绑事件,运行事件的 content_scripts

这里引入了2个js, js/jquery-1.8.3.js为了绑定和解绑. js/bind-unbind.js主要逻辑,第一步的自动化原理都写在这里.

演示效果

为了保证x,y坐标的准确性,请尽量在分离模式下使用开发者工具,浏览器窗口大小也需要和录制时保持一致.

录制演示

chrome插件-自动化脚本 录制使用演示

运行演示

chrome插件-自动化脚本 运行演示

代码地址

github.com/yjj5855/chr…