如何分别在Angular、React、Preact、Vue和Svelte中使用Web组件? - netbasal


Web组件使我们能够建立可重复使用的、可定制的元素。网络组件最大的优点是它的互操作性:由于浏览器原生支持,网络组件可以在任何HTML环境中使用,与任何框架一起使用,或者根本不使用框架。

网络组件的一个主要方面是封装。你可以分离和隐藏标记结构、样式和行为,这样不同的页面部分就不会发生冲突。

因此,网络组件是开发设计系统、可共享组件和嵌入式小工具的完美选择。
让我们来看看如何用行业内的顶级库来使用本地Web组件。
 
使用 Lit 创建 Web 组件
使用纯 Javascript 从头开始​​构建 Web 组件很快就会变得一团糟,难以扩展和维护。
幸运的是,我们可以在Lit的帮助下构建快速、轻量级的 Web 组件。
Lit 的核心是一个消除样板的组件基类,它提供反应状态、作用域样式和一个小巧、快速且富有表现力的声明性模板系统。

让我们构建一个最小的自定义select元素,看看我们如何处理原始和对象输入值。
请注意,我们不会讨论构建工具。你可以随意使用任何你想要的东西。
 让我们从select元素开始:

import { css, html, LitElement } from 'lit';

export class SelectElement<T extends Record<string, unknown>> extends LitElement {
  static styles = css`
    button[active] {
      background-color: honeydew
    }
  `
  @property({ type: Array }) 
  data: T[] = [];
  
  @property({ attribute: 'id-key' }) 
  idKey: keyof T = 'id' as keyof T;
  
  @property({ attribute: 'val-key' }) 
  valKey: keyof T = 'label' as keyof T;
  
  @property({ state: true }) 
  private activeItem: T | null = null;

  render() {    
    return html``
  }
}

customElements.define('ui-select', SelectElement);

我们创建一个SelectElement组件,并定义三个输入:data、idKey和valKey。
我们还定义了一个activeItem状态来跟踪当前选定的项目。

继续加入方法功能:

import { css, html, LitElement } from 'lit';
import { repeat } from 'lit/directives/repeat.js';

export class SelectElement extends LitElement {
  ...

  selectItem(item: T) {
    this.activeItem = item;

    const event = new CustomEvent<T>('select-item', {
      detail: item,
      bubbles: true,
      composed: true
    });

    this.dispatchEvent(event);
  }

  render() {    
    return html`
      <p>Active: ${this.activeItem ? this.activeItem[this.valKey] : 'None' }</p>
      
      ${repeat(
         this.data, 
         current => current[this.idKey], 
         current => html`
          <button ?active=${this.activeItem?.[this.idKey] === current[this.idKey]} 
                  @click=${() => this.selectItem(current)}>
            ${current[this.valKey]}
          </button>
      `)}
      `
  }
}

customElements.define('ui-select', SelectElement);

我们加入第三方库中的repeat指令来有效地渲染我们的元素, 它接收三个参数

  1. 一个collection:
  2. 一个keyFunction(将单个项目作为参数并返回其唯一的键),
  3. 以及一个itemTemplate(将项目和其当前索引作为参数并返回TemplateResult)。

点击一个项目会将其设置为活动项目,并派发一个自定义的事件,父级可以监听。

我们使用Lit的一个很好的功能,叫做布尔属性表达式。活动属性将根据表达式的结果被添加或删除。我们用它来设计活动元素。
如果你正在寻找一个lit文件生成器,你可能会发现这个库很有用。
 
在Angular中使用Web组件
首先,我们需要使用CUSTOM_ELEMENTS_SCHEMA模式。Angular会忽略它不认识的自定义元素(用破折号命名),而不是抛出一个错误。

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TodosComponent } from './todos.component';

@NgModule({
  declarations: [TodosComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class TodosModule {}

现在我们可以在TodosComponent组件内使用我们的ui-select网络组件。

import '@org/ui/lib/select';
import { Component } from '@angular/core';
import { Todo, randTodo } from '@ngneat/falso';

@Component({
  selector: 'app-todos',
  template: `
    <button (click)="replace()">Replace</button>
    <ui-select [data]=
"todos" 
               (select-item)=
"onSelect($event)" 
               val-key=
"title">
    </ui-select>   
  `
})
export class TodosComponent {
  todos: Todo[] = randTodo({ length: 3 });

  replace() {
    this.todos = randTodo({ length: 3 });
  }

  onSelect(e: CustomEvent<Todo>) {
    console.log(e.detail);
  }
}

我们需要做的不多。Angular与自定义元素的集成是开箱即用的。

  • 它将todos组件属性与ui-select元素的数据属性绑定。
  • 事件绑定监听select-item 事件,并在它被触发时调用组件的onSelectItem()方法。

 
在Vue中使用Web组件
和Angular一样,我们需要告诉Vue忽略自定义元素,这样它在遇到自定义元素时就不会出现错误。下面是我们如何在Vite中做到这一点。
// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: {
         
// treat all tags with a dash as custom elements
          isCustomElement: (tag) => tag.includes('-')
        }
      }
    })
  ]
}

