CSS

和React面临的问题类似, CSS已经发展得过于复杂.
有大量原本需要JavaScript才能实现的效果, 现在可以用纯CSS来实现.
由于硬件加速的存在, 能用CSS实现的效果, 总是推荐用纯CSS实现.
自然堆叠顺序是元素在DOM里的先后顺序, 在大多数情况下都不需要关心.
需要特意处理堆叠顺序的最常见情况是出现了浮动内容, 比如弹出下拉菜单.
堆叠上下文是HTML里的Z轴.
堆叠上下文可以由很多方式创建.
一个堆叠上下文里可能还嵌套着其他堆叠上下文.
堆叠上下文可以由很多方式创建.
具有相同父堆叠上下文的 同级子堆叠上下文之间根据z-index决定堆叠顺序.
默认值为auto, 相当于0, 可以为正值和负值, 数值越大离用户越近.
CSS规定当元素没有z-index属性时, 具有position属性的元素的渲染层总是比没有position属性的元素的渲染层要高.
因为这个原因, 只要将position属性设置为static(默认值)以外的值, 就可以让元素浮在其他元素上方.
本质上, 这是因为大多数position属性都会创建一个新的堆叠上下文,
新堆叠上下文在渲染时总是高于它们的父堆叠上下文.
参考:
  • https://developer.mozilla.org/en-US/docs/Web/CSS/position
  • https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index
float元素位于没有position属性的元素和有position属性的元素之间.
参考: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float
CSS的特征查询功能, 被所有现代浏览器支持.
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Conditional_Rules/Using_Feature_Queries
inline inline-block block
非块方向margin(通常是左右margin) 支持 支持 支持
块方向margin(通常是上下margin) 不支持 支持 支持
padding 支持 支持 支持
padding会影响行的高度[fn:padding与行高] 否 是 不适用
width, height 不支持 支持 支持
与其他元素相邻[fn:inline-block与换行] 是 是 否
transform 不支持 支持 支持
inline元素内只能存在inline元素, 如果在inline元素里存在block元素, 则会导致意料之外的渲染结果:
  • inline元素的响应范围超出实际的盒模型, 常见于a元素是block元素的父元素的情况.
以下方法只适合用于单行文本的垂直对齐.
vertical-align指的是元素和并行的其他元素在垂直方向上的对齐, 而不是元素内部的对齐方式.
换言之, 不存在text-align在垂直方向上的等价物.
<!-- 这种结构不能正确实现垂直对齐 -->
<label>
<input type="checkbox" />
text
</label>
<!-- 能够正确垂直对齐的结构 -->
<label>
<input type="checkbox" />
<span>text</span>
</label>
<style>
/* 必须在input和span上都设置vertical-align, 因为此属性不能继承 */
input, span {
vertical-align: middle;
}
</style>
<label>
<input type="checkbox" />
text
</label>
<style>
label {
display: inline-flex;
align-items: center;
}
</style>
与多行文本的第一行对齐, 这个需求暂时是无法通过CSS自动实现的.
在费尽力气利用flex属性尝试对齐后, 最终可能会得到一个与预期位置仅差几个像素的渲染结果, 很难不气馁.
将被对齐元素(inline-block)的高度(height)与文本的行高(line-height)匹配即可.
这种解决方案很难适应样式变更.
浏览器非常不愿意让一个非文字元素直接具有行高属性, 如果被对齐元素是以字体的形式显示的, 垂直对齐会容易得多.
创建脱离文本流的伪元素, 再手动调整其坐标偏移量将其对齐.
这种解决方案很难适应样式变更.
使用em单位设置SVG图标的大小与行高一致.
浏览器的亚像素处理算法是缺乏规范的, 不同的浏览器有不同的舍入和渲染方式.
在使用em, rem这样的相对单位时, 浏览器计算出来的px很可能不是整数,
进而在一些显示设备上出现亚像素渲染问题.
Firefox(v91.0.1)和Chrome(v92.0.4515.159)在border-radius的亚像素渲染上都存在变形的问题.
样式的优先级与选择器的特异性有关, 选择器的特异性越高, 则样式的优先级越高.
选择器特异性按以下顺序从低到高排列:
  1. 1.
    类型选择器, 伪元素选择器
  2. 2.
    类选择器, 属性选择器, 伪类
  3. 3.
    id选择器
当样式使用 !important 时, 会破坏优先级规则, 强制使用相关的样式.
类名顺序不会影响样式的优先级, 后添加的类名不一定会覆盖之前的类名.
通过 max-width: 100%, 图片将永远不会放大到超过其原始尺寸.
img {
max-width: 100%;
height: auto;
}
为img添加 max-height: 100%.
有别于将长宽中的一个设置为auto的等比缩放, 固定纵横比缩放的指的是由用户指定纵横比的缩放,
这种缩放不受资源本身的纵横比影响.
典型的使用场景是缩放像iframe这样的不具有比例的元素.
https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio
https://www.bram.us/2020/11/30/native-aspect-ratio-boxes-in-css-thanks-to-aspect-ratio/
https://www.bram.us/2017/06/16/aspect-ratios-in-css-are-a-hack/
使用vertical padding进行固定纵横比缩放是一个历史悠久的著名CSS hack,
它最终会被CSS4的 aspect-ratio 属性替代.
实施vertical padding的关键在于将盒子的高度设置为0,
然后用以宽度为比例计算的垂直padding来撑起盒子的高度.
按4:3缩放背景图片:
.ratio4-3 {
background-image: url(image.svg);
background-size: cover;
width: 100%;
height: 0;
padding: 0; /* reset padding */
padding-bottom: calc(3 / 4 * 100%);
}
当元素不是 position: static 时, 元素会响应top, right, bottom, left, z-index属性.
默认值, 元素处于文档布局流内.
元素处于文档布局流内, 相对其原本的位置进行偏移.
元素脱离文档布局流, 向上查找找到最近的不是 position: static 的祖先元素, 相对其位置进行定位.
如果没有找到这样的祖先元素, 就会选取html.
在不设定top, right, bottom, left的情况下, 元素会处于其原本在文档布局流的位置.
元素脱离文档布局流, 相对于视口(viewport)进行偏移.
表现与static相同, 但在页面向下滚动导致元素超出视口(viewport)可见范围时, 变成fixed.
margin collapse指两个相邻元素之间的margin被渲染引擎合并为其中更大的那个margin.
会出现margin collapse的情况:
  • 相邻元素
  • 完全空白的盒子
  • 父元素和第一个及最后一个子元素
不会出现margin collapse的情况:
  • position: absolute
  • 元素是Flexbox或Grid容器及其子元素
margin和padding的取舍关键在于属性对布局的影响(在 box-sizing: border-box 的情况下):
margin是盒模型"盒子外部"的一项属性, 这导致margin会使元素与其他元素拉开距离.
padding是盒模型"盒子内部"的一项属性, 使用padding不会对元素所处的布局有任何影响, 因此可以让元素布局效果更清晰.
由于margin collapse机制的存在, margin适用于需要折叠的情况, padding适用于不需要折叠的情况.
max-content 内容不会溢出的最大尺寸.
min-content 内容不会溢出的最小尺寸.
fit-content是max-content和min-content的结合体, 当空间足够时, 会选择max-content, 反之选择min-content.
::marker 在不同浏览器上的显示效果不同, 例如 list-style-type: disc; 在Chrome上有margin, 但Firefox上没有, 并且两者显示的图案也是有明显差异的.
::marker 最大的问题在于它目前允许使用的CSS属性少得可怜, 以至于无法通过CSS对样式进行归一化.
list-style-image在多大多数场合下都不如组合使用 ::before 和 background-image.
因为list-style-image的样式的可配置相当有限, 图片大概率不会按照期望显示.
显示出的::marker宽度为0.
content-box 默认值, 只有content会被算进盒模型的长宽, 因此在 width: 100% 时设置padding或border会导致盒子的尺寸超出width的设置.
border-box 常用值, 会将padding和border算进盒模型的长宽, 因此在 width: 100% 时设置padding和border, 盒子的尺寸仍然遵从width的设置.
默认值为auto, 这意味着元素将 扩展至其父元素下的剩余空间 (这相当反直觉, auto作为默认值实际上不如fit-content有用), 尊重broder, padding.
100%意味着使元素与父元素一样的宽度, 不尊重border, padding.
通过为子元素设置 box-sizing: border-box, 可让100%具有和auto类似的行为.
100%可以处理一些auto无法实现的填充情况, 例如 display: inline-block 的auto就可能无法填充.
子元素的 height: auto 通常是无用的.
在为元素设置height百分比值时, 需要其父元素有明确指定的高度, 否则百分比值无法生效.
若要在父元素没有明确指定高度的情况下, 让子元素的高度基于其父元素的高度, 需要将父元素的display改为 flex 或 inline-flex.
一维布局解决方案.
display: flex
flex元素及其子项默认具有 min-width: auto 和 min-height: auto.
参考: https://www.w3.org/TR/css-flexbox-1/#min-size-auto
这些默认值会导致部分元素无法按符合直觉的方式缩小, 其造成的结果就是元素溢出.
Chrome浏览器在2017年将部分元素的实际表现修改为了 min-width: 0; min-height: 0 的效果,
但Firefox并没有跟进这项修改, 这导致Firefox上更容易出现元素无法按理想方式收缩的情况.
为解决此问题, 建议将元素重置为 min-width: 0.
参考: https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size
为了更符合直觉, 开发人员可能会考虑将所有元素的min-width和min-height重置为0, 但这 不安全:
当元素的宽度可能被收缩得比min-content要小时, 文本会溢出显示, 因此错误样式可能在意想不到的地方出现,
为此需要再将它们重置为 min-width: min-content, 这样的话还不如只重置需要重置的元素.
建议重置的元素:
  • form
  • input
  • 嵌套了响应式img的容器
是flex-basis的默认值auto在作怪.
这是 建议使用flex的简写形式而不是单独设置flex-grow或flex-shrink 的主要原因.
这导致当需要设置相应属性时, 需要添加一层额外的包装.
标准解决方案是使用新增的 safe center 属性值, 需要注意的是Chromium直到115版本才正式支持这些属性值.
作为fallback, 可以使用传统的 margin: auto 来替代相关属性.
参考: https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container
flex 是 flex-grow, flex-shrink, flex-basis 的简写属性.
flex: initial 相当于 flex: 0 1 auto.
flex: auto 相当于 flex: 1 1 auto.
flex: none 相当于 flex: 0 0 auto , 元素既不扩张也不收缩.
flex: 1 相当于 flex: 1 1 0% (注意它的flex-basis被设置成了 0%).
一个很关键但经常被忽视的属性.
该属性表示元素的理想大小, 浏览器会在无法按理想大小显示时, 根据flex-grow和flex-shrink改变大小.
该属性的值为 auto 以外的值时, 会将元素上对应的width或height属性覆盖掉.
这就是为什么 flex: 1 (flex-basis: 0%) 会导致width或height被浏览器忽视.
属性的默认值为auto.
auto会导致在扩张和收缩时参考元素的初始尺寸(放入flexbox之前的尺寸),
这在将Flexbox用于布局时可能会导致flex-grow和flex-shrink以非预期的方式工作.
flex-basis: 0% 意味着元素的初始尺寸为0%, 即不占用空间, 此时元素的尺寸会完全由flex-grow和flex-shrink控制.
flex-basis: 100% 意味着元素的初始尺寸为100%, 即占满父元素的空间.
在用作布局时, 你很可能需要将子项的此属性重置为 0%, 因为你大概率会希望完全控制子项的显示尺寸.
在希望根据各个子项初始尺寸进行缩放时, 则需要其保持默认值 auto.
元素的扩张比率, 默认值为0(不扩张).
元素的收缩比率, 默认值为1(会收缩).
0 不收缩.
元素收缩能达到的最小大小是 min-content.
flex-flow 是 flex-direction 和 flex-wrap 的简写属性.
flex-flow: column wrap 相当于 flex-direction: column 和 flex-wrap: wrap.
决定元素布局方向和排列顺序.
  • row
  • row-reverse
  • column
  • column-reverse
决定元素是否在溢出时换行, 默认值为 nowrap 不换行.
  • nowrap
  • wrap
justify开头的CSS属性都与主轴有关, 默认(=flex-direction: row=)的主轴是x轴.
align开头的CSS属性都与侧轴有关, 默认的侧轴是y轴.
place-content 是 align-content 和 justify-content 的缩写.
主轴上存在多组元素时的空间分布.
flex-start, 默认值, 元素优先排在容器头部, 空白留在容器尾部.
flex-end, 元素优先排在容器尾部, 空白留在容器头部.
center, 元素优先排在中间, 空白留在容器头尾.
space-between, 均匀分布元素, 将空白留在元素与元素之间的空隙.
space-around, 均匀分布元素, 将空白留在元素与容器头部, 元素与容器尾部, 元素与元素之间.
容器头尾部分的空白比元素之间的空白要小.
space-evenly, 均匀分布元素, 类似于 space-around, 但头尾部分的空白与元素之间的空白更接近.
类似于 align-content, 但该属性将侧轴同一条线上的元素视作一组(例如一行或一列),
而不是将所有元素视作一组.
在默认情况下, 该值为 normal, 会将元素沿着侧轴拉伸.
典型的横向布局时, 会将所有元素的高度拉伸到最高, 通过 flex-start 取消此行为.
侧轴上存在多行元素时 的空间分布, 大部分值与 justify-content 一样, 只是作用在侧轴上.
一般语境下的侧轴对齐是 align-items, 而不是此属性.
stretch, 拉伸元素的侧轴尺寸, 从而占满侧轴空间.
单个元素的侧轴对齐, 该属性设置在具体元素而不是容器上.
Flexbox和Grid不需要使用margin创造间隙, gap更好用.
gap 是 row-gap 和 column-gap 的简写.
二维布局解决方案.
display: grid
在Chrome的开发者工具中的layout选项卡里, 可以开启与Grid有关的辅助功能, 例如显示行号和列号.
Grid使用与Flexbox相同的分布, 对齐, 空隙属性.
通过游戏学习它: http://cssgridgarden.com/
  • 页面整体布局
  • 二维的流式布局
  • 行/列子元素数量固定情况下的固定列数均分网格布局, 此时实际上是在模拟设计语境下的网格.
