0%

什么是Webpack?

Webpack是一个打包工具,它是基于Node.js的,也就是说它是用JS写的,所以它的速度比较慢,但是它的功能非常强大。而其他的打包工具,比如ESBuild是用Go语言书写的,速度会快很多。

为什么需要使用Webpack呢?
在前端技术日益发展的今天,前端技术极大丰富,各种框架和库层出不穷,比如Angular/React/Vue等,我们的前端项目可能是用不同的框架书写的,可能还要支持typescript,我们还可能使用了不同的CSS预处理器,比如SASS/LESS等,而浏览器是不懂框架的语法的,需要转换成原生JS,浏览器也不懂typescript,也需要转换成JavaScript,也不懂SASS/LESS语法,需要转换成原生的CSS。

此外,如果你使用了新的ES语法,而浏览器此时尚未支持该语法,那么Webpack也可以帮你转换成浏览器支持的语法。

Webpack的主要功能有:

  1. 打包JS、CSS、图片等资源文件。
  2. 代码转换,比如ES6转ES5、SCSS转CSS等。对于比较新的语法,Webpack可以弥补浏览器未实现的部分。
  3. 代码分割,将代码分割成多个文件,按需加载。- Lazy loading,将需要lazy load的模块单独打包成一个文件,当需要的时候再加载。
  4. 代码压缩,减少文件体积。
  5. 代码优化,比如去除无用代码、去除重复代码等。- Tree shaking
  6. Webpack-dev-server - 本地开发服务器,实时刷新。

Webpack中的hash、chunkhash、contenthash有什么区别?

This post includes the Browser DOM related interview questions

Performance optimization

前端性能优化的方法有哪些?

  1. 使用懒加载 - 减少首屏加载时间
  2. 減少包体积 - 提升首页加载速度
  3. 合并API call - 减少网络请求次数
  4. 使用CDN - 加速资源加载速度
  5. 使用缓存 - 减少重复请求

What’s the event loop in Chrome browser?

事件循环的核心流程如下:

  1. 执行同步代码 :
    所有同步代码会被依次执行,并压入调用栈。
    同步代码执行完毕后,调用栈清空。
  2. 检查微任务队列 :
    如果调用栈为空,事件循环会优先检查微任务队列。
    将微任务队列中的任务依次取出并执行,直到微任务队列为空。
  3. 渲染更新 :
    在浏览器环境中,如果需要更新 DOM 或绘制页面,此时会进行渲染。
  4. 检查宏任务队列 :
    当微任务队列为空时,事件循环会从宏任务队列中取出一个任务并执行。
    宏任务执行完毕后,再次回到步骤 2,检查微任务队列。

以下代码输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
console.log('Start'); // 1. 同步代码,立即执行

setTimeout(() => {
console.log('Timeout'); // 5. 宏任务,最后执行
}, 0);

Promise.resolve().then(() => {
console.log('Promise'); // 3. 微任务,优先于宏任务
});

console.log('End'); // 2. 同步代码,立即执行

答案:

1
2
3
4
Start
End
Promise
Timeout

什么是cookie?如何保证cookie的安全性?

我们现不纠结cookie的定义,而是看为什么需要cookie,可能你已经听过这个结论:HTTP是无状态的,也就是说每次请求都是独立的,服务器无法知道这次请求和上次请求是否来自同一个用户。cookie就是为了解决这个问题而诞生的。关于cookie的使用方法,可以看这篇

保证cookie的安全性,主要有以下几个方面:

  1. 设置HttpOnly属性:HttpOnly属性是cookie的一个属性,它的作用是禁止JavaScript访问cookie,这样可以防止XSS攻击。
  2. 设置Secure属性:Secure属性是cookie的一个属性,它的作用是要求cookie只能通过HTTPS协议传输,这样可以防止MITM攻击。
  3. 设置SameSite属性:SameSite属性是cookie的一个属性,它的作用是指定cookie是否可以随着跨域请求发送,这样可以防止CSRF攻击。
  4. 合理设置cookie的过期时间,过期时间够用就好,不要太长。
  5. 不要在cookie中存储敏感信息,如密码等。

以下是使用Express设置cookie的示例代码:

1
2
3
4
5
6
7
8
9
app.get('/set-cookie', function(req, res) {
res.cookie('session_id', 'your-session-id-here', {
httpOnly: true, // 设置HttpOnly标志
secure: true, // 强制HTTPS传输
sameSite: 'strict', // 防止CSRF攻击
maxAge: 3600 // 设置Cookie有效期为1小时.
});
res.send('Cookie has been set');
});

localStorage和sessionStorage的区别是什么?

localStorage和sessionStorage都是HTML5提供的用于在浏览器端存储数据的机制,它们属于Web Storage API的一部分。尽管两者有相似之处,但也存在一些关键区别:

  • 生命周期:
    • localStorage:数据没有过期时间,一旦数据被存储,它将永远保留在那里,除非用户通过浏览器设置手动清除数据,或者使用JavaScript代码显式地删除它。
    • sessionStorage:数据的生命周期与页面会话等同。页面会话在浏览器打开期间一直保持,包括页面重新加载或恢复。一旦浏览器关闭,所有在sessionStorage中保存的数据都会被清除。
  • 作用范围:
    • localStorage:在同一源(协议+域名+端口)下的所有窗口、标签页之间共享数据。这意味着即使你在一个标签页中修改了localStorage中的数据,在同一网站的其他标签页中也能访问到更新后的数据。
    • sessionStorage:对于来自同一源的每个页面,都有其自己的独立存储空间,并且这些存储空间互不干扰。也就是说,不同的标签页或窗口间无法共享sessionStorage中的数据,即使是同一个网站。
  • 使用场景:
    • localStorage适用于需要长期存储的数据,如用户偏好设置、应用状态等。
    • sessionStorage则更适合于临时性的、仅在当前会话期间有用的场景,比如保存表单数据以防止刷新页面时丢失信息。

What’s the benefit of web component?

  1. Cross platform, framework free.

What’s the differences between document.write and innerHTML?

Answer:

  • document.write update the entire page.
  • innerHTML update the content of an element, it can be used at any time.

What’s the output of the following code?

1
2
3
4
5
6
7
8
<body>
<script>
document.body.style = "background-color: blue;";
Promise.resolve().then(() => {
document.body.style = "background-color: red;";
});
</script>
</body>

Answer:
The browser start to render the page after micro task queue is empty in each event loop. So the background color of the body will be red directly(You won’t see the blue background color).

How to create group for options in a select element?

Answer:
You can use optgroup element to group options in a select element.

1
2
3
4
5
6
7
8
9
10
<select>
<optgroup label="China">
<option>Beijing</option>
<option>Dalian</option>
</optgroup>
<optgroup label="America">
<option>Washington</option>
<option>New York</option>
</optgroup>
</select>

Closure

Use case 1: cache data.

In the following example, the inner function getId referenced the variable id in outer function createIdGenerator. The variable id is memorized and each call to generateId will return the next id.

1
2
3
4
5
6
7
8
9
10
11
function createIdGenerator() {
let id = 0;
return function getId() {
return id++;
};
}

const generateId = createIdGenerator();
console.log(generateId()); // 0
console.log(generateId()); // 1
console.log(generateId()); // 2

event loop

prototype

this

this in javascript is a very complex concept, and we need a separated post to elaborate it. Here we just give a brief introduction.

promise

async/await

据说async/await底层是借助Promise和generator来实现的,有待学习。

Primitive wrapper objects

1
2
3
4
5
6
7
8
9
10
11
const s1 = new String('abc');
const s2 = new String('abc');

const s3 = String('abc');
const s4 = String('abc');

console.log(s1 == s2);
console.log(s3 == s4);

console.log(s1 == s3);
console.log(s1 === s3);

Answer:

1
2
3
4
false
true
true
false
  • s1和s2都是object类型,所以它们的引用地址不同,所以s1 == s2返回false
  • s3和s4都是string类型,所以它们的值相同,所以s3 == s4返回true
  • s1是object类型,s3是string类型,比较时先将s1转换为primitive类型,也就是字符串类型,所以s1 == s3返回true
  • s1是object类型,s3是string类型,所以s1 === s3返回false
    关于JavaScript中==的详细规则,请看这里

输出结果是什么?

1
2
const data = new Uint8Array();
console.log(Array.isArray(data));

Answer: false
Uint8ArrayArrayBuffer 对象的一个视图,它表示一个8位无符号整数的数组。Uint8Array 实例不是一个数组,所以 Array.isArray(data) 返回 false

var in for loop.

现在是2025年,ES6已经发布快10年了,除了一些基础前端框架外,在常规的业务代码中,我们不应该再使用var了,因为 var有很多的问题:比如变量提升,比如没有块级作用域。我们从一道面试题来分析var的缺点。

