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 订阅我的博客,继续关注后继文章。

2018年9月23日星期日

把我的个人网站推倒重来(Part 3 - 用 Netlify 做静态网站发布)

之前两篇文章讲述了我用 Harp 和 Bootstrap 搭建新版个人网站的过程,执行 harp compile 进行构建,输出的 www 目录就是我们想要的静态网站。我可以找个传统的静态网站 host,然后通过 FTP 这种古老的方式把文件上传上去。然而这真是我在第一篇文章中说到的一个痛点,我不希望通过 FTP 部署,最好是好像我熟悉的 Heroku 那样通过 GitHub 触发部署,我每次本地更新后执行一下 git push 就行。

Heroku

因为我已经熟悉 Heroku,所以我想到的第一个服务自然是 Heroku。Heroku 官方提供一个静态网站 buildpack,把 www 目录扔进去就可以了。当然这样还是不够简单,最好是把源代码扔进去,然后 buildpack 自动帮我调用 harp compile。曾经有人做过这样一个 Harp 专用 buildpack,可惜现在已经不维护了。基于 Heroku 官方的静态网站 buildpack 自己写一个 Harp 专用 buildpack 也不是很难,而且还能继承静态网站 buildpack 那一堆 header、redirect 等配置功能。

然而 Heroku 的免费版有两个问题:

  1. 应用会自动休眠——一段时间没有请求后它就会让你的服务进入睡眠状态,直到有新请求时再唤醒。要避免这个问题,需要每个月花 $7 来升级到业余爱好者版。有一个方法能够绕过这个问题,那就是在 Heroku 上加载某些用来监控服务可靠性的 add-on,因为这种 add-on 会定期访问你的网站检查可靠性,相当于帮你不停地阻止休眠。
  2. 不支持自定义域名 HTTP——同样是必须每个月花 $7 升级才能解决。绕过的办法也不是没有,那就是用 Cloudflare 做负载均衡器,然后用它免费提供的 HTTPS 服务。(Hue Explorer 就是用这个方法做 HTTPS 的。)引入 Cloudflare 的代价是,我必须把 DNS nameserver 迁到 Cloudflare,不能继续用 Google Domains 自带的 nameserver。

尽管每个月 $7 也不是很贵,但因为我知道我的个人网站很有可能部署后就几年都不碰一下,还要惦记着在 Heroku 上保持有效的付款方式好像有点麻烦,而一旦忘记了就可能导致网站下线。因此 Heroku 不是一个很好的解决方案。

因为我最近读完了《Working with Static Sites: Bringing the Power of Simplicity to Modern Sites》,所以就翻开了看看书中还推荐哪些静态网站的 host,然后就找到了以下两个服务。

Surge.sh

Surge.sh 超级简单,它甚至没有一个基于 web 的应用管理界面,它只提供一个命令行工具把你指定的目录发布到 CDN 上。没错,Surge.sh 提供的是非常直白的 CDN 服务,你提供一个要发布的目录,它就保证所有文件都能通过一个特定的域名在 CDN 上访问到。没有 Heroku 那么复杂的功能,连动态服务器都没有,一切都是静态的。

我安装了 Surge.sh 的命令行工具,把我老版本的个人网站部署上去了,然后发现它存在两个问题:

  1. 不支持自定义域名 HTTP——这跟 Heroku 一样,只是付费的专业版更贵,每个月要 $30。此外 Surge.sh 不像 Heroku 那样交钱了它就自动调用 LetsEncrypt 帮你认证域名然后申请证书,它需要你手动申请证书。我希望部署后永远不需要维护的,这跟我的理想有点差距。
  2. 每次都要重复上传所有文件——习惯了 git 只上传 delta 的速度,体验了几次 Surge.sh 整个目录重新上传的速度,然后就不想再用了。我的理想还是执行 git push 就能完成发布。(git 利用 rsync 实现 delta 上传,很早就有人提议 Surge.sh 用 rsync 但至今尚未实现。)

Surge.sh 有意思的地方是,所有的配置都在你上传的文件里面了。需要指定域名?写一个 CNAME 文件。需要配置重定向?写一个 ROUTE 文件。

Netlify

Netlify 给我的第一印象就是 Heroku 和 Surge.sh 中间的平衡点。它的服务性质跟 Surge.sh 一样,为静态网站提供 host,而不像 Heroku 那样可以用来 host 动态网站。然而它的管理界面更像是 Heroku,配置能够在 web 上面完成,既能拖放上传整个目录,也能关联 GitHub 在 git push 时触发部署。

git push 部署

在设置好 GitHub 关联后,我尝试用 Netlify 自动部署我现有的代码。Heroku 有 buildpack 的概念,可以在 buildpack 中执行很复杂的事情,例如说指定依赖于 Node 平台上的 Harp 然后执行 harp compile。因为 Netlify 没有 buildpack 的概念,只有一个简单的「build command」文本框,我一开始还以为它做不了很复杂的事情。在研究了一番文档和别人的开源项目后,我发现只需要在「build command」填入 npm install -g harp && harp compile 就可以了。因为 Netlify 的构建系统内置了常见的脚本语言平台,所以不需要 buildpack 这样复杂的概念,直接就有现成的 npm 可以调用。接着「publish directory」填入 www,输出的静态文件目录就会成为服务的根目录。

在 Netilfy 上成功部署一次 GitHub 代码后,接下来的就很简单了。它跟 Heroku 一样,每次我本地修改完源代码只需要 git push 就会触发它部署。因为它的部署过程比 Heroku 的 buildpack 简单,所以部署速度非常快。如果我留意到部署没有成功,我可以打开 Netlify 看日志,看看什么导致部署失败了。很多时候是我写的 Harp 页面有问题,导致 harp compile 执行出错,后来我养成了 git commit 前先调用 harp compile 验证的习惯,也就是没问题了。

HTTPS 证书

Netlify 可以配置自定义域名,而且免费支持 HTTPS。(当然也是调用 LetsEncrypt 颁发证书。)可能因为我在 Netlify 添加域名时还在用 Cloudflare,没有正规地把域名指向 Netlify 的地址,只是把流量倒过去了,所以导致证书申请卡住了。后来我把 DNS 指向 Netlify 地址了,然后找他们客服帮忙,很快就搞掂了证书的问题。

这时候我发现 Cloudflare 已经没有存在的意义了,就考虑把 Cloudflare 撤销掉,但在撤销之前还是做了几个比较。第一个比较是证书本身,Cloudflare 免费版让多个网站共用一张 LetsEncrypt 证书,然后通过 SNI 支持多域名。Netlify 专门为我的 catchen.me 申请一张独立的 LetsEncrypte 证书,然后用 SNI 在这张证书上添加子域名(其实只有 www.catchen.me 一个子域名)。尽管大多数用户并不会去留意证书的细节,但我自己一张证书这感觉就很爽。

CSS/JS 打包

在上一篇文章里我说到,我把 Bootstrap 和 jQuery 的 CSS/JS 文件都放到了我自己的部署目录当中去,这样浏览器就不需要连接不同的域名和 IP 来下载这些文件。Netlify 提供一组优化选项,帮我打包和压缩这些文件。因为 harp compile 并不对 CSS/JS 进行打包和压缩,所以我就选择了让 Netlify 进行优化。

这是 Netlify 比 Heroku 和 Surge.sh 要好的地方,照顾到了 Harp 这种不自带 Webpack 且不进行优化的构建流程。使用 Cloudflare 的话,它可以帮忙进行压缩,但没有打包的选项。同样是免费的 HTTPS,这方面 Netlify 比 Cloudflare 有优势。

CDN

尽管这些 CSS/JS 文件在部署时都存在于我网站的 /css//js/ 目录当中,但一旦启用 Netlify 打包功能这些文件就不会从我的域名进行加载,打包后的版本只存在于 Cloudfront 上面,也就是 AWS 的 CDN。我尝试拿它跟 Cloudflare 的 CDN 进行对比,发现 AWS 的节点数量略微比 Cloudflare 的多一些但也没有绝对的优势。

基于上述对比,我最终选择了使用 Netlify 来发布我新版的个人网站,并且不使用 Cloudflare 来做优化,全盘依赖于 Netlify 自身的优化。这一步做完之后,新版网站就重新上线了。接下来还有很多细节可以优化,如果你想要了解更多的话,欢迎通过邮件RSS/Atom 订阅我的博客。

2018年9月3日星期一

把我的个人网站推倒重来(Part 2 - 用 Bootstrap 做移动网页)

配置好 Harp 做静态网站构建后,就可以开始做网页了。上一个版本的个人网站样式是我自己设计的,当年用的还是 Macromedia/Adobe Fireworks,做出来一个 PNG 文件然后导出为不同的小图片。这次我也有考虑过要不要自己重新设计一个新的样式,但考虑到新设计不如解决其他几大问题重要,于是决定推迟样式设计。现在的计划是,先用 Bootstrap 解决绝大部分的问题,将来有时间重新设计样式了再做成 Bootstrap 主题。

导航栏

我的个人网站对组件的需求很简单:

  1. 要有一个导航栏,显示「首页」、「简历」、「项目」、「文章」。过去后面两项是「作品」和「更多」,我觉得改成新的名字会更贴近我现在能展示的内容。
  2. 要有一个语言切换菜单,能在英文和中文之间切换。过去我还区分繁体中文和简体中文,现在决定框架上要做到支持任意多的语言,但首要目标是支持英文和中文。

这两项需求对 Bootstrap 来说不是什么难事,直接扔一个 navbar 上去就解决问题了。首先把 4 大页面放在 navbar 上,然后再加一个选择语言的下拉菜单显示 2 种语言。因为 Bootstrap 内置了移动网页的适配,navbar 移动浏览器上会自动变成一个可缩放的菜单,完全不需要我做任何优化。

内容分栏

不同的页面有不同的分栏需求,首页左侧只显示大头照,简历页的每一项左侧时间右侧详情。Bootstrap 的 grid system 能够很好地解决我的问题。为了进一步优化不同尺寸屏幕上的体验,我又针对不同屏幕尺寸做了不同的布局,借助 Bootstrap 的 responsive breakpoint 这超级容易实现。

首先,分栏仅对宽度至少有 768px 的屏幕生效,这对应 Bootstrap 中 md (medium) 的定义。然后所有分栏的 CSS 类都会加上 md 限定条件,例如 col-md-4col-md-8。尺寸小于 md 定义的屏幕,统统只会看到一栏布局,首页就会把我的头像放上面,简历页也会把时间放在详情上面。

首页里面的「联系方式」用到了分栏内嵌分栏,这个分栏无论屏幕大小都会存在。我想要尽力保证美观的前提下不产生换行,然而每一栏都很窄,为此我要针对多种不同的屏幕尺寸按不同的比例来分栏了:col-lg-3 col-sm-4 col-5。这一行代码的意思是:这一栏在大屏幕时宽度为 3/12、中屏幕时为 4/12、默认(小屏幕)时为 5/12。(Bootstrap grid system 把屏幕分成 12 栏,所以分母永远是 12。)

字体大小

Bootstrap 默认的 h1h6 字体都挺大的。因为我用 h2h3 做小节标题,但又不希望出现巨型的文字,所以选择了把 h2h3 的尺寸调小。在 md 或以上屏幕上,因为存在分栏布局,内容条例已经很清晰,h2h3 大小可以很接近正文大小。在更小的屏幕上,我还是需要依赖它们来划分小节的,大小跟正文太过接近反而会把人弄晕。为此我特意写了个很小的 CSS 文件来调整字体大小:

h2 {
  font-size: 2em;
}
h3 {
  font-size: 1.5em;
}

@media (min-width: 768px) {
  h2 {
    font-size: 1.5em;
  }
  h3 {
    font-size: 1em;
  }
}

理想的做法是写 Sass 而非 CSS,然后用 @include media-breakpoint-up(md) 而非 @media (min-width: 768px),这样可以保证我的代码跟 Bootstrap 的代码同步。Harp 是支持编译 Sass 文件的,但它不支持 Bootstrap 所依赖的 Autoprefixer,不做这一步就意味着 -webkit- 这类 prefix 不会被自动加上,影响浏览器兼容性。我现在的选择是,用编译好的 Bootstrap CSS,然后我自己额外写的也是 CSS,只要我不手动更新 Bootstrap 版本就没有不同步的风险。

CSS/JS 文件来源

Bootstrap 所依赖于的 CSS/JS 文件可以不下载直接使用,这是 Bootstrap 官网提供的 CDN 地址

https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css
https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js
https://code.jquery.com/jquery-3.3.1.slim.min.js
https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js

