如何在 React 中使用 Web Components


theme: fancy
highlight: tomorrow-night-bright

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

在之前的教程中,通过上下两篇实战教程,我们从 0 ~ 1 实现了一个 Web Components 下拉组件。也通过 Webpack & npm 将我们的 Web Components 下拉组件打包发布了。那在本教程中,你将学习如何在 React 应用中使用 Web Components 。如果在此之前你也想先构建一个自己的 Web Components 组件,请先参考之前的教程。

Web Components 入门实战(上篇)

Web Components 入门实战(下篇)

将 Web Components 组件打包发布

本教程中,将使用之前打包发布的下拉组件为例,将它在 React 应用中运用起来。废话不多说,Come on body,let’s go body。

从 React 组件到 Web Components

如果我们想使用一个第三方的组件,它代表 React 组件中的下拉组件,一般情况,我们可以导入这个组件并在我们的 React 组件中呈现它。比如这样:

npm install baixiaobai-web-components-dropdown@1.0.0
import React from 'react';
import 'baixiaobai-web-components-dropdown';

const 业务组件 = props => {
  return (
    <baixiaobai-web-components-dropdown></baixiaobai-web-components-dropdown> 
  );
};

防腐层

但是我希望不要直接在业务组件中这样使用它,这样不利于系统解耦,也不利于隔离双方变更的影响。建议大家在使用第三方依赖时,自建一个防腐层,像这样。

import React from 'react';
import 'baixiaobai-web-components-dropdown';

const WebDropdown = props => {
  return (
    <baixiaobai-web-components-dropdown></baixiaobai-web-components-dropdown> 
  );
};
import React from 'react';
import WebDropdown from "./WebDropdown";

const 业务组件 = () => {
  return (
    <div>
      <WebDropdown ... />
    </div>
  );
};

防腐层可以让两方的系统解耦,隔离双方变更的影响,允许双方独立演进;防腐层也允许其它的外部系统能够在不改变现有系统的领域层的前提下,与该系统实现无缝集成,从而降低系统集成的开发工作量。

属性

到这里为止,我们的 React 组件只是单单的渲染了 Web Components,并没有传递任何道具给它。它不像通过以下方式将参数作为属性传递那么简单,因为我们需要以不同的方式将对象数组函数传递给 Web Components 组件 。

import React from 'react';
import 'baixiaobai-web-components-dropdown';

const WebDropdown = props => {
  return (
    <baixiaobai-web-components-dropdown
  {...props}
    ></baixiaobai-web-components-dropdown> 
  );
};

对于我们之前开发的下拉组件baixiaobai-web-components-dropdown,我们需要传递如下参数,不同类型的参数处理起来不太一样。

const props = {
  label: '下拉菜单组件',
  option: 1,
  options: [
    { "label": "黄金糕", "value": 1 }, 
    { "label": "狮子头", "value": 2 }, 
    { "label": "螺蛳粉", "value": 3 },
    { "label": "双皮奶", "value": 4 },
    { "label": "蚵仔煎", "value": 5 }
  ]
};

对于基础类型的属性,我们可以直接传递,例如:label、option。

import React from 'react';
import 'baixiaobai-web-components-dropdown';

const WebDropdown = ({ label, option }) => {
  return (
    <baixiaobai-web-components-dropdown
  label={label}
      option={option}
    ></baixiaobai-web-components-dropdown> 
  );
};

但是,对 options 参数我们需要做一些处理,因为它是一个数组(对象类似),我们不能简单地作为属性传递。我们需要作为 JSON 格式的字符串而不是 JavaScript 数组传递给 Web Components:

  import React from 'react';
  import 'baixiaobai-web-components-dropdown';
  
  const WebDropdown = ({ label, option, options }) => {
    return (
      <baixiaobai-web-components-dropdown
    label={label}
        option={option}
        options={JSON.stringify(options)}
      ></baixiaobai-web-components-dropdown> 
    );
  };

