最近在两个多语言项目的改版里,我反复被同一个问题折磨:一套界面在中文、英文下都好好的,只要切到阿拉伯语或者日文竖排,布局就碎成一地。改完 margin-left 改 padding,改完 padding 又发现绝对定位跑偏,整个下午都在做无用功。后来我把视线收回到“文本方向布局”这个方向上,把 CSS 里那些平时没人细看的逻辑属性、书写模式重新梳理了一遍,再做了一套轻量封装。没错,就是标题里说的 Pretext。它不是什么重型 UI 库,而是一套聚焦文本方向、文本装饰与前排队列的布局方案,让开发者不用在物理属性里反复横跳,直接按“文本实际流向”来写样式。
这篇文章不聊虚的,就围绕 Pretext 这套思路,把文本布局这件事从原理、实操到踩坑、面试怎么讲,一次性说透。适合正在做国际化项目、电子书阅读器、中文竖排排版,或者准备高级前端面试的朋友。你会看到它到底解决了什么问题,我会带着你从零实现一个能用的文本布局层,也会把我在竖排、RTL、混排里踩过的坑全部摊开。
1. 项目定位与核心需求拆解
1.1 Pretext 到底解决什么问题
先说结论,Pretext 解决的是“文本在页面里按什么方向流动”这件事。传统 CSS 布局里,大多数人默认文字是从左往右、从上往下走的,所以写样式的时候也习惯用 margin-left、padding-top、text-align: left 这类物理属性。可一旦文本方向变了,比如阿拉伯语从右往左读,或者中文古籍按列从上往下排,这些物理属性全部失效。
我举个真实例子。一个卡片组件,内部有个图标和一段说明文字,我最初用 margin-left 把图标和文字拉开距离。英文版本没问题,阿拉伯语版本一上,图标应该挪到文字右侧,但 margin-left 纹丝不动,最后我只能写两套样式,用 dir 属性去切换。这还不算最惨的,遇到绝对定位的元素,如果参照系还是左上角,那 RTL 下整个位置就是错的。Pretext 的核心思路,就是把 margin、padding、border、inset 这些属性全部换成逻辑属性,让布局元素跟着文字方向自动翻转。
同时,Pretext 也把文本装饰和文本方向做了统一处理。text-decoration、text-emphasis、underline-offset 这些属性,在竖排文本里表现完全不一样,而大多数项目根本不会去关心这些细节。Pretext 把它们也纳入布局层管理,避免出现“方向对了、装饰线错位”这种问题。
1.2 为什么传统方案不够用
很多人会觉得,我又不做阿拉伯语和竖排,这东西跟我没关系。但有三个场景,早晚会找上你。
第一个是国际化。现在的后台管理系统、出海应用,几乎都要支持多语言。就算现在只做中英文,保不准哪天产品经理就会提一嘴“我们下个版本要上中东市场”。到那时候再改布局体系,成本比从开始就用逻辑属性高好几倍。我见过一个项目,RTL 切换靠一个全局 class 覆盖几百个选择器,每次版本迭代都提心吊胆。
第二个是响应式大屏。vue3+element plus 那套自适应大屏方案里,很多布局是用 flex 或者 grid 撑开的。flex 本身会跟着 direction 翻转,可一旦某个子元素用了 margin-left,栅格错位就来了。Pretext 的逻辑属性体系在大屏自适应里也站得住脚,因为它不关心具体屏幕宽度,只关心块级方向和内联方向。
第三个是文档排版。电子书、古籍阅读器、诗词展示、甚至简历设计,都有横排竖排混排的需求。纯用 writing-mode 切竖排不难,难的是竖排之后,段落间距、标点压缩、数字横排这些细节怎么处理。Pretext 把这些细节封装成统一的类,让排版设计者不用去背 CSS 规范。
1.3 适用场景和对象
这套思路最适合三类人。第一类是国际化项目的前端,需要处理 RTL、多语言文本布局;第二类是阅读器、文档站、排版工具的前端,需要处理竖排、中西混排;第三类是准备面试的前端工程师,文本方向布局几乎是高级岗位躲不开的考点,把 Pretext 这套方法论讲清楚,比背八股文有说服力得多。
换句话说,Pretext 不是一个必须安装的 npm 包,它更像一种设计约定:用文本的逻辑流向组织布局,而不是用屏幕的物理坐标组织布局。掌握了这个约定,不管以后出现什么新框架,你都能在文本布局这块游刃有余。
2. 文本方向布局的关键原理
2.1 物理属性与逻辑属性的博弈
物理属性就是 margin-left、padding-top、border-right、left、top 这些,它们绑定的是屏幕坐标。逻辑属性是 margin-inline-start、padding-block-end、border-inline-end、inset-inline-start 这些,它们绑定的是文本流方向。这里的 inline 指的是文字书写方向上的“行内方向”,block 指的是段落堆叠的“块级方向”。
我打个比方。物理属性像是按地图方位指路,“往北走 100 米再往西拐”,逻辑属性是按人的左右指路,“往前走 100 米再往左拐”。在中文里,“前”“后”“左”“右”是相对的,换个人面朝另一个方向,指令依然成立;但“北”“南”是绝对的,人换了方向就错了。文本布局也一样,只要界面的书写方向一变,物理属性就全乱,逻辑属性会自动跟着文字方向走。
在实际开发中,我会给自己定一条硬性规则:布局相关的间距、定位、边框,一律使用逻辑属性;只有盒模型本身相关的 width、height 这种明确的物理尺寸,才继续用传统写法。这样做的好处是,当项目需要做 RTL 或者竖排的时候,我的样式代码基本不用动,只要在根节点切换 direction 和 writing-mode 就行。
2.2 writing-mode、direction 与 unicode-bidi 三兄弟
文本方向布局里,有三个 CSS 属性最容易让人混淆,分别是 writing-mode、direction 和 unicode-bidi。
writing-mode 决定的是整个文档或者某个元素内部的书写模式。默认是 horizontal-tb,即横向书写、块级方向自上而下;vertical-rl 是纵向书写、块级方向从右到左,类似古书排版;vertical-lr 是纵向书写、块级方向从左到右,常见于部分日文排版。
direction 决定的是内联元素的起始方向。ltr 是左到右,rtl 是右到左。它不改变块级方向,只改变一行内文字和行内元素的走向。大多数情况下,RTL 布局只需要把 direction 切到 rtl,flex 和 grid 会自动跟随翻转,但前提是你的 margin、padding 都是逻辑属性。
unicode-bidi 则处理的是双向文本算法中的嵌套方向。比如一段阿拉伯语里夹杂英文,浏览器怎么决定谁从左往右、谁从右往左,就是 unicode-bidi 和 direction 协同工作的结果。Pretext 在封装时,把这三个属性统一到一个方向上下文里,通过一个>:root { --ptx-writing-mode: horizontal-tb; --ptx-direction: ltr; --ptx-text-orientation: mixed; --ptx-block-gap: 1.5rem; --ptx-inline-gap: 1rem; } [data-ptx-mode="rtl"] { --ptx-direction: rtl; } [data-ptx-mode="vertical"] { --ptx-writing-mode: vertical-rl; --ptx-text-orientation: upright; }
细看这段代码,我并没有直接把 writing-mode 写到某个类上,而是统一通过变量来控制。好处是,后续你可以在 JavaScript 里动态修改>.ptx-context { writing-mode: var(--ptx-writing-mode); direction: var(--ptx-direction); text-orientation: var(--ptx-text-orientation); }
这段代码看似简单,但它把 writing-mode、direction、text-orientation 三者的组合关系封装成了一个类。你在 HTML 里只要写<div class="ptx-context"><div class="ptx-context">.ptx-card { padding-inline: var(--ptx-inline-gap); padding-block: calc(var(--ptx-block-gap) * 0.75); border-inline-start: 4px solid #2563eb; } .ptx-card__title { margin-block-end: 0.5rem; } .ptx-card__desc { text-align: start; } .ptx-card__tag { margin-inline-start: auto; }
这段代码里藏着几个关键点。padding-inline 就是左右方向的内边距,在 RTL 下它会自动左右互换,你不用写两套。border-inline-start 是文本起始方向的边框,在 LTR 下是左边框,在 RTL 下变成右边框,一个卡片左边框变右边框,布局就完成了镜像。text-align: start 是文本对齐的逻辑写法,比 left 更安全。最有意思的是 margin-inline-start: auto,这个用法可以把一个元素推到文本流的末尾方向,在 RTL 下标签会自动跑到左侧,卡片布局瞬间镜像,样式代码一行都不用改。
竖排模式下,这套逻辑同样是有效的。还是这个卡片,只要把>import { ref, computed } from 'vue'; const modeMap = { ltr: { writingMode: 'horizontal-tb', direction: 'ltr', textOrientation: 'mixed' }, rtl: { writingMode: 'horizontal-tb', direction: 'rtl', textOrientation: 'mixed' }, vertical: { writingMode: 'vertical-rl', direction: 'rtl', textOrientation: 'upright' }, }; export function useTextLayout(initialMode = 'ltr') { const mode = ref(initialMode); const ptxStyle = computed(() => { const conf = modeMap[mode.value]; return { writingMode: conf.writingMode, direction: conf.direction, textOrientation: conf.textOrientation, }; }); function setMode(nextMode) { mode.value = nextMode; } function toggleMode() { const keys = Object.keys(modeMap); const idx = keys.indexOf(mode.value); mode.value = keys[(idx + 1) % keys.length]; } return { mode, ptxStyle, setMode, toggleMode }; }
这段代码里,我特意把 writingMode、direction、textOrientation 一起塞进了 ptxStyle,这样在模板里可以直接用 :style 绑定。不要只在 direction 上做文章,writing-mode 不变的话,再怎么切 direction,中文古籍也不会变成竖排。
在组件里用的时候,也很简单:
<template> <section class="ptx-context" :style="ptxStyle"> <slot /> </section> <button @click="toggleMode">切换排版方向</button> </template> <script setup> import { useTextLayout } from '@/composables/useTextLayout'; const { ptxStyle, toggleMode } = useTextLayout('ltr'); </script>这样一个面向方向的容器组件就出来了。你可以在它内部放任何业务组件,切方向只动容器,内部组件的逻辑属性自动生效。我之前在一个阅读器项目里试过,整套排版从横排切竖排,只改了这个容器组件的状态,内部十几个组件完全没有动过样式,实测下来很稳。
3.4 用配置化 JSON 驱动整个页面布局
到这里,单组件方案已经能用了,但离“革命性突破”还差一步。我把 Pretext 的路子再往前走了一点:让文本布局可以像配置数据字典一样,用一个 JSON 对象全盘描述。为什么要这样做?因为前端项目里,布局方案的调整往往不是开发者一个人说了算,产品、设计、运营都会参与,他们不懂 CSS,但都看得懂 JSON。
{ "page": { "mode": "vertical", "contextClass": "ptx-context" }, "header": { "tag": "header", "direction": "ltr", "blockGap": "1.25rem" }, "content": { "tag": "main", "direction": "rtl", "textAlign": "start", "deco": { "emphasis": "filled", "underlineOffset": "0.25em" } }, "footer": { "tag": "footer", "direction": "ltr" } }这个 JSON 里,page 定义了整页的书写模式,header、content、footer 各自定义了方向、间距和装饰配置。前端要做的事情,就是把这个 JSON 解析成 CSS 变量和类名,渲染到对应的 DOM 节点上。这里我只展示一个小型的实现思路。
function applyConfig(config) { document.documentElement.style.setProperty('--ptx-block-gap', config.page.blockGap || '1.5rem'); const sectionEls = document.querySelectorAll('[data-ptx-section]'); sectionEls.forEach((el) => { const key = el.dataset.ptxSection; const section = config[key]; if (!section) return; if (section.direction === 'rtl') { el.setAttribute('dir', 'rtl'); } else { el.removeAttribute('dir'); } if (section.deco) { el.classList.add('ptx-deco'); el.style.textDecorationStyle = section.deco.emphasis ? 'wavy' : 'solid'; el.style.textUnderlineOffset = section.deco.underlineOffset || '0.1em'; } }); }这段脚本的好处在于,后续如果产品要做 A/B 测试,或者运营要临时调整某个版块的排版方向,只需要改配置文件,前端不用发版。我在一个双语资讯站里跑过这套方案,中东版和中文版共用同一套代码,只差一份 JSON 配置,效果比预期好很多。
4. 常见问题与排查技巧实录
4.1 兼容性:逻辑属性和 writing-mode 的底线在哪
很多同行一听逻辑属性就担心老旧浏览器,实际上现代浏览器的支持已经非常完善。Chrome、Edge、Firefox、Safari 在 2023 年之后的主流版本里,对 margin-inline、padding-block、border-inline-start 这些属性基本都支持了。writing-mode 更不用多说,IE 时代就有,只是当时用的还带前缀,现在早就不用了。
如果你必须支持 IE11 或者极老的内核浏览器,我建议用 @supports 做降级。
.ptx-card { padding-left: 1rem; padding-right: 1rem; } @supports (padding-inline: 1rem) { .ptx-card { padding-left: unset; padding-right: unset; padding-inline: 1rem; } }先写一遍物理属性兜底,再在支持的浏览器里用逻辑属性覆盖。这样至少能保证老浏览器不崩,新浏览器享受逻辑属性的便利。如果项目本身有 PostCSS 构建链,也可以查一下 postcss-logical 插件,它会把逻辑属性自动转成物理属性,省去手写兜底的麻烦。
4.2 竖排文本与标点处理的坑
竖排模式好看,但细节非常多。我最开始做竖排时,第一个遇到的问题是中英文和数字混排。默认竖排模式下,数字和英文单词会整体旋转 90 度,看起来特别别扭。我查了一圈,发现 text-combine-upright 这个属性可以解决数字横排在竖排文本里的问题。
.ptx-deco-numeral { text-combine-upright: digits 2; }text-combine-upright: digits 2 的意思是把最多两位数字横向压缩,整体正立出现在竖排文本里。这个在竖排的日期、页码、电话号码场景里非常有用。需要注意的是,这个属性适合两到三位数字,长度太长会压缩得没法看。
第二个坑是标点压缩。竖排中文时,句号、逗号、引号应该压在文字的一角,而不是单独占一个宽字符位置。CSS 的 text-spacing-trim 属性是可以处理这个的,但这个属性目前 Chrome 支持得还不好。退而求其次,我会在竖排容器里手动把标点的字号缩小一点,或者给标点加一层 white-space 处理,让标点贴着上一个字符走。
第三个坑是下划线。横排里 text-decoration: underline 很简单,竖排里下划线默认跑到文字底下,看起来像是装饰消失了。我实测下来的处理方式是:竖排容器里用 text-underline-offset 配合 writing-mode 调整偏移量,同时改成 text-decoration-line: underline 加 text-underline-position: under,让竖排里的强调线能顺着文字方向走。
4.3 切换语言时布局崩掉的排查顺序
遇到切语言布局崩溃,不要慌,按顺序排查,大概率几分钟就能定位。
先看方向上下文有没有生效。打开开发者工具,选中最外层容器,检查 writing-mode 和 direction 实际计算值,确认不是别的样式覆盖掉了。再看内部元素用的是物理属性还是逻辑属性。如果某个元素还在用 margin-left 这种写法,方向切换它自然不会动。
接着看定位类型。absolute 定位的元素,方向切换后参考点不会自动翻转。我后来养成的习惯是:能用 inset-inline-start、inset-inline-end 就用,不要写 left、right。再看 flex 和 grid 内部是不是有显式排序。flex 默认感知 direction,但 flex-direction: row 不会变,flex-direction: row-reverse 也不会变,所以尽量不要手动去 reverse,让 direction 自己去控制。
最后看文本对齐。text-align: left 是个隐蔽杀手,很多布局崩了查来查去,就是因为它。把所有 text-align: left/right 换成 start/end,至少能避免一半以上的 RTL 问题。
4.4 面试里怎么把 Pretext 思路讲出价值
文本方向布局在面试题里出现频率不低。常见的问法是“怎么实现一个双语网站的无障碍布局”或者“RTL 下你的样式会出什么问题”。如果你只回答“加个 dir=rtl”,那基本就是初级水平。把 Pretext 这套思路讲清楚,面试观感会完全不同。
我会这样拆解回答:先抛出问题,文本方向是布局的隐藏维度,物理属性在这里会失效。然后分三层讲解决方案,第一层是逻辑属性,把 margin、padding、border、inset 全部换成 inline/block 维度,保证布局随方向自动镜像;第二层是书写模式,用 writing-mode 和 text-orientation 处理竖排和字符方向;第三层是工程化封装,通过组合式 API 或者配置化 JSON,把方向状态从组件里抽离出去,让设计师和运营也能参与排版调整。
这三层讲完,面试官基本能判断你是踩过坑的人,而不是背八股文的。最后你还可以补一句:真实项目里,这套方案最重要的收益是,开发、产品和设计之间多了一层配置化沟通语言,方向切换不再是代码改动,而是数据改动。
我在实际使用中还有一个自己的体会:做文本方向布局,第一步不是写代码,而是把你项目里所有物理属性全部查一遍。你可以在编辑器里全局搜margin-left、padding-right、left:、right:、text-align: left,把它们换成逻辑属性,再上方向切换,基本所有布局问题都会浮出水面。最后再分享一个小技巧:在你做好的方向容器根节点上,加一个transition: writing-mode 0.2s, direction 0.2s,这样切换横排竖排的时候,整个版面会有一个平滑过渡效果,视觉上比硬切高级不少,用户在阅读器里来回切换时体验会很舒服。