使用 Proxy 添加魔术属性/方法

使用 Proxy 实现魔术属性/方法。

最近在开发我厂的 QA 工具时,遇到一个问题。我需要模拟 Puppeteer 的所有方法,以便兼容原先的 JS 文件。Puppeteer 提供一个 .asElement() 方法,可以把函数执行结果转换成一个伪 DOM Element(如果函数返回的就是 DOM Element 的话),然后我们就可以在 Node.js 里调用原本属于 DOM 的方法,比如 .focus()。Pupputeer 会替我们完成映射和函数调用,并且返回结果。

对于大部分对象来说,我只要模拟对应的属性、方法,然后用自己的函数实现功能即可。但是 DOM Element 有上百个属性和方法,手工实现一遍实在太低效了。必须寻找其它途径。

好在我之前看过 Proxy 的介绍,赶紧翻出文档和书又复习两遍,就大概知道怎么做了。

Proxy 类如其名,可以“代理”对某个对象的访问。你可以把他理解成明星的经纪人。明星成名之前都是自己处理一切事务,有了经纪人之后,大部分事务就由经纪人负责,但仍然有一些事情需要明星自己处理。

Proxy 的用法很简单,实例化时,把要代理的对象传进去,定义一下代理方法就好。

const obj = { name: 'meathill' };
new Proxy(obj, {
  get(target, property) {
    // 如果对象中有要求的属性或方法,则返回
    if (property in target) {
      return target[property];
    }
    // 没有的话,进行其它处理
    return 'hello';
  }
});

obj.name // 'meathill'
obj.age // 'hello'
obj.sex // 'hello'

接下来,比如我们访问 obj.foo,那么代理就会生效,它会先检查 obj,如果这个对象上本来就有 foo 属性,就会返回;如果没有,则会调用我们定义的方法来处理。

如此一来,我们可以定义一个 VElement 类,这个类可以实现一些特殊方法,比如 .type(str) 输入,.click() 点击等;然后用 Proxy 代理其它方法和属性,让对象进入插件 Context 执行。


Proxy 还有其它方法也很有用,尤其是 get 对应的 set ,以后再介绍。大家可以自己抽空研究下。

参考

  • 阮一峰的 ES6
  • 《深入理解 ES6》

WebSocket.onerror 没有错误描述

用 WebSocket 时遇到一个问题:有时候连接出错,我希望把错误描述报告给用户,方便他们排除。但是尝试了好几种方法,都无法获得错误描述。

于是只有 Google 之,发现了这个答案:https://stackoverflow.com/questions/18803971/websocket-onerror-how-to-read-error-description。原来是为了防止开发者利用 WebSocket 搞破坏,扫描特定条件下的网络,WebSocket 的 ErrorEvent 只包含一个 error,没有更进一步的描述。oncloseCloseEvent.code也只有 1006——非正常退出,这样毫无价值的信息。 

所以我的处理方式是:建议用户按 F12 打开开发者工具看错误信息。

在 WebSocket 中处理二进制文本

我厂产品中有个需求,要用 WebSocket 接收很长的一段文本。生产环境中发现,有些内容发送时会失败,经查,是服务器端进行文本转换时,特殊字符处理存在一些问题。于是决定改为直接发送二进制流,我这边也要修改。开始以为要处理 ArrayBuffer,人工转换,后来经过一些摸索找到方法,记录一下。

我厂产品中有个需求,要用 WebSocket 接收很长的一段文本。生产环境中发现,有些内容发送时会失败,经查,是服务器端进行文本转换时,特殊字符处理存在一些问题。于是决定改为直接发送二进制流,我这边也要修改。

开始以为要处理 ArrayBuffer,人工转换,后来经过一些摸索找到方法,记录一下。

const socket = new WebSocket('wss://mydomain.com/path/to/api');
socket.binaryType = 'arraybuffer';
socket.onmessage = event => {
  const decodedString = new TextDecoder('utf-8').decode(event.data);
  // go on
}

其实很简单,主要用到原生类 TextDecoder,并且配置 WebSocket 以二进制文档流的方式来处理返回的数据。 

Promise 改造 child_process.exec

`util.promisify(child_process.exec)` 得到的函数无法获取 `exit code`,所以我重新封装了一下。

child_process 是 Node.js 的一个内建模块,用于分裂出(spawn)一个子进程,执行一些特定操作。.exec() 是它的方法,接受一个参数,即要执行的 shell 命令,然后通过回调返回结果。.exec().spawn() 的不同之处在于,前者重在返回结果,后者则重在返回内容。所以当你需要执行一个命令,你并不关心执行过程中发生了什么,只要看到结果就好,那么就用 .exec();反之,假如执行过程中产生的信息对你特别有价值,你并不是特别在意结果,就应该用 .spawn()

另外,我之前在《Node.js 8 中的 util.promisify》中介绍过,Node.js 8 引入了一个新函数,位于 util 模块,叫做 promisify(),用于将回调风格的 Node.js 函数改造成 Promise 规范的函数。

OK,背景知识介绍结束。近期开发中,我需要执行一个命令,并且取得它的 stdoutstderrexit code,使用 promisify() 之后发现没有 exit code,于是只好重新写了一下,代码如下:

import {exec as BaseExec} from 'util';

function exec(command, options) {
  return new Promise((resolve, reject) => {
    let result = {};
    const cp = baseExec(command, options, (err, stdout, stderr) => {
      if (err) {
        err.stdout = stdout;
        err.stderr = stderr;
        reject(err);
        return;
      }

      result.stdout = stdout;
      result.stderr = stderr;
      if ('code' in result) {
        resolve(result);
      }
    });

    cp.on('exit', (code, signal) => {
      result.code = code;
      result.signal = signal;
      if ('stdout' in result) {
        resolve(result);
      }
    });
  });
}

