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 手机上没有问题。

解决 [Vue warn]: You may have an infinite update loop in a component render function

在 `v-for` 循环当中,如果用方法或者计算属性对 vm.$data 的属性进行操作,理论上,可能因为修改到循环对象,诱发无限循环。此时 Vue 就会发出警告(并不是真的已经无限循环了)。

今天写着写着,突然发现控制台里有错误:

[Vue warn]: You may have an infinite update loop in a component render function

这个问题很奇怪,之前从来没有遇到过。如果是我自己主导的项目,倒也好办,慢慢 debug 就是;偏偏在公司的项目里遇到这个问题,而公司项目的体系结构很复杂,我还没完全掌握。更恼火的是,因为体系复杂,debug 也非常困难,再加上尚无测试框架,这个难搞啊……

好死不死的,当时是下午3、4点钟,正好到了肚饿的时刻,结果又落入低血糖状态,真是屋漏偏逢连阴雨,船小又碰顶头风,饿得我脑仁生疼……

不过终于还是被我 Google + debug 出来。事实上是这样的,在 v-for 循环当中,如果用方法或者计算属性对 vm.$data 的属性进行操作,理论上,可能因为修改到循环对象,诱发无限循环。此时 Vue 就会发出警告(并不是真的已经无限循环了)。

例如这样一个组件,它里面是用 :checked + <label> 实现的一组按钮。它有以下功能:

  1. 为了能够分组,需要设置它们的 name 属性
  2. 为了能够用 <label> 控制 <input>,需要给 <input> 设置 id
  3. 按钮可以被删除

于是我选择这样做:

<template>
<div>
  <template v-for="(item, index) in items">
    <input type="checkbox" :name="'my-component-' + selfIndex" :id="getID">
    <label :for="getID(false)">
    <button type="button" @click="remove(index)">&times;</button>
  </template>
</div>
</template>

<script>
let count = 0;

export default {
  data() {
    return {
      selfIndex: 0,
      itemIndex: 0,
    }
  },
  methods: {
    getID(increase = true) { // 注意,问题就出在这里
      if (increase) {
        this.itemIndex++;
      }
      return `my-component-${this.selfIndex}-${this.itemIndex}`;
    },
  },
  beforeMount() {
    this.selfIndex = count;
    count++;
  }
}
</script>

这里,为了能生成唯一 ID,我选择每次循环都对 vm.itemIndex++,这就会出现前面说的问题,存在隐患。

解决的方案有两种,一种是把 itemIndex 也放在局部变量里,使它不直接关联在组件上;另一种则是写一个全局的唯一 ID 生成函数,然后引用进来。原理都是一样的。


这两天听评书《乱世枭雄》,学到一句话“拉屎脸朝外”,形容讲义气,不知道咋联系的……

MediaElement 笔记

贵司官网需要放视频,于是需要用播放器,最后选定 MediaElement。然后就开始踩坑之旅。当然,有些是我的问题,不过也有不少是文档没说清楚。这里记录一下。

贵司官网需要放视频,于是需要用播放器。然后就产生如下需求:

  1. 协议好,支持免费商用,最好 MIT
  2. 支持 SRT 字幕
  3. 支持尽可能多的平台

经过筛选,最后选定 MediaElement,一看 WordPress 在用基本就放心了。然后就开始踩坑之旅。当然,有些是我的问题,不过也有不少是文档没说清楚。这里记录一下。


全屏和弹性宽度

当前版本 4.2.5 有个 Bug,如果设置宽度为 100%,那么从全屏恢复到内嵌状态时,会留着全屏的宽度,撑开页面。如果设定一个特定的宽度就不会。但我们的页面是响应式的,需要允许它随页面变化而变化。

这个时候只好手工来做,我的选择是让它 100%,然后侦听从全屏返回的事件,发生后就把宽度恢复。另外,因为 fullscreenchange 事件还没有统一标准,所以不同浏览器里的事件名称也不一样,更不爽的是,IE 下是驼峰,其它浏览器是全小写,我也懒得再写函数转化了,反正就3种情况,全写一遍好了。

// fix MediaElement's issue
  let fitVideo = function() {
    setTimeout(() => {
    $('.mejs__container').width('100%')
      .find('video').width('100%')
      .end().find('.mejs__layers')
      .children().width('100%');
  }, 50);
};
if ('onwebkitfullscreenchange' in document) {
  document.onwebkitfullscreenchange = function() {
    if (!document.webkitFullscreenElement) {
       fitVideo();
    }
  };
} else if ('onmozfullscreenchange' in document) {
  document.onmozfullscreenchange = function() {
    if (!document.mozFullScreenElement) {
      fitVideo();
    }
  };
} else if ('MSFullscreenChange' in document) {
  document.MSFullscreenChange = function() {
    if (!document.msFullscreenElement) {
      fitVideo();
    }
  };
}

默认字幕

Web 标准是有默认字幕的,<track defualt> 即可。但是 MediaElement 没有,最后在 StackOverflow 上找到答案,原来有个未记录在文档里的初始化属性可以控制:

let player = new MediaElementPlayer('intro-video', {
  startLanguage: 'cn', // 这里的名字应该和 <track> 种的 srclang 属性一致
});

插件与生态

MediaElement 把一些有用但不太常用的功能抽出来做成了插件,放在 MediaElement Plugins。不过看起来这个项目有些属于维护,使用的时候很多问题。比如,直接在 features 里开启特定功能,即使对应的插件没有加载,也不会报错,就是没反应,很令人迷茫。

避免发起又取消请求

网站上线后,老板发现一个问题:从 Chrome 的 Network 面板可以看到,网页打开后,对视频文件发起了一到多个不等的请求,并且都取消了。

经过研究,我认为这个请求是 <video> 发起的,因为 preload 的默认值是 auto(参见:MDN),也即打开页面就预加载。而此时 MediaElement 被初始化,为了正确显示 UI,它会把 <video> 标签挪到自己创建的 <div> 里,这个过程就会导致“预加载 -> 取消”。因为视频默认不播放,所以我给 <video> 加上了 preload="none",问题解决。


其它大体上文档都能找到,就不多说了。