GPT4o给了三种方法, 在后面给出. 第三种方法是改赋值=为调用函数, 意味着所有的赋值都要改为函数调用,不实用. 分析下方法一Proxy和方法二getter/setter.
Proxy的好处是不会改变原对象,而是建立了一个新的proxy对象, 对新对象的赋值会触发set函数. 问题有三个: 一是任何键值变化都会触发set,需要自己判断键值; 二是需要将原对象的名称指向新proxy对象, 才能实现无感. 三是无法通过instanceof判断pr一个实例是不是Proxy的实例, 只能自己在创建实例时添加一个自定义key如_isProxy来判断.(后面段落说明)

getter/setter是最早一代vue.js用的方法, 所有在app.data中的键都会用Object.defineProperty()的get/set重定义一遍, 做到自动更新UI. 好处是可以逐个key定义,不影响其他key, 问题是原key值就会消失,除非定义前先保存一下. 并且需要保存到另一个键下面(一般前面加个下划线_)而不能保存在本key下面, 否则会导致反复触发直到调用栈溢出Maximum call stack size exceeded.
这个话题在vue的官方文档中也有讨论: https://cn.vuejs.org/guide/extras/reactivity-in-depth, 里面也提到在vue3中,从getter/setter已经转向了proxy机制.
另外一个问题, 是如果键值是一个对象(数组也是),也就是下面有子键, 对子键键值的修改, 只会调用get而不是set!并且对Proxy和getter/setter机制都一样. 如:

proxyObject.k = {k1:1} // 调用set
proxyObject.k.k1 = 2 //调用get

怎么解决这个问题,GPT4o的回答是递归代理. 见后面一节.

方法1: 使用Proxy

在 JavaScript 中,如果希望在对象的某个键变化时自动调用一个函数,可以使用 Proxy 对象来实现。Proxy 允许你创建一个代理对象,以便对其属性的访问和修改进行拦截。

下面是一个使用 Proxy 的示例代码,当对象的 someKey 属性变化时,会自动调用函数 a

// 定义要监控的对象
const targetObject = {
    someKey: '初始值',
};

// 定义要自动调用的函数
function a(newValue) {
    console.log(`someKey 的新值是: ${newValue}`);
}

// 创建 Proxy 代理对象
const proxyObject = new Proxy(targetObject, {
    set(target, property, value) {
        // 当设置 someKey 时,调用函数 a
        if (property === 'someKey') {
            a(value);
        }
        // 修改属性的值
        target[property] = value;
        // 返回 true 表示设置成功
        return true;
    }
});

// 测试:修改 someKey
proxyObject.someKey = '新的值'; // 这将输出: someKey 的新值是: 新的值

// 可以继续修改其他属性也不会触发 a()
proxyObject.anotherKey = '另一个值';
console.log(proxyObject.anotherKey); // 输出: 另一个值

说明:

  • 在上面的示例中,创建了一个对象 targetObject 和一个函数 a
  • 使用 Proxy 创建了代理对象 proxyObject,并在其 set 拦截器中定义了逻辑。当 someKey 被修改时,会自动调用函数 a(newValue)
  • 修改其他属性不会触发这个函数,只会在指定的键发生变化时执行。

通过这种方式,你可以灵活地监控对象属性的变化,并根据需要触发相应的逻辑。

方法2 使用getter/setter

你可以使用 JavaScript 的 Object.defineProperty() 方法来定义 getter 和 setter,使得每次修改对象属性时都能执行相应的函数。

const targetObject = {
    _someKey: '初始值',  // 私有变量
};

// 定义要自动调用的函数
function a(newValue) {
    console.log(`someKey 的新值是: ${newValue}`);
}

// 使用 Object.defineProperty 定义 getter 和 setter
Object.defineProperty(targetObject, 'someKey', {
    get() {
        return this._someKey;
    },
    set(value) {
        this._someKey = value;
        a(value);  // 调用函数 a
    },
});

// 测试:修改 someKey
targetObject.someKey = '新的值'; // 这将输出: someKey 的新值是: 新的值

// 读取 someKey
console.log(targetObject.someKey); // 输出: 新的值

// 可以继续修改其他属性也不会触发 a()
targetObject.anotherKey = '另一个值';
console.log(targetObject.anotherKey); // 输出: undefined (因为没有定义)

方法3:使用自定义方法

另一种简单的方法是创建一个用于更新对象属性的方法,以便所有更改都通过此方法发生,这样就可以在里面调用相关逻辑。

const targetObject = {
    someKey: '初始值',
};

// 定义要自动调用的函数
function a(newValue) {
    console.log(`someKey 的新值是: ${newValue}`);
}

// 自定义方法用于更新键并执行相应逻辑
function setSomeKey(value) {
    targetObject.someKey = value;   // 更新属性
    a(value);                       // 调用函数 a
}

// 测试:修改 someKey
setSomeKey('新的值'); // 这将输出: someKey 的新值是: 新的值

// 读取 someKey
console.log(targetObject.someKey); // 输出: 新的值

// 修改其他属性的方法不变,不会触发函数 a()
targetObject.anotherKey = '另一个值';
console.log(targetObject.anotherKey); // 输出: 另一个值

如何知道实例是Proxy的实例

在 JavaScript 中,可以使用 Proxy 对象来创建代理,从而控制对某个对象的基本操作,如属性查找、赋值、枚举、函数调用等。如果想要判断一个对象是否是一个 Proxy 实例,可以通过以下方法:

  1. 利用 WeakMap: 在将对象包装为 Proxy 时,可以使用 WeakMap 来存储原始对象和其代理之间的关系。通过这种方式,你可以检查某个代理是否存在于这个 WeakMap 中。

     const originalObject = {};
     const proxyMap = new WeakMap();
    
     const proxy = new Proxy(originalObject, {
         set(target, prop, value) {
             return Reflect.set(target, prop, value);
         }
     });
    
     // 存入 WeakMap
     proxyMap.set(proxy, originalObject);
    
     function isProxy(obj) {
         return proxyMap.has(obj);
     }
    
     console.log(isProxy(proxy)); // true
     console.log(isProxy(originalObject)); // false
  2. 标记法: 你可以在创建 Proxy 时在其内部设置一个标记属性,以便后续判断。

     const originalObject = {};
    
     const proxy = new Proxy(originalObject, {
         get(target, prop) {
             if (prop === '_isProxy') return true;
             return Reflect.get(target, prop);
         }
     });
    
     function isProxy(obj) {
         return obj && obj._isProxy === true;
     }
    
     console.log(isProxy(proxy)); // true
     console.log(isProxy(originalObject)); // false
  3. 使用 instanceof 关键字: 不幸的是,直接通过 instanceof 来判断是否为 Proxy 是不可能的,因为我们无法直接访问 Proxy 的构造函数。

总的来说,上述方法是较常见的判断一个对象是否为 Proxy 的方案。选择适合自己需求的方法来实现即可。

递归代理

要解决object下的key是对象或者数组, 其子键或者子元素被修改时调用的是get而不是set的问题, GPTo答复是递归代理. 下面给出的是对象的递归代理, 实际还需要考虑数组.
递归代理是对了,但是GPT4o在第一次给出的代码中并没有实现递归代理,指出后,在第二次实现了递归代理, 但设置深层子元素的时候调用了2次set(重复代理了),指出问题后, GPT4o再修改了6次, 也没能回答正确. 感觉是黔驴技穷啦. 这儿贴出其中一次的代码.

function createReactiveObject(obj) {
    return new Proxy(obj, {
        set(target, property, value) {
            // 检查设置的值是否为对象
            if (typeof value === 'object' && value !== null) {
                // 用 Proxy 处理该对象
                value = createReactiveObject(value);
            }
            // 设置实际值
            target[property] = value;
            console.log(`属性 ${property} 被设置为: ${value}`); // 输出变更信息
            return true; // 返回成功标识
        },
        get(target, property) {
            const value = target[property];
            // 仅当获取的是对象时才返回新的代理,避免递归代理产生重复
            if (typeof value === 'object' && value !== null) {
                return createReactiveObject(value); // 利用递归返回新的代理而不是多次创建同一对象的代理
            }
            return value;
        }
    });
}

