中文网页排版优化之字距调整和首尾字符控制
已于2015年9月2日14:00重新修订了源代码
难以标准的中文排版
在编写 WordPress 主题的时候,我重新认识到网络标准对中文排版的轻视,几乎每个细节都会出现各种问题。这对于略有强逼症的我来说,简直不能忍啊。
起初我只是想实现首行缩进,但发现只有部分段落缩进了,查询得知是 br
标签不支持缩进。好吧,这个问题很好解决,~~把 br 标签全局替换成 p 标签可以解决掉。~~用 li
标签来实现零行距的缩进。但接踵而来的是标点换行,标点会在换行后出现在行首,这明显不符合中文的排版标准,在 CSS3 里找到个似乎相关的属性,结果只有 IE 才支持。最后是行对齐,理想的排版应该是两端对齐,但 CSS 里只支持英文两端对齐。
来回研究,在排版方面,居然是最为人唾弃的 IE 做得最好,标点能正确换行,也可动态调整中文的字距来实现两端对齐,只能说,不愧是做了 Office 的微软啊……
网上搜索了一通,没发现有什么好的解决办法,还是大神都藏了一手?但看了好些网站的排版,其中不乏知名文学类的,确实也没解决掉我发现的问题。
直到前几天在浏览 HTML 5 标准的时候,无意中看到了个标签 wbr
,忽然灵机一动——或许这就是解决问题的一个关键。没想到接着脑洞大开,把问题一个接一个地解决了。我的方法可能不是独创,但确实有其价值,值得单独开一篇文章,以下是我的独家分享。
字体大小和行距
正文的字体大小,一般从 12~16px 中选择。我选择在文章正文中使用 16px
,摘要则是 14px
,并设置了 1.5 倍行距。
这只是全凭我个人喜好的选择,具体要选择怎样的字体大小,还要看内容和网站定位而定。
段首缩进和段间距
曾经看到一个说法,段首缩进在网页中并不是必要的。段首缩进和段间距都是用来区分段落,在空一行和缩进两字宽之间对比,似乎空一行的视觉效果更好。
翻开我手上不少的图书,有很多是不设置段间距,只靠段首缩进来区分段落。而同样的版面,在网页上居然会感觉有点密密麻麻,没那么舒服。
排除媒体方面的作用,这可能是因为我们看网页是快速阅读,而阅读书本是一种慢阅读,心态决定一切,我们更愿意在网页上直观地看到清晰明确的逻辑关系。
当然,段首缩进和段间距并不是二选一的关系,而是可以同时采用,这样既有清晰的段落逻辑,也兼顾出版的中文标准。
实现段首缩进的方式有几种,一是打两个全格空格,这个方法的好处是对 br 标签也生效,缺点是有些编辑器会吞掉这些空格,比如 WordPress。二是在 CSS 里实现,好处是兼容性高,缺点是可能把一下不想缩进的标题或图片也缩进了,也不对 br 标签起作用。第三种是手动在需要缩进的段落插入设置了缩进的 div 或 p 标签,好处是排版准确度高,缺点就是需要手动。本人采用的是第三种方法。
~~前面多次提及,br 标签,即硬回车换行是不会对缩进起作用的,所以我通过代码把它替换成 p 标签,又因为 br 本身是没有段间距的,所以我又给它添加了一个 间距为 0 的 CSS 类。~~改变了想法,br 还是有保留它的必要,不能这样把它替换掉,改用 li
标签来实现无行距的换行,效果会更好。问题是,br 可以由编辑器自己生成,li 就需要你去手动插入了。
在 css 里添加样式
/* 段首缩进两个字 */
.indent{text-indent:2em}
WordPress 也可以把以下代码添加到主题的函数模板 functions.php 里来添加按钮。
// 替换 br 为 无段间距的 p
function Replace_Html($html) {
$html=str_replace('<br />','</p><p class="br">',$html);
return $html;
}
add_filter('the_content','Replace_Html');
//在文本编辑器中添加按钮
add_action('admin_print_footer_scripts','wpjam_add_quicktags');
function wpjam_add_quicktags() {
if(wp_script_is('quicktags')) {
?>
<script type="text/javascript">
QTags.addButton('indent2em','缩进','\n\n','\n\n');
</script>
<?php
}}
[coll title="旧方法,已弃用"]
在 css 里添加样式
/* 将 br 的段间距设置为 0 */
.br{margin-bottom:0!important}
/* 段首缩进两个字 */
.indent{text-indent:2em}
jQuery通用代码,替换 br 标签为无段间距的 p
$('.the_content').html(function() {
return $(this).html().replace(/
/ig,'');
});
除了 jQuery 之外,WordPress 也可以把以下代码添加到主题的函数模板 functions.php 里来实现。
// 替换 br 为 无段间距的 p
function Replace_Html($html){
$html=str_replace('<br />','</p><p class="br">',$html);
return $html;
}
add_filter('the_content','Replace_Html');
//在文本编辑器中添加按钮
add_action('admin_print_footer_scripts','wpjam_add_quicktags');
function wpjam_add_quicktags() {
if(wp_script_is('quicktags')){
?>
<script type="text/javascript">
QTags.addButton('indent2em','缩进','\n\n','\n\n');
QTags.addButton('pbr','0行距','<p class="br">\n','\n</p>');
</script>
<?php
}
}
[/coll]
WordPress 请在 jQuery 和函数模板两种方式中任选一个,两者有本质区别,函数模板里的代码是在服务器段上运行,会将修改直接输出到 HTML 源代码中;而 jQuery 是在本地浏览器中执行,jQuery 修改的内容不会体现在 HTML 源代码中,只能在浏览器的开发者工具中查看。如果是增量修改的话,前者会增加 HTML 的体积,增加传输成本,而 jQuery 则不会对传输有任何影响。后面优化排版的代码可能会使 HTML 的体积成倍增加,因此全部改由 jQuery 实现。
中文标点
首先,在 Windows 里,相信不少网站都是使用了微软雅黑,但雅黑的部分全角标点很奇怪,一点也不适合中文排版,所以我选择调用另一个字体来做标点。为节省资源,我们需要使用一些工具对字体进行压缩,使它只保留我们需要使用的标点,网上有很多网站能做到这种效果,这里推荐一些。
阿里妈妈webfont平台
和图标字体一样,推荐它的原因是完全免费以及提供了字体托管,操作简单,甚至连注册也不需要,但阿里这个站的缺点也很明显,就是只有思源黑体
这个字体,没有其他选择。
有字库
功能非常强大,有多种字体可供选择,如果你要的字体没有授权,甚至可以自己上传,也有托管服务。缺点是这个网站的操作比较繁复,需要注册才能使用,只提供调用字体的代码,而不能下载压缩好的字体本身。总之因为太复杂,我没做过多的尝试。
字蛛
这个不是网站服务,更是一个工具,可以在本地压缩字体,优点是无限制。确定是无托管服务,虽然把字体上传到自己的空间或[七牛云存储](https://portal.qiniu.com/signup?code=3ln23sakgyv6a)
等地方,使用起来也比较复杂。
百度字体编辑器
这是一个在线工具,可以上传字体进行编辑和转换格式,它的编辑功能相当强大,但它本身又不能简单地压缩字体,所以我是在需要修改已压缩的字体的时候使用它。
除了 Internet Explorer 之外,其他浏览器都不能正确地换行中文标点,常常出现把标点换行到行首的情况。我在随意翻阅 HTML 标准的时候,看到 wbr
这个标签。
如果单词太长,或者您担心浏览器会在错误的位置换行,那么您可以使用 wbr 元素来添加 Word Break Opportunity(单词换行时机)。
当我看到它的时候,第一时间想到,如果把 wbr
添加到中文标点后面,不就可以正确地换行吗?经过测试,它的确能达到我的要求,只是为什么我没看到有什么网站去使用它呢?从兼容性来说,只有 IE 浏览器不支持 wbr
,但 IE 本身就能正确对标点换行,因此也就没关系了。
两端对齐和字距调整
网页默认的排版是靠左对齐,所以在中文网页里,经常会看到右边会有空缺。特别在中英混排中,如果设置了不在单词内换行,有时这空缺会更明显,影响观感。
虽然可以在 CSS 里改成两端对齐,然而并没有什么卵用。如果是纯中文,实际效果和靠左对齐是一样的,只有在中英混排时体现出不同,特别是段落中含有空格的时候,这些空格有时就会被拉伸得非常宽广,看起来奇怪极了。这虽然是实现了两端对齐,但无疑又带来了更奇怪的排版。
那么正确是怎样排版的呢,可以去看看 IE 或者 Word 的做法,它们是通过压缩或拉伸每个字符(包括中文、英文、标点等)的字距来实现两端对齐的,和只能拉伸空格的 Chrome、Firefox、Safari、Opera 有本质上的差别。
慢着,既然它们是只对空格进行处理,那么实际上并不是不支持中文,而是只对空格起作用呢?这时我脑海里又浮现出某些港台网站在每个字中间加空格的做法,这实际上除了不让繁体字看起来密密麻麻之外,也是为了实现两端对齐?
经过验证,确实就是这样的道理,在文字中间加空格就能解决问题了,但在简体中文中加空格并不好看,所以我又找来 kbd 标签来嵌套这些空格,并添加 CSS 样式来调整宽度和字距。经过一番修改,终于离我理想中的效果越来越近了。
最终代码
本文的 JavaScript 代码基于 jQuery 库,我不太懂 JavaScript,因此无法提供无需 jQuery 的代码。本文的全部代码已经在所有主流浏览器中测试通过,如有有异常,应该是你的浏览器版本太旧了。
[coll title="CSS 样式"]
/* 这处用的是阿里妈妈 webfont 平台的字体,只含有部分标点 */
@font-face {font-family: 'webfont';
src: url('//at.alicdn.com/t/b6a4gx5uj2xrbe29.eot'); /* IE9*/
src: url('//at.alicdn.com/t/b6a4gx5uj2xrbe29.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('//at.alicdn.com/t/b6a4gx5uj2xrbe29.woff') format('woff'), /* chrome、firefox */
url('//at.alicdn.com/t/b6a4gx5uj2xrbe29.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
url('//at.alicdn.com/t/b6a4gx5uj2xrbe29.svg#NotoSansHans-DemiLight') format('svg'); /* iOS 4.1- */
}
/* 在标点的样式上应用 webfont,注意这里别设置字体大小 */
font{font-family:'webfont'!important}
/* 把空格的宽度设置为 0,再减去 0.4em 的字距,这刚好和未加空格的文本是一样的字距 */
kbd{width:0!important;letter-spacing:-.4em}
/* 这是我正文的 CSS 样式 */
.the_content{
text-align:justify; /* 文本两端对齐 */
text-justify:distribute; /* 文本两端对齐 */
word-break:break-word; /* 只对超长的英文单词断句 */
}
/* 将段间距设置为空一行 */
.the_content p{margin-bottom:1em}
[/coll]
[coll title="jQuery 代码"]
$('body').html(function() {
return $(this).html().replace(/[\u2010-\u203B]|[\u3001-\u301E]|[\uFE10-\uFE44]|[\uFF01-\uFF0F]|[\uFF1A-\uFF20]|[\uFF3B-\uFF40]|[\uFF5B-\uFFE5]/g,'<font>$&</font>').replace(/<\/font><font>/ig,'');
});
/*
我选择的元素是整个 <body>,这将对网站的所有可见内容进行修改,示例中没包含所有的标点,你可以自己添加,如果要添加半角标签,请加上一个斜杠 \,比如「\,」。
修改内容:
1. 在全角标点嵌套 <font> 标签
2. 删除连续标点中间的 <font> 标签,变成只用一套 <font> 标签来嵌套连续的标点
*/
$('.the_content,.preview').html(function() {
return $(this).html().replace(/<\/font>/ig,'$&<wbr>').replace(/[\u4E00-\u9FFF]|<wbr>/ig,'$&<kbd> </kbd>').replace(/<kbd> <\/kbd> /ig,' ').replace(/<kbd> <\/kbd><font>/ig,'<font>');
});
/*
这处只选择了文章正文和主页的文章摘要的 div(请根据你的网站修改选择器里的 div 类),因为使用 <wbr> 标签会使禁止换行的 CSS 无效,可能会出现换行错误。
修改内容:
1. 在标点的 <font> 标签后插入 <wbr> 标签
2. 在所有宽字节字符(包括汉字、日文)和 <wbr> 标签后插入 <kbd> 空格
3. 如果 <kbd> 空格后面紧接着一个未嵌套的空格,则删除嵌套了 <kbd> 的空格
4. 将全角标点之前的 <kbd> 空格删除
2015/09/02 修订:加入浏览器判断,如果是IE就不再插入空格,因为IE本身就能完美排版
*/
$('.prettyprint').html(function(){return $(this).html().replace(/<kbd> <\/kbd>/ig,'');});
/* 如果正文中有不适应插入空格的内容,针对它的 CSS 类进行删除 */
$('a').each(function() {
var t=$(this).attr('title');
if(t) {
t=t.replace(/<font>/ig,'').replace(/<\/font>/ig,'').replace(/<wbr>/ig,'').replace(/<kbd> <\/kbd>/ig,'');
$(this).attr('data-title',t);
}
});
/*
除了直接显示的文字之外,网页里还包含了一些鼠标悬停显示的文字,这些 title 属性的内容不会对 HTML 标签进行转义,因此要特别删除里面的标签。
这行代码的功能是删除 <a> 标签内 title 属性中的 <font> 和 <wbr> 标签。
你的网站也可能在其他地方用了 title 属性,请添加到选择器中。
如果你用了 :after 和 :befot 等 CSS 选择器或第三方库来取代浏览器默认的鼠标悬停样式,你可能需要把代码中的 title 改成 data-title。当然,一切都依你网站的具体设计而定。
比如本站使用了 Layer 弹出层库来取代浏览器默认的鼠标悬停样式,Layer 弹出层支持对 HTML 标签进行转义,因此我可以保留 <font> 标签,只需要删除可能会造成排版混乱的 <wbr>
2015/09/02 修订:加入浏览器判断,如果是IE就不再插入空格,因为IE本身就能完美排版
*/
[/coll]
[coll title="如果你不想替换标点的字体,可以把代码改成这样"]
$('body').html(function() {
return $(this).html().replace(/[\u2010-\u203B]|[\u3001-\u301E]|[\uFE10-\uFE44]|[\uFF01-\uFF0F]|[\uFF1A-\uFF20]|[\uFF3B-\uFF40]|[\uFF5B-\uFFE5]/g,'$&<wbr>').replace(/<wbr>(.)<wbr>/g,'$1<wbr>').replace(/<wbr>(.)<wbr>/ig,'$1<wbr>');
});
/*
后两次重复的替换是为了删除连续标点之间的 <wbr>,两次替换可以处理三个连续的标点,因为中文里连续的标点一般不超过三个,一般是够用了,觉得不够可以重复多一两次
*/
$('.the_content,.preview').html(function() {
return $(this).html().replace(/[\u4E00-\u9FFF]|<wbr>/ig,'$&<kbd> </kbd>').replace(/<kbd> <\/kbd> /ig,' ').replace(/<kbd> <\/kbd><font>/ig,'$&<font>');
});
$('a').each(function() {
var t=$(this).attr('title');
if(t!=''&&t!=null) {
t=t.replace(/<wbr>/ig,'').replace(/<kbd> <\/kbd>/ig,'');$(this).attr('data-title',t);
}
});
[/coll]
该方法的不足之处是,在复制网页上的文字时会把空格一起复制,这必然会造成一定的困惑。我尝试通过 JavaScript 实现复制文字的时候,从剪贴板中移除文字间的空格,但我在网上抄了不少代码,都不起作用,原来是只有 IE 支持使用 clipboardData 访问系统剪贴板。希望谁能给我提供一个可行的跨浏览器代码。总算编写好可以实现的代码……
[coll title="复制文本时去掉多余空格"]
/* 插入到 body 标签中 */
<script type='text/javascript'>
function remove_space() {
if(navigator.userAgent.indexOf('MSIE')>0||navigator.userAgent.indexOf('Trident/')>0) {
/* 判断浏览器是否IE,如果是IE,执行以下的代码 */
window.setTimeout(function() {
var text=clipboardData.getData('text');
if(text) {
text=text.replace(/([^\x00-\xff])\s([^\x00-\xff])/g,'$1$2').replace(/([^\x00-\xff])\s([^\x00-\xff])/g,'$1$2').replace(/([\u2010-\u203B]|[\u3001-\u301E]|[\uFE10-\uFE44]|[\uFF01-\uFF0F]|[\uFF1A-\uFF20]|[\uFF3B-\uFF40]|[\uFF5B-\uFFE5])\s(\w)/g,'$1$2');
clipboardData.setData('text',text);
}
},100);
} else {
/* 非 IE 浏览器执行以下代码 */
var body_element = document.getElementsByTagName('body')[0];
var selection=window.getSelection();
var selection_s=selection.toString();
var copy_text=selection_s.replace(/([^\x00-\xff])\s([^\x00-\xff])/g,'$1$2').replace(/([^\x00-\xff])\s([^\x00-\xff])/g,'$1$2').replace(/([\u2010-\u203B]|[\u3001-\u301E]|[\uFE10-\uFE44]|[\uFF01-\uFF0F]|[\uFF1A-\uFF20]|[\uFF3B-\uFF40]|[\uFF5B-\uFFE5])\s(\w)/g,'$1$2');
var new_div=document.createElement('div');
new_div.style.left='-99999px';
new_div.style.position='absolute';
body_element.appendChild(new_div);
new_div.innerText=copy_text;
selection.selectAllChildren(new_div);
window.setTimeout(function() {
body_element.removeChild(new_div);
},0);
}
}
document.body.oncopy = remove_space;
</script>
代码解释:
IE 浏览器使用 window.clipboardData 对象访问剪贴板,这个函数相当简单直接,但只支持 IE 浏览器。其他浏览器则使用了 Selection API,这个接口几乎支持所有的浏览器,除了 IE 6/7/8……
Selection API 的用法有些复杂:
- 获取剪贴板对象
- 把剪贴板对象转换成字符串,注意获取文本不再是网页源代码,没有 HTML 标签了,所以下面移除空格也变得稍为负责
- 用正则表达式去除字符串中多余的空格:两个中日韩文字之间的空格、全角标点后面的空格
- 因为 Selection 不能直接把字符串获取到剪贴板,所以在 body 里新建一个 div 元素,并把 div 定位在窗口外面,使其不可见
- 将字符串写入到新建的 div 中,注意我这里用的是
innerText
,我原来参考的代码用的是innerHtml
,区别在于后者会对 HTML 标签进行转义,如果文章里有贴出源代码的话,访客复制的文本可能就被改变了,后者也不支持 \n 换行符,这样复制出来的文本是不换行的。改用 innerText 可解决这些问题 - 重新从 div 中获取 Selection 对象到剪贴板
- 删除 div 元素
[/coll]
此外,IE 浏览器应该(注意是“应该”,因为我未测试早期的 IE 版本,不敢打包票)是不需要在汉字中间插入空格也能两端对齐,所以有不少代码可以加入条件判断,如果是 IE 就不执行。所以部分代码可以改成以下的样子。
[coll title="加入 IE 条件判断"]
/* 中日韩文字间插入空格的代码 */
if(navigator.userAgent.indexOf('MSIE')<0||navigator.userAgent.indexOf('Trident/')<0) {
// 首先判断浏览器是否IE,非 IE 才继续执行代码
$('.the_content,.preview').html(function() {
return $(this).html().replace(/<\/font>/ig,'$&<wbr>').replace(/[\u4E00-\u9FFF]|<wbr>/ig,'$&<kbd> </kbd>').replace(/<kbd> <\/kbd> /ig,' ').replace(/<kbd> <\/kbd><font>/ig,'<font>');
});
}
/* 复制文本时去掉多余空格 */
<script type='text/javascript'>
// 首先判断浏览器是否IE,非 IE 才继续执行代码
if(navigator.userAgent.indexOf('MSIE')<0||navigator.userAgent.indexOf('Trident/')<0) {
function remove_space() {
var body_element = document.getElementsByTagName('body')[0];
var selection=window.getSelection();
var selection_s=selection.toString();
var copy_text=selection_s.replace(/([^\x00-\xff])\s([^\x00-\xff])/g,'$1$2').replace(/([^\x00-\xff])\s([^\x00-\xff])/g,'$1$2').replace(/([\u2010-\u203B]|[\u3001-\u301E]|[\uFE10-\uFE44]|[\uFF01-\uFF0F]|[\uFF1A-\uFF20]|[\uFF3B-\uFF40]|[\uFF5B-\uFFE5])\s(\w)/g,'$1$2');
var new_div=document.createElement('div');
new_div.style.left='-99999px';
new_div.style.position='absolute';
body_element.appendChild(new_div);
new_div.innerText=copy_text;
selection.selectAllChildren(new_div);
window.setTimeout(function() {
body_element.removeChild(new_div);
},0);
}
document.oncopy = remove_space;
}
</script>
[/coll]
总结
不直接在 CSS 里设置段首缩进,而是改用手动插入,是为了降低出错的机会。而最稳妥的做法是自己写好所有该出现的 HTML 标签,最大限度地避免由程序去自动生成标签,只是这样的效率太低下,不值得提倡。
替换标点字体的初衷是为了改善显示效果,如果部分标点替换后的表现反而更差,就应该把这些标点从字体中删除。另外,现在大部分 CSS 中都设置了多个字体,其中需要替换标点字体的可能只有微软雅黑,这个修改会将所有字体的标点替换掉,有点一刀切。
如果你的网站有使用图标字体,那还可以把标点字体和图标字体合并,我的方法是先压缩好标点字体,然后把它上传到 IcoMoon,再下载字体,里面会有一个图标一个文件的 SVG 格式,把这些 SVG 文件上传到 Iconfont,合并到原来的图标字体中,再把标点图标的 Unicode 码修改成正确的就可以了(可以用百度字体编辑器打开原来的标点字体来查询 Unicode 码)。
这个两端对齐的排版总算是搞掂了,我曾经考虑在英文字母之间也插入这些看不见的空格,但我最终连测试也没有做,因为现在看起来也足够了。提醒一下,写文章的时候记得在英文和汉字之间手动输入一个空格,这也是中文排版的要求。
这篇文章只是抛砖引玉,期望高手能扔给我更好的解决方案。
PS. 才发现 blink 内核的浏览器已经能很好地实现中文两端对齐和首尾字符控制。
支付宝扫码打赏
微信扫码打赏
本博客文章采用 知识共享(Creative Commons) 署名-非商业性使用-禁止演绎 4.0 进行许可。