观察一下这有什么问题?它们分别指向 3 个不同的域名啊,如果解释出来的 IP 不一样这可是 3 个独立的 HTTPS 连接啊,多浪费时间!我用 dig 命令查了一下,发现这 3 个域名确实指向不同的 IP,尽管非 Cloudflare 的那两个都指向了 hwcdn.net,但是是不同 IP。(我还特意查了一下这 hwcdn.net 是哪家 CDN 的,原来是一家叫做 Highwinds 的公司,现在已经被整合到 StackPath。这两个名字我之前都没听说过。)

为了解决这个问题,我把 Bootstrap 4.1.3 和 jQuery 3.3.1 下载下来了,然后放到我的本地目录中来使用。这样子所有 CSS/JS 文件都能够在同一个域名中获取,无需建立多个 HTTPS 连接浪费多重 RTT。

CSS/JS 文件体积

使用 Bootstrap 的话,默认需要下载的 CSS/JS 体积太大了,有些功能完全用不到但也需要下载。

最容易去掉的是 Popper.js,它存在于一个独立的 JS 文件中,直接不引用就行。根据 Bootstrap 的官方文档,只要用了 dropdown 就需要使用 Popper.js。但仔细看 Boostrap 源代码就会发现,在 navbar 中使用 dropdown 是不需要依赖于 Popper.js 的。(可能因为 navbar 的 dropdown 永远只能朝下展开,没有任何选项可言。)干掉 Popper.js 能省 6.9kb (Brotli 压缩后体积)。

最应该去掉的是 Bootstrap CSS 内众多我没用到的模块,但这又回到了我们之前的那个问题——如果我让 Harp 来编译 Bootstrap,我就失去了 Autoprefixer。我需要解决对 Autoprefixer 的依赖,然后才能让 Harp 编译 Bootstrap,接着我就可以把我没有用到的 Bootstrap 模块全部干掉了。这项优化留待我将来做吧。

折腾完 Bootstrap,我的网站模板也就没问题了,内容可以慢慢填充。接下来我需要解决静态网站发布的问题了,因为网站做好了总需要找个地方放啊。如果你想要继续跟着我一起折腾的话,欢迎通过邮件RSS/Atom 订阅我的博客。

2018年8月31日星期五

把我的个人网站推倒重来(Part 1 - 用 Harp 做模板引擎)

根据模板和数据生成静态网站的框架有很多,例如说 HarpJekyllHugo 等等。我对比了一下,最终选择了用 Harp,因为它是用 JavaScript 写的,如果我真的需要做什么改动我可以轻易地去改它的源代码。用 Harp 的坏处也很明显,这个项目在 GitHub 的源代码上已经很久没更新,搞不好将来不再有人维护。

安装 Harp 和用 Harp 编译生成静态页面很容易。因为 Harp 是「convention over configuration」的框架,所以每个页面在使用数据时会优先在里自己目录里的 _data.json 文件,然后看项目根目录里的 harp.json。然而有一个问题是 Harp 没解决的:本地化(多国语言文字)。

本地化

我用每种语言一个目录的方式解决本地化问题:

public/
  ├ _data.json
  ├ index.jade
  ├ _partials/
  │   ├ index.jade
  │   └ resume.jade
  ├ en/
  │   ├ _data.json
  │   ├ index.jade
  │   └ resume.jade
  └ zh/
      ├ _data.json
      ├ index.jade
      └ resume.jade

无论是哪个语言的 index.jade,调用的代码都是 public/_partials/index.jade,只不过把自己当前所拥有的 _data 传过去。无论是哪个语言的哪个文件,都调用 public/_partials/ 下的同名文件,因此所有这些特定语言特定文件都只有一行相同的代码

至于背后实际干活的那个 public/_partials/ 下的文件,它需要同时看 public/_data.jsonpublic/{locale}/_data.json 来进行渲染:前者为它提供语言无关的数据,后者为它提供语言相关的数据。

一开始的时候我严格执行 public/_data.json 只放语言无关数据,后来我发现这样编辑起来很麻烦,因为一个模板往往同时涉及两种数据,我需要修改两个 _data.json 文件。于是我把英文数据和语言无关数据都放到了 public/_data.json 里面,写文档时专注于写英文版。英文版完成后再翻译为其他语言,这时候 public/{locale}/_data.json 里的翻译就会覆盖 public/_data.json 中的同名数据,语言无关的数据自然不会被覆盖。