1
2
3
4
5
6
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log(100);

Answer:

1
2
3
4
5
6
100
5
5
5
5
5

分析:

  1. 最后一行代码console.log(100);是同步代码,所以会先输出100。
  2. for循环一共执行5次,每次都会调用setTimeout,但是setTimeout是异步代码,JS引擎会将setTimeout的回调函数放到宏任务队列中,等待执行。
  3. 当前Event loop执行完毕后,会去执行宏任务队列中的任务,这时候setTimeout的回调函数才会被执行,但是这时候i已经变成了5,所以会输出5个5。

所以导致这段代码的问题有二,解决其中任何一个都可以让它输出0, 1, 2, 3, 4。

  1. 使用了var, var是函数作用域,所以所有的循环都共享一个i
  2. 使用了setTimeoutsetTimeout是整个循环结束后才开始执行的!如果我们把setTimeout删除就不会有问题了。以下代码works well!
    1
    2
    3
    for (var i = 0; i < 5; i++) {
    console.log(i);
    }

那么是不是不用setTimeout就没有问题了呢?不是的,我们来看这道题的一个变种:

1
2
3
4
5
6
7
8
9
10
const func = [];

for (var i = 0; i < 5; i++) {
func.push(() => {
console.log(i);
});
}

func.forEach((f) => f());
console.log(100);

这个变种没有使用setTimeout,但是结果依然是5个5。为什么呢?观察这个变种和原始题目我们可以看到,他们共同的特点都是在for循环结束后才执行代码,此时i的值已经是5了。这才是根本原因。

好了,现在我们来看看如何解决这个问题。

方法一,使用let代替var

由于let有块级作用域,所以每次循环都会创建一个新的变量i,而不是像var那样所有的循环都共享一个i。

1
2
3
4
5
6
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log(100);

方法二:将循环变量i参数化

说得再通俗一点,将setTimeout的逻辑单独抽离成一个函数print。这样每次循环的时候,都会将当前的i传递给print函数,这样setTimeout的代码加入宏任务队列时会记住当前的i值。

1
2
3
4
5
6
7
for (var i = 0; i < 5; i++) {
print(i);
}

function print(i) {
setTimeout(() => console.log(i), 1000);
}

如果觉得单独抽离函数不够优雅,可以使用IIFE(立即执行函数表达式)。注意,这里和闭包没有半毛钱关系,IIFE这里只是为了传递参数,也就是上面抽离函数的简化版。

1
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
});
})(i);
}
console.log(100);

方法三:使用setTimeout的第三个参数

这个方法可能很多同学都没有见过,其实setTimeout方法是有第三个参数的,这个参数是用来传递给回调函数的参数的。所以我们可以将当前的i值传递给setTimeout的回调函数。

1
2
3
4
for (var i = 0; i < 5; i++) {
setTimeout((j) => console.log(j), 1000, i);
}
console.log(100);

为了做区分,我把回到函数中的i改成了j,这样就不会混淆了。循环变量i通过setTimeout的第三个参数传递给回调函数。

你学会了吗?

JS中的箭头函数有哪些特点?

特性 优点 缺点
语法简洁 箭头函数语法更短、更简洁,适合简单的回调函数或匿名函数。 复杂逻辑中使用隐式返回或多层嵌套时,可能降低代码可读性和可维护性。
词法作用域绑定 this 箭头函数继承定义时的 this,避免了传统函数中 this 动态绑定的问题。 无法动态绑定 this,在需要动态绑定 this 的场景(如对象方法)中不适合使用。
没有 arguments 对象 避免与普通函数中的 arguments 冲突,可以使用剩余参数(...args)代替。 缺乏 arguments 对象,在需要访问动态参数列表的场景下可能带来不便。
不能用作构造函数 避免误用为构造函数的风险(箭头函数不能通过 new 调用)。 在需要定义构造函数的场景中无法使用箭头函数。
隐式返回 单行箭头函数支持隐式返回,无需使用 return 关键字。 隐式返回在复杂逻辑中可能导致代码难以理解。
调试困难 - 箭头函数的简洁性可能导致代码可读性下降,调试和维护可能会变得困难。
不能用作生成器函数 - 箭头函数不能用作生成器函数(无法与 function* 结合使用)。
适用场景 回调函数、不需要动态绑定 this 的场景、简短的函数逻辑。 需要动态绑定 this 的场景、需要访问 arguments 对象的场景、复杂的逻辑、生成器函数场景。