// 创建动态对象并使用代理函数包裹它
const dynamicObject = createReactiveObject({
    k1: { k2: { k3: {} } } // 初始化嵌套结构 
});

// 测试:设置嵌套属性
dynamicObject.k1.k2.k3.k4 = 20; // 应只输出一次: 属性 k4 被设置为: 20

// 打印查看当前状态
console.log(dynamicObject); 

node版本

作为tools.js的功能, 代码如下

const EventEmitter = require('events');
const eventEmitter = new EventEmitter();

  // 事件发射
  emitEvent(event, arg){
    eventEmitter.emit(event, arg)
  },

  //等待事件, 成功则返回args, 失败则返回null
  async waitEvent(event, timeout = 500){
    return new Promise((resolve, reject) => {
      let timer = setTimeout(() => {
        reject(timeout) // 如果用await waitEvent(),则抛出异常值为timeout
      }, timeout)
        // 监听消息事件
      const messageListener = (arg) => {
          clearTimeout(timer); // 清除定时器
          eventEmitter.removeListener(event, messageListener); // 移除监听器
          resolve(arg); // 解决Promise,返回数据
      };

      eventEmitter.on(event, messageListener); // 添加监听器
    })
  }

使用示例:

f = async ()=>{
  setTimeout(()=>tools.emitEvent('laugh',"hahaha"), 100)
try{
  let res = await tools.waitEvent('laugh', 500)
  console.log("res is", res)
  }catch(e){console.log('timeout', e)}
}
f() // 打印: res is hahaha

上一篇写了面向对象编程的几个原则, SOLID, 思想原则是很好的, 也就是无论一个模块功能怎么变, 都不能让别的模块非得跟着自己一起变. 为了实现这个效果, 要借助接口interface这个特定于一系列面向对象编程设计的语言, 如Java, C#, 并作为这些语言的基石. 其他一些语言如Go, Kotlin, Swift, Typescript, PHP也有interface, C++没有interface,但据说virtual/template可以实现类似功能(根据GPT4回答).
但总觉得interface这个思想是好的,实现起来很啰嗦. 这儿又看到一篇文章,里面推崇函数式编程,而嫌弃面向对象编程: 函数式编程入门经典
在知乎的这篇问答中, 其中有个回答翻译了Why OO Sucks的全文, 也值得一看.
总的来说, 函数式编程认为数据和函数必须分离,面向对象编程认为两者必须绑定. 从实现来说, 函数式编程往往比面向对象要简洁得多, 这一点我更喜欢.
事实上语言设计之初就是从某种思想出发的, 最主流的就是面向过程,面向对象和函数式编程, 问了GPT4o这个问题, 回答如下:

编程语言可以根据其设计理念和主要特性分为几类,包括面向对象编程(OOP)、面向过程编程(POP)和函数式编程(FP)。以下是对这些分类的详细说明:

问: 那些语言是面向对象编程的思想设计的,哪些是面向过程的思想设计的,哪些是函数式编程的思想设计的

面向对象编程语言 (OOP)

面向对象编程强调“对象”的概念,通过封装、继承和多态性来组织代码。常见的面向对象编程语言包括:

  • Java
  • C++
  • Python(同时支持多种范式,包括OOP)
  • C#
  • Ruby

面向过程编程语言 (POP)

面向过程编程关注于以过程或功能为中心,通过调用函数来执行任务。常见的面向过程编程语言包括:

  • C
  • Pascal
  • Fortran
  • BASIC

函数式编程语言 (FP)

函数式编程强调使用数学函数的方式,避免状态改变和可变数据。常见的函数式编程语言包括:

  • Haskell
  • Lisp (及其方言,如 Scheme)
  • Erlang
  • F#

其他混合范式语言

