Things I Learned (2020-05)

querySelector with escape🔗

• JavaScript

通过 ID 选择 DOM 上的节点时,一般常用的 API 是 document.getElementById 或者更普适的 document.querySelector。

问题

其中,对于后者(document.querySelector)来说,需要额外考虑一些特殊 ID 的处理。

举例来说,如果一个 ID 是以数字开头的,那么可以通过 document.getElementById 获取到,但是使用 document.querySelector 却会出现报错:

const div = document.createElement('div');
div.id = '1';
document.body.appendChild(div);

document.getElementById('1'); // => div
document.querySelector('#1'); // => Error: '#1' is not a valid selector.

同理,如果 ID 带有 : 字符,也会出现上述的问题。

这是因为,这样的命名方式,并不是合法的 CSS Selector。换句话说,不仅仅是 JavaScript API 无法生效,CSS 也无法直接生效。: 的情况很好理解,ID 中存在这样的字符,就无法很好的和 Pseudo Class 进行区分了(比如 :before)。

解决方案

要处理这样的情况,最简单的方案是使用 document.getElementById。但是,在一些场景下,可能需要生成 CSS Selector 来标记元素,方便统一的存储和使用(比如 unique-selector 这样的使用场景)。此时,可以考虑如下的几种方案去绕过这一限制:

  1. 用 \ 的方式处理 : 字符(同 CSS 的处理方案),比如 a:b 变为 #a\:b(当然,在 JavaScript 实际使用的时候,需要注意字符串内 \ 本身的 Escape,最终的结果:document.querySelector('#a\\:b'));
  2. 使用 Unicode 来 Escape,比如将 : 改写成 \3A,对于 JavaScript 来说可能需要一次额外的 Escape,最终写法是 document.querySelector('#\\3A')
  3. 使用 [id=""] 的方案进行 ID 的选取,如 document.querySelector('[id="a:b"]')。因为属性选择器本身是在寻找字符串,因此其中就算有 : 这样的特殊字符,也不会有问题;

参考

针对 CSS Selector 的 Escape 方案(第一种和第二种),可以参考这篇文章,其中还详细列举了 CSS 中的特殊字符。

unique-selector 使用了第三种方案,具体代码可以参考这里。


Trust Event🔗

• JavaScript

在 JavaScript 中,除了用户操作触发事件外,也可以通过 Element.dispatchEvent 去触发一个事件。举例来说,对于点击事件,除了用户触发外,也可以通过下面的代码触发:

const button = document.createElement('button');
button.addEventListener('click', (event) => {
  console.log('clicked: ', event);
});
button.textContent = 'Click Me';
document.body.appendChild(button);

button.dispatchEvent(new MouseEvent('click'));

其中,点击事件更为特殊一些,还可以通过 Element 上本身自带的 .click 方法触发:

button.click();

那么,如何可以从代码层面区分,一次事件到底是用户触发的,还是程序触发的呢?

JavaScript 的 Event 中提供了一个 isTrusted 只读属性,用于标记当前的属性是否是由用户触发的。如果是,那么 isTrusted 就是 true,否则都是 false。这里无论是 dispatchEvent 还是直接调用 .click 方法,结果都是一样的(false)。

同时,因为 isTrusted 属性是只读属性,因此想要进行修改也是不会成功的:

const event = new MouseEvent('click');
console.log(event.isTrusted); // => false
event.isTrusted = true;
console.log(event.isTrusted); // => false;

而如果试图通过 Proxy 去包装 Event,最终虽然可以拿到 isTrusted=true,但是却无法被 dispatchEvent 认可:

const event = new MouseEvent('click');
const proxy = new Proxy(event, {
  get(target, prop, receiver) {
    if (prop === 'isTrusted') return true;
    return Reflect.get(target, prop);
  },
});
console.log(proxy.isTrusted); // => true
console.log(proxy instanceof MouseEvent) // => true
console.log(proxy.constructor) // => MouseEvent

// Error: Failed to execute 'dispatchEvent' on 'EventTarget':
//        parameter 1 is not of type 'Event'.
document.body.dispatchEvent(proxy);

// Success
document.body.dispatchEvent(event);

Screenshot of Touchbar🔗

• MacOS

在新版的 Mac 电脑中,内置了一个 Touchbar 来实现触摸操作,不少软件都在 Touchbar 上添加了一些按钮,方便用户快捷操作。如果想要截取当前 Mac 电脑上 Touchbar 都有哪些内容,可以使用下面的快捷键:

Command-Shift-6

这里,截取的快照会被存放在本地目录下(默认是保存在桌面,如果需要修改存放的位置,可以参考之前的笔记)

类似的,使用 Command-Control-Shift-6 可以将截图直接保存到剪贴板中。


How to Remove Git Merge🔗

• Git

一些 Git 项目的操作规范,会禁止 Git Merge 的提交,而只允许使用 Git Rebase。以下列举一些可以去除 Git Merge 节点的操作思路。

Merge 节点在顶部

如果是最近的一个提交不小心产生了 Merge 节点,有几个方案可以参考:

  1. 可以撤销了重新用 Git Rebase 再来一次,不过坏处是可能需要重新处理一遍冲突(视情况可能会有很多次冲突处理要执行);
  2. 或者可以考虑先 git reset --soft <commit_id> 然后再重新提交,直接将所有新的改动压缩成一个 commit,不过缺点是历史 commit 信息都丢失了。

Merge 节点在中间

如果因为一些操作原因,在历史 Commit 中生成了 Merge 节点,可以考虑如下的方案去除。

首先,假设处理之前的 Git 分支如下:

A -> B -> C -> D -> M -> G -> HEAD
       \-> E -> F -/

那么,可以通过如下的命令去除 Merge 节点 M:

git rebase F HEAD

这里的 F 节点是分支在 Merge 前的最后一个节点,而 Head 就是当前的最新节点。Rebase 完成之后,新的 Commit 顺序应该如下:

A -> B -> E -> F -> C -> D -> G -> HEAD

其中,A -> B -> E -> F 就是 Git Rebase 命令中到 F 点为止的整条路径,而后续的 C -> D -> G -> HEAD 就是剩下在这条路径上不存在的新节点。

可以通过如下的命令创建一个环境来手动试一试:

git init;
echo "A" > file.txt && git add -A && git commit -m "A";
echo "B" > file.txt && git add -A && git commit -m "B";
git checkout -b "new-branch";
echo "E" > file.txt && git add -A && git commit -m "E";
echo "F" > file.txt && git add -A && git commit -m "F";
git checkout -;
echo "C" > file.txt && git add -A && git commit -m "C";
echo "D" > file.txt && git add -A && git commit -m "D";
git merge new-branch;
echo "E" > file.txt && git add -A && git commit --no-edit;
echo "G" > file.txt && git add -A && git commit -m "G";

此时,只需要使用命令 git rebase new-branch HEAD 即可以去除 Merge 节点(当然,需要手动处理一些 Merge Conflict)。