2019年4月19日星期五

如何重置 macOS 的蓝牙模块

问题现象

我买了新的 Bose QC35 II 主动降噪耳机,买的原因是我老的 QC25 不支持蓝牙且磨损到一定程度了。新耳机到手后,跟 iPhone 配对没问题,但跟我的一台 MacBook Pro 就是无论如何也配对不上。通过跟另外一台 MacBook Pro 配对,证明问题发生在这台 MacBook Pro,而 Bose QC35 本身没问题。

无法配对的具体现象是,在 macOS 设置的蓝牙面板中无论如何也搜索不到 Bose QC35。(而且也搜索不到周边的其他蓝牙设备。但 Apple 自己的蓝牙键盘鼠标之前能正常配对,因为那是通过 Lightning 线配对的。)我尝试过重启 macOS,但依然搜索不到任何蓝牙设备。

解决方案

解决方案就是重置 macOS 的蓝牙。我搜索后发现,老的 macOS(准确来说还是 Mac OS X)需要删除 Bluetooth.plist 来重置,新的 macOS 已经不用那么麻烦。同时按住 Shift 和 Option,然后点击菜单栏上的蓝牙图标,这时候下拉菜单就会显示更多信息,包括一个「Debug」子菜单。打开「Debug」子菜单,然后选择「Reset the Bluetooth module」,并且确认真的要重置,最后重启 macOS,问题就解决了。之后我能在蓝牙面板中搜索到 Bose QC35,配对后播放音乐没有任何问题。

2019年2月26日星期二

把我的个人网站推倒重来(Part 8 - Sitemap)

为了让 Google 更好地发现和索引我的网站,我会使用 Google Search Console 提交 Sitemap。Sitemap 本质上是一个 XML 文件,描述网站包含的网页信息,帮助 Google 了解网站上都有哪些网页,以及这些网页的重要程度和更新频率。(Google 会参考这些信息,但我们不能通过 Sitemap 直接控制 Google 爬虫或排名。)我上一版的个人网站就包含了 Sitemap,所以重新新版本时也会把 Sitemap 加上。

上一个版本的 Sitemap 是我手工维护的 XML 文件,既然新版本的网站使用 Harp 进行编译,我希望 Sitemap 的 XML 文件也是编译时自动生成的。Harp 要使用模板生成 XML 文件很简单,把文件的后缀写成 .jade.xml 就可以了,Harp 从文件名可以推断出这个文件需要使用 Jade 模板来编译,结果保存为 XML 文件。我希望这个文件尽可能不需要手工维护,最好自己根据语言和页面的组合生成所有有效页面的链接。

每一个有效页面背后都有一个同路径的 .jade 文件,例如说 /en/resume 背后存在对应的 /en/resume.jade,这个文件会生成 /en/resume.html,然后 Netlify 会美化 URL 删掉不必要的 .html 后缀。如果我需要找出所有的有效页面,其中一个方法就是遍历项目目录,找出所有这些 .jade 文件,然后把它们的路径映射过去。因为遍历所有目录需要用到递归,我就写了一个 mixin 来做这个事情,原因是 Jade 里面的 mixin 类似于函数,可以自己调用自己从而实现递归。

mixin tree(current, path)
  for value, key in current
    if key === '_contents'
      for file in value
        if /\.html$/.test(file) && !/^\d{3}\.html$/.test(file)
          url
            loc
              | https://catchen.me#{path}#{file.replace(/(index)?\.html$/, '')}
            changefreq
              | hourly
            priority
              if path === '/' && file === 'index.html'
                | 1.0
              else
                | #{public._data[file.replace(/\.html$/, '')].priority}
    else if !/^[._]/.test(key)
      mixin tree(value, path + key + '/')

这个 mixin 接收两个参数,第一个是代表当前目录的对象,第二个是当前目录所在路径。在递归的入口,我会调用 mixin tree(public, '/'),把代表根目录的 public 对象和对应的路径字符串 '/' 传进去,然后这个 mixin 就会遍历所有子目录把所有编译后的 .html 文件找出来。它找到的每一个 .html 文件都会被添加到 Sitemap 里面。

这个 mixin 在把 .html 文件映射为 URL 时,它还做了以下的特殊处理:

  1. 删除 .html 后缀,因为 Netlify 在美化 URL 时也会进行这个操作。如果我们在浏览器中打开 https://catchen.me/zh/resume.html,Netlify 会返回 301 重定向到 https://catchen.me/zh/resume,保证用户看到的是不包含 .html 的路径。
  2. 删除 index 默认文件名,因为 https://catchen.me/zh/index.htmlhttps://catchen.me/zh/ 等价,同样会被 URL 美化删除。
  3. 过滤掉 404.html 等状态码特殊页面,因为这个页面是专门给 Netlify 返回对应状态码时使用的,不应该被索引。