有些现代语言结合了多种编程范式,不局限于某一种。例如:

  • Python:支持面向对象、面向过程和部分函数式特性。
  • JavaScript:既可写成面向对象,也可实现函数式风格。
  • Scala:结合了面向对象与函数式特性。

这些分类帮助开发者根据项目需求选择合适的工具与方法来实现解决方案。

问: 除了OOP,POP,FP这几种设计理念, 还有其他设计理念吗,代表的编程语言有哪些

除了面向对象编程(OOP)、面向过程编程(POP)和函数式编程(FP)这三种主流设计理念外,还有其他几种重要的编程设计理念及其代表的编程语言:

1. 逻辑编程 (Logic Programming)

逻辑编程基于形式逻辑,通过声明“什么是”而非“如何做”,使用规则和事实来推理。代表的编程语言包括:

  • Prolog
  • Mercury

2. 声明式编程 (Declarative Programming)

声明式编程关注于描述要完成的任务,而非具体的控制流程。它强调“是什么”而不是“怎么做”。代表的语言包括:

  • SQL(结构化查询语言,主要用于数据库查询)
  • HTML(超文本标记语言,用于网页内容表示)

3. 并发/并行编程 (Concurrent/Parallel Programming)

并发/并行编程涉及多个计算过程同时运行,提升程序执行效率。代表的语言包括:

  • Go:具有内置的并发支持。
  • Erlang:以其强大的并发特性著称,广泛用于实时系统。

4. 反应式编程 (Reactive Programming)

反应式编程是一种异步数据流处理方式,可以自如应对时间变化或事件变化。代表的语言或框架包括:

  • RxJava:Java中的反应式扩展。
  • Scala:结合了函数式和反应式编程特性。

5. 数据流编程 (Dataflow Programming)

数据流编程将计算看作数据在节点之间传递的过程,更关注数据流动而非控制流。代表性的工具和环境,包括:

  • LabVIEW
  • Apache NiFi

6. 面向方面编程 (Aspect-Oriented Programming, AOP)

面向方面编程通过关注横切关注点,将其与业务逻辑分离,以减少重复代码和提高模块化。代表的语言包括:

  • AspectJ:是一种基于Java的面向方面扩展。

这些设计理念为解决特定问题提供了不同的方法和思路,开发者可以根据需求灵活选择合适的方法论进行开发。

简称SOLID

单一职责原则 (SRP):

每个类应该只有一个职责,避免承担过多功能。

开放-关闭原则 (OCP):

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
我的理解: 新增功能的时候, 是新增函数就能实现(即可以扩展), 而不是要修改已有函数才能实现, 因为修改已有函数, 会改变函数行为, 导致已经依赖此函数的其他模块的行为不可预测. 说到底,就是你改你的就别影响别人.

里氏替换原则 (LSP):

子类应该能够替换父类而不影响程序的正确性。
我的理解: 子类应保持父类函数的意图.

接口隔离原则 (ISP):

不应该强迫客户依赖他们不需要的接口。
我的理解: 因为接口内所有函数都是是必须实现的(C++?), 所以如果定义的一个接口里有太多要实现的函数, 客户(底层模块)又不需要, 就不胜其烦. 所以接口作为系统平台, 紧密关联的一组函数可以作为一个接口, 而相对独立的作为另一个接口, 客户可以选择实现哪个.

依赖倒置原则 (DIP):

高层模块不应依赖于低层模块,两者都应依赖于抽象(接口)。
我的理解: 接口相当于系统平台, 而模块无论是高层还是低层, 都相当于和平台打交道的人. 没有系统平台的情况下, 人和人直接打交道,无论是上下级(DIP原则说的是上下级)还是平级, 都可能是不规范的, 随时会变更自己交给别人的数据形态的, 这样一个人提交的数据变了另一个人即使没做错任何事都得跟着变, 就很讨厌. 通过系统平台, 就不会把变化溢出到其他人那里.
具体到实现, 就是无论高低层模块, 都与接口打交道, 低层实现接口, 高层调用接口, 而不是直接调用低层的类.