JS中apply, call, bind的作用和区别是什么?

特性 apply call bind
调用方式 立即调用函数 立即调用函数 返回一个新函数
参数传递 参数以数组或类数组形式传递 参数逐个传递 参数逐个传递,可预填部分参数
适用场景 参数数量不确定时(如动态参数) 参数数量固定时 需要延迟调用或复用绑定函数时
是否修改原函数 否(返回新函数,不修改原函数)

JS中mapweakMap的区别是什么?

特性 Map WeakMap
键的类型 可以是任何类型(包括对象、原始值) 只能是对象或者non-registered symbols
值的类型 可以是任何类型 可以是任何类型
键的引用方式 强引用 弱引用
垃圾回收 键对象不会被垃圾回收 如果键对象没有其他引用,则会被回收
可迭代性 支持(可通过 keys()values() 等方法遍历) 不支持(无法直接遍历或获取大小)
获取大小 支持(通过 size 属性) 不支持
典型应用场景 通用键值对存储 私有数据存储、避免内存泄漏

简单描述一下JS中的事件循环机制?

简单描述一下JS中的Promise

以下代码输出什么?

1
[1, 2, 3].map(parseInt);

答案:[1, NaN, NaN]

解析:先看一下MDN上关于Array.prototype.map的定义,这里我们没有传递第二个参数,所以使用的是下面第一种调用方式。

1
2
map(callbackFn)
map(callbackFn, thisArg)

callbackFn接受三个参数

  • element:当前正在处理的元素
  • index:当前正在处理的元素的索引
  • array:调用map方法的数组

parseInt函数有如下两种形式,由于map传递过来的参数有三个,所以这里会调用第二种形式。

1
2
parseInt(string)
parseInt(string, radix)

所以上面的代码就变成了如下形式

1
[1, 2, 3].map((element, index, array) => parseInt(element, index));

展开之后相当于三次parseInt调用

1
2
3
parseInt(1, 0); // 1
parseInt(2, 1); // NaN
parseInt(3, 2); // NaN

注意:parseInt函数的第二个参数是进制,这个参数有如下限制

  • 如果传递的进制不在2-36之间,那么parseInt返回NaN
  • 如果不传递该参数,或者传递0,那么parseInt会根据第一个参数推断:
    • 如果第一个参数以0x或者0X开头,那么会被解析为十六进制。
    • 否则会被解析为十进制。

所以:

  • parseInt(1, 0); radix为0,根据第一个参数判断,而第一个参数1并非以0x0X开头,所以会被解析为十进制,返回1。
  • parseInt(2, 1); radix为1,不在2-36之间,返回NaN
  • parseInt(3, 2); radix为2,但是3不是二进制数,返回NaN

需要注意的是:parseInt会将第一个参数转换为字符串,然后再解析。所以parseInt(3, 2);等价于parseInt('3', 2);。可是二进制数中不可能有3这个数字,所以返回NaN。一个合法的例子是parseInt('11', 2);,这个会返回3。

大数相加

实现一个函数,输入两个字符串,返回它们的和。这两个字符串表示的数字不会以0开头,且不会以空格开头。返回的结果也不会以0开头。
这个题考察的点有以下几个

  1. 字符串如何转换为整数?
  2. 进位处理
  3. 字符串对齐处理,如何填充前导0?
  4. 如何取整。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function bigNumberAdd(a, b) {
const maxLength = Math.max(a.length, b.length);
a = a.padStart(maxLength, '0');
b = b.padStart(maxLength, '0');

let result = '';
let carry = 0;

for (let i = maxLength - 1; i >= 0; i--) {
const sum = Number(a[i]) + Number(b[i]) + carry;
// This is string concatenation, not numeric addition.
result = (sum % 10) + result; // if sum > 10, we only need the last digit.
carry = Math.floor(sum / 10); // the carry is the first digit when sum > 10.
}

// Don't forget the last carry.
if (carry > 0) {
result = carry + result;
}

return result;
}
console.log(bigNumberAdd('999', '999')); // 1998

JS中如何判断Primitive类型?

可以使用Object()函数。

1
2
3
4
5
6
7
8
function isPrimitive(value) {
return value !== Object(value);
}

// or
function isPrimitive(value) {
return value === null || (typeof value !== 'object' && typeof value !== 'function');
}

