Things I Learned (2019-05)

Placeholder Images🔗

Service

以下整理了一些可用的占位图片服务:

lorempixel

地址:http://lorempixel.com/

参考引用方法:

<img src="http://lorempixel.com/400/200" alt="placeholder image" />

服务会返回随机的照片。可以通过参数选择需要返回的照片的类别(具体方法参考网站上的文档)。

效果:

placeholder image

lorem picsum

地址:https://picsum.photos/

参考引用方法:

<img src="https://picsum.photos/400/200" alt="placeholder image" />

服务会返回随机的照片。可以通过参数选择需要返回的照片的类别(具体方法参考网站上的文档)。

效果:

placeholder image

placeholder

地址:https://placeholder.com

参考引用方法:

<img src="https://via.placeholder.com/400x200.png" alt="placeholder image" />

服务器会根据要求返回需要的图片,可以指定背景颜色/文字/大小等,具体配置的方法见网站上的文档。

效果:

placeholder image

其他

其他可以参考的类似服务有:


Transform node_modules in Jest🔗

JavaScript

默认情况下,Jest 配置文件中的 transform 属性,是不会被应用到 node_modules 目录下的。如果引用的库本身使用了非 JavaScript 文件(比如 CSS 文件),会造成 Jest 无法正确处理。

一个可行的替代方案,是用 moduleNameMapper 来代替 transform 的功能。

以 CSS 的处理为例:

{
  // ...
  transform: {
    "^.+\\.(less|css)$": "jest-transform-stub"
  },
  // ...
}

上面这个是常规方案,但是对 CSS / Less 的处理不包含 node_modules 的部分。

{
  // ...
  moduleNameMapper: {
    "^.+\\.(less|css)$": "jest-transform-stub"
  },
  // ...
}

上面这个方案,可以达到一样的效果,但是 node_module 内的 CSS 引用也会被正确的处理。

两种方案没有优劣,主要是看使用的场景。


Upgrade Npm Dependencies🔗

Bash

npmyarn 都提供升级依赖的命令。

针对 npm,可以使用 npm update 来执行,命令格式如下:

npm update [-g] [<pkg>...]

更新的时候,默认会更新 package.json 文件,可以通过增加 --no-save 标记来禁用这一改动。

npm 的文档可以看这里

yarn 的命令会更加丰富一些,命令格式如下:

yarn upgrade [package | package@tag | package@version | @scope/]... [--pattern]

其中,--pattern 后面可以跟 grep 的 pattern,只有匹配到的依赖会被升级。

默认情况下,升级会参考 package.json 里定义的依赖允许的升级范围来选择可行的最高版本进行升级。如果希望直接升级到最新版本(往往意味着会有 breaking change),那么可以加上 --latest 标志。

yarn 的文档可以看这里


Recursively delete files by type🔗

Bash

以下 Bash 代码可以递归删除指定的 .map 文件。

find . -type f -name '*.map' -delete

如果同时希望删除 .map.xxx 文件,可以加上 -o flag

find . -type f -name '*.map' -o -name '.*' -delete

一些参数说明:

  • -type f 表示需要查找的是文件
  • -name 'xxx' 定义需要匹配的文件名
  • -o 表示 or,后面可以跟新的匹配规则
  • -delete 表示匹配到的文件需要被删除

CSS for Dark Mode🔗

CSS

prefers-color-scheme 这个 Media Query 可以用于检测当前的操作系统是否选择了 Dark Mode。这是一个依然处于初始草案阶段的功能(见 Draft),不过 Safari (12.1) / Chrome (76) / Firefox (67) 的最新版本都已经做了支持。

示例代码如下:

@media (prefers-color-scheme: dark) {
  body {
    background-color: #333;
    color: #fff;
  }
  :not(pre) > code[class*="language-"] {
    background-color: rgba(255,229,100,0.8);
  }
}

下面是一个可编辑的 CSS 代码,可以直接试一试:

注:上面这段代码是可改的,修改后的 CSS 会直接生效。但是由于 contenteditable 的限制,所有代码需要在一行内完成。

除了 dark 之外,prefers-color-scheme 可以接受的属性还有 lightno-preference 两种。其中,light 表示用户选择的是 Light 模式,no-preference 表示用户并没有做选择。

