使用writing-mode实现垂直排版

发布于 大漠

大约一年前,我写了一篇关于在Web中实现垂直排版的文章。这是一个简单的Demo,它允许你通过复选框来切换书写模式。

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

当我重新打开这个Demo的时候,看到的结果让我感到无语。为了宣泄一下,我将一步一步写下各种浏览器的渲染结果,以及目前可能的解决方案。

这篇文章的篇幅很长,可能会花费你一定的时间。

初步的调查结果

我只看我能立即访问的浏览器。因为我还有别的事情需要做。

Chrome(64.0.3278.0 dev)

Chrome效果

效果看起来很不错。要是我说所有的一切都被破坏了,那其实有点夸张。所有的文字和图片都占满,在垂直书写模式下没有重大的渲染问题。Chrome,这就是我喜欢你的原因之一。

Chrome效果

切换排版模式,将东西都放到浏览器的右侧去。我记得在垂直排版下将东西水平居中是一件让人特别痛苦的事情,所以在第一次不太顺利的尝试中,我肯定是用了某些我自己都忘了的黑魔法。

这个在2017年年初是绝对可行的,我有绝对的信心这么的认为。因为我为在Webconf.Asia做分享的PPT中有这个效果的录屏。我很确定当时用的是Chrome。几个月的时间,Demo的效果就变得令人难以置信。我的老大曾经提到过一个词叫作代码腐烂,也许就是这样的吧。

Firefox (59.0a1 Nightly)

Firefox效果

效果令我再次感到无语了。Firefox Nightly还是我的默认浏览器,所以我的最初反应是这一切都被破坏了,整个人都崩溃了。事实上,垂直排版是被破坏了,还出现了无限的水平滚动条。到底发生了什么?我自己也是一脸的蒙逼。

Firefox效果

我们来切换一下状态,可我找了半天的复选框,都不知道在哪里了。不管怎么说,至少我将复选框绑在了label标签上,这样我仍然可以点击label标签来切换。所以,这绝对不是居中,但也没崩溃。两个浏览器的渲染效果可以说是天差地别。

Safari Technology Preview 44

Safari效果

哈哈哈...。看起来令人惊讶,效果出奇的好。甚至连高度都是正确的。可能我太轻看了Safari。Safari浏览器的渲染引擎到底是什么呢?好吧,是Webkit

Safari效果

这是页面的中间部分。如果不查看代码,我也确定我尝试过一些很奇怪的转译来改变整个内容块,因此在每个浏览器中行为不一致。但这是个令人欣慰的惊喜。

Edge 16.17046

这是Windows 10内置快速通道版本,所以我认为我的Edge浏览器应该比大多数人的版本更高。没关系,我也可以用我的手机((^_^)不要有任何的奇怪,我用的就是Windows Phone)。

Edge效果

不管怎么说,效果看上去还不算太坏。只是那个复选框有点偏了。更重要的是滚轮能正常的工作!其他所有的浏览器都不允许我用滚轮水平滚动。虽然我不知道这是Windows的功劳还是Edge的功劳。

Edge效果

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

Edge 15.15254

Edge效果

Edge 15的vertical-rl效果

Edge效果

Edge 15的horizontal-tb效果

几乎和Edge 16的效果一模一样。我有理由相信Windows Phone上的Edge浏览器用的是与桌面版本同样的渲染引擎**EdgeHTML**,如果有错,烦请各路大婶拍正。

iOS 11 WebKit

iOS 11效果

iOS 11 Webkit的vertical-rl效果

iOS 11效果

iOS 11 Webkit的horizontal-tb效果

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

开始撸码

好了,既然我们已经确定了排版被打破的基准,现在是时候揭开尘封的面纱,看看下面到底有什么怪异的代码。说句公道话,没有太多的东西,考虑到这是一个非常简单的示例,所以效果还是不错的。

同量我还要强列的安利一波Browsersync,这也是我最重要的开发工具,尤其是需要在不同的设备的不同浏览器上调试的时候。如果没有Browsersync,我将会多很多无趣的事情做。

一些背景知识

切换器的实现可以用两种形式,一种是通过JavaScript切换类,另一种是使用复选框(其实也可以使用单选按钮)。不过我通常倾向于只使用CSS的解决方案(能用CSS实现的功能,绝不用JavaScript),所以我决定使用复选框的方案。这个Demo很简单,所以不会有太多键盘控制方面的干扰。我的意思是,你可以像其它任何的复选框一样用tab切换到它然后切换。

我真的需要研究可访问性的问题,以确定我是否会在屏幕阅读器上搞砸它,但那是另外一回事了,不是今天要聊的事情。回到我们的主题中,处理垂直排版的问题。

如果你没有尝试过复选框,这也不要紧,因为它不会太复杂,它涉及到:checked状态选择器的使用和兄弟或子选择器。你可以通过这种方式来控制复选框选中的状态。

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

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

    <main>
        <!-- All the markup for the content -->
    </main>

    <script src="scripts.js"></script>
</body>

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

一般的复选框黑魔法策略

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

调试:重置到基线

记住,这是一个大脑转储条目,如果你觉得无聊,我对此表示抱歉。我做的第一件事情就是删除所有样式,重头再来。再说一次,这是可行的,因为这个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的含义

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

因此,我们把事情提高到一个层次,在body上设置writing-mode:vertical-rl。Chrome、Safari和Edge几个浏览器中的渲染结果,正如我们所想的一样从右到左渲染内容。但在Firefox浏览器中仍然显示文章的末尾,尽管这确实修复了滚动条溢出的问题。这看起来和Bug 1102175有关。

vertical-rl的含义

最后,如果我们把html设置writing-mode:vertical-rl,Firefox终于正常的渲染了,从右到左显示,而且没有溢出。

vertical-rl的含义

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

布局切换

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

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

解决方案一:使用JavaScript

让我们先来谈谈解决方案。既然问题出在混用的writing-mode上,也许我们停止混用,效果就会不一样呢?基于上面的观察,用一个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设置为auto就可以在所有浏览器中完美实现居中:

.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;
    }
}

我们经常看到的是margin-leftmargin-right设置为auto,并且配合元素的width可以完美的实现水平居中的效果,那是因为我们的书写模式是水平的。而现在我们聊的是垂直书写模式,所以需要将对应的margin-leftmargin-right换成margin-topmargin-bottom。比如上面的示例所示。

解决方案一

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

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

解决方案二:复选框

这种技术方案的原理类似于使用JavaScript,和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; // this doesn't work
    margin-right: auto; // this doesn't work
}

布局代码与.vertical.horizontal一样,但遗憾的是,结果却不一样。垂直居中是好的,看起来好像是我们在用JavaScript。但是水平居中更向右偏了。marginauto似乎并没有发挥作用。

但仔细想想,这样的行为并没有错,因为我们同样不能用这种方式在水平书写模式下实现垂直居中。为什么呢?看看规范就懂了,我想你也懂的。

所有的CSS属性都有值,一旦你的浏览器解析了一个文档并构造了DOM树,每个元素的每个属性都需要赋值。@Lin Clark写了一个精彩的动画(代码漫画)来阐述CSS引擎如何工作,强烈建议你阅读,不要错过。话又说回来,值在规范中是这样描述的:

一个属性的最终值是四步计算的结果:首先通过规范确定值(指定值),然后解析为一个用于继承的值(计算值),接下来要是没有必要,就转换成绝对值(使用值),最后依据具体场景限制,再做转换(实际值)。

与此同时,根据规范,heightmargin的计算由各类盒模型的规则来决定的。如果下下的值同时为auto,它们的使用值将被解析成0

当我们把书写模式设置为垂直方式,height似乎在计算的时候会变成水平坐标。我说似乎是因为我并不百分百确定它真是这样计算的。它让我觉得JavaScript解决方案很神奇。