line: 网格由line组成, line有垂直和水平两种.
track: 连续的两条line之间的空间被称为track, 相当于人们熟知的"行"和"列".
cell: 垂直与水平方向两条连续的线之间切出的空间被称为cell, 即单元格.
area: 多个单元格组成的区域.
gaps: 单元格与单元格之间的空隙.
  • fit-content(wrap_value) 定义 fit-content, 但以 wrap_value 作为判断是否溢出的标准.
  • minmax(min_value, max_value) 定义track的最小和最大大小, 从而使网格具有弹性.
  • repeat(times, value) 将 value 重复 times 次, 其中 value 可以是多个值.
  • repeat(auto-fill, value) 流式网格布局, 尽可能多的定义行/列, 每一行/列的尺寸为 value, 如果行/列容纳不下, 则会换行/列.
    如果在 value 使用 minmax(), 则会出现类似于响应式设计的效果.
  • repeat(auto-fit, value) 与auto-fill类似, 但在只有一行/一列的情况下, auto-fit的单元格会自动拉伸以填满剩余空间.
grid-template 是 grid-template-rows, grid-template-columns, grid-template-areas 的简写形式.
.container {
display: grid;
grid-template:
"head head head" minmax(150px, auto)
"sidebar content content" auto
"sidebar footer footer" auto / 1fr 1fr 1fr;
}
这是一个多值属性, 每一个值代表一列的尺寸, 值可以是任何长度单位, 并且单位可以混用, 值的数量会决定网格的列数.
这是一个多值属性, 每一个值代表一行的尺寸, 值可以是任何长度单位, 并且单位可以混用, 值的数量会决定网格的行数.
这两个属性决定了当没有定义track的尺寸时, 使用的默认尺寸.
改变元素的放置方法, 默认值为 row.
第一参数:
row 元素先从左到右, 再从上到下依次成为单元格.
column 元素先从上到下, 再从左到右依次成为单元格.
第二参数:
dense 当由于元素换行导致出现空白区域时, 试图用后面大小相符的元素来填补空白.
该属性与 grid-area 命名的元素配合使用, 可明确指定它在网格中的位置.
每一个字符串都是一行, 重复的名称会使元素跨越track.
由特殊值 . 组成的名称用于将对应的单元格填充为空白.
.container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-areas:
"....... header header header"
"sidebar content content content"
"sidebar footer footer footer";
}
grid-area 有两种值类型:
  • 网格直接子代的名称(推荐).
  • grid-row-start, grid-column-start, grid-row-end, grid-column-end 的缩写.
如果一个单元格既没有 grid-area, 也没有 grid-column 和 gird-row, 它会按照 order 和HTML元素的顺序依次摆放.
grid-column 是 grid-column-start 和 grid-column-end 的缩写.
grid-row 是 grid-row-start 和 grid-row-end 的缩写.
属性的值需要用 / 分隔开, 示例:
auto / span 2
设置行和列的起始位置(指定线的序号或名称).
默认值为 auto.
设置行和列的结束位置(指定线的序号或名称), 可以为负值, 代表倒数的第x条line.
grid-column-end 和 grid-row-end 在使用 span 时可以相对start跨越多条track, 示例:
span 2 相当当前start跨越两条track
有符号整数, 默认为 0, 代表子元素按顺序摆放时的顺序.
grid 是以下属性的简写, 太复杂所以不建议使用:
  • grid-template-rows
  • grid-template-columns
  • grid-template-areas
  • grid-auto-rows
  • grid-auto-columns
  • grid-auto-flow
用 [name] 标识line的名称, 可用空格来给line设置多个名称.
通常会使用start和end作为线名称的修饰符, 尽管可以偷懒使用不带有修饰符的名称, 但出于可读性考虑不推荐这么做.
属性值定义的是 line的名称 和 track的大小:
.container {
display: grid;
grid-template-columns: [sidebar-start] 1fr [sidebar-end content-start] 2fr [content-end];
}
.content {
grid-column: content-start / content-end;
}
fr单位之所以比%和vw更好, 是因为fr自动计算占据的百分比, 而不是由用户手动表示.
例如, 1fr 2fr相当于33% 66%, 我们无需手动计算比例.
CSS网格很容易被认为是对设计里的网格系统的实现, 这是错的:
网格系统将界面均等划分为十几列, 每个元素的具体位置会通过网格系统进行描述, 例如从第n列开始, 横跨m列.
相比之下, CSS网格的尺寸是弹性的, 不需要也不应该使用这种网格系统.
/* BAD */
/* 均分为14列的网格系统, 子元素的位置会缺乏可读性和可维护性 */
grid-template-columns: repeat(14, 1fr);
/* GOOD */
/* 不受具体列数限制的网格设计 */
grid-template-columns: [left-gutter] 1fr [sidebar] 4fr [content] 8fr [right-gutter] 1fr;
原生的多列功能主要用于文本的多列显示.
如果将其用作布局, 处于一列尾部的内容可能被破坏, 导致其溢出到下一列的首部, 为了避免这种中断, 需要设置 break-inside: avoid.
该属性是 column-count 和 column-width 的缩写, 两种值的先后顺序是任意的, 也可以省略其中一个值.
列的数量.
列的宽度, 值为 auto 时意为由其他属性决定列的宽度.
列之间的空隙, 该值也被用于支持Flexbox和Grid的列间隙.
column-span: all 使子元素强制跨列显示.
列之间的线的样式.
参考: https://css-tricks.com/piecing-together-approaches-for-a-css-masonry-layout/
水平砌体布局显示起来像砖块堆成的墙.
Flexbox非常适合实现水平砌体, 只需要设置 flex: wrap 就能实现.
为了让"砖块"两端对齐, 还需要加上 justify-content: space-between.
启用两端对齐后, 尾行内容如果不能填满整行, 就会出现比较差的两端对齐效果.
为了解决这个问题, 需要在flex容器最后添加一个不可见的隐藏元素, 这可以用 ::after 伪元素来实现.
为了让隐藏元素撑开空间, 需要将它的flex-grow设置为一个较大的值, 比如 flex-grow: 9999.
  • https://github.com/desandro/masonry (推荐使用colcade替代它)
  • https://github.com/desandro/colcade
  • https://github.com/e-oj/Magic-Grid
可以使用CSS多列来实现垂直砌体布局, 实现起来非常简单.
CSS多列的子元素是先放满一列, 然后再放进下一列, 这种顺序通常不是人们想要通过砌体布局实现的效果.
有一种基于CSS多列的方案是把子元素重新排序后再显示, 这样就能以相对正确的顺序显示.
然而这不适合绝大多数使用砌体布局的场景: 列数随页面宽度改变, 向下滚动页面会加载更多的子元素.
Flexbox实现垂直砌体布局的方式和CSS多列基本一样, 所以也 存在相同的顺序缺陷.
https://tobiasahlin.com/blog/masonry-with-css/
当没有用 grid-template-rows 显式定义行轨道模板的时候, Grid会自动构建隐式的行轨道.
在这种情况下, 为单元格设置带有 span 的 grid-row-end, 会产生类似砌体布局的效果, 并且元素的布局顺序是正确的.
运用这种特性, 可以用JavaScript计算单元格的 grid-row-end 值, 从而实现砌体布局.
参考:
  • https://w3bits.com/css-grid-masonry/
  • https://medium.com/@andybarefoot/a-masonry-style-layout-using-css-grid-8c663d355ebb
当前有一个让Grid原生支持砌体布局的草案, 为 grid-template-rows 支持特殊值 masonry:
  • https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Masonry_Layout
  • https://www.smashingmagazine.com/native-css-masonry-layout-css-grid/
  • https://drafts.csswg.org/css-grid-3/
使用Canvas实现的图表库.
HTML的类名是固定的, CSS通过这些固定的类名为页面添加样式.
例子:
  • CSS Zen Garden.
CSS的类名是固定的, HTML通过添加类名为页面添加样式.
例子:
  • Bootstrap
  • Bulma
  • Tailwind
  • Animate.css
HTML的元素名的语义含糊不清, 最终带来混乱局面和意外的样式, 请只使用类名.
内联样式是最脆弱最难维护的样式, 禁止它们.
这比禁止使用内联样式更进一步, 在编写类的样式时也禁止硬编码, 使用CSS变量, 或将原子化CSS混入当前样式.
通过避免编写"真实的CSS样式", 我们创建出了统一的"抽象层".
这可以和设计领域的"设计系统"很好地融合起来.
使用语义化的类名(例如title), 而不是描述样式的类名(例如align-center).
BEM定义了一种语义化类名:
block--modifier
block--modifier-value
block__element--modifier
block__element--modifier-value
特点:
  • 模块化, 每个组件只有一个block类名, 不同block不会重叠在一个组件上.
    每个组件下有该组件特有的element类名, 不同element不会重叠在一个元素上.
  • 类名是语义化的, 有结构的.
  • block与element仅仅通过语义建立联系, 不使用CSS选择器的层级结构.
示例:
<!-- block: author-bio -->
<div class="author-bio">
<!-- element: name -->
<!-- modifier: color -->
<!-- modifier value: yellow -->
<h2 class="author-bio__name author-bio__name--color-yellow"></h2>
<!-- element: avatar -->
<h2 class="author-bio__avatar"></h2>
</div>
.author-bio {}
.author-bio__name {}
.author-bio__name--color-yellow {}
.author-bio__name--color-red {}
.author-bio__name--color-green {}
.author-bio__avatar {}
在团队中讨论决定语义化类名很可能是一个痛苦的过程.
原子化CSS使用原子化的类名, 由于没有高级的, 抽象的类名, 于是无需决定命名.
当我们需要创建一个与原组件在设计上相似但又有决定性不同的新组件时,
由于语义化的类名, 我们需要创建一个新的组件名, 接着复制原组件的所有CSS类名将其改为新的组件名.
我们接下来需要维护这两套内容极为相似的CSS类名, 两套组件的一部分会各自发展, 但又有一部分是共通的,
这会发展成为一个维护问题.
这种关系很像OOP里的继承, 因此我们可以通过提取出多个组件的共同部分形成一个基类来解决问题(@extend).
我们认为继承可能导致子类激增(类名会变得超级长),
于是我们将样式拆解成更小可重用单位以组合出我们需要的样式(@mixin).
"组合优于继承"的哲学使得我们意识到我们需要复用更小的样式单位,
这实际上就是原子化CSS(Atomic CSS).
这条路径最终引出基于设计系统的可重用原子CSS类名方案: Tailwind.
这将使得那些原本看起来很具体的类名变成泛化的类名, 例如article变成media-card, 后者是泛化的.
这种方案的缺点在于泛化的类名与不同类型的组件混合在一起.
泛化的类名可能有很多变体, 最终变得难以管理,
它使用倒退的方式解决问题, 因此在使用语义化类名之前的CSS问题将卷土重来.
此外, 当泛化的类名需要进一步泛化时, 又该怎么办? 这显然不是一种合理的解决方案.
一个元素会继承它父组件的样式, 因此当CSS样式变得很多且分散的时候, 推理元素依赖的样式变得非常困难.
这种元素与样式, 样式与样式之间遥远的认知距离使得开发人员需要频繁从浏览器的开发人员工具中寻求帮助,
并且在修改样式时更倾向于"仅追加(append only)"的编码风格,
这使得旧样式难以被删除(会破坏那些隐式依赖它的组件),
最终进一步推高样式的数量, 形成恶性循环.
CSS原子化的成功之处在于CSS样式到具体的元素的认知距离非常近, 开发人员不必担心隐式的样式依赖.
CSS in JS方案大多遵循语义化类名思想, 但以不同的方式实现.
CSS in JS会生成随机的类名, 类名只是用于绑定到具体组件的桥梁, 本身没有价值.
CSS in JS很接近内联样式, 样式与组件紧密联系在一起, 而不是像传统CSS模块化方案那样独立编写HTML和CSS.
这种方案直接解决了"仅追加样式"的问题, 因为组件需要在多种任何场合可用,
因此组件必须在设计时就避免被外部事实来源改变(例如某个全局CSS文件),
只留下少数可定制的props(或者CSS变量).
它们使用了一种不遵循语义化类名思想的CSS in JS方案:
让组件样式具有隔离性, 以至于组件样式不会泄漏到其他组件上.
在这种组件里可以直接编写"真正的CSS"(可以用CSS选择器而不是只用类名).
由于组件的代码规模通常不会太大, CSS代码得以能够维护.
使用浏览器的Shadow DOM进行隔离.
为元素添加随机的属性, 在编译CSS时为元素选择器添加相关的属性选择器.
无论是模拟内联样式还是具有隔离性的CSS, 都没有消除上述问题的认知距离, 只是将认知距离缩小到可控范围.
  • 一些CSS in JS实现(例如styled-components)依赖于动态解析CSS, 性能比原生CSS的性能差.
  • 组件助长了通过自定义属性/CSS变量来隐藏具体的内部样式的模式:
    虽然从OOP的角度来看, 隐藏内部细节经常是一件好事,
    但这对样式这种具有一套公共语言(CSS)的属性集来说则恰好相反.
    这种模式是现代组件库缺乏定制性的罪魁祸首.
    renderProp和Headless组件因此走向了CSS in JS的反面: 抛弃向上层暴露样式接口的做法.