在 JavaScript 中,也可以通过下面的代码来判断当前是否是 Dark Mode:

const ifDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;

注:从当前的实验结果来看,matchMedia 的结果用 .addListener 注册回调事件并不能生效(Safari 12 测试)。

注:经测试,Safari 13 可以支持 matchMedia 结果 .addListener 的回调。因而,通过 JavaScript 来感知 Dark Mode 的修改,可以通过类似如下的代码完成:

function listenToDarkMode(callback: (isDarkMode: boolean) => void) {
  const matchQueryList = window.matchMedia('(prefers-color-scheme: dark)');
  matchQueryList.addListener(function (event: MediaQueryListEvent) {
    callback(event.matches);
  });
  callback(matchQueryList.matches);
}

如果图片的展示也需要区分,mediaQuery 也可以帮上忙:

<picture>
  <source srcset="mojave-night.jpg" media="(prefers-color-scheme: dark)">
  <img src="mojave-day.jpg">
</picture>

rxjs and hooks🔗

JavaScript

一直以来,rxjsreact 都不太搭,要在 React 中使用 rxjs 往往需要写并不怎么优雅的代码,比如:

class Example extends React.Component {
  constructor(props) {
    super(props);
    const initial = -1;
    this.state = {
      value: initial,
    };
    this.subscription = null;
  }
  componentDidMount() {
    this.register();
  }
  componentDidUpdate(prevProps) {
    if (prevProps.value$ !== this.props.value$) {
      this.unregister();
      this.register();
    }
  }
  componentWillUnmount() {
    this.unregister();
  }
  register() {
    this.subscription = this.props.value$
      .subscribe((value) => {
        this.setState({ value });
      });
  }
  unregister() {
    if (this.subscription) this.subscription.unsubscribe();
  }
  render() {
    const { value } = this.state;
    return (
      <div>{value}</div>
    )
  }
}

ReactDOM.render(<Example value$={interval(1000)} />, document.body);

上面这段代码,会根据 value$ 这个 Observable 的数据,通过 React State 这个桥梁,去更新 UI。并且,代码考虑到了给定的 value$ 可能后续变化的情况。如果不考虑后续 props 的修改,上面的代码依然需要在 componentDidMount 的时候注册回调并更新,然后在 componentWillUnmount 的时候注销,显得非常的啰嗦。

LeetCode 提供的 rxjs-hooks 提供了一个更为优雅的解决方案:

const Example = (props) => {
  const initial = -1;
  const value = useObservable(
    (inputs$) => inputs$.pipe(
      switchMap(([value$]) => value$),
    ),
    initial,
    [props.value$]
  );
  return (<div>{value}</div>);
};

ReactDOM.render(<Example value$={interval(1000)} />, document.body);

rxjs-hooks 另外提供了 useEventCallback 来更好的处理事件流,具体可以查看官方的文档


Disk Usage of Folder🔗

Bash

如果需要对目录下文件的占用空间做排序,可以使用下面的命令:

du -d 3 -k | sort -h

其中,du -d 3 表示,最多显示三层子目录,-k 会让输出以 KB 作为单位。sort -h 会对结果进行排序,排序的依据是文件夹的大小。这里,排序需要带上 -h 的标识位,不然以字符串进行排序的话,输出没有意义(比如,100 会排在 9 的前面)。


Extract all function properties from given type🔗

TypeScript

假设有一个 TypeScript 的类型是:

interface Example {
  str: string;
  num: number;
  func1: (param1: string, param2: number) => null;
  func2: () => void;
}

以下这个 TypeScript 的定义,可以用于将 T 中函数的部分抽离出来,形成新的类型:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

最终,新类型的定义如下:

type Result = Pick<Example, FunctionPropertyNames<Example>>;

等价于:

interface Equivalent {
  func1: (param1: string, param2: number) => null;
  func2: () => void;
}

classnames & css module🔗

JavaScript

classnames 库提供了一个 bind API,用于处理 CSS Module 的情况。

在 Webpack 中用 CSS Module 的方案编译 CSS 文件,后续在 JavaScript 中 import style from 'xxx.css'; 后,style 就是一个对象。这个对象的大体结构如下:

style = {
  foo: 'foo-abcde',
  bar: 'bar-12345',
  // ...
};

其中,对象的 key 是原始的 class name,而 value 则是施加 CSS Module 之后得到的唯一名称。

如果直接使用 classnames 的标准 API,那么写起来就需要大量使用 computed property name 的语法,比如:

<div className={classnames({ [style.foo]: true, [style.bar: false ]})} />

而使用 bind API,可以事先告知 classnames class name 的对应关系(通过指定 this),后续只需要使用字符串,classnames 就可以自动使用合适的结果:

import classNames from 'classnames/bind';
import styles from './style.css';

const cx = classNames.bind(styles);

const Component = () => (
  // result in: className="foo-abcde"
  <div className={cx({ foo: true, bar: false })} />
);

Multiple Git Configuration🔗

Git

对于有多个 Git 仓库的情况,不同的仓库可能需要配置不同的用户信息。

一种麻烦的方案是,每个仓库都配置一个本地的 Git 配置,不使用全局的设置,就不会有问题。但是这样配置非常的麻烦,也容易忘。Git 提供了配置覆盖的功能,可以指定某一子目录,使用另外一个指定的 Git 配置覆盖默认的全剧配置。

如下:

[includeIf "gitdir:~/work/github/"]
    path = ~/work/github/.gitconfig

这个配置指定了在 ~/work/github/ 目录下,除了全局的 .gitconfig 文件之外,读取 ~/work/github/.gitconfig 文件对配置进行覆盖改写。在 ~/work/github/.gitconfig 的优先级高于 ~/.gitconfig 的配置,会优先使用,没有定义的部分才会去全局中找。


Command to enter folder after git clone🔗

Git

下面的脚本,执行之后,可以完成 git clonecd 至目标文件夹内。

