Things I Learned (2019-06)

Draw Math Equation🔗

Service

desmos 是一个在线的网站,可以用于绘制数学表达式对应的函数图像。

生成的结果可以通过链接分享,也可以生成 PNG 格式的图片下载到本地。

以下是一个例子:

desmos-graph

对应的在线观看地址:这里


Submit Button outside Form🔗

HTML

在一个表单中敲击回车按钮,默认会触发 <form> 的 submit 事件。当 <form> 里面有 <button type="submit"> 按钮的时候,敲击回车会先触发 button 的 click 事件,再触发 form 的 submit 事件。

因为有了这样的机制,在一个表单中,只要指定了 submit 按钮,那么不论是点击按钮还是直接按回车,都可以触发相同的 click 事件,达到同样的代码提交功能。这使得代码可以很容易的复用,不需要额外的工作量就可以让鼠标和键盘都能够方便的提交表单。

然而在实际开发的过程中,因为组件的划分,有时候不能做到 <button type="submit"> 放在 <form> 中。HTML5 中,增加了 buttonform 属性,可以用于关联表单和表单外的提交按钮。

举例如下:

<form id="form-id">
  <input type="text" name="name" />
</form>

<button type="submit" form="form-id">Submit</button>

在上述代码中,button 存在于 form 的外面,理论上和 form 是没有关联的。但是,因为 form 字段的存在,使得 form 和 button 被关联了起来。这样,和 button 在 form 中的表现形式一样,在 form 内敲击回车,就可以触发 button 的 click 事件。

注:如果 click 事件和 submit 事件(如果定义了)都没有 event.preventDefault(),那么就会触发默认的 form 提交流程,会造成页面提交。如果 form 没有指定提交的地址,那么就会提交到当前页面。这可能并不是预期中的行为。


scripts-prepend-node-path🔗

Node.js

在使用服务器编译上线包的过程中,可能会使用 nvm 来管理编译使用的 Node.js 版本。此时,运行时 Node.js 的二进制文件和实际服务器本身的 Node.js 文件将会是不同的。

这样做的好处是可以隔离,保证编译的环境是稳定的。

但是,当 npm 试图运行 Node.js 的时候,就要格外小心了。默认情况下,npm 会使用 path 中定义的 Node.js 版本。而对于打包服务器来说,这个 path 上的 Node.js 很可能并不是 nvm 管理的那个版本。

举个例子,如果 package.json 中定义了 postinstall 的脚本,如下;

{
  "scripts": {
    "postinstall": "some-cli"
  }
}

那么,在 yarn install 之后,执行这个 postinstall 的过程中,some-cli 需要 Node.js 来执行,而 npm 使用的 Node.js 是 path 上的默认版本,和 nvm 管理的并不是同一个。命令行会有如下的报错:

npm WARN lifecycle The node binary used for scripts is xxx but npm is using xxx itself. Use the --scripts-prepend-node-path option to include the path for the node binary npm was executed with.