为了实现「覆盖」这一项功能,我还专门实现了一个 deepCopy 函数用来深复制 JSON 数据。我希望 Harp 能够内置这个功能:_data.json 从文件所在目录开始层层往上覆盖,一直覆盖到 harp.json 为止。在 Harp 支持这个功能之前,我只能自己先实现一个版本来方便我做本地化。

调试技巧

有时候我们实在想看一下 Harp 可访问的数据结构。我们知道 public 对象是任何页面都能访问的,public/ 目录下每一个子目录都会产生一个同名子对象,每一个文件的文件名会以字符串形式出现在对应子目录对象的 _content 中,每一个 _data.json 文件的内容都会展开在对应子目录对象的 _data 中。说这么多,还不如直接把整个 public 打印出来看看!

为此我专门做了一个 debug.jade 的 partial,需要打印 Harp 运行时变量的话就调用一下 != partial('debug', { data: public }),变量立即从 JavaScript 中打印出来。(其中 public 可以被替换为任何我想要查看的变量。)

关于 Harp 的部分到此结束,虽然我还没有把所有改重写的页面都写完,但我觉得 Harp 应用上的问题都已经解决完了。在下一篇文章里,我开始要解决页面布局问题了,如果你喜欢跟着我一起折腾的话,欢迎通过邮件RSS/Atom 订阅我的博客。

2018年8月24日星期五

把我的个人网站推倒重来(Part 0 - 历史背景)

我的个人网站最初是架设在 catchen.biz 上面的,当时还在大学里,只是想做个网站存放简历和作品,方便找工作。那个时候我还自己设计了这个网站的模板,然后用上了时下最流行的 XHTML + CSS + JavaScript 来实现这个模板。每个页面顶部都有一行在 HTML5 时代早已不需要的声明:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

这么多年过去了,我只更新过上面的简历信息,模板没有重新设计过,实现也没有更新。这造成了很多问题,也给我这次的推倒重来留下了很多可以做的事情:

  1. 不支持 responsive design,在移动浏览器上难以使用。Google 直接发邮件来通知说,不为移动浏览器优化的页面会在搜索结果里面降权。如果支持 AMP 的话,可能能在 Google 搜索结果和移动浏览器中获得更好的体验。
  2. 不支持 HTTPS。当年搞个 HTTPS 证书很麻烦,而且 SNI 尚未流行所以还需要跟服务运营商要求独立 IP,现在这一切都变得很简单。
  3. 缺乏页面模板,每个页面的源代码都需要手工维护。现在静态网站引擎那么多,这个问题可以轻易解决掉。
  4. FTP 上传麻烦,必须覆盖所有文件。这样的部署方式太不方便,我还是需要为此而保留一个 FTP 客户端。因为不知道哪些文件更新了,所以每次都必须要上传并覆盖所有文件。

为了解决这些问题,我选择了把整个网站推倒重来。在这个过程中,我顺便把网站迁移到 catchen.me。这样子我就可以在 catchen.me 上慢慢折腾我的半成品,同时保持 catchen.biz 正常运作。完工后,我只需要在 catchen.biz 上用 301 转跳指向 catchen.me 即可。

在下一篇文章里,我就要开始讲述我如何折腾我的个人网站了,如果你喜欢跟着我一起折腾的话,欢迎通过邮件RSS/Atom 订阅我的博客。

2018年8月21日星期二

《The Dictator's Handbook》摘要

这次试试新玩法:在读数过程中把想到的直接发往 Twitter,然后再把所有的 tweets 加到一个 moment 里面,最后把 moment 嵌入到博客当中。这次读的书是《The Dictator’s Handbook: Why Bad Behavior is Almost Always Good Politics》,摘要请看下面嵌入的 tweets。

P.S. 嵌入 tweets 的坏处是对 SEO 不利,因为所有 tweet 的内容都没有出现在页面的 HTML 里面。此外这样做也可能影响 Feed 和邮件阅读的体验,不过我总要试一次才知道体验如何嘛。

2018年7月21日星期六

Sonos One:满足但折中你的每一个愿望

Sonos One

