WebComponents框架direflow實現原理

趁你還年輕發表於2022-04-29
這個框架支援React方式寫WebComponents。
框架地址:https://github.com/Silind-Sof...

假設有這樣一個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

image.png

下面再來詳細分析一下:

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框架在這塊還有一定的優化空間。

相關文章