蘇z 发布的文章

1.继承和层叠

继承(Inheritance)是从一个元素向其后代元素传递属性值所采用的机制。确定应向一个元素应用哪些值时,用户代理不仅要考虑继承,还要考虑声明的特殊性(优先级),以及声明本身的来源,这个过程就叫做层叠(cascade)。

2.CSS特殊性值的计算规则

  1. 对于选择器中给定的各个ID属性值,加 0, 1, 0, 0
  2. 对于选择器中给的的各个类属性值、属性选择或伪类,加 0, 0, 1, 0
  3. 对于选择器中给定的各个元素和伪元素,加 0, 0, 0, 1。
  4. 内联样式的特殊性为 1, 0, 0, 0
  5. 结合符和通配选择器对特殊性没有任何贡献,但又不完全一样。结合符继承根本没有特殊性,通配选择器的特殊性是0, 0, 0, 0,是要高于结合符继承的。
  6. !important 令该样式规则获取到最高的优先级,甚至是超过内联样式
h1 {color: red;}                   /* specificity = 0, 0, 0, 1 */
p, em {color: red;}                /* specificity = 0, 0, 0, 2 */
.grape {color: red;}               /* specificity = 0, 0, 1, 0 */
*.grape {color: red;}              /* specificity = 0, 0, 1, 0 */
p.bright em.dark {color: red;}     /* specificity = 0, 0, 2, 2 */
#id666 {color: red;}               /* specificity = 0, 1, 0, 0 */
div#sidebar *[href] {color: red;}  /* specificity = 0, 1, 1, 1 */

注意:

通配选择器的特殊性是0, 0, 0, 0,这与根本没有特殊性是有区别的,后续会讲到。

相对来说,结合符则根本没有特殊性,甚至连0特殊性都没有。

声明性与特殊性

一旦确定一个选择器的特殊性,这个值将被授予其所有的相关声明。

h1, h2.section {color: red; background: black;},为了计算优先级,用户代理会将完整的规则"解组"为单独的规则,如下所示,这样计算出的优先级就可以赋值给每一个相关声明。

h1 {color: red;} /* 0, 0, 0, 1 */
h1 {background: black;}  /* 0, 0, 0, 1 */
h2.section {color: red;} /* 0, 0, 1, 1 */
h2.section {background: black;} /* 0, 0, 1, 1 */

任何情况下,用户代理都会确定那些规则与一个元素匹配,计算出所有相关的声明及其特殊性,确定哪些规则胜出,然后将胜出的规则应用到元素,从而得到应用样式后的结果。每个元素、选择器和声明上都必须完成这些工作。这个行为是层叠的一个重要部分。

重要性(重要声明important declaration)

在声明的结束分号之前插入!important来标志重要声明。

重要声明并没有特殊的特殊性值。如果一个重要声明和一个非重要声明冲突,胜出的总是重要声明。

另外:重要声明与非重要声明的特殊性是分开处理的。也就是说,所有重要声明会放在一起,它们之间的冲突在这个范围内解决。同样,非重要的声明作为一个整体,其中的冲突使用特殊性解决。

3.继承与特殊性

基于继承,样式不仅可以应用到指定的元素,还可以应用到它的后代元素。但是要注意继承到后代的样式是没有特殊性的,连0都没有。

所以:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <style>
        *{
            color: gray;
        }
        h1{
            color: red;
        }
    </style>
</head>
<body>
<h1>
    <!--em继承的gray不存在特殊性,*通配符的特殊性虽然是0但是比没有高-->
    今天是<em>周五</em>
</h1>
</body>
</html>

image-20211008222900895

如上所示的代码,我们知道通配选择器的特殊性是0,然而继承根本没有特殊性,所以上面的代码运行结果是”周五“是灰色而不是红色。总之,0特殊性比无特殊性要强。

4.层叠

CSS2.1的层叠规则

  1. 找出匹配特定元素的所有规则
  2. 按显示权重排序应用到特定元素上的所有声明。标志!important的规则的权重要高于没有!important标志的规则。
  3. 按来源对应用到给定元素的所有声明排序。共有3种来源:创作人员、读者和用户代理。正常情况下,创作人员的样式要胜过读者的样式。有!important标志的读者样式要强于所有其他样式,这包括有!important标志的创作人员样式。创作人员样式和读者样式都比用户代理的默认样式要强。
  4. 按照特殊性对应用到给定元素的所有声明排序。有较高特殊性的元素权重要大于有较低特殊性的元素。
  5. 按出现顺序对应用到给定元素的所有声明排序。一个声明在样式表或文档中越后出现,他的权重就越大。如果样式表中有导入的样式表,一般认为出现在导入样式表中的声明在前,主样式表中的所有声明在在后。

声明的权重级别

在声明权重方面要考虑从5级,权重由大到小的顺序依次为:

  1. 读者的重要声明
  2. 创作人员的重要声明
  3. 创作人员的常规声明
  4. 读者的常规声明
  5. 用户代理的默认声明

按特殊性排序(当声明的权重相同时)

如果向一个元素应用多个彼此冲突的声明,而且它们的权重相同,则要按特殊性排序,最特殊的声明最优先。

按前后位置排序

后声明的规则比之前声明的规则具有更高的权重。

对于不同样式表的规则,如:

@import url(basic.css)
h1 {color: blue;}


// basic.css 中
h1 {color: red;}

通过import导入的样式表,相当于是在import处声明的,所以后声明的h1 {color: blue;}权重更高。

LVFHA

相同特殊性、来源、权重情况下,越靠后的规则会胜出,内联样式通常被视为位于最后。所以对于link-visited-focus-hover-active的推荐顺序是LVFHA,其它的顺序可能不会正常工作

CSS之外的表现提示(presentational hint)

如HTML中的font元素或一些内联样式属性(如bgcolorsize等)是表现提示。它们虽然不属于常规css规则,但是也能够影响元素的样式。

这种变现提示的特殊性为0,并且认为出现在创作人员编写的样式表的开头。表现提示将被创作人员编写的样式或渎职提供的样式覆盖,但是不会被用户代理的默认样式覆盖。

1.样式的基本规则

元素选择器

元素选择器(element selector)通常是HTML元素。

声明和关键字

声明的格式统一为属性: 值;的格式,一个声明块中可能有一个或多个声明,要注意的是如果声明中的属性或值有误,整个规则都将会被忽略(书上是这样写的,但实际上应该是只会忽略有问题的声明)。属性的值可以是多个关键字,关键字之间通常以空格分隔,有时也会用斜线或逗号。

2.分组

选择器和选择器分组

h2, p {color: gray;}这样的一条css规则,将会分别作用于h2和p元素上,即应用于这两个选择器所引用的元素。这个逗号告诉浏览器,规则包含两个不同的选择器。

可以将任意多个选择器分组在一起。

通配选择器

* {color: white;}*号可以视为是一个通配符,这个选择器可以于任何元素匹配。使用这个选择器可以把文档中所有元素的color值都指定为white。

声明分组

如: h2, p {color: gray; font: 18px;}(大括号内的是声明块,每个逗号与之前的一部分叫做声明)

注意: 对声明分组的时候,一定要在各个声明的最后使用分号。

2.类选择器和ID选择器

类选择器和ID选择器允许以一种独立于文档元素的方式来指定样式,这些选择器可以单独使用,也可以与其他元素选择器结合使用。

类选择器

要应用样式而不考虑具体涉及的元素,最常用的方法就是使用类选择器。

p.waning {font-weight: bold;},选择器将会匹配class属性包含warning的所有p元素。

.waning {font-weight: bold;},选择器将会匹配class属性包含warning的所有元素。

注意:使用id选择器、类选择器、属性选择器、伪类、伪元素选择器时,如果没有使用元素选择器,那么将隐式蕴含通用选择器。

多类选择器

通过把两个类选择器相连在一起,可以选择同时包含这些类名的元素(类名的顺序不限)。

.warning.urgent这样的选择器。

如果一个多类选择器包含类名列表(类名以空格分隔)中所没有的一个类名,匹配就会失败。

p.warning.help {background: red;}。这个选择器将值匹配class包含warning和help的那些p元素。So,如果一个p元素的class属性中只有词warning和urgent,将不能被匹配到。不过,他能匹配以下元素:<p class="urgent warning help">Help me!!!</p>

id选择器

ID选择器引用元素id属性中的值。其前面有一个‘#’号区别于类选择器的'.'。

,因为ID属性不允许有以空格分隔得词列表。

类选择器还是id选择器

可以为多个元素指定类,但是ID选择器只会匹配到一个元素(按照规则)。

3.属性选择器

分类:简单属性选择器、精准属性值选择器、部分匹配属性值选择器

简单属性选择器(根据是否有某属性选择)

h1[title] {color: red;}将会选择所有具有title属性的h1元素,并且不会考虑title属性的值是什么。

a[href][title] {font-weight: bold;}会将同时拥有href和title属性的HTML超链接元素设置为粗体

精准属性值选择器(根据具体属性值选择)

h1[title="my"] {color: red;}将会选择所有具有title属性且值为"my"的h1元素。

h1[title="my" href="/test"] {color: red;}将会选择所有同时具有title属性且值为“my”以及href属性且值为“/test”的元素。

部分匹配属性值选择器(根据部分属性值选择)

如果属性能接受词列表(词之间用空格跟个),可以根据其中的任意一个词进行选择,如class属性。

p[class~="warning"] {color: red;},注意选择器中的'~'波浪号,这将导致选择器根据元素属性值中出现的一个用空格分隔的词来完成选择。

