最近开发一个 Node 的命令行程序,其中会用到 child_process.spawn 来创建子进程做一些事情。
本地跑通了测试之后,到了 Travis 的环境下发现子进程创建不成功,debug 到 CI 服务器上之后,发现报了下面这个错误:

spawn EACCES

查了一下这个错误,发现可能是因为创建子进程调用的文件权限不正确,由于我的项目中子进程的相关文件通过源代码编译生成,因此没有不在 git 的追踪中,也就无法直接控制文件权限设置。
于是就选择在 Travis 构建完项目,执行测试之前设置相关文件权限:

sudo chmod -R a+rwx folder/files

设置之后,子进程就可以正常创建了,这个问题比较容易被忽略。

最近在由于某个开发的移动平台的工作环境变成了 Firefox,在一次重新安装了 Ubuntu 系统之后,mp3 无法正常播放,提示错误大致如下

 Warning: Cannot play media. No decoders for requested formats: audio/mpeg

不管是通过是 <audio src></audio> 还是 new Audio() 的方式,都报出这个错误,最后总结出来三条解决方法:

  1. 安装 ubuntu-restricted-extras sudo apt-get install ubuntu-restricted-extras
  2. 安装 gnash sudo apt-get install gnash (一个 gnu 做的 flash 播放器,其中包含了浏览器插件)
    这条比较容易忽略,虽然我也不明白为什么 Firefox 调用解码器还和 flash 有关。
  3. 安装最新版的 Firefox,实测 59 版本就算满足了上面两条,还是无法正常播放,而 63 版本正常。

一直以来,MAC 下鼠标的功能按键(比如侧键)都不能正常工作,罗技自己的驱动对鼠标型号的支持都非常有限,比如我手中的 G500s 和 G502 就不能正常支持,尝试过很多方案,都不能完美解决,最近发现了 SensibleSideButtons 这个应用,完美的解决了这个问题。

根据作者的描述,他一直以来也被这个问题困扰,知道后来研究了 Logitech MX Master 的技术方案,最终采用模拟 Trackpad 三指手势的方式,完美的解决了这个问题。

具体可以看作者写的技术原理和实现 http://sensible-side-buttons.archagon.net/

1. Tray(托盘图片)消失

假如你把 tray 的实例变量定义在了函数内部(比如 app.on('ready', createWindow) 的回调函数内),那么当 JavaScript 引擎进行垃圾回收的时候,这个变量就会被销毁,这样 tray 的实例就没了,托盘图标就消失了。

2. 无法进行复制、粘贴之类的操作

由于默认没有绑定任何快捷键,当然操作不了,使用官方提供的下面这段代码,将各平台所有常见的菜单(快捷键)绑定好。

const template = [
  {
    label: 'Edit',
    submenu: [
      {role: 'undo'},
      {role: 'redo'},
      {type: 'separator'},
      {role: 'cut'},
      {role: 'copy'},
      {role: 'paste'},
      {role: 'pasteandmatchstyle'},
      {role: 'delete'},
      {role: 'selectall'}
    ]
  },
  {
    label: 'View',
    submenu: [
      {role: 'reload'},
      {role: 'forcereload'},
      {role: 'toggledevtools'},
      {type: 'separator'},
      {role: 'resetzoom'},
      {role: 'zoomin'},
      {role: 'zoomout'},
      {type: 'separator'},
      {role: 'togglefullscreen'}
    ]
  },
  {
    role: 'window',
    submenu: [
      {role: 'minimize'},
      {role: 'close'}
    ]
  },
  {
    role: 'help',
    submenu: [
      {
        label: 'Learn More',
        click () { require('electron').shell.openExternal('https://electron.atom.io') }
      }
    ]
  }
]

if (process.platform === 'darwin') {
  template.unshift({
    label: app.getName(),
    submenu: [
      {role: 'about'},
      {type: 'separator'},
      {role: 'services', submenu: []},
      {type: 'separator'},
      {role: 'hide'},
      {role: 'hideothers'},
      {role: 'unhide'},
      {type: 'separator'},
      {role: 'quit'}
    ]
  })

  // Edit menu
  template[1].submenu.push(
    {type: 'separator'},
    {
      label: 'Speech',
      submenu: [
        {role: 'startspeaking'},
        {role: 'stopspeaking'}
      ]
    }
  )

  // Window menu
  template[3].submenu = [
    {role: 'close'},
    {role: 'minimize'},
    {role: 'zoom'},
    {type: 'separator'},
    {role: 'front'}
  ]
}

