一、问题来源与需求分析
1、问题来源
需求:接到一个需求,需要写一个浏览器插件去注入系统页面,去实现自动登录的功能。
没错,在我们的生活中,总是能收到这种一句话的需求,开局一句话,内容全靠想
2、需求分析
浏览器插件好说(再次强调,后面会出一个系列),注入系统页面也好说,只要使用 content.js 写一下注入脚本,然后在 mainifest.json 里一定义,直接齐活,这没什么难的。
{
"manifest_version": 2,
"name": "xxx",
"version": "1.0",
"description": "xxx",
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"],
"css": ["styles.css"],
"run_at": "document_end"
}
],
}
自动登录理论上也好说,一般来说登录页面的input元素都有定制化的样式逻辑什么的,因此类名、id什么的,应该很容易找到他的dom元素。那么理论上来说,这个需求应该没有那么难实现
注意到我划横线的话了么,这是一个程序员的思维重点,以后我的思维重点都会用下划线表示,培养思维很重要
需要考虑的技术栈有:
- vue3
- angular
3、开始操作挖坑
找到发布的系统登录入口,开始寻找有特点的类名,发现 angular 的输入框,都被定义了 login 的类名,通过这个和input 的type,就可以找到两个input,但是找 vue3 的时候发现,都是统一的标准input:没有id,class名是:el-input,好好好....
既然常规玩法不同,那就玩点非常规的,发现系统的入口都有提示语:请输入xxx,明白了,就用这个就可以找到了!
const user = document.querySelector( 'input[placeholder="请输入账户名"]' )
const pass = document.querySelector( 'input[placeholder="请输入密码"]' )
找到了input元素,接下来找 button ,不出我所料,也是非常标准的 antd 和 element 组件类名,可以看出来用的都是组件库的按钮组件,这组件里甚至还套了一层 <span>,都不能用我的 innerHTML 大法了,但是不出我所料,果然登录页只有一个按钮,那好说了,直接就是一波获取。
const loginButton = document.querySelector( 'button' )
ok,元素获取完了,接下来理一下思路:
第一步:获取元素第二步:写入用户名密码
第三步:点击登录按钮
接下来开始写入用户名密码:相信学过一个礼拜JS的都知道如何搞:
user.value = '小刘'
pass.value = 'https://blog.51cto.com/u_13981731/12619250'
直接搞定,那么开始第三步,点击登录按钮,其实也简单,学过一个月JS的都知道怎么搞:
setTimeout( () => {
loginButton.click()
}, 0)
看到了这个 setTimeout 了没有,学过一年JS的都知道这里面的门道有多深,这一手体现了我对JS那似海深般的理解(手动狗头)。
ok,问题解决,直接上效果
3、开始踩坑
不运行不知道,一运行吓一跳,首先,脚本根本执行不到,元素根本获取不到!!!
坑点一:资源加载问题
一开始我是直接写在js里的,肯定获取不到,忽然想到当年写 jQuery时,js 都是写在 onload 里的,原来如此,赶紧试试
window.onload = () => {
// 写入逻辑
}
发现好像解决了又好像没解决,有时候能获取到,有时候获取不到,输出了日志,发现dom一会儿是 原始的 public/index.html 的内容,一会儿又是渲染后的内容,尤其是angular,好好好,那我问豆包了:
js 如何等待所有的资源都执行完后,再执行?
window.onload
事件会在整个页面(包括 HTML 文档、样式表、脚本、图像等所有资源)加载完成后触发。- 示例:
window.onload = function() { console.log("所有资源加载完成后执行此函数"); // 在这里可以安全地访问和操作页面中的所有元素 var element = document.getElementById("myElement"); if (element) { element.style.color = "red"; } };
- 在这个示例中,当所有资源加载完成后,会在控制台打印一条消息,并且如果页面上存在
id
为myElement
的元素,会将其颜色设置为红色。不过要注意,window.onload
只能设置一个函数,如果多次设置,后面的设置会覆盖前面的。
- 这种方式和
window.onload
类似,但是它可以添加多个监听器,不会出现覆盖的情况。- 示例:
function myFunction() { console.log("所有资源加载完成后执行此函数"); // 可以在这里对页面元素进行操作 var element = document.getElementById("myElement"); if (element) { element.style.color = "blue"; } } window.addEventListener("load", myFunction);
- 首先定义了一个函数
myFunction
,然后使用addEventListener
将这个函数添加到window
对象的load
事件监听器列表中。当所有资源加载完成后,myFunction
就会被执行,从而可以对页面元素进行操作。
- 可以创建一个
Promise
,通过检查document.readyState
的状态来确定页面是否完全加载。document.readyState
有三个可能的值:loading
(文档正在加载)、interactive
(文档已被解析,用户可以与部分内容交互,但图像等资源可能还没加载完)和complete
(文档和所有资源都已加载完成)。- 示例
function waitForAllResources() { return new Promise((resolve, reject) => { function checkState() { if (document.readyState === "complete") { resolve(); } else { setTimeout(checkState, 100); } } checkState(); }); } waitForAllResources().then(() => { console.log("所有资源加载完成后执行此函数"); var element = document.getElementById("myElement"); if (element) { element.style.color = "green"; } });
- 在这里,
waitForAllResources
函数返回一个Promise
。在这个Promise
内部,定义了一个checkState
函数来检查document.readyState
。如果状态是complete
,就调用resolve
来表示加载完成,否则通过setTimeout
每隔 100 毫秒再次检查状态。当Promise
被解决(resolve
)后,then
方法中的函数就会被执行,从而可以对页面元素进行操作。
一二点和我的水平差不多,解决不了,那就看第三种方法,看着就高级:
简单来说,就是 document 有一个 readyState 的状态来控制是否加载完成,只要他完成了就完成了,我们需要搞个监听去监听他,但是js是单线程的啊,直接监听直接阻塞啊,所以要搞个 promise (看看多细节)。
再附一下我的代码:
function waitForAllResources() {
return new Promise((resolve, reject) => {
function checkState() {
if (document.readyState === "complete") {
resolve();
} else {
setTimeout(checkState, 100);
}
}
checkState();
});
}
waitForAllResources().then(() => {
console.log( "所有资源加载完成后执行此函数" );
console.log("你是谁", window.location.href)
}
再运行,终于可以获取到元素了,然后也可以写入数据了,然后也可以点确定,然后...
坑点二:model 未更新
还是想简单了,光想着把值写进 input ,没想登录一般都是需要校验的啊,使用框架,都是双向绑定的数据,光是改了 input 的 value ,框架的 model 并没有更新,校验校验过不了,即使登录也没有值啊(手动大哭)
做到这里,其实有点想放弃了,只是想简单写个脚本,怎么可能动摇框架的逻辑根基.......
突然灵光乍现:那我做操作触发model 更新不久好了么,什么能触发更新呢,尝试一下输入框的 input 事件吧!
那么问题来了,怎么才能在js 里手动触发一个事件呢?豆包~~
- 首先,需要获取到
input
元素。可以通过document.getElementById
、document.querySelector
等方法来获取。例如,假设你的input
元素有一个id
为myInput
,可以这样获取它:
收起
javascript
复制
const inputElement = document.getElementById('myInput');
- 然后创建一个
ChangeEvent
(change
事件)对象。在 JavaScript 中,可以使用new Event
或者new CustomEvent
来创建事件对象。对于change
事件,使用new Event
就足够了。
收起
javascript
复制
const changeEvent = new Event('change');
- 最后,使用
inputElement.dispatchEvent
方法来触发这个change
事件。
收起
javascript
复制
inputElement.dispatchEvent(changeEvent);
- 完整的示例代码如下:
收起
javascript
复制
window.onload = function () { const inputElement = document.getElementById('myInput'); const changeEvent = new Event('change'); inputElement.dispatchEvent(changeEvent); };
- 这里的代码假设
input
元素已经存在于页面上,并且window.onload
事件确保页面加载完成后再执行代码。
- 在旧版本的 IE 浏览器(IE 9 及以下)中,不支持
dispatchEvent
方法。可以使用fireEvent
来触发事件。不过,需要注意的是,现代浏览器已经不再支持fireEvent
,如果要兼容旧版本 IE 浏览器,可能需要进行浏览器检测。- 同样先获取
input
元素:
收起
javascript
复制
const inputElement = document.getElementById('myInput');
- 然后使用
fireEvent
触发change
事件:
收起
javascript
复制
if (inputElement.fireEvent) { inputElement.fireEvent('onchange'); }
- 完整的示例代码如下:
收起
javascript
复制
window.onload = function () { const inputElement = document.getElementById('myInput'); if (inputElement.fireEvent) { inputElement.fireEvent('onchange'); } };
- 这种方法主要是为了兼容旧版本的 IE 浏览器,在实际开发中,尽量使用现代浏览器的标准方法(
dispatchEvent
)来触发事件,并且在需要兼容旧版本 IE 时,可以考虑使用工具或者代码库来进行适当的处理
对不起,不用旧IE,果断选择第一种:
user.value = 'superadmin'
pass.value = 'wld8600'
user.dispatchEvent( new Event( "input" ) )
pass.dispatchEvent(new Event("input"))
神奇!!!问题解决!
一套连招,输入点击,登录,丝滑~