1.在github上提issue通常第三方库都有自己的github仓库地址或者官网，如果有github仓库的话就可以在github上提issue，要注意的是一定要按照仓库作者规定的格式提issue，写明bug复现过程，否则issue可能会被忽略。这种方法理论上是最好的，但是也有一些缺点：仓库作者可能不维护了，那么issue将不会被解决仓库作者不认同观点，可能会拒绝所提的issue仓库作者接受所提的issue，但认为其优先级低，可能需要很长时间才会解决2.fork代码自己修改如果急需解决bug的话，可以尝试自己把原仓库fork下来进行修改，并且将代码发布到npm仓库中，供项目使用。这种方法的缺点有：增加维护成本，需要和原仓库保持同步，否则会丢失更新后期可能会和原仓库出现难以合并的分歧注意合法性问题，可能需要原作者授权综上，如果原仓库一直在更新的话，需要考虑原仓库和自己仓库的代码同步问题，因此这种方法更适合在原仓库不再维护的情况下使用。此外还可以给仓库作者提PR，这样就不用担心后续维护的问题了，也是一种比较好的方式，即满足了自己的需求还为开源库做了贡献，但前提是作者接受了PR。3.使用pack-package打补丁在不修改源代码的情况下，也可以通过直接修改其打包后的代码来实现所需功能，步骤如下：安装pack-packagenpm i patch-package修改代码在node_modules中找到对应模块的位置，以element-plus为例，假设要对Button组件的点击事件进行修改，找到Button组件以及代码中相应位置，如图所示，添加代码console.log('click')。代码修改后的预期结果是所有用了Button组件的地方，在点击按钮时都会在控制台输出click。验证代码模版中使用el-button组件: <el-button>Default</el-button> 如下图所示代码中并没有主动监听el-button的click事件，但是每次点击时都会在控制台输出"click"注：如果使用了vite，要注意在重新运行前清除缓存，否则可能会出现代码不生效的问题。保存修改的代码为了便于代码的分发，需要把在element-ui中修改的代码保存下来，通过执行命令npx patch-package package-name会生成一个.patch文件，里面包含了修改前后的diff信息。其中内容为：diff --git a/node_modules/element-plus/es/components/button/src/use-button.mjs b/node_modules/element-plus/es/components/button/src/use-button.mjs index 884214f..59c9c6c 100644 --- a/node_modules/element-plus/es/components/button/src/use-button.mjs +++ b/node_modules/element-plus/es/components/button/src/use-button.mjs @@ -52,6 +52,7 @@ const useButton = (props, emit) => { return false; }); const handleClick = (evt) => { + console.log('click') if (_disabled.value || props.loading) { evt.stopPropagation(); return;至此，只要执行patch-package命令即可将patch中的代码并入到node_modules对应文件中。为简化此过程，通常在package.json的scripts对象中加入"postinstall": "patch-package"配置，这样每当执行npm install之后，都会自动执行patch-package命令。适用场景和第二种方法的适用场景类似，但更加适用于修改一些较小的bug，因为node_modules中通常是打包后的代码，特别是经过混淆和压缩的代码，修改难度相对较高。另外打补丁的方法也不需要去另外维护仓库。需要注意的是用这种方法修改，需要锁定包的版本，如果后期需要升级包的话，补丁也需要看实际情况去做相应修改。4.pnpm patch打补丁和pack-package类似。使用命令pnpm patch package-name，还是以element plus为例，命令为pnpm patch element-plus，执行命令后会将对应npm包复制到一个叫.pnpm_patches的目录中，如下图所示。然后就可以在这个新生成的目录中编辑代码，依然在相同位置添加代码console.log('click')。编辑完成后，执行上图中绿色的提交修改的代码。此时package.json中自动多了代码:"pnpm": { "patchedDependencies": { "element-plus": "patches/element-plus.patch" } }同时目录中也多了patches文件夹，以及element-plus.patch这个补丁文件，里面也是修改前后的diff信息。最后删除node_modules文件夹，重新安装依赖，查看修改后的代码是否被添加进来。这种方法的适用场景和使用pack-package是类似的。5.动态修改代码(Vue)由于在vue中通过ref可以拿到所使用组件的实例对象，所以理论上我们可以直接修改这个实例对象来达到一些目的。以element-plus的el-cascader级联组件为例，查看实例对象：里面有一个cascaderPanelRef属性，el-cascader是基于cascaderPanel的，它包括了级连组件的核心功能。cascaderPanelRef中有：注意到里面有一个handleKeyDown的方法，这个方法的详细信息并没有在官方文档中所体现，根据其名称，猜测会对键盘方向键的事件进行处理。尝试对其进行修改：onMounted(() => { let value = refCascade.value nextTick(() => { let cascaderPanelRef = value.cascaderPanelRef let old = cascaderPanelRef.handleKeyDown cascaderPanelRef.handleKeyDown = function (...args) { old.apply(cascaderPanelRef, args) console.log('keydown') } }) })如上述代码所示，在保持原有功能的基础上，每当触发keydown事件时，还会在控制台打印keydown。由于handleKeyDown是cascaderPanelRef对象自身的方法，并非是从原型链上继承的，因此上面所做的修改只在当前组件实例生效，其它同样的组件是不受任何影响的。注：vue2中的ref可拿到完整的组件实例对象，而vue3中只能拿到组件主动暴露出来的属性或方法，修改受限，这种方法在vue2中更能发挥作用。总结在有条件的情况下，尽可能选择第一种方案，可以免去后续维护代码的工作。否则要修改的代码较多的话可以选择第二种方案，同时也可以给作者提PR，若作者接受的话也可以免去后续代码维护的工作。其他情况酌情选择第3、4、5种方案。https://www.cnblogs.com/smileZAZ/p/18310045https://juejin.cn/post/7356534347509497919
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测量从页面开始加载到其主要子资源加载完成的时间，此时页面能够快速可靠地响应交互。具体定义：从FCP开始。向前搜索至少五秒的安静窗口，其中安静窗口定义为：没有长任务并且不超过两个正在进行的网络get请求。向后搜索安静窗口之前的最后一个长任务，如果没有找到长任务，则在FCP处停止。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特殊性值的计算规则对于选择器中给定的各个ID属性值，加 0, 1, 0, 0对于选择器中给的的各个类属性值、属性选择或伪类，加 0, 0, 1, 0对于选择器中给定的各个元素和伪元素，加 0, 0, 0, 1。内联样式的特殊性为 1, 0, 0, 0结合符和通配选择器对特殊性没有任何贡献，但又不完全一样。结合符和继承根本没有特殊性，通配选择器的特殊性是0, 0, 0, 0，是要高于结合符和继承的。!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都没有。所以: Title 今天是周五 如上所示的代码，我们知道通配选择器的特殊性是0，然而继承根本没有特殊性，所以上面的代码运行结果是”周五“是灰色而不是红色。总之，0特殊性比无特殊性要强。4.层叠CSS2.1的层叠规则找出匹配特定元素的所有规则按显示权重排序应用到特定元素上的所有声明。标志!important的规则的权重要高于没有!important标志的规则。按来源对应用到给定元素的所有声明排序。共有3种来源：创作人员、读者和用户代理。正常情况下，创作人员的样式要胜过读者的样式。有!important标志的读者样式要强于所有其他样式，这包括有!important标志的创作人员样式。创作人员样式和读者样式都比用户代理的默认样式要强。按照特殊性对应用到给定元素的所有声明排序。有较高特殊性的元素权重要大于有较低特殊性的元素。按出现顺序对应用到给定元素的所有声明排序。一个声明在样式表或文档中越后出现，他的权重就越大。如果样式表中有导入的样式表，一般认为出现在导入样式表中的声明在前，主样式表中的所有声明在在后。声明的权重级别在声明权重方面要考虑从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元素或一些内联样式属性（如bgcolor、size等）是表现提示。它们虽然不属于常规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，将不能被匹配到。不过，他能匹配以下元素: Help me!!! 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\|="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 匹配同一父元素下， 元素后的所有 元素。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-activeUI状态伪类:required 和 :optional：针对必需的或可选的表单控件。:valid 和 :invalid、:in-range 和 :out-of-range：针对表单控件，根据对其设置的表单验证约束，或范围内/范围外，是有效/无效的。:enabled 和 :disabled、:read-only 和 :read-write：针对启用或禁用的表单控件（例如，设置了 disabled HTML 属性），以及读写或只读的表单控件（例如，设置了 readonly HTML 属性）。:checked、:indeterminate 和 :default：分别针对被选中的复选框和单选按钮，处于不确定的状态（既不被选中也不被选中），以及页面加载时默认选择的选项（例如，一个设置了 checked 属性的 ，或者一个设置了 selected 属性的 元素）。:target伪类与连接中的锚点有关如链接“www.baidu.com/search/#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:first-letter>T</h2:first-letter> his is an paragraph element. 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前面显示了一个%号。又或者body::after {content: "the end.";}。
1.字体族介绍font-family（字体族/字体系列）指具有同种类似特性的一系列字体的组合或者说是同种变形的组合，并不单单指的是一个字体，包含了一个或多个的font-face（字型）。 为了覆盖所有情况，CSS定义了5种通用字体族：Serif字体（衬线字体）字形宽度各异，且有上下短线。包括Times、Georgia、New Centure Schoolbook。Sans-serif字体（无衬线字体）字形宽度各异，但没有上下短线。包括Helvetica、Geneva、Verdana、Arial、Univers。Monospace字体（等宽字体）字形宽度相等，每个字符在横向上所占的空间是一样的，可能有也可能没有上下短线。这种字体通常用于显示编程代码或表格数据。包括Courier、Courier New、Andale Mono。Cursive字体（手写体、草书字体），这种字体在笔划末端有较大的花饰，且比衬线字体华丽。包括Zapf Chancery、Author、Comic Sans。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-family和src是两个必填描述符。 @font-face中描述符font-family与font-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。如果未指定500的加粗度，则其加粗度与400相同；如果未指定300的加粗度，则为之选一种比400更细的字体，如果没有则等同于400。这种情况同样适用于200,100；如果未指定600的加粗度，则为之选一种比500更粗的字体，如果没有则等同于500。这种情况同样适用于700,800,900。一种特定字体系列的假想加粗指定（仅供参考）字体指定的关键字指定的数字Zurich Light 100， 200， 300Zurich Regularnormal400Zurich Medium 500Zurich Boldbold600，700ZUrich Black 800Zurich UltraBlack 900继承font-weight是可以继承的让字体更粗如果把一个元素的加粗设置为bolder，则用户代理首先要计算其继承的父元素的加粗度，然后选一个数，它对应于比继承值更粗的加粗字体，而且在满足这条件的所有数中，选一个最小的。如果没有可选的字体，则选下一个更大的数值作为加粗度，如果已经是900的话，将会保持900不变。 例如（对于某种字体）： pppp_text_ span_text_ strong_text_bbbb_text 执行结果如上图所示。让字体更细是类似的。只不过是反过来了，让字体更粗是加粗度上移，而让字体更细是加粗度下移。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-smallx-smallsmallmediumlargex-largexx-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-styleitalic是一种单独的字体风格，斜体oblique是倾斜，是指将正常normal的字体利用算法倾斜，一般来说italic的斜体会更好看，因为是被设计过的如果没有italic字体，但是存在oblique字体，那么会在需要前者的地方使用后者。但是如果存在italic字体，不存在oblique字体，用户代理可能只是计算竖直字体的一个倾斜版本来生成oblique字体（当然这并不绝对）。通常，italic和oblique字体在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取值很多，常见的有normal和small-caps。取值为small-caps指小型大写字母文本，注意此效果既非一般的大写字母，也不是小写字母。9.font 属性所有关于字体的属性（如font-style、font-variant、font-weight）都可以放到font属性中写，作为一种简写方式。对于前三个值font-style、font-variant、font-weight，他们的顺序可以是任意的，如果某个属性的值为normal，则可以忽略不写（甚至是三个都可以不写）。后两个值，font-size和font-family必须以此顺序出现，而且其值必须有，如果少了其中某个属性，那么整个规则都将是无效的。行高 line-heightfont属性值，可以设置航高需要和font-size属性一起设置，如h2{font: bold italic 200%/1.2 Verdana, Helvetica, Arial, sans-serif};font属性的简写注意事项font属性所有简写的值都会被重置为其默认值，因此会覆盖其它地方声明已经声明过的font的 值（当然只能覆盖权重比较低的规则）。
最近碰到了一个问题，环境是vue2、ts、vue-class-component，写一个方法时发现声明的变量虽可以访问到但没有响应式，随后发现方法中的this不是VueComponent，在检查为什么this指向有问题时发现新写的方法用的是箭头函数，改成普通函数后就正常了，对于这块有点疑惑，因此整理记录关于this指向的情况。1.不同情况下this的指向方法/函数调用中的thiswindow.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指向的对象是obj1。obj2.fun()的执行结果为“bob”，表明obj2对象的fun函数虽然是由obj1的fun函数赋值得到，但由于是obj2对象调用的，所以this指向obj2，打印了obj2的name属性。window.name = 'win' let fun = () => { console.log(this.name) } let obj = { name: 'jack', funA } obj.fun() // win如上代码，当fun函数为箭头函数时，因为定义funB时所在的作用域是在全局作用域window下，所以funB内的this是window，因此obj.fun()的打印结果是”win“而非”jack“。事件回调中的this行内绑定 直接将函数执行语句"handleClick()"作为属性绑定到dom标签上，当click事件触发时，语句可视为是在全局作用域下执行的，因此this指向window。 当回调函数是箭头函数时，函数内this也为window。js动态绑定 通过js赋值函数给dom对象的onclick属性的方式绑定回调函数，回调函数是被dom对象所调用的，因此this指向dom对象。 当绑定的回调函数是箭头函数时，函数内this指向window。事件监听 通过事件监听方法绑定的回调函数，this同样指向了dom对象。 当绑定的回调函数是箭头函数时，函数内this指向window。构造函数中的thisfunction Obj() { console.log(this) } let o = new Obj() // Obj {} console.log(this) // Window {}如上代码第五行执行new Obj()语句时，打印了“Obj {}”，这是因为在new关键字会首先创建一个空对象，并将控对象的隐式原型指向构造函数的显式原型，最后将构造函数中的this指向了这个新创建的对象，因此在构造函数中，this会指向一个新创建的对象，同时它也是new语句的返回值。另外箭头函数由于没有自己的this，是不可以作为构造函数的。call、apply、bindcall、apply和bind是js中三个很神奇的函数，使用它们可以改变函数体内部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如上代码，call和apply方法都可以改变函数内部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,3bind方法可以返回一个新的函数，这个新函数的this为bind的参数，这样无论新函数在哪里被调用，其内部的this都是不会变的。class中this的指向class Person { constructor(name){ this.name = name } getName() { console.log(this.name) } } let p = new Person("tom") p.getName() // tomclass中所定义的普通函数会被添加到类的原型上，调用方法时，调用方是类的实例对象，因此方法中的this指向类的实例对象。class Person { constructor(name){ this.name = name } getName = () => { console.log(this.name) } } let p = new Person("tom") p.getName() // tomclass中所定义的箭头函数会在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内的this是window。2.问题分析将开篇所提到的问题进行简化： 如上的Vue代码中，在组件成功挂载后的created回调函数中，分别调用了普通函数click和箭头函数click1打印this和this.name，他们所打印出来的this.name是一样的，但普通函数中的this是Vue组件的实例对象，而箭头函数中的this是basicInfoTab类的实例对象。按照上面class中this的指向所述的那样，普通函数和箭头函数中的this应该是一样的才对。要搞清楚原因就需要去探究basicInfoTab类成为组件类，以及实例化组件类的过程，通过查询资料了解到大概有以下步骤：实例化类：Vue使用“new”关键字和类的构造函数创建类的新实例。初始化组件：Vue初始化新创建的类实例，设置其数据（data）、计算属性（computed）、方法（methods）等。创建组件实例：Vue将初始化后的类实例转化为Vue组件实例。在这个过程中，Vue会添加一些特殊的属性和方法，例如生命周期钩子（如created、mounted等）。挂载组件：最后，Vue将组件实例挂载到DOM元素上。在这个过程中，Vue会创建一个新的Vue实例，并将模板编译成虚拟DOM，然后将虚拟DOM渲染成真实DOM，并插入到挂载的元素中。由上可知，VueComponent组件实例对象是在basicInfoTab类实例对象的基础上初始化的，click是普通函数，因此他的this是VueComponent组件实例对象，由于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类中的属性a和b都是从Person类的实例对象p赋值而来的，Dog类的实例对象d分别调用a方法和b方法，作为普通函数的a方法中this指向自身，而箭头函数根据其定义依然指向着Person类实例对象p。因此可以对开篇遇到的问题进行解释，因为箭头函数中的this是basicInfoTab类的实例对象，所读取到的属性没有经过Vue的处理，所以不具备响应式。3.总结方法调用模式下，this总是指向调用的对象，this的指向与所在方法的调用位置有关，而与方法的声明位置无关（箭头函数例外）；在事件回调中，行内绑定的事件不论是普通函数还是箭头函数，它们的this都指向window；动态绑定及事件监听方式绑定的普通函数，this指向dom对象，绑定的箭头函数则指向window；构造函数调用模式下，this指向被构造的对象；apply、call、bind 调用模式下，this 指向函数的第一个参数；class中，不论是普通函数还是箭头函数内的this都指向实例对象；箭头函数体内的this和实际调用函数的对象无关，是定义该函数时所在的作用域所指向的对象，而不是使用时所在作用域指向的对象。参考内容：https://zhuanlan.zhihu.com/p/42145138https://juejin.cn/post/6973654160872833038https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this#%E5%B0%9D%E8%AF%95%E4%B8%80%E4%B8%8Bhttps://juejin.cn/post/6981251280236707853#heading-3https://juejin.cn/post/7035257186565488670https://juejin.cn/post/6954692013489029151#heading-7https://juejin.cn/post/6930896449878097927#heading-14https://www.zhihu.com/tardis/zm/art/57204184?source_id=1003https://blog.csdn.net/yiyueqinghui/article/details/106792408https://juejin.cn/post/6844903906917482503
项目包括一个loading组件以及Swiper控制的内容组件，Swiper的每个Slide也是一个组件，通过Swiper滑动控制切换不同的显示组件，DOM结构如下所示： <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> Swiper不能滑动对于IOS12及以下系统，Swiper11版本无法通过触摸手势触发滑动操作，调试发现直接调用slideNext和slidePre方法是可以切换下一项和上一项的。由于手动监听DOM上的touchstart、和touchmove事件是可以正常触发的，因此解决方法是通过监听touchstart、和touchmove事件根据手指移动情况判断上滑以及下滑操作，并分别调用slideNext和slidePre方法实现滑动切换。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信息并不能直接定位到具体报错代码行列，只能定位至问题的组件。因此只能逐行代码进行查错，先把 和 注释掉，再逐步放开里面的代码，最终发现是使用可选链运算符?的两行代码导致的报错。因此确定是项目使用的vue3+vite的技术栈不兼容导致的，未对可选链?运算符进行转换，低版本浏览器不能识别而报错。解决方法：使用其他写法替换可选链?运算符安装模块@babel/plugin-proposal-optional-chaining，并配置babel.config.tsmodule.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; }通过直接设置animation为none达到暂停动画的目的，不足之处是再次播放动画时，不会保留暂停时的状态。资源缓存策略项目中有大量的图片资源和一个音乐资源，为了不让用户在资源加载完成之前就进入系统中，因此在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上的表现是会溢出可视区域的，因为IOS上100%高度会把工具栏的高度也算上了。另外使用vh也存在同样的问题。在查资料过程中发现了一个叫做DVH（Dynamic Viewport Unit）动态适口单位的属性，可以完美解决百分数以及vh的缺点，但是dvh是新单位，兼容性不好。最终采用了js获取可视区域高度设置为页面高度的方法：let element = document.querySelector('#app'); // 设置元素的高度为视口的高度 if (element) { element.style.height = window.innerHeight + 'px' }
过去几个月中有两个需求都用到了html2canvas插件，目的是使Web中的DOM结构转为图片，但由于html2canvas的原理限制，遇到了一系列问题：跨域问题html2canvas转图片的原理是将传入的dom元素渲染到canvas画布上，然后利用canvas的toDataURL方式转位图片的。然而html2canvas在将dom元素绘制在canvas上时，如下html2canvas加载图片的核心代码所示：return await new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; //ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous if (isInlineBase64Image(src) || useCORS) { img.crossOrigin = 'anonymous'; } img.src = src; if (img.complete === true) { // Inline XML images may fail to parse, throwing an Error later on setTimeout(() => resolve(img), 500); } if (this._options.imageTimeout > 0) { setTimeout( () => reject(`Timed out (${this._options.imageTimeout}ms) loading image`), this._options.imageTimeout ); } });对于img会重新发起网络请求获取图片，因为是js发起的请求，所以对于有跨域限制的图片会提示跨域错误，而且可以注意到当传入的options参数设置useCORS时，是会设置img.crossOrigin = 'anonymous'的。要想解决这个问题，最简单的方法就是在图片服务器上进行设置允许跨域，但是这种方式在服务器自己不可控的情况下是行不通的。所以在解决问题的过程中就想着是否有前端侧的方法来避免这个问题呢？网上有很多文章介绍说可以给图片设置crossorigin="anonymous"、html2canvas的option设置userCORS: true、提前把跨域的网络图片转为base64等方法来解决，但是经过一系列折腾后发现，这些方法都是需要服务端的支持才行得通的。可行的方法主要包括：服务端配置跨域访问设置代理服务器图片数量固定且不多的情况下可选择放置在项目中图片显示不完整问题当dom元素有滚动条时在生成图片后可能会出现图片不完整，部分区域是空白的情况，可以调用图下代码 dom.scrollTo(0, dom.scrollHeight)控制滚动条至页面底部解决。另外还可能出现图片如果有偏移的话，可添加配置scrollY:0, scrollX:0,解决。所生成图片边缘有缝隙/空白问题由于不同设备的屏幕像素密度不一样，html2canvas在计算宽高的时候可能出现了误差导致。解决方法：设置宽度或高度减去几个像素，刚好把空白的地方裁掉（当然前提是在不影响图片内容的情况下）如果是纯色背景，可以设置html2cavas配置里的背景色为相同颜色即可透明图片异常显示的问题很奇怪的异常，最开始这个图片是以背景图方式显示的，最后换成img标签形式展示就没这个问题了。由dom元素的动画引起的显示不完整问题问题的根因是html2canvas在解析传入的dom节点之前会先去克隆一个副本，这个操作的过程可能就重新触发了动画的播放条件，于是html2cavas在将dom绘制至canvas时可能只是截取到了动画过程中的一个状态。解决这个问题的本质是要在html2cavans绘制dom时，dom的动画是不执行的或者动画已经执行完毕，具体解决方法包括：另外创建一个不可视且无动画版本的dom作为html2canvas的入参利用定时器保证在动画播放完毕时再执行html2canvas声明一个css类，设置规则animation-duration: 0s !important; animation-delay: 0s !important;使动画执行时间可延迟时间都为0s，这样dom会直接处于动画执行完毕的状态，当执行html2canvas之前动态添加这个类，当执行完毕后再删除掉这个类，不影响后续动画的执行。
问题背景在一次使用Chrome开发者工具调试中，发现找不到接口参数了，但仔细检查代码后以及结合接口响应数据来看确确实实是传参数了的，并且后端也正常接收到了请求数据，当时没有深入探究原因，现在对这个问题进行一下复盘。接口数据传输的方式Payload和Form Data是接口数据传输的两种方式，浏览器通过Content-Type来进行区分使用哪种方式：当值为application/x-www-form-urlencoded或multipart/form-data时，是Form Data方式当值为application/json时，是Request Payload方式Request Payload如下代码所示，使用axios发起post请求，设置请求头'Content-Type':'application/json'，成功发起请求后可在调试工具Payload Tab面板中看到请求参数。Request Payload通常用于发送JSON 或 XML格式的数据。axios.post(host + '/payload', { username: 'admin', password: 'admin', }, { headers: { 'Content-Type': 'application/json' } }).then((res) => { console.log(res) }).catch((err) => { console.log(err) })Form Data如下代码所示，设置请求头'Content-Type':'application/x-www-form-urlencoded'，请求参数使用Qs插件进行序列化后使用axios发送，在调试工具Payload Tab面板中可看到请求参数为Form Data类型。Form Data类型通常用于发送表单数据，并且可用来进行上传文件。axios.post(host + '/payload', Qs.stringify({ username: 'admin', password: 'admin', }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).then((res) => { console.log(res) }).catch((err) => { console.log(err) })如上图所示，再点击view source后，可看到实际上传输的数据是一个字符串，由于设置了正确的Content-Type，可以被浏览器正常识别。但如果请求参数不进行序列化，还是用Form Data方式进行请求，浏览器依然是能够正常发送出请求的，并且浏览器的Payload将会显示：但是如果Server端如果仍按照Form Data方式对请求参数处理，那么可能会出错。所以，请求参数一定要和Content-Type是对应的，否则可能会影响Server端对请求参数的正常解析。Query String Parameters另外在Chrome中，Query参数也会被放到Payload中，如下图所示：注：Chrome 96版本前，Request Payload和Form Data信息是在HeadersTab中的，96版本之后它们都被放到了PayloadTab中。问题复现所发生问题的接口参数是Request Payload格式的，并且注意到其中有个对象数据量是很大的，因此根据这一特征构造一个请求尝试复现问题：如图所示，通过循环构造了一个长字符串作为对象data属性值，经过调整不同的循环次数进行测试，确定了当Content-Length超过10000000字节时Payload Tab就会消失，并且Preview和Response显示“Failed to load response data: request content was evicted from inspector cache”。这句话翻译为中文意思是“未能加载响应数据：请求内容已从检查器缓存中收回”，大概意思就是由于超出缓存限制，请求参数以及响应参数都不能正常显示了。通过观察后端接口的请求以及浏览器请求状态码（200 OK）可知，虽然调试工具不显示请求以及响应数据，但实际上是没有影响接口的正常请求以及响应的，并且前端也能够正常接收到后端响应数据，也就是说这个问题是不影响项目的正常运行的。另外通过测试发现，当Server端的响应数据过长时，在浏览器调试工具中也会发生同样的事情，而且也不会实际影响接口数据的响应。问题原因通过查询发现以下一些情况都可能导致此问题：请求的请求数据超过了浏览器的缓存限制请求的响应数据超过了浏览器的缓存限制在浏览器的开发者工具中查看了大量的请求，并超过了浏览器的缓存限制电脑可用内存不足
背景有个移动端项目用了Vant组件库进行开发，其中有个需求要实现一个轮播图的效果，但是Vant组件库自带的轮播图组件功能有限，只能实现一些很基础的效果，而且在其基础上只进行一些样式的调整也难以实现想要的效果，所以就考虑使用其它的插件。Swiper是很著名的一个用于轮播图及移动动画的插件，面向手机、平板电脑等移动终端，并且开源免费。在网上进行搜索之后发现大多数的用法都是用原生js方式所实现的，并且Swiper官方在7版本之前也是没有适配Vue的，因此为了在Vue中能以组件方式进行调用Swiper，还诞生了一个12.7k star的第三方开源项目vue-awesome-swiper。另外在Vant的官网上也提到，如果需要更复杂的轮播效果，可以使用vue-awesome-swiper，因此就开始了一系列的踩坑之旅。遇到的问题（坑）安装模块点击进入vue-awesome-swiper的github主页，就可看到黄色的提示以及官方介绍，说是由于Swiper官网对vue的支持，这个库已经弃用了，当时看到这个提示的时候并没有细想（确实是有点急），看了如下的版本兼容性列表后发现 vue-awesome-swiper是支持项目所使用的Vue2的，于是就想着那就用它了。Swiper 5-6 vue-awesome-swiper@4.1.1 (Vue2)Swiper 4.x vue-awesome-swiper@3.1.3 (Vue2)Swiper 3.x vue-awesome-swiper@2.6.7 (Vue2)点击进去vue-awesome-swiper@4.1.1的github主页之后，看到了官方给的安装指令，心想真好，一顿CV执行了命令yarn add swiper vue-awesome-swiper，甚至都没有多看一眼但是很顺利安装上了vue-awesome-swiper。组件调用根据官方的引导进入了示例网站Examples (Vue2)，在里面找到了一个与想要效果类似的示例，并将代码复制到项目中测试运行，首先是导入组件：import { Swiper, SwiperSlide } from 'vue-awesome-swiper' import 'swiper/css/swiper.css'然后IDE报错，说是swiper.css找不到，其实这时就应该有所察觉的，但当时就想着怎么官网实例都有问题呢，可真不靠谱。在晚上一顿搜索说是，Swiper不同的版本swiper.css的位置可能是不一样的（这会竟然还没意识到Swiper的版本有问题），然后修改了正确的地址，没报错了。 <swiper class="swiper" :options="swiperOption"> <swiper-slide>Slide 1</swiper-slide> <swiper-slide>Slide 2</swiper-slide> <swiper-slide>Slide 3</swiper-slide> <swiper-slide>Slide 4</swiper-slide> <swiper-slide>Slide 5</swiper-slide> <swiper-slide>Slide 6</swiper-slide> <swiper-slide>Slide 7</swiper-slide> <swiper-slide>Slide 8</swiper-slide> <swiper-slide>Slide 9</swiper-slide> <swiper-slide>Slide 10</swiper-slide> </swiper> 接着把上面的代码复制进行了项目中，也成功运行了，并且没有任何报错，但是出来的效果没有和官网效果图一样，代码中slidesPerView设置3含义是容器中同时会出现3个swiper-slide，如下图所示。但实际上运行出的效果只有一个swiper-slide，之后又对比了与官方代码的区别，并没有什么不同，这就导致对官方代码暂时失去了信任。初步实现效果于是就在网上搜索了vue-awesome-swiper的相关用法，最终找到了能实现类似效果的一篇文章，那篇文章中的代码和官方代码相比，除了swiperOption配置外，最大的区别就是添加了一些自定义的css样式，使其实现了想要的效果。但是一个最大的问题就是分页器不显示，配置中的pagination属性就描述了分页器的相关信息，最初以为是配置写错了，但是经过一番检查后还是没发现问题，而且还用了旧版本的pagination写法也还是不行。通过Chrome的调试工具可发现，模板中写的pagination的div里面是空的，但是官方运行的实例里面是有元素的，也就是说pagination根本就没有生成，所以这就不是单单的样式问题了，就像是我写的配置没有生效一样，然后我还尝试修改了其它的一些配置发现都是没有效果的。新的思路这时想这应该还是仔细看下官方文档比较好，于是又回到了vue-awesome-swiper的github主页，才重视了"Swiper官方已对vue进行了支持"这句话，又心想既然已经有了官方的支持，那么就不用这个作为中间桥梁的第三方插件了吧，免不了这个插件会有什么莫名其妙的bug呢。Swiper官方在从7版本开始就对各大流行组件库进行了支持，包括Vue、React、Angular等。但是Swiper Vue组件的文档中有一句红色提示，说Swiper Vue组件以后很可能就不再支持了，推荐使用Swiper Element。Swiper7和8为适配Vue、React、Angular，都要为他们单独各写一套代码，随着Web Component技术的发展与流行，Swiper就提倡开发者使用Web Component组件进行开发。经过这一番考量，想着既然这样就直接用最新的吧（完全把兼容性抛在了脑后），又是一顿安装运行，发现总是报错，最终发现是不支持Vue2的。接着又尝试了Swiper Vue组件，甚至是退回到了7版本，都是不支持Vue2的（官网并没有看到醒目的兼容性提示）。这才醒悟了为什么vue-awesome-swiper为什么重点适配到了Swiper6，因为Swiper6之前是没有官网支持的组件的，而vue-awesome-swiper恰恰就干了这个事情，并且也支持Vue2。那为什么刚开始我运行是有问题的呢，总不能用原生js方式去用吧？恍然大悟干了这么多问题也没解决，但让我突然想到了是不是我最开始安装的vue-awesome-swiper版本有问题了，这时又去看了一下最开始安装命令yarn add swiper vue-awesome-swiper发现没有加版本号，这就导致我安装的Swiper是最新的11版本！！！怪不得出现了乱七八糟的问题呢。最终解决调整了安装命令为yarn add swiper@5 vue-awesome-swiper@4，又一次成功安装了Swiper。运行了代码，分页器的DOM结构终于出来了，但样式还有点问题。通过审查发现官网实例上的分页器一些样式，在我的项目里是没有的，所以肯定就是引入的样式有问题了，去看了node_modules里的css文件，重新引入了一下，问题终于解决。这时调整Swiper配置swiperOption，都是有效果的，这也证实了刚开始的配置根本没生效的猜想。反思本来没有多难得事情浪费了那么久，实属不应该，其中暴露出来的问题主要有：在不熟悉插件的情况下没有认真看文档习惯性的运行安装插件的代码，没有进行思考用新的模块/插件，没有考虑兼容性的问题做事不能乱着急，所谓心急吃不了热豆腐呀