前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >遇到这三个 api,你会把它封装成组件么?

遇到这三个 api,你会把它封装成组件么?

作者头像
神说要有光zxg
发布2024-04-17 17:56:46
740
发布2024-04-17 17:56:46
举报

最近遇到一些组件,它们只是对 api 的一层简易封装,用起来也和直接用 api 差不多。

但是这种组件的下载量还是挺多的。

今天我们一起来写三个这样的组件,大家来感受下和直接用 api 的区别。

Portal

react 提供了 createPortal 的 api,可以把组件渲染到某个 dom 下。

用起来也很简单:

代码语言:javascript
复制
import { createPortal } from 'react-dom'

function App() {
  const content = <div className="btn">
    <button>按钮</button>
  </div>;

  return createPortal(content, document.body);
}

export default App;

但我们也可以把它封装成 Portal 组件来用。

接收 attach、children 参数,attach 就是挂载到的 dom 节点,默认是 document.body

然后提供一个 getAttach 方法,如果传入的是 string,就作为选择器来找到对应的 dom,如果是 HTMLElement,则直接作为挂载节点,否则,返回 document.body:

然后在 attach 的元素下添加一个 dom 节点作为容器:

当组件销毁时,删除这个容器 dom。

最后,用 createPortal 把 children 渲染到 container 节点下。

此外,通过 forwardRef + useImperativeHandle 把容器 dom 返回:

代码语言:javascript
复制
import { forwardRef, useEffect, useMemo, useImperativeHandle } from 'react';
import { createPortal } from 'react-dom';

export interface PortalProps {
    attach?: HTMLElement | string;
    children: React.ReactNode;
}

const Portal = forwardRef((props: PortalProps, ref) => {
  const { 
    attach = document.body, 
    children 
  } = props;

  const container = useMemo(() => {
    const el = document.createElement('div');
    el.className = `portal-wrapper`;
    return el;
  }, []);

  useEffect(() => {
    const parentElement = getAttach(attach);
    parentElement?.appendChild?.(container);

    return () => {
      parentElement?.removeChild?.(container);
    };
  }, [container, attach]);

  useImperativeHandle(ref, () => container);

  return createPortal(children, container);
});

export default Portal;

export function getAttach(attach: PortalProps['attach']) {
    if (typeof attach === 'string') {
        return document.querySelector(attach);
    }
    if (typeof attach === 'object' && attach instanceof window.HTMLElement) {
        return attach;
    }

    return document.body;
}

这个 Portal 组件用起来是这样的:

代码语言:javascript
复制
import Portal from './portal';

function App() {
  const content = <div className="btn">
    <button>按钮</button>
  </div>;

  return <Portal attach={document.body}>
    {content}
  </Portal>
}

export default App;

还可以通过 ref 获取内部的容器 dom:

代码语言:javascript
复制
import { useEffect, useRef } from 'react';
import Portal from './portal';

function App() {
  const containerRef = useRef<HTMLElement>(null);

  const content = <div className="btn">
    <button>按钮</button>
  </div>;

  useEffect(()=> {
    console.log(containerRef);
  }, []);

  return <Portal attach={document.body} ref={containerRef}>
    {content}
  </Portal>
}

export default App;

看下效果:

这个 Portal 组件是对 createPortal 的简单封装。

内部封装了选择 attach 节点的逻辑,还会创建容器 dom 并通过 ref 返回。

还是有一些封装的价值。

再来看一个:

MutateObserver

浏览器提供了 MutationObserver 的 api,可以监听 dom 的变化,包括子节点的变化、属性的变化。

这样用:

代码语言:javascript
复制
import { useEffect, useRef, useState } from 'react';

export default function App() {
  const [ className, setClassName] = useState('aaa');

  useEffect(() => {
    setTimeout(() => setClassName('bbb'), 2000);
  }, []);

  const containerRef = useRef(null);

  useEffect(() => {
    const targetNode = containerRef.current!;
  
    const callback = function (mutationsList: MutationRecord[]) {
      console.log(mutationsList);
    };
    
    const observer = new MutationObserver(callback);
    
    observer.observe(targetNode, { 
      attributes: true, 
      childList: true, 
      subtree: true 
    });

  }, []);

  return (
    <div>
        <div id="container" ref={containerRef}>
          <div className={className}>
            {
              className === 'aaa' ? <div>aaa</div> : <div>
                <p>bbb</p>
              </div>
            }
          </div>
        </div>
    </div>
  )
}

声明一个 className 的状态,从 aaa 切换到 bbb,渲染的内容也会改变。

用 useRef 获取到 container 的 dom 节点,然后用 MutationObserver 监听它的变化。

可以看到,2s 后 dom 发生改变,MutationObserver 监听到了它子节点的变化,属性的变化。

observe 的时候可以指定 options。

attributes 是监听属性变化,childList 是监听 children 变化,subtree 是连带子节点的属性、children 变化也监听。

attributeFilter 可以指定监听哪些属性的变化。

这个 api 用起来也不麻烦,但可以封装成自定义 hooks 或者组件。

ahooks 里就有这个 hook:

而 antd 里更是把它封装成了组件:

这样用:

我们也来写一下:

首先封装 useMutateObserver 的 hook:

代码语言:javascript
复制
import { useEffect } from "react";

const defaultOptions: MutationObserverInit = {
  subtree: true,
  childList: true,
  attributeFilter: ['style', 'class'],
};

export default function useMutateObserver(
  nodeOrList: HTMLElement | HTMLElement[],
  callback: MutationCallback,
  options: MutationObserverInit = defaultOptions,
) {
  useEffect(() => {
    if (!nodeOrList) {
      return;
    }

    let instance: MutationObserver;

    const nodeList = Array.isArray(nodeOrList) ? nodeOrList : [nodeOrList];

    if ('MutationObserver' in window) {
      instance = new MutationObserver(callback);

      nodeList.forEach(element => {
        instance.observe(element, options);
      });
    }
    return () => {
      instance?.takeRecords();
      instance?.disconnect();
    };
  }, [options, nodeOrList]);
}

支持单个节点,多个节点的 observe。

设置了默认的 options。

在销毁的时候,调用 takeRecords 删掉所有剩余通知,调用 disconnect 停止接收新的通知:

然后封装 MutateObserver 组件:

代码语言:javascript
复制
import React, { useLayoutEffect } from 'react';
import useMutateObserver from './useMutateObserver';

interface MutationObserverProps{
  options?: MutationObserverInit;
  onMutate?: (mutations: MutationRecord[], observer: MutationObserver) => void;
  children: React.ReactElement;
}

const MutateObserver: React.FC<MutationObserverProps> = props => {
  const { 
    options, 
    onMutate = () => {},
    children, 
  } = props;

  const elementRef = React.useRef<HTMLElement>(null);

  const [target, setTarget] = React.useState<HTMLElement>();

  useMutateObserver(target!, onMutate, options);

  useLayoutEffect(() => {
    setTarget(elementRef.current!);
  }, []);

  if (!children) {
    return null;
  }

  return React.cloneElement(children, { ref: elementRef });
}

export default MutateObserver;

useMutateObserver 的 hook 封装了 MutationObserver 的调用。

而 MutateObserver 组件封装了 ref 的获取。

通过 React.cloneElement 给 children 加上 ref 来获取 dom 节点。

然后在 useLayoutEffect 里拿到 ref 通过 setState 触发更新。

再次渲染的时候,调用 useMutateObserver 就有 dom 了,可以用 MutationObserver 来监听 dom 变化。

用一下:

代码语言:javascript
复制
import { useEffect, useState } from 'react';
import MutateObserver from './MutateObserver';

