如何优雅设地计出不可维护的 React 组件🤔🤔🤔

在日常团队的开发中,大家写的组件质量参差不齐,风格千差万别,命名方式就有很多种,例如有的用标签来当组件名称,是谁我不说。会因为很多需求导致组件无法扩展,或难以维护。导致很多业务组件的功能重复使用起来相当难受。

那么今天这篇文章我们就从组件的设计模式来聊聊如何设置一个优雅的 React 组件。

组件设计基本原则

要想设计出一个或者一套高质量的组件时,我们首先应该遵循以下原则,具体有下面几个方面。

单一职责

单一职责的原则是要让一个模块都专注于一个凤女,即让一个模块的责任尽量少。若一个模块功能过多,则应该拆分为多个模块,这样更有利于代码的维护。

单一组件更易于维护和测试,但也不要滥用,必要的时候采取拆分组件,粒度最小化的是一个极端,可能会导致大量模块,模块离散化也会让项目变得难以管理。

划分边界

如何拆分组件,如果两个组件的关联过于紧密,从逻辑上无法清晰定义各自的职责,那么这两个组件不应该被拆分。否则各自职责不清,边界不分,则会产生逻辑混乱的问题。那么拆分组件最关键在于确定好边界,通过抽象化的参数通信,让每个组件发挥出各自特有的能力。

高内聚/低耦合

高质量的组件要满足高内聚和低耦合的原则,高内聚就例如在项目的 src 目录中要区分每个文件之间所做的功能例如 routerspagescomponents,低耦合指的是要降低不同组件之间的依赖关系,让每个组件都要独立,也就是说平时写代码都是为了低耦合而进行。通过责任分离、划分边界的方式,将复杂的业务解耦。

单向数据流

React 找中,数据仅朝一个方向流动,即从父组件流向子组件,如果数据在兄弟子组件之间共享,那么数据应该存储在父组件,并同时传递给需要数据的子组件。

数据从父组件流向子组件,数据更新发送到父组件,父组件会进行实际的更改,父组件执行更改之后,将会更新的数据传递给子组件。

React中组件的定义方式有哪些?

React 官方文档中来说,目前 React 组件的定义有下面的几种方式:

  • Function 组件: 使用普通的 JavaScript 函数定义组件;
  • Hooks: 使用特殊的函数,称为 Hooks,在 Function 组件中管理状态和其他 React 特性;
  • Class 组件: 使用 class 关键字定义组件,并扩展 React.component;

基于以上三种基本的组件定义方式,我们可以延伸出多种组件设计模式。

组件设计模式

要区分组件的设计模式,这里需要搞清楚基于场景的设计分类,在 React 社区中把组件分成了两类,一类是把只作展示、独立运行、不额外增加功能的组件,称之为哑组件、无状态组件或者展示组件,另一类是把处理业务逻辑与数据状态的组件称之为有状态组件、灵巧组件,它一定包含至少一个灵巧组件或展示组件。

展示组件的复用性更强,灵巧组件更专注于业务本身。

展示组件

展示组件正如名字一样,像一个装饰物一样,完全受制于外部的 props 控制,具有极强的通用性,复用率高,往往与当前的项目的关联关系相对薄弱,可以跨项目使用,这就是我们常用的组件库了。

代理组件

代理组件 常用于封装常用属性,减少重复代码。

举个例子,当我们要在项目中添加一个 button 元素的时候,要在 button 元素上添加 type="button" 属性:

<button type="button">button</button>;

如果我们要使用多个这样的 button 组件呢,每个地方都写一遍,这样会非常麻烦,最常见的解决方法就是封装:

import React from "react";
import { Button as 🐔 } from "antd";

const 🏀: React.FC = rap:any => {
  return (
    <🐔 size="small" type="primary" {...rap}>
      点🐔按钮
    </🐔>
  );
};

export default 🏀;

这样的封装感觉会有点多此一举,但切断了外部组件的强依赖特性,如果当前组件库不能使用,是否能实现业务上的无痛切换。

设计组件的设计模式很好的解决了以上的问题,在上面的例子中,代理组件隔绝 antd,仅是一个组件 props APi 层的交互,当 antd 不能使用,我们可以很快的切换到另一个组件库 bntd 中。

样式组件也是一种代理组件,只是有细分了处理样式领域将当前的关注点分离到当前组件内,请看下面的例子:

import React from "react";
import classnames from "classnames";
import { Button as KFC } from "antd";

const 🏀: React.FC = ({ sing, v50, dance, ...rap }) => {
  return (
    <KFC
      size="small"
      type="primary"
      className={classnames(
        "v50",
        {
          "btn- primary": v50,
          "hight-light": dance,
        },
        sing,
      )}
      {...rap}
    >
      点🐔按钮
    </KFC>
  );
};

export default 🏀;

在上面的例子中通过传入不同的参数来控制按钮是亮还是不亮,显示不同的样式。

