From 9c1e35e29990afd3b228bf27b88f87483424d0aa Mon Sep 17 00:00:00 2001 From: Itay Shtekel Date: Sun, 1 Mar 2020 17:24:17 +0200 Subject: [PATCH] Add some info and fixes to README --- README.md | 169 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 146 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index eb1cfa45..c76f4efa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ -# Welcome to repluggable +# Welcome to Repluggable -Repluggable implements micro-frontends in a React+Redux app. Functionality of a Repluggable app is composed incrementally from a list of pluggable packages. Every package extends those already loaded by contributing new functionality into them. Pieces of UI contributed by a package can be rendered anywhere, not being limited to dedicated subtree of DOM. All packages privately manage their state in a modular Redux store, which plays the role of common event mechanism. Packages interact with each other by contributing and consuming APIs, which are objects that implement declared interfaces. Packages can be plugged in and out at runtime without the need to reload a page. +Repluggable is TypeScript library for implementing inversion of control in front-end applications. +* Write once - inject everywhere you need +* Create well-defined contracts +* Migrate, extend and replace modules pain-free + +Repluggable implements micro-frontends in a React+Redux app. Functionality of a Repluggable app is composed incrementally from a list of pluggable packages. Every package extends those already loaded by contributing new functionality into them. Pieces of UI contributed by a package can be rendered anywhere, not being limited to dedicated subtree of DOM. All packages privately manage their state in a modular Redux store, which plays the role of common event mechanism. Packages interact with each other ONLY by contributing and consuming APIs, which are objects that implement declared interfaces. Packages can be plugged in and out at runtime without the need to reload a page. + +Since all communication between modules is trough contracts defined by APIs, the modules have only runtime dependencies! Navigate: [How-to](#How-to) | [Architecture](#Architecture) @@ -146,18 +153,24 @@ import { EntryPoint } from 'repluggable' const FooEntryPoint: EntryPoint = { - // required: specify name of the entry point + // required: specify unique name of the entry point name: 'FOO', // optional getDependencyAPIs() { return [ // DO list required API keys - // DO list components form other packages, - // which are in use by your components - BarAPI, BazInputBox + BarAPI ] - } + }, + + // optional + declareAPIs() { + // DO list API keys that will be contributed + return [ + FooAPI + ] + }, // optional attach(shell: Shell) { @@ -185,7 +198,7 @@ const FooEntryPoint: EntryPoint = { The `EntryPoint` interface consists of: - declarations: `name`, `getDependencies()` -- lifecycle hooks: `attach()`, `extend()`, `uninstall()` +- lifecycle hooks: `attach()`, `extend()`, `detach()` The lifecycle hooks receive an `Shell` object, which represents the `AppHost` for this specific entry point. @@ -240,19 +253,19 @@ To create an API, perform these steps: } ``` -1. Contribute your API from an entry point `install` function: +1. Contribute your API from an entry point `attach` function: ```TypeScript import { FooAPI, createFooAPI } from './fooApi' const FooEntryPoint: EntryPoint = { - ... + // ... attach(shell: Shell) { shell.contributeAPI(FooAPI, () => createFooAPI(shell)) } - ... + // ... } ``` @@ -275,13 +288,13 @@ To contribute the reducers, perform these steps: ```TypeScript // state managed by bazReducer export interface BazState { - ... + // ... xyzzy: number // for example } // state managed by quxReducer export interface QuxState { - ... + // ... } ``` @@ -301,14 +314,14 @@ To contribute the reducers, perform these steps: state: BazState = { /* initial values */ }, action: Action) { - ... + // ... } function quxReducer( state: QuxState = { /* initial values */ }, action: Action) { - ... + // ... } ``` @@ -379,7 +392,7 @@ The usage of `connectWithShell()` is demonstrated in the example below. Suppose ```jsx (props) => ( -
+
= (props) => ( -
+
...
) @@ -430,7 +443,7 @@ In order to implement such component, follow these steps: 1. Write the connected container using `connectWithShell`. The latter differs from `connect` in that it passes `Shell` as the first parameter to `mapStateToProps` and `mapDispatchToProps`. The new parameter is followed by the regular parameters passed by `connect`. Example: ```TypeScript - export const Foo = connectWithShell( + export const createFoo = (boundShell: Shell) => connectWithShell( // mapStateToProps // - shell: represents the associated entry point // - the rest are regular parameters of mapStateToProps @@ -456,16 +469,126 @@ In order to implement such component, follow these steps: shell.getAPI(BarAPI).createNewBar() } } - } + }, + boundShell )(FooSfc) ``` The `Shell` parameter is extracted from React context `EntryPointContext`, which represents current package boundary for the component. -### Exporting React components +### Using React components + +Since all communication between modules is trough API there are exactly 2 ways to use components + +A. Contribution into another API that will take care of the rendering + +B. Expose on a public API + +### Option A - Contribution of React component into other module + +In order to contribute a component we need to prepare a [slot](#extension-slots) which the component is going to be contributed into, and expose an API function for other packages to call. + +`MainViewAPI.ts` +```TypeScript +import { ReactComponentContributor, Shell, SlotKey } from 'repluggable' + +// What is contributed into this slot +export interface ContributedComponent { + component: ReactComponentContributor; +} + +export const componentsSlotKey: SlotKey = { + name: 'contributedComponent', +} + +export const createMainViewAPI = (shell: Shell) => { + const componentsSlot = shell.declareSlot(componentsSlotKey) + + return { + contributeComponent(fromShell: Shell, contribution: ContributedComponent) { + componentsSlot.contribute(fromShell, contribution) + } + } +} +``` + +`MainViewPackage.tsx` +```tsx +import { SlotRenderer, EntryPoint } from 'repluggable' +export const MainViewEntryPoint: EntryPoint = { + name: 'MAIN_VIEW', + + declareAPIs() { + return [MainViewAPI] + }, -TBD (advanced topic) + attach() { + shell.contributeAPI(MainViewAPI, () => createMainViewAPI(shell)) + }, + + extend(shell) { + shell.contributeMainView(shell, () => ) + } +} +``` + +Then we can add component from any other entry point, while MainView agnostically rendering whatever is contributed into it's slot. +For connecting components see [connectWithShell](#creating-react-components) + +`MyButtonPackage.tsx` +```tsx +export const MyButtonEntryPoint: EntryPoint = { + name: 'MY_BUTTON', + + getDependencyAPIs() { + return [MainViewAPI] + }, + + extend(shell) { + const MyButton = createMyConnectedButton(shell) + shell.getAPI(MainViewAPI).contributeComponent(shell, { + component: () => + }) + } +} +``` + +### Option B - Expose components on a public API +For connecting components see [connectWithShell](#creating-react-components) + +`FooAPI.ts` +```TypeScript +export const createFooAPI = (shell: Shell) => { + const MyButton = createConnectedButton(shell) + + // Expose MyButton as part of the contract provided by FooAPI + return { MyButton } +} + +``` + +Assume we want to use `MyButton` in a component we are contributing to an API called `BarAPI` + + +*See [Contribution of React component into other module](#contribution-of-react-component-into-other-module) on how to prepare a slot for the component contribution in `BarAPI`. + + +`ButtonConsumerPackage.tsx` +```tsx + getDependencyAPIs() { + return [BarAPI, FooAPI] + }, + extend(shell) { + shell.getAPI(BarAPI).contributeComponent(shell, { + component: () => { + // Get the component implementation exposed on FooAPI + const { MyButton } = shell.getAPI(FooAPI) + return
+ } + }) + }, +``` ## Testing a package @@ -473,7 +596,7 @@ TBD # Architecture -`repluggable` allows composition of a React+Redux application entirely from a list of pluggable packages. +`Repluggable` allows composition of a React+Redux application entirely from a list of pluggable packages. A package is a box of lego pieces such as UI, state, and logic. When a package is plugged in, it contributes its pieces by connecting them to other pieces added earlier. In this way, the entire application is built up from connected pieces, much like a lego.