最终的 sitemap.jade.xml 其实并不复杂,记得加上 doctype xmlurlset(xmlns="http://www.sitemaps.org/schemas/sitemap/0.9") 这类必要的元素就行了。编译后立即可以发布到 Netlify,然后在 Google Search Console 进行手工添加 Sitemap。数天后 Google Search Console 就会显示这个 Sitemap 已经被抓取了,然后还能看到 Sitemap 中页面的抓取情况。

这有可能是这个系列的最后一篇文章了,将来如果我对我的个人网站继续有更新,我会继续写这个系列。如果你对我的博客感兴趣的话,欢迎通过邮件RSS/Atom 进行订阅。如果你对这个话题有什么问题或者观点的话,欢迎对本文发表评论。

2019年1月17日星期四

把我的个人网站推倒重来(Part 7 - Google Analytics & Facebook Pixel)

网站上线之后,我自然关心访客的数量和来源,于是我决定加上 Google Analytics。同时纯粹出于好奇,我把 Facebook Pixel 也加上了。

Google Analytics 和 Facebook Pixel 都需要插入 JavaScript 到每一个页面上,因此把代码加到 _layout.jade 是最合适的,因为这是所有页面共享的模板。

Google Analytics

在 Google Analytics 创建好「property」后,复制 Google Analytics 生成的代码到 _layout.jade 其实就完事了。(Google 把独立的每一个网站和 app 叫做「property」,这样使得一个 Google 帐号可以管理多个网站和 app。)

因为我想知道我的网站是否有 JavaScript 出错,所以我特意通过 window.onerrorwindow.onunhandledrejection 事件把错误信息上报到 Google Analytics。如果 Google Analytics 在创建 property 时选择的是网站而不是 app,默认是不会有报告显示 exception 事件的。这时候我们需要手工在 Google Analytics 添加一份自定义报告,然后选择显示 exception 事件,并把 exception message 展示出来。这样我就能看到是否存在 JavaScript 错误,以及具体在哪个页面发生什么错误了。

Facebook Pixel

Facebook Pixel 跟 Google Analytics 类似,在 Facebook 上配置后然后复制粘贴代码就可以了。需要注意的是,一个 Facebook 广告帐号只能创建一个 Pixel,不像 Google Analytics 那样可以创建多个 property。如果需要跟踪多个网站,那就需要创建多个广告帐号,或者一个 Pixel 用不同的事件来跟踪不同的网站。

在配置好 Facebook Pixel 并运行一段事件后,我发现 Facebook Pixel 并不能提供 Google Analytics 那样复杂的分析功能,更多是为了跟踪广告效果和生成广告受众群体。例如说,如果我在我的博客上使用 Facebook Pixel,然后我就能生成一个访问我博客的受众群体,然后在 Facebook 上面向这些受众投放广告让他们来赞我的专页。除此之外,我没有去深入研究 Facebook Pixel 还能做什么别的事情。(如果你知道 Facebook Pixel 还能做什么别的事情,欢迎在评论区讨论。)

搞掂 Google Analytics 和 Facebook Pixel 后,我还想做的是支持 Sitemaps 好让 Google 抓取所有页面。如果你对此感兴趣的话,欢迎通过邮件RSS/Atom 订阅我的博客,关注接下来的文章。

2018年11月23日星期五

把我的个人网站推倒重来(Part 6 - hreflang)

因为我在上一篇文章讲 Open Graph 元数据时提到 hreflang,我可以用这篇文章简单讲一讲如何支持 herflang。使用 hreflang 好处的是让 Google 知道多个页面其实是同一内容的不同语言版本,这样在用户搜索时 Google 就可以尽量提供正确的语言版本。

Google 官方对 hreflang 提供了详尽的解释。要让网站支持 hreflang 有三种做法:HTML 标签、HTTP header 以及 Sitemap。我选择了 HTML 标签,因为在我添加 hreflang 的时候 Google 还没提供另外两种做法。如果让我现在重新选择的话,我很有可能选择使用 Sitemap 从而减少页面中对用户没有价值的字节。

我在之前的篇文章解释了我的网站是如何支持多语言的,我的 hreflang 实现同样依赖于我做的这个基于 Harp 的多语言方案。对于每一个页面 /page,我都有 /zh/page/en/page 对应其中文版和英文版。这时候根据 Google 的要求,hreflang="x-default" 应该指向 /page,然后 hreflang="zh" 应该指向 /zh/page,英文版同理。举个例子,首页的 hreflang 标签是这样子的:

<link rel="alternate" hreflang="x-default" href="https://catchen.me/">
<link rel="alternate" hreflang="en" href="https://catchen.me/en/">
<link rel="alternate" hreflang="zh" href="https://catchen.me/zh/">

为了让 Harp 生成这组标签,我在模板中先取出当前页面语言无关的名称(也就是 /zh/page 中的 page),然后以此生成 x-default 的标签。接着我再遍历网站支持的语言,逐一生成对应语言的 hreflang 标签。具体代码可以在 GitHub 上看到。因为我让 Harp 遍历网站支持的所有语言,将来如果我添加了新的语言,只要让 Harp 重新便宜网站新的语言便会出现,不需要我做任何的手工修改。

hreflang 就这么简单的搞掂了。接下来让我们对网站加上 Google Analytics 和 Facebook Pixel,好让我们统计网站的访问来源。如果你想要继续关注这个系列的话,欢迎通过邮件RSS/Atom 订阅我的博客。

2018年11月17日星期六

把我的个人网站推倒重来(Part 5 - Open Graph 元数据)

网站发布之后我开始做各种细小的优化,其中一项是为网站加上 Open Graph 元数据( metadata),使得网站在被 Facebook 抓取时能够显示正确的预览信息。(Google 在抓取时也会参考 Open Graph 元数据,虽然 Open Graph 是 Facebook 提出的标准。)

Open Graph 标准

Open Graph 标准本身并不复杂,看着官方的标准信息把每一项相关的属性都加上。以下是我个人网站上使用的 Open Graph 信息:

<meta property="og:type" content="profile">
<meta property="og:title" content="Cat Chen">
<meta property="og:description" content="Cat Chen's personal website.">
<meta property="og:url" content="https://catchen.me/">
<meta property="og:image" content="https://catchen.me/images/profile_picture_360_360_1x.jpg">

Facebook 专门提供了一个工具来验证网页上的 Open Graph 元数据。在把元数据添加好之后,我用这个工具测试了一下 Facebook 抓取到的数据。

Screenshot 2018-11-10 16.35.56

Open Graph 文本属性其实随便怎么填都可以,URL 属性需要保证是正确的地址。以下是两个常用的 URL 属性:

og:image 属性

这个属性用来设置 Facebook 预览时显示的图片。我选择了用来显示我的头像,因为这也是我显示在网站首页左侧的图片。

og:url 属性

这个属性用来指定所谓的「Canonical URL」,我为这个属性折腾了很久。这是因为 Facebook 把 Canonical URL 看作重定向,如果一个 URL1 返回的网页使用 og:url 属性指向另一个 URL2,在 Facebook 看来这根 URL1 返回 302 重定向到 URL2 一样。这导致 Facebook 在抓取我的页面时陷入了无限重定向循环。

