这篇文章将深入探讨使用组件点表示法时的这些优势,重点介绍一些问题,并提供一些示例。
顾名思义,它使用“点”来访问对象的属性,通常称为点表示法。但是,由于这是在组件级别(仍然只是对象),为了清楚起见,我更喜欢“组件点表示法”。一个简单的例子是 React Context (https://reactjs.org/docs/context.html)。
const ThemeContext = React.createContext("light");
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}
}
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
在此示例中,创建了 ThemeContext
并且是顶级组件。 Provider
和 Consumer
都是 ThemeContext
的子组件,使用点符号访问。
这些术语将在帖子的其余部分中使用。
•顶级组件:实际导入的组件(例如:ThemeContext
或 Flex
)。每组组件只有一个。•子组件:使用点符号访问的任何组件(例如:ThemeContext.Provider
或 Flex.Item
)。每组有一个或多个组件。•组件点符号:使用点符号从顶级组件访问子组件。
在使用组件点符号来维护和使用一组组件时,我体验到了一些关键的好处。
✏️ 命名空间
由于使用组件点表示法,所有子组件本质上都由顶级组件命名。我们以一个包装了 CSS flexbox
的 Flex
组件为例。顶层组件名为 Flex
,带有一个子组件:Flex.Item
。
import { Flex } from "flex";
function User() {
return (
<Flex align="center">
<Flex.Item shrink={0} grow={0}>
<Avatar />
</Flex.Item>
<Flex.Item shrink={1} grow={1}>
<UserInfo />
</Flex.Item>
</Flex>
);
}
它不会强制或停止使用 Flex
。 Flex
之外的项目,但由于它是一个子组件,它确实暗示任何可能使用它的开发人员,它应该只用作 Flex 的子组件。
🚢 单一导入
使用这种技术,只有一个入口点可以使用 flex
组件。 Flex.Item
组件定义和逻辑是否与 Flex
在同一个文件中、在同级文件中或在嵌套目录中都没有关系。底层实现和文件结构可以随时更改,因为唯一的公共合约是 Flex
的导出。与单独导入每个组件相比,这减少了“公开”API 过多,其中实现或文件结构的更改将破坏现有用法。
随着功能随着时间的推移而发展,并且由于需求的变化而添加和删除部分,导入可以保持不变,这可以减少导入更改的噪音。
🔍 可发现性
如果一组中有“n”个组件,则开发人员必须记住所有“n”个组件名称才能知道要导入哪个组件或进行文件搜索以找到他们需要的组件。但是,使用组件点表示法,只需要记住顶级组件,并且所有组件选项都将建议在点之后!没有必要记住。这也提高了可能未知的所有可用组件的可发现性。
当组件点表示法运作良好时,有各种实际示例。例如,像 Flex 这样的包装组件,将 Flex.Item 作为子组件。
class Flex extends React.Component<Props> {
public static Item = FlexItem;
public render() {
// ...
}
}
或者设计系统中可能具有多个构建块的稍微复杂的组件。例如,一个 Table 组件具有许多子组件,例如 Table.Row、Table.Cell 和 Table.Head,这些子组件只能在 Table 中用作子组件。
class Table extends React.Component<Props> {
public static Body = TableBody;
public static Cell = TableCell;
public static Controls = TableControls;
public static Head = TableHead;
public static Header = TableHeader;
public static Row = TableRow;
public render() {
// ...
}
}
最后,它适用于大型或复杂的组件集,例如具有各种过滤器组件、分页、结果等的搜索功能。
<Search category="Users">
<Search.Filters>
<Search.Query title="Search" placeholder="Enter a keyword..." />
<Search.Facet title="Status" />
<Search.DateRange title="Application Date" />
</Search.Filters>
<Search.PaginationCounter />
<Search.Sort />
<Search.Results
component={UsersCard}
renderEmpty={UsersNoResults}
renderLoading={UsersLoading}
/>
</Search>
在使用组件点表示法时,您可能会偶然发现一些值得注意的“陷阱”。
在顶级组件上使用更高阶的组件(例如从 react-redux 连接)可能会很棘手。特别是在使用 connect 时,它会将所有静态属性提升到包装组件(大多数高阶组件都会这样做),但不会保留正确的类型。在这种情况下,需要强制转换高阶组件,或者如果可能,避免将高阶组件与顶级组件一起使用。
如上所述,子组件的底层实现并不重要。在 Flex 的情况下,Flex.Item
组件实现本身可以命名为 NeverCallThisComponentDirectly
。这很好,但唯一的缺点是在 React Devtools 中,它会显示为 NeverCallThisComponentDirectly
,这可能会非常混乱,因为它从未被直接调用过。
解决此问题的一种方法是在组件上设置 displayName
以匹配它的使用方式。在这种情况下,组件名称仍为 NeverCallThisComponentDirectly
,但现在显示名称为 Flex.Item
。
class NeverCallThisComponentDirectly extends React.Component<Props> {
public static displayName = "Flex.Item";
public render() {
// ...
}
}
底层实现根本没有改变,但现在组件既用作 Flex.Item
,又在 React Devtools 中正确地视为 Flex.Item
。
到目前为止,所有示例都使用类组件,但同样的方法也可以用于函数组件。但是,它需要在类型声明中显式声明子组件。
const Flex: React.FC<Props> & { Item: typeof FlexItem } = () => {
// ...
};
Flex.Item = FlexItem;
此类型声明使用交集将标准 React 函数组件类型与声明 Item
属性的类型结合起来。然后,这允许以与上面的类组件相同的方式分配和稍后使用 Flex.Item
。
这种方法的一个缺点是它可以“打破”摇树。在高层次上,tree shaking 的工作原理是删除未导入或未使用的代码。由于顶级 Search 组件导入并公开了所有子组件,因此即使从未使用过它们也会全部包含在内。但是,如果这是一个实际问题,则可能表明组件点符号的过度使用或组件集不相关。
在使用一组组件时,组件点表示法可能是一种有用的技术。它将 API 表面积最小化为单个导出,保持导入简单并提高可用子组件的可发现性。