JS中有哪些方法可以判断数组类型?

  1. Array.isArray([])
  2. [] instanceof Array
  3. Object.prototype.toString.call([]) === '[object Array]'
  4. Array.prototype.isPrototypeOf([])
  5. [].__proto__ === Array.prototype
  6. [].constructor === Array

深拷贝要注意哪些问题?

  1. 循环引用
  2. 特殊对象,比如Date, RegExp, Map, Set, Function等。

以下是一个简单的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function deepCopy(obj, hash = new WeakMap()) {
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);
if (obj === null || typeof obj !== 'object') return obj; // primitives or function
if (hash.has(obj)) return hash.get(obj); // circular reference

let newObj = Array.isArray(obj) ? [] : {};
hash.set(obj, newObj);

for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepCopy(obj[key], hash);
}
}

return newObj;
}

// 示例对象
const obj = {
a: 1,
b: [2, 3],
c: { d: 4 },
e: new Date(),
f: /abc/,
g() {
console.log('g');
},
};

// 深拷贝
const copy = deepCopy(obj);
console.log(copy);
copy.g(); // g

类型转换,以下代码输出什么?

详情请参考+的运算规则:

1
2
3
4
console.log([] + []);
console.log([] + {});
console.log({} + []);
console.log({} + {});

输出结果是什么?

1
2
3
4
const numbers = [1, 2, 3];
numbers[4] = 4;
console.log(numbers); // 1, 2, 3, <1 empty item>, 4
console.log(numbers[3]); // undefined

以下代码输出什么?

1
2
3
4
5
6
const a = {};
const b = { key: 'b' };
const c = { key: 'c' };
a[b] = 123;
a[c] = 456;
console.log(a[b]);

答案:456
本题考察的点是:对象的键名只能是字符串或者Symbol类型,如果不是,会被转换为字符串。所以a[b]a[c]都会把键名转换为[object Object],所以a[b]a[c]实际上是同一个键,最后赋值会覆盖前面的值。

Introduction

HTTP has two types of messages, request message and response message. Both messages consist of a start line, headers, and an optional body.

Note that, HTTP messages, as defined in HTTP/1.1 and earlier, are human-readable. In HTTP/2, these messages are embedded into a binary structure, a frame, allowing optimizations like compression of headers and multiplexing.

Request Message

http-request-message
Http Request message consists of the following elements:

  1. An HTTP method.
  2. The path of the resource to fetch.
  3. The version of the HTTP protocol.
  4. Optional headers.
  5. An optional body.

Response Message

http-response-message
Http Response message consists of the following elements:

  1. The version of the HTTP protocol.
  2. A status code indicating the success or failure of the request.
  3. A status message.
  4. Optional headers.
  5. An optional body.

Reference

  1. MDN - HTTP Messages

The version history of HTTP

Version Year introduced Current status Usage in August 2024 Support in August 2024
HTTP/0.9 1991 Obsolete 0 100%
HTTP/1.0 1996 Obsolete 0 100%
HTTP/1.1 1997 Standard 33.8% 100%
HTTP/2 2015 Standard 35.3% 66.2%
HTTP/3 2022 Standard 30.9% 30.9%

HTTP/0.9

HTTP/0.9 is the first version of the HTTP protocol. It was a simple protocol that allowed clients to request a file from a server. The server would then send the file back to the client. The protocol did not support headers or status codes, and the client and server communicated using plain text.

HTTP/1.0

HTTP/1.0 was released in 1996 and introduced several new features to the protocol. These features included support for headers, status codes, and the ability to send multiple files in a single request. HTTP/1.0 also introduced the concept of persistent connections, which allowed clients to reuse a single connection to send multiple requests.

HTTP/1.1

HTTP/1.1 was released in 1999 and is the most widely used version of the HTTP protocol today. It introduced several new features, including support for chunked transfer encoding, which allows servers to send data in chunks, and the ability to reuse connections for multiple requests. HTTP/1.1 also introduced several new status codes, such as 100 Continue and 206 Partial Content.

Details about HTTP/1.1:

  1. HTTP/1.1 is a text-based protocol.

HTTP/2

HTTP/2 was released in 2015 and introduced several new features to the protocol. These features included support for multiplexing, which allows clients to send multiple requests over a single connection, and server push, which allows servers to send resources to clients before they are requested. HTTP/2 also introduced several new status codes, such as 103 Early Hints and 421 Misdirected Request.