不过这里需要注意一下,我们之前在封装baixiaobai-web-components-dropdown这个下拉组件的时候,我们在底层已经其进行了 JSON 的转换。源码地址看这里

  get options() {
    return JSON.parse(this.getAttribute('options'));
  }

  set options(value) {
    this.setAttribute('options', JSON.stringify(value));
  }

所以我们这里就不需要在进行处理了,但是如果底层没有处理,我们就需要对数组(对象)类的数据类型进行 JSON 格式化处理。

<baixiaobai-web-components-dropdown
  label={label}
  option={option}
  options={options}
></baixiaobai-web-components-dropdown> 

事件

而对于事件处理程序,我们并不能像 label、option 直接进行传递,也不能像 options 进行 JSON 格式化。我们需要为其注册一个事件侦听器,而不是将其作为属性传递,比如这样:

  React.useEffect(() => {
    document
        .querySelector('baixiaobai-web-components-dropdown')
          .addEventListener('onOptionChange', (event) =>  {
            console.log(event.detail);
          });
  }, []);

但我们可以换一种写法,我们为我们的下拉组件创建一个引用,创建 ref 属性场地给自定义原始,为我们的 React 挂钩中,添加一个事件处理程序或者说事件侦听器。由于我们正在从自定义下拉元素中调度自定义事件,因此我们可以注册一个自定义事件并使用我们自己的事件处理程序从 props 传播信息。自定义事件带有一个 detail 属性,用于发送一个可选的负载。

const WebDropdown = ({ label, option, options }) => {
  const ref = React.useRef();
  React.useEffect(() => {
    const { current } = ref;
    current.addEventListener('onOptionChange', (event) =>  {
      console.log(event.detail);
    });
  }, [ref]);

  return (
    <baixiaobai-web-components-dropdown
      ref={ref}
      label={label}
      option={option}
      options={options}
    ></baixiaobai-web-components-dropdown> 
  );
};

我们之前在baixiaobai-web-components-dropdown这个下拉组件中注册的自定义事件是onOptionChange,所以我们这里也需要监听这个自定义事件。

注意:如果您的 Web 组件中有一个内置的 DOM 事件(例如click或change事件),您也可以注册到该事件。但是,此 Web 组件已经调度了一个与 React 组件的命名约定相匹配的自定义事件。

并且我们可以在优化一下代码程序,自定义事件作为回调函数由上级传入,并在销毁组件的时候删除自定义事件。完整代码如下:

import React from 'react';
import WebDropdown from "./WebDropdown";

function App() {
  const props = {
    label: '下拉菜单组件',
    option: 1,
    options: [
      { "label": "黄金糕", "value": 1 }, 
      { "label": "狮子头", "value": 2 }, 
      { "label": "螺蛳粉", "value": 3 },
      { "label": "双皮奶", "value": 4 },
      { "label": "蚵仔煎", "value": 5 }
    ],
    onOptionChange(event) {
      console.log(event.detail);
    }
  };

  return (
    <div className="App">
      <WebDropdown {...props} />
    </div>
  );
}

export default App;
import React from 'react';
import 'baixiaobai-web-components-dropdown';

const WebDropdown = ({ label, option, options, onOptionChange }) => {
  const ref = React.useRef();
  
  React.useEffect(() => {
    const { current } = ref;
    current.addEventListener('onOptionChange', onOptionChange);
    return () => current.removeEventListener('onOptionChange', onOptionChange);
  }, [ref]);

  return (
    <baixiaobai-web-components-dropdown
      ref={ref}
      label={label}
      option={option}
      options={options}
    ></baixiaobai-web-components-dropdown> 
  );
};

export default WebDropdown;

因此,我们使用附加到自定义元素的引用来注册此事件侦听器。所有其他属性都作为属性传递给自定义元素。最后,你现在应该可以在 React 中使用这个 Web Components 下拉组件了。

将 Web Components 连接到 React Hooks

上面已经为大家展示了在 React 中使用 Web Components。如何传递属性、格式化数组(对象)和注册函数。这一系列操作其实都是可以封装为一个 React Hook 来统一处理。

这个 Hook 就负责:

  • 将数组(对象)JSON 格式化
  • 为函数注册事件侦听器
  • 保持字符串、整数和布尔值不变
  • 并从自定义属性中删除函数,为我们提供了自定义格式的所有属性

最后我们想要的效果就是,通过这个 HOOK,可以处理 Web Components 需要处理的所有。

import React from 'react';
import 'baixiaobai-web-components-dropdown';
import useWebComponentsCustomElement from "./useWebComponentsCustomElement";

const WebDropdown = (props) => {
  const [elementProps, ref] = useWebComponentsCustomElement(props);

  return (
    <baixiaobai-web-components-dropdown
      ref={ref}
      {...elementProps}
    ></baixiaobai-web-components-dropdown> 
  );
};

属性处理

这个 HOOK 对于属性,我们知道不同类型的数据结构,需要不同的处理。

对象、数组就需要为它 JSON 格式化。

字符串、整数和布尔值 直接传递就行。

function getType(obj) {
  return Object.prototype.toString.call(obj).replace(/$[object (\S+)]^/, '$1').slice(8, -1);
}

function isFunctionType(obj) {
  return getType(obj) === 'Function';
}

function isObjectOrArrayType(obj) {
  return getType(obj) === 'Object' || getType(obj) === 'Array';
}

const elementProps = Object.keys(props)
  .filter(key => !(isFunctionType( props[key])))
  .reduce((acc, key) => {
    const prop = props[key];

    if (isObjectOrArrayType(prop)) {
      // 是否 JSON 格式化,还取决于你是否已经在底层处理过,如果处理过就不需要在处理了
      return { ...acc, [key]: JSON.stringify(prop) };
    }

    return { ...acc, [key]: prop };
  }, {});

对象数组对象,如果你在组件中有处理,在 HOOK 中就不需要处理了,就像 baixiaobai-web-components-dropdown 组件一样。

事件处理

针对函数就需要为他注册事件监听器。函数在 HOOK 中注册为事件侦听器时,不要忘记将 ref 属性也传递给 Web Components 组件,原因时需要将所有回调函数注册到 Web Components 组件,所以在我们的 HOOK 中,我们将返回一个 ref。并且我们需要在适当的时机将我们的事件侦听器删除回收。

注意:事件处理可能不止一个,所以在进行注册和删除回收时都需要批量处理。

const ref = React.createRef();

React.useEffect(() => {
  const { current } = ref;
  let fns;
  if (current) {
    fns = Object.keys(props).filter(key => isFunctionType( props[key]))
      .map(key => ({
        key: key,
        fn: (customEvent) =>
          props[key](customEvent.detail, customEvent),
      }));

    fns.forEach(({ key, fn }) => current.addEventListener(key, fn));
  }
  return () => {
    if (current) {
      fns.forEach(({ key, fn }) =>
        current.removeEventListener(key, fn),
      );
    }
  };
}, [props, ref]);

自定义映射

我们的 HOOK 可能不是你一个人使用,你可能将它封装处理好了,提供给其他人使用,为了进行兼容处理,我们还可以将属性自定义映射。什么意思了?

比如上文中举例传递的属性格式是这样子的:

const props = {
  label: '下拉菜单组件',
  option: 1,
  options: [
    { "label": "黄金糕", "value": 1 }, 
    { "label": "狮子头", "value": 2 }, 
    { "label": "螺蛳粉", "value": 3 },
    { "label": "双皮奶", "value": 4 },
    { "label": "蚵仔煎", "value": 5 }
  ],
  onOptionChange(event) {
    console.log(event.detail);
  }
};

但是在一些特殊常见下,比如 label 不叫 label ,叫 text。option 不叫 option,叫 selected。options 不叫 options,叫 mapList。onOptionChange 不叫 onOptionChange,就叫 onChange。