开个玩笑,实际上因为在JavaScript解决方案中没有混用书写模式,所以将各自的值解析成0,并不影响我们想要的居中效果。可能你需要重读这句话几次,才能更明白其中的深意。

想要在切换到垂直书写模式的时候将main元素水平居中,我们需要使用好的切换技巧。

.c-switcher__checkbox:not(:checked) ~ main {
    position: absolute;
    top: 0;
    right: 50%;
    transform: translateX(50%);
}

这在Chrome,Firefox和Safari上可行。不幸的是,Edge上有点毛病,东西都偏于页面中间的某个地方以及左边。是时候记下这个Edge的bug。另外,滚动条出现在了左侧而不是右侧。

处理图像对齐

是不是有点累了,不过不要停下来,继续往下。在垂直书写模式时,希望有两张图片的figure元素堆叠显示,而在水平书写模式时,如果空间允许,则并排显示。理想情况下,figure元素(图像和标题)将在各自的书写模式下居中。

经典属性

既然我们在一个干净的页面上运行,那就让我们试试最基础的居中技术:text-align。默认情况下,图像和文本是内联元素。给figure元素设置text-align:center, Oh, My God。一切都OK了。神奇吧!

水平和垂直书写模式下的图像都已经成功的居中了。我现在非常怀疑一年前我做这个Demo的时候,智力突然为0了。很显然,为了我的目的和意图,弹性盒模型是没有必要的。我首先尝试了新的技术,这也让我付出了很高的代价。

在水平书写模式中,不需要加太多额外的东东。只是一个简单的margin-bottom:1em,给figure之间留点空间。由于空间关系,我确实需要将竖直的图像旋转,在这里我使用transformrotate来完成。

.vertical {
    figure {
        margin-bottom: 1em;
    }

    figcaption {
        max-width: 30em;
        margin: 0 auto;
        display: inline-block;
        text-align: justify;
    }

    .img-rotate {
        transform: rotate(-90deg);
    }
}

问题是,当你旋转一个元素,浏览器仍然会记住它原来的宽高,所以在我的Demo中,当视窗变得非常小的时候,它将触发水平溢出。可能有办法修复这个问题,但我没有找到。如果你在这方面有经验,欢迎指点。

这就是我将为RICG编写的用例。我的想法是,如果可以通过媒体查询获得书写模式,我就可以使用srcset定义一个垂直的图像和一个水平的图像,分别为对应的书写模式提供不同的图片。

在垂直书写模式中,我们通常希望文字整齐,或者至少在短行上对齐半孤立的字符,然后文字的空隙,margin应该设置为left而不是bottom

.vertical {
    figure {
        margin-left: 1em;
    }

    figcaption {
        max-height: 30em;
        margin: auto 0.5em;
        display: inline-block;
        text-align: justify;
    }
}

现在我们几乎可以称之为圆满的一天。最终结果已实现了。我还想补充的是,除了我之前提到的Edge缺陷之外,无论JavaScript方案还是复选框方案,得到的结果都是完全相同的。

使用Flexbox实现居中

我一度怀疑我使用Flexbox实现居中的理由,尽管我真的不记得到底为什么会认为这是一个好的方案。很明显,我不需要Flexbox的任何特点。那我应该也做个大脑转储?

可是我看了我自己的原始代码,我才发现我给包裹图像的容器div设置了display:flex,这让图像成为Flex的子元素,导致Firefox的垂直书写模式渲染混乱。

使用Flexbox实现居中

使用这种方法,效果看上去很美好,而且我测试过Chrome,Edge以及Safari的所有版本都是OK的,因此图像在垂直和水平两种书写模式下都居中对齐了。但Firefox浏览器下不行,切换到垂直书写模式时,图片在我的页面上不可见。

使用Flexbox实现居中

我已经用display:flexdiv来包裹应该堆叠显示的图像,但不知道为什么在Firefox的垂直书写模式下却有问题。我怀疑这个渲染行为和这些Bug有关:Bug 1189131Bug 1223180Bug 1332555Bug 1318825Bug 1382867

