Things I Learned (Chrome)

Rendered Fonts🔗

• Chrome

要知道一个网页中的文本,具体是使用什么字体渲染出来的,并不是非常简单的事情,可以有以下一些方案进行尝试。

getComputedStyle

使用 window.getComputedStyle 属性获取对应元素的 font-family 字段。因为字体的设置一般是通过顶层配置,子元素继承的方式完成的,因此在大多数的元素上,并没有 font-family 设置。即使有,大概率也是通过 CSS 完成的,因此从 .style 或者 .attributeStyleMap 无法拿到需要的数据。getComputedStyle 可以获取当前元素上样式的最终计算值,因此即使字体实际来自于继承或者系统默认字体,都可以通过该 API 获取到。比如,在一个没有 CSS 设置的页面上,可以通过下面的代码知道具体使用的系统默认字体是什么:

window.getComputedStyle(document.body).fontFamily;
// MacOS (Chrome): Times
// Windows (Chrome): Microsoft YaHei

非编程的方案,可以在 Chrome DevTools 中直接找到 Elements 下的 Computed 部分,查看实际使用的 CSS 属性值。注:如果 font-family 并没有被定义过,可以勾选 All 来查看系统默认的属性值。

Screenshot of Chrome DevTools > Elements > Computed

Rendered Fonts

使用 getComputedStyle 只能得到实际使用的 CSS 属性值,有时候并不能准确表达实际真实使用的具体字体类型。

举例来说,一个 font-family 定义可能是:

body {
  font-family:
    'Rubik',
    -apple-system, 'system-ui', 'BlinkMacSystemFont', 'PingFang SC',
    'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
    'Helvetica Neue', 'Helvetica', 'Arial', 'Hiragino Sans GB',
    'Microsoft Yahei', 'WenQuanYi Micro Hei', sans-serif;
}

在如此众多的定义中,具体浏览器使用了哪一款字体,并不容易知道。这其中涉及到了大量的变量。用户使用的系统环境、字体下载情况、正在阅读的文字是中文还是英文等,这些都有可能影响到最终浏览器所选用的字体。甚至在不复杂的 font-family 设置下,最终的选用字体依然可能并不直观。举例来说,如果设置 CSS 为:font-family: system-ui,具体使用的字体并没有从设置的字段中直观的反馈出来。

Chrome DevTools 提供了一个 Rendered Fonts 功能,可以帮助开发者了解当前真实使用的字体。示意图如下:

Screenshot of Chrome DevTools > Elements > Computed > Rendered Fonts

Chrome 会将当前选中元素真实使用的所有字体都列出来。需要注意两点:

  1. 需要选中一个有文字内容的元素,不然这里并不会显示;
  2. 选中的文字内容可能需要多种字体类型共同配合渲染,Chrome 会讲所有用到的元素都列举出来。

    比如,笑,😊,smile 这样一段文字,在 MacOS Chrome 下,默认就需要以下三种字体来进行渲染:

    • Times(渲染英文)
    • Songti SC(渲染中文)
    • Apple Color Emoji(渲染 emoji)

    以上三种字体在 Rendered Fonts 中都会被列举出来。

Chrome 的 Blog 介绍可以查看这里。

这一方案暂时没有 JavaScript API 可以直接调用,无法在程序运行时进行自动的判断。


DevTools of DevTools🔗

• Chrome

Google Chrome 的 DevTools 本质上也是一个由 Web 技术编写的应用,在必要的时候,可以通过以下的方式打开 DevTools 的 DevTools:

  1. 首先打开 DevTools;
  2. 选择将 DevTools 在独立窗口中打开,然后按下 Cmd + Opt + I(Mac)或 Ctrl + Shift + I(Windows)

这样,就可以打开 DevTools 的 DevTools 了。

注:这里一定要选择将最开始的 DevTools 在独立窗口打开,然后再按 Cmd + Opt + I;否则对于嵌入在页面中的 DevTools 来说,按下上面这个组合键,会将 DevTools 收回,而不是打开 DevTools 的 DevTools。

另一个稍微麻烦一些的方法是:

  1. 打开一个 DevTools;
  2. 在 Chrome 中打开:chrome://inspect;
  3. 选择 Other,就可以找到刚才打开的 DevTools 了,点击 inspect 链接,就可以打开这个指定 DevTools 的 DevTools 了。

一个可以在 DevTools 的 DevTools 中进行的操作,是查看和修改 DevTools 中记录的 snippets。对应的 API 分别是:

InspectorFrontendHost.getPreferences(
  _ => console.log(JSON.parse(_.scriptSnippets))
);

InspectorFrontendHost.setPreference(
  'scriptSnippets',
  JSON.stringify(yourSnippets)
);

关于通过代码来管理 Chrome DevTools 中的 snippets,可以参考 GitHub 上的这个讨论。

同时,Chrome DevTools 本身也是开源的,代码可以在 GitHub 上找到。


Save file in Chrome🔗

• Chrome

在 Web 环境中,一般对内容的存储都是依托于 Cookie 或是 LocalStorage 进行的(个别会使用 IndexDB)。其实,在早些时候,Web 曾推出过一个 FileSystem 的标准(已经废弃),用于将数据直接存储到本地的沙盒环境中,方便日后的使用。这个 API 目前只有 Chrome 进行了实现。

