React 中的组件设计原则 - 总结

我们已经使用 SOLID 原则来概述我们可以做的一些事情,以使我们的 React 应用程序的架构更易于管理。这些都以模块化我们的代码的想法为中心,因为这使得插入不同的项目更容易,也更容易测试。此外,代码越孤立,其中的错误影响代码库其他区域的可能性就越小。这就是所希望的“松散耦合”。我们讨论了如何使用自定义挂钩等现代工具提取功能,以遵守单一责任原则。在解释如何使用开放/封闭原则以使我们的代码更具可扩展性时,我们谈到了从较小的组件组​​合的想法。我们简要地看了一下 Liskov 替换原则,以及我们如何将它应用到我们编写的任何类中。然后,我们深入研究了接口隔离原则和依赖倒置原则,这些原则告诉我们,我们应该只提供组件发挥其功能所需的功能,并且我们应该始终通过高级代码中的一些抽象来行动。这些的目的是能够大幅度更改实现细节,例如数据库提供程序,而不必重写大面积的高级代码。

我最近一直在应用这些原则来重构我一直在从事的项目,并认为展示一个示例来说明如何将这些原则应用到实际系统中可能会很好,这些系统通常比此处概述的要复杂得多。

注意:以下内容使用 TypeScript,但我会在遍历过程中解释发生了什么。下面是主体,未重构的组件(不包括返回 JSX)——我建议使用它作为参考,因为我会取出片段并在我们检查和重构每一部分时重点关注这些片段。这也使用 WebSockets,这就是那些引用的内容socket- 将它们视为后端和前端之间的持续链接,以便它们可以相互交谈并实时执行代码。[14]