const props = {
  text: '下拉菜单组件',
  selected: 1,
  mapList: [
    { "label": "黄金糕", "value": 1 }, 
    { "label": "狮子头", "value": 2 }, 
    { "label": "螺蛳粉", "value": 3 },
    { "label": "双皮奶", "value": 4 },
    { "label": "蚵仔煎", "value": 5 }
  ],
  onChange(event) {
    console.log(event);
  }
};

为了应对这种情况,我们可以可以自定义映射,做一层属性的映射。

const [elementProps, ref] = useWebComponentsCustomElement(props, {
  text: 'label',
  selected: 'option',
  mapList: 'options',
  onChange: 'onOptionChange',
});

所以我们在 HOOK 入参时增加一个对应的 propsMapping参数,然后再进行属性处理和事件注册时,去获取 map 中的对应关系。

到这里我们的 HOOK 就基本完成了,完成代码如下:

import React from 'react';

function getType(obj) {
  return Object.prototype.toString.call(obj).replace(/$[object (\S+)]^/, '$1').slice(8, -1);
}

function isFunctionType(obj) {
  return getType(obj) === 'Function';
}

function isObjectOrArrayType(obj) {
  return getType(obj) === 'Object' || getType(obj) === 'Array';
}

const useWebComponentsCustomElement = (props, propsMapping = {}) => {
  const ref = React.createRef();

  React.useEffect(() => {
    const { current } = ref;
    let fns;
    if (current) {
      fns = Object.keys(props).filter(key => isFunctionType( props[key]))
        .map(key => ({
          key: propsMapping[key] || key,
          fn: (customEvent) =>
            props[key](customEvent.detail, customEvent),
        }));
  
      fns.forEach(({ key, fn }) => current.addEventListener(key, fn));
    }
    return () => {
      if (current) {
        fns.forEach(({ key, fn }) =>
          current.removeEventListener(key, fn),
        );
      }
    };
  }, [propsMapping, props, ref]);

  const elementProps = Object.keys(props)
    .filter(key => !(isFunctionType( props[key])))
    .reduce((acc, key) => {
      const prop = props[key];

      const computedKey = propsMapping[key] || key;

      if (isObjectOrArrayType(prop)) {
        return { ...acc, [computedKey]: JSON.stringify(prop) };
      }

      return { ...acc, [computedKey]: prop };
    }, {});

  return [elementProps, ref];
};

不过这里我还需要提醒一下,对于数组和对象的 JSON 格式化,如果你在底层组件中已经处理过了,在这里的 HOOK 就不需要处理了,如果没有处理过就需要进行处理。

小结

通过这个自定义 HOOK 将 Web Components 组件连接到 React 组件时过程进行了自动包装。整个包装过程也就是对触及的属性和事件进行解析处理。自定义 React HOOK 通过将所有数组和对象格式化为 JSON,保持字符串、整数和布尔值不变,并从自定义属性中删除函数,为我们提供了自定义格式的所有属性。接着,函数将在 React HOOK 中注册为事件侦听器,并在适当的时机删除回收事件侦听器。最后将 ref 属性返回传递给 Web Components 组件。

总结

再次声明,Web Components 并不是为了取代任何现有框架而生,它不会取代 React,也不会取代 Vue,Web Component 的目的是为了从原生层面实现组件化,可以使开发者开发、复用、扩展自定义组件,实现自定义标签,解决 Web 组件的重用和共享问题,并使 Web 生态保持持续的开放和统一。如果你对 Web Components 感兴趣,可以关注本专栏。

本文是 Web Components 专栏的第七篇文章。专栏从带你初入 Web Components 世界了解 Web Components 基础实战开发 Web Components 组件发布 Web Components 组件,以及本文的 Web Components 组件的是使用,文章都是承上启下,一步一步进阶,如果你对 Web Components 感兴趣,可以从头阅起,构建整个 Web Components 知识体系。

最后,我希望从这个 Web Components 教程中你可以学到了很多东西。

如果文中有什么问题或者错误,请在评论区告诉我。

如果你觉得这篇文章对你有帮助,点个赞吧。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容