Skip to content

lit-html

lit-html: js 中高效、富有表现力、可扩展的 HTML 模板。用于构建快速 Web Componentslit-html 允许您使用模板文字在 js 中编写HTML模板

使用

先安装lit-html,使用命令npm install lit-html,之后使用以下js进行测试,表示获取一个app元素,再通过lit-htmlhtml 创建模版,最后通过render将模版渲染到app元素中

  • html:用于生成 TemplateResult 的 js 模板标记 ,TemplateResult 是模板的容器,以及应填充模板的值
  • render():将 TemplateResult 渲染到 dom 容器(例如元素或阴影根)的函数
  • 并且打开控制台查看元素会发现只有count的值修改了,而不是像vue响应式那样连着dom元素一起在销毁重新创建
js
import {html, render} from 'lit-html';

let count = 0;

const app = document.querySelector('#app');

function doRender() {
  const template = html`<h1>${count}</h1>`;
  render(template, app);
}

setInterval(() => {
  count++;
  doRender();
}, 1000);

实现原理

  • 模板字面量标记(Tagged Template Literals):

    • html 是一个模板标签函数,它接收模板字符串和插值表达式
    • 通过 html 标记的模板会被解析成 TemplateResult 对象
  • 增量更新机制:

    • lit-html 会分析模板结构,识别出动态插值部分(如 ${count}
    • 首次渲染时创建 DOM 结构
    • 后续更新时只修改变化的部分,而不是重新渲染整个模板
  • 高效渲染:

    • render 函数负责将 TemplateResult 渲染到指定容器(如 #app
    • 通过智能的 DOM 操作,只更新需要变化的文本节点或属性

这种方式比传统字符串模板拼接更高效,比虚拟 DOM 更轻量。

Web Component

自定义 Web Component

首先做一个自定义的组件modify-switch,它是一个开关组件,当点击开关时,会切换checked 属性的值,这个时候直接在html当中使用,会发现是没有效果的,这是因为现在压根不知道这个render函数是干啥的

js
import {html} from 'lit-html';


class ModifySwitch extends HTMLElement {

  state = {
    checked: false
  };

  render() {
    return html`
        <div>
            <label>
                <input type="checkbox" checked="${this.state.checked}"/>
                <slot></slot>
            </label>
        </div>
    `;
  }
}

customElements.define('modify-switch', ModifySwitch);

封装中间层

  • 基类封装:

    • 作为其他 Web Components 的基类,提供通用功能
    • 继承自原生 HTMLElement
  • Lit-html 集成:

    • 导入并封装了 lit-htmlhtmlrender 函数
    • html 挂载为实例属性,方便子类使用
  • 自动渲染机制:

    • connectedCallback 生命周期方法中自动调用 render
    • 当组件被添加到 DOM 时会自动渲染,子类只需实现 render 方法返回模板即可

这样设计的好处是让其他组件可以继承这个基类,无需重复实现渲染逻辑,只需专注于组件自身的 render 方法实现

js
import {html, render} from 'lit-html';

export default class BaseHTMLElement extends HTMLElement {
  html = html;

  connectedCallback() {
    const content = this.render();
    render(content, this);
  }
}
js
import BaseHTMLElement from './BaseHTMLElement.js';

class ModifySwitch extends BaseHTMLElement {

  state = {
    checked: false
  };

  handleChange() {
    this.state.checked = !this.state.checked;
    console.log('checked:', this.state.checked);
  }

  render() {
    return this.html`
        <style>
            .switch {
                font-size: 17px;
                position: relative;
                display: inline-block;
                width: 3.5em;
                height: 2em;
            }

            .switch input {
                opacity: 0;
                width: 0;
                height: 0;
            }

            .slider {
                position: absolute;
                cursor: pointer;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background-color: rgb(182, 182, 182);
                transition: .4s;
                border-radius: 10px;
            }

            .slider:before {
                position: absolute;
                content: "";
                height: 1.4em;
                width: 1.4em;
                border-radius: 8px;
                left: 0.3em;
                bottom: 0.3em;
                transform: rotate(270deg);
                background-color: rgb(255, 255, 255);
                transition: .4s;
            }

            .switch input:checked + .slider {
                background-color: #21cc4c;
            }

            .switch input:focus + .slider {
                box-shadow: 0 0 1px #2196F3;
            }

            .switch input:checked + .slider:before {
                transform: translateX(1.5em);
            }
        </style>
        <label class="switch">
            <input type="checkbox" @change=${this.handleChange.bind(this)}>
            <span class="slider"></span>
        </label>
        <div>${this.state.checked ? 'ON' : 'OFF'}</div>
    `;
  }
}

customElements.define('modify-switch', ModifySwitch);

加入响应式

前言:因为vue3采用的是monorespo架构,所以可以直接单独引入@vue/reactivity模块,来实现响应式,修改BaseHTMLElement类如下

  • 添加的逻辑,先判断state是否为响应式对象,如果不是则转为响应式对象
  • 添加的逻辑,在connectedCallback中添加effect,当state发生变化时,会重新渲染
js
import {html, render} from 'lit-html';
import {effect, isReactive, reactive} from '@vue/reactivity';

export default class BaseHTMLElement extends HTMLElement {
  html = html;

  connectedCallback() {
    if (isReactive(this.state)) {
      this.state = reactive(this.state) || {};
    }
    effect(() => {
      const content = this.render();
      render(content, this);
    });
  }
}

样式隔离(Shadow DOM)

在前面实现的web Component中,没有做到样式隔离,不过在之前学习的微前端框架当中有知道web Component 怎么做这个样式隔离,微前端框架-Web Components

直接查找目标元素,设置为 shadowRoot 即可完成样式隔离

js
const content = document.querySelector('#app');
const shadowRoot = content.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `<div></div>`;

对于现在的webComponent只需要这样调整

js
export default class BaseHTMLElement extends HTMLElement {
  html = html;

  connectedCallback() {
    this.attachShadow({mode: 'open'});
    if (isReactive(this.state)) {
      this.state = reactive(this.state) || {};
    }
    effect(() => {
      const content = this.render();
      render(content, this.shadowRoot);
    });
  }
}

类型

  • 设置为 open 时,外部可以访问到 shadowRoot,可以通过 querySelector 等方法访问到 shadowRoot 中的元素
  • 设置为 closed 时,外部不能访问到 shadowRoot,不能通过 querySelector 等方法访问到 shadowRoot 中的元素

插槽

在组件中,可以使用 <slot> 标签来定义插槽,在组件中,可以使用 <slot name="xxx"> 标签来定义具名插槽,在外部使用组件和vue 一样

在外部通过插槽传递到组件内部时,是不受组件的ShadowDOM的样式影响的,当然了,如果真的想控制插槽的样式,可以通过::slotted 选择器来控制插槽的样式,

html

<div class="center">
    <slot name="center"></slot>
</div>
html

<modify-switch>
    <span slot="center">Switch</span>
</modify-switch>
css
::slotted(span) {
    font-size: 24px !important;
}

自定义事件

采用原生的事件机制,在组件内部抛出事件,在组件外部监听事件,实现组件之间的通信

js
this.dispatchEvent(new CustomEvent('change', {
  detail: this.state.checked
}));
js
const switchElement = document.querySelector('modify-switch');
switchElement.addEventListener('change', (e) => {
  console.log('change:', e.detail);
});

By Modify.