大家好,我是Jcode!
前言
近期手头在做一个html原生项目,需要使用组件化来构建代码,方便后面的维护、重用和解耦。 但是html怎么写原生组件呢?vue和React的组件写法,相信各位佬都是手到擒来的👍,那html组件的写法又有什么差异,需要注意什么?
概念
Web Component实际本身就是一个自定义的HtmlElement,能够创建可复用、封装良好的自定义元素的UI组件,它支持:
- 样式和事件相关隔离影子
Shadow(避免造成与原生代码冲突和样式污染)。 - 插槽
slot的使用,不同于Vue的是不能传递作用域插槽,有具名插槽和默认的插槽。 HTML Template:定义可复用的模板结构
在开发场景中,一些常见的对话框组件、下拉组件和特定功能的UI数据组件。
例子
对话框的自定义组件实现
- 模版
单独把模版放在html中,方便维护。也可以放在js中使用模版字符串去声明成一串字符串。
<!-- index.html -->
<style>
.dialog {
background-color: #d1e2e9;
padding: 20px;
border-radius: 5px;
width: fit-content;
min-width: 300px;
min-height: 200px;
position: absolute;
cursor: move;
}
.dialog-header {
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.dialog-footer {
justify-content: flex-end;
margin-top: 10px;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
}
</style>
<div class="dialog">
<div class="close-button" tabindex="0" role="button" aria-label="Close dialog">✕</div>
<div class="dialog-header">
<slot name="header">title</slot>
</div>
<div class="dialog-content">
<slot></slot>
</div>
<div class="dialog-footer">
<slot name="footer">
<button class="footer-button button button-primary" id="confim">Confirm</button>
<button class="footer-button button button-primary" id="cancel">Cancel</button>
</slot>
</div>
</div>
- 组件
//index.js
class XLayoutDialog extends HTMLElement {
constructor() {
super();
this._top = '50%';
this._left = '50%';
this.attachShadow({ mode: 'open' });//打开影子DOM模式
}
//监听变化的属性
static get observedAttributes() {
return ['left', 'top'];
}
// 元素添加到文档中时调用
async connectedCallback() {
await this.loadTemplate();
}
//属性更改、添加、移除或替换时调用 对应attributeChangedCallback函数写实现
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'left') {
this._left = newValue;
} else if (name === 'top') {
this._top = newValue;
}
if (this.dialog) this.updateOptions();
}
//动态加载模版
async loadTemplate() {
try {
const response = await fetch('/x-dialog-layout/index.html');
const text = await response.text();
//创建Fragment片段
const template = document.createRange().createContextualFragment(text);
this.shadowRoot.appendChild(template);
this.style.display = 'none';
this.initDialog();
this._render();
//发送自定义事件
this.dispatchEvent(new CustomEvent('load',{bubbles:true,cancelable: true});
} catch (error) {
console.error('Failed to load template:', error);
}
}
//初始化事件监听
initDialog() {
this.closeIcon = this.shadowRoot.querySelector('.close-button');
this.dialog = this.shadowRoot.querySelector('.dialog');
this.closeIcon.addEventListener('click', () => this.close());
this.dialog.addEventListener('mousedown', e => this.startDrag(e));
document.addEventListener('mousemove', e => this.drag(e));
document.addEventListener('mouseup', () => this.endDrag());
this.shadowRoot.querySelector('#confim').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('confirm', { bubbles: false, composed: false }), () => {
this.close();
});
});
this.shadowRoot.querySelector('#cancel').addEventListener('click', () => this.close());
}
_render() {
this.updateOptions();
}
set top(top) {
this._top = top;
}
set left(left) {
this._left = left;
}
open() {
this.style.display = 'block';
}
updateOptions(isInit = true) {
Object.assign(this.dialog.style, {
top: this._top,
left: this._left,
transform: isInit ? `translate(-${this._left}, -${this._top})` : 'none',
});
}
close() {
this.style.display = 'none';
}
startDrag(e) {
this.dragging = true;
const { left, top } = this.dialog.getBoundingClientRect();
this.offsetX = e.clientX - left;
this.offsetY = e.clientY - top;
}
drag(e) {
if (!this.dragging) return;
let x = e.clientX - this.offsetX;
let y = e.clientY - this.offsetY;
const dialogRect = this.dialog.getBoundingClientRect();
const maxLeft = window.innerWidth - dialogRect.width;
const maxTop = window.innerHeight - dialogRect.height;
x = Math.min(Math.max(0, x), maxLeft);
y = Math.min(Math.max(0, y), maxTop);
this._top = `${y}px`;
this._left = `${x}px`;
this.updateOptions(false);
}
endDrag() {
this.dragging = false;
}
}
//判断是否已经注册过
if (!customElements.get("x-dialog-layout")) {
customElements.define('x-dialog-layout', XLayoutDialog);
}
技巧
- 使用customElements.define注册的自定义组件是全局注册的。不能支持按需加载,只需要执行一次就行。
- 在自定义的组件上不能绑定浏览器不支持解析的dom事件(例如自定义事件),只能通过
Dom.addEventListener监听,当然这里也有一个暗渡陈仓的办法。
// bubbles: false 不冒泡,composed: false 仅在影子 DOM 内部传播,不会影响到外部 DOM
//手动触发load事件
this.dispatchEvent(new CustomEvent('load', { bubbles: false, composed: false }),this);
//监听事件
<x-dialog-layout onload="loadComponent(this)"></x-dialog-layout>
- 通过fetch获取的html模版中如果含有script标签且使用innerHTML生成dom的情况下,因为安全原因可能不会执行script,此时则可以使用appendChild方法。
欢迎指正!