自定义标签通过扩展一个HTMLElement
或HTMLElement
的子类来定义一个新的html
标签,是通过原生js实现的组件化。
自定义标签通过window.customElements.define
来定义,
-
且全是小写字母HTMLElement
的类extends
属性可以配置,如果构造函数是继承自HTMLElement
的子类,如HTMLDivElement
就需要指定extends:"div"
在定义好自定义元素后就可以直接在html
中使用自定义的元素了,如果自定义元素继承自其它元素,需要使用原来的标签加上is
属性指定自定义标签的名字
<!-- 继承自HTMLElement -->
<ce-myelement></ce-myelement>
<!-- 继承自p标签 -->
<p is="ce-my-p-element"></p>
复制代码
下面是一个简单例子,点击元素后这个元素会打印出自己
class CopyCode extends HTMLElement {
constructor() {
super();
this.onclick = (e) => {
if (e.target != this) return;
console.log(e.target);
};
}
}
window.customElements.define("ce-myelement", CopyCode);
复制代码
前面的自定义标签只是定义了自己的一些特别的通用方法,也能插入子元素,已经拥有了组件化的方法,但和复杂的组件相比是完全不够用的,它应该配合另一个特性Shadow DOM
一起使用
Shadow DOM
能封闭内部,让js和css都无法选择到内部元素(只是无法选择,还是会显示到页面上),里面可以定义<stype>
标签且只会影响到内部样式
通过下面方法就能将一个普通元素接管为影子DOM
const innerNode = document.createElement("p");
innerNode.innerText = "inner";
const div = document.querySelector("div");
const shadow = div.attachShadow({ mode: "closed" });
shadow.appendChild(div.appendChild(innerNode));
复制代码
将一个元素设置为shadow DOM
后,它的所有子元素都会被页面隐藏,shadow DOM
中的元素会出现在屏幕上
通过原来的元素的shadowRoot
属性能获得其中的影子DOM,如果创建时mode
属性为closed
则不能获得影子DOM,这意味着这个元素是完全封闭的,外部无法更改它
const shadow = div.attachShadow({ mode: "closed" });
console.log(div.shadowRoot); // null
const shadow = div.attachShadow({ mode: "open" });
div.shadowRoot == shadow; // true
复制代码
通过上面方法已经给div
创建了shadowDOM
,现在就能向其中添加元素和样式了,样式和普通的页面一样创建
<style>
标签使用innerText
手动写css
的import url()
方法引入外部样式<link>
标签引入外部样式通过影子dom接管了普通元素的内部内容,元素中原来的内容都会被隐藏起来,这时可以通过插槽元素<slot>
来将外部元素引入影子dom,让它在适当的地方显示出来
一个简单的例子,让div中的文字换成红色的h1
大小的文字
const div = document.querySelector("div");
const shadow = div.attachShadow({ mode: "open" });
const h1 = document.createElement("h1");
h1.style.color = "red";
h1.appendChild(slot);
shadow.appendChild(h1);
复制代码
插槽也支持命名插槽,通过在<slot>
上定义name
属性指定名字,在普通元素上使用slot
属性指定同名的插槽,就会把普通元素替换到影子中,同时<slot>
中也可以放入默认的元素
const div = document.querySelector("div");
const shadow = div.attachShadow({ mode: "open" });
const slot = document.createElement("slot");
slot.setAttribute("name", "h1");
const h1 = document.createElement("h1");
h1.style.color = "red";
h1.appendChild(slot);
shadow.appendChild(h1); // 在影子中加入一个含插槽的元素
const text = document.createElement("div");
text.innerText = "h1";
text.setAttribute("slot", "h1");
div.appendChild(text); // 将指定了插槽的元素放入原来的元素中
复制代码
上面例子中一直使用代码构建dom树,其实可以使用<templates>
标签来构造模板,和普通标签不同,<templates>
标签中的内容不会显示到页面上,同时也和影子DOM一样有css
的作用域
将上面的代码改写成模板的形式:
<div>aaa</div>
<template id="text">
<b slot="h1">text</b>
</template>
<template id="temp">
<style>
h1 {
color: red;
}
</style>
<h1>
<slot name="h1"></slot>
</h1>
</template>
<script src="./index.js" type="module"></script>
复制代码
const div = document.querySelector("div");
const shadow = div.attachShadow({ mode: "open" });
shadow.appendChild(document.querySelector("#temp").content.cloneNode(true));
const text = document.querySelector("#text").content;
div.appendChild(text);
复制代码
这样,结合上面的自定义标签,就可以制作一个组件了
<body>
<ce-red-h1 data-text="abc">
<b slot="h1">b</b>
</ce-red-h1>
<template id="temp">
<style>
h1 {
color: red;
}
</style>
<h1>
<slot name="h1"></slot>
</h1>
</template>
<script src="./index.js" type="module"></script>
</body>
复制代码
class RedH1 extends HTMLElement {
text;
constructor() {
super();
const template = document.querySelector("#temp");
this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true));
this.text = this.dataset.text;
const p = document.createElement("p");
p.innerText = this.text;
this.shadowRoot.appendChild(p);
}
}
window.customElements.define("ce-red-h1", RedH1);
复制代码
虽然自定义标签也能通过
data-
来传递数据,但只能是字符串
vue中提供了一个defineCustomElement
来创建一个自定义标签的构造函数,它接收defineComponent
相同的参数,返回的类需要使用window.customElements.define
来注册,因为是使用原生的方法注册,这样的组件不需要挂载为全局组件就能全局使用,通过vue模板来创建的自定义标签能支持传递对象等复杂数据
在vue中使用自定义标签得先配置loader
,否则会有警告提示标签不是vue组件
// vite
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith("ce-"), // 自定义标签开头用`ce-`这样就不会抛错
},
},
})
// vuecli
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => ({
...options,
compilerOptions: {
// 将所有以 ion- 开头的标签作为自定义元素处理
isCustomElement: tag => tag.startsWith('ion-')
}
}))
}
}
复制代码
为了防止打包时将样式单独打包到外部,需要将vue文件后缀名改为.ce.vue
通过单文件组件定义的内容全都放入了自定义元素的影子DOM中
<template>
<h1 class="redH1">
<slot name="h1"></slot>
</h1>
<p>{{ text.value }}</p>
</template>
<script setup lang="ts">
interface Props {
text: {
value?: string;
};
}
withDefaults(defineProps<Props>(), {
text: () => ({
value: "a",
}),
});
</script>
<style>
// 因为不会被打包到外部且样子只会应用于影子中,所以不用加scoped
h1 {
color: red;
}
</style>
复制代码
// index.ts
import { defineCustomElement } from "vue";
import RedH1 from "./RedH1.ce.vue";
window.customElements.define("ce-red-h1", defineCustomElement(RedH1));
复制代码
然后在main
中通过副作用引入index
就能在全局使用了
<template>
<ce-red-h1 .text="text">
<b slot="h1">b</b>
</ce-red-h1>
</template>
<script setup lang="ts">
import { reactive } from "@vue/reactivity";
const text = reactive({
value: "abc",
});
</script>
复制代码
注意,如果是传递对象,数组等数据,不是使用
v-bind:text
,而是v-bind:text.prop
,简写.
使用单文件时会打包更多的代码进去,如果只是使用简单的功能组件更推荐使用原生写法
如果需要扩展从外部获取的html
并添加比较复杂的功能,自定义标签就是个很好的选择,比如我的博客的文章通过markdown
解析为html,只需要在解析出的html
文本的代码片段的右上角的复制按钮就是一个自定义标签,通过自定义点击事件直接将父元素中的innerText
复制进剪贴板,就不用像思否的粘贴按钮一样单独设置每个代码片段的粘贴内容
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。