我家里在饭厅和厨房之间放了一个老一代的 Echo,然后在我床头柜上放了一个 Echo Show。因为老一代的 Echo 感觉「听力下降」,整天喊它开电视关灯之类的都听不到,所以就想把它替换掉。正好前几天 Amazon Prime Day,Sonos One 打折到 $150 ,买两个都还是比 Apple HomePod 便宜,所以就赶紧买了两个。我的计划是,把 Sonos One 放到睡房里替换掉 Echo Show,这样我在睡房里播音乐打游戏就有 AirPlay 2 立体声了;然后用 Echo Show 把厨房的 Echo 替换掉解决它听力不足的问题。

第一印象

两个 Sonos One 到了之后,从包装上来看就感觉比 Echo 高级很多。Amazon 硬件包装简单优雅,Sonos 则要豪华和结实。Sonos One 包在一个布袋里面,然后放在厚厚的缓冲纸托盘上,底下还专门有一层纸托盘放电源线和网线。Sonos One 的 Wi-Fi 设置流程比我见过的其它硬件都要简单,因为它调用了 iOS 的外置硬件 Wi-Fi 设置流程,iOS 会自动帮你把 Sonos One 连上家里 Wi-Fi,完全不需要手动输入密码。在设置好两个 Sonos One 后,Sonos 会问你是不是把它们设置为一对立体声音箱,然后用 Trueplay Tuning 来校准它们。

Sonos 跟 Echo 对比

在把 Sonos One 设置好之后就可以把它们加入到 Alexa 设备当中了。Sonos One 说了自己不支持所有 Echo 支持的功能,是时候看看到底什么功能不支持了。

我之前设置了床头柜上的 Echo Show 每天早上播报新闻的,但在换成 Sonos One 之后发现 Alexa Routine 并不能在非 Echo 上执行。Alexa Multi-Room Music 不支持 Sonos One,所以不能让 Echo 和 Sonos 一起播放音乐。由此看来,Sonos One 只能对 Alexa 作出主动请求,而 Echo 能够被动地在 Alexa 的远程操纵下做事情。此外,Sonos One 对于 Alexa 唤醒的响应也没有 Echo 那么自然,喊完「Alexa」后它会响一下表示听到了,这时候我已经在说后面的指令了,这一下响声反而会把我打断。

关于 Echo,还有一件很奇怪的问题:我把老的 Echo 断电后,发现就再也不能通过 Echo 控制家里的 Hue 灯泡了。然而只要我把原来的 Echo 接上去,Hue 就又能用了。(不仅仅那个 Echo 能用,Echo Show 和 Sonos One 都能控制 Hue。)这让我怀疑那个 Echo 是否充当了什么神奇的角色,暂时的解决方案是让它继续保持在线。

Sonos 跟 HomeKit 连接

在配置好 Sonos One 之后,它会提醒你配置 Alexa,但不会提醒你配置 HomeKit。我一开始看到 AirPlay 2 能用了,还忘记要配置 HomeKit 了,想起来之后还查了一下才知道如何配置。(其实很简单,打开 Home 添加一下就可以了。)配置好 HomeKit 之后可以用 Siri 叫 HomeKit 操作 Sonos One,但因为我没有 Apple Music 也不怎么用 Siri,所以对我来说其实没用。

有一件很神奇的事情,那就是我在家里就算连接的不是这些智能硬件所在的 Wi-Fi 网络,我也能在 AirPlay 看到我的 Apple TV,然而却看不到 Sonos One。理论上最新版的 AirPlay 不要求设备都在同一个 Wi-Fi 网络,因为它们可以私下建立一个点对点网络,至少 Apple 的设备是这样做的。显然 Sonos 不支持这一项功能,不知道是硬件上的限制还是软件没有实现。这应该是一个 HomePod 能做好但 Sonos One 没做好的事情。(我没有 HomePod 所以不能确认,但我觉得 Apple TV 能做的事情 HomePod 应该也能做。)

总结

Sonos One 尝试把 Echo 和 HomePod 能做的事情给做了,确实也做到了一部分,但又明显有做得不完美的地方。这就是我标题所说的,满足你的每一个愿望,但每一个都要折中。

如果你喜欢类似的硬件测评文章的话,欢迎通过邮件RSS/Atom 订阅我的博客。