你不知道的前端新特性

2022-09-13
6分钟阅读时长

提醒:本文最后更新于 802 天前,文中所描述的信息可能已发生改变,请谨慎使用。

Featured Image

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

CSS Toggles

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

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

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

  • 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:定义该元素为 的切换触发器

    <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():根据 值选择元素

  • toggle:当可切换元素和切换触发器元素为同一个时,可使用 toggle 进行简写

  • toggle-group:指定该元素为 的组内元素

我们可以通过 Element.toggles() 来获取可切换元素的所有切换状态枚举,也通过 Element.addEventListener('togglechange', ...) 获取当前可切换元素的当前状态。

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

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

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

所以就有好多组件封装,虽然原生已经有 <dialog> 标签可以干类似的事情了。但毕竟还是有点原始,所以 Chrome 就将 PopUp 原生实现了~~(真卷啊)~~。

  • 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://weibo.com/1708684567/LzHEY1wGm

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

structuredClone

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

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

structuredClone(value: any, { transfer?: any[] })
  • 不允许克隆ErrorFunctionDOM对象,如果对象中含有,将抛出DATA_CLONE_ERR异常。
  • 不保留RegExp 对象的 lastIndex 字段。
  • 不保留属性描述符,setters 以及 getters(以及其他类似元数据的功能)。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write
  • 不保留原型链。

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

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 中比较有用,可能会多次滚动
function shouldNotIntercept(navigationEvent) {
  return (
    !navigationEvent.canIntercept ||
    navigationEvent.hashChange ||
    navigationEvent.downloadRequest ||
    navigationEvent.formData
  );
}

signalscroll() 的例子:

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://caniuse.com/mdn-api_navigation_navigate

URLPattern

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

可以算是原生版的 path-to-regexp ,用来做 URL 格式匹配解析的。最开始是因为 service worker 场景有解析拦截的资源请求地址的需求创造,但适用所有 URL 解析场景。

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

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 规范化之后再进行解析,而不是简单的做一个字符串的正则转换匹配。

目前仅 Chrome 系兼容性还行,Node 也暂时还没有跟上版本。不过有对应的 Polyfill 了已经。

图片

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

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

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

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

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

<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> 进行图片渲染。

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

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

<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>

参考资料:

Avatar
怡红公子 擅长前端和 Node.js 服务端方向。热爱开源时常在 Github 上活跃,也是博客爱好者,喜欢将所学内容总结成文章分享给他人。

6 评论

Uyoahz Chrome105.0 Windows 10
2022-09-15 09:29:52 回复

下午好~

怡红公子 Chrome105.0 Mac OS 10.15.7
2022-09-15 13:59:42 回复

@Uyoahz: 晚上好 OwQ

Yumin Gui Chrome106.0 Mac OS 10.15.7
2022-10-20 00:55:35 回复

@怡红公子: 晚上好

怡红公子 Chrome106.0 Mac OS 10.15.7
2022-10-20 02:11:02 回复

@Yumin Gui: 早上好~

kangkk Chrome105.0 Mac OS 10.15.7
2022-09-14 02:31:37 回复

早上好~

怡红公子 Chrome105.0 Mac OS 10.15.7
2022-09-15 06:14:25 回复

@kangkk: 哈哈哈哈,看到的时候已经是第二天的早上了,早上好呀~