最近处理一些用户录入数据时,发现有一些明明是纯数字的数据,却通过不了类型校验(无法解析成 Int/BigInt)类型,由于一直都是通过日志查看错误,觉得很疑惑,后来找到原始数据,发现因为这个『字符串』带了一个 zero width space,导致程序异常。

zero width space 在大多数文本输出的地方都是不可见的(顾名思义,没有宽度的空格),因此通过类似页面、日志排查都很难发现这个问题。

当把这个字符串手动复制粘贴到 Chrome 的控制台时,可以看到,这个 zero width space 表现成了一个·,这应该是 Chrome 为了方便调试设计的。

zero width space

而粘贴到 iTerm 终端,则可以直接看到 Unicode 值(但是当作为日志输出到终端时,是无法显示的)

zero width space in iTerm

这个字符一般来源都是用户从一些特定的平台复制而来,因此建议在类似的用户数据录入处进行一下相应处理,这里需要注意,一般语言的标准库的 trim 处理都是不会处理 zero width space 的,所以只能手动移除\u200B( 正则下)。

事实上,还有很多不可见字符,具体请参照这个回答:

https://stackoverflow.com/a/11305926/2530524

Control character in ASCII

引言

在一年多之前,我写了一篇文章《The way to Angular 2 | Part.1》介绍 Angular,最初打算持续完成一个系列,然后在我写完文章之后,Angular 和 TypeScript 都发生了相当大的变化,整个系列也搁浅了。 如今,我打算换一个角度,继续这个系列。

为什么要使用 Angular

angular.io_features

为什么要使用 TypeScript

www.typescriptlang.org_index.htm

- 阅读剩余部分 -

随机数与随机字符串

早前我都使用Math.random()来生成随机数,然后转成16进制字符串来生成随机字符串,后来看到了这个回答(stackoverflow)Math.random() 确实不是一个好的选择,因此我需要更加科学的方法。

window.crypto (Web Cryptography API)

Web Cryptography API 已经是 W3C Recommendation 级别,常见浏览器的近期版本也都已经实现了。 0ivS.png 使用的时候也可以不用担心兼容性问题。

crypto.getRandomValues

crypto.getRandomValues 是我们用来实现随机字符串的主要方法,这个方法从实现和随机性的角度来说,更加高效、可靠。 10.2.1. The getRandomValues method The getRandomValues method generates cryptographically random values. It must act as follows:

  1. If array is not of an integer type (i.e., Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array or UInt8ClampedArray), throw a TypeMismatchError and terminate the algorithm.
  2. If the byteLength of array is greater than 65536, throw a QuotaExceededError and terminate the algorithm.
  3. Overwrite all elements of array with cryptographically random values of the appropriate type.
  4. Return array.
Note Do not generate keys using the getRandomValues method. Use the generateKey method instead.

使用方法就是传入一个 Int 的数组,然后返回被加密之后的数组(覆盖原始数组)。

科学的随机字符串生成方法

知道了crypto.getRandomValues的使用方法之后,我们就可以来实现一个科学的随机字符串生成方法了。 首先毫无疑问我们需要一个 UintArray,这比我们手动生成随机数的数组要简单多了。

let len = 64;
const arr = new Uint8Array(len / 2);

假设我们要生成长度为 64 的随机字符串,len 就是 64,之所以在生成 Uint8Array 时候,要将长度除以2,是因为最终我们是通过16进制字符串的形式来输出的,并且在头部补0取末尾两位,所以只需要一半长度的 Uint8Array 就可以了。 用crypto.getRandomValues()加密这个数据

window.crypto.getRandomValues(arr);

把这个 Uint8Array 转换成普通数组,并且把每一个值转换成16进制字符串(首位补0),取末尾2位,最后拼接到一起,就是最终的随机字符串了。

// dec2hex :: Integer -> String
function dec2hex (dec) {
  return ('0' + dec.toString(16)).substr(-2);
}
    
// generateId :: Integer -> String
function generateId (len = 40) {
  const arr = new Uint8Array(len / 2);
  window.crypto.getRandomValues(arr);
  return Array.from(arr, dec2hex).join('');
}

使用这个方法来生成随机字符串,更加高效、科学、可靠。 gist: Generate random string/characters in JavaScript

问题

出于个人习惯,我在终端模拟器(iTerm2 等)中,都是将光标(cursor)设置为 vertical bar,而在 neovim 中则是将 normal 模式的光标设置为 block。

这样一来,当我从 neovim 的正常模式退出时,导致终端的光标样式也变成了 block,非常麻烦。主要是因为终端没有提供 API 查询当前的光标样式设置,因此 neovim 在退出时也不知道要如何重设光标。

解决

思路就是退出 neovim 的时候把光标重新设置为 vertical bar(ver25)。

if $TERM_PROGRAM =~ "iTerm"
  " reset cursor when vim exits
  au VimLeave * set guicursor=a:ver25-blinkon0
endif

其中ver25就是想要设置的样式,也可以是block等等,这样在退出 neovim 的时候就会重新将 iTerm2 的光标设置为 vertical bar。

鉴于 disqus 在国内访问越来越慢,资源和数据请求量越来越大,严重拖慢了页面加载,而且博客也只需要一个简单的评论对话系统,所以开始考虑进行评论系统的迁移。

替代方案 – hashover

hashover 满足了我对评论系统的期望,简单,数据本地化,可以轻松迁移。

hashover 的评论数据存储默认基于网站的 path,这就使得在迁移的过程中,只要网站的 path 没变,那么相应的评论数据就可以自动对应。

基于 PHP 也使得它在部署时非常方便,对于 WordPress 的运行环境来说是无缝的。

我选择把 hashover 的数据都直接保存在本地,这样如果以后我有迁移的需求,直接将 hashover 打包传输到新的地方,就可以完成迁移工作。

disqus to hashover

原先的评论系统是 disqus,所以需要转换到 hashover 的数据存储,在网上找到一个原始的转换器 https://github.com/ianrenton/disqus-to-hashover ,但是它只支持 hashover 1.0 的版本,而我直接使用了 next(2.0) 的版本,所以 fork 了一份,并做了一些改动,使它适配了 2.0 的版本,并且完善了 gravatar 头像的数据支持。

https://github.com/gucheen/disqus-to-hashover-next

问题

起因是这样的,一开始在项目中写了一个原始的 Popup 组件(通过 react-redux 的 connect 方法接入 redux),后来有了一个更高级的需求,我就想要直接 extends 这个原始的 Popup 组件,然后覆写一些方法。 然后遇到了一个问题,继承之后的子组件没有父组件的自定义方法。

解疑

最初一度怀疑是否是 babel 的 decorator 插件有问题(毕竟不是浏览器天然支持的特性),但是仔细看了 decorator 插件的源码,发现并不存在这种问题。 后来在这个插件的 issues 里看到了这么一个 issue,问题并不算是同一个,但是作者提到,react-redux 的 connect 方法可能实现有一些问题(当然这个地方并不是),我就想到去看看 connect 的实现。 然后就看到了底层方法 connectAdvanced 中有这么一句代码:

export default connectAdvanced() {
    // ...

    return hoistStatics(Connect, WrappedComponent);
}

hoistStatics 是一个第三方库 hoist-non-react-statics,它干的事情也比较简单:

Copies non-react specific statics from a child component to a parent component. Similar to Object.assign, but with React static keywords blacklisted from being overridden.

简单来说,就是复制所有非 React 自有的属性,因为使用了 Object.getOwnPropertyNames()的方法,所以我们定义在 Component 上的所有方法就不会被复制到这个新的对象上了。 而在 connectAdvanced 内部,render 时实际是这样的:

render() {
  const selector = this.selector
  selector.shouldComponentUpdate = false

  if (selector.error) {
      throw selector.error
  } else {
      return createElement(WrappedComponent, this.addExtraProps(selector.props))
  }
}

通过 createElement 方法给原始的 WrappedComponent 传入额外的 props,来实现 connect 的效果。 总之,最终 connect 方法返回这个对象,其实和原始的 Component 的结构并不一样,虽然它的 WrappedComponent 属性指向了原始的 Component,但是显然在我们的场景下,并不能使用。

那怎么办呢

通常的推荐解决方式就是『高阶组件』,这样就屏蔽了 connect 的逻辑。

为什么要用 hoist-non-react-statics 这种模式

关于这个问题,作者在 这一个 issue 的回答中 也说了很多,总结来说,就是为了避免使用者可以直接的接触到 connect 之后的组件的内部方法,防止因为各种改动导致组件的 "break",这也算是为了提升健壮性。