关于近日在开发一个UserScript的时候一些做法和思考

相关:

前言

本文主要探讨一下Javascript解决Vue的v-model绑定机制的问题。案例实现代码如上。

主要实现方法是事件捕获和事件生成。

准备

首先我们需要准备一个Vue页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试页面</title>
    <script src="//cdn.bootcss.com/vue/2.5.13/vue.js" ></script>
    <script src="//cdn.bootcss.com/jquery/3.3.1/jquery.js" ></script>
</head>
<body>
<div id="app1">
    <div id="in">
        <textarea name="input" id="myinput" cols="60" rows="5" v-model="text" @input.prevent="text=text.substr(0,10)"></textarea>
        <p>
            <span>输入:</span>{{text}}
        </p>
        <p>
            <button @click="sended=text,text=''">点击发送</button> {{sended}}
        </p>
    </div>
</div>
</body>
<script>
    new Vue({
        el:"#app1",
        data:{
            text:"",
            sended:"",
        },
    });
</script>
</html>

该页面实现了将输入的内容回显,在点击发送的时候将内容显示到发送位置,并且限制文本框最大输入字符个数为10,接下来将使用用户脚本,来干扰 Vue 的 v-model。

用户脚本

用户脚本是指由用户指定的运行在页面之上的 Javascript 脚本,用于为网站提供新功能或者对页面进行修改使其更易用,主要依赖 油猴、Tampermonkey 等浏览器插件,现在也有很多浏览器自带用户脚本的功能,在专门的网站上可以搜索到其他人上传的用户脚本,比如 GreasyFork

Vue 中的 v-model 是通过监听事件来实现监听页面元素更改的

用户脚本相当于是在页面中直接插入由你编写的一个 js 文件,并由浏览器扩展负责在每次展现指定页面的时候都自动引入这个文件。

为了方便调试,我们直接使用Chrome控制台来进行代码调试

调试

  • 首先,使用jq修改输入框
    运行$("#myinput").val("1234567890"),发现textarea元素改变,但是没有在页面中看到输入的回显,点击发送时也不能获取该值,说明 v-model 中的值没有得到更新。
  • 第二步,伪造一个input事件
    使用 $("#myinput")[0].dispatchEvent(new Event("input")) 后,发现能够正确回显,说明 v-model 的值得到了更新
  • 第三步,确认 v-model 的更新位置
    由于事件存在捕获阶段和冒泡阶段,我们在input上使用了stop修饰符阻止了事件冒泡,所以只需要添加一些监听器即可。
    使用以下代码部署监听器:

    let m = $("#myinput")[0];
    m.addEventListener("input",(e)=>{console.log("textarea :在冒泡阶段发现该事件!")});
    m.addEventListener("input",(e)=>{console.log("textarea :在捕获阶段发现该事件!")},true);
    let d = $("#in")[0];
    d.addEventListener("input",(e)=>{console.log("div :在冒泡阶段发现该事件!")});
    d.addEventListener("input",(e)=>{console.log("div :在捕获阶段发现该事件!")},true);

    然后使用 $("#myinput")[0].dispatchEvent(new Event("input")),触发监听器,结果为:

    div :在捕获阶段发现该事件!
    textarea :在冒泡阶段发现该事件!
    textarea :在捕获阶段发现该事件!
    textarea的冒泡事件触发在捕获事件之前,是 target 元素的事件触发遵循代码顺序,代码里面冒泡在前,所以先触发
2019年11月4日 更新,去查了一下文档,当事件进入到当前元素的时候,触发的是目标阶段,并没有冒泡和捕获的区别,所有绑定在该元素上的事件都会被按照代码顺序触发。

可以看到在整个事件流中,v-model 的监听是在目标阶段,然后就因stop修饰符阻止了事件继续冒泡。所以如果我们要对 input 事件进行劫持,可以依靠父元素的事件监听,在捕获阶段将事件劫持。

尝试

刷新页面清除之前的监听器,然后使用以下代码,尝试在捕获阶段对事件进行劫持:

$("#in")[0].addEventListener("input",(e)=>{
    e.stopPropagation(); //阻止事件传播
    console.log("Hijacked A Event");
    return false;
},true);

此时,直接在输入框中进行输入不会回显,可以看到控制台打印出 Hijacked A Event,说明劫持事件已经完成。

完成事件的劫持,则我们最终对 v-model 的欺骗完成了一半,接下来要使用自定义事件来使 v-model 的值可以被js 修改。

首先,刷新页面清除之前的监听器,然后对之前的生成事件的代码进行修改。

function FakedValue(){
     $("#myinput").val("Faked");//修改value    
  
    let e = new Event("input");
    e.myself = true;
      $("#myinput")[0].dispatchEvent(e)//伪造事件,触发V-model更新
}

为事件设置自定义属性,即可在捕获的时候进行区分,监听器的代码只需修改成:

$("#in")[0].addEventListener("input",(e)=>{
    if(e.myself) return true; //判断是否是伪造的事件
    e.stopPropagation(); //阻止事件传播
    console.log("Hijacked A Event");
    return false;
},true);

在页面上运行上述代码,然后就可以发现,自己在输入框的输入会被劫持,而执行 FakedValue() 才可以触发v-model的更新。

总结

以上就是使用 Javascript 通过对事件的劫持来对v-model进行控制的一些思路,对按钮的click事件劫持也可以使用同样的操作。

该文仅供同样开发 UserScript 的同学们参考,正常的网页开发用不到这些内容。详细案例代码参考在文章开头。