以前网站A在浏览器浏览过,重定向到了网站B, 后来需要删除网站B的域名, 直接用网站A, 却发现反复被定向到网站B. 而且我在chrome/edge/firefox都浏览过, 三个浏览器很一致得表现为重定向到B网站, 导致我一开始没有怀疑是浏览器端的问题. 然后就从服务器配置排查, 用宝塔把所有选项, 网站下所有文件, ssl, nginx所有配置全部排查了一遍, ssl重新申请, wordpress删除站点,重新部署....
最后忽然想到是不是可以用Chrome F12看看到底是怎么重定向的, 问了AI告诉我在网络里看有没有301, 结果发现提示信息:

请求网址:
https://xxxx.com/
请求方法:
GET
状态代码:
301 Moved Permanently (来自磁盘缓存)
远程地址:
xxxxxxxxxxxxx:443
引荐来源网址政策:
strict-origin-when-cross-origin

来自磁盘缓存!!! 果然... 让我白费好多功夫... 清空了以后就好了

Chrome和Edge访问aliyun和Ecount都出现连接拒绝, 用Firefox却没事. 昨天却都是OK的. 清空浏览记录还是不行. 把我安装的扩展看了一遍, 似乎只有ADP有这个嫌疑, 禁用后就正常了. 怀疑是这些网站后台共用的某个包升级了对adp的检测, 并做出了连接拒绝?

原以为很简单, 但其实很多坑.
首先Wordpress官方的迁移工具工具->导出/导入一点都不好用, 只是迁移了数据库, 无法迁移多媒体文件,插件,主题等等.
其次, 最方便的方式是通过插件迁移. 但插件多,坑也多. 搜索migration, 测试了其中三个插件:

  • 排名第一的updraftPlus的迁移功能要收费
  • 听起来最像的All-In-One WP Migration and Backup备份的文件有100多M, 但上传只能传50M, 并且要扩大上传容量需要再买一个插件. 用其他免费插件Big File Uploads扩展了上传大小, 但反复上传不成功
    最后Duplicator插件成功迁移! 步骤:
  1. 在源站点安装Duplicator插件, 点击右侧Create new, 然后Scan. 这一步可能报错,检查下报错内容,有时候是有其他插件引起的, 如果不用这个插件,删除再来一次.
  2. 目标站点安装好wordpress
  3. scan后会build一个installer.php和zip文件. 上传到wordpress网站的根目录下.
  4. 浏览器输入https://域名/installer.php,按提示完成迁移. 注意迁移后的管理员用户名密码也是和原站点相同的.

【符咒原理-哔哩哔哩】 https://b23.tv/03OxEjN

我的理解一生二、二生三、三生万物,这些数字是带有单位的。一是一维线段,二是线段的二个方向,三是线段的左中右三个端点,也可以视为一个中点+二个方向,此即一生二二生三。三生万物呢?就是一个维度(包括二个方向三个点)叠加上其他维度,生成了万物。此维度,既可以是几何空间的维度,也可以是时间维度、以及其他的任何维度。以最简单的两个维度相叠加,中点重合、相互垂直,共一个中点、四个方向,即为五。那这个五,其实就是直角坐标系,也可以是一个二维平面。五=直角坐标系=x轴+y轴+原点。两个轴代表不同维度,这个坐标系就可以表示各种概念,也就是我们在Excel和各种统计学中看到的折线图、柱形图、散点图……用于表示GDP增长、股票涨跌、企业竞争……维度从一维到再二维增加到更多维度,就是三生万物。
十和廿不太理解老师所讲,只能说下自己的理解。将一个二维概念——平面作为基础,则两平面相交为十,相交于一条直线。这似乎变成了三维概念,但我认为不是如此,因为三维概念应是七,即六方向+一原点,但是十=五+五,即两个独立系统有各自的维度,又相互关联,确实与父母两人相互独立又关联交合很像。此处“交合”应为广义的,不限于性的交合,也包括生活方方面面的交合,与此同时两人又有各自的独立性。扩展一些,可解读为任何人与人之间在生活上的共同交集。廿似乎能解读为相互平行的平面?套用在人这个维度上,也就是相互之间没有直接交集的人,而中间一横,代表了中介人,即人与人之间即使没有直接接触,也是通过其他人连接成一个整体。人与人如此,万事万物亦是如此。
关于咒,老师说咒是口号,我理解还有一个接近的概念:座右铭。我们都知道口号光喊是没用的,重要的是行动,座右铭也是如此。说到底,古人门上贴的咒语、现代墙上刷的口号、桌子上摆的座右铭,都不只是喊喊而已的,它们的作用是时刻激励自己行动起来去改变自己。然而,很多人、甚至多数人,对待它们都只是喊喊而已,甚至喊都不喊,摆在那里就期望生效,其实只是让咒语、口号、座右铭承担了自己无法行动的愧疚。这个时候,桌上的座右铭、墙上的口号就和现代人不明所以的咒、符一样,变成了单纯的精神寄托、甚至是迷信了。咒、口号、座右铭上所写,都是自己的心愿,是所想要达成的自己,如何达成?挂墙上就能达成吗?是念。念咒的念字,是心字底,如果只是嘴上说说而不行动,就是心口不一,就不叫念。心口一致,就是知行合一,长期坚持,就是定力,正是因为在自己在自己的心愿上长期坚持,知行合一,才会产生法力:量变到质变,达成心愿,产生奇迹。

