Creating Custom Plugins with Fusion.js, Uber’s Open Source Web Framework
February 22, 2019 / GlobalPlugins are a core element of Fusion.js, Uber’s open source universal web framework, and its architecture. Plugins can be used to encapsulate functions, which are reused in different parts of the application. Examples of functions encapsulated in plugins are logging, translations, implementation of libraries and integration with data sources.
A number of official plugins are provided by Fusion.js, and they allow developers to build complex applications without having to write their own plugins. However, the framework also enables developers to build their own plugins. In this article, we offer an an example of how to create a custom plugin with Fusion.js.
Plugin concepts
Fusion.js applications are universal, which means they have a single entry point. Fusion.js plugins share this architecture, making it possible to install a library into an application with a single line of code. This is achieved through dependency injection.
Dependency injection
Dependency injection is not a built-in concept for many JavaScript-based back-end frameworks, and if it is, the implementation is not always ideal. Because of that, developers are often hesitant to use dependency injection, even more so if a third party library has to be used to manage the dependencies.
Unlike traditional JavaScript-based plugins, dependency injection is a vital part of Fusion.js plugins, and plugins can expose a well-defined API as a service to other plugins. This means plugins can be created, and easily injected where needed, across the application.
Some of the benefits of Fusion.js’ dependency injection functionality include the ability to:
- Create a plugin and inject it where needed
- Easily share code across the application
- Decouple the application
- Increase test coverage with testable plugins
Create and inject where needed
A common way to manage dependencies, even in larger applications, is to add them at the top of the application and include everything in the build. Fusion.js has a different approach: dependencies are injected where needed, which means only what is needed is included in the browser bundles.
Dependency injection is not only a core feature in Fusion.js, it strengthens the plugin concept by allowing them to expose well-defined APIs as services, which can be injected into other plugins. This functionality makes decoupling components or features, and even sharing code, easier.
Even encapsulating simple features as plugins can be useful. Take logging, for example. Logging is typically useful in all parts of an application, and with a logging service plugin, the application can easily implement logging features where needed. Using a plugin for logging also makes code easier to maintain, since all logging code is maintained in a single location. It’s not necessary to make changes everywhere logging is implemented.
Testing made easy
Fusion.js architects have been focused on optimizing testability since the early design phases. Coming from an architecture like Uber’s, where testing was a challenge due to high coupling of subsystems and order-of-operation requirements, improving testability was a requirement.
Fusion.js supports modern testing tools like Jest, Puppeteer, and Enzyme. In addition to supporting third-party testing tools, Fusion.js provides fusion-test-utils, a tool for testing plugins. This tool can mock the server itself, making it possible to quickly run integration tests.
Services exposed through Fusion.js plugins with dependencies can be unit tested by calling the service, and passing in the required dependencies as arguments. (Check out an example of how to unit test a plugin.)
Sample application
To illustrate how plugins can be created and used, we built a small sample application. In our use case, this sample application converts temperatures from Fahrenheit to Celsius, and vice versa.
The application’s functionality is handled by two plugins. One plugin exposes the temperature conversion services, taking the temperature as input and returning the converted temperature. The plugin’s middleware will render the result of the temperature conversion in both directions in the browser. The second plugin will inject the conversion services from the first plugin to do a one-way conversion, and thereby demonstrate how dependency injection works in Fusion.js.
The steps to our temperature conversion application are:
- Create a Fusion.js application
- Create the converter plugin
- Create a component for rendering results
- Register the plugin
- Re-use the plugin by injecting it in a new plugin
Create Fusion.js application
Creating the Fusion.js application is very easy when using yarn create. If you already have done this before, you can skip to the next step. If you are new to Fusion.js, this is where it all starts.
In a terminal, run the following command:
yarn create fusion-app <my-app-name> |
This command will create a boilerplate app, with all necessary files and directories to run an included demo page. After running yarn create, run this command:
cd <my-app-name> && yarn dev |
This serves up the Fusion.js demo page on http://localhost:3000.
The file structure inside the “src”-directory looks like this:
. ├── pages │ ├── home.js │ └── pageNotFound.js ├── main.js └── root.js |
The default demo page is not needed for this example, so the home.js file can be deleted, and the route for the home page can be removed from the root.js file as well. They can also be kept in the project.
Create converter plugin
The converter services are created in the converter plugin, and the result of the temperature conversion is rendered in the browser.
The converter plugin is created with the createPlugin() function:
// src/plugins/converters.js import {createPlugin} from ‘fusion-core’; export default createPlugin({ deps: {}, provides() {}, middleware() {} }); |
The plugin will not depend on other plugins, so deps can be removed. The conversion services will be created in provides, and the component will be rendered in middleware.
Conversion services
The plugin contains two services, one for converting temperatures from Celsius to Fahrenheit, and one for converting Fahrenheit to Celsius. Both services take one parameter, the temperature, and both return the converted temperature.
The temperature conversion functions are inserted into the plugin’s provides() function:
provides() { return { convertToCelsius(temp) { const celsius = (temp – 32) * 5/9; return celsius; }, convertToFahrenheit(temp) { const fahrenheit = (temp * (5/9)) + 32; return fahrenheit; } }; } |
Render component
Finally, we provide the converter API to our component tree by wrapping the ctx.element in a context provider.
The ctx.element holds the application root element and is set based on the App constructor. Plugins can add context providers by wrapping a ctx.element in a middleware. However, we must first check for the existence of the element, since it only exists on requests that map to page renders. For example, it does not exist on requests for static assets and POST requests.
middleware({}, self) { return (ctx, next) => { if (ctx.element) { ctx.element = ( <ConverterProvider value={self}> {ctx.element} </ConverterProvider> ); } return next(); }; } |
The first argument of middleware passes injected dependencies through, but since that is not needed in this application, the dependency object is empty. The second argument is the service provided by the plugin. In this case, it is the converter API.
This is the complete converter plugin:
// src/plugins/converters.js import React from ‘react‘; import {createPlugin, html} from ‘fusion-core‘; const {Provider, Consumer} = React.createContext(false); export {Consumer as ConverterConsumer}; export default createPlugin({ provides() { return { convertToCelsius(temp) { const celsius = (temp – 32) * 5/9; return celsius; }, convertToFahrenheit(temp) { const fahrenheit = (temp * (9/5)) + 32; return fahrenheit; } }; }, middleware (deps, converter) { return (ctx, next) => { if (ctx.element) { ctx.element = ( <ConverterProvider value={converter}> {ctx.element} </ConverterProvider> ); } return next(); } } }); |
Create component
The component contains all the logic for creating the content to render in the browser. For this simple application, the goal is to output the result of a temperature conversion. The result in the browser should look like this:
25° Fahrenheit converted to Celsius is -4°
25° Celsius converted to Fahrenheit is 77°
The temperature conversion is handled by the services in the converter plugin, and the component allows the services to be passed through. In this application, the temperature is just provided as a constant. In a real application, the temperature would of course be provided either by the user, a back-end, or a remote service.
This component uses React Context, which is a common way to render results in Fusion.js. In addition to the logic for creating the content to render, React Context is created with the component code.
// src/components/converters.js import React from ‘react’; import {Consumer} from ‘../plugins/converter’; export function ConverterComponent() { const temperature = 25; return ( <Consumer> {(value) => ( <div> <p> {temperature}° Fahrenheit converted to Celsius is {Math.round(value.convertToCelsius(temperature))}° </p> <p> {temperature}° Celsius converted to Fahrenheit is {Math.round(value.convertToFahrenheit(temperature))}° </p> </div> )} </Consumer> ); } |
The converted temperature may return a decimal number and can be rounded to an integer with Math.round(), like in the code above. The component ConverterComponent is exported, and the provider is exported as ConverterProvider.
Register the plugin
The last step is to register the plugin. This is done in the main.js file:
// src/main.js import React from ‘react’; import App from ‘fusion-react’; import ConverterPlugin from ‘./plugins/converter.js’; import Root from ‘./ConverterComponent’; export default () => { const app = new App(<Root />); app.register(ConverterPlugin); return app; }; |
Results
Now the application is complete, and by running the application with yarn dev, the application will be served on the default port 3000 on localhost:
Re-use conversion services
The conversion services in the middleware plugin are exposed to other plugins, and can easily be injected as a dependency in other plugins. So far, the conversion services have not been used in other plugins for the application, but let’s create a small plugin to illustrate how the dependency injection works.
The plugin injects the conversion services and adds two endpoints:
- /convert/celsius
- /convert/fahrenheit
Where the converter plugin will render the result for both conversion directions, these two endpoints will only render the result of one direction: either Celsius to Fahrenheit or Fahrenheit to Celsius. The temperature is provided by using a query parameter called degrees.
Create token
In order to inject the converter plugin as a dependency in the new plugin, a token must be created for and exported in the converter plugin script:
// src/plugins/converters.js import {createPlugin, createToken} from ‘fusion-core’; … export const ConverterToken = createToken(‘ConverterToken’); |
The token has to be registered in the main file, but this will be done in the last step. So for now, assume the token is available.
Create the endpoints plugin
The converter token is injected in the new plugin as a dependency, and now the conversion services can be used in the new plugin. (For more information about how endpoints work, check out our documentation on the subject).
// src/plugins/endpoints.js import React from ‘react’; import {createPlugin} from ‘fusion-core’; import {ConverterToken} from ‘./converters’; export default createPlugin({ deps: {converter: ConverterToken}, middleware({converter}) { return async (ctx, next) => { await next(); if (ctx.path.startsWith(‘/convert/celsius’)) { const tempF = Math.round(converter.convertToFahrenheit(ctx.query.degrees)); ctx.body = ctx.query.degrees + ‘° Celsius converted to Fahrenheit is ‘ + tempF + ‘°’; } else if (ctx.path.startsWith(‘/convert/fahrenheit’)) { const tempC = Math.round(converter.convertToCelsius(ctx.query.degrees)); ctx.body = ctx.query.degrees + ‘° Fahrenheit converted to Celsius is ‘ + tempC + ‘°’; } } } }); |
Register endpoints and converter token
As a final step, the endpoints plugin is registered, and the converter token is also registered:
// src/main.js import React from ‘react’; import App from ‘fusion-react’; import Converters, {ConverterToken} from ‘./plugins/converters.js’; import Endpoints from ‘./plugins/endpoints.js’; const root = <div/>; export default () => { const app = new App(root); app.register(ConverterToken, Converters); app.register(Endpoints); return app; }; |
Results
Now it’s possible to just convert the temperature in one direction, and render the result. For the Celsius to Fahrenheit conversion, call the endpoint with the temperature as a query parameter, /convert/celsius°rees=30:
Summary
Plugins are a great way to encapsulate services and middleware in Fusion.js, which the simple application in this article illustrates. Plugins can be injected into other plugins, and exposed plugin services can be consumed across the application.
This article only covers basic usage of plugins by showing how to create a service plugin, inject a plugin as a dependency in another plugin, create endpoints in plugins, and render results from plugins. For more information about Fusion.js plugins, please visit the official website for more code samples in the Getting Started and Fusion API documentation sections.
Curious about this open source framework? Try out Fusion.js for yourself!
If building large-scale web applications interests you, consider applying for a role on our team!
Carsten Jacobsen
Carsten Jacobsen is an open source developer advocate at Uber.
Posted by Carsten Jacobsen
Related articles
Most popular
Shifting E2E Testing Left at Uber
Continuous deployment for large monorepos
Pinot for Low-Latency Offline Table Analytics
Lucene: Uber’s Search Platform Version Upgrade
Products
Company