原子化CSS是一种实用工具库, 但它又不仅仅是实用工具库, 它实际上是一种在CSS里形成的语言/共识,
因为在使用原子化CSS之后, 除非必要, 不应该编写"真正的CSS".
在Tailwind的官方文档里, 不提倡把多个原子化CSS提取成语义化类名, 而是提倡用JavaScript把它们提取成组件或模板.
页面样式高度统一化, 极少出现独特样式的网站.
  • 不需要浪费时间发明类名和达成共识.
  • 不再出现"仅追加样式"的情况.
  • 设计统一性.
  • CSS文件明显小于其他方案
由于原子化CSS大量使用类名, HTML文件会明显比其他方案要大.
原子化CSS通常是一种建立在类名之上的语言, 在使用之前需要先学习这些语言.
原子化CSS在适应新的CSS属性上存在困难, 尤其是对于那些值很复杂的CSS属性而言.
例如, Tailwind就没有支持 grid-template-areas, 只支持transform的 rotate 而不支持 rotateX 和 rotateY.
一些复杂的属性值是难以扩展的.
拿Tailwind的transform举例, 框架使用了自定义的CSS变量来实现此属性值:
transform: translateX(var(--tw-translate-x)) ...
当用户需要自己添加新的属性值时, 自定义的tranform值会覆盖掉框架原先设定的值, 导致相关的类名失效.
像Tailwind这样的框架无法直接表达 calc(100vh - 4rem).
若想使用此值, 就需要依赖于预先定义好的配置,
这可能会导致实际的值与设置该值的地方距离过远, 带来认知负担.
为这些具有独特意义的复杂值命名也很困难.
在原子化CSS框架里, 通常只有过渡动画和几种预设的动画能够使用, 所有复杂的动画都需要自定义CSS类.
这是因为CSS动画经常涉及到多个元素, 多个属性在多个关键帧的变化.
此外, 将专用于动画的大量CSS类写进HTML里还会严重损害代码可读性.
由于缺乏高级抽象, 原子化CSS的可移植性存在疑问, 不同的原子化CSS库可能使用完全不同的原子类名.
由于缺乏语义, 并且添加了大量类名, 源代码的可读性会变差.
一些原子CSS框架完全避免抽象(例如Tailwind), 因此在使用时需要为每个元素手动指定样式.
在没有组件的情况下, 这会导致相当多的重复编写.
有一些CSS属性是无法继承的, 比如vertical-align.
这时, 如果需要子元素全部实现垂直对齐, 就需要给每一个子元素添加相关类名, 这种重复是不可接受的.
原子化CSS库可能会犯错误, 关键在于原子化CSS库的错误对使用库的开发人员来说是不可修复的,
开发人员只能等待维护者修复问题.
如果维护者认为这不是错误而是特性, 或者这已经是一个被广泛使用的错误实现, 因此无法修改,
那么开发人员将面临以下问题:
  • 很难快速迁移到其他库
  • 不得不迁就错误实现
  • 在原子化CSS之上实现自己的分支
使用量最大的预处理器.
最早用Ruby编写, 后来用Dart重写.
Bootstrap 4从Less切换到了Sass.
使用量第二大的预处理器.
Less晚于Sass推出, 但由于是通过JavaScript编写的, 更容易与前端项目集成, 赢得了一批红利.
Less的一些设计与现有的CSS语法容易混淆, 并且相比Sass和Stylus的功能更少.
使用量第三大的预处理器, 由TJ开发.
Stylus的主要特点是它的语法非常宽松.
时至今日, Stylus的开发和发布并不积极.
CSS的主流后处理工具, 分析和生成原生CSS代码, 可为CSS创建自定义语法.
PostCSS通过大量的插件支持各种各样的功能.
PostCSS最知名的插件, 用来将CSS转换成常见渲染引擎的特有形式, 以满足跨浏览器兼容性.
将CSS的 @import 转换为单文件的内联形式.
PostCSS主要的实现主流预处理器语法的插件.
该项目上一次更新是在2018年, 不应该继续使用了, 推荐直接使用Sass.
PostCSS主要的压缩工具.
考虑到[[https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API][Web Animations API]]已经普遍可用, 且CSS动画经常需要与JavaScript配合使用, 现在建议直接用JavaScript创建和控制动画.
CSS动画默认情况下会立即播放, 因此在页面载入后, 当元素获得相应的 animation-name 属性时就会开始播放.
移除并重新添加 animation-name 会导致动画再次播放.
用于设置动画的播放状态, 将状态从 paused 转换至 running 时, 暂停的动画会从暂停处继续.
决定动画的关键帧如何在动画开始前或结束后影响元素.
  • none: 默认值, 不影响.
  • forwards: 动画最后一帧的样式会在动画播放结束后仍应用于元素.
  • backwards: 动画第一帧的样式会在动画播放前就应用于元素.
  • both: forwards+backwards.
动画停止播放前应该播放的次数.
默认值为1, 最常用的值是infinite.
决定循环动画的播放方向, 对于多个动画, 可以指定不同的值:
  • normal: 正向播放动画, 播放完毕后重置到初始状态再开始.
  • reverse: 反向播放动画, 播放完毕后重置到初始状态再开始.
  • alternate: 正向播放动画, 播放完毕后反向播放动画, 如此反复.
  • alternate-reverse: 反向播放动画, 播放完毕后正向播放动画, 如此反复.
尽管HTTP/2的多路复用已经基本淘汰掉sprite的应用场景, 但在少数情况下可能仍然有用.
如果sprite不需要缩放, 则使用 background-position: -x -y 就足以显示sprite.
如果sprite需要缩放, 则需要先通过 background-size: calc(width * scale) calc(height * scale) 将图片放大,
然后以缩放过的尺寸为基准设置 background-position: calc(-x * scale) calc(-y * scale).
其中 background-size 的值要求已知整张图片的原始尺寸(百分比值在这个场景下完全无用).
不幸的是, 网络上的大部分CSS特效例子都很愚蠢,
其中至少有八成作品是由"缺乏对可维护性的认识"的作者,
在"对相应CSS属性一知半解"的情况下, 通过反复试错做出来的.
因此, 这些作品充斥着冗余的, 可读性不佳的代码.
  • animate.css
CSS原生支持滚动捕捉.
通过在滚动容器上留出边距, 并将 scroll-snap-type 设置为 mandatory, 可以实现回弹效果.
教程:
  • https://tobiasahlin.com/spinkit/
这个方法的优点是不需要JavaScript, 开启状态可以用选择器 details[open] 表示.
由于没有JavaScript, 多个details不能连动, 因此可以同时展开多个details元素.
设置一个不显示的checkbox(input[type="checkbox"] { display: none }),
然后创建一个绑定到该checkbox的label元素.
利用点击label等同于点击checkbox的特点, 把label作为可自定义外观的checkbox使用.
checkbox选中状态可以通过选择器 =input[type=checkbox]:checked= 表示,
利用这一点, 使用通用兄弟组合器 ~ 选择后续的兄弟节点, 将兄弟节点展开.
  • 比details + summary繁琐太多了, 属于没有details和summary年代的CSS hack
  • 由于没有JavaScript, 展开的元素之间不能连动.
实现方式与用隐形checkbox实现手风琴类似, 只不过将checkbox换成了radio.
值得一提的是, 纯CSS轮播的"上一个", "下一个"按钮的实现方式很有创意:
由于不能使用JavaScript, 所以不能基于状态复用"上一个", "下一个"按钮,
于是需要在每一个页面上创建与该页面相关的"上一个"和"下一个"的label按钮, 这些label只有在相应的radio处于checked状态时才会显示.
没有任何理由去推荐使用这种无JavaScript的轮播组件, 因为它们的实现是基于CSS hack, 代码非常不直观.
纯CSS形状是早年的CSS hack手法, 并不推荐使用, 因为编写出来的CSS很不直观.
参考: https://css-tricks.com/the-shapes-of-css/
clip-path通过裁切元素来获得形状, 它支持许多基本形状和路径.
使用时, 建议找一个专门的clip-path编辑器来可视化裁切结果.
https://codepen.io/stoumann/full/abZxoOM
创建多边形以外的空心形状很不方便.
有一个专门的CSS提案要求支持路径的反转:
https://github.com/w3c/csswg-drafts/issues/10445
大部分空心形状需求可以通过CSS的mask-image以SVG的<mask>创建一个背景蒙版来满足,
这种做法的主要缺陷是空心部分并非真正空心, 只是视觉上空心.
空心形状里, 空心多边形相对容易实现:
https://verpex.com/blog/website-tips/how-to-create-cut-out-shapes-using-the-clip-path-property
SVG图标近年来已经成为最流行的选择, 它最适合用来制作各种形状.
该属性/函数相当于CSS 3D效果的强度, 值越小, 3D效果越强烈.
perspective属性会影响到它的直接子元素.
参考: https://3dtransforms.desandro.com/perspective
在同一个3D渲染上下文中的物件才会相交, 进而在显示时出现一个物件靠前的部分遮挡另一个物件靠后部分的效果.
不在同一个3D渲染上下文中的物件位于不同的3D空间, 物件不会相交, 最终显示结果是上下文各自渲染完毕后, 合成两者的2D图像.
参考: https://css-tricks.com/things-watch-working-css-3d/
  • flat 默认: 元素的子元素不会使用相同的3D渲染上下文.
  • preserve-3d: 元素的子元素将使用相同的3D渲染上下文.
实现方式是在一个3D空间内创建两个大小相同的卡片.
<!DOCTYPE html>
<html>
<head>
<style>
.card {
width: 150px;
height: 200px;
transition: transform 1s;
transform-style: preserve-3d; /* 使子元素位于同一个3D空间, 能够互相遮挡 */
}
.card--flipped {
transform: rotateY(180deg); /* 将整张卡绕Y轴旋转180度, 相当于把卡翻过来 */
}
.card__face {
width: 100%;
height: 100%;
position: absolute; /* 使两张卡片重叠 */
}
.card__face--front {
background-color: brown;
}
.card__face--back {
background-color: darkcyan;
transform: rotateY(180deg); /* 将这一面绕Y轴旋转180度, 作为卡背 */
}
</style>
</head>
<body>
<div class="card">
<div class="card__face card__face--front">front</div>
<div class="card__face card__face--back">back</div>
</div>
<script>
const card = document.querySelector('.card')
card.addEventListener('click', () => card.classList.toggle('card--flipped'))
</script>
</body>
</html>
参考: https://3dtransforms.desandro.com/card-flip
  • https://3dtransforms.desandro.com/cube
  • https://codepen.io/desandro/pen/KRWjzm
  • https://3dtransforms.desandro.com/box
  • https://codepen.io/desandro/pen/MGpMOV
视差效果是通过将元素分层, 然后让位于不同层的元素以不同的速率位移, 从而形成的独特视觉效果.
展示视差效果时, "靠得越近"的事物移动的速度越快, "离得越远"的事物移动的速度越慢.
  • 浏览器原生解决方案: darkroomengineering/lenis
  • https://github.com/janpaepke/ScrollMagic
  • https://github.com/locomotivemtl/locomotive-scroll
  • https://github.com/alvarotrigo/fullPage.js
