首页 > 编程笔记 > CSS笔记

CSS :defined用法详解

CSS :defined 伪类在 Web Components 开发中比较有用,尤其是在 HTML 直出的 Web 页面中。要介绍适合使用 :defined 伪类的场景,就必须先讲解 Web 页面的渲染机制。

所有的 Web 页面,要么是服务端直出(查看页面源代码会显示几乎所有的 HTML 结构)的,要么是 JavaScript 输出(例如 Vue、React 框架编译的页面)的。

对于使用 JavaScript 构建的页面,:defined 伪类的作用有限,因为渲染的时候是先有 JavaScript,再有 HTML 元素,在 HTML 元素进入页面的那一刻,就注定了 :defined 伪类已经匹配自定义组件元素。

但若是服务端直出的页面,则情况可能会有所不同。这类页面,常常是 CSS 代码在顶部,JavaScript 代码在底部,而页面渲染是从上往下的。这就导致当用户看到页面中的某个 Web Components 元素的时候,JavaScript 代码尚未运行,因而此时的 Web Components 元素还是普通元素,等 JavaScript 代码加载完毕并运行后这个普通元素才会变成组件元素。而这个变化过程中可能会伴随着明显的样式变化,在用户眼中,可能就是不太友好的用户体验,于是就需要在元素还是普通元素的时候进行优化处理,此时就需要用到 :defined 伪类。

举个例子,目标是自定义一个名为 <square-img> 的元素,可以让图片以正方形显示,同时如果有 alt 属性值,则直接在图片上显示:
<square-img src="./1.jpg" size="200" alt="提示信息"></square-img>
最终的实现效果如下图所示。


图 1 <square-img>希望实现的效果

现在需求产生了,为了避免长时间的白屏和尺寸突然跳动,希望在组件初始化之前表现为 150px×150px 的基础尺寸,同时显示淡灰色的表示占位的背景色,该如何实现呢?

这就是使用 :defined 伪类的典型场景,可以在组件所处的上下文中编写如下 CSS 代码:
square-img:not(:defined) {
    display: inline-block;
    width: 150px; height: 150px;
    background-color: #f0f0f0;
}
此时,在 <square-img> 组件未被 customElements.define() 方法定义的时候,就会表现为 150px×150px 的基础尺寸,同时背景色是淡灰色。

普通元素的:defined适配规则

:defined 伪类并不是自定义元素专享的,对于普通的 HTML 元素,:defined 伪类也可以匹配,区别在于,自定义元素需要开发人员自行定义,而标准的 HTML 元素在浏览器内部早早地定义好了。因此,对于这些标准的 HTML 元素,即使作为虚拟 DOM 存在,:defined 伪类也可以匹配。

例如有如下 CSS 代码和 JavaScript 代码:
div:defined {
    width: 150px; height: 150px;
    border: solid;
    background-color: #eee;
}

// JavaScript代码
const div = document.createElement('div');
// 输出的是true
console.log(div.matches(':defined'));
// 插入页面
document.body.append(div);
// 输出的是true
console.log(div.matches(':defined'));
可以看到,div 对象无论是在内存中还是加载到页面中,:defined 伪类都可以匹配,同时选择器 div:defined 匹配了 div 元素,表现为 150px×150px 的尺寸,还有边框和背景色,效果如下图所示。


图 2 :defined伪类匹配div元素效果示意

但是有一个例外情况,那就是如果 div 元素设置了 is 属性,则 :defined 伪类的匹配不再由浏览器控制,而是由开发人员自行决定。

例如页面中有如下 CSS 代码和 HTML 代码:
div:defined {
    width: 150px; height: 150px;
    border: solid;
    background-color: #eee;
}

<div id="square-img">可以匹配</div>
<div is="square-img">无法匹配</div>
此时,:defined 伪类是无法匹配设置了 is="square-img" 的 div 元素的,两个 div 元素的渲染效果如下图所示。


图 3 :defined伪类无法匹配设置了is属性的div元素示意

可以看到后一个 div 元素既没有背景色也没有边框,显然 :defined 伪类没有匹配。

那么问题来了,为何标准 HTML 元素设置了 is 属性后,:defined 伪类就不再匹配呢?

这是因为 is 属性是一个和 Web Components 密切相关的属性。可以将标准 HTML 元素扩展为内置自定义元素,需要开发人员重新定义并注册才能匹配,例如:
class SquareImg extends HTMLDivElement {}
customElements.define('square-img', SquareImg, {
    extends: 'div'
});
此时,:defined 伪类就可以匹配 <div is="square-img"> 了。

内置自定义元素不仅可以继承 HTML 元素原本的特性,还能在此基础上进一步扩展和增强,是非常实用的前端技术。可惜 Safari 浏览器(目前版本是 Safari16)依然没有支持内置自定义元素。不过只要引入相应的 polyfill 代码,Safari 浏览器就能无缝对接内置自定义元素的语法和效果,除了例外情况,那就是 :defined 伪类。

Safari不支持内置自定义元素的处理

在 Safari 浏览器下,:defined 伪类无法匹配设置了 is 属性的标准 HTML 元素,我们无法根据这个伪类判断内置自定义元素是否完成了定义,怎么办呢?

其实并没有什么特别好的办法,我个人是这么处理的:在 connectedCallback 这个生命周期回调方法中给元素添加一个名为 defined 的属性,然后改用属性选择器 [defined] 进行匹配。

示意代码如下:
class SquareImg extends HTMLDivElement {
connectedCallback: function () {
  this.setAttribute('defined', '')
}
}
customElements.define('square-img', SquareImg, {
    extends: 'div'
});

div[defined] {}

推荐阅读