Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebComponents框架direflow实现原理 #258

Open
FrankKai opened this issue Apr 27, 2022 · 0 comments
Open

WebComponents框架direflow实现原理 #258

FrankKai opened this issue Apr 27, 2022 · 0 comments

Comments

@FrankKai
Copy link
Owner

FrankKai commented Apr 27, 2022

这个框架支持React方式写WebComponents。
框架地址:https://github.com/Silind-Software/direflow

假设有这样一个web component组件。

<test-component name="jack" age="18" />

完整构建步骤

一个完整的direflow web component组件,包含以下步骤。

  1. 创建一个web component标签
  2. 创建一个React组件,将attributes转化为properties属性转化并传入React组件(通过Object.defineProperty做劫持,通过attributeChangedCallback做attribute实时刷新)
  3. 将这个React应用,挂载到web component的shadowRoot

direflow

direflow的配置如下:

import { DireflowComponent } from "direflow-component";
import  App from "./app";

export default DireflowComponent.create({
  component: App,
  configuration: {
    tagname: "test-component",
    useShadow: true,
  },
});

创建一个Web component

const WebComponent = new WebComponentFactory(
  componentProperties,
  component,
  shadow,
  anonymousSlot,
  plugins,
  callback,
).create();

customElements.define(tagName, WebComponent);

通过customElements.define声明一个web component,tagName为"test-component",WebComponent为集成了渲染react组件能力的的web components工厂函数实例。

web components工厂函数

响应式

劫持所有属性。

public subscribeToProperties() {
  const propertyMap = {} as PropertyDescriptorMap;
  Object.keys(this.initialProperties).forEach((key: string) => {
    propertyMap[key] = {
      configurable: true,
      enumerable: true,

      set: (newValue: unknown) => {
        const oldValue = this.properties.hasOwnProperty(key)
          ? this.properties[key]
          : this.initialProperties[key];

        this.propertyChangedCallback(key, oldValue, newValue);
      },
    };
  });

  Object.defineProperties(this, propertyMap);
}

首先,将attributes转化为properties。
其次,通过Object.defineProperties劫持properties,在setter中,触发propertyChangedCallback函数。

const componentProperties = {
  ...componentConfig?.properties,
  ...component.properties,
  ...component.defaultProps,
};

上面这段代码中的property变化时,重新挂载React组件。(这里一般是为了开发环境下,获取最新的视图)

/**
 * When a property is changed, this callback function is called.
 */
public propertyChangedCallback(name: string, oldValue: unknown, newValue: unknown) {
  if (!this.hasConnected) {
    return;
  }

  if (oldValue === newValue) {
    return;
  }

  this.properties[name] = newValue;
  this.mountReactApp();
}

attribute变化时,重新挂载组件,此时触发的是web components原生的attributeChangedCallback。

public attributeChangedCallback(name: string, oldValue: string, newValue: string) {
  if (!this.hasConnected) {
    return;
  }

  if (oldValue === newValue) {
    return;
  }

  if (!factory.componentAttributes.hasOwnProperty(name)) {
    return;
  }

  const propertyName = factory.componentAttributes[name].property;
  this.properties[propertyName] = getSerialized(newValue);
  this.mountReactApp();
}

创建一个React组件

对应上面的this.mountReactApp。

<EventProvider>
  {React.createElement(factory.rootComponent, this.reactProps(), anonymousSlot)}
<EventProvider>

EventProvider-创建了一个Event Context包裹组件,用于web components组件与外部通信。
factory.rootComponent-将DireflowComponent.create的component传入,作为根组件,并且通过React.createElement去创建。
this.reactProps()-获得序列化后的属性。为什么要序列化,因为html标签的attribute,只接收string类型。因此需要通过JSON.stringify()序列化传值,工厂函数内部会做JSON.parse。将attribute转化为property
anonymousSlot-匿名slot,插槽。可以直接将内容分发在web component标签内部。

挂载React应用到web component

const root = createProxyRoot(this, shadowChildren);
ReactDOM.render(<root.open>{applicationWithPlugins}</root.open>, this);

代理组件将React组件作为children,ReactDOM渲染这个代理组件。

web component挂载到DOM时,挂载React App

public connectedCallback() {
  this.mountReactApp({ initial: true });
  this.hasConnected = true;
  factory.connectCallback?.(this);
}

创建一个代理组件

主要是将Web Component化后的React组件,挂载到web component的shadowRoot。

const createProxyComponent = (options: IComponentOptions) => {
  const ShadowRoot: FC<IShadowComponent> = (props) => {
    const shadowedRoot = options.webComponent.shadowRoot
      || options.webComponent.attachShadow({ mode: options.mode });

    options.shadowChildren.forEach((child) => {
      shadowedRoot.appendChild(child);
    });

    return <Portal targetElement={shadowedRoot}>{props.children}</Portal>;
  };

  return ShadowRoot;
};

获取到shadowRoot,没有的话attachShadow新建一个shadow root。
将子结点添加到shadow root。
返回一个挂载组件到shadow root的Portal,接收children的高阶函数。

思考

为什么要每一次attribute变化都要重新挂载React App?不能把它看做一个常规的react组件吗,使用react自身的刷新能力?

因为direflow的最终产物,是一个web component组件。
attribute变化,react是无法自动感知到这个变化的,因此需要通过监听attribute变化去重新挂载React App。
但是!React组件内部,是完全可以拥有响应式能力的

direflow是一个什么框架?

其实,direflow本质上,是一个 React组件 + web component +web component属性变化重新挂载React组件的 web component框架。

所以,direflow的响应式其实分为2块:
组件内部响应式(通过React自身响应式流程),组件外部响应式(WebComponents属性变化监听重渲染组件)。

如果外部属性不会经常变化的话,性能这块没有问题,因为组件内部的响应式完全是走了React自身的响应式。
属性外部属性如果会经常变化的话,direflow框架在这块还有一定的优化空间。

@FrankKai FrankKai changed the title WebComponents框架direflow原理 WebComponents框架direflow实现原理 Apr 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant