web components 前面我们已经介绍过,这一期我们就来讲一讲具体用法和这其中的关键只是点:
1 基本使用
如果我们想实现一个封装的原生组件,那就离不开使用js去封装,这里主要就是基于HTMLElement这个类,去创建创建一个子类,然后使用customElements.define去页面中注册这个组件。
export class dialog extends HTMLElement {
constructor() {
super(); // 必须首先调用 super()
// 正确的做法
if (!this.shadowRoot) {
this.attachShadow({ mode: "open" }); // 或者 'closed' 根据需求
}
// 创建并添加样式到影子DOM中
const style = document.createElement("style");
style.textContent = `
dialog {
padding: 0;
width: 300px;
height: 200px;
border: 1px solid #888;
}
dialog::backdrop {
background: rgba(0,0,0,0.5);
}
dialog .title {
box-sizing: border-box;
padding: 0 5px;
width: 100%;
height: 30px;
line-height: 30px;
color: #fff;
background: var(--primary-color);
}
dialog .close {
float: right;
cursor: pointer;
}
dialog .footer{
margin-top: 40px;
}
`;
this.shadowRoot.appendChild(style);
// 创建并添加元素到影子DOM中
const div = document.createElement("div");
const mode = this.getAttribute("mode") || "show";
const content = this.getAttribute("content") || "我是默认内容";
div.classList.add("custom-dialog");
const form = `<form method="dialog">
${content}
<div class="footer">
<button type="submit" value="submit">提交</button>
<button type="submit" value="cancel">取消</button>
</div>
</form>`;
const dialogContent = `
<dialog class="dialog">
<div class="title">dialog弹窗 <span class="close">X</span></div>
<div class="content">
${["show", "modal"].includes(mode) ? content : form}
</div>
</dialog>
<button class="show-dialog">${mode}弹窗</button>`;
div.innerHTML = dialogContent;
this.shadowRoot.appendChild(div);
this.dialog = this.shadowRoot.querySelector(".dialog");
const closeEvent = (detail) =>
new CustomEvent("onclose", {
detail,
bubbles: true, // 允许事件冒泡
composed: true, // 允许事件穿透shadow DOM
});
this.shadowRoot
.querySelector(".show-dialog")
.addEventListener("click", () => {
if (mode === "show") {
this.dialog.show();
} else {
this.dialog.showModal();
}
});
this.shadowRoot.querySelector(".close").addEventListener("click", () => {
this.dialog.close("top");
});
this.dialog.addEventListener("close", (e) => {
// 触发自定义事件
this.dispatchEvent(closeEvent(this.dialog.returnValue));
});
}
}
以上这个代码,我就封装了一个基于html5新标签dialog封装的弹窗组件,一下演示使用方法。
<custom-dialog></custom-dialog>
<custom-dialog
mode="modal"
content="modal模式演示有close监听"
></custom-dialog>
<custom-dialog
mode="modal-form"
content="modal模式演示带有表单"
></custom-dialog>
以上代码我来简单分解下:
1 定义style
首先就是要给shadow Dom中要插入style标签和你自定义内容的html代码,这样来实现样式和html代码封装,主要的还是要实现代码的隔离。当然你接下来的交互,自然也要约束在你组件内部。
2 组件内查找节点
当然你js中节点事件处理,还是要基于this.shadowRoot.querySelector去做节点查找,然后使用原生的方法去做事件监听。
3 自定义事件派发:
new CustomEvent("onclose", {
detail,
bubbles: true, // 允许事件冒泡
composed: true, // 允许事件穿透shadow DOM
});
这里就是定义一个自定义事件,然后使用this.dispatchEvent(closeEvent(this.dialog.returnValue))派发出去。
4 注册组件
接下来我们聊聊这个注册组件,以上代码仅仅是你定义组件,页面上还并不能直接使用。customElements.define(`custom-${i}`, dialog); 使用以上方法,才能在页面上以自己定义的名称使用。
2 插槽使用
插槽的使用给我们页面开发带来很大的方便,这里我们演示一下插槽的使用方法。在web components中插槽沿革执行了,组件的shadow-dom的特性,即插槽部分的样式定义和js是不受shadow-dom的this.shadowRoot的影响的,因为插槽为自定义内容,其内容被渲染也不属于组件shadow-dom内部,所以插槽被渲染出来dom结构并不在组件内部。
1 通过slot来定义插槽
<div slot="desc">
纯html+css实现,
无法实现更为复杂的功能和交互,而且无法实现shadow-root实现样式隔离
</div>
2 使用插槽
在组件内部通过 <slot name="">来使用,如下。
const editorHtml = `
<div class="editor-desc">
<slot name="desc"></slot>
</div>
<div class="editor-code">
<div class="code-type">${codeType}</div>
<div class="code-container">
<div class="code-lines"></div>
<div class="code-content">
<slot name="code"></slot>
</div>
</div>
</div>
`;
3 插槽的渲染
其渲染出来的效果如图:
这里需要特别注意插槽为自定义内容,其内容被渲染也不属于组件shadow-dom内部,所以插槽被渲染出来dom结构并不在组件内部,特别实在做样式控制时要特别注意,需要在使用组件的页面上定义样式。如果是在嵌套环境更要注意。
3 样式部分
因为shadow-dom的特性,我们样式只可以写在组件内部。但是也有一些技巧。这里我分享一个就是做样式分离的方法。
const style = document.createElement("link");
style.setAttribute("rel", "stylesheet");
style.setAttribute("href", "./src/assets/editor.css");
this.shadowRoot.appendChild(style);
1 分离css方法
创建一个link标签,将样式从外部链接进来,这样就可以不用做style这么麻烦了。
当然后面我再讲一讲如何做html代码分离。
样式分离了,但是这里面又有一个问题就是,样式穿透问题。这里就不得不说几个基于web component的味蕾和和选择器。
:host 这个使用在组件样式内部,表示跟组件。使用这个我们可以约束根组件的样式。通常我们封装的组件都要通过一个div元素放在根组件内部,而这个div通常我们并没有给其设置类名。这时使用这个伪类就比较好。
:host() 只选择自身包含特定选择器的自定义元素;
:host-context() 选择拥有特定选择器父元素的自定义元素。
这些选择器后面详细来讲,这里我主要谈一下,样式的从父级项向下穿透的样式规则和需求。比如我们通常定义样式有全局级别的,页面级别的,还有小组件级别的。但是现在这种shadow-dom样式的完全隔离,向实现其实比较困难。
2 样式控制
我先说第一种:页面级别,其页面级别样式可以直接影响到自己页面本身和插槽的样式,这个也很好理解。因为插槽本身就是就是小组件外的内容,样式的作用域能控制到当前页面,也就能控制当前页面内的插槽。特别是在组件嵌套的情况下,当家控制要是要特别注意。类似于这个:host/:host()都可以往出派生,都比较容易控制。
第二种情况就比较特殊了,就是全局样式。全局样式如果想穿透组件,控制组件内部,其实是比较麻烦的。这里我来详细讲一下。主要使用了::part伪元素。
.page::part(case-container) {
margin: 10px 0;
border: 1px solid #f6f6f6;
}
.page::part(case-title) {
padding: 0 10px;
line-height: 25px;
font-size: 16px;
}
.page::part(case-content) {
background-color: #f6f6f6;
padding: 10px;
}
3 样式穿透
这里大家看到,这个::part()括号中的是对应的组件.page中part属性的属性值的元素。
如上图,这些html标签都被协商了part的key-value键值对。
前面的.page大家注意,这个地方本来是要写组件名称的,但如果写组件名称,作用域就太小了,所以我们给由相同需求的组件写一个类名,这样作用范围就更广,来实现更多需求全局样式穿透。
这个地方是给跟组件设置class的方法,建议大家在这个地方处理,这样的化,组件本身就具备了类名。但是::part不能派生,这也是个限制,希望官方后期能更新这个限制。
this.setAttribute("class", "page");
今天,就先分享到这,我的第一部分的原生html-web components项目部分已经马上完成,即将和大家见面。大家请关注。