Things I Learned (2019-08)

Conditional Props in React🔗

• TypeScript

在 React 中,经常会有这样的场景:通过某一个参数是否是真值,来决定某一个元素是否需要显示出来。

以 Ant Design 为例,Tooltip 的定义中,就包含了 title 这个参数,用于决定是否显示 Tooltip 及显示什么。如果传递的是 false,null 或者 undefined,那么最终 Tooltip 就不会被显示出来。

常用的调用形式可能如下:

<Tooltip title={!this.state.hide && 'text'} />

在最初 Ant Design 对此的定义上,使用了如下的 TypeScript 类型定义:

interface Props {
  // ...
  title?: React.ReactNode | RenderFunction;
  // ...
}

这里,title 的定义用到了“可选参数”。看上去,是符合预期的行为,然而这里有几个细节值得注意:

  1. React.ReactNode 的定义是:
type ReactNode =
  ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

可以看到,即使不是可选参数,undefined 等一系列值也是可以赋予给 title 的;

  1. title?: string 和 title: string | undefined 之间存在着细微的差别。

这里展开对比一下 title?: string 和 title: string | undefined 之间的细微差别。如果定义的类型是 title?: string,那么,以下的调用方式都是正确的:

  1. 传递字符串作为参数:
<Tooltip title="string" />
  1. 传递 undefined 作为参数:
<Tooltip title={undefined} />
  1. 不传递参数:
<Tooltip />

而如果是 title: string | undefined,那么上面的第三种方案(即不传参数)就是不可行的。

还是以 Tooltip 为例,显然前两种调用方法都是真实存在的场景,毕竟 Tooltip 可能是需要根据外部条件来选择性展示的;但是对于第三种场景,即不提供 title 数据、一直保持不渲染 Tooltip 的状态,可以认为是有错误的,应该由 TypeScript 进行检查并报错。

故,改成以下这种形式就可以了,毕竟 React.ReactNode 就允许了 undefined 的使用:

interface Props {
  // ...
  title: React.ReactNode | RenderFunction;
  // ...
}

Ant Design 对这种情况进行了修正。


Cypress No Internet🔗

• Cypress

在 Cypress 开发的过程中,因为其他操作导致内存吃紧,最终 Cypress 被操作系统杀掉。之后,再重启 Cypress,发现一直运行失败,所有 Test 全部都无法执行成功。

通过 cypress open 来打开 UI 并执行任意测试用例,发现浏览器直接返回 No Internet。浏览器给出的建议是:

  1. 检查系统是否联网
  2. 检查是否有 Proxy 配置

电脑本身可以正常上网,也没有手动进行任何 Proxy 配置,浏览器给出的建议并不能真正解决问题。

通过 ps -ef | grep Cypress 后发现,即使在 Cypress 没有运行的情况下,依然有运行中的进程:

执行 kill 命令杀死这些个进程。再重试 Cypress 就可以正常运作了。


Get Element by Content🔗

• Cypress

在 Cypress 中可以通过字符串来查找和定位元素,常用的命令包括 get,find 等。这里以 get 命令为例,在其文档可以看到一些用法的说明。

JavaScript 中支持的 selector 在 get 中都是可以使用的,除此之外,文档指出所有 jQuery 支持的也同样支持。(事实上,在 Cypress 注册命令的代码处可以,可以找到和 DOM 相关的代码,这部分代码中不难发现 jQuery 的影子)。

有了 jQuery 的强力支持,就可以写出复杂的选择条件。比如,选取含有某一文案的 HTML 组件。

在 jQuery 中,提供了 :contains 这个选择器(文档),可以找出所有包含某一指定字符串的所有元素。

于是,想要找出弹出层中的 Submit 按钮,就可以这么写:

Cypress.get('[role=dialog] button:contains("Submit")')

这里,使用了 [role=dialog] 来找到弹出层(dialog 相关的介绍可以看 MDN),然后再通过 button 找到按钮,最后用 :contains("Submit") 来找到 Submit 按钮。

当然,如果一个产品本身支持 i18n,那么 :contains 后面的部分就不好写了。一个可行的方案,是通过当前页面的语言,从一组文案中找到合适的文案,再调用 :contains 选择器。比如,写一个简单的 Cypress 命令,如下:

Cypress.Commands.add('getByText', (query, texts) =>
  cy.get('html').first().then(html => {
    const { lang } = html;
    return cy.get(`${query}:contains('${texts[lang]}')`);
  })
);

这里,通过 HTML 上的 lang 标记来确定当前页面所选用的语言(lang 的一些细节可以参考 MDN),然后再根据语言,从一组文案(即 texts 这个对象)中选取当前需要使用的文案。

命令的使用方法:

cy.getByText('[role=dialog] button', { en: 'Submit', zh: '提交' })
  .first()
  .click();

Get Yesterday Date in Bash🔗

• Bash

在大多数时候,Mac 系统和 Linux 系统在终端的使用体验上是比较一致的,但偶尔也有一些命令,会出现两端不一样的情况。比如,当需要通过 date 命令获取昨天的日期。

在 Mac 中,可以通过如下的命令来完成:

date -v '-1d' '+%Y-%m-%d'

输出的结果是 2019-08-25。(这里,'+%Y-%m-%d' 指定 date 以“年-月-日”的格式输出日期;另外,如果想要得到明天的日期,可以通过 +1 day 或 +1d 来得到)

然而,在 Linux 系统下,同样的命令无法使用。需要修改成:

date -d '-1 day' '+%Y-%m-%d'

才可以得到同样的结果。这里需要注意一点,如果 Docker 是基于 Alpine 的,默认 date 不支持 -d 这个选项,需要额外安装 coreutils 之后,才可以使用。即,在 Dockerfile 中增加:

RUN apk add --update coreutils && rm -rf /var/cache/apk/*

之后,上面的命令才能正确运行。

如果希望一个命令可以在两个系统中运行,可以用如下的方法进行整合:

[[ $OSTYPE == "darwin"* ]] && \
  date -v '-1d' '+%Y-%m-%d' || \
  date -d '-1 day' '+%Y-%m-%d'

注意,这里需要使用 [[ 进行判断,[ 的比较是无法使用 * 元字符匹配的。当然,这里没有考虑 Windows 的情况,毕竟 Windows 的情况太特殊了,大部分的命令都不兼容。


Cron in Docker🔗

• Docker

用 Docker 管理定时任务,依然可以通过 Cron 来进行。

Cron 配置

可以通过以下的方式测试 Docker 中 Cron 的执行:

docker run -ti --rm alpine sh -c \
  "echo \"* * * * * echo hello\" | crontab - && crond -f -L /dev/stdout"

运用同样的原理,可以写一个简单的 Dockerfile 如下:

FROM alpine:latest

RUN touch /var/log/cron.log

CMD echo "* * * * * echo 'Hello World' >> /var/log/cron.log 2>&1" | \
  crontab - && \
  crond -f -L /dev/stdout

在上面的例子中,每一分钟都会输出一个 Hello World 到 /var/log/cron.log 文件中。Cron 的时间书写,可以在这里 进行直观的配置。

时区配置

如果要进行更加复杂的 Cron 配置,很可能就会涉及到时间的问题。默认情况下,Docker 使用的 Linux Alpine 使用的是 UTC 时间。如果配置的 Cron 也需要用 UTC 来书写,显然不直观,也不方便。

简单的处理方法,就是在创建 Docker 的时候,对时区进行配置。比如,将时区设置成北京时间:

FROM alpine:latest

ARG timezone="Asia/Beijing"

RUN apk add tzdata && \
  cp /usr/share/zoneinfo/$timezone /etc/localtime && \
  echo "$timezone" > /etc/timezone && \
  apk del tzdata

RUN touch /var/log/cron.log

CMD echo "30 8 * * * echo 'Wake Up!' >> /var/log/cron.log 2>&1" | \
  crontab - && \
  crond -f -L /dev/stdout

如上,通过 tzdata 来配置时区,将时间调整成北京时间,进而再执行每日 8:30 的 cronjob,以确保在北京时间的早晨执行某一个命令。


Remove Docker Images without Tag🔗

• Docker

在 Docker 开发的过程中,可能会产生很多无用的临时 Docker Image。这些 Image 很可能没有 Tag,在 docker image ls 显示的时候,Tag 一列显示为 <none>。

要批量清除这些 Image,可以使用如下的命令:

docker rm $(docker images -f "dangling=true" -q --no-trunc);

如果需要强制删除,可以将 rm 改为 rmi。

这里,docker images -f "dangling-true" 命令会列出所有没有 Tag 标记的 Image,加上 -q 标记后,将会只列出每个 Image 的 ID,--no-trunc 保证显示的是完整的 ID 而不仅仅只是 ID 的前几位。

再配合 docker rm 就可以将这批 Image 全部一次性删除了。


Seeding Search in VSCode🔗

• VSCode

在 VSCode 中,搜索的时候,默认会将选中的文字填充到搜索框中。这是一个见仁见智的功能,有时候并不非常好用。

可以通过一下方式取消这一行为:

  1. 打开 Preferences > Settings
  2. 搜索 Seed Search String From Selection
  3. 取消勾选

Inspect Element after MouseEnter🔗

• Chrome

在前端组件中,有不少组件对鼠标的响应并不是通过 CSS 的 hover 来触发的,而是通过 JavaScript 监听对应的鼠标事件,然后再进一步修改 DOM 的结构。比如,Ant Design 中的 Popover 控件,在鼠标移上去后,会在 DOM 中插入一组元素,并在鼠标移开后删除。

在这种情况下,一旦出现样式上的问题,就不容易在 DevTool 中对样式进行查看了。因为只要一点击右键审查元素,Popover 的内容很可能就会因为触发了鼠标事件而消失不见。

对于这种情况,没法直接用 DevTool 中的 CSS 模拟来强制样式显示。如果需要通过触发事件来触发 DOM 的修改机制(不论是 dispatchEvent 还是在 React Extension 中触发回调),总体上是比较麻烦的。因为组件的层级结构很可能很复杂,知道应该往哪儿触发什么事件,也不是个容易的事情。

既然从程序的角度触发比较复杂,不如换个思路,考虑从行为的角度来触发。比如,如果是通过鼠标悬停触发的样式修改,那么就直接通过这种行为来触发。唯一的问题是:应该如何保持这种样式,不在鼠标离开的时候被重制(否则就没法在 DevTool 里进行查看了)。

这种时候,有一个简单的方法可以“暂停”浏览器。在 Console 中输入:

setTimeout(() => { debugger; }, 3000);

就会在三秒后触发 debugger,从而暂停 JavaScript 的执行。这时候,鼠标离开的事件不会得到响应,也就可以安心在 DevTool 中对样式进行仔细的查看和调整了。

当然,这里触发 debugger 的方式可以根据实际情况来写。只要保证在 DOM 改变后触发 debugger 就可以了。


Overflow & InlineBlock🔗

• CSS

当 display: inline-block 和 overflow: hidden 一起使用的时候,会发现文字的显示比一般正常的情况要“高”一些。举个例子来说:

good

上面的四个文字中,第一个 o 被设置为 display:inline-block 以及 overflow:hidden。最终的显示效果,第一个 o 的底部明显高于两边的 g 和 o。

通过给第一个 o 和整行文字画上边框,不难发现,这个文字是整体被抬高了。

good

在上面的例子中,inline-block 的高度是由 line-height 决定的,因而看上去会比 inline 情况时候的要高(inline 情况下 border 画出来的高度是固定的,由 font-family 和 font-size 决定);同时,overflow:hidden 会让内容的底部和父元素的文字基线(baseline)持平,从而会让整体的显示结果更高(这一点从上面的显示中不难发现,其中 g 的部分有少量是低于基线显示的,可以看到也低于第一个 o 的底部区域)。

因此,在这种情况下,line-height 越大,会看到这种情况下的文字越是高,高出来的空白区域主要是 line-height 本身比文字大的部分,以及对齐方式不同造成的差异距离。

上述这种情况,想要正确的对齐,只需要修改垂直对齐的方式就可以了。设置 vertical-align:bottom 后的结果:

good

符合预期。


Download in HTML🔗

• HTML

在 Web 中,如果希望点击一个链接可以进行下载的操作,有以下两种方案可供参考:

后端的解决方案

后端在返回 Response Header 的时候,可以通过指定 Content-Disposition 的值,来改变浏览器默认对链接的行为,从而达到让浏览器直接下载某一个资源的目的。这里,Content-Disposition 的第一个参数有两种值可写:inline 和 attachment。其中,inline 是默认的值,表示响应中的消息体会以页面的形式展示,而 attachment 则会将这个行为改成下载到本地。

设置成 attachment 之后,还可以进一步通过配置 filename 来指定下载文件的文件名。例子如下:

Content-Disposition: attachment; filename="example.jpg"

如此设置之后,前端在访问到这个 URL 的时候,浏览器就会以 example.jpg 为文件名下载当前的资源了。

更多相关的介绍可以参考这篇文章。

前端的解决方案

除了后端的解决方案之外,前端也可以通过指定 a 标签中的 download 字段来下载文件。对于使用了 download 字段的 a 标签,点击后的默认行为将会有跳转浏览改成文件下载。download 属性可以跟一个文件名作为值,浏览器会将这个值作为下载文件的文件名来使用。

当然,前端的方案相对来说会有更多的限制,主要是以下几点:

  1. 文件必须是同域的,对于跨域的资源,download 并不会直接触发下载功能,行为上会和在新窗口打开资源一致;
  2. 如果后端在 Content-Disposition 指定了不同的文件名,那么会以后端指定的结果为准
  3. 如果后端 Content-Disposition 设置为 inline,不同的浏览器会有不同的行为:Firefox 会按 Content-Disposition 的结果来执行;Chrome 则会按 download 字段的设置来执行

更多细节可以参考 MDN 文档的相关部分。


Replace All Substring🔗

• JavaScript

JavaScript 内建的 String.prototype.replace 函数,如果传入的第一个参数是字符串,那么替换行为只会发生一次。如果需要将一个字符串内所有某子字符串都替换掉,往往需要一些额外的操作。以下提供一些可行的方案:

  1. 使用循环进行多次替换

最直观的想法,就是替换完成后通过 indexOf 等方案查找字符串,如果还有就继续替换:

function replace(input, from, to) {
  while (input.indexOf(from) >= 0) input = input.replace(input, from, to);
  return input;
}

当然,这并不是一个优雅的解决方案。

  1. 使用正则表达式

String.prototype.replace 支持第一个参数传递正则表达式。有了正则表达式,只要设置上 g 标签,就可以全局匹配并替换所有的情况了。示例代码如下:

function replace(input, from, to) {
  return input.replace(new RegExp(from, 'g'), to);
}

这个方案的劣势在于,如果需要替换的内容中含有某些正则表达式特有的匹配符号,可能会导致非预期的结果。举个例子来说,如果希望把 .+ 这个字符串替换成 +. 这样,上面的函数并不能达到预期的效果,因为 .+ 在正则表达式中可以匹配任意的字符。replace('hello.+world', '.+', '+.') 的执行结果是 +.。

  1. 使用 split & join

这是一个比较取巧的方案,先用 split 函数将字符串进行拆分,然后再用 join 将拆分后的结果重新拼接起来。示例代码如下:

function replace(input, from, to) {
  return input.split(from).join(to);
}

这个方案代码比较简洁,也不会有正则表达式中提到的问题。虽然计算会产生中间变量(数组),但只要不是频繁或在大规模数据上使用,效率的影响可以忽略不计。


Glob in NPM🔗

• JavaScript

在使用 stylelint 的时候,发现了一个有趣的问题:如果直接使用 stylelint 的 bin 文件对批量 LESS 文件进行检查,程序可以如预期的运行;但是如果把同样的命令写到 package.json 中,以 npm script 的方式进行运行,最终被检查的文件就少了很多,实际只有一个文件参与了检查。

具体来说,./node_modules/.bin/stylelint src/**/*.less 这个命令可以检查所有的 LESS 文件,但是把 stylelint src/**/*.less 写到 package.josn 中之后,再运行却只检查了一个文件。

