龙空技术网

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

王哥爱编程 107

前言:

如今姐妹们对“react封装请求api接口”大致比较着重,同学们都想要了解一些“react封装请求api接口”的相关知识。那么小编也在网上网罗了一些对于“react封装请求api接口””的相关知识,希望咱们能喜欢,咱们一起来学习一下吧!

最近遇到一些组件,它们只是对 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,直接用也是很简单的,可封装也可不封装。

作者:zxg_神说要有光 链接:

标签: #react封装请求api接口