const ChatBoard: React.FC<RouteComponentProps<any, StaticContext, ChatBoardLocationState>>
= (props: RouteComponentProps<any, StaticContext, ChatBoardLocationState>): JSX.Element => {

    // Return nothing if not redirected here
    if (props.location.search === "") return <div>Please create or join a room</div>;

    const [messageList, setMessageList] = useState<Message[]>([]);
    const [message, setMessage] = useState<string>("");
    const [boardId, setBoardId] = useState<string>("");
    const [socket, setSocket] = useState<SocketIOClient.Socket>();
    const [didCreateRoom, setDidCreateRoom] = useState<boolean>(false);
    const [redirect, setRedirect] = useState<string>("");
    const [warning, setWarning] = useState<string>();
    const [hideVotes, setHideVotes] = useState<boolean>(true);
    const [votedMessages, setVotedMessages] = useState<PersonalVotedMessage[]>([]);

    useEffect(() => {
        // Don't run if there's no search param for the board - here as a safety net from above
        if (props.location.search === "") return;

        // If here from the Redirect or not
        if (props.location.state !== undefined){
            const didCreate = props.location.state.roomCreator;
            setDidCreateRoom(didCreate);
        }
        const newBoardId = qs.parse(props.location.search).board as string;
        setBoardId(newBoardId);
        setSocket(io.connect(ENDPOINT, {query: `board=${newBoardId}`}));

        return () => {
            if (socket) socket.disconnect();
        };
    }, []);

    if (!socket) return <div></div>;


    socket.on("message", (messageList: Message[]) => {
        setMessageList(messageList);
    });

    socket.on("initial-vote-visibility", (voteVis: boolean) => {
        console.log("VOTE VIS", voteVis);
        setHideVotes(voteVis);
    });

    socket.on("creator-disconnect", (message: {msg: string, timeout: number}) => {
        setWarning(message.msg);
        setTimeout(() => {
            socket.close();
            setWarning("");
            setRedirect("/");
        }, message.timeout);
        return;
    });

    socket.on("error", (errorMessage: string) => {
      alert(`Error: ${errorMessage}`)
    });

    const voteMessage = (message: Message, value: number) => {
        const indexOfVoted = votedMessages.findIndex(msg => msg.messageId === message.id);
        // If not in votedMessages array, add it to it and give it a personal vote of +1/-1
        // Else, update its votes
        if (indexOfVoted === -1){
            setVotedMessages([...votedMessages, {messageId: message.id, personalVotes: value}]);
        } else {
            const votedMessage = votedMessages[indexOfVoted];
            const newVotedMessage = {...votedMessage, personalVotes: votedMessage.personalVotes + value};
            if (Math.abs(newVotedMessage.personalVotes) > NUM_VOTES) return alert("Can only vote 3 times per item");
            const newVotedMessageArray = votedMessages.filter(msg => msg.messageId !== message.id);
            setVotedMessages([...newVotedMessageArray, newVotedMessage]);
        }
        socket.emit("upvote", {message, value});
    };

    const renderList = () : JSX.Element[] => {
        return messageList.map((message: Message) => {
            const indexOfVoted = votedMessages.findIndex(msg => msg.messageId === message.id);
            const personalVote = indexOfVoted === -1 ? 0 : votedMessages[indexOfVoted].personalVotes;
            return (
                <FeedbackMessage key={message.id} hideVotes={hideVotes} voteMessage={voteMessage} message={message} personalVote={personalVote} />
            );
        });
    };

    const toggleHideVotes = () => {
        socket.emit("toggle-votes");
    };

    socket.on("toggle-votes", () => {
        setHideVotes(!hideVotes);
    });

    const handleClick = (): void => {
        if (message.length === 0) return alert("Message cannot be empty");
        if (message.includes("\n")) return alert("New line characters are not permitted");
        const user: string = socket.id;
        const newMessage = {user, message, upvotes: 0};
        socket.emit("message", newMessage);
        setMessage("");
        return; 
    };

    if (redirect) return <Redirect to={{pathname: "/", state: {message: "Disconnected due to admin inactivity"}}} />;

    return(
      <div>...</div>
};

如您所见,这看起来很可怕。它杂乱无章,超载,到处都是 UI 和逻辑……让我们看看是否可以稍微改变一下,并迈出第一步,让它变得更好。

使用效果

首先,让我们看看那个凌乱的.useEffect()

useEffect(() => {
    
    // 1. Check valid
    if (props.location.search === "") return;

    // 2. Check who created the chatboard
    if (props.location.state !== undefined){
        const didCreate = props.location.state.roomCreator;
        setDidCreateRoom(didCreate);
    }

    // 3. Parse the board they are attempting to join, and connect them to the backend via a socket
    const newBoardId = qs.parse(props.location.search).board as string;
    setBoardId(newBoardId);
    setSocket(io.connect(ENDPOINT, {query: `board=${newBoardId}`}));

    return () => {
        if (socket) socket.disconnect();
    };
}, []);

好吧,首先我们可以提取出关于确定是否有人将聊天板创建到更高层组件中的逻辑(使用前面提到的包装策略)。这给我们留下了:

useEffect(() => {
    // 1. Check valid
    if (props.location.search === "") return;

    // 3. Parse the board they are attempting to join, and connect them to the backend via a socket
    const newBoardId = qs.parse(props.location.search).board as string;
    setBoardId(newBoardId);
    setSocket(io.connect(ENDPOINT, {query: `board=${newBoardId}`}));

    return () => {
        if (socket) socket.disconnect();
    };
}, []);

但是,如果我们在包装器组件中处理房间创建,为什么不将验证也移到那里呢?props.location.search

useEffect(() => {

    // 3. Parse the board they are attempting to join, and connect them to the backend via a socket
    const newBoardId = qs.parse(props.location.search).board as string;
    setBoardId(newBoardId);
    setSocket(io.connect(ENDPOINT, {query: `board=${newBoardId}`}));

    return () => {
        if (socket) socket.disconnect();
    };
}, []);

好吧——这样更好。我们已经使用 SRP 从. 但是,我们也可以使用 ISP,而只是将它尝试获取的作为 prop 传递给我们的整个组件。useEffect()boardId

useEffect(() => {
    setSocket(io.connect(ENDPOINT, {query: `board=${props.boardId}`}));
    return () => {
        if (socket) socket.disconnect();
    };
}, []);

这看起来整洁多了,现在只做一件事。

套接字事件

在整个组件的顶层,我们有各种套接字事件侦听器。简而言之,这些类似于 onClick 处理程序,它们不是等待单击,而是等待服务器通知运行。

if (!socket) return <div></div>;

socket.on("message", (messageList: Message[]) => {
    setMessageList(messageList);
});

socket.on("initial-vote-visibility", (voteVis: boolean) => {
    console.log("VOTE VIS", voteVis);
    setHideVotes(voteVis);
});

socket.on("creator-disconnect", (message: {msg: string, timeout: number}) => {
    setWarning(message.msg);
    setTimeout(() => {
        socket.close();
        setWarning("");
        setRedirect("/");
    }, message.timeout);
    return;
});

socket.on("error", (errorMessage: string) => {
  alert(`Error: ${errorMessage}`)
});

鉴于我们useEffect关心的是设置套接字,我们可以将它们移到那里,以便将它们与组件的主体分开。

useEffect(() => {
    setSocket(io.connect(ENDPOINT, {query: `board=${props.boardId}`}));

    socket.on("message", (messageList: Message[]) => {
      setMessageList(messageList);
    });

    socket.on("initial-vote-visibility", (voteVis: boolean) => {
      console.log("VOTE VIS", voteVis);
      setHideVotes(voteVis);
    });

    socket.on("creator-disconnect", (message: {msg: string, timeout: number}) => {
      setWarning(message.msg);
      setTimeout(() => {
          socket.close();
          setWarning("");
          setRedirect("/");
      }, message.timeout);
      return;
    });

    socket.on("error", (errorMessage: string) => {
      alert(`Error: ${errorMessage}`)
    });

    return () => {
        if (socket) socket.disconnect();
    };
}, []);

这是有道理的,但是现在我们又回到超载我们的!此外,我们的套接字将花费少量时间来设置,因此这些事件侦听器可能会尝试在具有值之前创建,这意味着我们的组件会出错,因为我们将未定义。为了解决这个问题,让我们稍微改变一下,如果有的话,它会设置一个套接字,然后在套接字设置好后重新运行。useEffect()socketsocketuseEffect

useEffect(() => {
  if (!socket){
    setSocket(io.connect(ENDPOINT, {query: `board=${props.boardId}`}));
  } else {
    socket.on("message", (messageList: Message[]) => {
      setMessageList(messageList);
    });

    socket.on("initial-vote-visibility", (voteVis: boolean) => {
      console.log("VOTE VIS", voteVis);
      setHideVotes(voteVis);
    });

    socket.on("creator-disconnect", (message: {msg: string, timeout: number}) => {
      setWarning(message.msg);
      setTimeout(() => {
          socket.close();
          setWarning("");
          setRedirect("/");
      }, message.timeout);
      return;
    });

    socket.on("error", (errorMessage: string) => {
      alert(`Error: ${errorMessage}`)
      });
    }
    
  return () => {
      if (socket) socket.disconnect();
  };
}, [socket]);

好的,避免了那个错误。现在,继续解决超载问题。好吧,没有理由在这里详细说明事件侦听器,我们如何抽象出一些细节并实现 DIP?

// Set up socket connection and listeners
useEffect(() => {
    if (!socket){
        setSocket(io.connect(ENDPOINT, {query: `board=${boardId}`}));
    } else {
        socketOnVoteVis(socket, setHideVotes);
        socketOnMessageList(socket, setMessageList);
        socketOnCreatorDC(socket, setWarning, setRedirect);
        socketOnError(socket);
    }
    
    return () => {
        if (socket) socket.disconnect();
    };
}, [socket]);

在这里,我们已经从我们的组件中完全清除了我们的套接字侦听器,因此降低了它的复杂性。我们现在有一个完全独立的文件,socketFunction.ts其中包含:

// Listeners
export const socketOnMessageList
= (socket: SocketIOClient.Socket, setMessageList: (messageList: Message[]) => void): void => {
    socket.on("message", (messageList: Message[]) => {
        setMessageList(messageList);
    });
};

export const socketOnError
= (socket: SocketIOClient.Socket): void => {
    socket.on("error", (errorMessage: string) => {
        alert(`Error: ${errorMessage}`);
    });
};

export const socketOnVoteVis
= (socket: SocketIOClient.Socket, setHideVotes: (hideVotes: boolean) => void): void => {
    socket.on("toggle-votes", (newVoteVis: boolean) => {
        console.log("TOGGLEED VOTES");
        setHideVotes(newVoteVis);
    });
};

export const socketOnCreatorDC
= (socket: SocketIOClient.Socket, setWarning:(str: string) => void, setRedirect: (str: string) => void): void => {
    socket.on("creator-disconnect", (message: {msg: string, timeout: number}) => {
        setWarning(message.msg);
        setTimeout(() => {
            socket.close();
            setWarning("");
            setRedirect("/");
        }, message.timeout);
    });
};

请注意,我们的应用程序的复杂性一定存在于某个地方,我们的目标只是将其分散到多个区域,这样我们就可以从高度抽象的概念开始逐步跟踪文件线索,直至实现细节。下一步,可能是改为创建一个函数......setupSocketListeners()

// Set up socket connection and listeners
useEffect(() => {
    if (!socket){
        setSocket(io.connect(ENDPOINT, {query: `board=${boardId}`}));
    } else {
        setupSocketListeners(socket, setHideVotes, setMessageList, setWarning, setRedirect);
    }
    
    return () => {
        if (socket) socket.disconnect();
    };
}, [socket]);

虽然这可能是一个扩展,但我觉得这可能太过分了。我们将 5 个参数传递给这个有点混乱的函数,并且我们将所有套接字侦听器耦合在一起,这样对一个侦听器的任何更改都可能影响所有这些侦听器。然而,可以肯定的是,这种方法可以通过将所需的参数捆绑在一起成为不同的抽象,然后从整个函数中抽象出每个套接字设置的细节来工作。这是在您认为合适且适合您的点画线的示例。setupSocketListeners()

处理点击

作为最后一个示例,让我们看一下该函数。请注意,“消息”是一个有状态变量。handleClick()

const handleClick = (): void => {
    if (message.length === 0) return alert("Message cannot be empty");
    if (message.includes("\n")) return alert("New line characters are not permitted");
    const user: string = socket.id;
    const newMessage = {user, message, upvotes: 0};
    socket.emit("message", newMessage);
    setMessage("");
    return; 
};

我们希望从中得到什么行为?好吧,我们想验证消息是否正确,消息的格式是否正确,然后emit再次通过套接字发送一个事件(与 ;相反,不是设置一个处理程序在从后端接收到事件时运行,这是前端向后端发送一个事件,这将导致后端上的某些功能运行)。因此,我们可以将这些功能抽象出来,因为我们实际上不需要知道消息是如何验证的,只需要我们可以运行一些函数来检查它。socket.emitsocket.onmessage

const handleClick = (): void => {
    const err: string | null = messageValidator(message);
    if (err) {
        alert(err);
        return;
    }
    const user: string = socket.id;
    const newMessage = {user, message, upvotes: 0};
    socket.emit("message", newMessage);
    setMessage("");
};

// messageValidator.ts
export const messageValidator = (message: string): string | null => {
    if (message.length === 0) return "Message cannot be empty";
    if (message.includes("\n")) return "New line characters are not permitted";
    return null;
};

现在我们有了函数,如果消息无效,它将返回一个错误。否则,我们继续格式化并通过事件将消息发送到后端。但是请注意,我们可以进一步抽象它;我们不需要担心消息是如何发送的或以什么格式发送的。因此,让我们也将其抽象出来:messageValidator()socket.emit("message")

const handleClick = (): void => {
    const err: string | null = messageValidator(message);
    if (err) {
        alert(err);
        return;
    }
    socketEmitNewMessage(socket, message);
    setMessage("");
};

// Separate socketEmits.ts file
export const socketEmitNewMessage = (socket: SocketIOClient.Socket, message: string): void => {
    const user: string = socket.id;
    const newMessage = {user, message, upvotes: 0};
    socket.emit("message", newMessage);
};

好的!我们的now 准确地代表了我们想要的行为。最后,我们可以从字面上说“handleClick 将验证消息,将消息发送到后端,然后将消息状态重置为空字符串”——这反映了我们所做的功能。handleClick()

仅说出您希望函数执行的操作可能会有所帮助。您所做的每条语句都可能是一个单独的函数。

整理起来

虽然相对较小,但我希望现实生活中的例子对您有所帮助。我绝不说这是完全完美的,它不是,但它肯定比仅仅通过将非常基本的 SOLID 应用到我们的架构要好得多。

相关标签:
  • React组件设计原则
0人点赞

发表评论

当前游客模式,请登陆发言

所有评论(0)