视差滚动是与页面滚动相关的视差效果.
实现性能较好的视差滚动, 关键在于使用 perspective (在父元素上) 和 translateZ() (在子元素上).
translateZ() 调整Z轴, 正值让物体离观看者更近, 负值离观看者更远.
perspective 调整透视, 从而影响不同Z轴距离内容的滚动速度.
遗憾的是, 这两个函数配合工作的方式很不直观, 因此很难搞清楚应该采用什么值来达到想要的效果.
一种惯用的方案是设置 perspective: 1px, 然后以 -1px 为步长逐步减小 translateZ() 的值.
为了让元素不至于显得过小, 会用 scale() 进行补偿,
用以下公式可以计算出放大回原始大小需要的scale:
scaleFactor = (perspective — distance) / perspective
其中 distance 是 translateZ 的值, 举例来说, 当 translateZ(-1px) 时,
scaleFactor = (1 + 1) / 1 = 2.
参考: https://developers.google.com/web/updates/2016/12/performant-parallaxing
纯CSS实现的方式是将容器逆时针旋转90度, 再将容器内的元素顺时针旋转90度(即恢复到容器旋转前的方向),
从而将容器的垂直滚动条强行变成"水平滚动条"(语义上是垂直滚动条, 响应用户滚轮的向下滚动, 但视觉上的变化是水平的).
边框为渐变色的幽灵按钮, 通过 border-image-source + liner-gradient() 实现.
配合scroll事件或requestAnimationFrame回调实时计算元素的top值(元素距离视口顶端的距离),
当top值小于 window.innerHeight (文档可见范围的高度) 时, 意味着元素进入视线.
通过IntersectionObserver观察元素,
仅当元素进入视口时(entry.isIntersecting 为真), 向元素添加对应的class.
参考: https://dev.to/ljcdev/introduction-to-scroll-animations-with-intersection-observer-d05
尽管 text-decoration 可以设置文本下划线, 但这种下划线无法制作任何动画效果, 因此需要用其他方式实现下划线.
box-shadow: 0 8px 0 0 #ccc
在使用inset的情况下, 阴影会置于文本的下方.
box-shadow: inset 0 -8px 0 0 #ccc
a {
text-decoration: none;
background-image: linear-gradient(red, red);
background-position: bottom;
background-size: 100% 2px;
background-repeat: no-repeat;
}
a {
text-decoration: none;
box-shadow: inset 0 -2px 0 0 #ccc;
transition: box-shadow 0.3s;
&:hover {
box-shadow: inset 0 -1em 0 0 #ccc;
}
}
box-shadow无法使用百分比单位, 因此当文字的行高大于1em时, box-shadow将无法覆盖全部的文本, 解决此问题需要为元素专门计算行高.
a {
text-decoration: none;
background-image: linear-gradient(red, red);
background-position: bottom;
background-size: 100% 2px;
background-repeat: no-repeat;
transition: background-size 0.3s;
&:hover {
background-size: 100% 100%; /* 第二个值决定了展开的高度, 出于动画目的, 此值不可省略 */
}
}
Chromium系浏览器在background-size的过渡动画中可能会出现抖动现象.
这是一个非常奇葩的bug, 经测试, 是否出现抖动现象只取决于正在动画的a元素在HTML文档里的位置, 与网页使用的CSS样式毫无关系, 并且没有规律可循.
a {
text-decoration: none;
background-image: linear-gradient(red, red);
background-position: bottom; /* 图片从中下方开始显示(从中下方展开) */
background-size: 0 2px; /* 默认情况下宽度为0, 不显示下划线 */
background-repeat: no-repeat;
transition: background-size 0.3s;
&:hover {
background-size: 100% 2px;
}
}
只需要修改 background-position, 就可以实现从不同的位置展开下划线:
  • background-position: left bottom 从左至右展开
  • background-position: right bottom 从右至左展开
单行文本大多数是被其他方法淘汰的例子, 记录它们是为了避开它们.
https://tobiasahlin.com/blog/css-trick-animating-link-underlines/
通过添加 position: absolute 的before/after伪元素作为线条,
然后设置过渡动画 transform: scaleX(0) 到 transform: scaleX(1) 实现:
scaleX(0) 将伪元素在水平方向向中间收缩至不可见状态,
然后再通过 scaleX(1) 展开为原来的宽度.
这两种变体是通过 transform-origin (变换原点)实现的, 该属性的默认值是 center,
只要将原点该为 left 或 right 就可以变更收缩的位置.
https://tonsky.me/blog/centering/
https://github.com/zenozeng/fonts.css
该解决方案不能直接使用, 因为它虽然分类了各种字体, 但其中很多字体的显示效果太差, 不应该被用在页面上.
  • serif 衬线
  • sans-seirf 无衬线
  • monospace 等宽
  • cursive 花体, 草书
  • fantasy 装饰性字体
  • math 数学风格字体
  • emoji 表情符号字体
  • fangsong 仿宋体, 介于宋体和草书之间, 汉字专用
  • system-ui 与平台UI上的默认字体相同, 该值的出现使得人们不再需要手动声明系统字体
    遗憾的是Firefox不支持此值.
  • ui-serif
  • ui-sans-serif
  • ui-monospace
  • ui-rounded 环绕文本
  • 思源黑体(免费)
  • 方正悠黑(商业)
  • 苹方(macOS, iOS平台字体, 不可商用)
  • 苹黑(苹果官方网站中文字体, 不可商用)
  • 思源宋体(免费)
  • 方正悠宋(商业)
  • Titillium Web
  • Circular Std Book
  • Eudald News Regular
ch单位是字符0的宽度, 如果字体是等宽字体, 则可以通过此单位配合width决定一行文本应该包含多少个字符.
通常会将英文字体放在中文字体前面,
这是因为中文字体往往也包括英文字符, 如果放在前面会导致无法使用英文字体.
unicode-range的设计目的是为了能在下载Web Fonts时跳过那些页面里不存在相关字符的字体, 以节省带宽.
但是, unicode-range也可以通过字符集范围来为字符分别设置字体.
https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range
很奇怪, 一些字体不能以 src: local(FONT_NAME) 形式来使用, 例如 Fira Code.
https://stackoverflow.com/questions/68437455/font-face-srclocal-does-not-work-on-some-fonts
https://opentype.js.org/font-inspector.html
即使使用了web fonts, 仍有必要设置 font-display 属性(建议为swap), 以避免页面在加载字体时变成一片空白.
自托管web fonts: https://google-webfonts-helper.herokuapp.com/fonts
注意: @font-face中的大部分属性是作为过滤器使用的.
例如设置 font-weight 的意思是当使用到这些字重时, 加载此字体.
这与@font-face以外的 font-weight 属性的功能截然不同.
  • WOFF(.woff): 从TrueType和OpenType压缩而来的字体格式, 是最适合用于Web字体的格式, 所有浏览器都支持此格式
  • WOFF2(.woff2): 下一代WOFF, 使用Brotli改进压缩, 压缩率比WOFF更高, 除IE外所有浏览器都支持
  • TrueType(.ttf)/OpenType(.otf): 主流的字体格式, OpenType是TrueType的改进版本.
  • embedded-opentype(.eot): 微软提案的字体格式, 仅IE支持此格式, Edge并不支持.
  • svg(.svg): 已被弃用, 仅Safari还在支持此格式
从浏览器的市占率来看, 如今只提供woff2和woff格式的字体就够了.
@font-face {
font-family: 'Font';
src: url('font.woff2') format('woff2'),
url('font.woff') format('woff');
}
中文字符集非常巨大, 字体文件以MB级别起跳, 经常有超过10MB的字体.
WOFF2字体是可压缩的, 相比其他字体格式, 可减少30%的体积.
参考: https://web.dev/reduce-webfont-size/
可变字体往往比不可变字体体积小, 因为不需要加载额外的粗体和斜体版本.
2021年4月8日发布的新版思源黑体已经支持可变字体, 但字体大小不容乐观, 仍然在10MB以上.
对于中文字体来说, 将单个字体以不同Unicode范围切割为多个字体, 然后在使用时只下载被使用到的字体, 这种方案是很容易想象的.
对网页来说, 可以用unicode-range切分字形, 让页面只下载使用到的字符所需的字体.
由于以下原因, 此方案缺乏可行性:
  • 如何对中文字形进行切割是一个问题.
    如下方表格所示, 简体中文的常用字集有3500字, 可以覆盖99%的简体中文文本, 但是产生的字体大小对Web字体来说仍然不可接受.
    由于缺乏可以拿来作为参照的更精细的子集, 难以进一步切割.
  • 中文的常用字集在Unicode上不连续, 光是常用字集的Unicode范围就有20KB之巨.
汉字范围 字形数量 Unicode范围 字体大小(WOFF)
基本汉字 20902字 4E00-9FA5 12MB
《通用规范汉字表》(2013) 一级字表(常用字集) 3500字 不连续 1.7MB
《通用规范汉字表》(2013) 二级字表 3000字 不连续 1.7MB
《通用规范汉字表》(2013) 三级字表 1605字 不连续 976KB
参考: https://www.zhangxinxu.com/wordpress/2016/11/css-unicode-range-character-font-face/
中文的Unicode编码范围: https://www.zhangxinxu.com/study/201611/chinese-language-unicode-range.html
Python的fonttools包的一部分, 提供了很多用来定制字体子集的参数.
pyftsubset font.otf --text='你好世界' --flavor=woff
pyftsubset font.otf --text='你好世界' --flavor=woff2
https://github.com/zachleat/glyphhanger
glyphhanger利用pyftsubset实现字体子集化.
功能很丰富.
生成的文件大小优于fonteditor-core.
# 依赖
pip install fonttools
pip install brotli
pip install zopfli
  • 依赖过多
    glyphhanger是一个以CLI为主的程序, 因此打包了太多依赖.
    其中影响最大的依赖是@zachleat/spider-pig包间接依赖的puppeteer, 它会下载Chromium.
    对于精确子集化场景来说, glyphhanger的很多功能都是不必要的, 不如直接调用pyftsubset.
  • 性能损失
    glyphhanger会在转换前先根据文本文件/白名单生成一份Unicode范围表,
    然而pyftsubset实际上支持直接输入需要保留的字符, 这也更符合精确子集化的场景.
    经过测试, 直接调用pyftsubset的生成速度至少是glyphhanger的2倍.
https://github.com/harfbuzz/harfbuzz
一个用C++开发的字体整形(shaping)库, 可用来创建字体的子集.
该项目有官方的WASM版本, 可以通过JavaScript调用:
https://github.com/harfbuzz/harfbuzzjs
基于harfbuzzjs的子集模块:
https://github.com/papandreou/subset-font
经过测试, 该模块的子集化性能和生成的字体大小都与pyftsubset相当.
https://github.com/foliojs/fontkit
这是类似字蛛的库subfont使用的底层模块.
遗憾的是该库自带的subset.encodeStream方法只能输出.ttf格式的字体.
https://github.com/aui/font-spider
原理是扫描文件, 以获得所有使用该字体的CSS, 然后再从中提取到所有被使用的字符.
该项目是基于fontmin实现的.
  • 不适用于UGC网站, 因为用户会使用到的字符是不可能提前预测的, 只能加载全部字符.
  • 项目上一次更新已是2年前, 且官方网站的域名已经过期.
  • 共享fontmin所有的问题.
https://github.com/ecomfe/fontmin
字蛛的底层, 由百度EFE团队开发, 可以手动指定需要提取的字符, 然后生成字体.
只支持以ttf为源, 其他格式都要先转换为ttf再处理, 内置了一些转换插件.
该项目是的字体编辑功能是用fonteditor-core实现的.
  • 共享fonteditor-core所有的问题.
子集化所需的时间与所需的字形数量成正比.
  • 问题众多, 缺乏维护.
  • 一次把整个字体读取到内存里.
  • 对OTF的子集化实现有问题.
    https://github.com/kekee000/fonteditor-core/issues/18
通过Advanced Font Settings, 可以按ISO 15924中定义的script设置浏览器使用的默认字体.
例如, 简体中文的script是Simplified Han, 英文的script是Latin.
  • 现代网页的字体族设置方式通常是将默认字体放在最后一位作为fallback,
    页面上的文本能够使用到默认字体的情况极为罕见, 因此用户往往无法在页面上使用自己想要的字体.
  • Advanced Font Settings中设置的字体要求网页的语言与script匹配, 增加了设置的复杂性.
    在绝大多数情况下, 该扩展都不如Chrome自带的字体设置易用.
  • Advanced Font Settings使用的API Chrome.fontSettings, 曾经还有错误地将字重作为字体名提供的问题.
    虽然该问题在如今的Chrome里已经解决, 但仍然无法为默认字体配置字重.
    因此在Appoint Font里, 相关API只被用来获取字体列表.
