lit-html
lit-html: js 中高效、富有表现力、可扩展的 HTML 模板。用于构建快速 Web Components,lit-html 允许您使用模板文字在 js 中编写HTML模板
使用
先安装lit-html,使用命令npm install lit-html,之后使用以下js进行测试,表示获取一个app元素,再通过lit-html的html 创建模版,最后通过render将模版渲染到app元素中
html:用于生成TemplateResult的 js 模板标记 ,TemplateResult是模板的容器,以及应填充模板的值render():将TemplateResult渲染到 dom 容器(例如元素或阴影根)的函数- 并且打开控制台查看元素会发现只有
count的值修改了,而不是像vue响应式那样连着dom元素一起在销毁重新创建
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函数是干啥的
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-html的html和render函数 - 将
html挂载为实例属性,方便子类使用
- 导入并封装了
自动渲染机制:
- 在
connectedCallback生命周期方法中自动调用render - 当组件被添加到 DOM 时会自动渲染,子类只需实现
render方法返回模板即可
- 在
这样设计的好处是让其他组件可以继承这个基类,无需重复实现渲染逻辑,只需专注于组件自身的 render 方法实现
import {html, render} from 'lit-html';
export default class BaseHTMLElement extends HTMLElement {
html = html;
connectedCallback() {
const content = this.render();
render(content, this);
}
}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发生变化时,会重新渲染
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 即可完成样式隔离
const content = document.querySelector('#app');
const shadowRoot = content.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `<div></div>`;对于现在的webComponent只需要这样调整
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 选择器来控制插槽的样式,
<div class="center">
<slot name="center"></slot>
</div>
<modify-switch>
<span slot="center">Switch</span>
</modify-switch>::slotted(span) {
font-size: 24px !important;
}自定义事件
采用原生的事件机制,在组件内部抛出事件,在组件外部监听事件,实现组件之间的通信
this.dispatchEvent(new CustomEvent('change', {
detail: this.state.checked
}));const switchElement = document.querySelector('modify-switch');
switchElement.addEventListener('change', (e) => {
console.log('change:', e.detail);
});