正如报错指出的,为了修正这个问题,需要用到 scripts-prepend-node-path。这样,当 npm 需要 Node.js 来执行代码的时候,会选择和自身匹配的 Node.js 版本,而不是默认 path 上的版本。(文档

除了每次运行时加上命令行参数之外,一个比较简单的方法是使用 npm config 设置相对应的参数:

npm config set scripts-prepend-node-path=true

或是在 .npmrc 中加上

scripts-prepend-node-path true

设置完后,就不会再有报错了。


Screenshot in Chrome🔗

Chrome

在 Chrome 浏览器中,有专门用于页面截屏的扩展应用。其实 Chrome 自身也提供了截屏的工具。

要使用 Chrome 自带的页面截屏方案,首先需要打开 Chrome DevTools。接下来,按 Cmd + Shift + P 打开指令搜索框。在搜索框中,搜索 screenshots 就可以找到和截屏相关的各个命令:

  • Capture area screenshot - 可以截取页面某一个区域的图像(用鼠标选择)
  • Capture full size screenshot - 可以截取整个页面的图像
  • Capture node screenshot - 可以截取当前 DevTool 中选中的元素的图像
  • Capture screenshot - 可以截取当前可视区域的图像

其中,Capture node screenshot 是一个比较有意思的功能。在 DevTool 中选定了某一个元素(Element)之后,执行这个命令,就会将这个元素自身的区域截图。当然,元素所在区域内,由于排版的缘故,还存在父元素,子元素或是其他什么元素的内容。在执行截图命令的时候,这些内容也会被包含进去。


Get Available Port Number🔗

JavaScript

前端的开发工程,经常需要开启一些调试用的服务器,一旦调试的服务器多了,难免会出现网络端口号的冲突;类似的,如果在一台开发机上有多个人同时开发,开发脚本也就不能写死一个固定的端口号了。在这类情况下,如果要手动解决这些冲突(修改端口号或者手写端口号的分配规则),不免有些麻烦。

事实上,创建服务的时候,可以设置 0 作为端口号。这样的话,系统就会指定一个当前空闲可用的端口号,以保证不发生端口号冲突的情况。

下面是一段 Node.js 的示意代码:

const http = require('http');
const server  = http.createServer((request, response) => {
  response.writeHead(200, { 'Content-Type': 'text/html' });
  response.write([
    '<html>',
    '<head><title>Node.js Server</title></head>',
    '<body><h1>Hello World</h1></body>',
    '</html>',
  ].join(''));
  response.end();
});

// ask for an available port
server.listen(0);

server.on('listening', function() {
  // ask for actually used port
  var port = server.address().port;
  console.log(`server is listening: ${port}`);
});

其中,server.address().port 可以拿到当前系统具体分配的端口号。有了这个端口号,就可以自动/手动的打开对应的 URL 地址以访问新生成的服务了。


Detect Overflow🔗

JavaScript

下面展示的这段 CSS 设置非常常见,可以让文字超过宽度的时候,显示 ...

.ellipsis {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

在实际业务中,经常会碰到这样的需求:只有当显示出 ... 的时候,才做某些事情(比如,才在鼠标悬停的时候展示 Tooltip,用以显示完整的文字内容)。CSS 本身并没有提供足够的接口,让开发者可以直观的了解到当前的文字内容是否已经超出了可以显示的范围。如果需要判断当前内容是否超出了元素的宽度(因而让 CSS 渲染出 ...),可以用下面的 JavaScript 代码:

function isOverflow(element) {
  return element.scrollWidth !== element.clientWidth;
}

根据 MDN 的介绍,scrollWidth 是一个制度元素,其值等于该元素实际的宽度(包含了元素因为 overflow 没有展示出来的部分);而于此相对的,clientWidth 是元素实际展示的宽度。注,对行内元素和没有 CSS 的元素来说,clientWidth 是 0,具体见 MDN。针对判断是否出 ... 的情况,这里不需要考虑 clientWidth 的特殊情况。因为 scrollWidth 实际给出的值包含了被隐去的部分,因而当 scrollWidthclientWidth 不想等的时候,就可以认定当前有部分内容被隐去了,因而 ... 也出现了。


TypeScript Non-null Assertion🔗

TypeScript

在 TypeScript 中,常常存在一个对象可能是 undefinednull 的情况。如果试图直接使用这样的对象,很可能会造成 TypeScript 的报错(在 --strictNullChecks 开启的情况下)。这本身是一个正确的行为,也可以在编译时帮助开发者避免一些不必要的错误。

然而,在实际的开发中,不免遇到这样的情况:在某些特定的生命周期中,开发者可以很明确的知道某一个值不会是 undefined 或者 null。然而,这样的前置条件 TypeScript 本身并不知情。此时,为了防止 TypeScript 报错,就需要通过某些显式的方法,声明这一情况。

最常见的方案,是通过 as 来强制类型转化。比如:

function throwIfUndefined(input: any) {
  if (typeof input === 'undefined') throw new Error('Undefined!');
}
function handler(optional?: string) {
  throwIfUndefined(optional);
  console.log((optional as string).length);
}

TypeScript 提供了一个 Non-null assertion 运算符:!.,就是用于上述情景的。具体来说:

function handler(optional?: string) {
  throwIfUndefined(optional);
  console.log(optional!.length);
}

上面这样写之后,TypeScript 就不会报错了。optional 被认为一定是非 undefinednull 类型的。(至于 TypeScript 会认为这个变量是什么类型的,就要看这个变量除了 undefined | null 的类型之外,还可能是什么类型的了)

当然,不难看出,这个运算符只是一个编译时帮助编译器理解类型用的辅助手段,本身并不是一个语法糖。因此,在 TypeScript 转化成 JavaScript 的过程中,这里的运算符会直接被去掉。optional!.length 生成的就是 optional.length,没有生成任何额外的东西。


Record in TypeScript🔗

TypeScript

Record 是 TypeScript 中一个很实用的范型类型。它需要两个具体的参数类型,Record<K, V>,用于指定一个对象的类型。其中,对象的所有 key 都是 K 类型的,而这些 key 对应的值则都是 V 类型的。如果不使用 Record 类型,可能需要用如下的方法来达到同等的效果:

type RecordExample = Record<string, number>;
interface EquivalentExample {
  [key: string]: number;
}

显然,等价的写法更为的复杂,看起来也不那么清晰。

当然,对于 JavaScript 来说,对象的属性其实只能是 string 类型的。虽然有时候也会直接使用 number 作为值(TypeScript 里面也可以专门这么来做类型强制),但是其实在用作 key 的时候,会经过一步 toString 的转化。比如:

const obj = { key: 'value' };
const key = { toString() { return 'key'; }};
console.log(obj[key]); // output: value

这么看起来,Record 的应用场景似乎非常有限,只有 Record<string, xxx> 或者 Record<number, xxx> 两种可能性。然而,TypeScript 中除了可以使用一些泛用的类型之外,也可以对类型做更进一步的限定。比如,指定类型只能是 'apple' | 'banana' | 'orange'。如此一来,Record 就有了更多的应用场景。

举例来说,如果希望写一个函数,可以将参数对象中所有的值都转化成对应的数字,就可以这么写:

type Input = Record<string, string>
function transform<T extends Input>(input: T): Record<keyof T, number> {
  const keys: (keyof T)[] = Object.keys(input);
  return keys.reduce((acc, key) => {
    acc[key] = +input[key];
    return acc;
  }, {} as Record<keyof T, number>);
}

这样,就可以保证输入和输出的对象,有相同的 key。

然而,需要注意的一点是,在使用联合类型的时候 Record 本身也存在局限性(这一点本身是 TypeScript 的局限性)。还是以上面的 'apple' | 'banana' | 'orange' 为例,如果这么写,那么下面的代码将是错误的:

type Fruit = 'apple' | 'banana' | 'orange';
type Price = Record<Fruit, number>;
// type error
const prices: Price = {
  apple: 20
};

原因是,上面只定义了 apple 的数据,但是没有定义剩下的 bananaorange 的数据。以下定义不会报错,但有时候并不满足需求:

const prices: Price = {
  apple: 20,
  banana: 30,
  orange: 40,
};

Record 天然并不能解决可选 key 的情况。Record<'A' | 'B', number> 的含义是 AB 都需要是这个类型的 key,而不是说只需要有 AB 一个做 key 就可以了。对于这种需要可选的情况,可以再套上一层 Partial 来满足需求:

type Price = Partial<Record<Fruit, number>>;
// correct
const prices: Price = {
  apple: 20,
};

更多实现的细节,可以参考 Record 定义Partial 定义


SSH ProxyJump🔗

Bash

ssh 自带跳板机功能:-J。示例代码如下:

ssh -J userA@a.xxx.com userB@b.xxx.com

命令会需要依次输入 a.xxx.com 和 b.xxx.com 两台机器的登陆信息。校验通过之后,就会登陆 b.xxx.com 这台机器,登陆的用户是 userB。并且,登陆是通过 a.xxx.com 这台机器的 userA 完成的。a.xxx.com 在这里就是一个跳板机的功能。

参考文档


Unzip using JavaScript🔗

JavaScript

使用 JavaScript 库 jszip 可以对传入的本地 zip 包进行解压缩并读取其中的文件。

一段参考代码如下:

const filename = 'example.txt';
async function onDrop(event: DragEvent<HTMLDivElement>) {
  const file: File = event.dataTransfer.files[0];
  const zip = await jszip.loadAsync(file);
  const content = await zip.files[filename].async('text');
  console.log('content of example.txt: ', content);
}

这里,loadAsync 函数接受的参数是一个 Blob,而无论是 input[type=file] 还是通过 drag-n-drop 得到的文件(File),都是一种特殊的 Blob,因而可以直接传递给 loadAsync 使用。

.async('text') 可以将结果异步转化成字符串。API 也支持其他的格式类型,比如 ArrayBuffer 等。(文档

除了读取内容之外,jszip 也支持创建修改 zip 包的内容。完整的 API 文档可以参考这里


HashHistory & State🔗

JavaScript

react-router 底层使用了 history 库来处理路由。一般有两种选择,一种是使用 BrowserHistory(背后的实现依赖了 History API),一种是使用 HashHistory(背后的实现主要依赖 window.location.hash)。

如果尝试在 HashHistory 上调用 push API 并传递 state 参数:

var history = HashHistory();
history.push('path', { state: 'value' });

会在 console 看到如下的报错,并且 state 并没有传递成功:

Hash history cannot push state; it is ignored

这个是 history 库的默认行为,具体的代码可以参考 modules/createHashHistory.jspush 的代码:

function push(path, state) {
  warning(
    state === undefined,
    'Hash history cannot push state; it is ignored'
  );

  const action = 'PUSH';
  const location = createLocation(
    path,
    undefined,
    undefined,
    history.location
  );
  // ...

上面的代码可以看到,如果传递了第二个参数 state,那么就会输出报错。同时,在 createLocation 函数调用的时候,第二个参数本来应该是 state,这里显式地写成了 undefined,明确拒绝了传递 state 的做法。

然而,这并不意味着就完全不能传递 state 了。

事实上,push 函数有两种 API 可供选择,一种是 path 是字符串,然后 state 作为第二个参数传递;另一种则第一个参数就是一个对象,其中一个属性就是 state。

如果使用下面的方法调用,state 就可以在 react-router 中被传递了:

hashHistory.push({ path: 'path', state: { state: 'value' } });

但是,需要指出的是。这个只是 state传递了,并不代表 state 真的被存储了下来。事实上,HashHistory 并没有依赖浏览器的 History API 功能。因此,这里的 state 传递之后,会出现在 history.location.state 中,但是在浏览器前进/后退的操作中,数据会被丢弃,无法找到。

一个简单的例子是:

var h = createHashHistory();
h.listen((location) => {
  console.log(location.state);
});
h.push({ pathname: 'a', state: 1 });
// output: 1
h.push({ pathname: 'b', state: 2 });
// output: 2
h.goBack();
// output: undefined
h.goForward();
// output: undefined

如果将 HashHistory 改成 BrowserHistory,则可以正确输出:

var h = createBrowserHistory();
h.listen((location) => {
  console.log(location.state);
});
h.push({ pathname: 'a', state: 1 });
// output: 1
h.push({ pathname: 'b', state: 2 });
// output: 2
h.goBack();
// output: 1
h.goForward();
// output: 2

Capture Screen Shot with Mac OS🔗

MacOS

Command-Shift-4 可以截取部分屏幕。截取之后的结果会保存到桌面。

一些技巧

在点击 Command-Shift-4 之后,如果单击 Space,会转化成对某一个窗口进行截屏(再次点击 Space 可以切换回部分屏幕截取模式)。

保存后的结果,会短暂的以预览图的形式出现在屏幕的右下角。和 iPhone 上的操作类似,点击这个预览图,可以打开截图并进行图片的修改操作,比如增加一些提示性的标识。

注:在进行上述修改的时候,如果点击右上角的垃圾桶按钮,就不会将最终结果保存;如果点击右上角的关闭按钮,则是会保存的。

另外,在截取的时候,按住 Option 可以按比例缩放选取的范围;按住 Shift 则可以固定上下或左右方向,只在一个轴上选取截取区域。

如果不希望保存到本地桌面,可以使用 Command-Control-Shift-4。截取后的结果会存放到剪贴板中。保存到剪贴板的这个操作,是不会在右下角生成预览图的,因而也不能直接对截屏结果进行二次操作。

其他相关操作

  • Command-Shift-3 截取当前整个屏幕,并保存到本地
  • Command-Control-Shift-3 截取当前整个屏幕,并保存到剪贴板中

修改默认配置

默认情况下,截图是保存到桌面的。可以执行以下的命令行改变这个默认行为:

defaults write com.apple.screencapture location ~/Pictures/
killall SystemUIServer

其中第一行是将默认地址改到 Pictures 下面;第二行则是让配置当即生效。注:需要确保保存到的位置是存在的,如果不存在,最终保存的结果还是会放到桌面下。

同时,默认保存的图片格式是 .png,也可以通过下面的方式改成 .jpg 或其图片格式:

defaults write com.apple.screencapture type jpg
killall SystemUIServer

取决于屏幕显示的内容类型,有时候 .jpg 的结果会比 .png 更小。


Generate v4 UUID🔗

JavaScript

这里有一段生成 UUID v4 非常短小的 JavaScript 代码。虽然有一些注释,但总体上并不是非常的好理解。

首先,用 TypeScript 翻译一遍原始的代码:

const slot = '10000000-1000-4000-8000-100000000000';

function rand(from: number, to: number) {
  return Math.floor(Math.random() * (to - from + 1) + from);
}

function getRandomChar(input: number) {
  const seed = input === 8 ? rand(8, 11) : rand(0, 15);
  return seed.toString(16);
}

function uuidReplacer(char: string) {
  return getRandomChar(+char);
}

function uuid() {
  return slot.replace(/[018]/g, uuidReplacer);
}

接下来对原始的代码中的部分做一些解释:

a ^ Math.random() * 16 >> a/4

这个部分的代码不太好理解。首先,先根据运算符的优先级,给上面这个算式加上帮助理解的括号:

a ^ ( (Math.random() * 16) >> (a / 4) )

接下来,再来看每个部分都是什么意思:

Math.random() * 16

创建了一个 0~16 范围内的随机数,>> (a / 4) 这个运算之后,会得到几种不同的结果(注意,这里 a 只可能是 '0''1''8' 三种情况):

  • '0''1' 的时候,结果是 0~15 的随机数(整数)
  • '8' 的时候,等价于 >> 2,所以结果是一个 0~3 的随机数(整数)

最后一步异或运算 ^,得到的可能结果分别是:

  • '0' 的时候,异或结果不变,是 0~15 的随机数(整数)
  • '1' 的时候,异或结果最后一个比特位的值正好相反,最终的结果仍然是 0~15 的随机数(整数)
  • '8' 的时候,最终结果是 8~11 的随机数(整数)。因为 8 的第四位二进制是 1 其他都是 0,而 0~3 这几个数的二进制位数不超过两位,所以位数之间不存在交集,异或运算相当于是 8 + nn0~3 中的某一个数。最终得到的就是 8~11 的随机数(整数)。

最终,将生成的不超过 16 的整数转化成十六进制的对应字符。

第二部分的代码用到了正则匹配和替换(注意,这里 '4' 没有被替换,依然保留),主要不容易理解的是下面这个部分:

[1e7] + -1e3 + -4e3 + -8e3 + -1e11    

这里用到了 JavaScript 比较奇怪的类型转化功能。因为最开始是一个数组,所以这里的相加实际上是字符串的拼接。等价于 '10000000-1000-4000-8000-100000000000' 这个字符串。上面这样写主要是字符数上比较少。


DOM Interface🔗

HTML

当用 JavaScript 去访问 HTML 中的元素的时候,实际访问到的是 DOM 元素。比如,访问 div 元素,实际拿到的是 HTMLDivElement;访问 span 元素,实际拿到的是 HTMLSpanElement

大多数情况下,不同的 HTML 元素有不同的 DOM interface 对应,因为不同的元素很可能有一些行为/属性上的不同。

但是也存在一些 HTML 元素,并没有特定的 DOM interface 对应,直接使用了 HTMLElement 这个基类。常见的例子有 ib 或者 ruby 等。

注:根据 MDN 的说明,Firefox 中 i 对应的 DOM 是 HTMLSpanElement。但实际测试下来,最新版本的 Firefox 实现和标准是一致的,使用的 DOM interface 是 HTMLElement 而不是 HTMLSpanElement

可以用下面的方法简单的查看具体使用的是哪一个 DOM interface:

// should be: HTMLElement
document.createElement('i').constructor.name;

// should be: HTMLSpanElement
document.createElement('span').constructor.name;

Read certain type of files🔗

HTML

HTML 中的 input 组件,如果设置成 type=file,就可以变成一个文件选择控件。

默认情况下,系统默认打开的这个文件选择框,可以接受任意的文件选择。如果需要指定可以选择的文件类型,可以使用 accept 参数:

<input type=file accept="image/x-png,image/gif,image/jpeg" />

上面这个例子中,浏览器将会只接受 png / gif 或 jpg 的输入。

也可以写:

<input type=file accept="image/*" />

以支持任意类型的图片格式输入。同理,类似的比如 video/* 将会只接受任意类型的视频;audio/* 将会只接受任意类型的音频文件。

除了指定 MIME 类型之外,也可以指定后缀。比如:

<input type=file accept=".pdf,.doc,.docx" />

将会只允许以 pdf / doc / docx 这三种名称作为后缀的文件被选择。

各个浏览器的支持情况可以看这里

(当然,这个只是前端的一个校验,后端依然需要重新对前端给的输入进行检查才行)


Order of font-family🔗

CSS

由于系统的差异,不同的电脑上存在的字体是不一样的。为了网站的效果可以兼顾各个设备,一般在写 CSS 的时候,font-family 总是很长的一串。通过字体 fallback 的功能,让浏览器自行选择最先能匹配到的字体文件,从而保证显示的效果大体上接近于视觉效果图。在实际书写中,有一个值得注意的细节:英文字体应该在中文字体的前面

中文字体文件往往包含英文字符,但是这些英文字符的样式很可能并不是设计师希望看到的。如果中文字体展示在英文字体的前面,英文字体就没法被使用到,导致最终的效果略有偏差。下面展示了中文字体 PingFang SC 和苹果默认英文系统字体(SF NS Display)针对英文字母的渲染效果(需要在 MacOS 下查看):

ffi

ffi

所以,应该写:

.example {
  font-family:
    -apple-system, BlinkMacSystemFont,
    'Segoe UI', 'Helvetica Neue', Helvetica, Arial,
    'PingFang SC', 'Microsoft YaHei',
    sans-serif;
}

而不是:

.example {
  font-family:
    'PingFang SC', 'Microsoft YaHei',
    -apple-system, BlinkMacSystemFont,
    'Segoe UI', 'Helvetica Neue', Helvetica, Arial,
    sans-serif;
}

这里,-apple-system, BlinkMacSystemFont 针对苹果下的 Safari 和 Chrome 内核调用系统自带字体,对应到的英文字体是 SF (SF NS Display),中文字体是 PingFang (PingFang SC)。


TextDecoder🔗

JavaScript

在 JavaScript 中,如果得到了一串字节(比如 Uint8Array),要转化成对应的字符串,就可以用到 TextDecoder。简单的使用方法如下:

const decoder = new TextDecoder('utf-8');
const bytes = new Uint8Array([
  104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100
]);
const result = decoder.decode(bytes);

console.log(result); // => "hello world"

当然,上面的这个例子是比较简单的。不使用 TextDecoder 也可以直接转化成字符串:

const bytes = new Uint8Array([
  104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100
]);
const result = String.fromCharCode(...bytes);

console.log(result); // => "hello world"

TextDecoder 的主要优势,需要在非 ASCII 码范围内才体现出来。特别是 utf-8 这类变长字符串编码,直接处理比较困难。交给现成的 API 来处理,简单方便。

参考文档:MDN


global Provider for storybook🔗

JavaScript

一个项目工程里的组件,很可能需要依赖于某些项目顶层定义的 Provider 才能正确使用。比如,mobx 的项目可能会在顶层通过 mobx-react 中的 Provider 提供 store 参数。

如果需要在每一个 story 中都写:

<Provider {...info}>
  <ComponentForThisStory />
</Provider>

显然太啰嗦了。

storybook 提供了全局定义 decorator 的方法,可以以此来注册一些全局都用得到的改动。举例如下:

import { configure, addDecorator } from '@storybook/react';
import { Provider } from 'mobx-react';

function withProvider(story) {
  return <Provider {...info}>{story()}</Provider>
}

function loadStories() {
  // ...
  addDecorator(withProvider);
  // ...
}

configure(loadStories, module);

这样,在 story 中,只需要简单的提供组件就可以了,decorator 会自动为组件加上合适的外层 Provider

参考文档


glob🔗

Bash

Glob 类似于 Regular Expression,主要的使用场景是用于批量的文件匹配,在 bash 或是配置文件中常常被使用。下面列举了一些常见的语法规则:

  • * 匹配任意多个字符(包括匹配零个)
  • ? 匹配任意一个字符
  • [abc] 匹配方括号中的任意一个字符
  • [!abc][^abc] 匹配除了方括号中定义的三个字符外的任意字符
  • [a-z] 匹配方括号定义范围内的任意一个字符
  • [!a-z][^a-z] 匹配除了方括号定义范围内的任意一个字符
  • {ab,cd,ef} 匹配花括号中定义的三个字符串中的任意一个

举个例子,如果 Jest 的单元测试文件命名规范的正则表达式是:.+\.(?:test|spec)\.[tj]sx?$,也就是匹配下面的这些文件:

  • a.test.js
  • b.test.jsx
  • c.test.ts
  • d.test.tsx
  • e.spec.js
  • f.spec.jsx
  • g.spec.ts
  • h.spec.tsx

那么,相应的 Glob 可以写:*.{test,spec}.{js,jsx,ts,tsx}

如果不涉及到 React 的代码(没有 jsx),可以写成:*.{test,spec}.[tj]s

参考文档: