参考页面
自动化脚本原理
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插件开发
整体通信逻辑
配置文件 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坐标的准确性,请尽量在分离模式下使用开发者工具,浏览器窗口大小也需要和录制时保持一致.