这篇文章 针对 FileSystem API 做了详细的介绍。这个 GitHub 仓库 则在 FileSystem 原生 API 的基础上,进行了二次封装。(注:第一个链接给出的文章,部分代码有误,可能无法正常运行。实际使用过程中,可以参考第二个链接给出的 GitHub 仓库中的相关代码进行调整)

假设,需要实现一个分片的文件下载功能,即文件被服务器分割成很多块,通过 JavaScript 依次下载这些内容,再在本地拼接后提交给用户。这里,考虑到文件可能非常大,如果只是存储在内存中,一旦用户刷新页面或是遇到其他问题,已经下载的内容就都失效了,只能重新再来一次。这种情况下,可以考虑使用 FileSystem API 将分片的文件内容下载后先存放在本地的沙盒文件中,等到全部下载完成之后,再将拼接好的内容提交给用户。

下面给出一个实例代码,用以介绍 FileSystem API 的可能使用方法:

/**
 * 实际中 Chrome 给出的 API 只用 window.webkitRequestFileSystem
 */
const requestFileSystem = window.requestFileSystem ||
  window.webkitRequestFileSystem;

/**
 * 下载 Link 并保存文件为 filename
 * 只是示例代码,实际的可行方案请参考 file-saver 的实现
 */
function download(link, filename) {
  const a = document.createElement('a');
  a.href = link;
  a.target = '_blank';
  a.download = filename;
  a.click();
}

function save(blob, filename) {
  function errorHandler(e) {
    console.log(e);
  }
  function handler(fs) {
    /**
     * 获取名为 filename 的文件,{ create: true } 表示如果文件不存在,就创建一个
     * fileEntry 中包含的 API 可以用于对这个文件进行操作
     */
    fs.root.getFile(filename, { create: true }, (fileEntry) => {
      fileEntry.createWriter(writer => {
        /**
         * 指定文件的写入位置在当前文件内容的末尾
         */
        writer.seek(writer.length);
        writer.onwriteend = () => {
          /**
           * FileSystem 中的文件,可以通过类似如下的 Link 获取到:
           * filesystem:https://xxx.com/persistent/filename
           * 具体的 URL 地址通过 `fileEntry.toURL()` 获取
           */
          const url = fileEntry.toURL();
          download(url, filename);
        };
        writer.onerror = console.error;
        writer.write(blob);
      }, errorHandler);
    }, errorHandler);
  }
  /**
   * 对于 PERSISTENT 存储的文件,需要事先通过浏览器询问权限
   * 声明需要使用的大小为 blob.size
   * 第二个参数是 success callback,在成功后调用,可以在这里进行文件读写
   * 第三个参数是 error callback,用于处理报错
   */
  navigator.webkitPersistentStorage.requestQuota(
    blob.size,
    grantedBytes => {
      /**
       * 以 PERSISTENT 的方式,写入 grantedBytes 这么多的内容
       * 允许写入会执行 handler,否则会执行 errorHandler
       */
      requestFileSystem(window.PERSISTENT, grantedBytes, handler, errorHandler);
    },
    console.error
  );
}

/**
 * 示例代码的调用,将 hello world 写入到 output.txt 文件中
 */
save(new Blob(['hello world'], { type: 'text/plain' }), 'output.txt')

上面这个例子,展示了如何将 Blob / File 写入到本地沙盒的文件中(例子中写入到了 output.txt 文件内)。有几点需要注意:

  1. 文件是写入到沙盒环境中的,因而虽然 fileEntry.fullPath 的值是 /output.txt,并不代表真的可以在根目录下找到 output.txt 文件
  2. window.PERSISTENT 和 window.TEMPORARY 是两种可能的存储方式。如果是 PERSISTENT 的,那么需要用户授权(也就是 requestQuota 做的事情)且清理需要程序或用户手动执行;如果是 TEMPORARY 类型的存储方式,那么浏览器可能会在某些情况下自动清理文件(比如,当空间不够的时候)
  3. 通过 fileEntry.toURL API 可以拿到当前文件存储对应的 URL 地址,进而可以通过常规手段将这个内容下载到本地
  4. 代码中的 errorHandler 函数写的比较粗糙,更丰富的 Error Handler 写法,可以参考 chromestore.js 中的代码

MyAirBridge 网站可能使用了类似上面提到的技术来存储下载中的文件内容。


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 就可以了。


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

参考文档


Clone Current Tab in Chrome🔗

• Chrome

在 Chrome 中,如果当前的光标在地址栏上,可以通过快捷键 Cmd + Enter 来打开一个和当前页面一样的新标签页(焦点依然在当前标签页上)。如果光标不在地址栏上,可以先使用 Cmd + L 来将光标移动到地址栏,然后再 Cmd + Enter。

如果改用 Option + Enter,则可以新创建同一个 URL 地址的标签页,并将焦点定位到新打开的标签页上。

如果改用 Shift + Enter,则可以在一个新窗口打开当前的 URL 地址。


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)之后,执行这个命令,就会将这个元素自身的区域截图。当然,元素所在区域内,由于排版的缘故,还存在父元素,子元素或是其他什么元素的内容。在执行截图命令的时候,这些内容也会被包含进去。