要解释这件事情,首先要解释我在 Netlify 上做了什么重定向配置,这是我在上一篇文章中没有提到的。我在之前一篇文章中说了,我的网站支持多语言(暂时只有英文和中文),不同语言使用不同的目录,例如说中文版在 /zh/* 英文版在 /en/*。这造成了一个问题,网站首页 / 应该显示中文版还是英文版?为此我让 Netlify 根据用户浏览器提供的 accept-language header 来选择正确的语言。(如果这个 header 不存在则默认为英文。)这是当时的 netlify.toml 配置文件。

如果只有这一种重定向,无限循环并不出现。无限循环之所以出现,是因为我把 /zh//en/ 的 Canonical URL 都指向了 /。为什么要指向 / 呢?因为我认为中文版和英文版都只是特定语言的版本而已,只有未指定语言的 / 才有资格叫做 Canonical URL。

最终我撤销了第一种重定向(302),保留了第二种重定向(Canonical URL)。撤销第一种重定向后,我把 / 改了了内容代理,也就是说如果浏览器的 accept-language header 选择了中文,那 / 显示的就是中文版,内容跟 /zh/ 完全一样但不改变 URL。这使得我的网站被分享到 Facebook 时指向的都是未指定语言的 URL,每一个用户实际打开时看到的语言都由他浏览器的设置来决定。我觉得这是我能做到的对用户最便利的选择。

这是我修改后的 netlify.toml。在 Netlify 的配置中使用 200 重定向意思就是代理目标页面内容——用户看不到重定向的发生,但实际返回的内容时目标页面的内容。做了这一件事情后,接下来要支持 hreflang 就很方便了,我在下一篇文章里就会讲述 hreflang 的设置。如果你对此感兴趣的话,欢迎通过邮件RSS/Atom 订阅我的博客。

2018年11月7日星期三

NPM 打包时该忽略哪些文件?

最近在写一个新的 JavaScript 库,叫做 dice-chance,用来分析掷骰子的概率。计划是库写完了就用 PWA 封装一下发布给大家用。因为在写的时候用到了 Flow 做类型声明,所以源代码文件不能不经处理直接被调用,必须经过 flow-remove-types 处理一下删除 Flow 类型声明。

为了保证在包发布时 Flow 类型会被删除掉,我在 package.json 中定义了 build 脚本,然后设置了 prepublish 事件触发 build 脚本:

"scripts": {
  "build": "flow-remove-types src/ -d lib/",
  "prepublish": "yarn run build"
},

奇怪的是,在执行 npm publish 时我明明看到了 build 脚本被触发了但打包时却没有引入 lib 目录。这样打包出来的库不能用,因为 index.js 里面引用的文件都来自于 lib 目录而非 src 目录。打包时的输出时这样子的:

$ npm publish --dry-run

> dice-chance@2.0.1 prepublish .
> npm run build

> dice-chance@2.0.1 build .
> flow-remove-types src/ -d lib/

src/Analyzer.js
 ↳ lib/Analyzer.js
src/DiceChance.js
 ↳ lib/DiceChance.js
src/Parser.js
 ↳ lib/Parser.js
src/Tokens.js
 ↳ lib/Tokens.js
src/__tests__/Analyzer-test.js
 ↳ lib/__tests__/Analyzer-test.js
src/__tests__/DiceChance-test.js
 ↳ lib/__tests__/DiceChance-test.js
src/__tests__/Parser-test.js
 ↳ lib/__tests__/Parser-test.js
npm notice
npm notice 📦  dice-chance@2.0.1
npm notice === Tarball Contents ===
npm notice 1.1kB   package.json
npm notice 48B     .babelrc
npm notice 58B     .flowconfig
npm notice 232B    .travis.yml
npm notice 48B     index.js
npm notice 1.1kB   LICENSE
npm notice 1.5kB   README.md
npm notice 112.4kB yarn.lock
npm notice 6.1kB   src/__tests__/Analyzer-test.js
npm notice 2.8kB   src/__tests__/DiceChance-test.js
npm notice 2.6kB   src/__tests__/Parser-test.js
npm notice 2.0kB   src/Analyzer.js
npm notice 1.4kB   src/DiceChance.js
npm notice 1.3kB   src/Parser.js
npm notice 949B    src/Tokens.js
npm notice === Tarball Details ===
npm notice name:          dice-chance
npm notice version:       2.0.1
npm notice package size:  37.7 kB
npm notice unpacked size: 133.7 kB
npm notice shasum:        5d7c0aca59b63aef43e32885fd7d254676a6db8f
npm notice integrity:     sha512-4kzv5srVKgrYJ[...]ynoZKx48wAKfw==
npm notice total files:   15
npm notice
+ dice-chance@2.0.1

这到底是为什么呢?一番搜索后我才发现,NPM 默认会用 .gitignore 来决定打包时忽略哪些文件。因为 lib 是构建的产物,不应该属于源代码的一部分,所以我用 .gitignore 文件把 lib 目录忽略掉了。NPM 因此在打包时也把 lib 忽略掉了,但其实 lib 必须被打包,而 src 反而可以被忽略掉(因为 src 中的源文件不会被 index.js 引用。

为了解决这个问题,我需要引入 .npmignore 文件。这个文件的格式跟 .gitignore 的格式一致,只要这个文件存在 NPM 打包时就用它来决定忽略什么,不再理会 .gitignore。我把 .gitignore 先复制为 .npmignore,再把里面的 lib 替换成 src,然后打包就再也没有问题了。以下是正确的打包输出:

$ npm publish --dry-run

> dice-chance@2.0.1 prepublish .
> yarn run build

yarn run v1.12.1
$ flow-remove-types src/ -d lib/
src/Analyzer.js
 ↳ lib/Analyzer.js
src/DiceChance.js
 ↳ lib/DiceChance.js
src/Parser.js
 ↳ lib/Parser.js
src/Tokens.js
 ↳ lib/Tokens.js
src/__tests__/Analyzer-test.js
 ↳ lib/__tests__/Analyzer-test.js
src/__tests__/DiceChance-test.js
 ↳ lib/__tests__/DiceChance-test.js
src/__tests__/Parser-test.js
 ↳ lib/__tests__/Parser-test.js
✨  Done in 0.26s.
npm notice
npm notice 📦  dice-chance@2.0.1
npm notice === Tarball Contents ===
npm notice 1.1kB   package.json
npm notice 48B     .babelrc
npm notice 58B     .flowconfig
npm notice 232B    .travis.yml
npm notice 48B     index.js
npm notice 1.1kB   LICENSE
npm notice 1.5kB   README.md
npm notice 112.4kB yarn.lock
npm notice 6.1kB   lib/__tests__/Analyzer-test.js
npm notice 2.8kB   lib/__tests__/DiceChance-test.js
npm notice 2.6kB   lib/__tests__/Parser-test.js
npm notice 2.0kB   lib/Analyzer.js
npm notice 1.4kB   lib/DiceChance.js
npm notice 1.3kB   lib/Parser.js
npm notice 949B    lib/Tokens.js
npm notice === Tarball Details ===
npm notice name:          dice-chance
npm notice version:       2.0.1
npm notice package size:  37.6 kB
npm notice unpacked size: 133.7 kB
npm notice shasum:        218703ab30ffad27bc76c1c9a6a2852838e0fb58
npm notice integrity:     sha512-gammoNvPgDcyd[...]BpngI1zA2X7dg==
npm notice total files:   15
npm notice
+ dice-chance@2.0.1

2018年10月7日星期日

把我的个人网站推倒重来(Part 4 - Responsive Image)

网站整体完成后,我就可以开始做各种小优化了。其中一个优化是使用 responsive image 来适应不同分辨率和不同像素密度的屏幕,用到的是 <img /> 新增的 srcsetsizes 属性以及新增的 <picture /> 元素。因为现在有多套新旧并存的 responsive image 方案,而且它们使用的属性存在重叠,所以要搞清楚到底这些属性如何运作,还是要动手实验。

sizes 属性

<img srcset="elva-fairy-320w.jpg 320w,
             elva-fairy-480w.jpg 480w,
             elva-fairy-800w.jpg 800w"
     sizes="(max-width: 320px) 280px,
            (max-width: 480px) 440px,
            800px"
     src="elva-fairy-800w.jpg" alt="Elva dressed as a fairy">

这是一段来自 MDN 的代码。虽然大家都是先写 srcset 再写 sizes,但其实更符合直觉的阅读顺序是先读 sizessizes 的值是一组类似 media query 的命令,它们描述了在什么情况下这个 <img /> 应该有多宽。拿上面这段代码举例:如果屏幕宽度是 320px 或以下,图片宽度为 280px;如果屏幕宽度是 480px 或以下(但 320px 以上),图片宽度为 440px;其它屏幕宽度,图片宽度默认为 800px。

这一切看起来都很简单,但这是因为我们只在讨论分辨率没在讨论像素密度。如果是 2 倍像素密度的 Retina Display,上述图片宽度计算是否保持不变?答案是跟 media query 一样保持不变。无论像素密度是多少,sizes 关注的都是 CSS 像素而不是物理像素。我觉得这个设计是合理的,因为在描述 <img /> 宽度时,我们的思维模式跟在写 CSS 时一样,所以应该使用 CSS 像素。

尽管在 MDN 的例子中 sizes 属性的取值都是固定值,但其实这里可以使用 calc() 表达式进行复杂的计算,如我的代码中就用到了 calc(100vw - 30px),意思是 100% 的 viewport 宽度减去屏幕两侧各 15pxmargin

srcset 属性

看完 sizes 接着看 srcset。在上面这段代码里,我们看到了一个神奇的单位叫做 w,这是指代图片文件的像素宽度。文件图片的像素宽度跟 <img /> 的 CSS 像素宽度不是 1:1 对应的吗?这需要看像素密度。如果 <img /> 的宽度是 100px,像素密度是 1 时最佳图片文件宽度是 100w;像素密度是 2 时最佳图片文件宽度是 200w;像素密度是 3 时最佳文件宽度是 300w;如此类推。

在上面这段代码中,srcset 描述了 3 个图片文件地址,它们的文件图片像素宽度分别是 320w480w800w。这也就是说,如果在一个 1000px 宽 1 像素密度的屏幕上,根据 sizes 这个 <img /> 的宽度应该是 800px,因此应该选择 800w 的图片文件地址;如果在一个 480px 宽 2 像素密度的屏幕上,根据 sizes 这个 <img /> 的宽度应该是 440px,但因为像素密度为 2 所以最佳图片文件宽度是 880w,由于找不到 880w 的图片文件地址所以选用略差一档的 800w 图片文件地址。

为什么 sizessrcset 这两个属性要如此设计呢?因为在之前的标准(如 CSS media query)里,我们需要在代码中描述如何根据两个变量(屏幕宽度和像素密度)来选择正确的图片文件地址,这个过程超级复杂,看这篇文章就能理解为什么这样的标准不好用。为了解决这个问题,新的标准让我们把这两件事情分离开来:sizes 决定图片在 CSS 布局中的大小,但跟像素密度无关;srcset 提供文件图片像素宽度,浏览器会自行根据 sizes 的结果和像素密度作出最佳选择,我们根本不需要知道像素密度。

(如果 <img /> 在布局中的大小永恒不变,可以不设置 sizes 属性,然后在 srcset 中使用 x 单位描述像素密度而不使用 w 单位。这时候 2x3x 可以对应不同的图片文件地址,浏览器会作出正确的选择。之所以我不选择这样做,是因为我的 <img /> 大小本身需要 responsive,所以必须用 sizes。因为 wx 这两种单位不能在同一个 <img /> 上混合使用,所以我用了 w。)

src 属性

看完 sizessrcset 这两个属性后,最后我们看 src 属性。这是给那些看不懂前两个属性的老浏览器看的,也就是默认的图片文件地址。

<picture /> 元素

上述 <img /> 元素属性能够实现同一张图片适应不同的屏幕尺寸和像素密度,但做不到根据屏幕尺寸现实不同的图片。我的网站首页布局本身是 responsive 的:如果屏幕宽度至少有 768px,使用左右两栏布局;否则使用一栏布局。

Screenshot 2018-09-30 15.21.45

左侧栏只显示我的个人照片,所以在能够使用两栏布局时我希望显示正方形(1:1)的剪裁。同样的剪裁显示在只能显示一栏的手机屏幕上就会显得很占地方,因此我需要换个剪裁方式(3:2)减少它占用的垂直高度,把更多的首屏垂直高度留给文本信息。

Screenshot 2018-09-30 15.22.05

为了实现根据屏幕尺寸使用不同的剪裁,我必须引入 <picture /> 元素然后在里面放入 <source /><img />

<picture>
  <source media="(max-width: 799px)" srcset="elva-480w-close-portrait.jpg">
  <source media="(min-width: 800px)" srcset="elva-800w.jpg">
  <img src="elva-800w.jpg" alt="Chris standing up holding his daughter Elva">
</picture>

这也是一段来自 MDN 的代码。浏览器在碰到 <picture /> 后,就开始按顺序看里面的 <source />。每个 <source /> 元素都有 media 属性,浏览器就如同执行 media query 一样来判断这个 media 属性的值是否通过,通过了就使用这个 <source /> 来显示图片,后面的子元素都会被忽略掉。如果所有 <source /> 都无法通过 media query 检测,最后那个 <img /> 就会用来显示。(不兼容 <picture /><source /> 的老浏览器只会显示 <img />。)

由于 <source /> 支持上述 <img /> 特有的 sizessrcset 属性,所以就算是放在 <source /> 中的图片也可以用上述方式支持不同的像素密度。考虑到大多数移动浏览器都会获得及时更新,能够支持 <picture /><source />,所以我选择了把正方形的剪裁当作默认剪裁放在 <img /> 里面,而针对小屏幕的剪裁放在 <source /> 里面。老旧的桌面浏览器如果打开我的网站首页,就算什么新元素和属性都不认识,至少能够根据 img.src 显示默认图片(像素密度为 1 的正方形剪裁)。

总结一下,如果需要针对不同的屏幕尺寸显示不同的图片(尤其是剪裁不一样的),必须使用 <picture /> 配合 <source /> 来选择正确的图片。一旦选定图片后,根据屏幕尺寸和像素密度设定图片尺寸只需要用到 sizessrcset 属性。

我的图片优化不仅仅用到了首页的个人照片上,也用到了项目页面的截图上。在图片之后,我们还有很多其它东西可以优化的,欢迎通过邮件RSS/Atom 订阅我的博客,继续关注后继文章。