阅读 395

[译] 垂直排版:重提 writing-mode

垂直排版:重提 writing-mode

大约一年前, 我写了在一次 Web 中文垂直排版的尝试中的一些发现。这是一个简单的 demo,它允许你通过复选框来切换书写模式。

我在不久后遇到了 Yoav Weiss,并聊了一下响应式图片社区小组,因为我提到如果可以通过媒体查询得到 picture 元素的 writing-mode,我就不必在切换排版的时候通过一些比较 hack 的方式对图像进行转换。他建议我把它写成一个响应式图像用例

但当我重新打开这个一年没打开的 demo 的时候,我的表情在最初的五分钟由 😱 变成了 😩(我还能说什么呢,我就是这么表情丰富 🤷)。所以为了宣泄,我将一步步写下谁(也就是各种浏览器)破坏了什么以及目前可能的解决办法。

帖子很长,可以使用链接来跳转。

大脑转储结构

最初的发现

我只在看我能立即访问的浏览器,因为我的人生还有很多别的事要做 🙆。

Chrome (64.0.3278.0 dev)

vertical-rl on Chrome

好的,这看起来非常棒。我说所有东西都被破坏了其实有点夸张。所有的文字和图片都占满,在垂直书写模式下没有重大的渲染问题。做的好,Chrome。

horizontal-tb on Chrome

切换排版模式将东西都踢去了右边。我记得在垂直排版下将东西水平居中是一件让人特别痛苦的事情,所以在第一次不太顺利的尝试中我肯定用了某些 hack 手段。

这在 2017 年初是绝对可行的,因为我为我的 Webconf.Asia 幻灯片做了这个截屏。我很确定当时用的是 Chrome。几个月时间一个 demo 的变化让人惊讶。我的老大提到过一个词叫「代码腐烂」,也许这就是吧。

Firefox (59.0a1 Nightly)

vertical-rl on Firefox

天哪,这,我都无语了。Firefox Nightly 是我的默认浏览器,所以我的最初反应是一切都被破坏了。一切确实都被破坏了,看看这无限滚动的水平滚动条,到底发生了什么?!

horizontal-tb on Firefox

让我们切换……等等,我的复选框呢?唉,这可能要等一会。不管怎么说,至少我将复选框绑在了 label 上,所以我仍然可以通过点击 label 来切换排版。所以,这绝对不是居中,但也没有太崩。两个浏览器的表现形式天差地别。

Safari Technology Preview 44

vertical-rl on Safari TP

嘿,嘿,嘿!这看起来令人惊讶的好。甚至连高度都是正确的。Safari,我可能误判你了。Safari 的渲染引擎到底是什么?好吧,WebKit。

horizontal-tb on Safari TP

噢噢噢,这有点居中。不看代码,我也能确定我尝试过一些很奇怪的转译来改变整个内容块,因此在每个浏览器中行为不一致。但这是个令人欣慰的惊喜。

Edge 16.17046

这是 Windows 10 内置快速通道版本,所以我想我的 Edge 浏览器应该比大多数人的版本更高。没关系,我也可以用我的手机(没错,我用的是 Windows phone,不服来战)。

vertical-rl on Edge 16

无论如何,这看起来也不算太坏。只是那个复选框有点错位。更重要的是滚轮正常工作!其他所有的浏览器都不允许我用滚轮水平滚动。虽然我不知道这是 Windows 的功劳还是 Edge。

horizontal-tb on Edge 16

也是隐约的居中。我真的需要马上检查下我的转换代码。现在我可能对我的复选框究竟怎么了也产生了疑问。啊,使用滚轮无法垂直滚动,这就有意思了。另外,注意滚动条在左边 🤔。

Edge 15.15254

Edge 15 上的 vertical-rl

Edge 15 上的 horizontal-tb

跟 Edge 16 几乎一模一样。我有理由相信 Windows phone 上的 Edge 浏览器用的是与桌面版本同样的渲染引擎 EdgeHTML,如果有错还望指正。

iOS 11 WebKit

iOS 11 WebKit 上的 vertical-rl

iOS 11 WebKit 上的 horizontal-tb

尽管我的 iPad 上装了一大堆浏览器,但我知道它们的渲染引擎都是 WebKit,因为苹果从未允许过第三方的浏览器引擎。正如在桌面版展示的那样,这是表现比较好的浏览器。

代码时间

好了,既然我们已经确定了破坏的基准,现在是时候把防尘罩拆下来,看看底下到底有什么怪异的代码。公平地说,没有太多,考虑到这是一个非常简单的演示,所以还不错。

同时我还要强烈安利(无数次)Browsersync,那是我最重要的开发工具,尤其是需要在不同设备的不同浏览器上调试的时候。如果我没有 Browsersync,我将不会为此做这么多工作。

一些背景

切换器的实现可以用两种形式,一是通过 Javascript 切换类,二是 hack 复选框。我通常倾向于只使用 CSS 的解决方案,所以决定 hack 复选框。这个 demo 足够简单,所以不会有太多键盘控制方面的干扰。我的意思是,你可以像其它任何的复选框一样用 tab 切换到它然后切换。 我真的需要研究可访问性的问题以确定我是否会在屏幕阅读器上搞砸它,但那是另一回事了。今天优先处理布局问题。

如果你没有尝试过 hack 复选框,它涉及到 :checked 伪选择器的使用和兄弟或子选择器,你可以通过这种方式用 CSS hack 复选框的状态。

需要注意的是,切换 :checked 状态的 input(通常是复选框元素),必须处于与你想切换状态的目标元素相同或更高的层级。

<body>
  <input type="checkbox" name="mode" class="c-switcher__checkbox" id="switcher" checked>
  <label for="switcher" class="c-switcher__label">竪排</label>

  <main>
    <!-- 内容样式 -->
  </main>

  <script src="scripts.js"></script>
</body>
复制代码

问题就在复杂度上。在同一个页面上混合使用不同的嵌套的书写模式确实会搞垮浏览器。我不是浏览器工程师,但我有足够的常识知道渲染东西不是微不足道的。但是我是一个执着的人,所以必受其苦。

一般的复选框 hack 策略

原始的 demo上,我在 body 元素上设置默认的书写模式为 vertical-rl,然后使用复选框来切换 main 元素里的书写模式。但是看起来似乎每个人(浏览器渲染引擎)都向上面的截图目录一样,以不同的方式处理嵌套的书写模式。

调试 101: 重置为基准

记住,这是一个大脑转储条目,如果你觉得无聊,我对此表示抱歉。我做的第一件事就是删除所有样式,重新开始。再次重申,这个 demo 有效是因为它十分简单。上下文才是一切,朋友们。

html {
  box-sizing: border-box;
  height: 100%;
}

*,
*::before,
*::after {
  box-sizing: inherit;
}

body {
  margin: 0;
  padding: 0;
  font-family: "Microsoft JhengHei", "微軟正黑體", "Heiti TC", "黑體-繁", sans-serif;
  text-align: justify;
}
复制代码

这几乎成了我所有项目的事实起点。将所有元素设置成 border-box,而且通常我还会加上 margin: 0padding: 0 作为样式重置的基础。但是就这个 demo 而言,我将让浏览器保留它的空白只重置 body 元素。

这个 demo 几乎全是中文,所以我只添加了中文字体,把系统自带的 sans-serif 作为后备。不过大多数情况来说,优先选择基于拉丁语的字体是个普遍的共识。但在这里,中文字体支持基本的拉丁字符,而反过来情况就不一样了。

当浏览器遇到中文字符时,它不会在基于拉丁语的字体中寻找,所以它会选用下一种备选字体,直到找到合适的。如果你先将中文字体列出来,浏览器将使用中文字体中的拉丁语字符,有时候这些字形没被打磨,看起来也不太好,尤其是在 Windows 上。

接下来是一些不太影响布局的美化(line-height 算吗?🤔)

img {
  max-height: 100%;
  max-width: 100%;
}

p {
  line-height: 2;
}

figure {
  margin: 0;
}

figcaption {
  font-family: "MingLiU", "微軟新細明體", "Apple LiSung", serif;
  line-height: 1.5;
}
复制代码

这一个合理、体面的基准。现在我们可以调查 writing-mode 的行为了。

vertical-rl 的含义

每一个元素的 writing-mode 的默认值都是 horizontal-tb,而且它是一个继承属性。如果你设置了一个元素的 writing-mode,这个值将传递到它所有的子元素。

如果我们将 main 元素的 writing-mode 设置为 vertical-rl ,在每个浏览器上,所有的文字和图像都被正确渲染了。Firefox 有 15px 轻微的垂直溢出,我怀疑是因为滚动条,不过我不能确定。其它的浏览器一点水平溢出都没有。

vertical-rl on the main element

main 元素是垂直书写模式的同时,document 本身是水平书写模式,就会产生问题,意味着内容从左边开始,而且我们最终会看到第一次加载的文章的末尾。

