侧边栏壁纸
  • 累计撰写 47 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

第三方库:Lit的原理和Web Component开发

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>
    `;
  }
}
0

评论区