本文来自于 Excalidraw 的 Rethinking the Component API 由 ikoofe 完成翻译。Excalidraw 是一个比较受欢迎的手绘风格的白板应用,它提供了编辑器以及相关组件来支持开发者自己搭建应用。
自从去年年底我们公布完成编辑器的重新设计以来,许多开发人员都迫切地提到了一个问题,那就是什么时候把它发布到 Excalidraw package 中,以及为什么没有在一开始就发布它?
我们没有在第一时间发布 excalidraw.com 的原因是要满足一些定制化开发。在我们的对外的公共应用中,有一些内容需要进行硬编码,例如主菜单(main menu)和欢迎页(welcome screen)-- 这些内容可能并不需要在个人的私有应用中进行硬编码 -- 我们不太确定如何设计 API 以使其更易于定制。
今天,我们发布了新 API 的初始版本,之前阻塞新版本发布的那部分内容,基本都可以以自定义的方式实现。如果大家给出的反馈是积极的,我们将继续以类似的方式公开更多的 API,以便大家可以根据个人和用户的需要来定制编辑器。
现在介绍一下我们都做了哪些事情,包括我们是如何以及为什么这么做?
作为重新设计的一部分,我们引入了两个新的主要内容:作为主菜单的左上角下拉菜单,以及欢迎页包括对新用户的提示,以帮助他们快速了解 UI 的要点。
这里很明确的是,你会根据自己的需要来定制这些功能,或者是在你自己的应用程序中移除那些不需要的部分。
到目前为止,我们一直在使用 config objects(例如 props.UIOptions)和 render props(例如,props.renderFooter)的组合方式。虽然这些都很好,您可以通过这种方式完成大多数事情,但我们需要更加灵活和可组合的 API,并且使用起来更加直观。
我们希望您不仅能够渲染自定义组件(例如自定义页脚),还可以修改默认组件。之前我们通过 render props 来暴露扩展点。虽然 render props 也能完成相关工作,但我们设计了这样一种 API,可以将所有内容都渲染为 Excalidraw 组件的子项,如下所示:
import { Excalidraw, MainMenu, Footer } from '@excalidraw/excalidraw';
import { MyCustomButton } from './MyCustomButton';
export const App = () => (
<Excalidraw>
<MainMenu>
{/* menu items */}
</MainMenu>
<Footer>
<MyCustomButton>
</Footer>
</Excalidraw>
)
未来,我们甚至可能将插件作为组件公开,您只需这样去使用它们:
import { Excalidraw, MinimapPlugin } from "@excalidraw/excalidraw";
export const App = () => (
<Excalidraw>
<MainMenu>{/* menu items */}</MainMenu>
<MinimapPlugin />
</Excalidraw>
);
说到底,这更像是一个美学决定,而不是功能决定,因为我们也可以通过 render props 实现这些功能。但是这样做的一个好处是 API 表面积更小。我们不需要为组件提供 render props,只需将其直接渲染即可。
这么做的另一个目标是开始将 UI 与编辑器分离。我们希望核心在没有 React 的情况下可用,因此,更清晰地描绘 UI 并将其与编辑器配置和逻辑分离是有意义的。
但是,使用 children 并不是没有权衡,所以让我们来看一下这些新 API 的变化,解释一下它是如何在幕后工作的,以及对您的应用程序的影响。
<Footer/>
以下是编辑器中页脚的外观:
我们有默认的页脚 UI,您当前无法更改。中间有一个区域是可以渲染的。之前,您会使用 renderFooter props 来执行此操作。现在,您将从我们的包中导入Footer 组件,并将其作为 Excalidraw 的子组件进行渲染。
import { Excalidraw, Footer } from "@excalidraw/excalidraw";
const App = () => (
<Excalidraw>
<Footer>
<button onClick={() => console.log("Clicked!")}>Click me</button>
</Footer>
</Excalidraw>
);
那么这到底是如何实现的呢?我们如何知道哪些是 Footer 子组件,哪些是不相关的组件,以及如何将其渲染到 UI 中恰当的位置上?
为此,我们决定使用一个很古老的 React API,当前被认为是已经过时的 API:React.Children。它能够很好地解决一些问题,所以我们继续使用它:)。
简而言之,我们通过循环将子组件传递给 Excalidraw,并使用它们的 displayName 筛选出我们要查找的组件。下面是简化后的代码:
export const getReactChildren = <
ExpectedChildren extends {
[k in string]?: React.ReactNode;
}
>(
children: React.ReactNode,
) => {
return React.Children.toArray(children).reduce(
(acc: Partial<ExpectedChildren>, child) => {
if (React.isValidElement(child)) {
acc[child.type.displayName] = child;
}
return acc;
},
{},
);
};
在实践中,我们还根据预期对子组件的名称进行验证,然后对这些组件进行渲染。这么做的好处是,允许您将所有 UI 组件作为子组件进行渲染,而不必考虑顺序,由我们来判断在何处渲染。
但它也有一些限制。首先,您必须将组件渲染为 Excalidraw 组件的顶级子组件。虽然这不是什么大问题,但这意味着不能将 Footer 组件作为另一个组件的子组件,否则我们将无法找到它:
const MyFooter = () => {
return <Footer />;
};
const App = () => (
<Excalidraw>
{/* nope :( */}
<MyFooter />
</Excalidraw>
);
让我们继续介绍下一个组件。
<MainMenu/>
在新的编辑器设计中引入了左上角的下拉菜单,我们希望您能够根据自己的需要对其进行自定义。
下面是 excalidraw.com 上的菜单(左),以及我们在包中默认呈现的菜单(右)。
我们保证了最大程度上的灵活性。如果默认的选项不适合您,您可以渲染自己的MainMenu 组件,我们允许您从头开始构建它 -- 无论是使用默认菜单项组件还是您自己的组件。
import { Excalidraw, MainMenu } from "@excalidraw/excalidraw";
const App = () => (
return (
<Excalidraw>
<MainMenu>
<MainMenu.DefaultItems.LoadScene />
<MainMenu.DefaultItems.Export />
<MainMenu.DefaultItems.SaveAsImage />
<MainMenu.Separator />
<MainMenu.Item onSelect={() => alert("Hello to you too!")}>
Hello!
</MainMenu.Item>
</MainMenu>
</Excalidraw>
);
);
与 Footer 一样,您需要确保它是 Excalidraw 组件的顶级子组件。
<WelcomeScreen/>
我们在重新设计之后的另外一项内容是欢迎页。这是一个有点复杂的组件,因为它由几个单独的元素组成,每个元素在 UI 的不同位置渲染。
其中两个最主要的组件是包含 logo 和快速操作的中心部分,以及指出用户可以在 UI 中找到什么的提示组件。
您可以对其中的大部分内容进行自定义,比如可以只渲染中心部分,也可以改变一下提示。
值得注意的是,我们不仅要求 <WelcomeScreen> 是顶级子组件,而且还要求 <WelkeScreen.Center/> 和 <WelcomeScreen.Hints/> 是 <WelcomeScreen> 的直接子组件。
import { Excalidraw, WelcomeScreen } from "@excalidraw/excalidraw";
const App = () => (
return (
<Excalidraw>
<WelcomeScreen>
<WelcomeScreen.Hints.ToolbarHint />
<WelcomeScreen.Center>
<WelcomeScreen.Center.Logo />
<WelcomeScreen.Center.Heading>
You can draw anything you want!
</WelcomeScreen.Center.Heading>
<WelcomeScreen.Center.Menu>
<WelcomeScreen.Center.MenuItemHelp />
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
onSelect={() => setCollabDialogShown(true)}
/>
{!isExcalidrawPlusSignedUser && (
<WelcomeScreen.Center.MenuItem
onSelect={() => console.log("doing something!")}
>
Do something
</WelcomeScreen.Center.MenuItem>
)}
</WelcomeScreen.Center.Menu>
</WelcomeScreen.Center>
</WelcomeScreen>
</Excalidraw>
);
);
现在,您可以在我们的文档中阅读有关新引入的 API 的更多信息。但是,我们正在努力完善文档,并且将很快发布这些文档,以及上面 API 和如何处理特定情况的更详细示例。敬请期待!💜
关注公众号,阅读最新文章