Excalidraw 对组件 API 的思考和设计

自从去年年底我们公布完成了编辑器的重新设计以来,许多开发人员都迫切地提到了一个问题,那就是什么时候把它发布到 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 来暴露扩展点。虽然 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 组件的子组件,以及您需要的任何 UI。

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 中找到什么的提示。

您可以对其中的大部分内容进行自定义。你可以只渲染中心部分,也可以改变一下提示。

值得注意的是,我们不仅要求 是顶级子组件,而且还要求 <WelkeScreen.Center/> 和 <WelcomeScreen.Hints/> 是 的直接子组件。

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.MenuItemLiveCollaborationTriggeronSelect={() => setCollabDialogShown(true)}/>{!isExcalidrawPlusSignedUser && (<WelcomeScreen.Center.MenuItemonSelect={() => console.log("doing something!")}>Do something</WelcomeScreen.Center.MenuItem>)}</WelcomeScreen.Center.Menu></WelcomeScreen.Center></WelcomeScreen></Excalidraw>);
); 

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

猜你喜欢

转载自blog.csdn.net/web2022050903/article/details/129597904