Details about HTTP/2:

  1. HTTP/2 Message is binary, instead of textual.

HTTP/3

HTTP/3 is the latest version of the HTTP protocol and is currently in development. It is based on the QUIC protocol, which is a new transport protocol that is designed to improve the performance of web applications. HTTP/3 introduces several new features, including support for multiplexing, which allows clients to send multiple requests over a single connection, and server push, which allows servers to send resources to clients before they are requested. HTTP/3 also introduces several new status codes, such as 103 Early Hints and 421 Misdirected Request.

Version Release Date Feature Comments
Http 0.9 1991 只支持Get请求,没有Head,没有Status Code 只支持Plain Text,不支持图片,视频,语音等
Http 1.0 1996 无状态、短连接,队头阻塞 每个请求都要建立一个TCP连接,请求处理完TCP就关闭。下一个请求必须在前一个请求完成时才能开始,可能导致队头阻塞问题
Http 1.1 1999
  • 持久连接(keep alive)
  • 请求管道化(pipeling)
  • 增加缓存处理(新增Cache-Control字段)
  • 增加Host字段、支持断点续传
TCP连接可以复用。客户端可同时发送多个请求,但是服务端仍是一个一个处理,仍然存在队头阻塞问题。
Http 2.0 2015
  • 二进制分帧
  • 多路复用(或连接共享)
  • 头部压缩
  • 服务器推送
仍然基于TCP,虽然没有Http队头阻塞,但是有TCP队头阻塞
Http 3.0 2020 不再基于TCP,而是基于UDP,采用由Google在UDP协议上进行封装而形成的QUIC协议,保证传输的可靠性,并且一路传输失败不影响其他路。

Introduction

SOLID是五个面向对象设计原则的首字母缩写,分别是:

  • Single Responsibility Principle (SRP) - 单一职责原则
  • Open/Closed Principle (OCP) - 开闭原则
  • Liskov Substitution Principle (LSP) - 里氏替换原则
  • Interface Segregation Principle (ISP) - 接口隔离原则
  • Dependency Inversion Principle (DIP) - 依赖倒置原则

Single Responsibility Principle (SRP)

单一职责原则是指一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一个职责。这不意味着一个类只能有一个功能 - 这是不准确的说法。确切的说,一个类或者模块应该对且仅对一类行为主体负责!

Open/Closed Principle (OCP)

开闭原则是指一个软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着一个实体应该通过扩展来实现新的功能,而不是通过修改已有的代码来实现。

这通常通过使用抽象类和接口来实现。抽象类和接口定义了一组规范,具体的实现类来实现这些规范。如果将来要添加新的功能,只需要添加新的实现类,而不需要修改已有的代码。

Liskov Substitution Principle (LSP)

里氏替换原则是指一个软件实体(类、模块、函数等)应该可以被它的子类替换,而不影响程序的正确性。这意味着一个子类应该可以替换它的父类,而不需要修改父类的代码。

Interface Segregation Principle (ISP)

接口隔离原则是指一个类对另一个类的依赖应该建立在最小的接口上。换句话说,一个类不应该依赖它不需要的接口。

Dependency Inversion Principle (DIP)

依赖倒置原则是指高层模块不应该依赖于低层模块,两者都应该依赖于抽象。换句话说,模块之间的依赖关系应该是通过抽象发生的,而不是通过具体实现发生的。

Introduction

缓存是前端开发中非常重要的概念,它可以提高网站的性能,减少服务器的负担。本文主要介绍Http和浏览器缓存的相关知识。

不知道各位读者是否有这样的体验:

  1. 某个网站第一次打开的时候特别慢,再次打开的时候就快很多。
  2. 登录某个网站后,下次再访问的时候自动就是登录状态了。

其实以上现象都是缓存的功劳。

Http Cache

缓存相关的头部字段

Expires

Expires是一个HTTP响应头部字段,用于指定资源的过期时间。它的值是一个GMT格式的时间字符串。以下代码表示资源的过期时间是2024年11月16日14点10分23秒。

1
expires:Sat, 16 Nov 2024 14:10:23 GMT

Expires是HTTP/1.0的标准,它有一个问题,就是我们判断缓存过期时,浏览器会使用当前系统时间与Expires字段的时间进行比较。如果系统时间大于Expires字段的时间,那么缓存就会被认为过期了。但是当前系统时间是可以人为修改的,这就会导致缓存过期时间不准确。