export default function App() {
  const [ className, setClassName] = useState('aaa');

  useEffect(() => {
    setTimeout(() => setClassName('bbb'), 2000);
  }, []);

  const callback = function (mutationsList: MutationRecord[]) {
    console.log(mutationsList);
  };

  return (
    <div>
        <MutateObserver onMutate={callback}>
          <div id="container">
            <div className={className}>
              {
                className === 'aaa' ? <div>aaa</div> : <div>
                  <p>bbb</p>
                </div>
              }
            </div>
          </div>
        </MutateObserver>
    </div>
  )
}

效果一样:

但是现在不用再 useRef 获取 ref 了,MutateObserver 里会做 ref 的获取,然后用 useMutateObserver 来监听。

这个组件和 hook 的封装都算是有用的。

再来看一个

CopyToClipboard

有这样一个周下载量百万级的组件:

它是做复制的。

基于 copy-to-clipboard 这个包。

我们也来写写看。

直接用 copy-to-clipboard 是这样的:

代码语言:javascript
复制
import copy from 'copy-to-clipboard';

export default function App() {

  function onClick() {
    const res = copy('神说要有光666')
    console.log('done', res);
  }

  return <div onClick={onClick}>复制</div>
}

用 react-copy-to-clipboard 是这样的:

代码语言:javascript
复制
import {CopyToClipboard} from 'react-copy-to-clipboard';

export default function App() {

  return <CopyToClipboard text={'神说要有光2'} onCopy={() => {
    console.log('done')
  }}>
    <div>复制</div>
  </CopyToClipboard>
}

如果元素本来有 onClick 的处理:

代码语言:javascript
复制
import {CopyToClipboard} from 'react-copy-to-clipboard';

export default function App() {

  return <CopyToClipboard text={'神说要有光2'} onCopy={() => {
    console.log('done')
  }}>
    <div onClick={() => alert(1)}>复制</div>
  </CopyToClipboard>
}

只会在原来的基础上添加复制的功能:

我们也来实现下这个组件:

代码语言:javascript
复制
import React, { EventHandler, FC, PropsWithChildren, ReactElement } from 'react';
import copy from 'copy-to-clipboard';

interface CopyToClipboardProps {
    text: string;
    onCopy?: (text: string, result: boolean) => void;
    children: ReactElement;
    options?: {
        debug?: boolean;
        message?: string;
        format?: string;
    }
}

const CopyToClipboard: FC<CopyToClipboardProps> = (props) => {
    const {
        text,
        onCopy,
        children,
        options
    } = props;

    const elem = React.Children.only(children);

    function onClick(event: MouseEvent) {    
        const elem = React.Children.only(children);
        
        const result = copy(text, options);
        
        if (onCopy) {
            onCopy(text, result);
        }
        
        if (typeof elem?.props?.onClick === 'function') {
            elem.props.onClick(event);
        }
    }

    return React.cloneElement(elem, { onClick });
}

export default CopyToClipboard;

React.Children.only 是用来断言 children 只有一个元素,如果不是就报错:

然后用 cloneElement 给元素加上 onClick 事件,执行复制,并且还会调用元素原来的 onClick 事件:

换成我们自己的组件:

效果一样:

这个组件也挺简单的,作用就是被包装的元素,在原来的 click 事件处理函数的基础上,多了复制文本的功能。

也算是有用的,不用把 copy 写的 onClick 函数里了。

总结

今天我们实现了三个 react 组件,它们是对 api 的简单封装。

直接用这些 api 也挺简单,但是封装一下会多一些额外的好处。

Portal 组件:对 createPortal 的封装,多了根据 string 选择 attach 节点,自动创建 container 的 dom 的功能

MutateObserver 组件:对 MutationObserver 的封装,通过 cloneElement 实现了内部自动获取 ref 然后监听的功能,省去了调用方获取 ref 的麻烦。

CopyToClipboard 组件:对 copy-to-clipboard 包的封装,不用侵入元素的 onClick 处理函数,只是额外多了复制的功能

这三个 api,直接用也是很简单的,可封装也可不封装。

你会选择直接用,还是封装成组件呢?

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-15,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 神光的编程秘籍 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Portal
  • MutateObserver
  • CopyToClipboard
  • 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com