Skip to content

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

Published:

本周见闻

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 缓存时间大概在一分钟左右:

// 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 提出了四种新方法:

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

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

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

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

arr[index] = value

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

CSS 父选择器 - :has()

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

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

<div class="card">
    <img src="a.jpg" >
</div>

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

我们可以使用 :has()

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

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

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

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

分享文章

React 18 允许组件渲染 Undefined

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

//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,我们将得到以下报错信息:

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

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

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. 很难创建正确的类型,考虑以下代码:

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

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

  3. 保持一致的行为

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

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

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:

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 属性值).

以下代码证明了这点:

var regex = /foo/g;

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

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

再来看另外一段代码:

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 的神奇特性:使用字符串作为参数,简单来说就是:

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. 阻塞页面跳转,直到请求被响应:
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 选项,使请求继续保留,即便页面已终止。
<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() 方法
<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 属性
<a href="http://localhost:3000/other" ping="http://localhost:3000/log">
  Go to Other Page
</a>

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

headers: {
  'ping-from': 'http://localhost:3000/',
  'ping-to': 'http://localhost:3000/other'
  'content-type': 'text/ping'
  // ...other headers
},

但有如下限制:

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

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

有趣的链接


作者 : 4Ark

地址 : https://4ark.me/posts/2022-04-17-weekly-09/

来源 : https://4ark.me

著作权归作者所有,转载请联系作者获得授权。