通过检查 stylelint 的文档,发现官方在写命令的时候,写法和上述略有不同,为:stylelint "src/**/*.less"。

经过排查问题,发现根源在于:npm 使用了 sh 来执行代码,而 sh 和 zsh 在解析 Glob 的时候,行为是不同的。

npm,包括其他 Linux 进程,在使用 shell 的时候,默认使用的都是 sh,除非有其他明确的指定。这意味着,即使当前正在使用的 shell 是 zsh,在运行 npm 命令的时候,还是默认使用了 sh 对脚本进行执行。也就是说,./node_modules/.bin/stylelint src/**/*.less 这个命令执行,使用的是当前打开的 shell 程序(比如 zsh);而当这个命令写到 package.json 中,并以 npm script 的方式进行运行的时候,执行 shell 的就是 sh 了。

使用不同的 shell 程序,难免就会在行为上造成不一致。这里的 Glob 解析就是一个例子。在 zsh 里面可以简单的做一个实验。执行如下的命令:

ls src/**/*.less

可以看到,zsh 给出了当前 src 目录下所有的 LESS 文件, 不管这个文件是在多深的子目录下;而如果先在 zsh 中执行 sh 或 bash 进入到 sh 或 bash 的工作环境中,再执行同样的命令,可以看到输出的结果可能就是不同的。实际上,对于 sh 来说,它本身并不识别 ** 这个语法,这个表示在 sh 中会被简单的识别为 *,src/**/*.less 在 sh 中等价于 src/*/*.less。换句话说,在 sh 的环境中,上述命令只会寻找所有在 src 目录下一级子目录中的 LESS 文件,一旦层级大于一层,就不会被找到了。

这也是为什么同样的命令,直接执行和在 npm 中执行会有差异的原因。

最后,加上双引号 stylelint "src/**/*.less" 就可以解决这一问题的原因在于:一旦加上了双引号,这一个 Glob 就不会被 shell 直接解析,而是会以字符串的形式直接传递给 stylelint。(具体来说,如果不加双引号,shell 会先将 Glob 解析成一组具体的文件,stylelint 实际拿到的 process.env.argv 很可能会是一个很长的字符串数组,每一个元素都是一个具体的文件;而如果加上了双引号,stylelint 拿到的只有一个 Glob 表达式字符串。)有了这个 Glob 的字符串,stylelint 内部就可以使用相应的 package 来进行解析,从而得到一串具体的文件列表。因为使用了 stylelint 内部自带的 Glob 解析,就可以保证在不同的 shell 环境中都得到一致的结果了。

