深入浅出 solid.js 源码 (二十五)—— Web Components

1,058 阅读3分钟

这是我参与「掘金日新计划 · 8 月更文挑战」的第25天,点击查看活动详情

Web Components 是浏览器原生支持的组件规范,在现代浏览器中是一项通用技术。不过由于现在大多数应用都使用 Vue、React 或其他开发库来构建,Web Components 组件反而使用的人非常少。每个开发库有自己的组件系统,主要原因还是因为从前浏览器不支持原生的组件机制,类比从前浏览器没有 querySelector API,大家都选择使用 jQuery 来选取元素。现在浏览器的 Web Components 已经很成熟了,我们完全可以写一个 Web Components 组件,这样可以在任何环境下工作,不限于开发库。

原生的创建 Web Components 的方法很简单,我们可以写一个自定义的标签类继承自 HTML 现有的标签,在其内部使用 template 来创建 DOM 结构,使用 shadowDOM 来封装内容,在标签的生命周期中处理输入和输出,最后通过 customElements.define 把标签注册到页面上,就可以向普通 html 标签一样使用了。

solid 也提供了 Web Components 的支持,我们可以很方便地把 solid 组件封装成 Web Components 组件,这样就可以在任何地方使用组件了。

Web Components 的实现位于 solid-element 目录下,同时内部还依赖了一个独立的仓库 github.com/ryansolid/c…

import { customElement } from 'solid-element';

customElement('my-component', {prop1: ''}, MySolidComponent)

这里主要用到的就是 customElement,其内部调用的是 component-register 中的 register 函数。

export function register<T>(
  tag: string,
  props = {} as PropsDefinitionInput<T>,
  options: RegisterOptions = {}
) {
  const { BaseElement = HTMLElement, extension } = options;
  return (ComponentType: ComponentType<T>) => {
    if (!tag) throw new Error("tag is required to register a Component");
    let ElementType = customElements.get(tag);
    if (ElementType) {
      // Consider disabling this in a production mode
      ElementType.prototype.Component = ComponentType;
      return ElementType;
    }

    ElementType = createElementType(BaseElement, normalizePropDefs(props));
    ElementType.prototype.Component = ComponentType;
    ElementType.prototype.registeredTag = tag;
    customElements.define(tag, ElementType, extension);
    return ElementType;
  };
}

在这部分中可以看到 customElements.define 的调用,传入的 ElementType 参数就是 Web Components 的自定义标签类,因此 createElementType 做的转化就是把 solid 组件转化为 Web Components 组件,这里内部做的就是一个标准的 Web Components 类的定义,源码在这里 github.com/ryansolid/c…,在 connectedCallback 中直接调用传入的组件函数,因为 solid 本身是没有虚拟 dom 的,这里构建的直接就是 html 的 template 模板,刚好就可以直接用于 Web Components 中。属性的监听也是通过在 attributeChangedCallback 中把属性修改传入 solid 组件中完成的。最后是把组件渲染到页面上,这里我们会注意到,传入的组件函数并不是原始的组件函数,而是使用 withSolid 封装了一层:

function withSolid<T>(ComponentType: ComponentType<T>): ComponentType<T> {
  return (rawProps: T, options: ComponentOptions) => {
    const { element } = options as {
      element: ICustomElement & { _$owner?: any };
    };
    return createRoot((dispose: Function) => {
      const props = createProps<T>(rawProps);

      element.addPropertyChangedCallback((key: string, val: any) => (props[key as keyof T] = val));
      element.addReleaseCallback(() => {
        element.renderRoot.textContent = "";
        dispose();
      });

      const comp = (ComponentType as FunctionComponent<T>)(props as T, options);
      return insert(element.renderRoot, comp);
    }, lookupContext(element));
  };
}

withSolid 本身会调用 createRoot 创建一个 solid 响应式节点,这样每个组件实际上就是一个小的 solid 应用,本身就具备了响应式能力,这里最终返回的是 insert(element.renderRoot, comp),insert 我们见过,就是把内容插入到页面中,这里的目标是 element.renderRoot,这个 renderRoot 就是刚刚在 Web Components 组件中创建的 shadowRoot。

把 solid 封装成 Web Components 来使用也是一个很值得尝试的方向,Web Components 不限于运行的环境,不依赖其他库,很容易做到跨技术栈共享,solid 又简化了 Web Components 的开发过程,适合构建跨应用组件。