React中的复合组件模式

简介

随着 React 继续主导 Web 开发领域,开发者们不断寻求构建灵活且可重用用户界面的新方法和高效方式。其中一个获得显著流行的模式是复合组件模式。该模式允许开发者创建高度可组合的组件。

React 的基于组件的架构已经促进了可重用和模块化代码的概念。然而,复合组件模式通过使开发者能够将相关组件封装在一个父组件中,进一步推动了这一概念,从而实现了协调的组合和配置。通过利用这种模式,我们可以构建强大的 UI 组件,这些组件具有灵活性、可维护性和易于扩展性。

在本文中,我们将重构一个示例组件以使用复合组件模式。在这个过程中,我们将涵盖一些概念,如React库自带的Context API 和属性下钻(prop drilling),这些概念对于理解设计是必不可少的。 (译者注: Context API是一套官方提供的状态管理方案,prop drilling是指状态通过props层层传递给子组件)

通过本文的学习,您将对如何有效地设计和实现复合组件有一个深入的理解,赋予您能够创建高度模块化、可定制和用户友好的 UI组件的能力。首先,让我们了解掌握这种设计模式所需的基础知识。

什么是复合组件模式

React 复合组件是一种强大的模式,它允许您通过将相关的子组件封装在一个父组件中来创建可重用和灵活的组件。复合组件背后的想法是为用户提供一个干净而直观的 API,使其与您的组件进行交互,同时隐藏实现细节。

将复合组件视为乐高积木,集合中的每个单独的乐高积木可能具有自己的形状、颜色和用途,但当您以特定的方式将它们组合在一起时,就可以构建出更复杂和令人兴奋的东西。

在 React 的情境中,一个父组件可以充当集合,而子组件则是单个乐高积木。父组件定义了复合组件作为整体的结构和行为,而子组件则表示特定的部分或功能。下图展示了一个典型的 React 组件的结构。

配图1: 典型的React 组件结构

在这种情况下,我们有一个单独的 React 组件(以蓝框标出),具有许多 UI 状态。根据输入 props 和状态逻辑中规定的条件,我们将呈现相应子组件。子组件(C1、C2、C3 和 C4)嵌套在父组件中,不暴露给外部世界。根据输入 props 渲染不同排列的子组件。这种设计模式对于小型组件来说是可以的,但是当组件大小增加时,会有以下缺点:

  • 组件最终可能会有大量的输入 props。
  • 很难理解组件在做什么,违反了单一职责原则。
  • 很难自定义内部组件,毕竟它们没有被暴露出来。

为了解决上述问题,我们可以使用复合组件模式,通过松散耦合而不是嵌套将子组件封装在父组件中。思考下面使用复合组件结构的设计:

配图2: 使用复合组件模式的React组件设计

要使用复合组件,您需要渲染父组件(C1)并将所需的组件(C2、C3、C4)作为其子组件提供。一个设计良好的 React 复合组件应该在父组件中定义状态和有状态逻辑,然后通过 React Context将这些状态提供给子组件消费。通过这样做,不仅可以重用父组件,还可以以有意义的方式排列和自定义子组件以实现所需的功能。这种设计模式带来以下好处:

  • 很容易自定义子组件。我们不再需要通过父组件暴露自定义 props,每个子组件都可以单独自定义。
  • 使用 React 复合组件时,很容易理解父组件集合的内部结构。
  • 一个设计良好的 React 复合组件应该只需要非常少的状态管理。

我希望您现在能够有一个大致的图景。一般来说,复合组件适用于这样的场景:有一个父组件,封装了相关的子组件,并提供了一个灵活和可定制的 API,用于构建复杂的 UI 功能。目前,你已经拥有足够的理论知识了,现在让我们看一个例子:我们将使用复合组件模式,重构一个 React 组件。

一个用于演示的轮播图组件

我已经在一个 React 项目中准备了一个轮播组件,该组件用于演示复合组件模式。您可以在下方找到仓库地址,并将该仓库克隆到本地目录中。一旦您克隆了该仓库,运行以下命令安装依赖项:

react-compound-component

bash 复制代码
cd compound-component-carousel && \
npm install

您需要安装 Node.js 才能使其正常工作。本项目使用的 Node 版本是 v19.9.0。

安装依赖项后,您可以使用以下命令运行该组件:

bash 复制代码
npm run dev

执行完上述命令后,将会启动一个本地的 React 服务器,并运行在 http://localhost:5173 上。在浏览器中打开此链接,应该会显示下面的组件。

让我们检查下文件目录,便于更加深入的了解该轮播图组件:

配图3: React版轮播图组件文件目录

这个React项目的入口文件是App.tsx文件,文件内容如下:

ts 复制代码
import { ReactNode } from "react";
import Carousel from "./carousel/Carousel";

function App() {
  const images: ReactNode[] = [
    <img src="https://picsum.photos/800/300/?random" alt="1" />,
    <img src="https://picsum.photos/800/301/?random" alt="2" />,
    <img src="https://picsum.photos/800/302/?random" alt="3" />,
    <img src="https://picsum.photos/800/303/?random" alt="4" />,
    <img src="https://picsum.photos/800/304/?random" alt="5" />,
  ];

  return (
    <Carousel 
      images={images} 
      autoplay 
      interval={3000} 
      showArrow 
      title="Carousel" 
      showPaging 
    />
  );
}

export default App;

我们导入了一个 Carousel 组件,并使用它来渲染了五张图片。该组件通过其输入 props 进行配置。例如,如果我们想要轮播自动向右滑动,则需要指定 autoplay prop。

Carousel.tsx 中, 提供了 Carousel 的父组件,其内容如下所示:

ts 复制代码
// Project dependencies
import { useState, useEffect, useRef, ReactNode } from "react";
import { CarouselProps, Slide } from "./types/Carousel";

// Styling
import "./styles/carousel.scss";
import SingleSlide from "./components/SingleSlide";
import Title from "./components/Title";
import Paging from "./components/Paging";

/**
 * Initialise the slides state for the carousel.
 * If there are less than 3 images, duplicate the images until there are at least 3 images.
 * @param images 
 * @returns 
 */
const initialSlides = (images: ReactNode[]): Slide[] => {
  let lessThanThree = images.length < 3;
  const slides: Slide[] = [];
  while (lessThanThree) {
    images = images.concat(images);
    lessThanThree = images.length < 3;
  }

  for (let idx = 0; idx < images.length; idx++) {
    let slideClass = "slider-single proactivede";
    slides.push({
      class: slideClass,
      element: images[idx],
    });
  }
  return slides;
};

/**
 * Main carousel component. This component is responsible for displaying the slides and the arrows.
 * It has a state that keeps track of the current slide index.
 * It also has the ability to autoplay the slides at a specified interval.
 * @param props 
 * @returns 
 */
const Carousel = (props: CarouselProps) => {
  const { images, interval = 1000, autoplay, height = "300px", showArrow, title, showPaging } = props;
  const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
  const [slides, setSlides] = useState(initialSlides.bind(null, images));
  const intervalRef = useRef<number>();
  const nextRef = useRef<HTMLDivElement>(null);

  // First render will initialise some styling
  useEffect(() => {
    slideRight();

    // Autoplay needs to use a ref or else the sliding function will have sticky states due to closure
    if (autoplay) {
      clearInterval(intervalRef.current);
      intervalRef.current = setInterval(() => {
        nextRef.current?.click();
      }, interval);
    }

    return () => {
      clearInterval(intervalRef.current);
    };
  }, [nextRef.current]);

  /**
   * This function is responsible for sliding the carousel to the right. It is called when the user clicks the right arrow button on the carousel, 
   * or when the carousel is set to autoplay. The class of the previous slide is set to "slider-single preactive", the class of the active slide is set to activeClass, 
   * and the class of the next slide is set to "slider-single proactive". The slides array and currentSlideIndex state variables are updated, and if autoplay is enabled, 
   * the intervalRef timer is cleared and a new timer is set.
   */
  const slideRight = () => {
    const activeClass = "slider-single active";
    const updatedSlides = [...slides];
    const numSlides = updatedSlides.length;
    const lastSlideIndex = numSlides - 1;
    let nextSlideIndex = currentSlideIndex + 1;
    if (currentSlideIndex === lastSlideIndex) {
      nextSlideIndex = 0;
    }

    const previousSlide = updatedSlides[currentSlideIndex];
    const activeSlide = updatedSlides[nextSlideIndex];
    // If next slide index overflows, set to first slide
    const nextSlide = updatedSlides[nextSlideIndex + 1 > lastSlideIndex ? 0 : nextSlideIndex + 1];

    updatedSlides.forEach((slide) => {
      if (slide.class.split(" ").includes("preactivede")) {
        slide.class = "slider-single proactivede";
      }
      if (slide.class.split(" ").includes("preactive")) {
        slide.class = "slider-single preactivede";
      }
    });

    previousSlide.class = "slider-single preactive";
    activeSlide.class = activeClass;
    nextSlide.class = "slider-single proactive";

    setSlides(updatedSlides);
    setCurrentSlideIndex(nextSlideIndex);

    if (autoplay) {
      clearInterval(intervalRef.current);
      intervalRef.current = setInterval(() => {
        nextRef.current?.click();
      }, interval);
    }
  };

  /**
   * The logic is very much the same as the slideRight function, except that the slide classes are different and specified in reverse order.
   */
  const slideLeft = () => {
    const numSlides = slides.length;
    let nextSlideIndex = currentSlideIndex - 1;
    const lastSlideIndex = numSlides - 1;
    const updatedSlides = [...slides];

    if (currentSlideIndex === 0) {
      nextSlideIndex = lastSlideIndex;
    }

    // If prev slide index overflows, set to last slide
    const previousSlide = updatedSlides[nextSlideIndex - 1 < 0 ? lastSlideIndex : nextSlideIndex - 1];
    const activeSlide = updatedSlides[nextSlideIndex];
    const nextSlide = updatedSlides[currentSlideIndex];

    slides.forEach((slide) => {
      if (slide.class.split(" ").includes("proactivede")) {
        slide.class = "slider-single preactivede";
      }
      if (slide.class.split(" ").includes("proactive")) {
        slide.class = "slider-single proactivede";
      }
    });

    previousSlide.class = "slider-single preactive";
    activeSlide.class = "slider-single active";
    nextSlide.class = "slider-single proactive";

    setSlides(updatedSlides);
    setCurrentSlideIndex(nextSlideIndex);
  };

  let content = null;
  const hasSlides = slides && slides.length > 0;
  if (hasSlides) {
    const slideComponents = slides.map((slide, index: number) => (
      <SingleSlide
        slide={slide}
        onSlideLeft={slideLeft}
        onSlideRight={slideRight}
        key={index}
        ref={nextRef}
        showArrow={showArrow}
      />
    ));
    content = (
      <div className="slider-container">
        {title && <Title text={title} />}
        <div className="slider-content">{slideComponents}</div>
        {showPaging && <Paging numSlides={slides.length} currentSlideIndex={currentSlideIndex} />}
      </div>
    );
  }

  return (
    <div className="react-3d-carousel" style={{ height }}>
      {content}
    </div>
  );
};

export default Carousel;

Carousel 组件通过 slides 状态显示图片。它通过 currentSlideIndex 状态管理当前幻灯片索引,并提供自动播放、箭头、标题和分页等功能。该组件初始化幻灯片,处理幻灯片转换并应用样式。SingleSlideTitlePaging 等子组件则基于提供的 props,决定要不要被渲染。slideRightslideLeft 函数将使用适当的样式更新幻灯片和 currentSlideIndex。此外,useEffect 钩子用于设置一个 interval ref 对象,如果 autoplay 设置为 true,则该对象将以 interval 指定的固定频率自动模拟单击事件,并触发在 SingleSlide 组件上。单击事件通过将 React 的ref 对象即nextRef 附加到 SingleSlide 子组件来模拟。轮播的样式相当复杂,出于本文的目的,我不会详细介绍它们,但如果您感兴趣,可以查看 carousel.scss

Carousel 组件中,有四个不同的子组件。它们分别如下所示,注释会描述它们的具体作用:

Title

ts 复制代码
/**
 * Component that displays the title of the slide.
 */
const Title = (props: { text: string }) => {
    const { text } = props;
    return <p className="slide-title">{text}</p>;
};

export default Title;

SlideArrow

ts 复制代码
import { forwardRef } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";

type SliderArrowProps = { direction: "left" | "right"; onSlide: VoidFunction; show: boolean };

/**
 * This component is used to display the arrows that allow the user to move back and forth between slides in the slider.
 */
const SliderArrow = forwardRef<HTMLDivElement, SliderArrowProps>((props, ref) => {
  const { direction, onSlide, show } = props;
  return (
    <div className={`slider-${direction}`} onClick={onSlide} ref={ref} style={{ display: show ? "" : "none" }}>
      <div>
        <FontAwesomeIcon size="lg" icon={direction === "left" ? faArrowLeft : faArrowRight} />
      </div>
    </div>
  );
});

