2024年6月

1.边框重叠问题

如图,两个input框纵向紧密排列,相邻的边框靠在一起但没有重叠,视觉效果就是边框不对称,中间的边框看起来很粗,很奇怪。

如果直接隐藏上面input的下边框或者下面input的上边框虽然默认状态下是可以解决问题的,但当input框处于:hover状态时,边框会高亮,隐藏边框的地方高亮就会缺失,看起来更奇怪了。为了避免“露馅”,一个很巧妙的方法是通过设置box-shadow来模拟边框的样式。

input {
    border-top: 0;

    &:hover,
    &:focus {
        box-shadow: 0 -1px 0 0 #0364ff;
    }
}

实现效果如下图:

2.vue文件引入外部css文件的作用域问题

vue文件中的script标签以及style标签都可引入外部css文件:

<script setup lang="ts">
    import './css1.scss'
</script>

<style lang="scss" src="./css1.scss"></style>

如上代码,import方式导入的话默认是全局引入的,如果只想组件在内部生效可以使用CSS Modules,使用起来比较麻烦。

style标签默认也是全局引入的,如果只想组件在内部生效可以添加scoped关键字,相对import的方式更加方便。

3.el-table横线问题

使用el-table组件并且开启固定列时,固定列地步可能会出现多一条异常横线的问题,可通过调用表格组件的doLayout方法或者添加以下样式解决:

<style>
  .el-table__fixed-right {
    height: 100% !important;
 }
</style>

4.el-table滚动条位置重置(丢失)问题

如上图的场景,在一个抽屉中会显示多个“子页面”,包括列表页、编辑页、查看页。默认显示列表页,通过v-show控制显示不同页面。列表页中使用了el-table组件显示一个数据列表,当表格中数据较多时,会显示竖向滚动条,此时如果点击显示其他页面然后再切回列表页时,el-table组件会重新渲染,滚动条位置会重置到顶部,原有位置丢失。一个解决方法是,在切换其他页面前获取当前滚动条位置,然后切换回列表页时再恢复滚动条位置:

  // 获取滚动条位置
  public getListScrollHeight() {
    let bodyWrapper = table.querySelector('.el-table__body-wrapper')
    this.saveListScrollHeight = bodyWrapper ? bodyWrapper.scrollTop || 0
  }

  // 恢复滚动条位置
  public recoverListScrollHeight() {
    table.doLayout()

    let bodyWrapper = table.$el.querySelector('.el-table__body-wrapper')
    if (bodyWrapper) {
      this.$nextTick(() => {
        bodyWrapper.scrollTop = this.saveListScrollHeight
      })
    
      table.doLayout()
    }
  }

另外需注意对于表格数据tableData,如果直接赋值新的数组数据或动态添加/删除数据,滚动条位置都不会丢失,但如果先赋空数组[],再赋值新数据,滚动条位置也会丢失。

5.js中的事件传播

js中事件传播分为3个阶段

  • 捕获阶段(Capture Phase):事件从Window对象向下传递到目标元素(触发事件的元素)
  • 目标阶段(Target Phase):事件到达目标元素
  • 冒泡阶段(Bubbling Phase):事件从目标元素开始向上冒泡,直到最外层的DOM元素(Window对象)。

addEventListener方法的第三个参数默认为false,指事件会在冒泡阶段触发,若设置为true,事件将会在捕获阶段触发。因此对于这样一个场景,如需要在全局监听click事件并调用事件处理函数handleClick,如果用默认方式添加事件的话,handleClick会在click事件的冒泡阶段调用,但系统中其它元素也可能会绑定click事件,甚至还可能会用stopPropagation方法阻止事件的冒泡,这会导致全局绑定的handleClick不会被触发。这种场景下可以设置全局添加的click事件在捕获阶段触发,从而避免因调用stopPropagation导致阻止事件冒泡。

6.vue指令相互覆盖问题

有两个vue2项目a, b;a项目被构建为sdk,b项目作为一个业务项目引入了a项目sdk。碰到了这样一个问题:a项目中用了一个指令v-move,并且自己有实现,b项目中有一个同名的指令,在实际应用过程中发现,b项目中的组件触发v-move指令时,有时候触发的是a中的代码,有时候触发的是b中的代码。

造成这个问题的原因主要是两个项目中的指令都是全局注册的,而两个项目共用的是同一个全局Vue,因此在指令注册时可能会互相覆盖。

为避免这个问题可以采取以下方法:

  • 指令改为局部注册
  • 尽可能避免指令重名

7.vue2中this的指向

之前写过一篇博客介绍了关于vue2+vue-property-decorator项目中组件this的指向问题,其中讲到VueComponent组件实例对象是在类实例对象的基础上初始化的,所以针对多个组件实例,VueComponent组件实例对象肯定是不同的,而类实例对象理应是相同的。

但是如上图所示,对于两个组件实例,List类实例对象并不相同,和以前的推断不太一样,推测是vue-property-decorator的缘故,以后有时间再深入研究一下。

web核心性能指标(Core Web Vitals)

Web Vitals是谷歌开发的一系列指标,用于从不同方面去衡量网页的性能。而Core Web Vitals是Web Vitals的一个子集,包含了3个需要特别关注的指标,它们在谷歌搜索算法中扮演着一定的角色,并能够改善网站在谷歌搜索中的权重。

LCP(Largest Contentful Paint,最大内容绘制)

概念

LCP用于用于测量用户感知到的页面加载速度,指从页面开始加载到页面中最大的内容元素被渲染到屏幕上的时间点。这个最大的内容元素可能是图片、视频、SVG、含有背景图的元素、包含文本节点或其它含有文本子元素的块级元素。需要注意的是,随着页面的不断加载,页面中的最大内容元素可能会发生变化,因为更大的内容元素可能还未加载,因此LCP在页面加载过程中也会不断更新直至页面加载完毕。

LCP包含从上一个网页开始的所有卸载时间、连接设置时间、重定向时间和首字节时间(TTFB)。

测量方式

通过PerformanceObserver监测LCP:

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('LCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'largest-contentful-paint', buffered: true});

根据LCP的定义,随着网页的加载以上代码会获取到多个startTime值,而所获取到的最后一个值便是页面的LCP值。

通过web-vitals监测LCP:

import {onLCP} from 'web-vitals';

// Measure and log LCP as soon as it's available.
onLCP(console.log);

为获取更精确的LCP,推荐使用第二种方式,详见Web.dev

指标分数

为提供良好的用户体验,网站应努力将Largest Contentful Paint控制在2.5s以内。为确保大多数用户都达到此目标,最好衡量一下网页加载的第 75 个百分位(按移动设备和桌面设备细分)。

INP(Interaction to Next Paint,下一次绘制的交互)

概念

INP衡量页面在整个生命周期中的交互能力(响应能力),通过观察用户在访问网页期间发生的所有点击、点按和键盘互动的延迟时间,评估网页对用户互动的总体响应情况。INP的计算方法是观察用户与网页进行的所有交互并记录延迟时间,最终INP值是观测到的最长交互时间,离群值会被忽略。对于大多数网站来说,延迟时间最长的交互就是INP,但对于包含大量交互的页面来说可能会不准确,因此INP在计算过程中会忽略达到50次交互中的延迟最长的一次交互。

根据INP的定义,只会监测以下类型的交互:

  • 使用鼠标点击。
  • 点按带有触摸屏的设备。
  • 按实体键盘或屏幕键盘上的某个键。

如果页面一直未接收到以上类型的交互,那么页面将不会返回INP值。

2024年3月,INP取代了FID成为Core Web Vitals。

测量方式

通过web-vitals监测INP:

import {onINP} from 'web-vitals';

onINP(({name, value, rating}) => {
  console.log(name);    // 'INP'
  console.log(value);   // 512
  console.log(rating);  // 'poor'
});

指标分数

为提供良好的用户体验,网站应努力将Largest Contentful Paint控制在200ms以内,为确保大多数用户都达到此目标,最好衡量一下网页加载实际记录的第 75 个百分位(按移动设备和桌面设备细分):

CLS(Cumulative Layout Shift,累计布局偏移)

概念

布局偏移指页面中的元素所发生的意外移动,这主要发生在当资源以异步方式加载或DOM元素被动态添加到页面的现有内容中时。比如页面上方有一个广告,当用户刚进入页面时还未获取到广告数据,在获取到广告数据后,广告横幅被加载到页面顶部,此时页面中其它的内容都向下移动了广告横幅的高度,这就叫布局偏移。

CLS 用于衡量再页面的整个生命周期中发生的所有意外布局偏移的得分之和。布局的移动可能发生在可见元素从一帧到下一帧改变的任何时候,并且仅当现有元素更改其起始位置时才会发生布局偏移。如果将新元素添加到DOM或现有元素更改了大小,只要没有导致其他可见元素更改其起始位置,就不应将其视为布局偏移。

测量方式

布局偏移计算方式:

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('Layout shift:', entry);
  }
}).observe({type: 'layout-shift', buffered: true});

每次布局偏移时,都会打印出布局偏移的分数,通过累加每次的分数可获取到累计布局偏移CLS。

累计布局偏移计算也可使用web-vitals。

import {onCLS} from 'web-vitals';

onCLS(console.log);

为获取更精确的CLS,推荐使用第二种方式,详见Web.dev

指标分数

为提供良好的用户体验,网站应努力使CLS得分不超过 0.1。为确保大多数用户都达到此目标,最好衡量一下网页加载的第 75 个百分位(按移动设备和桌面设备细分)。

其他要衡量的重要指标

FCP(First Contentful Paint,首次内容绘制)

概念

FCP用来表示用户感知的页面加载速度,它标记了网页加载时间轴中用户可以看到屏幕上任何内容的一个时间点,衡量了从用户导航到相应网页到该网页的任何内容首次呈现在屏幕上所用的时间,内容包括文本、图片(背景图片)、SVG元素、非白色canvas元素。FCP与LCP的区别是FCP注重第一个出现的内容,而LCP则注重所出现的最大一个内容。

FCP同样包括从上一个网页开始的所有卸载时间、连接设置时间、重定向时间以及首字节时间(TTFB)。

测量方式

通过PerformanceObserver监测FCP:

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
    console.log('FCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'paint', buffered: true});

通过web-vitals监测LCP:

import {onFCP} from 'web-vitals';

// Measure and log FCP as soon as it's available.
onFCP(console.log);

为获取更精确的LCP,推荐使用第二种方式,详见Web.dev

指标分数

为提供良好的用户体验,网站应努力使FCP不超过1.8s。为确保您的大多数用户都能达到此目标,建议您衡量第 75 个百分位的网页加载情况(按移动设备和桌面设备细分)。

FID(First Input Delay,首次输入延迟)

概念

当页面还未完全加载完成时,可能因为浏览器主线程还在忙于执行其它操作导致不能及时响应用户的交互。因此使用FID评估用户与页面交互时的响应速度,它衡量了从用户首次与网页交互(如点击链接、按钮等)到浏览器实际能够开始处理事件处理脚本以响应用户交互的时间。FID通常发生在FCP和TTI之间,此时页面已渲染部分内容,但未完全加载完毕,尚不能可靠地互动。

FID本身不会衡量事件处理总时长,也不会衡量浏览器在运行事件处理脚本后更新界面所用的时间,FID衡量的是收到输入事件与主线程下次空闲之间的时间,也就是说即使没有注册与之对应的事件监听器,系统也会衡量 FID。

测量方法

通过PerformanceObserver监测FID:

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    const delay = entry.processingStart - entry.startTime;
    console.log('FID candidate:', delay, entry);
  }
}).observe({type: 'first-input', buffered: true});

以上代码在大多数情况下会输出正确的FID值,但也有特例具体详见Web.dev

通过web-vitals监测LCP:

import {onFID} from 'web-vitals';

// Measure and log FID as soon as it's available.
onFID(console.log);

为获取更精确的LCP,推荐使用第二种方式,详见Web.dev

指标分数

为了提供良好的用户体验,网站应努力将FID控制在100毫秒以内。为确保大多数用户都达到此目标,最好衡量一下网页加载的第75个百分位(按移动设备和桌面设备细分)。

TBT(Total Blocking Time, 总阻塞时间)

概念

TBT指在FCP之后主线程被阻塞并且无法响应用户输入的总时间。

运行的超过50毫秒的任务被称为长任务,主线程在运行长任务时被视为处于“阻塞状态”,如果用户在长任务期间与页面进行交互,浏览器必须等待长任务结束后才能做出响应。

测量方式

由于用户的交互可能会以各种方式影响页面的TBT指标,因此更推荐在实验室场景进行测量,可通过Chrome DevTools中的Performance或者LightHouse测量出TBT。

指标分数

为提供良好的用户体验,网站应努力将TBT控制在200ms以内。

TTI(Time to Interactive,可交互时间)

概念

TTI测量从页面开始加载到其主要子资源加载完成的时间,此时页面能够快速可靠地响应交互。

具体定义:

  1. 从FCP开始。
  2. 向前搜索至少五秒的安静窗口,其中安静窗口定义为:没有长任务并且不超过两个正在进行的网络get请求。
  3. 向后搜索安静窗口之前的最后一个长任务,如果没有找到长任务,则在FCP处停止。
  4. TTI 是安静窗口之前最后一个长任务的结束时间(如果未发现长任务,则与FCP相同)。

注意:TTI对异常网络请求和长任务过于敏感,导致该指标高度不稳定。TTI已从LightHose 10中删除。

测量方式

由于用户的交互可能会以各种方式影响页面的TTI指标,因此更推荐在实验室场景进行测量,可通过Chrome DevTools中的Performance或者LightHouse测量出TTI。

指标分数

为提供良好的用户体验,网站应努力将TTI控制在5s以内。

TTFB(Time To First Byte,首字节时间)

概念

TTFB指浏览器发起请求到接收到服务器返回的第一个字节所经过的时间。TTFB包括以下阶段:

  • 重定向时间
  • Server worker启动时间(如果有)
  • DNS查询时间
  • 连接(TCP)和TLS协商时间
  • 发起HTTP请求到接受到服务器响应的一个字节

测量方式

通过PerformanceObserver监测FCP:

new PerformanceObserver((entryList) => {
  const [pageNav] = entryList.getEntriesByType('navigation');

  console.log(`TTFB: ${pageNav.responseStart}`);
}).observe({
  type: 'navigation',
  buffered: true
});

通过web-vitals监测LCP:

import {onTTFB} from 'web-vitals';

// Measure and log TTFB as soon as it's available.
onTTFB(console.log);

指标分数

为提供良好的用户体验,网站应努力将TTFB控制在800ms以内。

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,并且认为出现在创作人员编写的样式表的开头。表现提示将被创作人员编写的样式或渎职提供的样式覆盖,但是不会被用户代理的默认样式覆盖。