
![](https://p.upyun.lithub.cc/imnerd.org/assets/2022/front-end-new-feature-you-dont-know/banner.png)


> 有些你不知道是正常的……因为他们基本都没怎么被浏览器实现 🥶

## CSS Toggles

<https://tabatkins.github.io/css-toggle/>

纯 CSS 实现状态切换一般使用 Checkbox 或者 Radio 配合选择器来实现。实现起来麻烦不说，而且 CheckBox/Radio 的位置限制了你可控制的范围，用起来很不方便。

<style>iframe { width: 100%; height: 300px; border: 1px solid #EFEFEF; }</style>
<iframe src="https://code.juejin.cn/pen/7136486392506351649"></iframe>

交互中越来越多依赖状态，比如 Tab, 弹窗, Summary 等，所以有了原生的状态切换草案。目前还是草案中，不过已经有对应的 Polyfill 了 <https://github.com/oddbird/css-toggles>

<iframe src="https://code.juejin.cn/pen/7136467557795495973"></iframe>

-  `toggle-root`：定义该元素可切换状态
    ```
      <toggle-root> = <custom-ident>
      [
        <toggle-states> [at <toggle-value>]? ||
        <toggle-overflow> ||
        group ||
        self
      ]?
    
    <toggle-states> = <integer [1,∞]> | '[' <custom-ident>{2,} ']'
    <toggle-value> = <integer [0,∞]> | <custom-ident>
    <toggle-overflow> = cycle | cycle-on | sticky

    // mode
    // mode 1 at 0 cycle wide
    // mode 3 at 0
    // mode [light dark] at light
    ```
    
  - `toggle-overflow` 定义设置的值超出之后的行为，针对数字类型有效
    -   cycle / cycle-on: 比最小值小则为最大值，比最大值大则为 0 / 1
    -   sticky: 最小值 <= value <= 最大值
  - `self` 定义触发元素和可切换元素的查找关系：
    - wide: 任意
    - narrow: 必须是父子关系
- `toggle-rigger`：定义该元素为 [<custom-ident>](https://drafts.csswg.org/css-values-4/#identifier-value) 的切换触发器
    ```
    <toggle-trigger> = <custom-ident> <trigger-action>?
    <trigger-action> =
      [prev | next] <integer [1,∞]>? |
      set <toggle-value>
    
    // mode
    // mode next 1
    // mode prev 1
    // mode next 2 
    // mode 2
    // mode set light
    ```
- `:toggle()`：根据 [<custom-ident>](https://drafts.csswg.org/css-values-4/#identifier-value) 值选择元素
- `toggle`：当可切换元素和切换触发器元素为同一个时，可使用 `toggle` 进行简写
- `toggle-group`：指定该元素为 [<custom-ident>](https://drafts.csswg.org/css-values-4/#identifier-value) 的组内元素
    
我们可以通过 `Element.toggles()` 来获取可切换元素的所有切换状态枚举，也通过 `Element.addEventListener('togglechange', ...)` 获取当前可切换元素的当前状态。

更多示例见：<https://toggles.oddbird.net>

## PopUp API

https://open-ui.org/components/popup.research.explainer

如果要自己从 0 开始写一个弹窗是比较麻烦的，需要考虑很多事情：全屏浮层，内容居中，页面滚动失效，遮罩点击关闭，ESC 按下关闭...

所以就有好多组件封装，虽然原生已经有 [\<dialog\>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) 标签可以干类似的事情了。但毕竟还是有点原始，所以 Chrome 就将 PopUp 原生实现了~~（真卷啊）~~。

<iframe src="https://code.juejin.cn/pen/7136554639964504097"></iframe>

- `popup`: auto | hint | manual 默认为 auto，指定多弹窗的关系
- popuptoggletarget：指向带有 `popup` 属性的元素 id，用来切换 popup 元素的显隐
- popupshowtarget：指向带有 `popup` 属性的元素 id，用来显示 popup 元素
- popuphidetarget：指向带有 `popup` 属性的元素 id，用来隐藏 popup 元素
- `Element.showPopUp()` `Element.hidePopUp()`

目前仅最新的 Chromium 生效 :-) 还有些问题~ <https://chromestatus.com/feature/5463833265045504>

## CSS 作用域

<https://www.w3.org/TR/css-scoping-1/>

以往我们要实现 CSS 作用域，组件之间样式不互相影响，一般就是 BEM 命名或者 CSS Module, CSS in JS 之流最终生成带 hash 的唯一选择器伪实现，亦或是使用 Shadow DOM 这种高成本的完美实现。

现在我们能直接使用 `@``scope` 来实现样式隔离了！

![](https://p.upyun.lithub.cc/imnerd.org/assets/2022/front-end-new-feature-you-dont-know/1.png)

> 图来自：<https://weibo.com/1708684567/LzHEY1wGm>

不用太多介绍，简单好用~

## structuredClone

<https://developer.mozilla.org/en-US/docs/Web/API/structuredClone>


原生的深拷贝方法，可以对结构化数据进行深拷贝，避免 `JSON.parse(JSON.stringify())` 的尴尬。

```js
structuredClone(value: any, { transfer?: any[] })
```

- 不允许克隆`Error`、`Function`和`DOM`对象，如果对象中含有，将抛出`DATA_CLONE_ERR`异常。
- 不保留`RegExp` 对象的 `lastIndex` 字段。
- 不保留属性描述符，`setters` 以及 `getters`（以及其他类似元数据的功能）。例如，如果一个对象用属性描述符标记为 `read-only`，它将会被复制为 `read-write`。
- 不保留原型链。

## Navigation API

SPA 的基石路由管理，早有 History API 支持，但因为本质是历史记录的管理，缺少一些切换后的控制等功能，所以 Chrome 从新提出了 Navigation API 来专门实现路由管理。

```js
navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;

  const url = new URL(navigateEvent.destination.url);

  if (url.pathname === '/') {
    navigateEvent.intercept({handler: loadIndexPage});
  } else if (url.pathname === '/cats/') {
    navigateEvent.intercept({handler: loadCatsPage});
  }
});
```

`navigateEvent`包含以下信息：
- `canIntercept` 是否支持拦截，跨域等无法拦截场景会返回 `false` 
- `destination.url` 跳转目标地址 
- `hashChange`是否是锚点跳转 
- `userInitiated` 是否是由页面内 `<a>` 标签触发的跳转，为 `false` 表示是浏览器前进后退等触发的跳转 
- `downloadRequest`是否是由具有`download`属性的链接带来的跳转 
- `formData`表单跳转时对应提交的表单数据，可以针对 Form 表单拦截后发送数据 
- `navigationType`枚举值`"reload"`, `"push"`,`"replace"`或`"traverse"`（类似 `history.goBack()`）之一。如果是`"traverse"`，则无法通过`preventDefault()`阻止跳转 
- `signal` 提供给拦截 handler 中异步请求使用，方便当跳转终端后同步中断请求 
- `scroll()`控制跳转后滚动，在异步 handler 中比较有用，可能会多次滚动

```js
function shouldNotIntercept(navigationEvent) {
  return (
    !navigationEvent.canIntercept ||
    navigationEvent.hashChange ||
    navigationEvent.downloadRequest ||
    navigationEvent.formData
  );
}
```

`signal` 和 `scroll()` 的例子：

```js
navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // The URL has already changed, so quickly show a placeholder.
        renderArticlePagePlaceholder();
        // Then fetch the real data.
        const articleContentURL = new URL(
          '/get-article-content',
          location.href
        );
        articleContentURL.searchParams.set('path', url.pathname);
        const response = await fetch(articleContentURL, {
          signal: navigateEvent.signal,
        });
        const articleContent = await response.json();
        renderArticlePage(articleContent);
        navigateEvent.scroll();

        const secondaryContent = await getSecondaryContent(url.pathname);
        addSecondaryContent(secondaryContent);
      },
    });
  }
});

// navigate
const { committed, finished } = navigation.navigate('/articles/hello-world');
```

设置好跳转拦截回调后，我们就能正常使用 `navigation.navigate()` 进行跳转了。返回两个 Promise 对象，分别对应 Navigate 完成的状态 `committed`，以及导航拦截的回调 Handler 结束的状态 `finished`。

比起 History API 惊喜的是，我们可以通过 `navigation.entries()` 获取当前所有的历史记录。通过 `navigation.currentEntry` 返回当前的记录。

> `navigation.entries()` 获取的只能是同域的历史记录，跨域的无法获取。

![](https://p.upyun.lithub.cc/imnerd.org/assets/2022/front-end-new-feature-you-dont-know/2.png)

兼容性：<https://caniuse.com/mdn-api_navigation_navigate>

## URLPattern

<https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API>

可以算是原生版的 [path-to-regexp](https://github.com/pillarjs/path-to-regexp) ，用来做 URL 格式匹配解析的。最开始是因为 service worker 场景有解析拦截的资源请求地址的需求创造，但适用所有 URL 解析场景。

虽然 URLPattern 可以解析完整的域名，但一般 hostname 相关可以用 URL 解析，query 可以使用 URLSearchParams 解析。所以其实用的比较多的场景还是 pathname 的解析。

```js
const pattern = new URLPattern({ pathname: '/books/:id(\\d+)' });
console.log(pattern.test('https://example.com/books/123')); // true
console.log(pattern.exec('https://example.com/books/123').pathname.groups); // { id: '123' }
console.log(pattern.test('https://example.com/books/detail')); // false
```

- 使用 `:<group>` 来为当前匹配内容分组 
- `{}`是非捕获组，相当于正则中的 `(?:)`，可以在后面增加`{}?`表示可选，不加的话其实可有可无 
- `(正则表达式)`也可以通过正则进行精确匹配，可以跟在命名分组的后面，相当于`(?<group>正则表达式)`。内部正则关键字需要做转义处理。 
- `*` 表示贪婪匹配，独立使用相当于正则 `.*`，也可以搭配在前几个规则后使用，例如 `:id*` 
- `+` 相当于 `{1,}` 不可独立使用

使用 URLPattern 而不是自己使用正则解析的一个好处就是它会帮助我们把 URL 规范化之后再进行解析，而不是简单的做一个字符串的正则转换匹配。

![](https://p.upyun.lithub.cc/imnerd.org/assets/2022/front-end-new-feature-you-dont-know/3.png)

目前仅 Chrome 系[兼容性](https://caniuse.com/mdn-api_urlpattern)还行，Node 也暂时还没有跟上版本。不过有对应的 [Polyfill](https://github.com/kenchris/urlpattern-polyfill) 了已经。

## 图片

<https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio>

如果需要按比例显示图片，一般会保持比例设置 DOM 尺寸后使用 `background-image` 或者使用 `<img>` 来展示，比较不便。所以增加了 `aspectio-ratio` 属性直接支持设置图片显示的比例。

配合 `object-fit` 属性指定图片比例不对的时候填充模式，食用更加。

```css
img { 
  aspect-ratio: 16 / 9; 
  object-fit: contain;
}
```

<https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading>

>`object-fit` 的兄弟属性 `object-position`：用于指定图片的展示区域
>
> https://developer.mozilla.org/en-US/docs/Web/CSS/object-position

为了性能优化我们一般都会为图片增加懒加载的支持，这个官方也做了原生的支持。

```html
<img src="image.jpg" alt="..." loading="lazy">
```

<https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images>


为了更好的性能优化，我们一般会从图片尺寸和图片格式上对图片资源进行处理，此为响应式图片。

关于图片格式，静图有 `jpg`,`png`,`webp`,`avif`,`jpeg XL` 这些格式。动图有 `gif`,`apng`,`webp`,`avif`之这些格式。`avif` 是基于视频编码 AV1 衍生的图片格式。针对不同的浏览器适配不同的格式，我们可以使用 `<picture>` 进行图片渲染。

```html
<picture>
  <source type="image/avif" srcset="....avif" />
  <img src="....webp" loading="lazy" />
</picture>
```

除了格式之外，我们还可以按照分辨率来设置，图片尺寸也不在话下。综合如下：

```html
<style>
img { 
    width: 320px;
    aspect-ratio: 320 / 240; 
    object-fit: contain; 
}
</style>
<picture>
  <source 
      type="image/avif" 
      srcset="...320x240.avif 1x ...640x480.avif 2x ...960x720.avif 3x" 
  />
  <img 
      srcset="...320x240.webp 1x ...640x480.webp 2x ...960x720.webp 3x" 
      src="....webp" 
      loading="lazy" 
  />
</picture>
```

**参考资料：**

- [The Future of CSS: CSS Toggles](https://www.bram.us/2022/04/20/the-future-of-css-css-toggles/) 
- [有哪些以往需要使用javascript实现的功能现在可以直接使用html或者css实现? - 知乎](https://www.zhihu.com/question/546390030/answer/2603655667) 
- [Scope Proposal & Explainer](https://css.oddbird.net/scope/explainer/) 
- [Proposal for CSS @when | CSS-Tricks](https://css-tricks.com/proposal-for-css-when/) 
- [JS 深拷贝的原生终结者 structuredClone API - 掘金](https://juejin.cn/post/7080433165264748557) 
- https://developer.chrome.com/docs/web-platform/navigation-api/ 
- [URLPattern brings routing to the web platform](https://web.dev/urlpattern/) 
- https://web.dev/learn/design/picture-element/