Lit 是一个用于构建 Web Components 的轻量级库,它通过一系列优雅的 API 极大简化了原生 Web 组件的开发流程。
Web Component 是一套由 W3C 标准化的浏览器原生技术,旨在通过封装实现可复用、高内聚的定制化 HTML 元素。
Web Component 特点
自定义元素:允许开发者自定义新的 html 标签
影子 DOM:组件 DOM 树与主文档树样式隔离
HTML 模板:通过 template 和 slot 元素定义可复用的 HTML 片段,其中 slot 充当占位符
// my-counter.js
class MyCounter extends HTMLElement {
constructor() {
super();
// 创建 Shadow DOM 实现样式封装
this.attachShadow({ mode: 'open' });
// 初始化计数器值,优先使用属性值
this._count = parseInt(this.getAttribute('value')) || 0;
// 定义组件模板
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
font-family: system-ui;
text-align: center;
}
.counter-value {
font-size: 1.5rem;
font-weight: bold;
margin: 0 1rem;
min-width: 3rem;
display: inline-block;
}
button {
padding: 0.5rem 1rem;
font-size: 1.2rem;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background-color 0.2s;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.counter-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
</style>
<div class="counter">
<div class="counter-title">
<slot name="title">计数器</slot>
</div>
<div class="counter-controls">
<button id="decrement" part="decrement-button">-</button>
<span id="count" class="counter-value" part="counter-value">${this._count}</span>
<button id="increment" part="increment-button">+</button>
</div>
<div class="counter-actions">
<button id="reset" part="reset-button">重置</button>
</div>
</div>
`;
}
// 定义需要观察的属性,当这些属性变化时触发 attributeChangedCallback
static get observedAttributes() {
return ['value', 'min', 'max', 'step'];
}
connectedCallback() {
// 元素被添加到DOM时调用
this._bindEvents();
this._updateButtonStates();
}
disconnectedCallback() {
// 元素从DOM移除时调用,进行清理工作
this._unbindEvents();
}
attributeChangedCallback(name, oldValue, newValue) {
// 属性变化时调用
if (oldValue === newValue) return;
switch (name) {
case 'value':
this.count = parseInt(newValue) || 0;
break;
case 'min':
this._min = newValue !== null ? parseInt(newValue) : null;
this._updateButtonStates();
break;
case 'max':
this._max = newValue !== null ? parseInt(newValue) : null;
this._updateButtonStates();
break;
case 'step':
this._step = parseInt(newValue) || 1;
break;
}
}
// 获取和设置 count 值的接口
get count() {
return this._count;
}
set count(value) {
const oldValue = this._count;
this._count = value;
// 更新显示
if (this.shadowRoot) {
const countElement = this.shadowRoot.getElementById('count');
if (countElement) {
countElement.textContent = value;
}
}
// 更新按钮状态
this._updateButtonStates();
// 触发自定义事件
this.dispatchEvent(new CustomEvent('count-change', {
detail: { oldValue, newValue: value },
bubbles: true
}));
// 同步到 value 属性
this.setAttribute('value', value);
}
get min() {
return this._min !== undefined ? this._min : null;
}
set min(value) {
if (value === null || value === undefined) {
this.removeAttribute('min');
} else {
this.setAttribute('min', value);
}
}
get max() {
return this._max !== undefined ? this._max : null;
}
set max(value) {
if (value === null || value === undefined) {
this.removeAttribute('max');
} else {
this.setAttribute('max', value);
}
}
get step() {
return this._step || 1;
}
set step(value) {
this.setAttribute('step', value);
}
_bindEvents() {
// 绑定按钮事件
this.shadowRoot.getElementById('increment').addEventListener('click', () => this.increment());
this.shadowRoot.getElementById('decrement').addEventListener('click', () => this.decrement());
this.shadowRoot.getElementById('reset').addEventListener('click', () => this.reset());
}
_unbindEvents() {
// 解绑事件(实际应用中可能需要更精细的事件管理)
const incrementBtn = this.shadowRoot.getElementById('increment');
const decrementBtn = this.shadowRoot.getElementById('decrement');
const resetBtn = this.shadowRoot.getElementById('reset');
if (incrementBtn) incrementBtn.replaceWith(incrementBtn.cloneNode(true));
if (decrementBtn) decrementBtn.replaceWith(decrementBtn.cloneNode(true));
if (resetBtn) resetBtn.replaceWith(resetBtn.cloneNode(true));
}
_updateButtonStates() {
// 根据最小/最大值更新按钮状态
const decrementBtn = this.shadowRoot.getElementById('decrement');
const incrementBtn = this.shadowRoot.getElementById('increment');
if (decrementBtn) {
decrementBtn.disabled = this.min !== null && this.count <= this.min;
}
if (incrementBtn) {
incrementBtn.disabled = this.max !== null && this.count >= this.max;
}
}
// 公共方法
increment() {
if (this.max === null || this.count < this.max) {
this.count += this.step;
}
return this.count;
}
decrement() {
if (this.min === null || this.count > this.min) {
this.count -= this.step;
}
return this.count;
}
reset() {
const initialValue = parseInt(this.getAttribute('value')) || 0;
this.count = initialValue;
return this.count;
}
}
// 注册自定义元素
customElements.define('my-counter', MyCounter);
// 导出类,便于其他模块使用
if (typeof module !== 'undefined' && module.exports) {
module.exports = MyCounter;
}
上方代码是用原生方法实现,其中 this.shadowRoot.innerHTML 组件模版需要解析,如果使用很多次就都要解析一遍,这时候就涉及性能问题了。
优化
可以使用 Template element 元素把组件模版写进去,然后 this.shadowRoot.innerHTML,因为 template 在渲染后就在页面上,后续使用的时候只需要复制一份,不用重新创建,从而节省性能。
<template id="my-counter">
//innerHTML的内容
</template>
const template = document.querySelector('#my-counter');
this.shadowRoot.innerHTML = template.content.cloneNode(true);
这时候就会出现一个问题,template 中有涉及到字符串拼接,就需要用到模版字符串的原理了,可以看出用原生 JavaScript 开发 Web Component 复杂,可以使用第三方 Lit 库进行开发
// my-counter.js
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('my-counter') // 定义自定义元素标签名
export class MyCounter extends LitElement {
// 定义组件的静态样式,这些样式会被封装在影子DOM内
static styles = css`
:host {
display: block;
padding: 1rem;
text-align: center;
}
button {
font-size: 1.5rem;
margin: 0 0.5rem;
cursor: pointer;
}
span {
font-weight: bold;
color: blue;
}
`;
// 声明一个响应式属性。当它的值改变时,组件会自动更新
@property({ type: Number })
count = 0;
// 私有方法,处理点击事件
_increment() {
this.count++;
}
_decrement() {
this.count--;
}
// 定义组件的模板。模板中使用了事件监听绑定 (@click) 和文本插值
render() {
return html`
<button @click=${this._decrement}>-</button>
<span>Count: ${this.count}</span>
<button @click=${this._increment}>+</button>
`;
}
}
评论区