cxxjackie 发表于 2022-7-10 11:47:04

异步获取元素的脚本库 ElementGetter

本帖最后由 cxxjackie 于 2023-5-14 20:39 编辑

### 这是一个供其他脚本使用的外部库,不应被直接安装,请使用以下地址在你的脚本中引用:

> // @require https://scriptcat.org/lib/513/2.0.0/ElementGetter.js

这是一个异步获取元素的脚本库。不少人写脚本都碰到过元素延迟加载的问题,使用定时器获取不仅实时性不足,还有性能问题,DOMNodeInserted的性能也不好,一般都推荐MutationObserver的方案。但是MutationObserver的语法较复杂,回调函数的写法也不易于使用,因此本库将相关代码加以封装,让元素获取一步到位,方便脚本的快速开发。

**注意**:自2.0.0版本开始,ElementGetter不再需要初始化,改为直接使用elmGetter对象。
<details>
<summary>如果你有兼容方面的困扰,请展开阅读下述兼容方案。</summary>
<pre>
// (旧)若使用了不同的实例名称:
const eg = new ElementGetter();
// (新)重命名即可:
const eg = elmGetter;
// (旧)若指定了jQuery选择器:
const elmGetter = new ElementGetter($);
// (新)改用新的selector方法:
elmGetter.selector($);
</pre>
</details>

elmGetter具有以下属性和方法:

## 属性

#### currentSelector

只读属性,一个表示当前选择器类型的字符串,可通过selector方法改变。

## 方法

#### get(selector[, parent][, timeout])

`selector` 必须, 选择器或选择器数组,**默认使用css选择器**。
`parent` 可选,父节点,默认值document。
`timeout` 可选,超时时间(毫秒),默认值0。
`返回值` Promise,selector为选择器时返回元素,为数组时返回元素数组。

根据选择器或选择器数组获取元素,若元素已存在则返回,否则监听父节点的元素插入,直至获取到元素或达到超时时间为止。返回类型为Promise,使用.then或async/await取得元素:

```js
// 回调写法
(function() {
    elmGetter.get('div').then(div => {
      console.log(div);
    });
})();
// 同步写法
(async function() {
    const div = await elmGetter.get('div');
    console.log(div);
})();
```

使用async/await时,若需要在同一父节点上同时获取多个元素,多次await可能造成性能问题,可以用Promise.all优化性能,库内部会尝试将多个监听器合并为一个。为简化这一流程,get方法的第一个参数允许是选择器数组,效果等同于Promise.all,参考以下示例:

```js
// 以下两种写法等价
const = await Promise.all([
elmGetter.get('.elm1'),
elmGetter.get('.elm2'),
elmGetter.get('.elm3')
]);
const = await elmGetter.get(['.elm1', '.elm2', '.elm3']);
```

***

#### each(selector[, parent], callback)

`selector` 必须,选择器,**默认使用css选择器**。
`parent` 可选,父节点,默认值document。
`callback` 必须,回调函数。
`返回值` 无。

为父节点设置监听,所有符合选择器的元素(包括页面已有的和新插入的)都将被传给回调函数处理,回调函数只在每个元素上触发一次。 回调函数接收2个参数,第一个是符合选择器的元素,第二个表明该元素是否为新插入的(已有为false,插入为true)。each方法适用于各种滚动加载的列表(如评论区),或者发生非刷新跳转的页面等,参考以下示例:

```js
// b站评论区自动展开回复
elmGetter.each('.reply-item', document, reply => {
    const btn = reply.querySelector('.view-more-btn');
    if (btn) btn.click();
});
```

令回调函数返回false即可移除监听,参考以下示例:

```js
const listener = elmGetter.each('div', document, (elm, isInserted) => {
    if (isInserted) {
      return false;
    }
});
```

***

#### selector(desc)

`desc` 可选,指定选择器类型,参考下述说明。
`返回值` 生效的选择器类型。
根据传入的参数更改get方法和each方法的选择器类型,可以被多次调用。参数desc存在以下几种情况:
传入jQuery引用:使用jQuery选择器。
传入"jquery"字符串(不区分大小写):在window和unsafeWindow中检索jQuery引用,若存在则使用jQuery选择器,否则使用css选择器。
传入"xpath"字符串(不区分大小写):使用XPath选择器。
其他情况或未调用selector方法:使用css选择器。

**关于jQuery选择器**
jQuery必须为页面原有或脚本引入,库本身不包含jQuery。
指定jQuery选择器时,get方法和each方法将返回jQuery节点。
尽管ElementGetter兼容低版本jQuery,但不同版本的jQuery对选择器的支持不尽相同,请自行测试或查询相关文档,以免得不到预期结果。
**关于XPath选择器**
库只获取元素,请勿匹配非元素节点。
以/或//开头时,根节点将基于document,这会导致父节点被忽略,要使其正确生效,选择器应以./或.//开头。
XPath选择器不能直接作用于shadowRoot,参考“**在特定环境中应用get和each方法**”。
XPath在性能上会不可避免地比css和jQuery慢,如非必要,不建议作为首选。

以下是一个使用XPath匹配特定文本内容元素的示例:

```js
elmGetter.selector('xpath');
elmGetter.get('.//*').then(el => {
    console.log(el);
});
```

***

#### create(domString[, returnList][, parent])

`domString` 必须,待解析的字符串。
`returnList` 可选,布尔值,是否返回以id作为索引的元素列表。
`parent` 可选,父节点,将创建的元素添加到父节点末尾处。
`返回值` 元素或对象,取决于returnList参数。

将html字符串解析为元素。注意,该方法只会返回一个元素,多个元素并列时返回第一个。示例:

```js
const div = elmGetter.create('<div class="mydiv">Hello world</div>', document.body);
```
若returnList为true,则在创建后的元素中查找其所有具有id的后代元素,并返回一个以id作为索引的对象,该元素本身以0为索引。示例:
```js
const list = elmGetter.create(`
    <div>
      <div id="div1"></div>
      <div>
            <div id="div2"></div>
      </div>
    </div>
`, true);
console.log(list, list.div1, list.div2);
```
***
#### 在特定环境中应用get和each方法
**同源iframe**:指定父节点为iframe.contentDocument即可。
**shadow DOM**:指定父节点为shadowRoot即可。若使用XPath选择器,则父节点不能为shadowRoot,但可以shadowRoot内的元素为目标,参考以下示例:
```js
const shadowRoot = document.querySelector('#shadow_dom').shadowRoot;
const div = shadowRoot.querySelector('div');
elmGetter.selector('xpath');
elmGetter.each('.//*', div, el => {
    console.log(el);
});
// 错误用法:
elmGetter.get('.//*', shadowRoot); // '#document-fragment' is not a valid context node type.
```


## 综合示例

以下综合示例展示了该库是如何工作的:

```js
// ==UserScript==
// @name         油猴中文网一键登录 - 示例脚本
// @namespace    ...
// @author       ...
// @version      2.0.0
// @match      https://bbs.tampermonkey.net.cn/*
// @require      https://scriptcat.org/lib/513/2.0.0/ElementGetter.js
// ==/UserScript==

(function() {
    'use strict';
    /* global elmGetter */
    elmGetter.each('', document, form => {
      const submit = form.querySelector('');
      const button = elmGetter.create(`
<button class="pn pnc" type="submit" tabindex="1" style="margin-left: 20px;">
    <strong>一键登录</strong>
</button>
      `, submit.parentNode);
      button.addEventListener('click', async e => {
            e.stopImmediatePropagation();
            // 这里也可以直接用querySelector,代码仅作为示例
            const = await elmGetter.get([
                '',
                '',
                ''
            ], form);
            username.value = '12345'; // 用户名
            password.value = '54321'; // 密码
            cookietime.checked = true;
            submit.click();
      }, true);
    });
})();
```

## 更新日志

**1.0.0**
初始版本
**1.1.0**
1.修复each方法的回调函数可能在相同元素上反复触发的问题,现在每个元素只会触发一次。
2.新增jQuery支持。
3.get方法和each方法允许单独省略parent参数。
**1.1.1**
1.修复上个版本的一点遗留问题(有句代码忘了删- -)。
2.each的回调函数现在可以通过return false移除监听(考虑改动remove机制,可能不向下兼容)。
**1.2.0**
1.废弃remove方法,改用return false的方式移除监听,each方法不再具有返回值。
2.废弃MutationEvent兼容。get方法和each方法现在额外监听属性变化,以使属性选择器的结果更准确。
3.each方法优化。
4.create方法新增parent参数。
5.精简了下代码,代码量减少约20%。
**1.2.1**
对私有属性的语法降级,以兼容低版本浏览器。
**2.0.0**
1.库不再需要实例化,不向下兼容。
2.新增selector方法,支持XPath选择器。
3.create方法新增returnList参数。

Ne-21 发表于 2022-7-10 13:04:52

正在用,感谢cxxjackie大佬{:4_110:}

李恒道 发表于 2022-7-10 13:35:51

之前一直想封
结果拖来拖去一直没搞...
终于cxxjackie大佬按捺不住了!
哈哈哈哈
其实还想看cxxjackie大佬写分析文章!
简单易懂还涨知识

李恒道 发表于 2022-7-10 13:36:14

也加入开发指南可以吗
哥哥
还有一个很呆的问题就是
为啥要加#

cxxjackie 发表于 2022-7-10 13:45:42

李恒道 发表于 2022-7-10 13:36
也加入开发指南可以吗
哥哥

可以吧,不过我感觉这个没什么技术性的分析,只是个工具。

cxxjackie 发表于 2022-7-10 13:51:34

李恒道 发表于 2022-7-10 13:36
也加入开发指南可以吗
哥哥
还有一个很呆的问题就是


#号是类的私有属性,一个比较新的特性(我也是尝试着用的哈哈)。

李恒道 发表于 2022-7-11 11:23:16

cxxjackie 发表于 2022-7-10 13:51
#号是类的私有属性,一个比较新的特性(我也是尝试着用的哈哈)。

c大!
碰到问题了
比如https://unpkg.com/element-ui/lib/theme-chalk/index.css
如果直接resource然后gm_addstyle会导致错乱
比如用link标签引入...
这种油猴有啥好的方案吗

cxxjackie 发表于 2022-7-11 12:20:27

李恒道 发表于 2022-7-11 11:23
c大!
碰到问题了
比如https://unpkg.com/element-ui/lib/theme-chalk/index.css


相对路径的问题,当成字符串处理一下就好了:
let css = GM_getResourceText('css');
css = css.replace(/(?<=url\()(?=fonts)/g, 'https://unpkg.com/element-ui@2.15.9/lib/theme-chalk/');
GM_addStyle(css);

李恒道 发表于 2022-7-11 14:51:47

cxxjackie 发表于 2022-7-11 12:20
相对路径的问题,当成字符串处理一下就好了:

这就...
很尴尬了
我以为是编码问题研究半天编码
结果是路径问题

wwwwwllllk 发表于 2022-7-11 18:34:45

为什么哥哥对原生的api这么熟悉
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 异步获取元素的脚本库 ElementGetter