一. JavaScript 中的数据类型
- 基本类型(原始类型) :
string、number、boolean、undefined、null、symbol(ES6+)、bigint(ES2020)。 - 引用类型(对象类型) :
Object、Array、Date、RegExp、Function、Map、Set等,以及通过构造函数创建的自定义类型。
二. JavaScript 中的类型判断方法(typeof、instanceof、Object.prototype.toString、constructor)
2.1 typeof 操作符
typeof 是最常用的类型判断操作符,它返回一个表示数据类型的字符串。
typeof 42; // "number"
typeof 'hello'; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof Symbol(); // "symbol"
typeof 123n; // "bigint"
typeof function(){}; // "function"
typeof {}; // "object"
typeof []; // "object"
typeof null; // "object" (历史遗留问题,null 被认为是一个空对象引用)
特点与局限:
- 对于基本类型(除了
null)能准确返回对应的类型字符串。 - 对于引用类型(包括
null)统一返回"object",无法区分具体是哪种对象(如数组、日期等)。 typeof对函数返回"function",这在某些场景下很有用。
2.2 instanceof 操作符
instanceof 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
[] instanceof Array; // true
{} instanceof Object; // true
new Date() instanceof Date; // true
/reg/ instanceof RegExp; // true
function(){} instanceof Function; // true
// 注意:所有对象本质上都是 Object 的实例
[] instanceof Object; // true
特点与局限:
- 能够区分不同的引用类型,但前提是该类型在原型链上。
- 不能用于基本类型(
'hello' instanceof String返回false,因为'hello'是字面量,不是String实例)。 - 如果页面中存在多个全局执行环境(如多个 iframe),不同环境中的构造函数引用不同,
instanceof可能会失效。
2.3 Object.prototype.toString
这是最准确判断内置类型的方法。每个对象都有一个 toString() 方法,当被调用时,会返回一个格式为 "[object Type]" 的字符串,其中 Type 是对象的内部属性 [[Class]] 的值。
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call('hello'); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Symbol()); // "[object Symbol]"
Object.prototype.toString.call(123n); // "[object BigInt]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call(/reg/); // "[object RegExp]"
Object.prototype.toString.call(function(){}); // "[object Function]"
Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(new Set()); // "[object Set]"
我们可以基于此封装一个通用的 getType 函数:
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}
getType([]); // "array"
getType(null); // "null"
这种方法几乎能判断所有 JavaScript 内置类型,但不能区分自定义类的实例(它们都会返回 "[object Object]")。
2.4 constructor 属性
每个对象都有一个 constructor 属性,指向创建该对象的构造函数。可以通过比较 constructor 来判断类型。
[].constructor === Array; // true
{}.constructor === Object; // true
new Date().constructor === Date; // true
(42).constructor === Number; // true(注意:字面量会被临时包装成对象)
注意:
constructor属性可以被修改,因此不保证绝对准确。- 对于
null和undefined,它们没有constructor属性,会报错。
2.5 特定类型的判断方法
-
数组:
Array.isArray()是 ES5 引入的专门用于判断数组的方法,优于instanceof因为它能正确处理跨全局对象的情况。Array.isArray([]); // true Array.isArray({}); // false -
NaN:
NaN是一个特殊的数值,表示“非数字”,它不等于自身。可以使用全局函数isNaN()或 ES6 的Number.isNaN()。isNaN(NaN); // true isNaN('hello'); // true(因为字符串会被隐式转换为数字) Number.isNaN(NaN); // true Number.isNaN('hello'); // false(不会强制转换) -
整数:
Number.isInteger()判断一个值是否为整数。Number.isInteger(42); // true Number.isInteger(42.0); // true Number.isInteger(42.1); // false -
有限数值:
Number.isFinite()判断一个值是否为有限的数字。Number.isFinite(42); // true Number.isFinite(Infinity); // false
三. CSS 盒模型(IE 盒模型 又称 怪异盒模型、标准盒模型 又称 W3C 盒模型)
3.1 标准盒模型(W3C 盒模型)
- 元素的
width和height只包含内容(content) ,不包括padding和border。 - 元素实际占用的宽度 =
width+ 左右padding+ 左右border+ 左右margin - 高度计算同理。
3.2 IE 盒模型(怪异盒模型)
- 元素的
width和height包含内容(content)、内边距(padding)和边框(border) ,但不包括外边距。 - 元素实际占用的宽度 =
width(已包含 padding 和 border)+ 左右margin - 这种方式下,设置的宽度就是元素在页面上占据的“视觉宽度”(不含 margin),更符合直觉,布局时更容易控制。
3.3 如何切换盒模型
/* 标准盒模型(默认) */
box-sizing: content-box;
/* IE 盒模型 */
box-sizing: border-box;
3.4 外边距合并
外边距合并只发生在垂直方向,且只有块级元素的上下外边距会合并,行内元素、浮动元素、绝对定位元素的外边距不会合并
在垂直方向上,相邻块级元素的上下外边距有时会合并(取较大值),而不是简单相加。这也是盒模型相关的一个重要现象。
<div style="margin-bottom: 30px;">A</div>
<div style="margin-top: 20px;">B</div>
A 和 B 之间的实际间距是 30px(取最大值),而不是 50px。
解决方式:
-
- 给父元素添加边框或内边距
-
- 改变元素的布局方式(浮动、绝对定位、Flex、Grid、inline-block)
-
- 使用 padding 代替 margin
-
- 给父元素设置
::before或::after伪元素
- 给父元素设置
四. 浏览器存储(Cookie、localStorage、sessionStorage、 IndexedDB、Cache API)
各存储方式对比
| 特性 | Cookie | localStorage | sessionStorage | IndexedDB | Cache API |
|---|---|---|---|---|---|
| 存储大小 | ~4KB | 5-10MB | 5-10MB | 通常 >250MB | 视磁盘空间而定 |
| 生命周期 | 可设置过期时间 | 永久,除非清除 | 标签页关闭后清除 | 永久,除非清除 | 开发者控制 |
| 作用域 | 同源 + path | 同源 | 同源 + 标签页 | 同源 | 同源(Service Worker) |
| 是否随请求发送 | 是 | 否 | 否 | 否 | 否 |
| API 类型 | 同步 | 同步 | 同步 | 异步 | 异步 |
| 数据类型 | 字符串 | 字符串 | 字符串 | 结构化数据 | Response 对象 |
| 支持索引/查询 | 无 | 无 | 无 | 支持索引、事务 | 无 |
| 常见用途 | 会话管理 | 用户设置、简单缓存 | 临时状态 | 大量结构化数据 | 离线资源缓存 |
4.1 localStorage
-
作用域:同源(协议 + 域名 + 端口)内的所有窗口、标签页、iframe 共享。
-
生命周期:持久化存储,除非用户手动清除或通过代码删除,否则数据不会过期。
-
新窗口/新页面行为:
- 无论通过何种方式打开新窗口/新标签页(普通链接、
window.open、右键新标签页打开等),只要是同源的,都可以访问同一个localStorage数据。 - 数据在所有同源页面中实时共享:在一个页面中修改
localStorage,其他同源页面如果监听了storage事件,会收到通知(但当前页面不会触发该事件)。
- 无论通过何种方式打开新窗口/新标签页(普通链接、
// 存储数据
localStorage.setItem('key', 'value');
localStorage.setItem('user', JSON.stringify({ name: 'John', age: 30 }));
// 读取数据
const value = localStorage.getItem('key'); // "value"
const user = JSON.parse(localStorage.getItem('user')); // { name: 'John', age: 30 }
// 删除指定键
localStorage.removeItem('key');
// 清空所有数据
localStorage.clear();
4.2 sessionStorage
-
作用域:仅限于当前标签页(或当前浏览上下文) ,并且只在该标签页的生命周期内有效。
-
生命周期:标签页关闭后数据被清除;页面刷新或恢复不会丢失。
-
新窗口/新页面行为:
- 通过普通链接(如
<a target="_blank">、右键新标签页打开)打开的新页面:会创建一个全新的、独立的sessionStorage实例。新页面不会继承原页面的任何数据,之后两者的修改也互不影响。 - 通过
window.open打开的同源新窗口:现代浏览器(Chrome、Firefox 等)的行为是:新窗口会获得一个独立的sessionStorage,但可能会复制原页面当前已有的数据作为初始值(这一行为取决于浏览器实现,但无论如何,两者之后不再同步)。例如,在 Chrome 中,通过window.open打开的同源页面会复制当前的sessionStorage,但后续修改各自独立。
- 通过普通链接(如
// 存储数据
sessionStorage.setItem('temp', 'some data');
// 读取数据
const temp = sessionStorage.getItem('temp');
// 删除指定键
sessionStorage.removeItem('temp');
// 清空所有
sessionStorage.clear();
4.3 Cookie
Cookie 是最早的客户端存储机制,由网景公司发明,用于解决 HTTP 无状态的问题。
- 存储大小:通常限制在 4KB 左右(不同浏览器略有差异)。
- 生命周期:可以设置
expires或max-age指定过期时间;如果不设置,则为会话 Cookie,关闭浏览器后失效。 - 作用域:受同源策略限制,但可以通过
domain和path控制可访问的页面。 - 传输方式:每次请求都会自动携带在同源的 HTTP 请求头中(
Cookie头),因此会增加网络开销。 - 安全性:可以设置
HttpOnly标志防止 JavaScript 访问(用于防范 XSS 攻击),Secure标志要求仅通过 HTTPS 传输,SameSite限制跨站请求。 - 使用场景:适合存储会话标识(Session ID)、少量用户偏好等需要自动携带到服务器的数据
// 设置 Cookie(可设置过期时间,不设置则默认会话 Cookie)
document.cookie = "username=John; path=/; max-age=3600"; // 有效期1小时
// 读取所有 Cookie(返回一个字符串,如 "username=John; theme=dark")
const cookies = document.cookie;
// 解析 Cookie 为对象
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
console.log(getCookie('username')); // "John"
// 删除 Cookie(设置过期时间为过去)
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
4.4 IndexedDB
IndexedDB 是一个浏览器内置的 NoSQL 数据库,用于存储大量结构化数据。
- 存储大小:通常较大,一般不少于 250MB,甚至可以达到剩余磁盘空间的很大比例(具体取决于浏览器)。
- 生命周期:持久化,除非用户清除浏览器数据。
- 作用域:同源。
- API:异步,基于事件或 Promise(现代包装器如
idb库可简化操作)。支持索引、事务、游标等数据库特性。 - 数据类型:可以存储任何结构化数据,包括文件、二进制数据(Blob)、对象等。
- 适用场景:需要存储大量数据(如离线应用、文档编辑器、大型缓存)时使用。
// 打开数据库(版本号 1)
const request = indexedDB.open('MyDatabase', 1);
request.onerror = (event) => {
console.error('数据库打开失败', event);
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log('数据库打开成功', db);
// 进行事务操作
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
store.add({ id: 1, name: 'John' });
};
// 数据库升级时创建对象仓库(表)
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建对象仓库,主键为 id
const objectStore = db.createObjectStore('users', { keyPath: 'id' });
objectStore.createIndex('name', 'name', { unique: false });
};
4.5 Cache API
Cache API 是 Service Worker 的一部分,主要用于缓存网络请求的响应(如 HTML、CSS、JS、图片等),实现离线访问和性能优化。
- 存储大小:取决于浏览器实现,通常与 IndexedDB 共享存储空间。
- 生命周期:由开发者通过代码管理(添加、删除),也可以由浏览器自动清理。
- 作用域:受 Service Worker 控制的域。
- API:基于 Promise,提供了
caches.open()、cache.put()、cache.match()等方法。 - 适用场景:离线应用、资源预缓存、动态缓存策略等。
// 打开或创建缓存
caches.open('my-cache-v1').then((cache) => {
// 添加资源到缓存
cache.addAll(['/', '/styles.css', '/script.js']);
// 手动缓存某个请求的响应
fetch('/api/data').then(response => {
cache.put('/api/data', response.clone()); // 注意 response 只能使用一次,需克隆
});
});
// 从缓存中读取
caches.match('/api/data').then(response => {
if (response) {
return response.json();
}
});
五、什么是 CSS Modules
CSS Modules 是一种 CSS 文件模块化的解决方案,它通过构建工具(如 webpack)在编译时为 CSS 类名生成唯一的名称,从而默认将样式作用域限制在局部,避免全局命名冲突。简单来说,你写的 .button 在编译后可能变成 .Button_button_1a2b3c,确保不同组件间的样式不会互相干扰。
5.1 对比
| 方案 | 作用域 | 动态样式 | 学习成本 | 构建配置 | 适用场景 |
|---|---|---|---|---|---|
| 普通 CSS | 全局 | 需手动切换 | 低 | 无 | 小型项目、静态页面 |
| BEM | 约定式局部 | 需手动管理 | 中 | 无 | 中大型项目,需团队规范 |
| CSS Modules | 编译时局部 | 配合 JS | 低 | 需配置 | 组件化开发(React/Vue) |
| CSS-in-JS | 运行时局部 | 灵活 | 中 | 需支持 | 高度动态样式、主题化 |
5.2 在 Vue 中使用
Vue 的单文件组件(SFC)可以通过 <style module> 直接支持 CSS Modules:
<template>
<p :class="$style.red">This should be red</p>
</template>
<style module>
.red {
color: red;
}
</style>
5.3 在 React 中使用
5.3.1 配置 webpack
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.module\.css$/, // 约定文件名以 .module.css 结尾
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true, // 开启 CSS Modules
},
},
],
},
// 处理普通 CSS 文件(不使用模块化)
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};
5.3.1 在组件中使用
// Button.js
import React from 'react';
import styles from './Button.module.css';
function Button({ children, large }) {
const className = large ? `${styles.button} ${styles.large}` : styles.button;
return <button className={className}>{children}</button>;
}
export default Button;
六. css响应式设计实现
6.1 核心要素(Ethan Marcotte 提出的三原则)
- 流式网格(Fluid Grids) :使用相对单位(如
%、vw/vh)而非固定像素来定义布局宽度。 - 弹性图片(Flexible Images) :图片通过
max-width: 100%等设置,使其能在不同容器内缩放,避免溢出。 - 媒体查询(Media Queries) :根据设备特征(宽度、高度、分辨率、方向等)应用不同的 CSS 规则。
6.2 基本实现
6.2.1 视口设置
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6.2.2 流式布局(Fluid Layout)
使用百分比或视口单位替代固定像素,使容器宽度随视口变化:
.container {
width: 100%; /* 父容器占满屏幕 */
}
.sidebar {
width: 30%; /* 侧边栏占比固定 */
}
.main {
width: 70%;
}
6.2.3 媒体查询(Media Queries)
通过 @media 规则,在不同条件下应用不同样式:
/* 基础样式(适用于所有设备) */
body { font-size: 16px; }
/* 当视口宽度 ≤ 768px(平板竖屏/手机横屏)时生效 */
@media (max-width: 768px) {
.sidebar { width: 100%; } /* 侧边栏占满,堆叠布局 */
}
/* 当视口宽度 ≤ 480px(手机竖屏)时生效 */
@media (max-width: 480px) {
body { font-size: 14px; } /* 字体调小 */
}
6.2.4 弹性媒体
确保图片和视频不会超出容器:
img, video, iframe {
max-width: 100%;
height: auto;
}
6.2.5 相对单位
- 字体:使用
rem(相对于根元素html的字体大小)或em,便于整体缩放。 - 间距:使用
%、vw、vh、clamp()等函数,使边距随屏幕变化。
6.3 为什么采用移动优先?
- 性能优化:移动端网络和硬件资源有限,优先加载核心内容和轻量样式。
- 内容优先级:迫使设计师和开发者思考“什么是真正重要的”,去掉冗余装饰。
- 渐进增强:从简单到复杂,避免处理复杂的向下兼容问题。
- 未来适应:新设备尺寸可能更小或更大,基础样式依然可用。
6.4 弹性盒子和网格无法完全替代媒体查询
弹性盒子、网格以及其他现代 CSS 特性可以大大增强布局的灵活性,在很多场景下减少甚至替代对媒体查询的依赖,但它们并非“代替品”,而是互补工具
七、弹性盒子
7.1 核心概念
Flexbox 由容器(flex container) 和项目(flex items) 组成。容器通过设置 display: flex 或 display: inline-flex 来启用 Flexbox 布局,其直接子元素自动成为弹性项目。
7.2 容器属性(设置在父元素上)
7.2.1 flex-direction:定义主轴方向
row(默认):水平从左到右。row-reverse:水平从右到左。column:垂直从上到下。column-reverse:垂直从下到上。
7.2.2. flex-wrap:控制项目是否换行
nowrap(默认):所有项目在一行/列,可能溢出。wrap:空间不足时换行。wrap-reverse:换行,但交叉轴方向相反。
7.2.3. justify-content:主轴上的对齐方式
flex-start(默认):起始端对齐。flex-end:末端对齐。center:居中对齐。space-between:两端对齐,项目之间间距相等。space-around:每个项目两侧间距相等。space-evenly:项目之间和两端间距完全相等。
7.2.4. align-items:交叉轴上的单行对齐
stretch(默认):如果项目未设置高度/宽度,则拉伸填满容器。flex-start:起始端对齐。flex-end:末端对齐。center:居中对齐。baseline:按基线对齐。
7.2.5. align-content:交叉轴上的多行对齐(当有多行时生效)
取值与 justify-content 类似:flex-start、flex-end、center、space-between、space-around、space-evenly、stretch。
7.2.6. gap:设置项目之间的间距
row-gap:行间距。column-gap:列间距。gap:简写,如gap: 10px 20px;(行 列)。
7.3 项目属性(设置在子元素上)
7.3.1. flex-grow:放大比例
- 默认
0,即即使有剩余空间也不放大。 - 若所有项目的
flex-grow都为 1,则等分剩余空间;若某个为 2,则占据的空间是其他 1 的两倍。
7.3.2. flex-shrink:缩小比例
- 默认
1,即空间不足时等比例缩小。 - 设为 0 则不会缩小。
7.3.3. flex-basis:项目在分配空间前的初始大小
- 可以是长度值(如
200px)或auto(默认,参考自身宽高)。 - 与
width或height类似,但在 Flex 中优先级更高。
7.3.4. flex 简写
-
语法:
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ] -
常用值:
flex: 1=flex-grow: 1; flex-shrink: 1; flex-basis: 0%;flex: auto=flex-grow: 1; flex-shrink: 1; flex-basis: auto;flex: none=flex-grow: 0; flex-shrink: 0; flex-basis: auto;
7.3.5. align-self:覆盖容器的 align-items 对该项目的对齐方式
取值与 align-items 相同,可单独调整某个项目的位置。
7.3.6. order:定义项目的排列顺序
- 默认
0,数值越小越靠前,允许负值。
八、哪些 CSS 属性会触发重排、重绘或合成?
浏览器的渲染流程大致包括:构建 DOM 树和 CSSOM 树 → 合并为渲染树 → 布局(计算几何位置)→ 绘制(填充像素)→ 合成(将各层合并显示)。修改 CSS 属性可能影响其中一到多个阶段。
8.1. 触发重排(Reflow / Layout)的属性
重排是指浏览器需要重新计算元素的几何属性(尺寸、位置等),代价最高。常见的触发属性:
- 盒模型相关:
width、height、padding、margin、border(包括border-width)、box-sizing。 - 定位与布局:
position(absolute、relative 等)、top、left、bottom、right、display(如display: none→block)、float、clear、flex属性(flex-basis、flex-grow等)、grid属性。 - 文字相关:
font-size、font-family、font-weight、line-height、text-align、overflow。 - 其他影响布局的属性:
vertical-align、min-height、max-height、visibility: collapse等。 - 获取布局信息的操作:虽然不直接是 CSS 属性,但像
offsetHeight、getBoundingClientRect()等也会强制触发重排(为了返回最新值)。
8.2. 触发重绘(Repaint)的属性
重绘是指元素外观改变但不影响布局,浏览器只需重新绘制该元素的可见区域。常见属性:
- 颜色与背景:
color、background-color、background-image(如果图片尺寸不变)、border-color、outline-color。 - 轮廓与阴影:
outline、box-shadow(通常只重绘,除非阴影尺寸变化影响布局)、text-shadow。 - 可见性:
visibility(hidden→visible不触发重排,但重绘)。 - 其他:
border-radius、cursor等。
8.3. 触发合成(Composite)的属性
合成是将各绘制层合并的过程,现代浏览器通常利用 GPU 加速。某些属性变化时,如果元素已提升为独立合成层(例如通过 will-change 或隐式提升),则只需合成,完全跳过重排和重绘。典型属性:
- 变换与透明度:
transform(如translate、rotate、scale)、opacity。 - 滤镜与混合模式:
filter、backdrop-filter(部分情况可能触发重绘,但通常优先合成)。 - 其他触发层提升的属性:
will-change(提示浏览器准备合成层)、position: fixed(可能创建独立层)、video或canvas元素、包含transform或opacity动画的元素等。
九、哪些 CSS 属性会触发重排、重绘或合成
浏览器的渲染流程大致包括:构建 DOM 树和 CSSOM 树 → 合并为渲染树 → 布局(计算几何位置)→ 绘制(填充像素)→ 合成(将各层合并显示)。修改 CSS 属性可能影响其中一到多个阶段。
9.1. 触发重排(Reflow / Layout)的属性
重排是指浏览器需要重新计算元素的几何属性(尺寸、位置等),代价最高。常见的触发属性:
- 盒模型相关:
width、height、padding、margin、border(包括border-width)、box-sizing。 - 定位与布局:
position(absolute、relative 等)、top、left、bottom、right、display(如display: none→block)、float、clear、flex属性(flex-basis、flex-grow等)、grid属性。 - 文字相关:
font-size、font-family、font-weight、line-height、text-align、overflow。 - 其他影响布局的属性:
vertical-align、min-height、max-height、visibility: collapse等。 - 获取布局信息的操作:虽然不直接是 CSS 属性,但像
offsetHeight、getBoundingClientRect()等也会强制触发重排(为了返回最新值)。
9.2. 触发重绘(Repaint)的属性
重绘是指元素外观改变但不影响布局,浏览器只需重新绘制该元素的可见区域。常见属性:
- 颜色与背景:
color、background-color、background-image(如果图片尺寸不变)、border-color、outline-color。 - 轮廓与阴影:
outline、box-shadow(通常只重绘,除非阴影尺寸变化影响布局)、text-shadow。 - 可见性:
visibility(hidden→visible不触发重排,但重绘)。 - 其他:
border-radius、cursor等。
9.3. 触发合成(Composite)的属性
合成是将各绘制层合并的过程,现代浏览器通常利用 GPU 加速。某些属性变化时,如果元素已提升为独立合成层(例如通过 will-change 或隐式提升),则只需合成,完全跳过重排和重绘。典型属性:
- 变换与透明度:
transform(如translate、rotate、scale)、opacity。 - 滤镜与混合模式:
filter、backdrop-filter(部分情况可能触发重绘,但通常优先合成)。 - 其他触发层提升的属性:
will-change(提示浏览器准备合成层)、position: fixed(可能创建独立层)、video或canvas元素、包含transform或opacity动画的元素等。
9.4. 为什么推荐使用 transform 和 opacity 进行动画性能优化?
在动画性能优化中,优先使用 transform 和 opacity 主要有以下几个原因:
9.4.1. 避免重排和重绘,仅触发合成
transform和opacity的变化通常不会改变元素的几何布局,也不会导致重绘。浏览器可以将动画元素提升到一个独立的合成层(Composite Layer),动画过程完全由合成线程处理,无需主线程参与布局和绘制,从而极大降低 CPU/GPU 负载。- 相比之下,修改
left/top等属性会触发重排,导致主线程重新计算布局,性能开销巨大,容易出现卡顿。
9.4.2. 充分利用 GPU 加速
- 现代浏览器会为合成层分配 GPU 内存,动画时直接由 GPU 处理(如纹理变换、透明度混合)。GPU 擅长并行处理此类变换,能以极低的功耗实现流畅的 60fps 动画。
transform的translate3d()等 3D 变换甚至能强制启用 GPU 加速(尽管 2D 变换也常走 GPU)。
9.4.3. 减少重绘区域和合成开销
- 当使用
transform移动一个元素时,浏览器只需重新合成该层与下方内容,不会污染其他区域。而使用top/left移动时,页面可能大面积重绘。 opacity动画同样由合成器处理,只需调整层的透明度,无需逐帧重绘元素内容。
9.4.4. 支持独立的合成线程
- 合成线程独立于主线程,即使主线程执行 JavaScript 或样式计算任务繁忙,动画仍能保持流畅。这使得
transform和opacity动画特别适合需要与用户交互并行的场景。
9.4.5. 浏览器内置优化提示
- 通过
will-change: transform, opacity可以提前告知浏览器哪些属性可能变化,让浏览器尽早创建合成层,进一步减少动画启动时的卡顿。
十、常见单位
10.1 绝对长度单位
这些单位在现实中对应固定的物理尺寸,但在屏幕上通常与参考像素(px)近似对应,不随其他因素变化。
px(像素) :最常用的单位,1px 在屏幕上通常对应一个物理像素点(取决于设备像素比)。
10.2 相对长度单位
相对单位相对于某个参考值(如字体、视口、父元素)计算。除了之前提到的 em、rem、vw、vh、vmin、vmax、%、ex、ch 外,还有:
-
字体相对单位(新/补充) :
em如果用于font-size属性本身,则相对于父元素的字体大小
.parent { font-size: 16px; } .child { font-size: 1.5em; } /* 计算值 = 16px * 1.5 = 24px */rem: 相对于根元素(<html>)的字体大小。通常浏览器默认根字体大小为16px
html { font-size: 16px; } .box { width: 10rem; } /* 160px */vw / vh:vw:相对于视口宽度的1%。vh:相对于视口高度的1%。常用于全屏元素、大标题等需要随视口缩放的情况。
.hero { height: 100vh; } /* 高度占满整个视口 */ h1 { font-size: 5vw; } /* 字体大小为视口宽度的5% */vmin / vmax:vmin:相对于视口宽度和高度中较小者的1%;vmax:相对于较大者的1%。常用于保持元素比例,避免在屏幕旋转时溢出。
.square { width: 50vmin; height: 50vmin; } /* 正方形始终基于较小边 */%: 相对于包含块(父容器) 的对应尺寸。常用于宽度、高度、内外边距等ex / ch:ex:相对于当前字体中字符“x”的高度(通常是字体的小写字母高度)。ch:相对于当前字体中数字“0”的宽度(等宽字体中常等于一个字符的宽度)。多用于排版微调,如限制行宽。
p { max-width: 60ch; } /* 每行最多约60个字符 */下面是不常用单位
lh:相对于当前元素的行高(line-height) 。1lh 等于该元素的行高值。可用于实现与行高对齐的布局。rlh:相对于根元素(html)的行高,类似rem基于根元素字体大小。兼容性逐渐提升。ic:相对于“水”字形(CJK ideograph)的尺寸,1ic 等于一个字形的宽度(通常是全角字符)。用于排版时与汉字对齐。cap:相对于字体中大写字母的高度(cap height)。rex:相对于根元素的ex单位(根元素字体的小写字母 x 高度)。
-
视口相对单位(新/补充) :
svh、svw:表示小视口(small viewport) 的百分比。在移动端地址栏动态显示/隐藏时,小视口对应较小(固定)的视口尺寸,避免布局跳动。lvh、lvw:表示大视口(large viewport) 的百分比,对应视口最大可能尺寸。dvh、dvw:表示动态视口(dynamic viewport) 的百分比,随浏览器 UI 变化实时更新。vi:相当于视口内联轴(inline axis)尺寸的1%(在书写模式下,内联轴通常为宽度)。vb:相当于视口块轴(block axis)尺寸的1%(在书写模式下,块轴通常为高度)。
-
容器查询单位(Container Query Units) :
cqw、cqh:分别相对于容器查询容器的宽度和高度。cqi:相对于容器内联尺寸(inline size)。cqb:相对于容器块尺寸(block size)。cqmin、cqmax:分别为容器内联尺寸和块尺寸中的较小值或较大值。
(需要父容器被声明为容器,如container-type: inline-size;)
10.3 角度单位(Angle Units)
用于旋转、渐变方向等,如 transform: rotate()。
deg(度) :一圈为 360deg。grad(百分度) :一圈为 400grad。rad(弧度) :一圈为 2π rad ≈ 6.2832rad。turn(圈数) :1turn = 一圈,例如0.25turn等于 90deg。
10.4 时间单位(Time Units)
用于过渡、动画的持续时间(transition-duration、animation-duration)和延迟。
s(秒)ms(毫秒) :1s = 1000ms。
10.5 频率单位(Frequency Units)
主要用于音频相关的 CSS 属性(如 <audio> 元素),但目前支持较少。
Hz(赫兹)kHz(千赫兹)
10.6 分辨率单位(Resolution Units)
用于描述设备分辨率,通常用于媒体查询(如 min-resolution)。
dpi(每英寸点数)dpcm(每厘米点数)dppx或x(每像素点数) :1dppx = 96dpi,常用于检测设备像素比(DPR)。
10.7 弹性单位(Flexible Units)—— 特殊
fr(分数单位) :用于 CSS Grid 布局中,表示剩余空间的比例分配。不是长度单位,但常用于定义网格轨道尺寸。例如grid-template-columns: 1fr 2fr;表示将可用空间按 1:2 分配给两列。
10.8 百分比(%)—— 分类说明
百分比是一种相对单位,但它的计算依据取决于应用的具体属性:
width、height等基于父容器的对应尺寸。margin、padding的百分比基于父容器的宽度(包括块级元素的上下内边距和外边距)。font-size基于父元素的字体大小。line-height基于当前元素的字体大小。transform: translate()的百分比基于元素自身尺寸。
十一 webpack 和 vite 区别
11.2.1 对比
| 对比维度 | Webpack | Vite |
|---|---|---|
| 开发服务器启动 | 需要从入口开始递归构建依赖图,打包成 bundle 后启动,耗时随项目增大而增加。 | 直接启动,浏览器按需加载模块,只编译当前请求的文件,秒级启动。 |
| 热更新(HMR) | 修改一个模块后,重新编译该模块及其依赖,更新 bundle,速度随项目变慢。 | 利用 ESM 的精确热边界,只编译修改的模块,利用浏览器缓存,极快。 |
| 生产构建 | 自身完成打包、压缩、代码分割等所有工作,通过 loader 和 plugin 扩展。 | 生产环境使用 Rollup 打包,配置简单,构建快,但不如 Webpack 灵活。 |
| 配置复杂度 | 配置复杂,需要手动配置 loader、plugin、优化选项等。 | 基于预设(如 Vue、React)零配置即可,需要定制时配置简单。 |
| 生态与插件 | 插件体系庞大,几乎涵盖所有需求,社区活跃。 | 兼容 Rollup 插件生态,同时发展自己的插件,但数量和质量仍在追赶。 |
| 模块化支持 | 支持 CommonJS、ESM、AMD 等多种模块规范。 | 开发环境依赖原生 ESM,生产环境通过 Rollup 将代码转为合适格式。 |
| 浏览器兼容性 | 通过打包和 polyfill 可以兼容任何浏览器。 | 开发环境要求现代浏览器支持 ESM;生产构建后可通过配置降级兼容。 |
11.2 底层原理解析
11.2.1 Webpack 的底层原理
初始化:读取配置文件,创建 Compiler 对象,注册插件,确定入口。
-
编译阶段:
- 从入口开始递归解析模块依赖,调用对应的 loader 转换文件内容(如将 TS 转为 JS,SCSS 转为 CSS)。
- 生成 AST(抽象语法树),分析模块间的依赖关系,构建依赖图(Module Graph)。
-
打包阶段:
- 将所有模块组合成 chunks,根据配置进行代码分割、优化(如 tree shaking、压缩)。
- 输出 bundle 文件,包含模块运行时(runtime)和模块定义,用于在浏览器中加载和执行。
-
热更新:
- 开发环境下,Webpack 启动一个 WebSocket 服务,当文件变更时,重新编译变更模块,生成补丁,通过 WebSocket 推送给客户端,客户端动态替换模块。
关键点:Webpack 在开发环境也进行全量打包,因此启动和热更新速度受项目规模影响较大。
11.2.2 Vite 的底层原理
开发环境
-
基于 ESM 的开发服务器:使用
esbuild预构建依赖(将 CommonJS 转为 ESM,并合并小模块以减少请求),然后启动一个静态服务器。 -
浏览器加载流程:
- 浏览器请求
index.html,其中通过<script type="module" src="/src/main.js">引入入口。 - 浏览器解析入口文件时,遇到
import语句,向服务器请求对应的模块。 - 服务器拦截请求,如果是源文件(如
.vue、.tsx),则使用相应的插件(如@vitejs/plugin-vue)进行实时编译,返回编译后的 JS 代码。 - 如果是第三方依赖,返回预构建后的 ESM 版本。
- 浏览器请求
-
热更新:文件变更时,Vite 通过 WebSocket 通知浏览器,浏览器只重新请求变更的模块,并执行 HMR 回调,无需刷新页面。
生产环境
- 使用 Rollup 打包:Vite 在生产环境放弃了 ESM,因为原生 ESM 在大型项目中仍有性能问题(如请求瀑布流)。Rollup 具有优秀的 tree shaking 和打包优化能力,Vite 直接利用 Rollup 的配置和插件体系进行构建,输出高度优化的静态资源。
- 预配置:Vite 提供了针对不同框架的预设,如
@vitejs/plugin-react、@vitejs/plugin-vue,开发者无需手动配置。
关键点:Vite 在开发环境利用了浏览器原生能力,避免了打包开销;生产环境借助 Rollup 保证了最终产物的质量。
十二 离线 功能实现方式:
在实际项目中,通常会组合使用:用 IndexedDB 存储业务数据,用 Cache API 缓存静态资源,用 localStorage 存储简单配置。
12.1 IndexedDB 是当前前端离线存储的主流方案,原因如下:
- 存储容量大:轻松存储几百 MB 数据,远超 localStorage。
- 功能丰富:支持复杂查询、索引、事务,适合结构化数据。
- 浏览器支持广泛:所有现代浏览器(Chrome、Firefox、Safari、Edge)均完全支持。
- 生态完善:封装库如 localForage、Dexie.js 极大降低了使用复杂度,提供类似 localStorage 的简单 API,同时底层使用 IndexedDB,并降级到 WebSQL/localStorage。
12.2 何时仍用 localStorage?
- 数据量小(几 KB 以内)
- 无需复杂查询
- 希望同步读写简单代码
12.3 何时用 Cache API + Service Worker?
- 需要缓存网络资源实现离线访问(PWA 必需)
- Service Worker 简介:在后台运行的脚本,可以拦截网络请求
- Cache API:用于存储网络请求的响应。
12.4 Cache API + Service Worker 对比 IndexdDB
| 对比维度 | Service Worker + Cache API | IndexedDB |
|---|---|---|
| 核心目的 | 拦截网络请求,缓存资源(HTML、CSS、JS、API 响应等),实现离线访问 | 在客户端存储大量结构化数据,支持复杂查询和事务 |
| 存储内容类型 | 完整的 HTTP 请求/响应对象(包括 URL、头信息、状态码、正文) | JavaScript 对象、数组、二进制数据(Blob、ArrayBuffer) |
| 数据模型 | 键值对(缓存名称 → 请求/响应对),基于 URL 匹配 | 对象存储(类似表),支持索引、游标、事务 |
| 查询能力 | 仅能通过请求 URL 精确匹配(caches.match()) | 强大的查询:索引、范围查询、排序、过滤、游标迭代 |
| 容量限制 | 无硬性上限,受浏览器磁盘配额管理(通常足够缓存应用资源) | 通常 > 250MB,甚至更多,同样受磁盘配额管理 |
| API 风格 | 基于 Promise,相对简单 | 基于 Promise 或回调,但底层 API 较复杂,通常借助库简化 |
| 生命周期 | 由 Service Worker 管理,可设置版本号,主动清理过期缓存 | 数据持久存储,需手动删除或过期处理 |
| 与网络请求的关系 | 直接拦截 fetch 事件,是离线访问的核心 | 独立于网络,需手动读写,可配合 Service Worker 返回数据 |
| 实现复杂度 | 中等(需理解 SW 生命周期、缓存策略) | 较高(原生 API 复杂,通常用 Dexie.js 等库简化) |
| 主要应用场景 | 静态资源缓存、API 响应缓存、离线回退页面、PWA 核心能力 | 离线数据存储(如购物车、消息列表)、本地数据库、复杂表单暂存 |
| 是否需要 Service Worker | 必须运行在 Service Worker 中 | 可在主线程或 Worker 中运行,无需 Service Worker |
十三 img 标签中 srcset 和 sizes (优化点、不同设备显示不同图片)
srcset 的值是一个字符串,用来定义一个或多个图像候选地址,以 , 分割,每个候选地址将在特定条件下使用
sizes 为每个媒体条件指定图像的布局宽度。在布局状态更改以匹配不同介质条件时自动选择不同图像(甚至是不同方向或长宽比的图像)的能力。
<img
src="fallback.jpg"
srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 1000px"
alt="..."
/>
十四 为什么0.5px不起作用
14.1 物理像素限制
CSS 的 px 是逻辑像素,实际显示时由设备像素比(DPR)决定映射到多少物理像素。屏幕无法显示半个物理像素,浏览器会将 0.5px 向上取整为 1px,导致边框比预期更粗。
14.2 浏览器兼容性差
只有少数浏览器(如 iOS Safari)在特定条件下支持 0.5px(需要配合 @media (-webkit-min-device-pixel-ratio: 2) 等媒体查询显式启用),Android 及桌面浏览器普遍不支持,直接写会被解释为 1px。
14.3 亚像素渲染问题
即使浏览器尝试渲染 0.5px,由于像素网格限制,结果往往呈现为模糊、发虚或宽度不均,无法达到理想的细线效果。
14.4 处理方式
这是最通用、兼容性最好的方案。利用 ::after 伪元素绘制 1px 边框,再通过 transform: scale(0.5) 缩放到一半高度/宽度,从而在视觉上呈现 0.5px 效果。
.border-0-5 {
position: relative;
}
.border-0-5::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
border: 1px solid #ccc;
transform: scale(0.5);
transform-origin: 0 0;
box-sizing: border-box;
}
十五 原型和原型链
15.1 原型的基本概念
- 原型就是构造函数的
prototype属性指向的那个对象,它用来存放实例共享的属性和方法。
15.2 prototype
- 每个对象(包括函数对象)都有一个
__proto__属性,指向它的原型(即构造函数的prototype对象)
15.3 原型链
-
描述:当访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript 会沿着这个链接往上找,也就是去它的原型对象上找;如果原型对象也没有,就继续找原型的原型……
这样一层一层向上查找,直到找到属性或者到达null(原型链的终点)。
这条由__proto__串联起来的链条,就叫做原型链。 -
简单说:原型链是 JavaScript 中对象查找属性和方法的链条机制。
十六 闭包(防抖和节流)
16.1 什么是闭包
函数嵌套函数,内部函数引用了外部函数的变量,就形成了闭包
16.2 防抖
/**
* 防抖函数
* @param {Function} fn 需要防抖的函数
* @param {number} delay 延迟时间(毫秒)
* @param {boolean} immediate 是否立即执行(true: 第一次触发立即执行,后续触发等待延迟结束才能再次触发)
* @returns {Function} 防抖处理后的函数
*/
function debounce(fn, delay, immediate = false) {
let timer = null; // 闭包保存定时器ID
return function(...args) {
const context = this;
// 清除之前的定时器,重新计时
if (timer) clearTimeout(timer);
if (immediate) {
// 立即执行版本:第一次触发立即执行,之后延迟期间内触发无效,直到延迟结束才能再次触发
const callNow = !timer; // timer 为 null 表示首次触发
if (callNow) {
fn.apply(context, args);
}
// 设置定时器,延迟结束后重置 timer
timer = setTimeout(() => {
timer = null;
}, delay);
} else {
// 非立即执行:延迟后执行,期间每次触发都重新计时
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
};
}
16.3 节流
时间戳版
/**
* 节流函数 - 时间戳版(立即执行,停止触发后不再执行)
* @param {Function} fn 需要节流的函数
* @param {number} interval 时间间隔(毫秒)
* @returns {Function} 节流处理后的函数
*/
function throttle(fn, interval) {
let lastTime = 0; // 闭包保存上次执行时间戳
return function(...args) {
const now = Date.now();
// 如果距离上次执行的时间差大于等于间隔,则执行
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now; // 更新上次执行时间
}
};
}
定时器版
function throttleWithTimer(fn, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
十七 for of 和for in
17.1 for of
- for in 循环对象或数组,值为对象的属性或数组的下标,会包括原型链中的属性
const arr = [1,2,3]
const obj = {
a: 'wan',
b: 'sss'
}
for(let key in arr){
console.log(key) // 0,1,2
}
for(let key in obj){
console.log(key) // a,b
}
17.2 for in
- 不能直接遍历 对象。 for of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构,可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
const arr = [1,2,3]
const obj = {
a: 'wan',
b: 'sss'
}
for(let item of arr){
console.log(key) // 1,2,3
}
for(let item of obj){
console.log(item) // Uncaught TypeError: obj is not iterable
}
十八 Promise
18.1 Promise 的状态
一个 Promise 对象处于以下三种状态之一:
- pending(进行中) :初始状态,既没有兑现,也没有拒绝。
- fulfilled(已成功) :操作成功完成,调用
resolve时进入该状态,并带有一个结果值。 - rejected(已失败) :操作失败,调用
reject时进入该状态,并带有一个错误原因。
状态不可逆:一旦从 pending 变为 fulfilled 或 rejected,就不能再改变。
18.2. 静态方法
- Promise.resolve(value)
返回一个以给定值解析后的 Promise。如果 value 是 Promise,则直接返回它。
- Promise.reject(reason)
返回一个以给定原因拒绝的 Promise。
- Promise.all(iterable)
接收一个 Promise 数组(或任何可迭代对象),当所有 Promise 都成功时,返回一个 Promise,结果数组按顺序对应每个 Promise 的结果;如果任一失败,则立即拒绝,并返回第一个失败的原因。
Promise.all([promise1, promise2, promise3])
.then(([res1, res2, res3]) => {
// 所有都成功
})
.catch(err => {
// 任何一个失败
});
- Promise.allSettled(iterable)
等待所有 Promise 都完成(无论成功或失败),返回一个 Promise,结果是一个对象数组,每个对象描述每个 Promise 的结果(status 和 value/reason)。
- Promise.race(iterable)
返回一个 Promise,一旦迭代器中的某个 Promise 解决或拒绝,返回的 Promise 就会随之解决或拒绝(以最快的那个为准)。
- Promise.any(iterable)
返回一个 Promise,一旦迭代器中的任意一个 Promise 成功,就返回该成功的结果;如果所有 Promise 都失败,则返回一个包含所有失败原因的 AggregateError。
十九 fetch、ajax、axois
19.1 对比
| 特性 | Ajax (XMLHttpRequest) | Fetch API | Axios |
|---|---|---|---|
| 底层实现 | 浏览器原生对象 | 浏览器原生 API | 封装 XMLHttpRequest / http |
| 语法风格 | 回调/事件 | Promise | Promise |
| 数据自动解析 | 需手动 JSON.parse | 手动调用 response.json() | 自动解析 JSON |
| HTTP 错误状态处理 | 需手动判断 status | 需手动检查 response.ok | 自动进入 catch |
| 请求拦截/响应拦截 | 需自己封装 | 不支持原生 | 支持 |
| 取消请求 | 可通过 xhr.abort() | 需借助 AbortController | 支持(CancelToken/AbortController) |
| 上传进度 | 支持 xhr.upload.onprogress | 不支持原生(可用流) | 支持 onUploadProgress |
| 超时设置 | xhr.timeout | AbortController + 定时器 | 内置 timeout 配置 |
| 携带 Cookie | 默认携带 | 需设置 credentials | 默认携带(可配置) |
| 浏览器兼容性 | 所有浏览器 | IE 不支持 | IE 不支持(需 polyfill) |
| 使用场景 | 需兼容老旧浏览器 | 现代浏览器简单请求 | 绝大多数项目首选 |
二十 CDN
20.1 CDN 的核心原理
-
分布式节点
CDN 服务商在全球各地部署服务器(称为边缘节点)。当用户请求资源时,CDN 会通过智能调度,将请求路由到距离用户最近、网络状况最好的节点。 -
缓存与回源
- 如果节点上已有缓存资源,直接返回给用户(命中)。
- 如果没有缓存,节点会向源站(原始服务器)请求资源,获取后缓存到本地,并返回给用户。后续相同区域的用户请求就能直接从缓存获取。
-
智能调度
通过 DNS 解析、Anycast 等技术,CDN 能动态选择最优节点,避免跨运营商、跨国访问的延迟。
20.2 CDN 的主要优点
| 优点 | 说明 |
|---|---|
| 加速访问 | 用户就近获取资源,减少网络往返时间(RTT),大幅提升首屏加载速度。 |
| 降低源站压力 | 大部分请求由 CDN 节点处理,源站只需处理少量回源请求,节省带宽和服务器资源。 |
| 高可用与容灾 | 若某个节点或源站故障,CDN 可自动切换到其他节点,保证服务不中断。 |
| 安全防护 | 许多 CDN 提供 DDoS 防护、WAF(Web 应用防火墙)等安全能力,隐藏源站真实 IP。 |
| 节省成本 | 通过减少源站带宽消耗,降低运营成本。 |
二十一 Less、Sass、Stylus、PostCSS 对比
| 特性 | Less | Sass (SCSS) | Stylus | PostCSS |
|---|---|---|---|---|
| 语法风格 | 类 CSS,易上手 | SCSS 类 CSS;Sass 缩进语法 | 灵活,可缩进/无括号,支持 CSS 风格 | 标准 CSS + 插件扩展 |
| 变量 | @变量 | $变量 | 变量 = value 或 $变量 | 通过插件(如 postcss-custom-properties)支持 CSS 变量 |
| 嵌套 | 原生支持(& 父选择器) | 原生支持(& 父选择器) | 原生支持(& 父选择器) | 通过插件(postcss-nested 或 postcss-nesting) |
| 混合 (Mixin) | 原生支持:.mixin() | 原生支持:@mixin / @include | 原生支持:类似函数 | 通过插件(如 postcss-mixins) |
| 继承 (Extend) | 不支持,可通过类合并模拟 | 原生支持:@extend | 原生支持:@extend | 通过插件(postcss-extend) |
| 控制指令 (条件/循环) | 无原生,可用递归模拟 | 强大:@if、@for、@each、@while | 强大:if、unless、for in、while | 通过插件(如 postcss-each、postcss-for、postcss-if) |
| 函数 | 内置函数 + 自定义函数 | 内置函数 + 自定义函数(@function) | 内置函数 + 自定义函数 | 通过插件(如 postcss-functions) |
| 导入 (Import) | @import 编译时合并 | @import 编译时合并 | @import 编译时合并 | 通过插件 postcss-import 合并 |
| 模块化 | 支持部分文件(_) | 支持部分文件(_) | 支持部分文件(_) | 插件支持 |
| 自动添加前缀 | 本身不支持,需配合 Autoprefixer | 本身不支持,需配合 Autoprefixer | 本身不支持,需配合 Autoprefixer | 原生插件 autoprefixer,最常用 |
| 压缩优化 | 支持输出压缩(--compress) | 支持输出压缩(style: compressed) | 支持输出压缩 | 插件 cssnano,功能更强大 |
| 未来 CSS 语法支持 | 无 | 无 | 无 | 插件 postcss-preset-env,支持降级 |
| 浏览器端实时编译 | ✅(通过 less.js) | ❌ | ❌ | ❌ |
| Source Map 支持 | ✅ | ✅ | ✅ | ✅ |
| 扩展性(插件生态) | 插件较少 | 插件一般 | 插件较少 | 插件生态极其丰富(400+) |
| 编译环境 | Node / 浏览器 | Dart Sass(Node / 原生) | Node | Node(配合构建工具) |
| 性能(编译速度) | 中等 | 中等(Dart Sass 较慢) | 较快 | 极快(轻量核心) |
| 学习曲线 | 低,接近 CSS | 低(SCSS)/ 中(Sass) | 中(语法灵活) | 中(需了解插件配置) |
| 社区规模与生态 | 中(Bootstrap 4 及以前) | 最大(Bootstrap 5、Ant Design、Element UI 等) | 较小 | 极大(现代构建工具标配) |
| 主要用途 | 轻量级样式增强,小型项目 | 大型项目全功能预处理,组件库 | 灵活简洁的预处理 | 现代化 CSS 工作流(编译、优化、兼容) |
二十二 WebSocket 与 SSE
| 对比维度 | WebSocket | SSE (Server-Sent Events) |
|---|---|---|
| 通信方向 | 全双工:客户端与服务端可同时主动发送消息 | 单向:仅服务端可推送消息;客户端需额外请求发送数据 |
| 协议 | ws:// / wss://(独立于 HTTP 的协议) | 标准 HTTP/HTTPS(text/event-stream 内容类型) |
| 连接建立 | HTTP 握手升级(Upgrade: websocket),随后切换协议 | 普通 GET 请求,服务端保持连接不关闭 |
| 数据格式 | 文本(UTF-8)或二进制(如 ArrayBuffer、Blob) | 仅文本(UTF-8);二进制需自行编码(如 Base64) |
| 自动重连 | ❌ 无内置机制,需手动监听 onclose / onerror 并实现重连逻辑 | ✅ 浏览器自动重连,可通过 retry: 字段控制间隔 |
| 连接状态检测 | 需应用层实现心跳(ping/pong)或依赖 TCP keepalive | 连接中断时浏览器自动触发重连,也可通过发送自定义心跳保持 |
| 浏览器支持 | 所有现代浏览器(IE10+) | 所有现代浏览器(IE 不支持,Edge 已支持);支持度略窄但基本可用 |
| HTTP 兼容性 | 部分代理/负载均衡需显式支持 Upgrade;长连接可能导致负载不均 | 完美兼容 HTTP 基础设施,CDN、反向代理原生支持(需禁用缓冲) |
| 并发连接数 | 无浏览器同源限制(受服务器资源限制) | HTTP/1.1 下同域名最多 6 个连接;HTTP/2 后基本无此问题 |
| 消息边界 | 基于帧(frame),每条消息独立 | 基于行(data: 等字段),通过空行分隔事件 |
| 资源开销 | 协议头开销小(建立后仅数据帧),但需维护全双工连接 | 与普通 HTTP 长连接相当,服务端实现更轻量(无复杂帧解析) |
| 服务端实现复杂度 | 较高:需处理帧解析、心跳、连接池、协议升级等 | 较低:仅需设置响应头并定时写入数据流 |
| 客户端 API 复杂度 | 较复杂:事件监听 + 手动重连 + 二进制处理 | 简单:EventSource 接口,自动重连,仅需处理 onmessage |
| 典型应用场景 | 聊天、在线游戏、协同编辑、金融交易(双向频繁交互) | 实时通知、股票/行情推送、日志流、仪表盘更新(单向推送) |
| 安全策略 | 同源策略限制较弱(可跨域,需服务端配置 CORS 支持) | 默认受同源策略限制,跨域需配置 Access-Control-Allow-Origin |
| 调试难度 | 协议非 HTTP,抓包需解析帧,调试相对复杂 | 纯 HTTP 流,可用 curl 或浏览器 DevTools 直接查看 |
22.1、API 速查表
22.1.1 WebSocket API
| 类型 | 名称 | 说明 |
|---|---|---|
| 构造函数 | new WebSocket(url[, protocols]) | 创建 WebSocket 连接 |
| 属性 | readyState | 连接状态:0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED |
bufferedAmount | 已排队但未发送的字节数 | |
protocol | 服务器选中的子协议 | |
| 方法 | send(data) | 发送数据(字符串、Blob、ArrayBuffer、TypedArray) |
close([code[, reason]]) | 关闭连接(可选状态码和原因) | |
| 事件 | onopen | 连接成功时触发 |
onmessage | 收到消息时触发,event.data 为数据 | |
onclose | 连接关闭时触发,event.code/event.reason 可用 | |
onerror | 发生错误时触发 |
22.1.2 SSE (EventSource) API
| 类型 | 名称 | 说明 |
|---|---|---|
| 构造函数 | new EventSource(url[, options]) | 创建 SSE 连接(options 可配置 withCredentials) |
| 属性 | readyState | 0=CONNECTING, 1=OPEN, 2=CLOSED |
url | 连接的 URL | |
withCredentials | 是否携带凭证 | |
| 方法 | close() | 关闭连接(不会自动重连) |
| 事件 | onopen | 连接成功时触发 |
onmessage | 收到默认事件(event: message 或无 event 字段)时触发 | |
onerror | 连接失败或中断时触发,浏览器会自动尝试重连 | |
addEventListener(type, listener) | 监听自定义事件类型(如后端 event: ping) |
|
22.2 WebSocket 封装 ( 支持自动重连、心跳、消息队列 )
/**
* 简单的 WebSocket 客户端封装
* 功能:连接、发送、关闭、事件监听
*/
class SimpleWebSocket {
/**
* @param {string} url WebSocket 服务地址
* @param {Object} [options] 配置项
* @param {boolean} [options.binary=false] 是否接收二进制数据(ArrayBuffer)
*/
constructor(url, options = {}) {
this.url = url;
this.options = { binary: false, ...options };
this.ws = null;
this.handlers = {
open: [],
message: [],
close: [],
error: [],
};
}
/**
* 建立连接
*/
connect() {
this.ws = new WebSocket(this.url);
if (this.options.binary) {
this.ws.binaryType = 'arraybuffer';
}
this.ws.onopen = (e) => this._trigger('open', e);
this.ws.onmessage = (e) => this._trigger('message', e.data);
this.ws.onclose = (e) => this._trigger('close', e);
this.ws.onerror = (e) => this._trigger('error', e);
}
/**
* 发送消息
* @param {string|ArrayBuffer|Blob} data
*/
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
console.warn('WebSocket 未打开,消息发送失败');
}
}
/**
* 关闭连接
* @param {number} [code=1000]
* @param {string} [reason]
*/
close(code = 1000, reason) {
if (this.ws) {
this.ws.close(code, reason);
}
}
/**
* 监听事件
* @param {string} event 事件名:open, message, close, error
* @param {Function} callback
*/
on(event, callback) {
if (this.handlers[event]) {
this.handlers[event].push(callback);
}
}
/**
* 触发事件
* @private
*/
_trigger(event, data) {
this.handlers[event].forEach(cb => cb(data));
}
}
const ws = new SimpleWebSocket('ws://localhost:3000');
ws.on('open', () => console.log('连接成功'));
ws.on('message', (msg) => console.log('收到消息:', msg));
ws.on('close', (e) => console.log('连接关闭', e.code));
ws.on('error', (err) => console.error('错误:', err));
ws.connect();
ws.send('Hello');
22.3 SSE 封装 (支持自动重连(自带)、心跳、事件类型)
/**
* 简单的 SSE 客户端封装
* 功能:连接、关闭、事件监听(包括自定义事件)
*/
class SimpleSSE {
/**
* @param {string} url SSE 服务地址
* @param {Object} [options] 配置项
* @param {boolean} [options.withCredentials=false] 是否携带凭证
*/
constructor(url, options = {}) {
this.url = url;
this.options = { withCredentials: false, ...options };
this.source = null;
this.handlers = {
open: [],
message: [],
error: [],
};
this.customEvents = new Map(); // 存储自定义事件监听器
}
/**
* 建立连接
*/
connect() {
this.source = new EventSource(this.url, {
withCredentials: this.options.withCredentials,
});
this.source.onopen = (e) => this._trigger('open', e);
this.source.onmessage = (e) => this._trigger('message', e.data);
this.source.onerror = (e) => this._trigger('error', e);
}
/**
* 关闭连接
*/
close() {
if (this.source) {
this.source.close();
this.source = null;
}
}
/**
* 监听事件
* @param {string} event 事件名:open, message, error 或自定义事件类型
* @param {Function} callback
*/
on(event, callback) {
if (event === 'open' || event === 'message' || event === 'error') {
this.handlers[event].push(callback);
} else {
// 自定义事件
if (!this.customEvents.has(event)) {
this.customEvents.set(event, []);
}
this.customEvents.get(event).push(callback);
// 如果已经连接,立即添加原生监听
if (this.source) {
this.source.addEventListener(event, callback);
}
}
}
/**
* 内部触发事件
* @private
*/
_trigger(event, data) {
this.handlers[event].forEach(cb => cb(data));
}
}
const sse = new SimpleSSE('/events');
sse.on('open', () => console.log('SSE 连接成功'));
sse.on('message', (data) => console.log('收到推送:', data));
sse.on('error', (err) => console.error('SSE 错误:', err));
sse.on('ping', (e) => console.log('自定义 ping 事件:', e.data));
sse.connect();
// 主动关闭
setTimeout(() => sse.close(), 10000);
二十三 强缓存 & 协商缓存
23.1 强缓存 ( expires、cache-control)
23.1.1 expires (逐渐被取代)
一个绝对时间(GMT),当系统时间超过该时间后缓存失效。由于客户端时间可能不准,目前已逐渐被 Cache-Control 取代,但为了兼容性仍会保留。
app.use(async (ctx) => {
const url = ctx.request.url
if (url === '/') {
// 访问根路径返回index.html
ctx.set('Content-Type', 'text/html')
ctx.body = await parseStatic('./index.html')
} else {
const filePath = path.resolve(__dirname, `.${url}`)
// 设置类型
ctx.set('Content-Type', parseMime(url))
// 设置 Expires 响应头
const time = new Date(Date.now() + 30000).toUTCString()
ctx.set('Expires', time)
// 设置传输
ctx.body = await parseStatic(filePath)
}
})
23.1.2 cache-control (优先级更高)
max-age=<seconds>:资源被缓存的最大时间(相对请求时间)。public/private:是否允许被中间代理缓存。no-cache:强制进行协商缓存(实际并非不缓存,而是每次使用前要验证)。no-store:真正禁止任何缓存
app.use(async (ctx) => {
const url = ctx.request.url
if (url === '/') {
// 访问根路径返回index.html
ctx.set('Content-Type', 'text/html')
ctx.body = await parseStatic('./index.html')
} else {
const filePath = path.resolve(__dirname, `.${url}`)
// 设置类型
ctx.set('Content-Type', parseMime(url))
// 设置 Cache-Control 响应头
ctx.set('Cache-Control', 'max-age=30')
// 设置传输
ctx.body = await parseStatic(filePath)
}
})
23.2 协缓存 (Last-Modified,If-Modified-Since 和 Etag,If-None-Match)
浏览器携带缓存的标识去向服务器“询问”资源是否过期,服务器根据标识决定返回新资源(200)还是告诉浏览器继续使用缓存(304)
| 响应头(服务器 → 浏览器) | 请求头(浏览器 → 服务器) | 说明 |
|---|---|---|
ETag: "abc123" | If-None-Match: "abc123" | 资源内容的唯一标识(通常是内容哈希或版本号),优先级最高 |
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT | If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT | 资源最后修改时间(精确到秒) |
二十四 浏览器从输入URL到渲染出页面都又哪些步骤
24.1 解析Url
-
URL 主要由
协议、主机、端口、路径、查询参数、锚点6部分组成! -
输入URL后,浏览器会解析出协议、主机、端口、路径等信息,并构造一个HTTP请求
-
浏览器发送请求前,根据请求头的
expires和cache-control判断是否命中(包括是否过期)强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。如果没有命中,则进入下一步。 -
没有命中强缓存规则,浏览器会发送请求,根据请求头的
If-Modified-Since和If-None-Match判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。 -
如果前两步都没有命中,则直接从服务端获取资源。
24.2 浏览器缓存
- 强缓存 (
Expires和Cache-Control) - 协商缓存 (
Last-Modified 响应头和If-Modified-Since 请求头,ETag 响应头和If-None-Match 请求头)
24.3 DNS域名解析
浏览器需要知道服务器在哪。它会从浏览器 DNS 缓存、操作系统 Hosts 文件、本地 DNS 服务器逐级查找,最终将域名(如 www.baidu.com)解析为对应的 IP 地址。
24.3.1 DNS负载均衡
DNS还有负载均衡的作用,现在很多网站都有多个服务器,当一个网站访问量过大的时候,如果所有请求都请求在同一个服务器上,可能服务器就会崩掉,这时候就用到了DNS负载均衡技术,当一个网站有多个服务器地址时,在应答DNS查询的时候,DNS服务器会对每个查询返回不同的解析结果,也就是返回不同的IP地址,从而把访问引导到不同的服务器上去,来达到负载均衡的目的。例如可以根据每台机器的负载量,或者该机器距离用户的地理位置距离等等条件。
24.3.2 DNS预解析
大型网站,有多个不同服务器资源的情况下,都可采取DNS预解析,提前解析,减少页面卡顿。!
24.4 TCP链接
- SYN:同步序列号(Synchronize),用于建立连接时初始化序列号。
- ACK:确认(Acknowledgment),表示确认号字段有效。
- seq:序列号(Sequence Number),标识本报文段数据的第一个字节的序号。
- ack:确认号(Acknowledgment Number),表示期望收到的下一个字节的序号,同时也隐含了之前的数据都已正确接收。
24.4.1 三次握手的具体步骤
假设客户端(浏览器)要连接服务器上的某个端口(如 80)。
1. 第一次握手:客户端 → 服务器(SYN)
客户端向服务器发送一个 TCP 报文段,其中:
- SYN = 1,表示这是一个连接请求报文。
- seq = x(x 是一个随机生成的初始序列号,比如 12345)。
- ACK = 0(因为此时还没有收到对方的任何数据,所以确认号字段无效)。
客户端发送后,进入 SYN-SENT 状态,等待服务器的确认。
2. 第二次握手:服务器 → 客户端(SYN + ACK)
服务器收到该请求报文后,如果同意建立连接,会返回一个报文段,其中:
- SYN = 1,ACK = 1,表示这是一个连接确认报文,同时携带了自己的同步序列号。
- seq = y(服务器随机生成的初始序列号)。
- ack = x + 1,表示期望收到客户端下一个字节的序号为 x+1,同时也确认了客户端的 SYN 报文(即客户端发来的 seq=x 的报文已被正确接收)。
服务器发送后,进入 SYN-RECEIVED 状态。
3. 第三次握手:客户端 → 服务器(ACK)
客户端收到服务器的 SYN+ACK 报文后,需要向服务器发送一个确认报文:
- ACK = 1。
- seq = x + 1(客户端发送的第一个数据字节的序号,因为之前用了 seq=x 发送 SYN,现在这个报文是确认报文,通常不携带数据,所以 seq = x+1)。
- ack = y + 1,表示期望收到服务器下一个字节的序号为 y+1,同时也确认了服务器的 SYN 报文。
客户端发送后,进入 ESTABLISHED 状态。服务器收到这个 ACK 报文后,也进入 ESTABLISHED 状态。至此,连接建立完成,双方可以开始传输数据。
补充:三次握手与 HTTPS
HTTPS 在 TCP 三次握手之后,还会额外进行一次 TLS 握手(证书验证、密钥交换等),这需要更多的往返(通常 2-4 次),因此 HTTPS 的首次连接会比 HTTP 慢一些。
24.5 HTTP
1. HTTP 是什么?
HTTP(HyperText Transfer Protocol,超文本传输协议)是一个应用层协议,用于在 Web 上传输超文本(HTML、图片、视频等)。它采用 客户端-服务器模型,浏览器作为客户端向服务器发起请求,服务器返回响应。
主要特点:
- 无状态:服务器不会记住之前的请求,每个请求都是独立的(后来通过 Cookie 等机制弥补)。
- 基于 TCP:默认使用 TCP 的 80 端口(HTTPS 用 443),依赖 TCP 的可靠性。
- 简单灵活:报文结构简单,易于扩展。
2. HTTP 版本演进
| 版本 | 诞生 | 特点 |
|---|---|---|
| HTTP/0.9 | 1991 | 只有 GET 方法,只能返回 HTML,没有头部信息,请求结束即断开连接。 |
| HTTP/1.0 | 1996 | 引入 POST、HEAD 方法;支持头部(Header)、状态码、内容类型;但每个请求/响应都新建 TCP 连接(开销大)。 |
| HTTP/1.1 | 1997(至今仍广泛使用) | 持久连接(默认 Keep-Alive)、管线化、Host 头(支持虚拟主机)、更丰富的缓存控制、分块传输等。 |
| HTTP/2 | 2015 | 二进制分帧、多路复用(解决队头阻塞)、服务器推送、头部压缩。 |
| HTTP/3 | 2022(逐步普及) | 基于 UDP 的 QUIC 协议,彻底解决 TCP 队头阻塞,连接迁移,0-RTT 等。 |
3. HTTP 报文结构
HTTP 报文分为 请求报文 和 响应报文,都由三部分组成:起始行、头部、正文(可选)。
3.1 请求报文
GET /index.html HTTP/1.1 // 请求行:方法 + 路径 + 版本
Host: www.example.com // 请求头
User-Agent: Mozilla/5.0
Accept: text/html
// 空行(CRLF),分隔头部和正文
// 正文(GET 通常没有)
3.2 响应报文
HTTP/1.1 200 OK // 状态行:版本 + 状态码 + 原因短语
Content-Type: text/html // 响应头
Content-Length: 1234
// 空行
<html>...</html> // 正文
4. HTTP 方法(HTTP Verbs)
| 方法 | 作用 | 是否幂等 | 是否安全 |
|---|---|---|---|
| GET | 获取资源 | 是 | 是 |
| POST | 提交数据(创建/修改) | 否 | 否 |
| PUT | 替换资源(全量更新) | 是 | 否 |
| PATCH | 部分更新资源 | 否 | 否 |
| DELETE | 删除资源 | 是 | 否 |
| HEAD | 同 GET,只返回头部 | 是 | 是 |
| OPTIONS | 询问支持的请求方法(CORS 预检) | 是 | 是 |
幂等:多次执行效果相同;安全:不应改变服务器状态(GET/HEAD/OPTIONS 应安全)。
5. HTTP 状态码(Status Codes)
1xx:信息性响应
100 Continue:服务器已收到请求头,客户端可继续发送正文。
2xx:成功
200 OK:一切正常。204 No Content:成功但无返回内容(如 PUT 成功)。206 Partial Content:范围请求,用于断点续传。
3xx:重定向
301 Moved Permanently:永久重定向。302 Found:临时重定向。304 Not Modified:资源未修改,可使用缓存(协商缓存)。
4xx:客户端错误
400 Bad Request:请求语法错误。401 Unauthorized:未认证。403 Forbidden:禁止访问。404 Not Found:资源不存在。405 Method Not Allowed:方法不允许。429 Too Many Requests:请求过于频繁。
5xx:服务器错误
500 Internal Server Error:服务器内部错误。502 Bad Gateway:网关错误。503 Service Unavailable:服务不可用(如过载)。504 Gateway Timeout:网关超时。
24.6 浏览器渲染页面
24.6.1 渲染过程概览
从接收到 HTML 字节流到最终显示,主要经历以下步骤:
- 解析 HTML → 构建 DOM 树
- 解析 CSS → 构建 CSSOM 树
- 结合 DOM + CSSOM → 构建 渲染树(Render Tree)
- 布局(Layout / Reflow)→ 计算元素几何位置
- 分层(Layer)→ 为特定节点生成图层
- 绘制(Paint)→ 生成绘制指令列表
- 分块与光栅化(Rasterization)→ 将绘制指令转为像素位图
- 合成(Composite)→ 将各图层合成为最终屏幕图像
24.6.2 详细步骤解析
1. 构建 DOM 树
- 浏览器从网络层(或缓存)拿到 HTML 字节流。
- 字节流经过解码、词法分析、语法分析,转换为文档对象模型(DOM) 。
- DOM 是一个树形结构,每个节点对应一个 HTML 元素或文本。
- 关键点:遇到
<script>标签(尤其是没有async或defer的)会暂停 DOM 解析,去下载并执行 JS,因为 JS 可能会修改 DOM。CSS 不会阻塞 DOM 解析。
2. 构建 CSSOM 树
- 解析 CSS(包括外部样式表、内联样式、
<style>标签)生成 CSS 对象模型(CSSOM) 。 - CSSOM 也是一棵树,描述了每个节点的样式信息(继承、层叠规则)。
- 关键点:CSS 不会阻塞 DOM 解析,但会阻塞渲染(因为渲染树需要同时拥有 DOM 和 CSSOM)。
3. 构建渲染树
- DOM 树和 CSSOM 树合并,生成渲染树。
- 渲染树只包含可见节点(如
display: none的节点不会被包含,visibility: hidden会被包含但不可见,head标签等不包含)。 - 并为每个可见节点计算出最终样式(通过继承和层叠)。
4. 布局(Layout / Reflow)
- 有了渲染树,浏览器需要知道每个节点在屏幕上的精确位置和尺寸。
- 这个过程称为“布局”或“重排”。从根节点开始,递归计算所有盒模型的几何信息(宽、高、x、y 坐标)。
- 布局结果是一个包含位置和尺寸信息的“盒”模型树。
5. 分层(Layer)
-
为了优化后续绘制和合成,浏览器会将一些节点提升为独立的图层(Layer)。
-
通常以下情况会创建独立图层:
- 有 3D 或透视变换(
transform: translateZ(0)、will-change) - 使用
<video>、<canvas>元素 - 使用 CSS 动画或过渡
- 有明确
z-index且处于独立层叠上下文
- 有 3D 或透视变换(
-
分层让浏览器在后续合成时只更新变化的部分,提高性能。
6. 绘制(Paint)
- 每个图层都会生成一系列的绘制指令(如“画一个红色矩形,宽100px,高50px,位置(10,20)”)。
- 绘制指令记录了绘制该图层所需的所有步骤。
7. 分块与光栅化(Rasterization)
- 将图层的绘制指令交给光栅化线程池,转换为像素位图(bitmap)。
- 为了优化首屏速度,浏览器通常只光栅化视口附近的区块(tiles),后续滚动时再动态光栅化其他部分。
- 光栅化利用 GPU 加速(通过 OpenGL / Vulkan / Metal 等)。
8. 合成(Composite)
- 所有图层的光栅化完成后,合成线程将各图层按照正确的顺序、透明度、变换等合成为一张完整的屏幕图像。
- 合成过程也由 GPU 完成,速度非常快。
- 最终图像通过显示系统输出到屏幕。
24.7 断开tcp连接
TCP 是全双工通信,每个方向必须独立关闭。
- 第一次挥手:客户端发送 FIN,表示不再发送数据,但还能接收数据。
- 第二次挥手:服务器回复 ACK,表示已收到客户端的 FIN,但服务器可能还有数据要发送。
- 第三次挥手:服务器发送 FIN,表示不再发送数据。
- 第四次挥手:客户端回复 ACK,确认收到服务器的 FIN。
因此,关闭两个方向需要至少四次交互。如果服务器在收到 FIN 后没有数据要发送,可以将第二次和第三次挥手合并,但仍然是四次(合并为 FIN+ACK)。
二十五 GET vs POST
| 对比维度 | GET | POST |
|---|---|---|
| 语义 | 获取资源 | 提交数据,创建/修改资源 |
| 幂等性 | 是(多次请求效果相同) | 否(多次提交可能创建多个资源) |
| 安全性 | 安全(不应改变服务器状态) | 不安全(会改变状态) |
| 参数传递 | 在 URL 中(查询字符串) | 在请求正文中(Body) |
| 缓存 | 可被浏览器/CDN 主动缓存 | 通常不缓存,除非明确指定 |
| 长度限制 | URL 长度受限(浏览器限制,约 2KB) | 理论上无限制(受服务器配置限制) |
| TCP 包发送 | 通常一次发送 | 可能分两次发送(先发 Header,后发 Body) |
| 历史记录 | 参数会保留在历史记录中 | 参数不会保留 |
| 编码类型 | 仅支持 URL 编码 | 支持多种(form-data、json 等) |
二十六 HTTP 与 HTTPS 区别
| 维度 | HTTP | HTTPS |
|---|---|---|
| 协议 | 明文传输 | 加密传输(SSL/TLS) |
| 端口 | 80 | 443 |
| 安全性 | 无加密,易窃听、篡改、冒充 | 机密性、完整性、身份认证 |
| 证书 | 不需要 | 需要 CA 签发的数字证书(付费或免费) |
| 性能 | 较快(无加解密开销) | 首次连接需 TLS 握手(1-2 RTT),后续有对称加密开销,但现代硬件影响很小 |
| SEO | 搜索引擎会降权 | 搜索引擎偏好,Chrome 等浏览器标记“不安全” |
| 连接过程 | TCP 三次握手 → HTTP 通信 | TCP 三次握手 → TLS 握手(证书验证、密钥交换) → 加密的 HTTP 通信 |
二十七 跨域
跨域是 Web 开发中经常遇到的问题。要理解跨域,首先要明白同源策略(Same-Origin Policy)——浏览器出于安全考虑,限制来自一个源的脚本与另一个源的资源进行交互。同源是指协议 + 域名 + 端口三者完全相同。
| 方式 | 原理 | 适用场景 | 关键点 |
|---|---|---|---|
| CORS | 服务器添加 Access-Control-Allow-Origin 等头 | 所有 API 请求(推荐) | 需后端配置;简单请求直接放行,复杂请求先 OPTIONS 预检;携带凭证时 Origin 不能为 * |
| JSONP | <script> 不受同源限制,通过回调函数获取数据 | 仅 GET 请求,兼容老旧浏览器 | 只支持 GET;不安全;需后端配合返回函数调用 |
| 代理(Proxy) | 同源请求发给代理服务器,由代理转发 | 开发环境(webpack/vite)或生产(nginx) | 前端无感知;无需后端改代码;需配置代理服务 |
| postMessage | 跨窗口(iframe、弹出窗)通过消息事件通信 | iframe 嵌套、多窗口交互 | 需双方监听 message 事件并验证 origin;不能用于普通 AJAX |
| WebSocket | 协议本身无视同源策略 | 实时双向通信 | 需服务端支持 WS/WSS;适合长连接 |
| document.domain | 将子域设为同一主域 | 同主域下的子域间(如 a.example.com ↔ b.example.com) | 仅限同主域;现代浏览器限制严格;需双方显式设置 |
二十八 前端路由的 Hash 模式 vs History 模式
| 对比项 | Hash 模式 | History 模式 |
|---|---|---|
| 原理 | 利用 URL 中的 #(hash)部分,# 后的内容变化不会触发页面刷新,浏览器通过 hashchange 事件监听变化。 | 利用 HTML5 History API(pushState、replaceState、popstate)直接修改 URL 路径,不刷新页面。 |
| URL 示例 | https://example.com/#/user/123 | https://example.com/user/123 |
| 美观性 | 有 #,略显冗余 | 美观,与正常 URL 一致 |
| 兼容性 | 所有浏览器都支持 | IE10+,现代浏览器均支持 |
| 服务端配置 | 不需要特殊配置,因为 # 后的内容不会发送到服务端 | 必须配置,否则直接访问路径会 404(需将所有路由指向 index.html) |
| 刷新行为 | 刷新时,# 后内容不会被发送到服务端,服务端始终返回同一页面,前端再根据 hash 渲染对应组件 | 刷新时会向服务端请求当前路径,若服务端未配置 fallback 则 404 |
| 实现 | window.location.hash + hashchange 事件 | history.pushState() / replaceState() + popstate 事件 |
| 使用场景 | 简单项目、无需服务端配合、兼容旧浏览器 | 正式线上项目(需配合 nginx 等配置) |