与此同时,我很好奇这些图片已经是Flexbox容器的子元素(Flex项目),但在Firefox垂直书写模式下效果就是不好。我百思不得其解,差点就想说,Firefox浏览器就是一坨粑粑。

使用Flexbox实现居中

抛开垂直书写模式,我还和@Jen Simmons聊过一些关于在不同浏览器中的Flexbox实现,她发现在所有的浏览器中,缩小图像的处理都是不同的。这个问题仍然在CSS工作组中讨论,对这方面感兴趣的同学,可以持续关注相关的更新。

这个缩小的问题与内部尺寸的概念有关,尤其是图像固有长宽比例的图像。CSS工作组对此进行过相当长的讨论,因为这不是一个小问题。

有趣的是,Firefox中,Flex容器的宽度受视窗宽度的限制,但目前在其他的浏览器上并没有发现类似的现象。当容器内所有的图像的宽度之和超过了视窗宽度,Firefox就会让图像缩小以适应视窗的宽度,但在别的浏览器上并不会缩小,只会让浏览器出现一个水平的滚动条。

为了暂时回避这个问题,我要确保我的图像都不是Flex项目(图像的容器不是Flex容器)。所有的图像,无论是单还是双,都被包裹在div中。figure元素设置了display:flex属性,让figcaption和包裹图像的div成为Flex容器的子元素而不是图像本身。

.vertical {
    writing-mode: vertical-rl;

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

    figure {
        flex-direction: column;
        align-items: center;
        margin-left: 1em;
    }

    figcaption {
        max-height: 30em;
        margin-left: 0.5em;
    }

    .img-single {
        max-height: 20em;
    }
}

.horizontal {
    writing-mode: horizontal-tb;

    main {
        max-width: 40em;
        margin-left: auto;
        margin-right: auto;
    }

    figure {
        flex-wrap: wrap;
        justify-content: center;
        margin-bottom: 1em;
    }

    figcaption {
        max-width: 30em;
        margin-bottom: 0.5em;
    }

    .img-wrapper img {
        vertical-align: middle;
    }

    .img-single {
        max-width: 20em;
    }

    .img-rotate {
        transform: rotate(-90deg);
    }
}

复选框的方案实现完全是一样。我从中学到的是,浏览器对于元素的区域计算需要下很大的功夫,尤其是具有固有尺寸比例的。

Gird如何?

我们已经远离了这个布局所需的东西,所以我考虑尝试使用Grid来实现图像对齐。我们可以尝试让每个figure都成为一个Grid容顺,或许可以用上grid-areafit-content这些有趣的属性,让事情变得简单。

不幸的是,我尝试之后,我整个人都要崩溃了。Firefox的Grid调试工具并不能匹配到我页面上的元素,但也有可能是因为页面上的东西太多了。

Gird如何?

我需要为使用Grid的垂直书写模式创建一个简化的测试用例,那将是一个简单得多的Demo,我还会单独写一篇这方面的文章(可能还会有相关的错误报告)。

成功的解决方案

我写的Demo实现的是没有Flexbox的复选框解决方案。我将保留复选框的版本,便于追踪Edge的Bug。对于Flexbox解决方案,如果你不介意增加额外的容器的话,也是可以的。使用JavaScript来实现,效果看起来更好一些,因为你将切换器包裹在一个div中,然后再处理它的样式。

在最后,有很多方法可以实现同样的效果。从别的地方复制代码也可以,但是出现莫名其妙的问题就蛋疼了。你不必从头开始写所有的东西,但要确保里面有没有使用一些黑魔法。

扩展阅读

Bug跟踪

本文根据@Hui Jing的《Vertical typesetting with writing-mode revisited》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://www.chenhuijing.com/blog/vertical-typesetting-revisited

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/css3/vertical-typesetting-revisited.htmlNike Tiempo Legend VI FG