export default SliderArrow;

SingleSlide

ts 复制代码
import { forwardRef } from "react";
import { SingleSlideProps } from "../types/Carousel";
import SliderArrow from "./SlideArrow";

/**
 * This component creates a single slide in a slide pack with the ability to slide left and right.
 */
const SingleSlide = forwardRef<HTMLDivElement, SingleSlideProps>((props, ref) => {
  const { slide, onSlideLeft, onSlideRight, showArrow = false } = props;
  return (
    <div className={slide.class}>
      <SliderArrow direction="left" onSlide={onSlideLeft} show={showArrow} />
      <SliderArrow direction="right" onSlide={onSlideRight} ref={ref} show={showArrow} />
      <div className="slider-single-content">{slide.element}</div>
    </div>
  );
});

export default SingleSlide;

Paging

ts 复制代码
/**
 * Component that displays the current slide index and the total number of slides.
 */
const Paging = (props: { numSlides: number; currentSlideIndex: number }) => {
    const { numSlides, currentSlideIndex } = props;
    const pagingElements = Array.from({ length: numSlides }, (_, idx) => {
      const isActive = idx === currentSlideIndex;
      return <span className={`slide-paging-element ${isActive ? "active" : ""}`} />;
    });
  
    return <div className="slide-paging">{pagingElements}</div>;
  };
  
  export default Paging;

现在我们已经了解了基本的 React 项目以及 Carousel 组件,让我们看看如何使用复合组件模式重构它。

重构轮播图组件

最终重构的组件在这里。您可以下载重构后代码,但我建议您在这样做之前先阅读本节。 react-compound-component-refactor

要实现复合组件模式,我们需要设置一个 React Context对象,以便将在父级管理的状态提供给所有子组件。我们将重构 Carousel.tsx 中的原始 Carousel 组件,内容如下:

ts 复制代码
// Project dependencies
import { createContext } from "react";
import { CarouselProps, Slide } from "./types/Carousel";

// Styling
import "./styles/carousel.scss";
import useSlideRotation from "./hooks/useSlideRotation";

// Children components
import Title from "./components/Title";
import Paging from "./components/Paging";
import Slides from "./components/Slides";
import SlidesContainer from "./components/SlidesContainer";

type CarouselContext = {
  currentSlideIndex: number;
  slides: Slide[];
  slideRight: () => void;
  slideLeft: () => void;
  nextRef: React.RefObject<HTMLDivElement> | null;
};

const defaultContext: CarouselContext = {
  currentSlideIndex: 0,
  slides: [],
  slideRight: () => {},
  slideLeft: () => {},
  nextRef: null,
};

export const CarouselContext = createContext<CarouselContext>(defaultContext);

/**
 * Main carousel component. This component is responsible for displaying the slides and the arrows.
 * It has a state that keeps track of the current slide index.
 * It also has the ability to autoplay the slides at a specified interval.
 * @param props
 * @returns
 */
const Carousel = (props: CarouselProps) => {
  const { images, interval = 1000, autoplay, children } = props;

  if (images && images.length <= 0) {
    throw new Error("Carousel requires at least one image");
  }

  const { currentSlideIndex, slides, slideLeft, slideRight, nextRef } = useSlideRotation({
    images,
    interval,
    autoplay,
  });

  return (
    <CarouselContext.Provider value={{ slideLeft, slideRight, nextRef, slides, currentSlideIndex }}>
      {children}
    </CarouselContext.Provider>
  );
};

// Set children components to be memeber of the Carousel component
Carousel.Title = Title;
Carousel.Paging = Paging;
Carousel.Slides = Slides;
Carousel.SlidesContainer = SlidesContainer;

export default Carousel;

有一些明显的变化:

  • 我们从 Carousel 父组件中删除了一些 props。这些包括 heightshowArrowtitleshowPaging,这些 props 都与底层组件相关,与父组件无关。
  • 所有状态和有状态逻辑都被移动到自定义 Hook useSlideRotation 中,以提高模块化程度。状态逻辑保持不变。currentSlideIndexslides 状态与 slideLeftslideRight 函数一起返回,这些函数控制这些状态。此外,用于跟踪轮播滑块箭头的 nextRef 对象也从自定义 Hook 中返回。
  • 我们创建了一个名为 CarouselContext的 React Context,用于向子组件提供 currentSlideIndexslidesslideLeftslideRightnextRef
  • 所有子组件都作为 Carousel 父组件的成员导出。