希望对大家有用。

新键盘到了,FC660C,静电容,试用一下,效果还不错。略硬,段落感不强,声音不大。

表单元素 disabled 的判定

判断一个表单元素是否为 disabled,应该使用 `.matches(‘:disabled’)`。

测试需求,判定表单元素是否 disabled。

看起来很简单,直接在浏览器控制台里用 $('input[type="text"]) 选中一个 <input>,验证一下它的 disabled 属性,没问题,于是就直接写成:

function isDisabled(element) {
  return element.disabled;
}

后来做到一个用户权限的功能,某一类用户,可以看到某些设置,但不能改。为了省事,就直接 禁用这部分表单。同时,因为我们用 <fieldset> 包装表单元素,于是我就把 disabled 加在 <fieldset> 上,这样可以不需要修改每个元素。

结果测试的时候就失败了,JS 认为这些元素不是 disabled,但视觉上和操作上它们的确被禁用了。于是调试,发现这些元素的 disabled 的确为 false,但是它们可以 .matches(':disabled'),Google 之,StackOverflow 的几个问答也是同样的结果。

于是将前面的函数改成

function isDisabled(element) {
  return element.matches(':disabled');
}

进一步的,我检查了其它几个属性,包括 readonlyrequired,发现 <fieldset> 只支持 disabled

记一个正则问题

`$` 和 `\b` 虽然并不能匹配到一个确定的字符,但它们同样意义重大;不特定长度匹配,包括 `*`,`+`,甚至 `{n, m}`,在懒惰模式下,后面都要尽量跟上明确的结束条件,以便让前面尽快结束。

前几天写代码,遇到一个需求:

  1. 解析 sleep NUMBER 这样的命令
  2. 能够识别缺参数或者参数错误的情况

这个正则并不复杂,初步写出来大概是这样:sleep\s+(.*)。这样,$1 就是参数,然后就可以检验。但是 .* 匹配“任意字符出现零次或多次”,所以实际测试发现它根本不匹配任何参数。

然后我就改成了 sleep(?:\s+(.*))?,然后在下一步 trim。这样,sleep 后面整个都是可选参数,就能解决上面的问题。

然后就被老板骂了……老板的答案是 sleep\s+(.*?)\s*$。重点在于后面的 $,要求正则必须匹配行尾,这样一来,懒惰模式的 .*? 就需要一直匹配到行尾,并且尽量少匹配内容,所以诸如 a b 之类的情况也可以正常跑匹配了。


从这里,我学到:

  1. $\b 虽然并不能匹配到一个确定的字符,但它们同样意义重大
  2. 不特定长度匹配,包括 *+,甚至 {n, m},在懒惰模式下,后面都要尽量跟上明确的结束条件,以便让前面尽快结束。

Puppeteer 笔记

记录使用 Puppeteer 的一些经验。

记录使用 Puppeteer 的一些经验。

安装使用

puppeteer 是一个“库”,没有自带的命令行功能。所以要使用的话必须写一个文件,然后实现对应的功能。

npm i puppeteer

在 WSL 下使用

因为 WSL 的环境比较特殊,直接使用会报错:

No usable sandbox! Update your kernel or see https://chromium.googlesource.com/chromium/src/+/master/docs/linux_suid_sandbox_development.md for more information on developing with the SUID sandbox. If you want to live dangerously and need an immediate workaround, you can try using –no-sandbox.

所以需要禁用 sandbox:

const browser = await puppeteer.launch({
  args: ['--no-sandbox', '--disable-setuid-sandbox'],
});

试用后抓图成功,汉字变成方框,可能跟我 WSL 里的字体配置有关,先不管了。

关于 sandbox,可以看这个文档,我其实也不太了解……

我的测试仓库和工具

参见 GitHub puppeteer-tool

Vue 小贴士

1. 使用 Vue + Webpack 开发
2. 使用 CDN 加载依赖
3. 在开发阶段尽量不要使用压缩的文件,一边取得尽量全面的错误信息

书说简短:

  1. 使用 Vue + Webpack 开发
  2. 使用 CDN 加载依赖
  3. 在开发阶段尽量不要使用压缩的文件,一边取得尽量全面的错误信息

继续阅读“Vue 小贴士”

Safari 下 Date 不支持”2018-01-01 00:00:00“

Chrome 可以支持非标准格式时间如 2018-01-01 00:00:00,safari 不支持,所以有时会导致错误。

前两天发现一个小程序的问题,Android 正常,iPhone 出错。我们都知道,Debug 的关键在定位,如果是某些特殊环节,不常见的错误,就会浪费很多时间。

这个 Bug 也是如此,反复拉锯之后终于发现,问题出在下面这句:

let a = new Date(`${date} 00:00:00`);

date 是服务器端返回的值,我是把它和后面的 00:00:00 连起来,记作某天零点零刻,和今天的零点零刻做减法,计算日期差,并按照日期差来决定接下来的逻辑。这段代码在开发工具(包括 Mac)、Android 手机上运行都正常,只有在 iPhone 上不正常,于是我打开 Safari——苹果这点做得不错,桌面版 Safari 环境和 iOS 几乎没有差别,该出的问题一定会出——果然复现了这个问题。

按照规范,中国的日期格式是:“2018/01/01”,Safari 只支持这个格式。而 2018-01-012018-01-01T00:00:00 ISO 格式,Safari 也支持,但是会以格林威治时间为准,和我们有8小时的时差。Chrome 和 Android 内嵌的 WebView(基于 Blink 或者 Webkit)则都支持,所以在本地和 Android 手机上没有问题。