看完本篇,教你用Lit实现简单组件开发

496 阅读2分钟

1、介绍

本文介绍皆基于Lit3.1.0版本 根据React的心智来描述Lit 在使用Lit之前建议先了解Web Component ,Lit是基于Web Component实现的框架。

Lit定义组件的方式与web component相同使用customElements.define来注册自定义元素,代码示例中的MyElement通常是传入元素的类。 Lit 是基于web component实现的框架,使用Lit几乎可以构建任何web页面,构建的组件无需考虑任何框架环境,ReactVue都可以嵌入Lit组件,适合有一定状态管理诉求的基础通用组件开发使用。

if(!customElements.get('my-element',)){
  customElements.define('my-element', MyElement);
}

2、渲染

  1. 基础渲染方式

Lit的渲染,只需要使用html方法用字符串包裹在render方法中返回,即可渲染Dom元素。定义的变量需要使用${}来进行包裹,这一点和大多数框架趋同。 对列表进行遍历也和React相同,可以使用map方法来进行遍历渲染,同样和React相同的可以使用函数直接进行渲染。 从其他页面导入组件进行组件化开发也十分便捷,只需要定义另一个组件导入即可。

import { LitElement, html } from "lit";
import "./my-header.js";
const name = "hello Lit";
const list = [1, 2, 3, 4];
class MyElement extends LitElement {
  render() {
    return html`<p>Hello from my template.${name}</p>
      ${list.map((item) => {
        return html`<div>${item}</div>`;
      })}
      ${this.divDom()}
      <my-header></my-header> `;
  }
​
  divDom() {
    return html`<div>divDom</div>`;
  }
}
customElements.define("my-element", MyElement);
​
import {LitElement, html} from 'lit';
​
class MyHeader extends LitElement {
  render() {
    return html`
      <header>header</header>
    `;
  }
}
customElements.define('my-header', MyHeader);
  1. slot 插槽

使用普通插槽,只需要在组件中使用<slot> </slot>为外部传入的子元素占好位置。

<my-element>
  <p>Render me in a slot</p>
</my-element>
​
​
​
export class MyElement extends LitElement {
  protected render() {
    return html`
      <p>
        <slot></slot>
      </p>
    `;
  }
}
​

使用命名插槽 需要为slot和传入元素双方共同命名,<div slot='box'>box </div> <slot name='box'> <slot> , 具备属性slot的元素 只会在对应name的插槽中渲染,对于需要使用多个slot,需要确定对应位置的组件十分必要。

3、状态

声明状态

在静态的properties类字段中声明属性 , 熟悉React的同学可以简单理解为state,虽然两者本质并不相同。

class MyElement extends LitElement {
  static properties = {
    mode: {type: String},
    data: {attribute: false},
  };
​
  constructor() {
    super();
    this.data = {};
  }
}

状态更新

  1. 状态更新过程

状态更新会触发property更新周期,模板会随之改变

1、调用组件的setter方法。

2、setter组件调用requestUpdata方法。

3、比较property新旧状态,即(newValue !== oldValue),有hasChanged函数会传入新旧状态调用。

4、检测到变化,即会安排异步更新,多次变化只会执行一次。

5、调用updata方法,更新模板

  1. 复杂数据类型更新

需要注意的是 Array 和 Object 类型的数据不会触发更新,即复杂数据类型在上述第三步会判断为false。

在改变ArrayObject两种数据类型时,可以使用不可变数据类型,构建一个新数据改变新数据后再对老数据进行赋值,或者使用this.requestUpdata手动触发更新视图。

  1. 观察属性和反应属性

观察属性attribute

Lit会为所有公共反应式属性添加观察属性,观察到的属性名称就是属性名会转化为小写。可以用attribute 创建不同的名字。 为了防止属性创建观察属性可以将attribute设置为false , 这样将不会创建观察属性,属性的变化也不会影响到元素属性 。 观察到的属性可以为状态提供初始值 例如:<my-element myvalue="99"></my-element>

反应属性 (reflect

当定义 reflecttrue时,将会开启反应属性,此时你定义的属性的任何变化都将反应在元素身上,开启后可以直接作用在cssDOM Api

export class App extends LitElement {
    static get properties() {
        return {
            title: { type: String, reflect: true },
        }
    }
    render() {
        return html`
                <div> ${this.title}</div>
                <Button @click='${() => ( this.name = 'click name' )}'>click</Button>
        `
    }
}

image.png

4、通信

  1. 父向子通信 值传递

React相似的是 直接将需要传递的值通过标签进行传递,<my-lit props='propsValue'></my-lit>,需要注意的是不能使用这种方法传递函数。子组件只需要static properties = {props: {type: String}}这样定义即可。

  1. 父子双向通信 通过事件侦听器来实现

事件侦听器是指通过自定义事件,使用@来进行监听。需要注意的是监听的位置的组件需要包裹子组件,详细代码看章节末尾。

  1. 通过 addEventListener 事件监听

不做过多介绍与原生的事件监听没有太多区别,需要注意composed设置允许在Shadow DOM上派发事件,和 bubbles选项允许事件沿着 DOM 树向上流动到调度元素的祖先。如果您希望事件能够参与事件委托,那么设置此标志非常重要,详细代码见下文。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <my-component></my-component>
    <script type="module" src="../src/index.js"></script>
  </body>
</html>
import { LitElement, html } from 'lit';
import './component/input'
class MyComponent extends LitElement {
  createRenderRoot() {
    const root = super.createRenderRoot();
    root.addEventListener('mylogin', (e) => {
      console.log(e.detail,'addEventListener')
    })
    return root;
  }
  change(e) {
    console.log(e.detail,'自定义事件')
  }
​
  render() {
    return html`
          <div @mylogin=${this.change}>这是我的Lit组件 <my-input></my-input> </div>
      `;
  }
}
​
if (!customElements.get('my-component')) {
  customElements.define('my-component', MyComponent);
}
import { LitElement, html } from 'lit';
class MyInput extends LitElement {
    static properties = {
        value: { type: String },
        props:{type:Object}
    };
    constructor() {
        super();
        this.value = '';
    }
    render() {
        return html`
          <input
           @input=${(e) => {
                this.dispatchEvent(new CustomEvent('mylogin',
                    { bubbles: true, composed: true, cancelable: true, detail: e.target.value }
                ));
            }}>input</input>
      `;
    }
}

if (!customElements.get('my-input')) {
    customElements.define('my-input', MyInput);
}

5、响应事件(表达式)

React自己实现了一套事件机制,Lit是基于原生的实现,两者在表现上相似 但在实现上完全不同。

  1. 特性:html<input .value=${this.itemCount}>` ==inputEl.value = this.itemCount`

也就是通过.操作符,Lit帮我们完成了对inputvalue 赋值的操作

  1. 事件监听器:<div @click="${this.onClick}">click<div>,其实这就类似于调用

addEventListener,需要注意的是React开发者们习惯了input直接使用onChange方法来获取数据的变化,这在原生或是Lit中都是不行的,详情可见这里

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

class MyComponent {
  constructor() {
    this.value = '';
  }

  render() {
    return html`
      <input value=${this.value} @input=${(e) => { this.value = e.target.value; }}>
    `;
  }
}

Lit框架中关于事件或表达式 很灵活包括 自定义事件Ref等等API,更复杂的需要可以自行到官网查看

6、生命周期

Lit组件是标准的自定义元素,继承标准自定义元素的生命周期。

// Create a class for the element
class MyCustomElement extends HTMLElement {
  static observedAttributes = ["color", "size"];

  constructor() {
    // Always call super first in constructor
    super();
  }

  connectedCallback() {
    console.log("Custom element added to page.");
  }

  disconnectedCallback() {
    console.log("Custom element removed from page.");
  }

  adoptedCallback() {
    console.log("Custom element moved to new page.");
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`Attribute ${name} has changed.`);
  }
}

customElements.define("my-custom-element", MyCustomElement);

在标准自定义元素生命周期以为,Lit自己实现了一套反应式更新周期image.png image.png image.png 对于反应式属性的初始化会放在constructor中进行初始化,this.requestUpdata()方法可以强制触发更新,如果你想要在页面初始化时加载请求可以在firstUpdated中完成

7、Shadow DOM 和 Styles

Shadow DOM 是web Component最重要的组成部分之一

Lit组件使用Shadow DOM来封装DOM ,Shadow DOM构建了一种独立的DOM空间,内部元素与样式无法被外部所改变,更多信息可以看这里

Lit将组件渲染到renderRoot,所以如何想要在Lit中使用原生的方法获取DOM可以使用this.renderRoot.querySelector('#box')这样的方法。

样式可以采取<style></style>标签的形式引入css样式

import style from './index.css'

render() {
  return html`
    <style>
      /* updated per instance */
    </style>
    <div>template content</div>
  `;
}

8、总结

Lit适合开发兼容场景更多的基础定制组件开发,适合进行有简单状态管理需求的组件,他的优点和缺点都十分明显,优点在无视任何框架环境,同原生没有差别,即插即用组件不受外部框架影响,缺点同样明显 基础设施相较于Reactvue依然缺乏,使用Lit搭建大型项目存在学习成本,Shadow DOM 成也萧何败也萧何,内部样式无法被外部修改使得组件开发成本变高。 最后需要注意的是在React中使用Lit开发的组件请阅读这篇文章