现在我们可以在我们的组件中使用我们的自定义元素。
<script setup lang="ts">
import { ref } from 'vue';
import '@org/ui/lib/select';
import { randTodo } from '@ngneat/falso';
const todos = ref(randTodo({ length: 3} ))
function replace() {
  todos.value = randTodo( {length: 3})
}
function onSelect(event: CustomEvent<Todo>) {
  console.log(event.detail)
}
</script>

<template>
  <button @click=
"replace">Replace</button>
  <ui-select :data=
"todos" @select-item="onSelect" val-key="title"></ui-select>
</template>

Vue让使用自定义元素变得轻而易举。
我们将该元素的数据属性与todos ref绑定,并监听select-item事件,当它被触发时,运行onSelect函数。
  

在Svelte中使用Web组件
要在Svelte组件中使用自定义元素,我们不需要做任何特别的事情。导入自定义元素并使用它。

<script lang="ts">
  import '@org/ui/lib/select';
  import { Todo, randTodo } from '@ngneat/falso';
  let todos: Array<Todo> = randTodo({ length: 3 });
  function replace() {
    todos = randTodo({ length: 3 });
  }
  function onSelect(e: CustomEvent<Todo>) {
    console.log(e.detail);
  }
</script>

<main>
  <button on:click={replace}>Replace</button>
  <ui-select data={todos} val-key=
"title" on:select-item={onSelect} />
</main>

 
 
在React中使用Web组件
React对自定义元素的支持是最不友好的。让我们看看我们如何用React来使用它。

import '@org/ui/lib/select';
import { randTodo, Todo } from '@ngneat/falso';
import { SelectElement } from '@org/ui/lib/select';
import { useEffect, useRef, useState } from 'react';

export function Todos() {
  const [todos, setTodos] = useState<Todo[]>(randTodo({ length: 3 }));
  const ref = useRef<HTMLElement>();

  useEffect(() => {
    const callback = (e: CustomEvent<Todo>) => {
      console.log(e.detail);
    };

    ref.current!.addEventListener('select-item', callback);

    return () => ref.current!.removeEventListener('select-item', callback);
  }, [])

  return (
    <>
      <button onClick={() => setTodos(randTodo({ length: 3 }))}>Replace</button>

      <ui-select ref={ref} data={JSON.stringify(todos)} val-key="title"></ui-select>
    </>
  );
}

由于自定义元素不是React组件,它们被视为标准的HTML元素。
我们需要使用JSON.stringify,因为数据属性值被当作一个字符串处理。
由于我们将数据属性转换器设置为数组,Lit将处理解析工作。
我们需要获得自定义元素的引用,并手动注册该事件,因为React不支持自定义元素事件。

为了让它更干净,你可以使用诸如use-custom-element这样的库。

import '@org/ui/lib/select';
import { randTodo, Todo } from '@ngneat/falso';
import { SelectElement } from '@org/ui/lib/select';
import { useState } from 'react';
import useCustomElement from 'use-custom-element';

export function Todos() {
  const [todos, setTodos] = useState<Todo[]>(randTodo({ length: 3 }));

  const [customElementProps, ref] = useCustomElement({
    data: todos,
    'val-key': 'title',
    'select-item'(e: Todo) {
      console.log(e);
    }
  });

  return (
    <>
      <button onClick={() => setTodos(randTodo({ length: 3 }))}>Replace</button>

      <ui-select  {...customElementProps} ref={ref}></ui-select>
    </>
  );
}

 
 
在Preact中使用Web组件
Preact对Web组件的支持比React好得多。Preact的渲染器通过检查DOM元素来决定是否使用一个属性或属性。
传递给DOM元素的未被识别的事件处理道具会完全按照指定的方式使用其套管进行注册。
import '@org/ui/lib/select';
import { randTodo } from '@ngneat/falso';
import { useState } from 'preact/hooks';

const Todos = () => {
   const [todos, setTodos] = useState(randTodo({ length: 3 }));

   return (
    <>
      <button onClick={() => setTodos(randTodo({ length: 3 }))}>Replace</button>
      <ui-select data={todos} 
                 val-key="title" 
                 onselect-item={(e) => console.log(e.detail)}>
      </ui-select>
    </>
   )
}