原始 Carousel 组件的演示部分进行了调整,我们创建了一个 SlidesContainer 组件,如下所示,注意 Carousel 组件中的 height prop 被移动到了这里:

ts 复制代码
import { SlidesContainerProps } from "../types/Carousel";

/**
 * This is the container compoennt that holds the slides.
 * @param props 
 * @returns 
 */
const SlidesContainer = (props: SlidesContainerProps) => {
  const { children, height = "300px" } = props;
  return (
    <div className="react-3d-carousel" style={{ height }}>
      <div className="slider-container">{children}</div>
    </div>
  );
};

export default SlidesContainer;

SlidesContainer 组件中,将渲染 TitlePagingSlides 组件。Slides 组件只是原始 SingleSlide 组件的映射版本。让我们在下面看看这些子组件的实际效果。

Slides

ts 复制代码
import { useContext } from "react";
import { SlidesProps } from "../types/Carousel";
import SlideArrow from "./SlideArrow";
import { CarouselContext } from "../Carousel";

/**
 * This component creates a slide pack where individual slides have the ability to slide left and right.
 */
const Slides = (props: SlidesProps) => {
  const { showArrow = true } = props;
  const { slides, slideLeft, slideRight, nextRef } = useContext(CarouselContext);

  const slideComponents = slides.map((slide, index: number) => (
    <div className={slide.class} key={`slide-${index}`}>
      <SlideArrow direction="left" onSlide={slideLeft} show={showArrow} />
      <SlideArrow direction="right" onSlide={slideRight} ref={nextRef} show={showArrow} />
      <div className="slider-single-content">{slide.element}</div>
    </div>
  ));

  return <div className="slider-content">{slideComponents}</div>;
};

export default Slides;

请注意,我们现在将 showArrow prop 从 Carousel 移动到了这里。更重要的是,我们通过使用 useContext Hook,通过 CarouselContext 访问 slidesslideLeftslideRightnextRef。这些都是在 Carousel 父级管理的状态,在子组件中被消费。您将在其他依赖于这些全局Context的子组件中看到这一点。

Title

ts 复制代码
/**
 * Component that displays the title of the slide.
 */
const Title = (props: { text: string }) => {
    const { text } = props;
    return <p className="slide-title">{text}</p>;
};

export default Title;

Title组件保持不变

Paging

ts 复制代码
import { useContext } from "react";
import { CarouselContext } from "../Carousel";

/**
 * Component that displays the current slide index and the total number of slides.
 */
const Paging = () => {
    const { slides, currentSlideIndex } = useContext(CarouselContext);
    const pagingElements = Array.from({ length: slides.length }, (_, idx) => {
        const isActive = idx === currentSlideIndex;
        return <span className={`slide-paging-element ${isActive ? "active" : ""}`} />;
    });

    return <div className="slide-paging">{pagingElements}</div>;
};

export default Paging;

Paging组件现在从全局的CarouselContext组件中获取currentSlideIndexslides的长度,这些参数之前是通过props drilling层层传递到当前组件。

App.tsx文件中,我们可以使用Carousel组件以及其子属性将其初始化。

ts 复制代码
import { ReactNode } from "react";
import "./App.css";
import Carousel from "./carousel/Carousel";

function App() {
  const images: ReactNode[] = [
    <img src="https://picsum.photos/800/300/?random" alt="1" />,
    <img src="https://picsum.photos/800/301/?random" alt="2" />,
    <img src="https://picsum.photos/800/302/?random" alt="3" />,
    <img src="https://picsum.photos/800/303/?random" alt="4" />,
    <img src="https://picsum.photos/800/304/?random" alt="5" />,
  ];

  return (
    <div className="main">
      <Carousel images={images} autoplay interval={3000}>
        <Carousel.SlidesContainer>
          <Carousel.Title text="Carousel" />
          <Carousel.Slides />
          <Carousel.Paging />
        </Carousel.SlidesContainer>
      </Carousel>
    </div>
  );
}

export default App;

