docs: lotsa more composability docs

This commit is contained in:
Patrik Oldsberg
2021-01-12 20:23:27 +01:00
parent 3e9b38e288
commit 492258d2a1
2 changed files with 234 additions and 23 deletions
+232 -23
View File
@@ -26,7 +26,12 @@ core is the new concept of Extensions, are exported by plugins for use in the
app. There is a new primitive called component data, which is used to connect
plugin and the app, and a new hook that provides a practical use of .
## Component Data
## New Concepts
This section is a brief look into all the new and updated concepts that were put
in place to support the new composability system.
### Component Data
Component data is a new composability primitive that is introduced as a way to
provide a new data dimension for React components. Data is attached to React
@@ -43,44 +48,248 @@ const myData = getComponentData(element, 'my.data');
```
The purpose of component data is to provide a method for embedding data that can
be inspected before rendering elements. It's a pattern that is quite common
among React libraries, and used for example by `react-router` and `material-ui`
to discover properties of the child elements before rendering. Although in those
libraries only the element type and props are typically inspected, while our
component data adds more structured access and simplifies evolution by allowing
for multiple different versions of a piece of data to be used at once.
be inspected before rendering elements. Element inspection is a pattern that is
quite common among React libraries, and used for example by `react-router` and
`material-ui` to discover properties of the child elements before rendering.
Though in those libraries only the element type and props are typically
inspected, while our component data adds more structured access and simplifies
evolution by allowing for multiple different versions of a piece of data to be
used at once.
The main use-case
The initial use-cases for component data is support route and plugin discovery
through elements in the app. Through this we allow for the React element tree in
the app to be the source of truth, both for which plugins are used and all
top-level plugin routes in the app. The use of component data is not limited to
these use-cases though, as it can be used as a primitive to create new
abstractions as well.
## Extensions
### Extensions
Extensions are what plugins export for use in an app. Most typically they are
React components, but in practice they can be any kind of value. They are
created using `create*Extension` functions, and wrapped with `plugin.provide()`
in order to create the actual exported extension.
The Backstage core API currently provides two different types of extension
creators, `createComponentExtension`, and `createRoutableExtension`.
The extension type is dead simple:
### Extensions from a plugin's point of view
```ts
export type Extension<T> = {
expose(plugin: BackstagePlugin<any, any>): T;
};
```
The power of extensions comes from the ability of various actors to hook into
their usage. The creation and plugin wrapping is controlled by whoever owns the
creation function, the Backstage core is able to hook into the process of
exposing the extension outside the plugin, and in the end the app controls the
usage of the extension.
The Backstage core API currently provides two different types of extension
creators, `createComponentExtension`, and `createRoutableExtension`. Component
extensions are plain react component with no particular requirements, such as
cards for entity overview pages. The component will be exported more or less as
is, but is wrapped up to provide things like an error boundary, lazy loading,
and a plugin context.
Routable extensions build on top of component extensions and are used for any
component that should be rendered at a specific route path, such as full pages
or entity page tab content. When creating a routable extension you need to
supply a `RouteRef` as `mountPoint`. The mount point will be the handle of the
component for the outside world, and is used by other components and plugins
that wish to link to the routable component.
As of now there are only two extension creation functions, but it is possible to
add more of them in the future, both in the core library and in plugins that
wish to provide an extension point for other plugins to build upon. Extensions
are also not tied to React, and can both be used to model generic JavaScript
concepts, as well as potentially bridge to rendering libraries and web
frameworks other than React.
### Extensions from a Plugin's Point of View
Extensions are one of the primary methods to traverse the plugin boundary, and
the way that plugins provide concrete content for use within an app. They
replace existing component export concepts such as `Router` or `*Card`s for
display on entity overview pages.
### Using Extensions in an app
It is recommended to create the exported extensions either in the top-level
`plugin.ts` file, or in a dedicated `extensions.ts` (or `.tsx`) file. That file
should not contain the bulk of the implementation though, and in fact, if the
extension is a React component it is recommended to lazy-load the actual
component. Component extensions support lazy loading out of the box using the
`lazy` component declaration, for example:
```ts
export const EntityFooCard = plugin.provide(
createComponentExtension({
component: {
lazy: () => import('./components/FooCard').then(m => m.FooCard),
},
}),
);
```
Routable extensions even enforce lazy loading, for example:
```ts
export const FooPage = plugin.provide(
createRoutableExtension({
component: () => import('./components/FooPage').then(m => m.FooPage),
mountPoint: fooRouteRef,
}),
);
```
### Using Extensions in an App
Right now all extensions are modelled as React components. The usage of these
extension is like regular usage of any React components, with one important
difference. Extensions must be all be part of a single React element tree
spanning from the root `AppProvider`.
For example, the following app code does **NOT** work:
```tsx
const AppRoutes = () => (
<Routes>
<Route path="/foo" element={<FooPage />} />
<Route path="/bar" element={<BarPage />} />
</Routes>
);
const App = () => (
<AppProvider>
<AppRouter>
<Root>
<AppRoutes />
</Root>
</AppRouter>
</AppProvider>
);
```
But it is simple to fix! Simply make sure that you don't create any intermediate
components in the app, for example like this:
```tsx
const appRoutes = (
<Routes>
<Route path="/foo" element={<FooPage />} />
<Route path="/bar" element={<BarPage />} />
</Routes>
);
const App = () => (
<AppProvider>
<AppRouter>
<Root>{appRoutes}</Root>
</AppRouter>
</AppProvider>
);
```
### New Routing System
A big piece of what is enabled by moving over to this new composability system
is to make `RouteRef`s useful. The `RouteRef`s no longer have their own path, in
fact the only required parameter is currently a `title`. Instead of assigning a
path to each `RouteRef` and possibly overriding these paths in the app, the
concrete `path` for each `RouteRef` is discovered based on the element tree in
the app. Let's consider the following example:
```tsx
<Routes>
<Route path="/foo" element={<FooPage />} />
<Route path="/bar" element={<BarPage />} />
</Routes>
```
We'll assume that `FooPage` and `BarPage` are routable extensions, exported by
the `fooPlugin` and `barPlugin` respectively. Since the `FooPage` is a routable
extension it has a `RouteRef` assigned as its mount point, which we'll refer to
as `fooRootRouteRef`.
Given the above example, the `fooRootRouteRef` will be associated with the
`'/foo'` route. The path is no longer accessible via the `path` property of the
`RouteRef` though, as the routing structure is tied to the app's react tree. We
instead use the new `useRouteRef` hook if we want to create a concrete link to
the page. The `useRouteRef` hook takes a single `RouteRef` as its only
parameter, and returns a function that is called to create the URL.
Now let's assume that we want to link from the `BarPage` to the `FooPage`.
Before the introduction of the new composability system, we would do this by
importing the `fooRootRouteRef` from the `fooPlugin`. This created an
unnecessary dependency on the plugin, and also provided little flexibility
allowing the app to tie plugins together rather than the plugins themselves. To
handle this, we introduce the concept of `ExternalRouteRef`s. Much like regular
route refs, they can be passed to `useRouteRef` to create concrete URLs, but
they can not be used as mount points in routable component and instead have to
be associated with an actual using route bindings in the app.
The `ExternalRouteRef` inside the `barPlugin` should also not be opinionated
about what it is linking to either, allowing the app to decide the final target.
It should however provide context in how the link is presented or used, to make
it easier to understand the flow of the app. If the `BarPage` for example wants
to link to an external page in the header, it might declare an
`ExternalRouteRef` similar to this:
```ts
const headerLinkRouteRef = createExternalRouteRef();
```
### Binding External Routes in the App
The association of external routes are controlled by the app. Each
`ExternalRouteRef` of a plugin is bound to an actual `RouteRef`, usually from
another plugin. The binding process happens once att app startup, and is then
used through the lifetime of the app to help resolve concrete route paths.
Using the above example of the `BarPage` linking to the `FooPage`, we might do
something like this in the app:
```ts
createApp({
bindRoutes({ bind }) {
bind(barPlugin.externalRoutes, {
headerLink: fooPlugin.routes.root,
});
},
});
```
Given the above binding, using `useRouteRef(external)`
Note that we are not importing and using the `RouteRef`s directly, and instead
rely on the plugin instance to access routes of the plugins. This is a new
convention that was introduced to provide better namespacing and discoverability
of routes, as well as reduce the number of different things exported from each
plugin package. The route references would be supplied to `createPlugin` like
this:
```ts
// In foo-plugin
export const fooPlugin = createPlugin({
routes: {
root: fooRootRouteRef,
},
...
})
// In bar-plugin
export const barPlugin = createPlugin({
externalRoutes: {
headerLink: headerLinkRouteRef,
},
...
})
```
### New Catalog Components
EntitySwitch & EntityLayout, and how to use those in the app
TODO
## RouteRefs, useRouteRef, and plugin routes and externalRoutes
## Porting Existing Plugins
TODO
## Binding external routes in the app
TODO
## New catalog components, EntitySwitch & EntityLayout, and how to use those in the app
TODO
## Porting Existing Apps