希望用油猴做一些自动化的工具. 不用自动化测试软件的原因是特征都能被浏览器和页面检测出来, 所以这就存在一些风险. UiBot RPA虽然也可以, 没有检测痕迹, 但一是感觉软件太重了, 二是只能解决一些简单的Web上的问题, 很多时候复杂的问题如shadowRoot还需要配合js代码注入, 三是Uibot的代码编辑页实在有点难用,仿VB语言的算法也实在不太适应,尤其是不等号<>, 还有赋值和比较都是=不区分, 四是缺乏第三方库.
先推荐一个教程, 据说是中文唯一写的好的. https://github.com/scriptscat/tampermonkey-learn-guide
油猴官方可以说没有教程tutorial, 只有按字母序排列的API的说明. 虽然会JS上手也能写, 但只有明白额外的API才能进阶高级, 感觉有点像是electron, 不知道油猴是不是能实现类似electron的效果?

油猴名字

油猴这个翻译其实是grease monkey, 代码里简称的GM就是. 而tamper monkey是破坏猴的意思.

// @

最前面的注释部分, 以// @开头的其实都是给油猴看的代码, 类似于预定义, 是和油猴交互的关键. 其中最重要的一定要改的部分呢列出来.

//@命令 子命令 解释
//@name 名称 脚本名称
//@match 网址 匹配网址, 也就是在哪个网址运行. 支持通配符*
// @grant none 脚本直接注入window环境, 同时也不能使用GM_*命令了,所以一般不用
- unsafeWindow 直接使用页面中的任何脚本. 这就会打通页面和本地,理论上页面可以窃取本地内容并操控本地的一切. 脚本中的window需要替换为unsafeWindow
- GM_* 申请指定的GM函数权限.页面window环境需要用unsafeWindow访问.
- GM_xmlhttpRequest 允许跨域请求API
- GM_setValue/ GM_getValue/ GM_deleteValue/ GM_addValueChangeListener/ GM_removeValueChangeListener/ GM_listValues 设置/获取/删除/监听/停止监听/列出本地键值对
- GM_registerMenuCommand /GM_unregisterMenuCommand 在油猴按钮上注册菜单
- GM_addStyle 增加
- window.onurlchange/.close/.focus 这三个window权限需要grant声明才能用
// @run-at document-start 在document加载时就尽快调用脚本(比页面脚本更早)
context-menu 右键增加tempermonkey菜单,子菜单为本脚本名称, 点击运行
// @connect 网址 允许被GM_xmlhttpRequest请求的网址
// @require js文件URL#md5=xxxx 加载外部js资源文件并附带md5校验.引入油猴环境还是页面环境要看@grant定义. 也可以通过在html中插入script的方式来引用
// @resource css css文件URL 加载外部css