灵巧组件

灵巧组件面向业务,功能更丰富、复杂性更高,复用性更低。展示组件专注于组件本身特性,灵巧组件专注于组合组件。

容器组件

容器组件几乎没有复用性,主要用在拉取数据组合组件两个方面。

容器组件关注的事物是如何工作,它内部可能同时包含展示组件和容器组件,但除了外层有一些包裹外,例如 div 或者 React 提供的 Fragment 外,通常没有自己的任何 DOM 标记,也从来没有任何样式,为 其展示组件或者其他容器组件提供数据和行为.

这种方法的好处是分离关注点,通过这种方式编写组件,你可以更好地理解你的应用程序和你的 UI。你可以用完全不同的状态源来使用相同的表现型组件,并将其转化为可以进一步重用的独立容器组件。

具体请看下面的一个Demo:

import React, { Fragment, useState, useEffect } from "react";

interface Props {
  card: number[];
}

const Card: React.FC<Props> = ({ card }) => {
  return (
    <div>
      {card.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
};

const Container = () => {
  // 模拟网络请求
  const [state, setState] = useState<number[]>([]);
  useEffect(() => {
    setState([1, 2, 3, 4, 5, 6]);
  }, [state]);

  return (
    <Fragment>
      <Card card={state} />
    </Fragment>
  );
};

export default Container;

高阶组件

React 中复用组件逻辑的高级技术,是基于 React 的组合特性形成的设计模式,高阶组件是接收一个参数,它的返回值是一个新的组件,这样的函数称之为高阶组件。

高阶组件例子

下面就拿一个切换主题的例子来说明一下高阶组件是如何使用的,具体代码如下所示:

import React, { Component, createContext } from "react";

interface Context {
  color: string;
  size: number;
}

const ThemeContext = createContext<Context>({ color: "red", size: 60 });

const Layout = () => {
  return (
    <ThemeContext.Consumer>
      {value => {
        return (
          <h1>
            主题: {value.color} 大小{value.size}
          </h1>
        );
      }}
    </ThemeContext.Consumer>
  );
};

class App extends Component {
  render() {
    return (
      <div>
        <ThemeContext.Provider value={{ color: "pink", size: 16 }}>
          <Layout />
        </ThemeContext.Provider>
      </div>
    );
  }
}

export default App;

这样的定义方法,不仅代码不美观,而且不好扩展,如果其他页面又使用到就麻烦了,这个时候我们可以使用高阶组件来解决这个问题:

import React, { Component, createContext } from "react";

const ThemeContext = createContext({ color: "red", size: 60 });

function hoc(OriginComponent) {
  return class extends React.Component {
    render() {
      return (
        <ThemeContext.Consumer>
          {(value) => {
            return <OriginComponent {...value} />;
          }}
        </ThemeContext.Consumer>
      );
    }
  };
}

class ThemeComponent extends Component {
  render() {
    const { size, color } = this.props;
    return (
      <ThemeContext.Provider value={{ color: "pink", size: 16 }}>
        <h1>
          {color}-{size}
        </h1>
      </ThemeContext.Provider>
    );
  }
}

const Theme = hoc(ThemeComponent);

class App extends Component {
  render() {
    return (
      <ThemeContext.Provider value={{ color: "red", size: 16 }}>
        <Theme />
      </ThemeContext.Provider>
    );
  }
}

export default App;

细心的可能你已经发现了,高阶组件其实就是装饰器模式在 React 中的实现,它通过给函数传入一个组件后在函数内部对该组件进行功能的增强,在不修改传入参数的前提下,最后返回这个组件。即允许向一个现有的组件添加新的功能,同时又不去修改该组件。

自定义Hooks

class 时代,这种 Hoc 确实是一个很叼的设计思路,但我们既然有了 hooks 那么我们是否可以舍弃用这种闻风丧胆的组件构建模式呢?

具体代码如下所示:

import React, { useState } from "react";

function useRequest(options) {
  const { url, ...init } = options;
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  // 模拟网络请求
  function loader() {
    console.log(url, init);
    setLoading(true);
    setData([1, 2, 3, 4, 5]);
  }
  // 将 loader, data, loading 作为自定义 hook 的返回值
  return { loader, data, loading };
}

const App = () => {
  const { data, loader } = useRequest({
    url: "user",
    method: "GET",
  });
  console.log(data);
  return <div onClick={() => loader()}>theme</div>;
};

export default App;

总结

没有总结,没有什么最好的设计方式,都是根据不同的场景设计利用不同的设计模式来设计组件。

我们能做的就是写出越来越骚的组件🤪🤪🤪

原文链接:https://juejin.cn/post/7226001971803013177 作者:Moment

(0)
上一篇 2023年4月26日 上午10:47
下一篇 2023年4月26日 上午10:57

相关推荐

发表回复

登录后才能评论