!f() {
  local tmp=$(mktemp);
  local repo_name;
  git clone $@ --progress 2>&1 | tee $tmp;
  repo_name=$(awk -F\' '/Cloning into/ {print $2}' $tmp);
  rm $tmp;
  cd $repo_name;
};
f

一些说明:

  • mktemp 可以创建一个临时文件,文件路径存放在 tmp 变量中
  • git clone $@ 中的 $@ 是执行脚本时候所有的传入参数
  • ---progress 2>&1 会将 Git clone 命令的结果输出。这里,默认情况下,clone 的过程数据只有在 error stream 输出到 terminal 的时候,才会显示。因为这里的命令需要将内容通过管道输出到 tmp 临时文件中,所以默认情况下 Git 就不会输出过程的数据了。为了能够让 Git 输出这部分内容,需要加上 --progress
  • tee $tmp 将管道的数据输出到临时文件中
  • awk -F\' '/Cloning into/ {print $2}' $tmp 的部分,会从输出的数据中,寻找 Cloning into 的输出,然后找到具体 clone 到了哪个文件夹中

Object.create(null)🔗

JavaScript

Object.create 可以用提供的对象做新对象的 __proto__。这导致了一个很有趣的现象,如果使用 Object.create(null) 来创建新对象,那么这个新对象上就没有任何 __proto__。因为 JavaScript 的对象经常被用来做字典使用,Object.create(null) 可以让这个功能使用更加的纯粹。

const dict = Object.create(null);
console.log(Object.getPrototypeOf(dict));
// output: null
console.log(typeof dict.hasOwnProperty);
// output: "undefined"
const obj = { };
console.log(Object.getPrototypeOf(obj));
// output:
// {
//   constructor,
//   hasOwnProperty,
//   isPrototypeOf,
//   propertyIsEnumerable,
//   toLocaleString,
//   toString,
//   valueOf,
//   ...
// }

同样,因为没有 prototype,理论上来说,后续如果有人对 Object.prototype 做操作,也不会影响到使用。

const dict = Object.create(null);
// ...
Object.prototype.addSomething = () => { };
console.log(typeof dict.addSomething);
// output: undefined
for (const key in dict) console.log(key);
// no output

console.log(typeof ({}).addSomething);
// output: function
for (const key in {}) console.log(key);
// output: 'addSomething'

所以,如果判断对象有某个字段,那么一定是他自身有这个字段,而不会是因为原型链上的定义。也就是说,不需要用:

if (Object.prototype.hasOwnProperty.call(dict, 'addSomething') { }) {
  // ...
}

而只需要写:

if (dict.addSomething) {
  // ...
}

当然,这也会有一些弊端,比如默认 Object.prototype 的东西就没了,如果需要 toString 之类的函数,得自己写。


Deletion of file in git🔗

Git

假设发现一个文件在历史版本中存在,但是当前不存在了,那么可能就需要知道是在什么时候,因为什么原因对文件做了删除。下面的命令可以一次性找出某一个文件的所有记录:

git log --full-history -- [file path]

如果只需要看最后一条记录(也就是被删除的那条记录),可以用:

git log --full-history -1 -- [file path]

Open Application in Terminal🔗

Bash

在 Mac 系统里面,.app 程序本质上就是一个目录,里面包含了很多文件。如果直接在 Terminal 输入 .app 的地址,会进入这个目录,而不是运行这个 App。如果需要运行,可以使用下面的命令:

open /Application/Example.app

如果需要指定 NODE_ENV 等信息,就可以一起配合使用

NODE_ENV=development open /Application/Example.app

performance data via JavaScript🔗

JavaScript

JavaScript 的 performance 除了常用的 now / mark 之外,也提供了和页面加载相关的很多接口。通过调用这些接口,就可以很方便的收集页面加载的相关指标,方便了解不同用户的实际体验。

perfomance.getEntries 返回的数据,有三种类型:navigationresourcepaint

其中,navigation 包含了 PerformanceNavigationTiming,里面记录了和页面导航相关的时间信息,比如 connection 的起始/结束时间等。可以通过下面的代码拿到完整的数据:

performance.getEntriesByType('navigation')[0].toJSON();
// output:
// connectEnd: xxx
// connectStart: xxx
// ...

resource 包含了所有的 PerformanceResourceTiming。每一个资源的请求,对应一个 PerformanceResourceTiming。例子:

performance.getEntriesByType('resource').forEach(({ name, duration }) => {
  console.log(`resource: ${name} use ${duration} milliseconds to load`);
  // output:
  // resource: https://xxxx use xxx milliseconds to load
  // ...
});

paint 包含了所有的 PerformancePaintTiming。一共有两个,分别是 first-paint 和 first-contentful-paint。例子:

performance.getEntriesByType('paint').forEach(({ name, startTime }) => {
  console.log(`name: ${name}, startTime: ${startTime}`);
  // output:
  // first-paint: xxxx
  // first-contentful-paint: xxx
});

download third party resource🔗

JavaScript

在 HTML 中,如果一个 a 标签,带上了 download 的属性,链接地址就会被浏览器直接用于下载。使用方法如下:

<a href="link_here" download="filename.suffix">Link</a>

同样,如果需要 JavaScript 能够直接触发一个资源的下载,可以创建带 download 属性的 a 标签,然后调用这个元素的 click 方法。

const a = document.createDocument('a');
a.href = 'link_here';
a.download = 'filename';
document.body.appendChild(a);
a.click(); // trigger download
document.body.removeChild(a);

download 的支持情况见这里

这个方案有一个问题:如果是跨域的资源,直接这样的 a 标签点击是不能调用下载的(因为执行了严格的同源策略),行为上就会和一个普通的导航没有区别(比如,增加 target=_blank 之后就会打开一个新窗口展示资源)。

解决跨域的一个前端方案是:fetch 资源,然后将结果转化成 Blob,然后将这个 Blob 生成一个 URL。代码如下:

fetch('link_here')
  .then(repsonse => response.blob())
  .then(blob => URL.createObjectURL(blob))
  .then((link) => {
    const a = document.createElement('a');
    a.href = link;
    a.download = 'filename.here';
    document.body.appendChild();
    a.click();
    document.body.removeChild();
  });

Promise.allSettled🔗

JavaScript

Promise.allSettled 已经在 Chrome 76 中上线了。

一个简单的例子:

const promises = [
  Promise.resolve('fulfilled'),
  Promise.reject('rejected'),
];

Promise.allSettled(promises)
  .then((result) => {
    /**
     * output:
     * [
     *   { status: 'fulfilled', value: 'fulfilled' },
     *   { status: 'rejected', reason: 'rejected' },
     * ]
     */
    console.log(result);
  });

只有所有数组中的 Promise 的结果不再是 pending.allSettled 才会返回结果。

.all.race 两个 API 最大的区别在于,.allSettled 不会提前结束。.all 会在任意一个 Promise reject 的时候失败,而 .race 则会在任意一个 Promise fulfilled 的时候成功。.allSettled 会等到所有结果都出来之后,再如实返回(以 fulfilled 的状态)。

需要注意的是,返回的结果是一个数组,其中的每一个元素都是一个对象。其中,每个对象都有 status 的字段,表示对应的 Promise 最终的结果是 fulfilled 还是 rejected。如果是 fulfilled 状态,那么对象会有 value 字段,值相当于 .then 回调中的第一个参数;如果是 rejected 状态,那么对象会有 reason 字段,值相当于 .catch 回调中的第一个参数。


debuglog in node.js🔗

Node.js

在 Node.js 中,utils 提供了 debuglog 模块可以用于调试信息的输出。默认情况下,debuglog 的内容是不会输出的,只有当 NODE_DEBUG 这个环境变量设置了合适的值,输出才会显示。这样,调试信息不会影响正常的使用,同时也依然保留了调试的需求。

具体的使用如下:

const debuglog = require('util').debuglog('name');

debuglog('hello world: [%d]', 1);

上面的例子中,如果直接运行,是不会包含 hello world: 1 的输出的。如果设置 NODE_DEBUGname(也就是 debuglog 函数调用时设置的值),那么再次运行,hello world: 1 就会输出了。

同时,可以用逗号分隔的方式一次性为 NODE_DEBUG 设置多个值,比如:NODE_DEBUG=foo,bar

在 Mac 下,一次性设置 NODE_DEBUG 可以输入:

NODE_DEBUG=name node ./index.js

在 Windows 下(Powershell),设置 NODE_DEBUG 可以输入:

$env:NODE_DEBUG="name"; node ./index.js

设置完成之后,NODE_DEBUG 会一直保留着,直到 powershell 被关闭。需要删掉原先设置的 NODE_DEBUG,可以输入:

Remove-Item env:\NODE_DEBUG

custom display time of notification🔗

JavaScript

浏览器显示 Notification 默认是有一个自动消失时间的。不同的浏览器,这里的消失时间并不一致,从测试来看:

  • Chrome: ~6s
  • Firefox: ~19s
  • Edge: ~6s

从目前浏览器公开的 API 来看,并没有一个接口可以直观的修改这里的消失时间。一个可行的解决方案是:用 requireInteraction 来强制要求浏览器不自动关闭 Notification,然后设置 setTimeout 并在合适的时机手动关闭这个显示的 Notification。

示例代码如下:

var delay = 10 * 1000;
Notification.requestPermission(function (status) {
  if (status === "granted") {
    var notification = new Notification(
      "Hi! ",
      {
        requireInteraction: true,
      },
    );
    var timer = setTimeout(function () {
      notification.close();
    }, delay);
  }
});

目前 requireInteraction 的浏览器支持情况并不非常理想,只有 Chrome, Edge(17+) 和 Opera 做了支持。具体的支持列表,可以看这里

另外,从实际的使用上来看,Edge 浏览器中即使设置了 requireInteraction,notification 在一定时间之后也会消失,只是消失的时间会比原来默认的情况要长一些,大约是 25 秒。Chrome 的 Notification 如果设置了 requireInteraction,会多一个 Close 的按钮,展示效果和没有 requireInteraction 的情况有所不同。


context menu of electron🔗

Electron

Electron 默认是没有右键支持的,右键点击也不会有效果。为了能够提供一些右键的行为,需要在合适的时间点,手动构造菜单并显示出来。

这里对右键点击的判断,如果放在 render 层用 JavaScript 去监听 contextmenu 事件,虽然可以从 event.target 上拿到元素,但是要判断当前选择的位置、能否选择/黏贴、是否有拼写错误的单词等,都比较困难,很容易写出问题来。

Electron 暴露了 Chromium 的数据,在 WebContents 中增加了 context-menu 的事件。在这个事件的回调函数中,提供了很多的数据,能够帮助更好的了解当前的右键点击状态,从而更好地显示右键菜单项。

context-menu 的文档见这里

这个功能提交的 Pull Request 见 #5379

context-menu 的回调函数中,第二个参数提供了非常多有用的数据,比如:

  • selectionText - 选中的文字
  • misspelledWord - 当前的拼写错误单词(如果没有拼写错误,这里的返回是空字符串)
  • editFlags - 包含了 canCut, canCopy, canPaste, canSelectAll 等各式布尔值,用于表示当前右键的位置是否允许剪切/复制/黏贴/全选等操作。完整的列表可以参考文档

一个例子:

const { remote, Menu } = require('electron');

const webContents = remote.getCurrentWebContents();

function buildMenuFromSuggestions(suggestions) {
  if (suggestions.length === 0) return [];
  return suggestions.map(function (suggestion) {
    return {
      label: suggestion,
      click: function () {
        webContents.replaceMisspelling(suggestion);
      },
    };
  }).concat([
    { type: 'separator' },
  ]);
}

webContents.on('context-menu', (event, info) => {
  const { canCut, canCopy, canPaste, canSelectAll } = info.editFlags;
  const { misspelledWord } = info;

  // use your own function of `getCorrections`
  const suggestions = getCorrections(misspelledWord);

  const menuConfig = buildMenuFromSuggestions(suggestions)
    .concat([
      { label: 'Cut', role: 'cut', enabled: canCut },
      { label: 'Copy', role: 'copy', enabled: canCopy },
      { label: 'Paste', role: 'paste', enabled: canPaste },
      {
        label: 'Select All',
        enabled: canSelectAll,
        // role: 'selectAll'
        // following shows an example of how to manually call the API
        click: webContents.selectAll,
      },
    ]);
  
  const menu = remote.Menu.buildFromTemplate(menuConfig);
  menu.popup(remote.getCurrentWindow());
});

contenteditable style🔗

JavaScript

style 本身是一个标准的 HTML 标签,在里面写的 CSS 样式,会被应用到页面上。同时,作为一个 HTML 标签,style 本身也可以被赋予一定的展示样式(比如将默认的 style { display: none; } 给覆盖掉)。加上 contenteditable 的属性,就会得到一个可编写的 style 标签。通过直接编写其中的 CSS 样式,页面会自动更新,展示应用样式后的效果。

上面展示的这个圆点,鼠标悬停之后,就会显示一个可输入的框。在里面输入一些 CSS 可以看到对页面元素的修改。比如,可以试试输入:

article small:nth-child(3) { color: #007acc; }

几点注意:

  1. 直接复制上面的 CSS 然后黏贴不会起效,因为样式也被黏贴到 style 里面去了,这会导致 style 里的内容不是合法的 CSS,无法应用样式
  2. CSS 需要写在一行里面,回车会导致插入 <br />,同样会导致 CSS 语法错误,无法应用样式

css backdrop filter🔗

CSS

传统的 CSS filter,可以对当前的元素应用指定的滤镜。以模糊(blur)滤镜为例,常常会被拿来实现毛玻璃的效果。然而,因为滤镜只能应用于元素自身,所以毛玻璃的效果也是局限性很大的。一个常见的做法是,背景图片在当前元素中用 background-image 的方式再赋值一次,然后通过定位对齐,再加上 blur 的效果。这样看上去,中间一块的图片就好像有了模糊的效果。

一个例子:

HTML 结构是:

<div class="container">
  <div class="filter"></div>
</div>

CSS 是:

.container {
  width: 620px;
  height: 414px;
  background-image: url("../../baseline-jpeg-demo.jpeg");
  background-size: 620px 414px;
  position: relative;
}
.filter {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 310px;
  height: 207px;

  background-image: url("../../baseline-jpeg-demo.jpeg");
  background-size: 620px 414px;
  background-position: -155px -103.5px;
  filter: sepia() hue-rotate(120deg);
}
.filter:hover {
  filter: blur(10px);
}

然而,显然这样的局限性是很大的。css backdrop filter 就是为了打破这种局限性。有了 css backdrop filter,当前元素的滤镜会加到当前元素下面的所有元素上,而不仅仅是自身的元素。

上面例子的改写(注意:当前浏览器不支持 backdrop-filter 功能)

HTML 保持不变,CSS 改动为:

.filter {
  -webkit-backdrop-filter: sepia() hue-rotate(120deg);
}
.filter:hover {
  -webkit-backdrop-filter: blur(10px);
}

可以看到,需要加 filter 的部分,没有做额外特殊的处理(比如背景图片的配适),就可以直接使用。简洁明了。

另外,鼠标悬停之后可以看到模糊效果的展示。使用 filter 和使用 backdrop-filter 的展示效果也是略有不同的。主要是,用 filter 这种方案,背后还是有图片的,所以当前景图片模糊之后,边缘部分,后面背景的图片会显示出来,效果有折扣。(如果需要处理,简单的做法是,加大 blur 元素的宽高,然后用 overflow:hidden 把整体显示出来的大小限定回原来需要的大小,这样边缘部分相当于被裁剪了)

当然,css backdrop filter 目前的支持还非常有限。除了 Safari 和 Edge,基本没有浏览器支持。具体可以看 Caniuse

在 Electron 中,可以通过下面的方法让打开 backdrop-filter 的支持:

new BrowserWindow({
  // ...
  webPreferences: {
    enableBlinkFeatures: 'CSSBackdropFilter',
  },
  // ...
});

Chrome 对 backdrop filter 的支持进展可以看这个 Issue


restore source map🔗

JavaScript

如果拿到了一份带有 source map 的 JavaScript 代码,那么理论上就可以通过这份 source map 去尽可能的还原出原始的文件内容。

首先,source map 本质上是一个 JSON 文件。在其中,sourceContent 数组就记录了所有源文件的纯文本内容,而这些文件的文件路径及文件名则存放在了 sources 数组中。两者相互对应,理论上来说参照这两者的数据,就可以将源文件还原到原始的目录下。

然而,Webpack 的打包结果,文件的路径名称都带上了 webpack:/// 的前缀。在实际处理的过程中,可以直接使用已有的库,比如 restore-source-tree

这个库因为已经比较老了,对 Webpack 3/4 等新版本的支持存在问题。在原库合并 PR 之前,可以先使用改进过的版本 restore-source-tree

这个修改过的版本,除了修复对新版 Webpack 编译结果的支持外,也加入了 glob 的支持,可以更方便的进行批量 source map 还原。

参考代码如下:

restore-source-tree -o output_folder path/to/source-maps/*.map

最终生成的文件会存放在 output_folder 下。


webpack dynamic import🔗

Configuration

使用 import 函数引入的代码,在 Webpack 中会被编译到一个异步模块中。import 函数返回一个 Promise,会在异步模块 加载完毕后 resolve。

Webpack 允许在调用 import 的时候加入注释来对异步加载进行配置。最常见的是指定新文件的 chunk name:

import('module-path-here' /* webpackChunkName: "name" */);

如果在 Webpack 的配置中指定了输出的文件名格式,比如 [name].js,那么最终输出的结果的文件名就会是 name.js

当然,import 输入的参数字符串可以不是一个固定值。比如:

import('module-parent-path' + moduleName);

在这种情况下,Webpack 会尝试将 module-parent-path 下所有的文件都各自打包成一个异步模块。在这种情况下,显然 webpackChunkName 没法直接写死一个字符串了。Webpack 提供了 indexrequest 两个参数,可以用于动态生成的这些异步加载模块的命名。比如:

import(
  'module-parent-path' + moduleName
  /* webpackChunkName: "name-[index]-[request]" */
);

其中,index 表示当前引用的文件的序号,request 则表示当前引用的模块中动态的部分。举例俩说,上面这里如果 moduleNameexample,且配置生成的文件名是 [name].js,那么最终这个模块的文件名就是 name-0-example.js

当然,一次性将 module-parent-path 下所有的文件都打包成独立的异步模块可能会太多了,Webpack 提供了一些裁剪的方案:

  • webpackInclude,允许配置一个正则表达式,匹配的部分才打包成异步模块,忽略其他的
  • webpackExclude,允许配置一个正则表达式,匹配的部分会被忽略,打包其他剩下的模块
  • webpackMode,默认的模式是 lazy,每一个文件都会打包成一个异步模块;lazy-once 则会要求 Webpack 将所有的文件打包到一个模块中;eager 会把模块打包到当前的 chunk 中,但是不执行,等到真正执行了 import 命令之后,才执行里面的代码(省去了网络请求),依然返回的是 promise;weak 不会产生网络请求,默认模块会由其他途径加载完成,如果其他途径没有事先加载过,那么此处调用就会造成 promise 的 reject。

如果有多个配置,可以叠加写在一起。一个例子:

import(
  'module-parent-path' + moduleName
  /* webpackChunkName: "name-[index]-[request]" */
  /* webpackInclude: /include\.js$/ */
  /* webpackExclude: /exclude\.js$/ */
  /* webpackMode: "lazy" */
);

除了上述之外,Webpack 还支持一些模块加载相关的配置,比如:

import('module-path-here' /* webpackPrefetch: true */);

可以指定当前的异步加载模块需要 prefetch 的支持。运行时,Webpack 会向 head 中插入一个 <link rel=prefetch />

import('module-path-here' /* webpackPreload: true */);

可以指定当前的异步加载模块需要 preload 的支持。运行时,Webpack 会向 head 中插入一个 <link rel=preload />

一个例子:

import(
  /* webpackPreload: true */
  /* webpackChunkName: "name" */
  'module-path-here'
);

注意到,这里配置注释写在前面还是写在后面都是不影响的。

参考


use case of switch🔗

JavaScript

在《JavaScript: The Good Parts》里,作者并不赞成 switch 语句的使用(主要是因为 fall-through 的情况很容易造成错误)。然而在实际的代码里,还是有不少地方可以看到 switch 的使用。目的各不相同,有不少可以借鉴的地方。

默认值设置

React 的 Scheduler 中,有这样一段代码:

switch (priorityLevel) {
  case ImmediatePriority:
  case UserBlockingPriority:
  case NormalPriority:
  case LowPriority:
  case IdlePriority:
    break;
  default:
    priorityLevel = NormalPriority;
}

不失为设置默认值的一种写法,看上去比使用 if 来得更明确一些:

if (
  priorityLevel !== ImmediatePriority &&
  priorityLevel !== UserBlockingPriority &&
  priorityLevel !== NormalPriority &&
  priorityLevel !== LowPriority &&
  priorityLevel !== IdlePriority
) {
  priorityLevel = NormalPriority;
}

防止代码篡改的判定

上面的需求,也很容易写成下面这种数组的方案:

const allowedValues = [
  ImmediatePriority,
  UserBlockingPriority,
  NormalPriority,
  LowPriority,
  IdlePriority,
];
const isNot = value => comparedTo => value !== comparedTo;
if (allowedValues.every(isNot(priorityLevel))) {
  priorityLevel = NormalPriority;
}

然而,这样的代码方式,可能存在被入侵的危险。不论是上面例子中的 every 函数,还是用 Array.prototype 上的任意函数,都有被篡改的可能性。如果其他地方的代码修改了 Array.prototype.every 的行为,让这里的返回值发生了变化,那么代码最终就会产生意料之外的行为。

在 Scheduler 中当然不需要考虑这个问题,但是在其他的应用场景下,这可能是不得不考虑的问题。举例来说,如果一个 Web 应用允许第三方脚本的运行,同时自身有对数据进行白名单检查的需求,那么就只能使用 switch 硬编码所有的情况,而不能使用数组或者对象,否则第三方的脚本有可能对最终的行为做篡改。

Microsoft Teams 的代码里,就有类似的应用场景(见 extracted/lib/renderer/preload_sandbox.js):

const isChannelAllowed = (channel) => {
  // ...
  let isAllowed = false;
  // IMPORTANT - the allowList must be a hardcorded switch statement.
  // Array and object methods can be overridden and forced to return true.
  switch (channel) {
    case xxx:
    // ...
    case zzz:
      isAllowed = true;
      break;
    default:
      isAllowed = false;
      break;
  }
}