参考


Chrome DevTools Blackbox🔗

• Chrome

在 Chrome 调试的构成中,单步执行代码是常有的操作。然而,一般来说,出问题的很可能是业务代码,具体依赖的库(如 React 或者 Mobx 等)相对则是更加稳定的。如果单步调试的过程中,会频繁进出库相关的代码,显然会对调试造成很多的干扰,不利于问题的排查。

为此,Chrome 提供了 Blackbox 的功能,可以帮助将部分指定的文件从调试中剔除。一旦使用 Blackbox 剔除了某些代码文件,那么:

  • 从这些文件中造成的报错不会暂停代码(除非开启了 Pause on exception)
  • Step in/out/over 不会执行到这部分的代码
  • 这些文件中的事件监听断点不会触发
  • 文件中设置的断点也不会被触发(代码不会暂停)

有几个方法可以添加 Blackbox:

  1. 打开 DevTools 后按 F1 打开 Settings 界面,然后选择 Blackboxing 并填写

DevTools 设置 Blackbox 匹配规则

如果网站的标准库是通过 CDN 文件直接引入的,可以把文件名直接写在这里,如 react.min.js 或是 jquery.min.js 等;类似的,如果页面是通过 Webpack 进行打包的,那么 vendor 的部分很可能也会打包到一个独立的文件中,比如就叫 vendor.xxx.js,那么也可以把相应的匹配写在这里。

  1. 打开 DevTools 后在 Sources 标签下找到需要屏蔽的文件,在文件内容处右键,并选择 Blackbox

DevTools 设置单个文件 Blackbox

参考文档


String Manipulation in SQLite🔗

• SQLite

在 SQLite 中,如果需要对列表的字符串数据做一些简单的变化,可以直接通过 SQLite 内建的函数来完成,而不需要借助外部的程序语言(如 Node.js)。使用内建的操作,转化的效率会远高于使用外部的语言来进行操作。下面通过一些例子来简单介绍一些和字符串相关的操作方法:

字符串截取

可以使用 SQLite 自带的 substr 的函数来截取字符串。函数的签名是 substr(string, start, length),其中 start 和 length 可以是负数,具体的行为可以参考这里的介绍。

假设有一列图像文件相关的数据:

xxx.jpg
yyy.gif
zzz.png

想要统计文件的格式,一个简单的写法如下(不考虑 .jpeg 之类的情况):

select substr(image_column_name, -3) as suffix from table_name group by suffix;

需要注意的一点是,SQLite 中的 substr 函数,记录的 start 下标,是从 1 而不是 0 开始的。

查找字符

在上例中,如果需要考虑 .jpeg 之类的情况,直接写死起始数字的下标就显得不太合适了。这时候,可以使用 instr 来配合查找:

select substr(column, instr(column, '.') + 1) as suffix from table_name;

instr 的文档可以看这里。

字符串长度

如果需要删除字符串的最后几位,光有 substr 函数就不够用了,还需要知道一个字符串具体的长度,才能确定需要截取的字符串长度是多少(定长字符串除外)。这就需要 SQLite 自带的 length 函数了。细节可以参见文档,以下举一个实际的例子。

假设有一列身高相关的数据:

170cm
168cm
182cm

想要截取其中数字的部分,可以使用 substr 和 length 配合着这么写:

select substr(column, 1, length(column) - 2) from table;

字符串转数字

接着上文的例子,如果希望进一步把字符串转化成数字,可以使用 cast 函数:

select cast('170' as integer);

结合起来:

select cast(substr(column, 1, length(column) - 2) as integer) from table;

注意,这里 substr 和 cask 函数在处理 NULL 的时候,都是不会做任何操作,直接返回 NULL 的。因此,如果上述的列中有数据是 NULL 而不是字符串,使用 cask + substr 的操作也会得到 NULL 的结果,不会有报错或其他问题。

转化成 NULL

然而在上例中,如果 cask 收到的参数是空字符串,那么转换的结果就是 0。这就不一定符合需求了。可以使用 nullif 这个操作符,将这种情况强制转化成 NULL:

select nullif(column, '');

获取 ASCII 码

使用字符串存储的成本会比使用整数来的大一些。除了上面提到的将字符串直接转化成数字的例子,对于一些单个字符类型的值,转化成数字存储也不失为一个好方案。比如,将某组 A 到 Z 的字母转化成 0 到 25 的数字。这时候,就可以使用 unicode 函数了:

select unicode(column) - 65 from table_name;

这里 65 是 A 的 ASCII 码。


Big Number in JavaScript🔗

• JavaScript

JavaScript 中可以很方便在字符串和数字之间进行转换,比如:+'123' => 123,(123).toString() => '123'。

然而,需要注意的一点是,JavaScript 中的数字并不是整数,而是浮点数。更确切的说,数字使用的是 64 bit 双精度浮点数来表示的。这意味着,如果服务器存储的数字是一个 Int64,那么在给到前端的时候,很有可能会出现转化上的问题。对于双精度浮点数来说,能够表示的最大的数是 253−12^{53}-1,超过的部分就会被截断,无法精确表示。

比如:

console.log(+'9223372036854775808');
console.log(2 ** 63)
// output: 9223372036854776000

JavaScript 提供了 Number.isSafeInteger 这个 API 来判断一个数字是否是在可表示的安全范围内。比如:

console.log(Number.isSafeInteger(2 ** 63));
// output: false
console.log(Number.isSafeInteger(9223372036854776000));
// output: false
console.log(Number.isSafeInteger(2 ** 53 - 1));
// output: true

这里,2 ** 53 - 1 就是 JavaScript 中可以表示的最大整数,Number.MAX_SAFE_INTEGER 这个常量也等于这个值。超过这个数值的所有值都会被认为是不安全的,哪怕该值实际表示的结果“凑巧”是正确的。上例中,9223372036854776000 这个数字的表示结果“刚好”就是 9223372036854776000 本身,但是因为这个数已经超过了 253−12^{53} - 1,所以依然被判定为是不安全的。

虽然 JavaScript 本身的数字不支持大数,但是 Chrome 已经集成了 BigInt 数据类型,它可以被用于表示任意大的整形数字,可以用于这样的使用场景。(注:BigInt 本身还在 staging 3,并不是标准的一部分)

简单的使用方法如下:

const num = BigInt(2 ** 63);
// or:
// const num = BigInt('9223372036854776000');
console.log(num);
// output: 9223372036854775808n
console.log(typeof num);
// output: bigint

需要注意的是,BigInt 不可以使用 new 运算符,否则会报错。直接像函数一样传递参数调用就可以了。

BigInt 也是支持数字运算的,运算的结果依然是 BigInt:

console.log(1n + 2n); // => 3n
console.log(3n - 1n); // => 2n
console.log(2n * 3n); // => 6n
console.log(5n / 2n); // => 2n

特别需要注意的是,因为是整型数字之间的转换,所以在做除法的时候,不会出现小数。在上面的例子中,5n 和 2n 的除法,结果是 2n 而不是 2.5,这一个行为和 C 中两个 Integer 之间除法的行为是一致的。

另外,BigInt 不支持和其他的数据类型进行混合计算。比如:1n + 2 这样的计算是会报错的,需要显式的进行类型转换后,才可以进行运算。这一点,和 JavaScript 中其他数据类型之间随意混乱的运算行为是不同的(比如,1 + '2' 这样的计算 JavaScript 就不会报错,还会得到 '12' 这样怪异的结果)。

虽然 BigInt 不允许和一般的 Number 进行混合计算,但是比较运算符是可以在两者之间进行比较的。比如:1n < 2 或 2n > 1 这些都是成立的。BigInt 和 Number 之间无法取得 === 的严格等价关系,但是 == 的比较是可能成立的。换句话说:1n == 1 是成立的,但是 1n === 1 是不成立的。

更多关于 BigInt 的行为,可以参考 MDN。


Screen Recording in MacOS🔗

• Tool

OSX 自带的 QuickTime Player 支持屏幕的录制功能。具体的操作步骤如下:

  1. 打开 QuickTime Player
  2. 点击菜单中的 File => New Screen Recording
  3. 拖拽选择需要录制的区域,并点击开始录制,点击右上角可以结束录制
  4. 录屏结束后,可以选择菜单中的 File => Export => As Movie 来保存视频

转化为 Gif 格式

假设上述步骤保存了一个名为 in.mov 的视频,通过以下步骤可以将视频转化为 Gif 格式:

  1. 右键点击 in.mov 文件,选择 Get Info,在 More Info 中找到视频的大小信息,这里假设大小为 60x40
  2. 运行如下命令,将 in.mov 转化为 out.gif 文件

    ffmpeg -i in.mov -s 60x40 -pix_fmt rgb24 -r 10 -f gif out.gif

    这里参数的具体含义如下:

    • -s 60x40 指定了最大宽度和最大高度。这里可以不指定,输出将按照原始大小来。如果指定较小的长宽值,输出将变小。
    • -r 10 将帧数从 25 调整为 10。

如果 ffmpeg 命令找不到,可以使用 Homebrew 进行安装:

brew install ffmpeg

优化 Gif 文件

可以使用 gifsicle 命令对产生的 gif 文件进行压缩:

gifsicle out.gif --optimize=3 -o optimized.gif

这里,--optimize=3 参数要求 gifsicle 使用最高等级优化图片。这会需要更多的时间和 CPU 来计算,但是压缩效果相对也是最好的。

如果找不到 gifsicle 命令,可以使用 Homebrew 进行安装:

brew install gifsicle

经测试,一个 463 KB 的文件,压缩后的大小为 417 KB,压缩了 10%。

以上生成 Gif 和优化 Gif 的命令可以放到一起执行:

ffmpeg -i in.mov -s 60x40 -pix_fmt rgb24 - | gifsicle --optimize=3 > out.gif

参考


Raspberry Pi as WOL🔗

• Bash

安装 etherwake 用于 WOL (wake on LAN) 操作

sudo apt-get install etherwake

接下来,可以通过命令:

sudo etherwake -i eth0 AA:BB:CC:DD:EE:FF

来唤醒 AA:BB:CC:DD:EE:FF 这个 MAC 地址的设备。几点注意:

  1. etherwake 需要 sudo 运行,否则会报错:etherwake: This program must be run as root.
  2. -i eth0 不是必须的。如果同时有有线和无线网卡,-i 可以强制要求 etherwake 走有线的路径

参考文献


Permission Denied for Rsync🔗

• Bash

相比于 scp,rsync 命令可以在 SSH 拷贝的时候提供更多的灵活性,比如只拷贝新修改的或未存在的文件。

一个简单的拷贝命令如下:

rsync -auv /local/folder host:/remote/folder

这里,-a 表示拷贝所有的文件(包括子文件夹中的),-u 表示只拷贝修改时间更新的部分,-v 则会将结果输出到 stdin 中方便查看。类似的,还可以使用 --ignore-existing 来要求 rsync 只拷贝新的文件,忽略已经存在的部分。

然而在实际使用的过程中,rsync 有如下报错:

Permission denied, please try again.
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(235) [sender=3.1.2]

如果换同样的 SSH 配置,使用 scp 就不会有类似的报错,可见本身并不是 SSH 登陆账户权限的问题。这里的 Permission denied 报错非常的具有误导性。实际上,更可能的情况是 rsync 无法在远程主机上找到,需要通过 --rsync-path 参数手动指定。

首先,可以先 SSH 到远程主机上,确认 rsync 本身是存在的:

rsync --help

接着,可以通过 type 命令确认 rsync 的实际位置:

type -a rsync

这里,假设输出的结果是 /bin/rsync,那么,可以将原先的 rsync 命令改写为:

rsync -auv /local/folder host:/remote/folder --rsync-path=/bin/rsync

再次运行就不会报错了。

参考文档


How Makefile works🔗

• Bash

在 C 编程中,经常会用到 Makefile 来对源代码进行编译。一个简单的 Makefile 如下:

out: input.c
  $(CC) input.c -o out -Wall -Wextra -std=c99

这里,第一行的 out: input.c 表示 make 应该根据输入 input.c 来产出 out 这个文件。

第二行的 $(CC) 会由 make 替换成本机的 cc 程序(即 c compiler);后面跟着的是 cc 编译会用到的参数,包括输入源文件 input.c,输出文件 out,编译输出所有的 Warning(-Wall 即 Warning all,-Wextra 即 Warning extra),同时指定使用 C99 标准来编译 C 代码(和 ANSI C 相比,C99 允许在函数的任意位置定义变量,而不是必须在顶部)。

运行 make 命令,程序会查找当前目录下的 Makefile 函数,读取其中的配置,根据输入输出的要求,查找文件,然后再选择编译。

第一次编译,程序会用 input.c 编译出一个 out 文件来。

在 input.c 没有修改的情况下,如果再运行一次 make 命令,会得到如下的输出:

make: `out` is update to date.

这里,make 程序并没有通过任何外部文件的方式记录编译的情况。判断是否需要编译完全依赖于系统默认的文件功能,即简单的比较 input.c 和 out 两个文件的最后修改时间。如果 out 的最后修改时间比 input.c 要晚,就认为 out 是最新的,不再重复编译;如果 input.c 的最后修改时间晚于 out 的时间,或是 out 压根就不存在,那么 make 就会执行 Makefile 中配置的编译命令。

可以通过以下方式欺骗 Makefile 来检查这一行为:

  1. 修改一下 input.c 并保存
  2. 删除 out 文件,然后用 touch 命令创建一个空的 out 文件。因为是先修改,再创建,所以 out 的创建时间会晚于 input.c
  3. 尝试执行 make 命令,会发现提示 out 已经是最新的,并没有执行真正的编译命令(尽管这里 out 并不是通过 make 编译出来的)

Check Exit Code of Command🔗

• Bash

在命令行中,一个命令会有一个返回数值,0 代表正确运行;如果命令返回了非 0 数据,则代表命令运行出现了错误。

比如,如果 Jest 命令跑单元测试出现了错误,那么就会返回一个非 0 的值。运用 set -e 可以让 Bash 在遇到非零返回的命令行之后即停止,不再运行接下去的命令。

那么,该如何确定之前的命令是否返回了 0 呢?

可以简单的使用如下的命令:

echo $?

这里的 $? 就是上一个命令返回的数值。如果上一条命令执行成功,那么这里应该输出 0。


Exit when Command Fail🔗

• Bash

在写 CI 脚本的时候,希望可以在脚本执行失败之后终止后续的所有操作。比如:

echo "start"
yarn test
echo "end"

如果 yarn test 这个命令失败了,希望不执行 echo "end" 语句。然而通过执行上面的代码,会发现默认是执行的。如果希望不执行这个操作,有几种思路:

第一种,是用 && 将语句串联起来,比如:

echo "start" && yarn test && echo "end"

这样的方案,缺点是比较的麻烦。一旦东西比较多,就很难保证代码的可读性了。

第二种方案,是使用 set -e,脚本改为:

set -e
echo "start"
yarn test
echo "end"

如此一来,脚本在语句执行失败(Exit Code 不是 0)之后就会退出,不会执行接下去的脚本。

参考文档