@ringcentral-integration/next-core
v0.0.1
Published
Core package for RingCentral Integration next-generation architecture.
Downloads
178
Readme
@ringcentral-integration/next-core
This is the foundation package for RingCentral integration products, it is based on Reactant.
Reactant is a progressive framework based on Redux, Inversify, React Router, and Mutative for OOP module model design.
It supports building shared web applications that support multiple windows.
- Shared Tab
- Shared Worker
- Detached window
- iframe
- Any other data-transport based application port, such as WebRTC
Table of Contents
- @ringcentral-integration/next-core
Usage
yarn add @ringcentral-integration/next-coreconst app = await createApp(config);
app.bootstrap(document.getElementById('app'));APIs
Dependency Injection
The foundation package provides dependency injection, which supports TypeScript(also supports JavaScript), and we recommend the experimentalDecorators feature based on TypeScript, as well as Reflect.metadata to record the dependency injection metadata.
If using
babel, make sure that you install@babel/plugin-propose-decorators,babel-plugin-transform-typescript-metadataand configure the babel settings correctly.
To enable experimental support for decorators, you must enable the experimentalDecorators and emitDecoratorMetadata compiler option either on the command line or in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}@injectable()
Use @injectable(), it will turn the current module into an injectable module.
@injectable({
name: 'Foo',
})
class Foo extends RcModule {}
@injectable({
name: 'Bar',
})
class Bar extends RcModule {
constructor(protected foo: Foo) {
super();
}
}If it does not depend on any other module, in fact, the
@injectable()modifier of the current module can be omitted, and turns it into an injectable module and injects it automatically when thecreateApp()runs.
@inject()
Use @inject() and bring its corresponding identifier parameter as dependency injection module.
If you only inject the class itself as a dependency identifier, then you can use
protected bar: Bar.
interface Bar {
text: string;
}
@injectable({
name: 'Foo',
})
class Foo extends RcModule {
constructor(@inject('Bar') protected bar: Bar) {
super();
}
get text() {
return this.bar.text;
}
}@optional()
Use optional() with a dependency identifier that you can use to inject an optional module.
If you only need the class itself as a dependency identifier, then you can abbreviate
@optional(Bar) protected bar?: Barto@optional() protected bar?: Bar.
interface Bar {
text: string;
}
@injectable({
name: 'Foo',
})
class Foo extends RcModule {
constructor(@optional('Bar') public bar?: Bar) {
super();
}
get text() {
return this.bar?.text;
}
}Optional modules are not automatically injected by default. If you need it, please specify the injected module on
createApp().
ModuleRef
If the app have circular dependencies, you can define a getter in this module and use ModuleRef to dynamically obtain an instance of the dependency.
@injectable({
name: 'Bar',
})
class Bar extends RcModule {
constructor(public foo: Foo) {
super();
}
}
@injectable({
name: 'Foo',
})
class Foo extends RcModule {
constructor(public moduleRef: ModuleRef) {
super();
}
get bar() {
return this.moduleRef.get(Bar);
}
}RcModule APIs
@ringcentral-integration/next-core provides RcModule base module, decorators @state, @action, @computed.
- onInit()
onInit lifecycle for current initialization after all deps modules are all ready.
- onInitOnce()
onInitOnce once lifecycle for current initialization after all deps modules are all ready.
- onInitSuccess()
onInitSuccess lifecycle for current initialization after this module is ready.
- onReset()
onReset lifecycle for current reset after one of deps modules is not ready.
The first full lifecycle is onInitOnce() -> onInit() -> "this.ready === true" -> onInitSuccess().
And The re-login lifecycle is logout -> onReset() -> login -> onInit() -> "this.ready === true" -> onInitSuccess().
If the current module has persistent state, then the full lifecycle can only start when the hydration is completed.
For example:
import {
RcModule,
state,
action,
computed,
} from '@ringcentral-integration/next-core';
class Auth extends RcModule {
constructor(protected _client: Client) {
super();
}
@state
connected = '';
@action
changeConnection(connected) {
this.connected = connected;
}
async connect() {
await this._client.connect();
this.changeConnection(true);
}
@computed((that: Auth) => [that.connected])
get permissions() {
return { writeable: this.connected, readable: true };
}
override async onInitSuccess() {
//
}
}@state
@state is used to decorate a module state, which is based on the Redux reducer.
- that will be an state in redux, and never be clear after some view be destroyed.
- if you want that state only inside some components, should use
useStatefrom React.
@action
@action is used to decorate a method that changes the state of the module (Executing it will dispatch a Redux action), and it does NOT support asynchronous methods.
The method decorated with
@actionin the current module CAN call the method decorated with@actionin other modules.
- The
@actiondecorated method should have no side effects.
@injectable({
name: 'ContactsList',
})
class ContactsList extends RcModule {
constructor() {
super();
}
@state
contacts: Contact[] = [];
@action
addContacts(contacts: Contact[]) {
// ❌ bad practice
contacts.forEach(() => {
// fetch avatar
});
// ....
}
override onInitOnce() {
// ✅ good practice
watch(
this,
() => this.contacts,
() => {
// fetch avatar
},
);
}
}- The state operations in the methods decorated by
@actionshould be *mutation updates as possible to ensure patch minimization.
@injectable({
name: 'ContactsList',
})
class ContactsList extends RcModule {
constructor() {
super();
}
@state
contacts: Contact[] = [];
@action
addContact(contact: Contact) {
// ❌ bad practice
this.contacts = [...this.contacts, contact];
}
@action
addContact(contact: Contact) {
// ✅ good practice
this.contacts.push(contact);
}
}
@actionsupports calling other actions that execute other update states and merge updates into the UI (They must be the current module state, cross-module states will still be updated one by one).
@computed()
@computed supports automatic collection of dependencies and automatic re-computing when the state of a dependency changes.
Note: Automatic collection of dependent target properties is only supported for properties decorated by @state and getter fields decorated by @computed.
If you want to manage dependencies manually then, you can use @computed(callback) for a getter, you should make sure that the return value of its callback function is an Array of dependency collections.
@injectable({
name: 'Auth',
})
class Auth extends RcModule {
constructor() {
super();
}
@state
connected = '';
@state
readable = false;
@computed
// or you can also manage dependencies manually like this:
// @computed((that: Auth) => [that.connected, that.readable])
get permissions() {
return {
writeable: getWriteable(this.connected),
readable: this.readable,
};
}
}Note: The
@computeddecorator only supports getter fields, and the getter fields must bereadonly. During the execution of a getter with@computed, no@actionmethods should be invoked.
delegate
The delegate() supports both function calls and being used as a decorator.
To maintain flexibility in the execution of delegates, we recommend giving priority to using function calls.
- Using function:
- You can simply delegate usage like this:
await delegate(this.counter, 'increment');, with the default delegation target being 'server'. - If you want to include the arguments of the function being delegated, then you can use it like this:
await delegate(this.counter, 'decrement', [1]);.
By defining the 'target' options of delegate(), we can specify our execution target. The following targets are supported:
await delegate(this.counter, 'increment', [], { target: 'server' });
It is equivalent to await delegate(this.counter, 'increment').
Delegate execution, run that method in server.
Unless the target is 'server', there are no more options available.
delegate(this.counter, 'increment', [], { target: 'all' });
Parallel execution, run that method in all clients and server.
delegate(this.counter, 'increment', [], { target: 'clients' });
Parallel execution, run that method in all clients.
await delegate(this.counter, 'increment', [], { target: 'mainClient' });
Delegate execution in main client.
- Using decorator:
@delegate('server')
Delegate execution, run that method in server.
@delegate('all')
Parallel execution, run that method in all clients and server.
@delegate('clients')
Parallel execution, run that method in all clients.
@delegate('mainClient')
Delegate execution in main client.
logger
The logger is a built-in logger that supports info, warn, error, debug, and trace methods.
@injectable({
name: 'Counter',
})
class Counter extends RcModule {
constructor() {
super();
}
@state
count = 0;
@action
increment() {
this.count += 1;
this.logger.info('increment', this.count);
}
}RcViewModule APIs
We can be used to implement a module with a View by inheriting the RcViewModule and defining the component method (a React function component).
It is possible to inject any method of the current RcViewModule in its component, and also to inject the current shared module state or other dependent module state using useConnector() React hook:
interface Todo {
text: string;
completed: boolean;
}
@injectable({
name: 'TodoView',
})
class TodoView extends RcViewModule {
@state
list: Todo[] = [];
addTodo(text: string) {
this.list.push({
text,
completed: false,
});
}
@action
toggleTodo(key: number, value: boolean) {
this.list[key].completed = !value;
}
component() {
const list = useConnector(() => this.list);
return (
<ul>
{this.list.map(({ text, completed }, key) => (
<li
key={key}
onClick={() => this.toggleTodo(key, completed)}
>
{text}
</li>
))}
</ul>
);
}
}useConnector also supports returning a state object, which automatically makes shallow comparisons:
const { list, visibilityFilter } = useConnector(() => ({
list: this.list,
visibilityFilter: this.visibilityFilter,
}));It should be noted that while the ViewModule supports inheritance, function components based on component method implementations must be called in the same way as components based on superclass component methods, not using the jsx:
@injectable({
name: 'BaseFooView',
})
class BaseFooView extends RcViewModule {
component() {
return <span>foo</span>;
}
}
@injectable({
name: 'FooView',
})
class FooView extends BaseFooView {
component(props) {
return (
<>
<span>foo</span>
{
super.component(props)
// Don't make it: <super.component />
}
</>
);
}
}State Subscription APIs
watch()
You can use watch() to observe a specific state changes in any class module.
For example,
@injectable({
name: 'Counter',
})
class Counter extends RcModule {
constructor() {
super();
watch(
this,
() => this.count.sum,
(newValue, oldValue) => {
//
},
);
}
@state
count = { sum: 0 };
}You can pass the option { multiple: true }, which will support watching multiple values.
For example,
@injectable({
name: 'Counter',
})
class Counter extends RcModule {
constructor() {
super();
watch(
this,
() => [this.count0, this.count1] as const,
([newCount0, newCount1], [oldCount0, oldCount0]) => {
//
},
{
multiple: true,
},
);
}
@state
count0 = 0;
@state
count1 = 0;
}watch option supports passing in
{ isEqual: () => {} }function for custom equal.
subscribe()
You can use subscribe() to subscribe state changes in any class module.
For example,
@injectable({
name: 'Counter',
})
class Counter extends RcModule {
constructor() {
super();
subscribe(this, () => {
//
});
}
@state
count = { sum: 0 };
}Storage APIs
@storage
You can use @storage to decorate a persistent state, and execute enable(instance) to enable the ability of persistence. When the account logouts, this persistence state will be cleared.
@injectable({
name: 'Counter',
})
class Counter extends RcViewModule {
constructor(public storage: StoragePlugin) {
super();
this.storage.enable(this);
}
@storage
@state
sum = 0;
@action
increment() {
this.sum += 1;
}
}@userStorage
If you want the persistent state not to be cleared when the current account logouts, you can use @userStorage to decorate a persistent state.
@injectable({
name: 'Counter',
})
class Counter extends RcViewModule {
constructor(public storage: StoragePlugin) {
super();
this.storage.enable(this);
}
@userStorage
@state
sum = 0;
@action
increment() {
this.sum += 1;
}
}You can use
storage.purge()for clear all storage.
Router APIs
The foundation package provides a router based on React Router v5.3.1, and you can use the Router module to define the routing configuration.
const app = await createSharedApp({
modules: [
RouterPlugin,
{
provide: RouterOptions,
useValue: {
createHistory: () => createBrowserHistory(),
} satisfies IRouterOptions,
},
],
main: AppView,
render,
});History is a dependency of the router, and you can use createBrowserHistory, createHashHistory or createMemoryHistory to create a history object. For detail information, please refer to the React Router and history documentation.
createSharedApp()
The createSharedApp() method is used to create a shared application, and you can use it to create a shared application that supports multiple windows or Base mode app.
const app = await createSharedApp({
modules: [
RouterPlugin,
{
provide: RouterOptions,
useValue: {
createHistory: () => createBrowserHistory(),
} satisfies IRouterOptions,
},
],
main: AppView,
render,
share: {
name,
type: 'Base', // 'Base' | 'SharedTab' | 'SharedWorker'
},
});If you want to create a shared application that supports multiple windows, you can use the SharedWorker type, When the browser does not support SharedWorker, then the program will automatically downgrade to setting SharedTab.
i18n
The foundation package provides an i18n hooks, and you can use the useLocale() React hook to get the t function.
import { useLocale } from '@ringcentral-integration/micro-core/src/app/hooks';
import i18n from './i18n';
const { t } = useLocale(i18n);
const text = t('loginSuccess');PortManager
If you want to do the multiprocessing programming, you can use the PortManager module to manage the ports. You can read the PortManager public API for more information.
You can use it to manage the logic for different types of ports:
- On the server side, you can use
onServerto manage server-side port logic. - On the client side, you can use
onClientto manage client-side port logic. - On the main tab side, you can use
onMainTabto manage main tab port logic.
For example:
@injectable({
name: 'App',
})
class App extends RcModule {
constructor() {
super();
if (this._portManager.shared) {
this._portManager.onServer(() => this.initializeOnServer());
this._portManager.onClient(() => this.initializeOnClient());
} else {
this.initializeOnServer();
this.initializeOnClient();
}
}
}Debugging
You can use devOptions for debugging, and you can use devOptions to enable the Redux DevTools and Module Lifecycle Check.
const app = await createSharedApp({
modules,
main: AppView,
render,
share,
devOptions, // enable devOptions
});This is the DevOptions interface:
interface DevOptions {
/**
* Enable strict mode for React and Mutative.
*/
strict?: boolean;
/**
* Enable state update patches.
*/
enablePatches?: boolean;
/**
* Enable inspector for state changing.
*/
enableInspector?: boolean;
/**
* Enable state auto freeze.
*/
autoFreeze?: boolean;
/**
* Enable auto computed.
*/
autoComputed?: boolean;
/**
* Enable redux dev tools.
*/
reduxDevTools?: boolean;
/**
* Redux dev tools enhancer options.
*/
reduxDevToolsOptions?: ReduxDevToolsOptions;
}Module Lifecycle Check
You can use checkModules() to check the lifecycle of all the module, And you can use checkModule(module) to check the lifecycle of a specific module.
Module Performance Track
If you want to track the performance of the module initialization, you can use trackModule() to track the performance of all the module.
Plugin Module
Pluggable is an important concept of the f. It provides an interface that allows you to encapsulate many of React's libraries in a clean design. You can base complex plugins on it to encapsulate them simply enough.
@injectable({
name: 'ExamplePlugin',
})
class ExamplePlugin extends PluginModule {
constructor() {
super();
}
}For detail information, please refer to the Pluggable API docs
Dynamic Module
You can use @dynamic() to define a dynamic module, and then use load() to load the lazy module.
@injectable({
name: 'Klazz',
})
class Klazz extends RcModule {
@dynamic('LazyLoad')
lazyLoad?: LazyLoad;
constructor() {
super();
}
async lazyLoadExample() {
const LazyLoad = await import('./LazyLoad').then((m) => m.LazyLoad);
await load(this, [
{
provide: 'LazyLoad',
useClass: LazyLoad,
},
]);
console.log(this.lazyLoad);
}
}Abstract class
You can use abstract to define an abstract class without @injectable(), and then use @injectable() to decorate the subclass.
abstract class Klazz extends RcModule {
constructor() {
super();
}
}And you can use @injectable() to decorate the subclass.
@injectable({
name: 'Foo',
})
class Foo extends Klazz {
constructor() {
super();
}
}Subclass
You can use extends to define a subclass, and then use @injectable() to decorate the subclass.
@injectable({
name: 'Klazz',
})
class Klazz extends RcModule {
constructor(public foo: Foo) {
super();
}
}
@injectable({
name: 'Foo',
})
class Foo extends Klazz {
constructor(public foo: Foo) {
// !!! we need to call super(foo) to pass the foo to the parent class
super(foo);
}
}