Cache-Control

Cache-Control字段的取值有如下几种:

max-age

max-age的值是一个整数,单位是秒,表示缓存会在xxx秒后过期。以下代码表示资源会在3600秒后过期。

1
cache-control:max-age=3600

那么max-age中指定的3600秒参照的是什么时间呢?它参照的是服务器生成Response的时间,而不是浏览器收到Response的时间。那么服务器生成Response的时间在响应头中有吗?有的,就是Date字段。Date字段表示服务器生成Response的时间。所以缓存的的过期时间是Date字段的时间加上max-age的值。

ExpireTime = Date + max-age

注意:如果max-ageExpires同时存在,那么max-age的优先级更高,Expires会被忽略。

no-cache

注意:这个字段的意思不是不使用缓存!
这个字段的意思是可以使用缓存,但是使用前需要向服务器验证缓存是否过期。也就是协商缓存。

no-store

不使用缓存,每次都需要向服务器请求资源。

must-revalidate

在缓存过期前,直接使用缓存,但是过期后需要向服务器验证缓存是否过期。

Last-Modified

Last-Modified是一个HTTP响应头部字段,用于指定资源的最后修改时间。它的值是一个GMT格式的时间字符串。以下代码表示资源的最后修改时间是2024年11月16日14点10分23秒。

1
last-modified:Sat, 16 Nov 2024 14:10:23 GMT

当客户端再次请求资源时,会将上次请求时服务器返回的Last-Modified值放在请求头中的If-Modified-Since字段中,服务器会根据这个值判断资源是否发生了变化。如果资源没有发生变化,服务器会返回304 Not Modified状态码,表示资源没有发生变化,客户端可以使用缓存。

ETag

ETag是一个HTTP响应头部字段,用于指定资源的唯一标识。它的值是一个字符串。服务器会根据资源的内容生成一个唯一的字符串(注意,生成算法因服务器而异),然后将这个字符串作为ETag的值。当客户端再次请求资源时,会将上次请求时服务器返回的ETag值放在请求头中的If-None-Match字段中,服务器会根据这个值判断资源是否发生了变化。如果资源没有发生变化,服务器会返回304 Not Modified状态码,表示资源没有发生变化,客户端可以使用缓存。

ETag相比Last-Modified的优点

精确性:

  • ETag 可以提供比 Last-Modified 更精确的资源版本标识。Last-Modified 是基于时间戳的,而时间戳的精度通常是秒级,这意味着如果一个资源在一秒内发生变化多次,Last-Modified 就无法区分这些变化。
  • ETag 是一个可以由服务器自由定义的字符串,它可以唯一标识一个资源的特定版本,甚至可以包括内容的哈希值,从而确保即使是微小的变化也能被准确识别。

灵活性:

  • ETag 允许更灵活的缓存控制策略。它可以是强验证器(保证内容完全一致)或弱验证器(允许一定程度上的差异,如压缩、格式转换等),这取决于服务器如何生成 ETag。
  • 例如,当资源的内容没有变化,但是某些元数据发生了变化(比如编码方式),服务器可以选择更新 ETag 或者不更新,这取决于是否希望客户端重新获取资源。

避免时间戳问题:

  • 由于 Last-Modified 基于时间,它可能会遇到时钟不同步的问题。如果客户端和服务器之间的系统时钟存在较大偏差,那么基于 Last-Modified 的缓存验证可能会出错。
  • ETag 不依赖于时间戳,因此不受时钟同步问题的影响。

支持非文件资源:

  • 对于动态生成的内容(例如数据库查询结果),可能没有明确的最后修改时间,此时使用 ETag 更为合适。服务器可以根据内容的状态生成一个唯一的 ETag。

总结

  1. expiresmax-age控制的是强缓存。
  2. Last-ModifiedETag控制的是协商缓存。
  3. 浏览器请求时会先检查强缓存,然后再检查协商缓存。

一道面试题

如果一次Http请求返回的状态码是200,那么这个请求走缓存了吗?
答案:可能走,也可能没走。
解析:我们大多数人都知道状态码304表示资源没有改变,那么就使用本地缓存,这个属于协商缓存。还有一种是强缓存,就是直接使用本地缓存,不需要向服务器验证。这种情况下,状态码是200。

References

  1. https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
  2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
  3. https://juejin.cn/book/6994678547826606095/section/6997029635766616077?scrollMenuIndex=1