常用js代码部分

在html中增加元素

let div = document.createElement("div");
div.innerHTML = '<span>span1</span><span>span2</span>';
// 插入到页面的body中
document.body.append(div);

在html中增加按钮并监听

监听单个元素

let btn = document.createElement("button");
btn.innerHTML =
  "按钮文字,其实也可以写html,变成下面的样子(不过谁用按钮来包那么多html标签呢)";
//innerText也可以,区别是innerText不会解析html
btn.onclick = function () {
  alert("点击了按钮");
};
document.body.append(btn);

监听多个元素

let div = document.createElement("div");
div.innerHTML =
  '<span id="span-1">span1</span><span class="sp">span class</span>';
div.onclick = function (event) {
  if (event.target.id == "span-1") {
    alert("span-1被点击了");
  } else if (event.target.className == "sp") {
    alert("sp这一类被点了");
  }
};
document.body.append(div);

用事件监听器去减轻监听

div.addEventListener("click", function (ev) {
  console.log(ev);
});

在html中插入script

let script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.src = "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.min.js";
document.documentElement.appendChild(script);

在html中插入css

let script = document.createElement('link');
script.setAttribute('rel', 'stylesheet');
script.setAttribute('type', 'text/css');
script.href = "https://blog.icodef.com/wp-content/themes/Kratos-3.0.7/assets/css/kratos.min.css?ver=3.2.4";
document.documentElement.appendChild(script);

直接用GM_addStyle插入样式

GM_addStyle("#HMRichBox{display:none !important}");

在页面加载完毕后再执行

等所有元素加载完毕(包括图像等)

    window.onload = ()=>{
        alert('loaded')
    }

在油猴按钮上注册菜单 GM_registerMenuCommand

let id = GM_registerMenuCommand(
  "自定义的菜单",
  function () {
    alert("菜单被点击");
    GM_unregisterMenuCommand(id); //删除菜单
  },
  "h"  // 这个是accessKey, 快捷键
);

其他问题

如何对付嵌套iframe的页面

要看iframe打开的是哪个url, @match进去就好

如何劫持js页面函数

在window下面的函数都可以直接劫持. 如 window.setInterval = 自己的函数, 页面中自定义的函数也可以这么劫持, 如window.页面函数 = 自己的函数
几个常常劫持的函数, 除了setInterval函数劫持后可以改变时间帧间隔外, XMLHttpRequest函数劫持可以获取网络请求. 比如劫持XMLHttpRequest.prototype.send就可以修改发送的内容.
除了用XMLHttpRequest,页面还可能用fetch获取服务, 这时候需要对fetch劫持

let oldFetch = fetch;
function hookFetch(url, init) {
  //这里进行提交内容劫持
  return oldFetch.apply(this, arguments);
}
window.fetch = hookFetch;

对事件监听器劫持

const oldEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (...args) {
  console.log("addEventListener Hook", this, ...args);
  return oldEventListener.call(this, ...args);
};

其他可以劫持的还包括window.WebSocket
还可以劫持原型链上的函数. 如某个object.func()在其原型链上, 可以object.prototype.func = 自己的函数

如何在代码中打断点

在代码中使用debugger;命令,F12打开时就会停在这儿

传统上有很多, 但有的太老,有的太难. 比如Selenium. 微软新出的playwright学习曲线也比较陡峭.
谷歌的puppeteer, testcase似乎更容易些. 然而最近比较火的是cypress, 基于node, 据说非常容易上手.
其实,我觉得油猴是否也可以用来写测试, 说不定也挺好用.
油猴就相当于在F12里写js. 但cypress的好处是可以自动等待页面或元素的加载.
但是cypress会被https://bot.sannysoft.com/检查出正在使用WebDriver, 也就是可能被识别为爬虫. 浏览器自身也会识别出来. 如用edge做cypress的时候, 会提示正在被自动测试软件控制.
另外UiBot creator打开https://bot.sannysoft.com/也不会被检测出任何问题.