Writing a Feature App
A Feature App is described by a consumer definition object. It consists of
optional dependencies
and optionalDependencies
objects, and a create
method:
const myFeatureAppDefinition = {
dependencies: {
featureServices: {
'acme:some-feature-service': '^2.0.0',
},
externals: {
react: '^16.7.0',
},
},
optionalDependencies: {
featureServices: {
'acme:optional-feature-service': '^1.3.0',
},
},
create(env) {
// ...
},
};
If a Feature App module is to be loaded asynchronously with the
FeatureAppManager
, it must provide a definition object as its default export:
export default myFeatureAppDefinition;
dependencies
The dependencies
map can contain two types of required dependencies:
With
dependencies.featureServices
all required Feature Services are declared. If one of those dependencies can't be fulfilled, the Feature App won't be created. This means the Feature App can be sure that those dependencies are always present when it is created.Feature Service dependencies are declared with their ID as key, and a semver version range as value, e.g.
{'acme:some-feature-service': '^2.0.0'}
. Since Feature Services only provide the latest minor version for each major version, a caret range should be used here. If instead an exact version or a tilde range is used, this will be coerced to a caret range by theFeature ServiceRegistry
.With
dependencies.externals
all required external dependencies are declared. This may include shared npm dependencies that are provided by the integrator.External dependencies are declared with their external name as key, and a semver version range as value, e.g.
{react: '^16.7.0'}
.
optionalDependencies
The optionalDependencies.featureServices
map contains all Feature Service
dependencies for which the Feature App handles their absence gracefully. If one
of those dependencies can't be fulfilled, the FeatureServiceRegistry
will only
log an info message.
Feature Service dependencies are declared with their ID as key, and a semver
version range as value, e.g. {'acme:some-feature-service': '^2.0.0'}
.
Since Feature Services only provide the latest minor version for each major
version, a caret range should
be used here. If instead an exact version or a tilde range
is used, this will be coerced to a caret range by the Feature ServiceRegistry
.
Note:
Optional external dependencies (i.e.optionalDependencies.externals
) are not yet supported (see #245).
create
The create
method takes the single argument env
, which has the following
properties:
config
— A config object that is provided by the integrator1:const myFeatureAppDefinition = { create(env) { const {foo} = env.config; // ... }, };
featureServices
— An object of required Feature Services that are semver-compatible with the declared dependencies in the Feature App definition:const myFeatureAppDefinition = { dependencies: { featureServices: { 'acme:some-feature-service': '^2.0.0', }, }, create(env) { const someFeatureService = env.featureServices['acme:some-feature-service']; someFeatureService.foo(42); // ... }, };
featureAppId
— The ID that the integrator1 has assigned to the Feature App instance. This ID is used as a consumer ID for binding the required Feature Services to the Feature App.featureAppName
— The name that the integrator1 might have assigned to the Feature App. This name is used as a consumer name for binding the required Feature Services to the Feature App. In contrast to thefeatureAppId
, the name must not be unique. It can be used by required Feature Services for display purposes, logging, looking up Feature App configuration meta data, etc.baseUrl
— A base URL to be used for referencing the Feature App's own resources. It is only set in theenv
if the integrator1 has defined abaseUrl
on the correspondingFeatureAppLoader
orFeatureAppContainer
.done
— A callback that the integrator1 might have defined. A short-lived Feature App can call this function when it has completed its task, thus giving the integrator1 a hint, that it can be removed. For example, if the Feature App was opened in a layer, the integrator1 could close the layer whendone()
is called.
The return value of the create
method can vary depending on the integration
solution used. Assuming the @feature-hub/react
package is used, a
Feature App can be either a React Feature App or a
DOM Feature App.
ownFeatureServiceDefinitions
A Feature App can also register its own Feature Services by declaring
ownFeatureServiceDefinitions
:
import {myFeatureServiceDefinition} from './my-feature-service';
const myFeatureAppDefinition = {
dependencies: {
featureServices: {
'acme:my-feature-service': '^1.0.0',
},
},
ownFeatureServiceDefinitions: [myFeatureServiceDefinition],
create(env) {
const myFeatureService = env.featureServices['acme:my-feature-service'];
// ...
},
};
This technique allows teams to quickly get Feature Apps off the ground, without being dependent on the integrator. However, as soon as other teams need to use this Feature Service, it should be published and included in the global set of Feature Services by the integrator.
Note:
If the Feature Service to be registered has already been registered, the new Feature Service is ignored and a warning is emitted.
Implementing a Feature App for an Integrator That Uses React
The @feature-hub/react
package defines two interfaces,
ReactFeatureApp
and DomFeatureApp
. A Feature App that implements one of
these interfaces can be placed on a web page using the FeatureAppLoader
or
FeatureAppContainer
components.
React Feature App
A React Feature App definition's create
method returns a Feature App instance
with a render
method that itself returns a ReactNode
:
const myFeatureAppDefinition = {
create(env) {
return {
render() {
return <div>Foo</div>;
},
};
},
};
Note:
Since this element is directly rendered by React, the standard React lifecyle methods can be used (ifrender
returns an instance of a ReactComponentClass
).
DOM Feature App
A DOM Feature App allows the use of other frontend technologies such as Vue.js
or Angular, although it is placed on a web page using React. Its definition's
create
method returns a Feature App instance with an attachTo
method that
accepts a DOM container element:
const myFeatureAppDefinition = {
create(env) {
return {
attachTo(container) {
container.innerText = 'Foo';
},
};
},
};
Loading UIs provided by the React Integrator
Both kinds of Feature Apps can specify a loading stage for Feature Apps, which are used to allow an integrator1 to hide an already rendered Feature App visually and display a custom loading UI instead. This feature is for client-side rendering only.
A Feature App can declare this loading stage by passing a Promise
in the
object returned from their create
function with the key loadingPromise
. Once
the promise resolves, the loading is considered done. If it rejects, the Feature
App will be considered as crashed, and the integrator1 can use the rejection
payload to display a custom Error UI.
const myFeatureAppDefinition = {
create(env) {
const dataPromise = fetchDataFromAPI();
return {
loadingPromise: dataPromise,
render() {
return <App dataPromise={dataPromise}>;
}
};
}
};
Note:
If you want the rendered App to control when it is done loading, you can pass the promiseresolve
andreject
functions into the App using your render method. An example for this is implemented in thereact-loading-ui
demo.
Note:
If a similar loading stage (after rendering started) is needed for server-side rendering, for example to wait for a data layer like a router to resolve all dependencies, it can be implemented using the@feature-hub/async-ssr-manager
'sscheduleRerender
API.
Implementing a Feature App for an Integrator That Uses Web Components
If the targeted integrator is using the @feature-hub/dom
package, a
Feature App needs to implement the DomFeatureApp
interface that the package
defines. Since this interface is compatible with the DomFeatureApp
interface
defined by @feature-hub/react
, this Feature App will also be
compatible with an integrator that uses React.
A DOM Feature App allows the use of arbitrary frontend technologies such as
Vue.js, Angular, or React, and is placed on the web page using Web
Components. The
Feature App will automatically be enclosed in its own shadow DOM. Its
definition's create
method returns a Feature App instance with an attachTo
method that accepts a DOM container element:
const myFeatureAppDefinition = {
create(env) {
return {
attachTo(container) {
container.innerText = 'Foo';
},
};
},
};
Bundling a Feature App
For the FeatureAppManager
to be able to load Feature Apps from a remote
location, Feature App modules must be bundled. The module type of a Feature App
bundle must be chosen based on the provided module loaders of the
integrators it intends to be loaded into.
Client Bundles
Out of the box, the Feature Hub provides two client-side module loaders.
AMD Module Loader
To build an AMD Feature App module bundle, any module bundler can be used that can produce an AMD or UMD bundle.
How npm dependencies can be shared using AMD is described in the "Sharing npm Dependencies" guide.
Webpack Module Federation Loader
To build a federated module, Webpack must be used as module bundler.
Here is an example of a Webpack config for a federated Feature App module:
module.exports = {
entry: {}, // intentionally left empty
output: {
filename: 'some-federated-feature-app.js',
publicPath: 'auto',
},
plugins: [
new webpack.container.ModuleFederationPlugin({
name: '__feature_hub_feature_app_module_container__',
exposes: {
featureAppModule: path.join(__dirname, './some-feature-app'),
},
}),
],
};
How npm dependencies can be shared using Webpack Module Federation is described in the "Sharing npm Dependencies" guide.
There are two important naming conventions a Feature App's Webpack config must follow:
- The
name
of the remote Feature App module container must be'__feature_hub_feature_app_module_container__'
.- The Feature App module (containing the Feature App definition as default export) must be exposed by the container as
featureAppModule
.
Server Bundles
To build a CommonJS Feature App module bundle for server-side rendering, any module bundler can be used that can produce a CommonJS or UMD bundle. The target of this bundle must be Node.js.
How npm dependencies can be shared on the server is described in the "Sharing npm Dependencies" guide.
- The "integrator" in this case can also be another Feature App.