周刊第9期:Web 发展中的 100 个重大事件

本周见闻

Web 发展中的 100 个重大事件

自 2008 年 Chrome 浏览器正式发布以来,到现在 Chrome 已经发展到第 100 个版本了,为此还开发了一个网站,该网站展示了从 2008 年 Chrome 浏览器发布以来的 100 个对于 Web 发展的重大里程碑事件,譬如 GitHub 一周年、Node.js 发布、Flexbox 提案等,有兴趣可以看看。

我们如何失去 54K 的 GitHub stars

相信大家都知道 httpie 这个命令行工具,近日,由于维护者误操作将仓库设置为私有仓库,导致 54K 的 stars 被清零,经与 GitHub 官方沟通后,被告知无法恢复,截止今日(2022-04-17)已经重新涨回 12.7K。

httpie 在吐槽之余,还顺便教了一下 GitHub 做产品:

  1. UI/UX 设计,在设置为私有仓库时,告知用户会损失哪些数据。
  2. 数据库的软删除设计。

一些 tips

Chromium 的 DNS 缓存时间

Chromium 的 DNS 缓存时间大概在一分钟左右:

1
2
// Default TTL for successful resolutions with ProcTask.
const unsigned kCacheEntryTTLSeconds = 60;

DNS 的解析过程比较复杂,有兴趣可以看这个:Chrome Host Resolution,或者简单看下这两张图:

img

img

上图源自笔者一篇旧文:在浏览器输入 URL 回车之后发生了什么(超详细版)

如果想要查看浏览器 DNS 配置的详细信息,可以按照以下流程:

  1. 打开:chrome://net-export,开始记录,打开任意一个网站发起请求,导出 JSON 文件。
  2. NetLog Viewer 导入查看,DNS 栏目。

ECMAScript 提案 - 通过复制改变数组

这篇博客文章描述了 Robin Ricard 和 Ashley Claymore 提出的 ECMAScript 提案 “Change Array by copy”。它为 Array 和 TypedArray 提出了四种新方法:

  • .toReversed()
  • .toSorted()
  • .toSpliced()
  • .with()

大多数 Array 方法是无副作用的 – 它们不会更改调用它们的数组,例如:filtermap 等。

但也有副作用的方法,例如:reversesortsplice

因此新加入的三个方法为上述三种方法提供了无副作用版本,除此之外还引入了一个新方法:with。

它是下面这段代码的无副作用版本:

1
2
3
4
arr[index] = value

// 无副作用
arr.with(index, value) // 返回一个新的 array

CSS 父选择器 - :has()

在以前, 我们无法根据父元素是否包含某个子元素时决定父元素的样式。

譬如,我们希望在 .card 有子元素 img 时设置特定样式:

1
2
3
4
5
6
7
<div class="card">
<img src="a.jpg" >
</div>

<div class="card">
<p>card text</p>
</div>

我们可以使用 :has()

1
2
3
.card:has(img) {
border: 1px solid red;
}

其实 :has() 不止可以用于检查父元素是否包含某个子元素,还可以检查后面的元素:

1
2
// 检查 h2 后面跟着 p
.card h2:has(+ p) { }

但遗憾的是,截止目前(2022-04-18)只有 Safari 15.4和 Chrome Canary 支持该特性,详见 caniuse

分享文章

React 18 允许组件渲染 Undefined

在 React 18 之前,如果我们这样渲染了一个组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Shape.jsx

import React from 'react';
import Circle from './Circle';
import Square from './Square';

function Shape({type}) {
if(type === 'circle') {
return <Circle />
}
if(type === 'square') {
return <Square />
}
}
export default Shape;

//App.jsx

function App() : ComponentType {
return(<Shape type="rectangle"/>)
}

由于 Shape 组件返回 Undefined,我们将得到以下报错信息:

1
Error: Shape(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.

为了修复报错,我们必须显式返回 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import Circle from './Circle';
import Square from './Square';

function Shape({type}) {
if(type === 'circle') {
return <Circle />
}
if(type === 'square') {
return <Square />
}
+ return null;
}
export default Shape;

但随着 React 18 的发布,即便组件未返回任何内容,也不会引发运行时错误。

基于以下三点原因,使 React 18 作出此改动:

  1. 与其抛出错误,不如使用 Lint 工具

    • 渲染 Undefined 报错这个机制是在 2017 年加入的,当时类型系统和 Lint 工具还没开始流行,但现在我们完全可以使用 ESLint 等工具帮我们处理这些类型的错误。
  2. 很难创建正确的类型,考虑以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //Shape.jsx 
    const Shape = ({ children }: ComponentType): ComponentType => {
    return children;
    }

    //App.jsx
    function App(): ComponentType {
    return (<Shape />);
    }

    我们必须在 ComponentType 类型将 Undefined 排除在外,但更好的解决方法就是允许渲染 Undefined。

  3. 保持一致的行为

JavaScript 中 RegExp 与 String.replace 的神奇特性

下面这段代码的执行结果是什么呢?

1
2
3
4
5
6
var regexp = /huli/g
var str = 'blog.huli.tw'
var str2 = 'example.huli.tw'

console.log(regexp.test(str)) // ???
console.log(regexp.test(str2)) // ???

相信很多人都会认为两个都是 true,但答案是 true 和 false,即便你写成这样,第二个输出结果也是 false:

1
2
3
4
5
var regexp = /huli/g
var str = 'blog.huli.tw'

console.log(regexp.test(str)) // true
console.log(regexp.test(str)) // false

这是因为 RegExp 是有副作用的,以下为 MDN 原话:

如果正则表达式设置了全局标志,test() 的执行会改变正则表达式 lastIndex 属性。连续地执行test()方法,后续的执行将会从 lastIndex 处开始匹配字符串,(exec() 同样改变正则本身的 lastIndex 属性值).

以下代码证明了这点:

1
2
3
4
5
6
7
var regex = /foo/g;

// regex.lastIndex is at 0
regex.test('foo'); // true

// regex.lastIndex is now at 3
regex.test('foo'); // false

再来看另外一段代码:

1
2
3
4
5
6
7
var str = '4ark'

var result = /\w+/.test(str)

str = ''

// 我们还能拿得到 str 之前的值吗?

答案是可以的,因为 RegExp 上有一个神奇的属性:RegExp.input

除此之外,还有这些:

  1. RegExp.lastMatch
  2. RegExp.lastParen
  3. RegExp.leftContext
  4. RegExp.rightContext

但是需要注意,这些特性是非标准的,请尽量不要在生产环境中使用它!

另外原文还有关于 String.replace 的神奇特性:使用字符串作为参数,简单来说就是:

1
2
3
4
5
6
7
8
9
10
const str = '123{n}456'

// 123A456
console.log(str.replace('{n}', 'A'))

// 123123A456,原本 {n} 的地方变成 123A
console.log(str.replace('{n}', "$`A"))

// 123456A456,原本 {n} 的地方变成 456A
console.log(str.replace('{n}', "$'A"))

在用户离开页面时可靠地发送 HTTP 请求

我们希望在用户离开当前页面时发送一个 HTTP 请求,这是一个非常常见的需求,譬如页面埋点等。

但根据 Chrome 页面的生命周期显示,在页面终止运行时,无法保证进程内的请求会成功,因此,在离开页面时发送请求可能并不可靠,如果我们依赖这个行为,则会出现潜在的重大问题。

通过下图可看出在页面离开时,请求会被取消掉:

在“网络”选项卡中查看 HTTP 请求失败

为什么请求会被取消呢?下面是 Chrome 对于页面终止生命周期(Terminated)的描述:

A page is in the terminated state once it has started being unloaded and cleared from memory by the browser. No new tasks can start in this state, and in-progress tasks may be killed if they run too long.

Possible previous states:
hidden (via the pagehide event)

Possible next states:
NONE

简单来说就是一个页面被卸载并从内存清除时,它就处于终止状态,在这种状态下,没有新的任务可以启动,正在运行的任务如果运行时间过长,则有可能会被 killed 掉。

那我们应该如何解决这个问题呢?有下面几种方案:

  1. 阻塞页面跳转,直到请求被响应:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
document.getElementById('link').addEventListener('click', async (e) => {
e.preventDefault();

// Wait for response to come back...
await fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: 'data'
}),
});

// ...and THEN navigate away.
window.location = e.target.href;
});

但这样也有很明显的缺点,1)损害用户体验;2)没有包含所有页面离开行为,例如关闭浏览器 tab。

  1. 使用 Fetch 的 keepalive 选项,使请求继续保留,即便页面已终止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<a href="/some-other-page" id="link">Go to Page</a>

<script>
document.getElementById('link').addEventListener('click', (e) => {
fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: "data"
}),
+ keepalive: true
});
});
</script>
  1. 使用 Navigator.sendBeacon() 方法
1
2
3
4
5
6
7
8
<a href="/some-other-page" id="link">Go to Page</a>

<script>
document.getElementById('link').addEventListener('click', (e) => {
const blob = new Blob([JSON.stringify({ some: "data" })], { type: 'application/json; charset=UTF-8' });
navigator.sendBeacon('/log', blob));
});
</script>
  1. 使用 a 标签的 ping 属性
1
2
3
<a href="http://localhost:3000/other" ping="http://localhost:3000/log">
Go to Other Page
</a>

点击该链接后,它会自动发出一个 POST 请求,并将 href 属性放在请求头中:

1
2
3
4
5
6
headers: {
'ping-from': 'http://localhost:3000/',
'ping-to': 'http://localhost:3000/other'
'content-type': 'text/ping'
// ...other headers
},

但有如下限制:

  1. 只能在 a 标签上使用
  2. 浏览器支持很好,但 Firefox 除外 :(
  3. 无法自定义发送的数据…

如果选择使用哪个方法呢?文中还给出了一个很好的提示:

  • 以下情况,推荐使用 fetch + keepalive
    • 需要自定义 header 和请求内容
    • 希望发出 GET,而不只是 POST
    • 需要支持较旧的浏览器,并且已有 fetch 的 polyfill。
  • 以下情况,推荐使用 sendBeacon()
    • 只是简单的请求,不需要太多的自定义内容
    • 喜欢更干净、更优雅的 API。
    • 您希望保证您的请求不会与应用程序中发送的其它高优先级的请求竞争。

有趣的链接

  • Turborepo:Turborepo 是一个针对 JavaScript 和 TypeScript 代码库的高性能构建系统。

作者 : 4Ark
来源 : https://4ark.me
著作权归作者所有,转载请联系作者获得授权。

🥳 加载 Disqus 评论