该方案的问题在于, 除非用于重置字体的扩展程序逐一读取元素的CSS样式,
否则扩展程序无法知晓字体应该被重置为哪种类型.
重置元素样式本身作为一种对页面的侵入行为, 还可能引起降低页面性能的副作用.
这一点在将Appoint Font用于Google的部分网页时已经得到验证.
可以用font-face替换用户代理已经安装的字体名.
加上可以通过Canvas判断出字体是等宽字体还是非等宽字体, 从而替换掉相应类型的字体.
虽然没有区分适用范围最广的衬线体和非衬线体的有效手段,
所幸对于用户代理的自定义需求而言, 通常是将它们一视同仁地替换掉.
一个字体可能有多种名称, 例如"微软雅黑"和"Microsoft YaHei"是同一个字体.
用 font-face 替换字体的前提是能够知道字体的所有别名.
\n \n"},{"type":"paragraph","children":[{"type":"text","value":"参考: https://3dtransforms.desandro.com/card-flip"}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"正方体"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"https://3dtransforms.desandro.com/cube"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"https://codepen.io/desandro/pen/KRWjzm"}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"长方体"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"https://3dtransforms.desandro.com/box"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"https://codepen.io/desandro/pen/MGpMOV"}]}]}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"视差 Parallax/滚动劫持 Scroll-hijacking"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"视差效果是通过将元素分层, 然后让位于不同层的元素以不同的速率位移, 从而形成的独特视觉效果."},{"type":"newline"},{"type":"text","value":"展示视差效果时, \"靠得越近\"的事物移动的速度越快, \"离得越远\"的事物移动的速度越慢."}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"原生滚动驱动动画支持 (实验性API)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"link","protocol":"https","url":"https://scroll-driven-animations.style/","children":[{"type":"text","value":"Scroll-driven Animations"}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"库"}],"tags":[]},"children":[{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"滚动劫持/视差效果"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"浏览器原生解决方案: "},{"type":"link","protocol":"https","url":"https://github.com/darkroomengineering/lenis","children":[{"type":"text","value":"darkroomengineering/lenis"}]}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"https://github.com/janpaepke/ScrollMagic"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"https://github.com/locomotivemtl/locomotive-scroll"}]}]}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"全屏幻灯片 "}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"https://github.com/alvarotrigo/fullPage.js"}]}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"视差滚动 Parallax Scrolling"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"视差滚动是与页面滚动相关的视差效果."}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"高性能视差滚动(纯CSS)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"实现性能较好的视差滚动, 关键在于使用 "},{"type":"verbatim","value":"perspective"},{"type":"text","value":" (在父元素上) 和 "},{"type":"verbatim","value":"translateZ()"},{"type":"text","value":" (在子元素上)."},{"type":"newline"},{"type":"verbatim","value":"translateZ()"},{"type":"text","value":" 调整Z轴, 正值让物体离观看者更近, 负值离观看者更远."},{"type":"newline"},{"type":"verbatim","value":"perspective"},{"type":"text","value":" 调整透视, 从而影响不同Z轴距离内容的滚动速度."}]},{"type":"paragraph","children":[{"type":"text","value":"遗憾的是, 这两个函数配合工作的方式很不直观, 因此很难搞清楚应该采用什么值来达到想要的效果."},{"type":"newline"},{"type":"text","value":"一种惯用的方案是设置 "},{"type":"verbatim","value":"perspective: 1px"},{"type":"text","value":", 然后以 "},{"type":"verbatim","value":"-1px"},{"type":"text","value":" 为步长逐步减小 "},{"type":"verbatim","value":"translateZ()"},{"type":"text","value":" 的值."},{"type":"newline"},{"type":"text","value":"为了让元素不至于显得过小, 会用 "},{"type":"verbatim","value":"scale()"},{"type":"text","value":" 进行补偿,"}]},{"type":"paragraph","children":[{"type":"text","value":"用以下公式可以计算出放大回原始大小需要的scale:"},{"type":"newline"},{"type":"code","value":"scaleFactor = (perspective — distance) / perspective"},{"type":"newline"},{"type":"text","value":"其中 "},{"type":"verbatim","value":"distance"},{"type":"text","value":" 是 "},{"type":"verbatim","value":"translateZ"},{"type":"text","value":" 的值, 举例来说, 当 "},{"type":"verbatim","value":"translateZ(-1px)"},{"type":"text","value":" 时,"},{"type":"newline"},{"type":"code","value":"scaleFactor = (1 + 1) / 1 = 2"},{"type":"text","value":"."}]},{"type":"paragraph","children":[{"type":"text","value":"参考: https://developers.google.com/web/updates/2016/12/performant-parallaxing"}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"将垂直滚动变为水平滚动"}],"tags":[]},"children":[{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"纯CSS实现"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"纯CSS实现的方式是将容器逆时针旋转90度, 再将容器内的元素顺时针旋转90度(即恢复到容器旋转前的方向),"},{"type":"newline"},{"type":"text","value":"从而将容器的垂直滚动条强行变成\"水平滚动条\"(语义上是垂直滚动条, 响应用户滚轮的向下滚动, 但视觉上的变化是水平的)."}]}]}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"渐变边框幽灵按钮"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"边框为渐变色的幽灵按钮, 通过 "},{"type":"verbatim","value":"border-image-source"},{"type":"text","value":" + "},{"type":"verbatim","value":"liner-gradient()"},{"type":"text","value":" 实现."}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"仅在内容可见时采取行动(例如播放动画)"}],"tags":[]},"children":[{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"getBoundingClientRect(低性能, 已淘汰)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"配合scroll事件或requestAnimationFrame回调实时计算元素的top值(元素距离视口顶端的距离),"},{"type":"newline"},{"type":"text","value":"当top值小于 "},{"type":"verbatim","value":"window.innerHeight"},{"type":"text","value":" (文档可见范围的高度) 时, 意味着元素进入视线."}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"IntersectionObserver(高性能)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"通过IntersectionObserver观察元素,"},{"type":"newline"},{"type":"text","value":"仅当元素进入视口时("},{"type":"verbatim","value":"entry.isIntersecting"},{"type":"text","value":" 为真), 向元素添加对应的class."}]},{"type":"paragraph","children":[{"type":"text","value":"参考: https://dev.to/ljcdev/introduction-to-scroll-animations-with-intersection-observer-d05"}]}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"下划线"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"尽管 "},{"type":"verbatim","value":"text-decoration"},{"type":"text","value":" 可以设置文本下划线, 但这种下划线无法制作任何动画效果, 因此需要用其他方式实现下划线."}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"适用于多行文本"}],"tags":[]},"children":[{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"通过box-shadow实现下划线"}],"tags":[]},"children":[{"type":"section","level":5,"headline":{"type":"headline","children":[{"type":"text","value":"下划线向文字外部增加宽度"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"verbatim","value":"box-shadow: 0 8px 0 0 #ccc"}]}]},{"type":"section","level":5,"headline":{"type":"headline","children":[{"type":"text","value":"下划线向文字内部增加宽度"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"在使用inset的情况下, 阴影会置于文本的下方."},{"type":"newline"},{"type":"verbatim","value":"box-shadow: inset 0 -8px 0 0 #ccc"}]}]}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"通过background-image实现下划线"}],"tags":[]},"children":[{"type":"example","params":["css"],"value":"a {\n text-decoration: none;\n background-image: linear-gradient(red, red);\n background-position: bottom;\n background-size: 100% 2px;\n background-repeat: no-repeat;\n}"}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"链接悬停动画"}],"tags":[]},"children":[{"type":"section","level":5,"headline":{"type":"headline","children":[{"type":"text","value":"垂直展开的下划线"}],"tags":[]},"children":[{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"用box-shadow实现"}],"tags":[]},"children":[{"type":"example","params":["scss"],"value":"a {\n text-decoration: none;\n box-shadow: inset 0 -2px 0 0 #ccc;\n transition: box-shadow 0.3s;\n\n &:hover {\n box-shadow: inset 0 -1em 0 0 #ccc;\n }\n}"},{"type":"section","level":7,"headline":{"type":"headline","children":[{"type":"text","value":"缺点: 无法使用百分比单位"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"box-shadow无法使用百分比单位, 因此当文字的行高大于1em时, box-shadow将无法覆盖全部的文本, 解决此问题需要为元素专门计算行高."}]}]}]},{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"用background-image实现"}],"tags":[]},"children":[{"type":"example","params":["scss"],"value":"a {\n text-decoration: none;\n background-image: linear-gradient(red, red);\n background-position: bottom;\n background-size: 100% 2px;\n background-repeat: no-repeat;\n transition: background-size 0.3s;\n\n &:hover {\n background-size: 100% 100%; /* 第二个值决定了展开的高度, 出于动画目的, 此值不可省略 */\n }\n}"},{"type":"section","level":7,"headline":{"type":"headline","children":[{"type":"text","value":"陷阱: Chromium过渡抖动bug"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"Chromium系浏览器在background-size的过渡动画中可能会出现抖动现象."},{"type":"newline"},{"type":"text","value":"这是一个非常奇葩的bug, 经测试, 是否出现抖动现象只取决于正在动画的a元素在HTML文档里的位置, 与网页使用的CSS样式毫无关系, 并且没有规律可循."}]}]}]}]},{"type":"section","level":5,"headline":{"type":"headline","children":[{"type":"text","value":"水平展开的下划线"}],"tags":[]},"children":[{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"用background-image实现"}],"tags":[]},"children":[{"type":"example","params":["scss"],"value":"a {\n text-decoration: none;\n background-image: linear-gradient(red, red);\n background-position: bottom; /* 图片从中下方开始显示(从中下方展开) */\n background-size: 0 2px; /* 默认情况下宽度为0, 不显示下划线 */\n background-repeat: no-repeat;\n transition: background-size 0.3s;\n\n &:hover {\n background-size: 100% 2px;\n }\n}"},{"type":"section","level":7,"headline":{"type":"headline","children":[{"type":"text","value":"变体: 从左至右和从右至左展开的下划线"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"只需要修改 "},{"type":"verbatim","value":"background-position"},{"type":"text","value":", 就可以实现从不同的位置展开下划线:"}]},{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"verbatim","value":"background-position: left bottom"},{"type":"text","value":" 从左至右展开"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"verbatim","value":"background-position: right bottom"},{"type":"text","value":" 从右至左展开"}]}]}]}]}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"仅适用于单行文本"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"单行文本大多数是被其他方法淘汰的例子, 记录它们是为了避开它们."}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"链接悬停动画"}],"tags":[]},"children":[{"type":"section","level":5,"headline":{"type":"headline","children":[{"type":"text","value":"水平展开的下划线"}],"tags":[]},"children":[{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"用伪元素实现(不推荐)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"https://tobiasahlin.com/blog/css-trick-animating-link-underlines/"}]},{"type":"paragraph","children":[{"type":"text","value":"通过添加 "},{"type":"verbatim","value":"position: absolute"},{"type":"text","value":" 的before/after伪元素作为线条,"},{"type":"newline"},{"type":"text","value":"然后设置过渡动画 "},{"type":"verbatim","value":"transform: scaleX(0)"},{"type":"text","value":" 到 "},{"type":"verbatim","value":"transform: scaleX(1)"},{"type":"text","value":" 实现:"},{"type":"newline"},{"type":"verbatim","value":"scaleX(0)"},{"type":"text","value":" 将伪元素在水平方向向中间收缩至不可见状态,"},{"type":"newline"},{"type":"text","value":"然后再通过 "},{"type":"verbatim","value":"scaleX(1)"},{"type":"text","value":" 展开为原来的宽度."}]},{"type":"section","level":7,"headline":{"type":"headline","children":[{"type":"text","value":"变体: 从左至右和从右至左展开的下划线"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"这两种变体是通过 "},{"type":"verbatim","value":"transform-origin"},{"type":"text","value":" (变换原点)实现的, 该属性的默认值是 "},{"type":"verbatim","value":"center"},{"type":"text","value":","},{"type":"newline"},{"type":"text","value":"只要将原点该为 "},{"type":"verbatim","value":"left"},{"type":"text","value":" 或 "},{"type":"verbatim","value":"right"},{"type":"text","value":" 就可以变更收缩的位置."}]}]}]}]}]}]}]}]},{"type":"section","level":1,"headline":{"type":"headline","children":[{"type":"text","value":"用CSS还原系统UI"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"link","protocol":"https","url":"https://github.com/nostalgic-css/NES.css","children":[{"type":"text","value":"nostalgic-css/NES.css: NES-style CSS Framework | ファミコン風CSSフレームワーク"}]}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"link","protocol":"https","url":"https://github.com/jdan/98.css","children":[{"type":"text","value":"jdan/98.css: A design system for building faithful recreations of old UIs"}]}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"link","protocol":"https","url":"https://github.com/botoxparty/XP.css","children":[{"type":"text","value":"botoxparty/XP.css: A CSS framework for building faithful recreations of operating system GUIs."}]}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"link","protocol":"https","url":"https://github.com/khang-nd/7.css","children":[{"type":"text","value":"khang-nd/7.css: A JS-independent, tree-shakeable CSS framework for building faithful recreations of the Windows 7 UI."}]}]}]}]},{"type":"section","level":1,"headline":{"type":"headline","children":[{"type":"text","value":"字体"}],"tags":[]},"children":[{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"完美垂直居中"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"https://tonsky.me/blog/centering/"}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"中文字体解决方案"}],"tags":[]},"children":[{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"fonts.css"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"https://github.com/zenozeng/fonts.css"}]},{"type":"paragraph","children":[{"type":"text","value":"该解决方案不能直接使用, 因为它虽然分类了各种字体, 但其中很多字体的显示效果太差, 不应该被用在页面上."}]}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"通用字体 generic-name"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"serif 衬线"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"sans-seirf 无衬线"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"monospace 等宽"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"cursive 花体, 草书"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"fantasy 装饰性字体"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"math 数学风格字体"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"emoji 表情符号字体"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"fangsong 仿宋体, 介于宋体和草书之间, 汉字专用"}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"UI通用字体"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"system-ui 与平台UI上的默认字体相同, 该值的出现使得人们不再需要手动声明系统字体"},{"type":"newline"},{"type":"text","value":"遗憾的是Firefox不支持此值."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"ui-serif"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"ui-sans-serif"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"ui-monospace"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"ui-rounded 环绕文本"}]}]}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"优秀字体"}],"tags":[]},"children":[{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"黑体"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"思源黑体(免费)"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"方正悠黑(商业)"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"苹方(macOS, iOS平台字体, 不可商用)"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"苹黑(苹果官方网站中文字体, 不可商用)"}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"宋体"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"思源宋体(免费)"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"方正悠宋(商业)"}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"英文非衬线体"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"Titillium Web"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"Circular Std Book"}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"英文衬线体"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"Eudald News Regular"}]}]}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"ch单位"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"ch单位是字符0的宽度, 如果字体是等宽字体, 则可以通过此单位配合width决定一行文本应该包含多少个字符."}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"font-family 中英文字体混合"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"通常会将英文字体放在中文字体前面,"},{"type":"newline"},{"type":"text","value":"这是因为中文字体往往也包括英文字符, 如果放在前面会导致无法使用英文字体."}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"通过@font-face的unicode-range选项区分中英字符"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"unicode-range的设计目的是为了能在下载Web Fonts时跳过那些页面里不存在相关字符的字体, 以节省带宽."},{"type":"newline"},{"type":"text","value":"但是, unicode-range也可以通过字符集范围来为字符分别设置字体."}]},{"type":"paragraph","children":[{"type":"text","value":"https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range"}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"陷阱: <<一些字体无法作为@font-face使用>>"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"很奇怪, 一些字体不能以 "},{"type":"verbatim","value":"src: local(FONT_NAME)"},{"type":"text","value":" 形式来使用, 例如 "},{"type":"verbatim","value":"Fira Code"},{"type":"text","value":"."}]},{"type":"paragraph","children":[{"type":"text","value":"https://stackoverflow.com/questions/68437455/font-face-srclocal-does-not-work-on-some-fonts"}]}]}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"查看字体元数据"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"https://opentype.js.org/font-inspector.html"}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"Web Fonts"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"即使使用了web fonts, 仍有必要设置 "},{"type":"verbatim","value":"font-display"},{"type":"text","value":" 属性(建议为swap), 以避免页面在加载字体时变成一片空白."}]},{"type":"paragraph","children":[{"type":"text","value":"自托管web fonts: https://google-webfonts-helper.herokuapp.com/fonts"}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"@font-face的属性"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"bold","value":"注意: @font-face中的大部分属性是作为过滤器使用的."},{"type":"newline"},{"type":"text","value":"例如设置 "},{"type":"verbatim","value":"font-weight"},{"type":"text","value":" 的意思是当使用到这些字重时, 加载此字体."},{"type":"newline"},{"type":"text","value":"这与@font-face以外的 "},{"type":"verbatim","value":"font-weight"},{"type":"text","value":" 属性的功能截然不同."}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"src"}],"tags":[]},"children":[{"type":"section","level":5,"headline":{"type":"headline","children":[{"type":"text","value":"限制格式"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"WOFF(.woff): 从TrueType和OpenType压缩而来的字体格式, 是最适合用于Web字体的格式, 所有浏览器都支持此格式"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"WOFF2(.woff2): 下一代WOFF, 使用Brotli改进压缩, 压缩率比WOFF更高, 除IE外所有浏览器都支持"}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"TrueType(.ttf)/OpenType(.otf): 主流的字体格式, OpenType是TrueType的改进版本."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"embedded-opentype(.eot): 微软提案的字体格式, 仅IE支持此格式, Edge并不支持."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"svg(.svg): 已被弃用, 仅Safari还在支持此格式"}]}]},{"type":"paragraph","children":[{"type":"text","value":"从浏览器的市占率来看, 如今只提供woff2和woff格式的字体就够了."}]},{"type":"example","params":["css"],"value":"@font-face {\n font-family: 'Font';\n src: url('font.woff2') format('woff2'),\n url('font.woff') format('woff');\n}"}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"中文web fonts的体积问题"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"中文字符集非常巨大, 字体文件以MB级别起跳, 经常有超过10MB的字体."}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"WOFF2"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"WOFF2字体是可压缩的, 相比其他字体格式, 可减少30%的体积."}]},{"type":"paragraph","children":[{"type":"text","value":"参考: https://web.dev/reduce-webfont-size/"}]}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"可变字体"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"可变字体往往比不可变字体体积小, 因为不需要加载额外的粗体和斜体版本."}]},{"type":"paragraph","children":[{"type":"text","value":"2021年4月8日发布的新版思源黑体已经支持可变字体, 但字体大小不容乐观, 仍然在10MB以上."}]}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"子集化方案"}],"tags":[]},"children":[{"type":"section","level":5,"headline":{"type":"headline","children":[{"type":"text","value":"范围子集化(不可行)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"对于中文字体来说, 将单个字体以不同Unicode范围切割为多个字体, 然后在使用时只下载被使用到的字体, 这种方案是很容易想象的."},{"type":"newline"},{"type":"text","value":"对网页来说, 可以用"},{"type":"link","protocol":"https","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range","children":[{"type":"text","value":"unicode-range"}]},{"type":"text","value":"切分字形, 让页面只下载使用到的字符所需的字体."}]},{"type":"paragraph","children":[{"type":"text","value":"由于以下原因, 此方案缺乏可行性:"}]},{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"如何对中文字形进行切割是一个问题."},{"type":"newline"},{"type":"text","value":"如下方表格所示, 简体中文的常用字集有3500字, 可以覆盖99%的简体中文文本, 但是产生的字体大小对Web字体来说仍然不可接受."},{"type":"newline"},{"type":"text","value":"由于缺乏可以拿来作为参照的更精细的子集, 难以进一步切割."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"中文的常用字集在Unicode上不连续, 光是常用字集的Unicode范围就有20KB之巨."}]}]},{"type":"table","header":{"type":"tableRowGroup","children":[{"type":"tableRow","children":[{"type":"tableCell","children":[{"type":"text","value":" 汉字范围 "}]},{"type":"tableCell","children":[{"type":"text","value":" 字形数量 "}]},{"type":"tableCell","children":[{"type":"text","value":" Unicode范围 "}]},{"type":"tableCell","children":[{"type":"text","value":" 字体大小(WOFF) "}]}]}]},"children":[{"type":"tableRowGroup","children":[{"type":"tableRow","children":[{"type":"tableCell","children":[{"type":"text","value":" 基本汉字 "}]},{"type":"tableCell","children":[{"type":"text","value":" 20902字 "}]},{"type":"tableCell","children":[{"type":"text","value":" 4E00-9FA5 "}]},{"type":"tableCell","children":[{"type":"text","value":" 12MB "}]}]},{"type":"tableRow","children":[{"type":"tableCell","children":[{"type":"text","value":" "},{"type":"link","protocol":"https","url":"https://zh.wikisource.org/wiki/%E9%80%9A%E7%94%A8%E8%A7%84%E8%8C%83%E6%B1%89%E5%AD%97%E8%A1%A8","children":[{"type":"text","value":"《通用规范汉字表》"}]},{"type":"text","value":"(2013) 一级字表(常用字集) "}]},{"type":"tableCell","children":[{"type":"text","value":" 3500字 "}]},{"type":"tableCell","children":[{"type":"text","value":" 不连续 "}]},{"type":"tableCell","children":[{"type":"text","value":" 1.7MB "}]}]},{"type":"tableRow","children":[{"type":"tableCell","children":[{"type":"text","value":" 《通用规范汉字表》(2013) 二级字表 "}]},{"type":"tableCell","children":[{"type":"text","value":" 3000字 "}]},{"type":"tableCell","children":[{"type":"text","value":" 不连续 "}]},{"type":"tableCell","children":[{"type":"text","value":" 1.7MB "}]}]},{"type":"tableRow","children":[{"type":"tableCell","children":[{"type":"text","value":" 《通用规范汉字表》(2013) 三级字表 "}]},{"type":"tableCell","children":[{"type":"text","value":" 1605字 "}]},{"type":"tableCell","children":[{"type":"text","value":" 不连续 "}]},{"type":"tableCell","children":[{"type":"text","value":" 976KB "}]}]}]}]},{"type":"paragraph","children":[{"type":"text","value":"参考: https://www.zhangxinxu.com/wordpress/2016/11/css-unicode-range-character-font-face/"},{"type":"newline"},{"type":"text","value":"中文的Unicode编码范围: https://www.zhangxinxu.com/study/201611/chinese-language-unicode-range.html"}]}]},{"type":"section","level":5,"headline":{"type":"headline","children":[{"type":"text","value":"精确子集化"}],"tags":[]},"children":[{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"<>(推荐)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"Python的fonttools包的一部分, 提供了很多用来定制字体子集的参数."}]},{"type":"example","params":["sh"],"value":"pyftsubset font.otf --text='你好世界' --flavor=woff\npyftsubset font.otf --text='你好世界' --flavor=woff2"}]},{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"glyphhanger"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"https://github.com/zachleat/glyphhanger"}]},{"type":"paragraph","children":[{"type":"text","value":"glyphhanger利用"},{"type":"link","protocol":"internal","url":"pyftsubset","children":[{"type":"text","value":"pyftsubset"}]},{"type":"text","value":"实现字体子集化."},{"type":"newline"},{"type":"text","value":"功能很丰富."},{"type":"newline"},{"type":"text","value":"生成的文件大小优于fonteditor-core."}]},{"type":"example","params":["sh"],"value":"# 依赖\npip install fonttools\npip install brotli \npip install zopfli"},{"type":"section","level":7,"headline":{"type":"headline","children":[{"type":"text","value":"缺点"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"依赖过多"},{"type":"newline"},{"type":"text","value":"glyphhanger是一个以CLI为主的程序, 因此打包了太多依赖."},{"type":"newline"},{"type":"text","value":"其中影响最大的依赖是@zachleat/spider-pig包间接依赖的puppeteer, 它会下载Chromium."},{"type":"newline"},{"type":"text","value":"对于精确子集化场景来说, glyphhanger的很多功能都是不必要的, 不如直接调用pyftsubset."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"性能损失"},{"type":"newline"},{"type":"text","value":"glyphhanger会在转换前先根据文本文件/白名单生成一份Unicode范围表,"},{"type":"newline"},{"type":"text","value":"然而pyftsubset实际上支持直接输入需要保留的字符, 这也更符合精确子集化的场景."},{"type":"newline"},{"type":"text","value":"经过测试, 直接调用pyftsubset的生成速度至少是glyphhanger的2倍."}]}]}]}]},{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"HarfBuzz"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"https://github.com/harfbuzz/harfbuzz"}]},{"type":"paragraph","children":[{"type":"text","value":"一个用C++开发的字体整形(shaping)库, 可用来创建字体的子集."}]},{"type":"paragraph","children":[{"type":"text","value":"该项目有官方的WASM版本, 可以通过JavaScript调用:"},{"type":"newline"},{"type":"text","value":"https://github.com/harfbuzz/harfbuzzjs"}]},{"type":"paragraph","children":[{"type":"text","value":"基于harfbuzzjs的子集模块:"},{"type":"newline"},{"type":"text","value":"https://github.com/papandreou/subset-font"},{"type":"newline"},{"type":"text","value":"经过测试, 该模块的子集化性能和生成的字体大小都与pyftsubset相当."}]}]},{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"fontkit "}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"https://github.com/foliojs/fontkit"},{"type":"newline"},{"type":"text","value":"这是类似字蛛的库"},{"type":"link","protocol":"https","url":"https://github.com/Munter/subfont","children":[{"type":"text","value":"subfont"}]},{"type":"text","value":"使用的底层模块."}]},{"type":"paragraph","children":[{"type":"text","value":"遗憾的是该库自带的subset.encodeStream方法只能输出.ttf格式的字体."}]}]},{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"字蛛(v3.0.8, 不可用)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"https://github.com/aui/font-spider"}]},{"type":"paragraph","children":[{"type":"text","value":"原理是扫描文件, 以获得所有使用该字体的CSS, 然后再从中提取到所有被使用的字符."}]},{"type":"paragraph","children":[{"type":"text","value":"该项目是基于"},{"type":"link","protocol":"internal","url":"fontmin","children":[{"type":"text","value":"fontmin"}]},{"type":"text","value":"实现的."}]},{"type":"section","level":7,"headline":{"type":"headline","children":[{"type":"text","value":"缺陷"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"不适用于UGC网站, 因为用户会使用到的字符是不可能提前预测的, 只能加载全部字符."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"项目上一次更新已是2年前, 且官方网站的域名已经过期."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"共享fontmin所有的问题."}]}]}]}]},{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"<>(v0.9.8, 不可用)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"https://github.com/ecomfe/fontmin"}]},{"type":"paragraph","children":[{"type":"text","value":"字蛛的底层, 由百度EFE团队开发, 可以手动指定需要提取的字符, 然后生成字体."}]},{"type":"paragraph","children":[{"type":"text","value":"只支持以ttf为源, 其他格式都要先转换为ttf再处理, 内置了一些转换插件."}]},{"type":"paragraph","children":[{"type":"text","value":"该项目是的字体编辑功能是用"},{"type":"link","protocol":"https","url":"https://github.com/kekee000/fonteditor-core","children":[{"type":"text","value":"fonteditor-core"}]},{"type":"text","value":"实现的."}]},{"type":"section","level":7,"headline":{"type":"headline","children":[{"type":"text","value":"缺陷"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"共享fonteditor-core所有的问题."}]}]}]}]},{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"<>(v2.1.8, 不可用)"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"子集化所需的时间与所需的字形数量成正比."}]},{"type":"section","level":7,"headline":{"type":"headline","children":[{"type":"text","value":"缺陷"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"问题众多, 缺乏维护."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"一次把整个字体读取到内存里."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"对OTF的子集化实现有问题."},{"type":"newline"},{"type":"text","value":"https://github.com/kekee000/fonteditor-core/issues/18"}]}]}]}]}]}]}]}]},{"type":"section","level":2,"headline":{"type":"headline","children":[{"type":"text","value":"用户代理自定义字体"}],"tags":[]},"children":[{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"Advanced Font Settings"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"通过Advanced Font Settings, 可以按ISO 15924中定义的script设置浏览器使用的默认字体."},{"type":"newline"},{"type":"text","value":"例如, 简体中文的script是Simplified Han, 英文的script是Latin."}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"为什么它不起作用"}],"tags":[]},"children":[{"type":"list","indent":0,"ordered":false,"children":[{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"现代网页的字体族设置方式通常是将默认字体放在最后一位作为fallback,"},{"type":"newline"},{"type":"text","value":"页面上的文本能够使用到默认字体的情况极为罕见, 因此用户往往无法在页面上使用自己想要的字体."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"Advanced Font Settings中设置的字体要求网页的语言与script匹配, 增加了设置的复杂性."},{"type":"newline"},{"type":"text","value":"在绝大多数情况下, 该扩展都不如Chrome自带的字体设置易用."}]},{"type":"listItem","indent":0,"checked":null,"term":null,"children":[{"type":"text","value":"Advanced Font Settings使用的API "},{"type":"link","protocol":"https","url":"https://developer.chrome.com/docs/extensions/reference/fontSettings/","children":[{"type":"text","value":"Chrome.fontSettings"}]},{"type":"text","value":", 曾经还有错误地将字重作为字体名提供的问题."},{"type":"newline"},{"type":"text","value":"虽然该问题在如今的Chrome里已经解决, 但仍然无法为默认字体配置字重."},{"type":"newline"},{"type":"text","value":"因此在Appoint Font里, 相关API只被用来获取字体列表."}]}]}]}]},{"type":"section","level":3,"headline":{"type":"headline","children":[{"type":"text","value":"潜在的字体修复方式"}],"tags":[]},"children":[{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"将页面里的所有元素重置为默认字体"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"该方案的问题在于, 除非用于重置字体的扩展程序逐一读取元素的CSS样式,"},{"type":"newline"},{"type":"text","value":"否则扩展程序无法知晓字体应该被重置为哪种类型."}]},{"type":"paragraph","children":[{"type":"text","value":"重置元素样式本身作为一种对页面的侵入行为, 还可能引起降低页面性能的副作用."},{"type":"newline"},{"type":"text","value":"这一点在将Appoint Font用于Google的部分网页时已经得到验证."}]}]},{"type":"section","level":4,"headline":{"type":"headline","children":[{"type":"text","value":"使用font-face替换其他字体名"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"可以用font-face替换用户代理已经安装的字体名."},{"type":"newline"},{"type":"text","value":"加上可以通过Canvas判断出字体是等宽字体还是非等宽字体, 从而替换掉相应类型的字体."}]},{"type":"paragraph","children":[{"type":"text","value":"虽然没有区分适用范围最广的衬线体和非衬线体的有效手段,"},{"type":"newline"},{"type":"text","value":"所幸对于用户代理的自定义需求而言, 通常是将它们一视同仁地替换掉."}]},{"type":"section","level":5,"headline":{"type":"headline","children":[{"type":"text","value":"缺陷"}],"tags":[]},"children":[{"type":"section","level":6,"headline":{"type":"headline","children":[{"type":"text","value":"字体别名问题"}],"tags":[]},"children":[{"type":"paragraph","children":[{"type":"text","value":"一个字体可能有多种名称, 例如\"微软雅黑\"和\"Microsoft YaHei\"是同一个字体."},{"type":"newline"},{"type":"text","value":"用 "},{"type":"verbatim","value":"font-face"},{"type":"text","value":" 替换字体的前提是能够知道字体的所有别名."}]}]}]}]}]}]}]}]}},"assetMap":{}},"highlightedCodeMap":{"\n\n\n\n\n\n":{"html":[[{"color":"#7A88CF","text":""}],[{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"label"},{"color":"#86E1FC","text":">"}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"input "},{"color":"#FFC777","text":"type"},{"color":"#86E1FC","text":"="},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"checkbox"},{"color":"#86E1FC","text":"\""},{"color":"#FF757F","text":" "},{"color":"#86E1FC","text":"/>"}],[{"color":"#C8D3F5","text":" text"}],[{"color":"#86E1FC","text":""}],[],[{"color":"#7A88CF","text":""}],[{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"label"},{"color":"#86E1FC","text":">"}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"input "},{"color":"#FFC777","text":"type"},{"color":"#86E1FC","text":"="},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"checkbox"},{"color":"#86E1FC","text":"\""},{"color":"#FF757F","text":" "},{"color":"#86E1FC","text":"/>"}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"span"},{"color":"#86E1FC","text":">"},{"color":"#C8D3F5","text":"text"},{"color":"#86E1FC","text":""}],[{"color":"#86E1FC","text":""}],[],[{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"style"},{"color":"#86E1FC","text":">"}],[{"color":"#B4C2F0","text":" "},{"color":"#7A88CF","text":"/* 必须在input和span上都设置vertical-align, 因为此属性不能继承 */"}],[{"color":"#B4C2F0","text":" "},{"color":"#FF757F","text":"input"},{"color":"#86E1FC","text":","},{"color":"#B4C2F0","text":" "},{"color":"#FF757F","text":"span"},{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"vertical-align"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"middle"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" }"}],[{"color":"#86E1FC","text":""}]]},"\n\n":{"html":[[{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"label"},{"color":"#86E1FC","text":">"}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"input "},{"color":"#FFC777","text":"type"},{"color":"#86E1FC","text":"="},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"checkbox"},{"color":"#86E1FC","text":"\""},{"color":"#FF757F","text":" "},{"color":"#86E1FC","text":"/>"}],[{"color":"#C8D3F5","text":" text"}],[{"color":"#86E1FC","text":""}],[],[{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"style"},{"color":"#86E1FC","text":">"}],[{"color":"#B4C2F0","text":" "},{"color":"#FF757F","text":"label"},{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"display"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"inline-flex"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"align-items"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"center"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" }"}],[{"color":"#86E1FC","text":""}]]},"img {\n max-width: 100%;\n height: auto;\n}":{"css":[[{"color":"#FF757F","text":"img"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"max-width"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"height"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"auto"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":"}"}]]},".ratio4-3 {\n background-image: url(image.svg);\n background-size: cover;\n\n width: 100%;\n height: 0;\n padding: 0; /* reset padding */\n padding-bottom: calc(3 / 4 * 100%);\n}":{"css":[[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"ratio4-3"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-image"},{"color":"#86E1FC","text":": "},{"color":"#65BCFF","text":"url"},{"color":"#B4C2F0","text":"("},{"color":"#FCA7EA","text":"image.svg"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-size"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"cover"},{"color":"#86E1FC","text":";"}],[],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"width"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"height"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"0"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"padding"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"0"},{"color":"#86E1FC","text":"; "},{"color":"#7A88CF","text":"/* reset padding */"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"padding-bottom"},{"color":"#86E1FC","text":": "},{"color":"#65BCFF","text":"calc"},{"color":"#B4C2F0","text":"("},{"color":"#FF966C","text":"3"},{"color":"#86E1FC","text":" / "},{"color":"#FF966C","text":"4"},{"color":"#86E1FC","text":" * "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":"}"}]]},".container {\n display: grid;\n grid-template:\n \"head head head\" minmax(150px, auto)\n \"sidebar content content\" auto\n \"sidebar footer footer\" auto / 1fr 1fr 1fr;\n}":{"css":[[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"container"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"display"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"grid"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"grid-template"},{"color":"#86E1FC","text":":"}],[{"color":"#86E1FC","text":" "},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"head head head"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":" "},{"color":"#65BCFF","text":"minmax"},{"color":"#B4C2F0","text":"("},{"color":"#FF966C","text":"150"},{"color":"#FC7B7B","text":"px"},{"color":"#86E1FC","text":", "},{"color":"#FCA7EA","text":"auto"},{"color":"#B4C2F0","text":")"}],[{"color":"#86E1FC","text":" "},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"sidebar content content"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":" "},{"color":"#FCA7EA","text":"auto"}],[{"color":"#86E1FC","text":" "},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"sidebar footer footer"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":" "},{"color":"#FCA7EA","text":"auto"},{"color":"#86E1FC","text":" / "},{"color":"#FF966C","text":"1"},{"color":"#FC7B7B","text":"fr"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"1"},{"color":"#FC7B7B","text":"fr"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"1"},{"color":"#FC7B7B","text":"fr"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":"}"}]]},".container {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n grid-template-areas:\n \"....... header header header\"\n \"sidebar content content content\"\n \"sidebar footer footer footer\";\n}":{"css":[[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"container"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"display"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"grid"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"grid-template-columns"},{"color":"#86E1FC","text":": "},{"color":"#65BCFF","text":"repeat"},{"color":"#B4C2F0","text":"("},{"color":"#FF966C","text":"4"},{"color":"#86E1FC","text":", "},{"color":"#FF966C","text":"1"},{"color":"#FC7B7B","text":"fr"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"grid-template-areas"},{"color":"#86E1FC","text":":"}],[{"color":"#86E1FC","text":" "},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"....... header header header"},{"color":"#86E1FC","text":"\""}],[{"color":"#86E1FC","text":" "},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"sidebar content content content"},{"color":"#86E1FC","text":"\""}],[{"color":"#86E1FC","text":" "},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"sidebar footer footer footer"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":"}"}]]},".container {\n display: grid;\n grid-template-columns: [sidebar-start] 1fr [sidebar-end content-start] 2fr [content-end];\n}\n\n.content {\n grid-column: content-start / content-end;\n}":{"css":[[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"container"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"display"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"grid"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"grid-template-columns"},{"color":"#86E1FC","text":": [sidebar-start] "},{"color":"#FF966C","text":"1"},{"color":"#FC7B7B","text":"fr"},{"color":"#86E1FC","text":" [sidebar-end content-start] "},{"color":"#FF966C","text":"2"},{"color":"#FC7B7B","text":"fr"},{"color":"#86E1FC","text":" [content-end];"}],[{"color":"#86E1FC","text":"}"}],[],[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"content"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"grid-column"},{"color":"#86E1FC","text":": content-start / content-end;"}],[{"color":"#86E1FC","text":"}"}]]},"/* BAD */\n/* 均分为14列的网格系统, 子元素的位置会缺乏可读性和可维护性 */\ngrid-template-columns: repeat(14, 1fr);\n\n/* GOOD */\n/* 不受具体列数限制的网格设计 */\ngrid-template-columns: [left-gutter] 1fr [sidebar] 4fr [content] 8fr [right-gutter] 1fr;":{"css":[[{"color":"#7A88CF","text":"/* BAD */"}],[{"color":"#7A88CF","text":"/* 均分为14列的网格系统, 子元素的位置会缺乏可读性和可维护性 */"}],[{"color":"#FF757F","text":"grid-template-columns"},{"color":"#C8D3F5","text":": repeat(14"},{"color":"#86E1FC","text":","},{"color":"#C8D3F5","text":" 1fr);"}],[],[{"color":"#7A88CF","text":"/* GOOD */"}],[{"color":"#7A88CF","text":"/* 不受具体列数限制的网格设计 */"}],[{"color":"#FF757F","text":"grid-template-columns"},{"color":"#C8D3F5","text":": "},{"color":"#86E1FC","text":"["},{"color":"#C099FF","text":"left-gutter"},{"color":"#86E1FC","text":"]"},{"color":"#C8D3F5","text":" 1fr "},{"color":"#86E1FC","text":"["},{"color":"#C099FF","text":"sidebar"},{"color":"#86E1FC","text":"]"},{"color":"#C8D3F5","text":" 4fr "},{"color":"#86E1FC","text":"["},{"color":"#C099FF","text":"content"},{"color":"#86E1FC","text":"]"},{"color":"#C8D3F5","text":" 8fr "},{"color":"#86E1FC","text":"["},{"color":"#C099FF","text":"right-gutter"},{"color":"#86E1FC","text":"]"},{"color":"#C8D3F5","text":" 1fr;"}]]},"\n
\n \n \n \n