类型描述
[foo\&#124;="bar"]选择foo属性值以bar和一个英文破折号(U+002D)开头,或者值就是bar本身的所有元素(起始值属性选择器)
[foo~="bar"]选择foo属性值是包含bar这个词的一组词的所有元素
[foo^="bar"]选择foo属性值以"bar"开头的所有元素
[foo$="bar"]选择foo属性值以"bar"结尾的所有元素
[foo*="bar"]选择foo属性值中包含子串"bar"的所有元素

不区分大小写标识符

a[href$=".pdf" i],CSS Selectors Level 4指明,在属性选择器结束方括号前加上i,将在匹配属性值时不区分大小写(注意是属性值而不是属性名)。

4.根据文档结构选择

后代选择器

h1 em {color: red;}这个规则会把作为h1元素后代(不局限于子元素)em元素的文本变为red。

在一个后代选择其中,规则左边的选择器一端包括两个或多个用空格分隔的选择器。选择器之间的空格是一种结合符。每个空格结合符可以解释为......在......中找到、......作为......的一部分、......作为.....的后代;但是要求必须从右向左读选择器。因此h1 em可以解释为”作为h1元素后代的任何em元素“。

注意: 两个元素之间的层次间隔可以是无限的,不仅仅针对于父子关系。

选择子元素

如果指向选择一个元素的直接子元素而不是后代元素,可以使用子结合符,即大于号(>)。如h1 > strong {color: red;},这个规则中符号>两侧是否有空格是不影响选择结果的,这个规则将会匹配作为h1子元素的所有strong元素变为red。

选择相邻兄弟元素

相邻兄弟结合符,一个+号。h1 + p {margin-top: 0;}代表去除紧接在一个h1元素后出现的段落的上边距。一个结合符只能选择两个相邻兄弟中的第二个元素。注意:两个元素之间的文本不影响相邻兄弟元素选择器的功能。

一般兄弟组合器(General sibling combinator)

~ 组合器选择兄弟元素,即选择一个元素后面同属一个父元素的另一个元素,与相邻兄弟选择器的区别在于两个元素可以是相邻的也可以不是。
语法A ~ B
例子p ~ span 匹配同一父元素下,&lt;p&gt; 元素后的所有 &lt;span&gt; 元素。

7.伪类和伪元素

伪类选择器和伪元素选择器可以为文档中不一定具体存在的结构指定样式。

  • 伪类用来描述一个元素的特殊状态,一般使用:开头
  • 伪元素,表示页面中一些特殊的并不真实存在的元素(特殊的位置)

7.1伪类选择器

a:visited {coor: red;}会使已经访问过的锚颜色为红色。规则中的(:)冒号,是伪类或为元素的”名片“,所有伪类和为元素关键字前面都有一个冒号。

伪类名描述
:link指示作为超链接(有一个href属性)并指向一个未访问地址的所有锚。
:visited指示作为已访问地址超链接的所有锚。

结构伪类

  • 选择文档的根元素:root
  • 选择空元素:empty,可以选择没有任何子代的元素(包括文本节点和空白文本)。
  • 选择唯一子代:only-child。他所选择的元素是另一个元素的唯一子元素。p:only-of-type同类型是唯一的,即p元素只有一个,但可以有其它不同类型子元素(注意:only-of-type指的是同类型的元素而不是类或其他)。
  • 选择第一个和最后一个子代:first-child:last-child:first-of-type:last-of-type同类型第一个、或者最后一个。
  • 选择每第n个子元素:nth-child:nth-last-child(从后向前计算);选择每第n个某种同类元素:nth-of-type:nth-last-of-type

动态伪类

伪类名描述
:focus指示当前拥有输入焦点的元素,即可以接受键盘输入或者能以某种方式激活的元素
:hover指示鼠标指针停留在哪个元素上
:active指示被用户输入激活的元素。如鼠标指针停留在一个超链接上时,如果用户点击鼠标就会激活这个超链接,:active将指示这个超链接

注:动态伪类可用于任何元素。

伪类的顺序

伪类的顺序很重要,推荐 link-visited-focus-hover-active

UI状态伪类

:target伪类

与连接中的锚点有关如链接“www.baidu.com/search/#searchInput”,假如有一个这样的元素<div id="searchInput">,为div设置样式#searchInput:target {},只有链接中有 #searchInput时,样式才会生效。

否定伪类 :not

否定伪类:用以将符合条件的元素从选择器中去除

ul > li:not(:nth-child(3))将会选中ul的所有li子元素,除了第三个子元素。

注意:否定伪类不能嵌套使用,但可以串在一起使用。

根据语言选择

*:lang(fr) {font-style: italic}会使所有法语元素变成斜体。注意和属性选择符*[lang|"FR"]之间的区别,属性选择符选择的元素自身必须有lang属性才能匹配,而:lang伪类能匹配设定了语言的元素以及它的后代,因此这种情况下伪类选择器更加可靠。

结合伪类

a:hover; a:visited:hover这些选择器叫做组合伪类选择器。另外注意,这些顺序并不重要,但是要注意不要把互斥的伪类结合在一起使用。

7.2伪元素选择器

就像伪类为锚指定幻像类一样,伪元素能够在文档中插入假想的元素,从而得到某种效果。CSS2.1中定义了4个为元素:设置首字母样式、设置第一行样式、设置之前和之后的样式。

伪元素标准定义是双冒号开头的,如首字母::first-letter但是写单冒号也能用。

设置首字母样式

p::first-letter {color: red;}用于设置一个块级元素首字母的样式,仅对该首字母有效。

这会导致用户代理对一个假想的元素做出响应,这个假想元素包含每一个p元素的第一个字母。可能如下所示:

<p>
    <p:first-letter>T</h2:first-letter>
    his is an paragraph element.
</p>

In other words,<p:first-letter>是一个伪元素。这个伪元素并不会出现在文档源代码中,它是由用户代理动态构造的,用于向相应文本块应用:first-letter样式。

设置第一行的样式

p::first-line {color: red;}这个样式应用于每一段所显示的第一行文本,不论显示区域多大或多小,都是如此。

::first-letter和::first-line的限制

它们只能应用到块级元素上,不能应用到行内元素上。

注意伪元素应出现在该伪元素的选择器的最后面。

另外,::first-letter::first-line可以使用的CSS属性是有限制的,并不支持所有CSS属性。

::selection

设置鼠标选中的内容的样式。

设置之前和之后元素的样式

::before ::after。分别表示元素开始和结束的位置。

p::before {content: "%"; color: gray;}这个片段,将会显示 如下图所示的效果,在数字98前面显示了一个%号。

image-20211007143159260

又或者body::after {content: "the end.";}

1.字体族

介绍

font-family(字体族/字体系列)指具有同种类似特性的一系列字体的组合或者说是同种变形的组合,并不单单指的是一个字体,包含了一个或多个的font-face(字型)。
为了覆盖所有情况,CSS定义了5种通用字体族:

  1. Serif字体(衬线字体)字形宽度各异,且有上下短线。包括Times、Georgia、New Centure Schoolbook。
  2. Sans-serif字体(无衬线字体)字形宽度各异,但没有上下短线。包括Helvetica、Geneva、Verdana、Arial、Univers。
  3. Monospace字体(等宽字体)字形宽度相等,每个字符在横向上所占的空间是一样的,可能有也可能没有上下短线。这种字体通常用于显示编程代码或表格数据。包括Courier、Courier New、Andale Mono。
  4. Cursive字体(手写体、草书字体),这种字体在笔划末端有较大的花饰,且比衬线字体华丽。包括Zapf Chancery、Author、Comic Sans。
  5. Fantasy字体(奇幻字体、艺术体)难以用任何特征来定义,但可以确定无法将其划归为其它类。包括Western、Woodblock、Klingon。

使用通用字体族

如果希望使用一种Sans-serif字体,而且不关心是哪一种具体字体,那么可以使用声明body {font-family: sans-serif;}。这样用户代理就会从sans-serif字体族中选择一个字体(如Helvetica),并将其应用到body元素。因为有继承,还会将其应用到body元素中包含的所有元素,除非优先级更高的选择符覆盖了这个属性。

指定字体系列

如这样的声明h1 {font-family: Georgia;},如果用户代理上Georgia字体不可用的话, 那么用户代理将不能使用这个规则,而是使用用户代理的默认字体来进行显示。此时应该指定h1 {font-family: Georgia serif;},这样的话如果用户代理上没有Georgia字体,那么用户代理将会选择serif字体进行使用。
此外font-family还可以指定多个字体,用户代理将会按照顺序取匹配,最先匹配到的字体将会被使用。

引号的使用

如果一个字体出现了空格或者一些特殊符号如#、$**等,那么这个字体需要加上引号(根据实际情况可选单引号或者双引号)才能够Working Normally。
另外注意使用通用字体系列名(如serif)是不可以加引号的,加引号的话,用户代理会将其视为与此同名的特定字体而不是一个通用字体系列。也就是说与font-family中可用的关键字同名的字体名称必须放在引号里,这样才能与关键字区分开。

2.@font-face

作用是可在设计中使用自定义的字体。

@fant-face {
font-family: "name";
src: url("name.otf"), url("fonts/font.otf") format("opentype"), local("Helvetica");
}

font-familysrc是两个必填描述符。
@font-face中描述符font-familyfont-family属性中出现的字体族名是一样的。
src描述符以逗号分隔的列表用于提供后备字体文件,format用于为不带有常规扩展名的字体文件指定格式,以防用户代理不识别,local指定本地已安装的字体(可用于为本地安装的字体自定义名称,比如添加别名或者简称)。注意src的url适用同源策略,如果指向其它服务器,通常无法下载,除非服务器设置允许跨域访问。

支持的字体格式

格式
embedded-opentypeEOT(Embedded OpenType)
opentypeOTF(OopenTYpe)
svgSVG(Scalable Vector Graphic)
truetypeTTF(TrueType)
woffWOFF(Web Open Font Format)

其它字体描述符

描述符默认值说明
font-stylenormal区分常规、斜体和倾斜字型
font-weightnormal区分不同的自重(粗细)
font-stretchnormal区分不同的字符宽度
font-variantnormal区分众多字体变体(例如小号大写字母),在很多方面与CSS中的font-feature-setting很像
font-feature-settingsnormal直接访问OpenType的底层特性(例如启用连字,如将“>=”自动替换显示成“≥”)
unicode-rangeU+0-10FFFF定义或指定字体中可用的字符范围,或者指定只有某些字才应用字体

3.字体加粗(font-weight)

font-weight属性可以使用字体系列的不同的加粗级别。

normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | inherit
初始值normal

加粗如何起作用

有9级加粗度,如果一个字体内置了这些加粗级别,那么这些数字就直接映射到预定义的级别,100对应最细的字体变形,900对应最粗的字体变形。CSS规范指出,每个数对应一个加粗度,它至少与前一个数指定的加粗度相同。因此100、200、300可能都映射到同样的较细变形,800和900可能都映射同样的很粗的字体变形。只要一个关键字对应的变形不会比前一个关键字所对应的的变形更细就都是允许的。
一般地,这些数值都被定义为与某个常用变形名等价:400等价于normal;700对应于bold。在字体的变形中,以”Normal“、”Reguular“、”Roman“或”Book“等表示的可以分配给数字400,以”Medium“表示的可以分配给数字500。然而如果只有一个变体且以”Medium“表示,那么应该分配给400,而非500。

  1. 如果未指定500的加粗度,则其加粗度与400相同;
  2. 如果未指定300的加粗度,则为之选一种比400更细的字体,如果没有则等同于400。这种情况同样适用于200,100;
  3. 如果未指定600的加粗度,则为之选一种比500更粗的字体,如果没有则等同于500。这种情况同样适用于700,800,900。

    一种特定字体系列的假想加粗指定(仅供参考)

    字体指定的关键字指定的数字
    Zurich Light 100, 200, 300
    Zurich Regularnormal400
    Zurich Medium 500
    Zurich Boldbold600,700
    ZUrich Black 800
    Zurich UltraBlack 900

继承

font-weight是可以继承的

让字体更粗

如果把一个元素的加粗设置为bolder,则用户代理首先要计算其继承的父元素的加粗度,然后选一个数,它对应于比继承值更粗的加粗字体,而且在满足这条件的所有数中,选一个最小的。如果没有可选的字体,则选下一个更大的数值作为加粗度,如果已经是900的话,将会保持900不变。 例如(对于某种字体):

<style>
     /* 假设字体系列只有两种特定字体,regular和bold */
    p {font-weight: 100;} /* 表现是regular */
    p span {font-weight: 400;} /* 表现是regular */
    strong {font-weight: bolder;} /* bold 700 */
    strong b {font-weight: bolder;} /* bold 800 逐步增加直至900,但其实800与900和700视觉效果相同*/
</style>

<body>
    <p>pppp_text_
        <span>
            span_text_
            <strong>strong_text_<b>bbbb_text</b></strong>
        </span>
    </p>
</body>


执行结果如上图所示。

让字体更细

是类似的。只不过是反过来了,让字体更粗是加粗度上移,而让字体更细是加粗度下移。

font-weight描述符

在使用@font-face自定义字体时,font-weight描述符可以为字型指定font-weight属性支持的字重等级。font-weight描述符的值可以是font-weight属性可取的任何一个之,inherit除外。

4.字体大小(font-size)

font-size取值

可能的值描述
xx-small、x-small、small、medium、large、x-large、xx-large把字体的尺寸设置为不同的尺寸,从xx-small到xx-large。默认值:medium。
smaller把 font-size 设置为比父元素更小的尺寸。
larger把 font-size 设置为比父元素更大的尺寸。
length把 font-size 设置为一个固定的值。
%把 font-size 设置为基于父元素的一个百分比值。
inherit规定应该从父元素继承字体尺寸。

注意: font-size属性与看到的实际字体大小之间的具体关系右字体的设计者来确定。这种关系设置为字体本身中的一个em方框(也称为em框)。em方框只是如果没有额外行间距(line-height)设置字体时基线间的距离。某种字体的字符可能比默认的基线间距离要高,这种情况是存在的。所以,font-size作用是为给定字体的em框提供一个大小,而不能保证实际显示的字符就是这种大小。

绝对大小

绝对大小关键字包括:

  • xx-small
  • x-small
  • small
  • medium
  • large
  • x-large
  • xx-large

大多数浏览器将medium作为默认值。
这些值之间有着复杂的换算关系,总体来说以medium为基准,其它值在其基础上更大或更小。
medium在大多数浏览器上相当于16px,但并不总是这样,需根据具体的操作系统、浏览器、用户设置而确定。

相对大小

  • larger 相对于父元素的大小在绝对大小梯度上上移
  • smaller 相对于父元素的大小在绝对大小梯度上下移

增大或减少的比例与计算绝对大小时使用系数一致。

百分数和em

有时,1em等价于百分数值,即在确定字体大小时1em等于100%
对于font-size相对于父元素的font-size来计算
对于line-height相对于元素自身的font-size来计算

注意:计算中可能会得出具有小数的数值,浏览器一般在计算保存结果过程中是不会舍去小数的,但是在渲染的时候有些浏览器可能会对小数值进行四舍五入或舍去。

字号的继承

font-size继承的是计算值而不是百分数

关键字和等宽文本

当父元素的font-size是关键字时,如medium,而子元素设置了字体是等宽字体且设置font-size: 1em。此时父元素一般实际渲染的大小是16px,因为浏览器默认大小通常是16px,但是由于父元素的字号是关键字medium,关键字的实际大小可能会根据用户的字体设置或浏览器的默认设置而有所不同,所以父元素直接将关键词传递给了子元素,子元素因为设置的是等宽字体,等宽字体的默认大小可能和普通字体不一致,所以会导致子元素大小和父元素不同。
避免方法:

span {
  font-family: monospace, serif; // 等宽字体后面添加一个普通字体;
  font-size: 1em; // 这样继承过来的medium,浏览器去判断其大小时会按照默认字体大小而不是等宽字体的默认大小
}

font-size-adjust

不同的字体有不同的高宽比值,这会导致同样的字号下,不同字体的清晰程度不一样,为了避免这个问题,通过合理设置font-size-adjust,浏览器可以自动调整字号。
注意:这个属性只有firefox和safari支持,慎用。

5.风格和变形

有风格的字体 font-style

  • italic是一种单独的字体风格,斜体
  • oblique是倾斜,是指将正常normal的字体利用算法倾斜,一般来说italic的斜体会更好看,因为是被设计过的
  • 如果没有italic字体,但是存在oblique字体,那么会在需要前者的地方使用后者。但是如果存在italic字体,不存在oblique字体,用户代理可能只是计算竖直字体的一个倾斜版本来生成oblique字体(当然这并不绝对)。
  • 通常,italicoblique字体在web浏览器中查看上去可能一样
  • 以“Italic”、“Cursive”、“Kursiv“等标识的字型通常对应于italic关键字,而oblique往往分配以”Oblique“、”Slaned”、“Incline”等标识的字型

6.字体拉伸 font-stretch

当字体包含不同宽度字体时,设置font-stretch才会起作用。
注意:此属性兼容性很差!

7.字距调整 font-kerning

有些字体定义了字符之间相对位置的数据,font-kerning用来设置是否采用字体提供的字距信息。注意font-kerning和字符间距letter-sapcing是不同的。
注意:如果既要调整字距又要调整字符间距,应采用先调整字距再调整字符间距的顺序。

8.字体变形 font-variant

变形信息内嵌在字型中,包括各种连写、小号大写字母、小数的表示方式、数字之间的间距、零有无下划线等。
font-variant取值很多,常见的有normalsmall-caps。取值为small-caps指小型大写字母文本,注意此效果既非一般的大写字母,也不是小写字母。

9.font 属性

所有关于字体的属性(如font-stylefont-variantfont-weight)都可以放到font属性中写,作为一种简写方式。对于前三个值font-stylefont-variantfont-weight,他们的顺序可以是任意的,如果某个属性的值为normal,则可以忽略不写(甚至是三个都可以不写)。后两个值,font-sizefont-family必须以此顺序出现,而且其值必须有,如果少了其中某个属性,那么整个规则都将是无效的。

行高 line-height

font属性值,可以设置航高需要和font-size属性一起设置,如h2{font: bold italic 200%/1.2 Verdana, Helvetica, Arial, sans-serif};

font属性的简写注意事项

font属性所有简写的值都会被重置为其默认值,因此会覆盖其它地方声明已经声明过的font的 值(当然只能覆盖权重比较低的规则)。

最近碰到了一个问题,环境是vue2tsvue-class-component,写一个方法时发现声明的变量虽可以访问到但没有响应式,随后发现方法中的this不是VueComponent,在检查为什么this指向有问题时发现新写的方法用的是箭头函数,改成普通函数后就正常了,对于这块有点疑惑,因此整理记录关于this指向的情况。

1.不同情况下this的指向

方法/函数调用中的this

window.name = 'win'

function fun () {
  console.log(this.name)
}
let obj1 = {
  name: 'jack',
  fun
}

let obj2 = {
  name: 'bob',
}
obj2.fun = obj1.fun

fun()       // win
obj1.fun()  // jack
obj2.fun()  // bob

如上代码,直接执行fun函数,打印“win”,表明此时函数中的this指向的对象是window。而使用obj1.fun()调用fun函数,打印“jack”,此时this指向的对象是obj1obj2.fun()的执行结果为“bob”,表明obj2对象的fun函数虽然是由obj1fun函数赋值得到,但由于是obj2对象调用的,所以this指向obj2,打印了obj2name属性。

window.name = 'win'

let fun = () => {
  console.log(this.name)
}
let obj = {
  name: 'jack',
  funA
}

obj.fun()  // win

如上代码,当fun函数为箭头函数时,因为定义funB时所在的作用域是在全局作用域window下,所以funB内的thiswindow,因此obj.fun()的打印结果是”win“而非”jack“。

事件回调中的this

行内绑定

<input type="button" value="按钮" onclick="handleClick()">
<script>
  function handleClick(){
    console.log(this) // Window {}
  }
</script>

直接将函数执行语句"handleClick()"作为属性绑定到dom标签上,当click事件触发时,语句可视为是在全局作用域下执行的,因此this指向window

<input type="button" value="按钮" onclick="handleClick()">
<script>
  let handleClick = () => {
    console.log(this) // Window {}
  }
</script>

当回调函数是箭头函数时,函数内this也为window

js动态绑定

<input type="button" value="按钮" id="btn">
<script>
    btn.onclick = function() {
        console.log(this)  // input
    }
</script>

通过js赋值函数给dom对象的onclick属性的方式绑定回调函数,回调函数是被dom对象所调用的,因此this指向dom对象。

<input type="button" value="按钮" id="btn">
<script>
    btn.onclick = function() {
        console.log(this)  // Window {}
    }
</script>

当绑定的回调函数是箭头函数时,函数内this指向window

事件监听

<input type="button" value="按钮" id="btn">
<script>
    btn.addEventListener('click', function() {
        console.log(this)
    })
</script>

通过事件监听方法绑定的回调函数,this同样指向了dom对象。

<input type="button" value="按钮" id="btn">
<script>
  btn.addEventListener('click', () => {
    console.log(this)
  })
</script>

当绑定的回调函数是箭头函数时,函数内this指向window

构造函数中的this

function Obj() {
    console.log(this)
}

let o = new Obj() // Obj {}
console.log(this) // Window {}

如上代码第五行执行new Obj()语句时,打印了“Obj {}”,这是因为在new关键字会首先创建一个空对象,并将控对象的隐式原型指向构造函数的显式原型,最后将构造函数中的this指向了这个新创建的对象,因此在构造函数中,this会指向一个新创建的对象,同时它也是new语句的返回值。
另外箭头函数由于没有自己的this,是不可以作为构造函数的。

call、apply、bind

callapplybindjs中三个很神奇的函数,使用它们可以改变函数体内部this的指向。

let func = function() {
  console.log(this.name, [...arguments].join(','))
}

let obj1 = {
  name: 'obj1',
  func
}
let obj2 = {
  name: 'obj2',
}

obj1.func(0)                      // obj1 0
obj1.func.call(obj2, 1, 2, 3)     // obj2 1,2,3
obj1.func.apply(obj2, [4, 5, 6])  // obj2 4,5,6

如上代码,callapply方法都可以改变函数内部this的指向,区别在于call方法第一个参数是所要改变的this(上下文),其它参数是func函数的参数,apply方法的第二个参数是fun函数参数的数组。

let func = function() {
  console.log(this.name, [...arguments].join(','))
}

let obj1 = {
  name: 'obj1',
  func
}

let obj2 = {
  name: 'obj2',
}

let newFunc = obj1.func.bind(obj2)
newFunc(1, 2, 3)  // obj2 1,2,3

bind方法可以返回一个新的函数,这个新函数的thisbind的参数,这样无论新函数在哪里被调用,其内部的this都是不会变的。

class中this的指向

class Person {
  constructor(name){
    this.name = name
  }

  getName() {
    console.log(this.name)
  }
}

let p = new Person("tom")
p.getName()  // tom

class中所定义的普通函数会被添加到类的原型上,调用方法时,调用方是类的实例对象,因此方法中的this指向类的实例对象。

class Person {
  constructor(name){
    this.name = name
  }

  getName = () => {
    console.log(this.name)
  }
}

let p = new Person("tom")
p.getName()  // tom

class中所定义的箭头函数会在constructor中自动添加到类的实例对象上,调用方法时,箭头函数的this也是指向类的实例对象的。

箭头函数

箭头函数体内的this对象,是定义该函数时所在的作用域所指向的对象,而不是使用时所在作用域指向的对象。

window.name = 'win'

let funA = function() {
  console.log(this.name)
}
let funB = () => {
  console.log(this.name)
}
let obj1 = {
  name: 'jack',
  funA
}

let obj2 = {
  name: 'bob',
  funB
}

obj1.funA()  // jack
obj2.funB()  // win

如上代码,obj2.funA()obj2.funB()打印结果分别是“jack”和“win”,说明箭头函数funB内的this取的是window,因为定义funB时所在的作用域是在全局作用域下,所以funB内的thiswindow

2.问题分析

将开篇所提到的问题进行简化:

<template>

</template>

<script lang="ts">
  import { Vue, Component } from "vue-property-decorator";

  @Component({})
    export default class basicInfoTab extends Vue {
      public name = 'aha'

      public click() {
        console.log(this);
        console.log(this.name);
      }

      public click1 = () => {
        console.log(this);
        console.log(this.name);
      }

      created() {
        this.click()   // VueComponent{...}, aha
        this.click1()  // basicInfoTab{...}, aha
      }
    }
</script>

<style scoped lang="scss">

</style>

如上的Vue代码中,在组件成功挂载后的created回调函数中,分别调用了普通函数click和箭头函数click1打印thisthis.name,他们所打印出来的this.name是一样的,但普通函数中的this是Vue组件的实例对象,而箭头函数中的thisbasicInfoTab类的实例对象。
按照上面classthis的指向所述的那样,普通函数和箭头函数中的this应该是一样的才对。要搞清楚原因就需要去探究basicInfoTab类成为组件类,以及实例化组件类的过程,通过查询资料了解到大概有以下步骤:

  1. 实例化类Vue使用“new”关键字和类的构造函数创建类的新实例。
  2. 初始化组件Vue初始化新创建的类实例,设置其数据(data)、计算属性(computed)、方法(methods)等。
  3. 创建组件实例Vue将初始化后的类实例转化为Vue组件实例。在这个过程中,Vue会添加一些特殊的属性和方法,例如生命周期钩子(如createdmounted等)。
  4. 挂载组件:最后,Vue将组件实例挂载到DOM元素上。在这个过程中,Vue会创建一个新的Vue实例,并将模板编译成虚拟DOM,然后将虚拟DOM渲染成真实DOM,并插入到挂载的元素中。

由上可知,VueComponent组件实例对象是在basicInfoTab类实例对象的基础上初始化的,click是普通函数,因此他的thisVueComponent组件实例对象,由于click1是箭头函数,所以click1内部this会一直指向basicInfoTab类的实例对象。即使是VueComponent组件实例对象调用了click1,其内部this仍然还是指向basicInfoTab类实例对象。
将此情况换成一个更简单的例子:

class Person {
  a() {
    console.log(this)
  }
  b = () => {
    console.log(this)
  }
}

let p = new Person()

class Dog {
  a = p.a
  b = p.b
}


let d = new Dog()
d.a() // Dog {...}
d.b() // Person {...}

上面代码中,Dog类中的属性ab都是从Person类的实例对象p赋值而来的,Dog类的实例对象d分别调用a方法和b方法,作为普通函数的a方法中this指向自身,而箭头函数根据其定义依然指向着Person类实例对象p。
因此可以对开篇遇到的问题进行解释,因为箭头函数中的thisbasicInfoTab类的实例对象,所读取到的属性没有经过Vue的处理,所以不具备响应式。

3.总结

  1. 方法调用模式下,this总是指向调用的对象,this的指向与所在方法的调用位置有关,而与方法的声明位置无关(箭头函数例外);
  2. 在事件回调中,行内绑定的事件不论是普通函数还是箭头函数,它们的this都指向window;动态绑定及事件监听方式绑定的普通函数,this指向dom对象,绑定的箭头函数则指向window
  3. 构造函数调用模式下,this指向被构造的对象;
  4. applycallbind 调用模式下,this 指向函数的第一个参数;
  5. class中,不论是普通函数还是箭头函数内的this都指向实例对象;
  6. 箭头函数体内的this和实际调用函数的对象无关,是定义该函数时所在的作用域所指向的对象,而不是使用时所在作用域指向的对象。

参考内容:
https://zhuanlan.zhihu.com/p/42145138
https://juejin.cn/post/6973654160872833038
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this#%E5%B0%9D%E8%AF%95%E4%B8%80%E4%B8%8B
https://juejin.cn/post/6981251280236707853#heading-3
https://juejin.cn/post/7035257186565488670
https://juejin.cn/post/6954692013489029151#heading-7
https://juejin.cn/post/6930896449878097927#heading-14
https://www.zhihu.com/tardis/zm/art/57204184?source_id=1003
https://blog.csdn.net/yiyueqinghui/article/details/106792408
https://juejin.cn/post/6844903906917482503

项目包括一个loading组件以及Swiper控制的内容组件Swiper的每个Slide也是一个组件,通过Swiper滑动控制切换不同的显示组件,DOM结构如下所示:

<template>
  <div class="main">
    <Loading v-show="showLoading" class="loading" :loadingText="loadingText"></Loading>

    <swiper v-show="!showLoading">
      <swiper-slide v-for="(item, index) in componentQueue">
        <component
          ref="components"
          :is="item"
          direction="vertical"
          />
      </swiper-slide>
    </swiper>
</template>

Swiper不能滑动

对于IOS12及以下系统,Swiper11版本无法通过触摸手势触发滑动操作,调试发现直接调用slideNextslidePre方法是可以切换下一项和上一项的。由于手动监听DOM上的touchstart、和touchmove事件是可以正常触发的,因此解决方法是通过监听touchstart、和touchmove事件根据手指移动情况判断上滑以及下滑操作,并分别调用slideNextslidePre方法实现滑动切换。

Swiper不能正常显示

在项目开发时刚开始使用的是v-if控制loading页内容页容器的显隐,但这样内容页DOM结构并不会生成,loading过程中内容页的图片资源也不会提前加载,loading页无法完成“预加载”资源的功能。更换为v-show之后虽然没有这个问题,但由于DOM没正常显示引起Swiper没有正常初始化,从而导致轮博图出现不能正常显示的问题。Swiper提供了update方法,官方文档称“应该在手动添加/删除幻灯片、隐藏/显示幻灯片或使用Swiper进行任何自定义DOM修改后调用该方法”。因此通过在初始化后添加了一个定时器调用update方法解决。
然而在IOS12及以下版本的手机上这个问题又复现了,推测是定时器的延迟时间不合适导致的。最终解决方案是设置observer属性为true,这样Swiper将会自己启动检测,每当显示/隐藏、添加/删除子元素时都会自动进行更新。

IOS12打开页面报错Unexpected token '.'

问题发现在IOS12及以下版本。这个错误是在loading页加载时抛出的,根据log信息并不能直接定位到具体报错代码行列,只能定位至问题的组件。因此只能逐行代码进行查错,先把<template><script>注释掉,再逐步放开里面的代码,最终发现是使用可选链运算符?的两行代码导致的报错。因此确定是项目使用的vue3+vite的技术栈不兼容导致的,未对可选链?运算符进行转换,低版本浏览器不能识别而报错。
解决方法:

  • 使用其他写法替换可选链?运算符
  • 安装模块@babel/plugin-proposal-optional-chaining,并配置babel.config.ts

    module.exports = {
        presets: ['@vue/app'],
        plugins: ["@babel/plugin-proposal-optional-chaining"]
    }

html2canvas转图片触发DOM动画

项目中有一个页面进入时有约3s的入场动画,同时此页面还有保存图片的需求。保存图片使用的是html2canvas插件,这样就产生了一个问题,html2canvas在绘制DOM时会重新触发页面的入场动画,最终截取到的图片是动画过程中的一帧,并不是最终的样式,因此需要在保存图片时禁止动画播放,并且在关闭保存动画组件后还不能触发及影响页面动画。
前期采用的方案是临时加了一个css类:

animation-duration: 0.001s !important;
animation-delay: 0.001s !important;

设置动画的过渡时间和延迟时间为0.001s,目的是能够快速执行动画。
然而在IOS12及以下手机上这两个属性并不能取得预期效果。
最终方法是使用以下属性,直接取消动画:

opacity: 1 !important;  // DOM元素默认opacity是0,使delay动画执行前隐藏元素
animation: none;

图片旋转动画不显示

需求是需要在点击图片时,图片会开始旋转,再次点击,图片停止旋转。
原本的方法是通过设置animation-play-state属性来控制动画的暂停和播放,并且能够保留暂停时动画的状态。

.music-play {
    animation-play-state: running;
    -webkit-animation-play-state: running;
}
.music-pause {
     animation-play-state: paused;
     -webkit-animation-play-state: paused;
}

但是在IOS11系统中,这个属性不能正常工作。
最终解决方案是:除IOS11及以下外的其他系统仍使用animation-play-state属性,IOS11及一下系统使用如下样式控制图片旋转:

.music-play-compatible {
    animation: spin 2s linear infinite;
}

.music-pause-compatible {
    animation: none !important;
}

通过直接设置animationnone达到暂停动画的目的,不足之处是再次播放动画时,不会保留暂停时的状态。

资源缓存策略

项目中有大量的图片资源和一个音乐资源,为了不让用户在资源加载完成之前就进入系统中,因此在loading页需要对资源加载情况进行判断,在满足条件之后才允许用户进入系统。本项目对于Image对象使用complete属性判断图片资源是否加载完成,对于音乐资源则通过audio对象的readyState属性判断音乐资源的加载情况,readyState有以下几种状态:

  • 0 = HAVE_NOTHING - 音频/视频尚未初始化
  • 1 = HAVE_METADATA - 音频/视频是就绪的,但尚未加载
  • 2 = HAVE_CURRENT_DATA - 音频/视频是当前的,但未完全加载
  • 3 = HAVE_FUTURE_DATA - 音频/视频的未来帧已加载,但尚未加载完
  • 4 = HAVE_ENOUGH_DATA - 音频/视频已加载完毕

大多数情况下,当readyState大于等于3时既可以不影响音乐播放也不会造成太久的等待时间,有良好的用户体验。这种方式在Chrome浏览器以及测试范围内的安卓手机都可以正常工作,但是在IOS系统(包括最新版)中出现了readyState一直为1的情况。
这是因为Chrome浏览器以及安卓手机在loading页时,虽然音乐没有播放,但是可以正常加载资源,但IOS上的策略是必须要触发音乐播放的事件后才开始加载资源。
最终处理方法是:

  • 进入loading页后,js触发音乐静音播放,然后再暂停播放并复位播放进度,从而触发音乐资源加载且不影响正常的播放
  • 检测资源加载是否完成,做一个兜底,记录检测到图片资源加载完成但音乐资源未加载完成后的事件,若超过所设置的超时时间,便认为第一条策略未正常执行,音乐资源未触发加载,跳过音乐资源的加载状态判断

IOS端Height属性异常

此问题主要出现在IOS上,本意是设置页面高度为100%使页面高度撑满屏幕,但IOS上的表现是会溢出可视区域的,因为IOS100%高度会把工具栏的高度也算上了。另外使用vh也存在同样的问题。在查资料过程中发现了一个叫做DVH(Dynamic Viewport Unit)动态适口单位的属性,可以完美解决百分数以及vh的缺点,但是dvh是新单位,兼容性不好。最终采用了js获取可视区域高度设置为页面高度的方法:

let element = document.querySelector('#app');
// 设置元素的高度为视口的高度
if (element) {
    element.style.height = window.innerHeight + 'px'
}