所以,让我们把东西提升一个层级,在 body 上设置 writing-mode: vertical-rl。Chrome,Safari 和 Edge 如我们所想从右到左渲染内容。但是 Firefox 仍然显示文章的末尾,尽管这确实修复了滚动条溢出的问题,它看起来和 Bug 1102175有关。

vertical-rl on the body element

最后,如果我们将 html 设置 writing-mode: vertical-rl,Firefox 终于正常并从右到左显示了,而且没有搞笑的溢出。And lastly, if we apply writing-mode: vertical-rl to the html element, Firefox finally comes around and reads from right-to-left. Also, no funny overflowing, just vertical right-to-left goodness.

vertical-rl on the html element

IE11 支持书写模式属性,只不过使用较早的规范中定义的旧语法 -ms-writing-mode: tb-rl。这工作正常,但我由于现在使用的 main 标签 IE11 并不支持,切换器失效了。甚至将 main 标签设置成 display: block 都无法修复。我可以为了更好的兼容性将 main 替换成 div。让我考虑一下。

布局切换

由于 Firefox 有已知的垂直书写的弹性盒模型的问题,所以我将把调试任务分成两个部分,一是纯粹的布局。找出使切换器正常工作的不同方法,而且没有任何奇怪的溢出。

第二个部分将与图像居中有关,这让我陷入混乱。除了居中,我还想调整图像的方向,它是让我首先重温 RICG 用例汇总的原因。#不起眼的注脚

解决方案 #1: Javascript

让我们先来尝试回避的解决方案,既然问题出在混用书写模式,也许我们可以停止混用。基于我们上面的观察,用一个 Javascript 事件监听器去切换 html 元素的 CSS 类可以隐性修复许多奇怪的渲染问题。好了,代码时间到。

我想切换的两个类的类名简单地叫做 verticalhorizontal。既然我已经有了复选框,也许也可以用作类的切换器。

document.addEventListener('DOMContentLoaded', function() {
  const switcher = document.getElementById('switcher')

  switcher.onchange = changeEventHandler
}, false)

function changeEventHandler(event) {
  const isChecked = document.getElementById('switcher').checked
  const container = document.documentElement

  if (isChecked) {
    container.className = 'vertical'
  } else {
    container.className = 'horizontal'
  }
}
复制代码

将内容块居中完成得很好。因为再也没有嵌套的书写模式或者弹性盒模型。直接的自动 margin 在所有浏览器中都完美实现了居中,甚至 Firefox。

.vertical {
  writing-mode: vertical-rl;

  main {
    max-height: 35em;
    margin-top: auto;
    margin-bottom: auto;
  }
}

.horizontal {
  writing-mode: horizontal-tb;

  main {
    max-width: 40em;
    margin-left: auto;
    margin-right: auto;
  }
}
复制代码

Auto margins for vertical centring

有趣的是,在垂直书写模式,我们可以用 margin-top: automargin-bottom: auto 来垂直居中。但相信我,水平居中将比你想象的更令人痛苦。在下一个 hack 复选框的部分你将看到。

意外的 TIL: Microsoft Edge 遵守 ECMAScript5「严格模式下不允许分配只读属性」的规范,但是 Chrome 和 Firefox 在严格怪异模式下仍然允许,很可能是为了代码兼容。我最初尝试使用 classList 来切换类名,但它是一个只读属性,而 className 则不是。相关阅读在下面的链接

解决方案 2: 复选框 hack

这个方案的原理类似使用 Javascript,区别在于我们不使用 CSS 类来改变状态,而是使用 :checked 伪元素。如我们前面所讨论的,复选框元素必须和 main 元素在同一层级才会生效。

.c-switcher__checkbox:checked ~ main {
  max-height: 35em;
  margin-top: auto;
  margin-bottom: auto;
}

.c-switcher__checkbox:not(:checked) ~ main {
  writing-mode: horizontal-tb;
  max-width: 40em; 
  margin-left: auto; // 无效
  margin-right: auto; // 无效
}
复制代码

布局代码与 .vertical.horizontal 一样,但,结果却不一样。垂直居中是好的,看起来好像是我们在用 Javascript。但是水平居中歪向了右边。自动 margin 在这一部分似乎完全没有发挥作用。 但仔细一想,这其实是「正确」的行为,因为我们同样不能用这种方式在水平书写模式下实现垂直居中。为什么呢?让我们来看一下规范。

所有的 CSS 属性都有值,一旦你的浏览器解析了一个文档并构建了 DOM 树,每个元素的每个属性都需要赋值。Lin Clark 写了