\n\n \n

\n
":{"html":[[{"color":"#7A88CF","text":""}],[{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"div "},{"color":"#FFC777","text":"class"},{"color":"#86E1FC","text":"="},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"author-bio"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":">"}],[{"color":"#C8D3F5","text":" "},{"color":"#7A88CF","text":""}],[{"color":"#C8D3F5","text":" "},{"color":"#7A88CF","text":""}],[{"color":"#C8D3F5","text":" "},{"color":"#7A88CF","text":""}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"h2 "},{"color":"#FFC777","text":"class"},{"color":"#86E1FC","text":"="},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"author-bio__name author-bio__name--color-yellow"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":">"},{"color":"#C8D3F5","text":" "}],[],[{"color":"#C8D3F5","text":" "},{"color":"#7A88CF","text":""}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"h2 "},{"color":"#FFC777","text":"class"},{"color":"#86E1FC","text":"="},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"author-bio__avatar"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":">"},{"color":"#C8D3F5","text":" "}],[{"color":"#86E1FC","text":""}]]},".author-bio {}\n\n.author-bio__name {}\n.author-bio__name--color-yellow {}\n.author-bio__name--color-red {}\n.author-bio__name--color-green {}\n\n.author-bio__avatar {}":{"css":[[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"author-bio"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{}"}],[],[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"author-bio__name"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{}"}],[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"author-bio__name--color-yellow"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{}"}],[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"author-bio__name--color-red"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{}"}],[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"author-bio__name--color-green"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{}"}],[],[{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"author-bio__avatar"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{}"}]]},"\n\n \n \n \n \n
\n
front
\n
back
\n
\n \n \n":{"html":[[{"color":"#86E1FC","text":""}],[{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"html"},{"color":"#86E1FC","text":">"}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"head"},{"color":"#86E1FC","text":">"}],[{"color":"#86E1FC","text":" <"},{"color":"#FF757F","text":"style"},{"color":"#86E1FC","text":">"}],[{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"card"},{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"width"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"150"},{"color":"#FC7B7B","text":"px"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"height"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"200"},{"color":"#FC7B7B","text":"px"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"transition"},{"color":"#86E1FC","text":": transform "},{"color":"#FF966C","text":"1"},{"color":"#FC7B7B","text":"s"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"transform-style"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"preserve-3d"},{"color":"#86E1FC","text":"; "},{"color":"#7A88CF","text":"/* 使子元素位于同一个3D空间, 能够互相遮挡 */"}],[{"color":"#86E1FC","text":" }"}],[],[{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"card--flipped"},{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"transform"},{"color":"#86E1FC","text":": "},{"color":"#65BCFF","text":"rotateY"},{"color":"#B4C2F0","text":"("},{"color":"#FF966C","text":"180"},{"color":"#FC7B7B","text":"deg"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":"; "},{"color":"#7A88CF","text":"/* 将整张卡绕Y轴旋转180度, 相当于把卡翻过来 */"}],[{"color":"#86E1FC","text":" }"}],[],[{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"card__face"},{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"width"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"height"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"position"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"absolute"},{"color":"#86E1FC","text":"; "},{"color":"#7A88CF","text":"/* 使两张卡片重叠 */"}],[{"color":"#86E1FC","text":" }"}],[],[{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"card__face--front"},{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-color"},{"color":"#86E1FC","text":": "},{"color":"#FF98A4","text":"brown"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" }"}],[],[{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"."},{"color":"#FFC777","text":"card__face--back"},{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-color"},{"color":"#86E1FC","text":": "},{"color":"#FF98A4","text":"darkcyan"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"transform"},{"color":"#86E1FC","text":": "},{"color":"#65BCFF","text":"rotateY"},{"color":"#B4C2F0","text":"("},{"color":"#FF966C","text":"180"},{"color":"#FC7B7B","text":"deg"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":"; "},{"color":"#7A88CF","text":"/* 将这一面绕Y轴旋转180度, 作为卡背 */"}],[{"color":"#86E1FC","text":" }"}],[{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":""}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":""}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"body"},{"color":"#86E1FC","text":">"}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"div "},{"color":"#FFC777","text":"class"},{"color":"#86E1FC","text":"="},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"card"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":">"}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"div "},{"color":"#FFC777","text":"class"},{"color":"#86E1FC","text":"="},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"card__face card__face--front"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":">"},{"color":"#C8D3F5","text":"front"},{"color":"#86E1FC","text":""}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"<"},{"color":"#FF757F","text":"div "},{"color":"#FFC777","text":"class"},{"color":"#86E1FC","text":"="},{"color":"#86E1FC","text":"\""},{"color":"#C3E88D","text":"card__face card__face--back"},{"color":"#86E1FC","text":"\""},{"color":"#86E1FC","text":">"},{"color":"#C8D3F5","text":"back"},{"color":"#86E1FC","text":""}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":""}],[{"color":"#86E1FC","text":" <"},{"color":"#FF757F","text":"script"},{"color":"#86E1FC","text":">"}],[{"color":"#B4C2F0","text":" "},{"color":"#C099FF","text":"const"},{"color":"#B4C2F0","text":" "},{"color":"#FF98A4","text":"card"},{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":"="},{"color":"#B4C2F0","text":" "},{"color":"#FFC777","text":"document"},{"color":"#86E1FC","text":"."},{"color":"#65BCFF","text":"querySelector"},{"color":"#B4C2F0","text":"("},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":".card"},{"color":"#86E1FC","text":"'"},{"color":"#B4C2F0","text":")"}],[{"color":"#B4C2F0","text":" "},{"color":"#FFC777","text":"card"},{"color":"#86E1FC","text":"."},{"color":"#65BCFF","text":"addEventListener"},{"color":"#B4C2F0","text":"("},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":"click"},{"color":"#86E1FC","text":"'"},{"color":"#86E1FC","text":","},{"color":"#B4C2F0","text":" () "},{"color":"#C099FF","text":"=>"},{"color":"#B4C2F0","text":" "},{"color":"#FFC777","text":"card"},{"color":"#86E1FC","text":"."},{"color":"#A9B8E8","text":"classList"},{"color":"#86E1FC","text":"."},{"color":"#65BCFF","text":"toggle"},{"color":"#B4C2F0","text":"("},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":"card--flipped"},{"color":"#86E1FC","text":"'"},{"color":"#B4C2F0","text":"))"}],[{"color":"#B4C2F0","text":" "},{"color":"#86E1FC","text":""}],[{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":""}],[{"color":"#86E1FC","text":""}]]},"a {\n text-decoration: none;\n background-image: linear-gradient(red, red);\n background-position: bottom;\n background-size: 100% 2px;\n background-repeat: no-repeat;\n}":{"css":[[{"color":"#FF757F","text":"a"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"text-decoration"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"none"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-image"},{"color":"#86E1FC","text":": "},{"color":"#65BCFF","text":"linear-gradient"},{"color":"#B4C2F0","text":"("},{"color":"#FF98A4","text":"red"},{"color":"#86E1FC","text":", "},{"color":"#FF98A4","text":"red"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-position"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"bottom"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-size"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"2"},{"color":"#FC7B7B","text":"px"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-repeat"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"no-repeat"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":"}"}]]},"a {\n text-decoration: none;\n box-shadow: inset 0 -2px 0 0 #ccc;\n transition: box-shadow 0.3s;\n\n &:hover {\n box-shadow: inset 0 -1em 0 0 #ccc;\n }\n}":{"scss":[[{"color":"#FF757F","text":"a"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"text-decoration"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"none"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"box-shadow"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"inset"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"0"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"-2"},{"color":"#FC7B7B","text":"px"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"0"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"0"},{"color":"#86E1FC","text":" #ccc;"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"transition"},{"color":"#86E1FC","text":": "},{"color":"#82AAFF","text":"box-shadow"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"0.3"},{"color":"#FC7B7B","text":"s"},{"color":"#86E1FC","text":";"}],[],[{"color":"#86E1FC","text":" "},{"color":"#FF757F","text":"&"},{"color":"#86E1FC","text":":"},{"color":"#4FD6BE","text":"hover"},{"color":"#86E1FC","text":" {"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"box-shadow"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"inset"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"0"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"-1"},{"color":"#FC7B7B","text":"em"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"0"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"0"},{"color":"#86E1FC","text":" #ccc;"}],[{"color":"#86E1FC","text":" }"}],[{"color":"#86E1FC","text":"}"}]]},"a {\n text-decoration: none;\n background-image: linear-gradient(red, red);\n background-position: bottom;\n background-size: 100% 2px;\n background-repeat: no-repeat;\n transition: background-size 0.3s;\n\n &:hover {\n background-size: 100% 100%; /* 第二个值决定了展开的高度, 出于动画目的, 此值不可省略 */\n }\n}":{"scss":[[{"color":"#FF757F","text":"a"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"text-decoration"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"none"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-image"},{"color":"#86E1FC","text":": "},{"color":"#65BCFF","text":"linear-gradient"},{"color":"#B4C2F0","text":"("},{"color":"#FF98A4","text":"red"},{"color":"#86E1FC","text":", "},{"color":"#FF98A4","text":"red"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-position"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"bottom"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-size"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"2"},{"color":"#FC7B7B","text":"px"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-repeat"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"no-repeat"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"transition"},{"color":"#86E1FC","text":": "},{"color":"#82AAFF","text":"background-size"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"0.3"},{"color":"#FC7B7B","text":"s"},{"color":"#86E1FC","text":";"}],[],[{"color":"#86E1FC","text":" "},{"color":"#FF757F","text":"&"},{"color":"#86E1FC","text":":"},{"color":"#4FD6BE","text":"hover"},{"color":"#86E1FC","text":" {"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-size"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#86E1FC","text":"; "},{"color":"#7A88CF","text":"/* 第二个值决定了展开的高度, 出于动画目的, 此值不可省略 */"}],[{"color":"#86E1FC","text":" }"}],[{"color":"#86E1FC","text":"}"}]]},"a {\n text-decoration: none;\n background-image: linear-gradient(red, red);\n background-position: bottom; /* 图片从中下方开始显示(从中下方展开) */\n background-size: 0 2px; /* 默认情况下宽度为0, 不显示下划线 */\n background-repeat: no-repeat;\n transition: background-size 0.3s;\n\n &:hover {\n background-size: 100% 2px;\n }\n}":{"scss":[[{"color":"#FF757F","text":"a"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"text-decoration"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"none"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-image"},{"color":"#86E1FC","text":": "},{"color":"#65BCFF","text":"linear-gradient"},{"color":"#B4C2F0","text":"("},{"color":"#FF98A4","text":"red"},{"color":"#86E1FC","text":", "},{"color":"#FF98A4","text":"red"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-position"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"bottom"},{"color":"#86E1FC","text":"; "},{"color":"#7A88CF","text":"/* 图片从中下方开始显示(从中下方展开) */"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-size"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"0"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"2"},{"color":"#FC7B7B","text":"px"},{"color":"#86E1FC","text":"; "},{"color":"#7A88CF","text":"/* 默认情况下宽度为0, 不显示下划线 */"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-repeat"},{"color":"#86E1FC","text":": "},{"color":"#FCA7EA","text":"no-repeat"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"transition"},{"color":"#86E1FC","text":": "},{"color":"#82AAFF","text":"background-size"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"0.3"},{"color":"#FC7B7B","text":"s"},{"color":"#86E1FC","text":";"}],[],[{"color":"#86E1FC","text":" "},{"color":"#FF757F","text":"&"},{"color":"#86E1FC","text":":"},{"color":"#4FD6BE","text":"hover"},{"color":"#86E1FC","text":" {"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"background-size"},{"color":"#86E1FC","text":": "},{"color":"#FF966C","text":"100"},{"color":"#FC7B7B","text":"%"},{"color":"#86E1FC","text":" "},{"color":"#FF966C","text":"2"},{"color":"#FC7B7B","text":"px"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" }"}],[{"color":"#86E1FC","text":"}"}]]},"@font-face {\n font-family: 'Font';\n src: url('font.woff2') format('woff2'),\n url('font.woff') format('woff');\n}":{"css":[[{"color":"#86E1FC","text":"@font-face"},{"color":"#C8D3F5","text":" "},{"color":"#86E1FC","text":"{"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"font-family"},{"color":"#86E1FC","text":": "},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":"Font"},{"color":"#86E1FC","text":"'"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":" "},{"color":"#82AAFF","text":"src"},{"color":"#86E1FC","text":": "},{"color":"#65BCFF","text":"url"},{"color":"#B4C2F0","text":"("},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":"font.woff2"},{"color":"#86E1FC","text":"'"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":" "},{"color":"#65BCFF","text":"format"},{"color":"#B4C2F0","text":"("},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":"woff2"},{"color":"#86E1FC","text":"'"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":","}],[{"color":"#86E1FC","text":" "},{"color":"#65BCFF","text":"url"},{"color":"#B4C2F0","text":"("},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":"font.woff"},{"color":"#86E1FC","text":"'"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":" "},{"color":"#65BCFF","text":"format"},{"color":"#B4C2F0","text":"("},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":"woff"},{"color":"#86E1FC","text":"'"},{"color":"#B4C2F0","text":")"},{"color":"#86E1FC","text":";"}],[{"color":"#86E1FC","text":"}"}]]},"pyftsubset font.otf --text='你好世界' --flavor=woff\npyftsubset font.otf --text='你好世界' --flavor=woff2":{"sh":[[{"color":"#FFC777","text":"pyftsubset"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"font.otf"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"--text="},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":"你好世界"},{"color":"#86E1FC","text":"'"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"--flavor=woff"}],[{"color":"#FFC777","text":"pyftsubset"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"font.otf"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"--text="},{"color":"#86E1FC","text":"'"},{"color":"#C3E88D","text":"你好世界"},{"color":"#86E1FC","text":"'"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"--flavor=woff2"}]]},"# 依赖\npip install fonttools\npip install brotli \npip install zopfli":{"sh":[[{"color":"#7A88CF","text":"# 依赖"}],[{"color":"#FFC777","text":"pip"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"install"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"fonttools"}],[{"color":"#FFC777","text":"pip"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"install"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"brotli"},{"color":"#C8D3F5","text":" "}],[{"color":"#FFC777","text":"pip"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"install"},{"color":"#C8D3F5","text":" "},{"color":"#C3E88D","text":"zopfli"}]]}},"assetURLsMap":{},"variants":[{"name":"en","displayName":"English"},{"name":"es","displayName":"Español"},{"name":"ja","displayName":"日本語"},{"name":"ko","displayName":"한국어"},{"name":"pt","displayName":"Português"},{"name":"ru","displayName":"Русский"},{"name":"zh-TW","displayName":"繁體中文"}]}}}