父级 Carousel 组件包裹了所有底层子组件,并通过 React Context提供相关状态。请注意,我们不再需要将特定于子组件的 props 传递给父级轮播图组件,以更改子组件的 UI 行为。我们使用复合组件模式进行的组件重构到此结束。

复合组件模式的威力

你可能会争论这种模式实际上增加了代码量并且没有让逻辑变得简单。这是事实,但是让我们考虑一个场景:假设我们希望Paging组件显示的是一个方形而不是一个圆形,可以做如下的改动:

ts 复制代码
import { useContext } from "react";
import { CarouselContext } from "../Carousel";

type PagingProps = {
  elementStyle?: string;
};

/**
 * Component that displays the current slide index and the total number of slides.
 */
const Paging = (props: PagingProps) => {
  const { elementStyle } = props;
  const {slides, currentSlideIndex} = useContext(CarouselContext);
  const pagingElements = Array.from({ length: slides.length }, (_, idx) => {
    const isActive = idx === currentSlideIndex;
    return <span className={`${elementStyle ? elementStyle : 'slide-paging-element'} ${isActive ? "active" : ""}`} />;
  });

  return <div className="slide-paging">{pagingElements}</div>;
};

export default Paging;

我们通过 elementStyle prop 暴露了 Paging 底层 span 元素的样式。这似乎是一件很明显的事情,然而,让我们想想如果我们没有使用复合组件模式会怎样。我们将不得不将 elementStyle 暴露为父组件的 prop。对于目前的场景,这不是一个问题,但如果我们有许多需要灵活定制的子组件,那么我们将很快得到一个带有大量的 props,且难以维护的组件。

当您需要构建由一组逻辑相关的底层组件组成的 UI 组件时,复合组件的优势真正显现。特别是当我们想要在必要时重用父组件,同时允许对底层子组件进行定制时。
如果您试图创建低级可重用的 UI 组件,则不应考虑使用此模式。依赖于使用 React Context来管理和提供状态意味着组件仅在嵌套在父级复合组件中时才能正常工作。

结论

在本文中,我们探讨了 React 中的复合组件模式,它允许我们创建灵活和可重用的 UI 组件。通过将相关的子组件封装在父组件中,我们可以实现协调的组合和配置,同时隐藏实现细节。

复合组件模式提供了几个好处。首先,它使得定制单个子组件变得容易,提高了对父组件内部组成的理解,其次,减少了对广泛的状态管理的需求。利用 React Context,我们可以向子组件提供必要的状态和函数,而无需通过 props 暴露它们,从而使代码库更简洁易于维护

重构示例轮播图组件展示了复合组件模式的威力。我们将状态管理和逻辑移动到自定义 hook 中,创建了React Context 以向子组件提供状态,并以更模块化和可重用的方式组织了组件。这种方法允许轻松定制轮播图组件,并促进对其内部结构的更清晰理解。

总的来说,复合组件模式是在 React 中构建高度模块化、可定制和用户友好的 UI 组件的有价值工具。通过拥抱这种模式,开发人员可以创建更高效和可维护的代码,同时赋予用户轻松创建复杂 UI 功能的能力。

原文链接,如侵则删。

相关推荐
FLZJ_KL39 分钟前
【设计模式】【创建型模式】单例模式(Singleton)
java·单例模式·设计模式
CL_IN1 小时前
企业数据集成:实现高效调拨出库自动化
java·前端·自动化
浪九天2 小时前
Vue 不同大版本与 Node.js 版本匹配的详细参数
前端·vue.js·node.js
Java知识技术分享2 小时前
使用LangChain构建第一个ReAct Agent
python·react.js·ai·语言模型·langchain
qianmoQ3 小时前
第五章:工程化实践 - 第三节 - Tailwind CSS 大型项目最佳实践
前端·css
椰果uu3 小时前
前端八股万文总结——JS+ES6
前端·javascript·es6
万兴丶3 小时前
Unity 适用于单机游戏的红点系统(前缀树 | 数据结构 | 设计模式 | 算法 | 含源码)
数据结构·unity·设计模式·c#
微wx笑3 小时前
chrome扩展程序如何实现国际化
前端·chrome
~废弃回忆 �༄3 小时前
CSS中伪类选择器
前端·javascript·css·css中伪类选择器
菜鸟一枚在这3 小时前
深入剖析抽象工厂模式:设计模式中的架构利器
设计模式·架构·抽象工厂模式