帮助
支持我们

Web 组件

Preact 的小巧体积和标准优先的方法使其成为构建 Web 组件的绝佳选择。

Web 组件是一组标准,可用来构建新的 HTML 元素类型 - 自定义元素,如 <material-card><tab-bar>。Preact 完全支持这些标准,允许无缝使用自定义元素生命周期、属性和事件。

Preact 旨在渲染完整应用程序和页面的各个部分,使其非常适合构建 Web 组件。许多公司使用它来构建组件或设计系统,然后将其包装成一组 Web 组件,从而可以在多个项目和其它框架中重复使用。

Preact 和 Web 组件是互补技术:Web 组件提供了一组用于扩展浏览器的低级基元,而 Preact 提供了一个高级组件模型,可以位于这些基元之上。



渲染 Web 组件

在 Preact 中,Web 组件与其他 DOM 元素的工作方式相同。它们可以使用其已注册的标签名称进行渲染

customElements.define('x-foo', class extends HTMLElement {
  // ...
});

function Foo() {
  return <x-foo />;
}

属性和特性

JSX 不提供区分属性和特性的方法。自定义元素通常依赖于自定义属性,以便支持无法表示为属性的复杂值设置。这在 Preact 中效果很好,因为渲染器会通过检查受影响的 DOM 元素自动确定是否使用属性或特性设置值。当自定义元素为给定属性定义 setter 时,Preact 会检测到它的存在,并使用 setter 而不是属性。

customElements.define('context-menu', class extends HTMLElement {
  set position({ x, y }) {
    this.style.cssText = `left:${x}px; top:${y}px;`;
  }
});

function Foo() {
  return <context-menu position={{ x: 10, y: 20 }}> ... </context-menu>;
}

使用 preact-render-to-string(“SSR”)渲染静态 HTML 时,不会自动序列化上述对象之类的复杂属性值。它们在客户端上对静态 HTML 进行水合后应用。

访问实例方法

为了能够访问自定义 Web 组件的实例,我们可以利用 refs

function Foo() {
  const myRef = useRef(null);

  useEffect(() => {
    if (myRef.current) {
      myRef.current.doSomething();
    }
  }, []);

  return <x-foo ref={myRef} />;
}

触发自定义事件

Preact 规范化了标准内置 DOM 事件(通常区分大小写)的大小写。这就是可以将 onChange 道具传递给 <input> 的原因,尽管实际事件名称是 "change"。自定义元素通常会触发自定义事件作为其公共 API 的一部分,但是无法知道可能触发哪些自定义事件。为了确保在 Preact 中无缝支持自定义元素,传递给 DOM 元素的未识别的事件处理程序道具会使用完全按照指定的大小写进行注册。

// Built-in DOM event: listens for a "click" event
<input onClick={() => console.log('click')} />

// Custom Element: listens for "TabChange" event (case-sensitive!)
<tab-bar onTabChange={() => console.log('tab change')} />

// Corrected: listens for "tabchange" event (lower-case)
<tab-bar ontabchange={() => console.log('tab change')} />

创建 Web 组件

任何 Preact 组件都可以使用 preact-custom-element 变成 Web 组件,这是一个非常精简的包装器,符合自定义元素 v1 规范。

import register from 'preact-custom-element';

const Greeting = ({ name = 'World' }) => (
  <p>Hello, {name}!</p>
);

register(Greeting, 'x-greeting', ['name'], { shadow: false });
//          ^            ^           ^             ^
//          |      HTML tag name     |       use shadow-dom
//   Component definition      Observed attributes

注意:根据 自定义元素规范,标签名称必须包含连字符 (-)。

在 HTML 中使用新标签名称,属性键和值将作为道具传递

<x-greeting name="Billy Jo"></x-greeting>

输出

<p>Hello, Billy Jo!</p>

观察到的属性

Web 组件需要明确列出要观察的属性的名称,以便在值更改时做出响应。这些可以通过传递给 register() 函数的第三个参数来指定

// Listen to changes to the `name` attribute
register(Greeting, 'x-greeting', ['name']);

如果您省略了 register() 的第三个参数,则可以使用组件上的静态 observedAttributes 属性指定要观察的属性列表。这也适用于自定义元素的名称,可以使用 tagName 静态属性指定

import register from 'preact-custom-element';

// <x-greeting name="Bo"></x-greeting>
class Greeting extends Component {
  // Register as <x-greeting>:
  static tagName = 'x-greeting';

  // Track these attributes:
  static observedAttributes = ['name'];

  render({ name }) {
    return <p>Hello, {name}!</p>;
  }
}
register(Greeting);

如果没有指定 observedAttributes,则会从组件上(如果存在)的 propTypes 的键中推断出它们

// Other option: use PropTypes:
function FullName({ first, last }) {
  return <span>{first} {last}</span>
}

FullName.propTypes = {
  first: Object,   // you can use PropTypes, or this
  last: Object     // trick to define un-typed props.
};

register(FullName, 'full-name');

将插槽作为道具传递

register() 函数有第四个参数来传递选项。目前仅支持 shadow 选项,它将影子 DOM 树附加到指定的元素。启用后,这允许使用命名的 <slot> 元素将自定义元素的子元素转发到影子树中的特定位置。

function TextSection({ heading, content }) {
    return (
        <div>
            <h1>{heading}</h1>
            <p>{content}</p>
        </div>
    );
}

register(TextSection, 'text-section', [], { shadow: true });

用法

<text-section>
  <span slot="heading">Nice heading</span>
  <span slot="content">Great content</span>
</text-section>