Garfish: A Micro Frontend Framework

How micro frontends work and how Garfish makes it easier to plug multiple apps into one seamless product.
Introduction to Micro Frontends
What is a Micro Frontend
It is an architectural style composed of multiple independently delivered frontend applications, decomposing a frontend application into smaller, simpler applications that can be independently developed, tested, and deployed, while still appearing as a cohesive single product to the user.
Example: In a product workbench, each module in the sidebar corresponds to a separate application, each developed independently.
TODO: Micro Frontend Example Diagram
When to Use Micro Frontends
Legacy project transformation. As the number of participants and teams increases and changes, an ordinary application evolves into a monolithic application, leading to the problem of the application becoming unmaintainable.
As a portal site that needs to integrate many systems, these systems are maintained by different teams, with varying code styles and diverse technology stacks, which can only be aggregated using iframes (but not recommended ๐ โ๏ธ).
Micro Frontend Solution: Garfish
Garfish is a micro frontend framework
It is mainly divided into three layers: deployment side, framework runtime, and debugging tools. Currently, it adopts an SPA architecture.
Why Not Use iframes
Although iframes provide isolation, they have some poor user experiences:
- Viewport size is not synchronized (for example, one iframeโs viewport needs to be centered in the main page).
- Communication between sub-applications is inconvenient.
- Extra performance overhead: loading speed, building the iframe environment.
Garfish Overall Architecture
A micro frontend framework needs to have the following functions:
Sub-application Loader (Loader)
- Support html-entry
- Preloading
Loader Work
The work of the loader is mainly divided into four steps:
- The loader packages the sub-application into a js-bundle.
- The sub-application exports routes.
- Garfish-loader downloads the js-bundle, and obtains the export content of the sub-application using the commonJS specification.
- Registers the routes into the main application.
Implementation example:
// Sub-application
export provider () {
return {
router: [
{
path: '/app2/home',
component: Home
},
{
path: '/app2/detail',
component: Detail
}
]
};
};
// Build result
///static/app2/index.js
// Main application, download app2/index.js compile => provider
let { router } = window.Garfish.loader.loadApp('app2');
routers.push(router);
However, this loading mode also has some disadvantages:
- The main application and sub-applications must use the same framework.
- Sub-applications must depend on the main application to run.
- Route conflicts may occur between sub-applications.
- High business intrusiveness.
- High transformation cost for existing sub-applications.
Solution: html-entry
โ๏ธWe hope that it is best to load the sub-application simply by knowing its HTML address, instead of packaging the sub-application into a single js-bundle and loading the routes of this js sub-application.
The convention of exporting routes has been changed into the convention of exporting render functions and destroy functions.
Html-entry
Route-driven views!
Browser loading page: download HTML content, parse and render HTML, load external script and style, execute scripts and styles, and draw the page.
Since we need to collect as many side effects of sub-applications as possible to avoid impacts between applications, it is necessary to extract style and script tags that may affect the page from HTML, and handle them through the sandbox.
Therefore, the loaderโs workflow becomes:
- Fetch HTML content
- Remove unnecessary nodes such as body, head…
- Extract script and style tags for sandbox handling
- Obtain sub-application provider content
Sandbox Isolation (Sandbox)
- Multiple applications running simultaneously
- No impact on the main application
- Styles do not affect each other
In micro frontends, the sandbox is very important. After splitting a monolithic application into multiple sub-applications, there are many developers involved, and it is difficult to ensure that applications do not affect each other just by code and standards. What side effects need to be effectively isolated to avoid sub-applications affecting each other?
Currently, possible mutual impacts between sub-applications mainly include:
- Global environment
- Event listeners
- Timers
- Network requests
- localStorage
- Styles
- DOM operations
๐ก Each sub-application has its own runtime environment, implementing browser-vm
Sandbox Implementation
Currently there are two isolation schemes: snapshot sandbox and vm sandbox.
- Snapshot Sandbox
Take a snapshot of the current runtime environment at a certain point, and then restore the snapshot when needed to achieve isolation.
sandbox class:
class Sandbox {
private snapshotOriginal
private snapshotMutated
activate: () => void;
deactivate: () => void;
}
- activate: traverse variables on window and store as snapshotOriginal
- deactivate: traverse window variables again, compare with snapshotOriginal, store differences in snapshotMutated, and restore window to snapshotOriginal
- When switching applications again, restore snapshotMutated variables back to window, achieving a sandbox switch (each sandbox corresponds to a different snapshotMutated)
const sandbox = new Sandbox();
sandbox.activate();
execScript(code)๏ผ
sandbox.deactivate();
- VM Sandbox
Create a sandbox => pass in the code to execute
class VMSandbox { // create sandbox
execScript: (code: string) => void;
destory: () => void;
}
const sandbox = new VMSandbox();
sandbox.execScript(code)๏ผ
const sandbox2 = new VMSandbox();
sandbox2.execScript(code2)๏ผ
Route Management (Router)
- Route distributes applications
- Control sub-application routing
The rendering area of sub-applications is usually a fixed node. In addition to providing manual mounting, Garfish also provides the ability to bind routes to sub-applications. Users only need to configure the application routing table, and entering or leaving the corresponding route will automatically trigger the mounting and destroying of sub-applications.
How to support route management and automatically distribute sub-applications?
- Listen for route changes and distribute sub-applications
- The main application can control sub-application routing and view updates
- Main application and sub-application routes stay synchronized
Building a Micro Frontend Application
For the Main Application
First, add the dependency package.
In the entry of the main application, we can register sub-applications as follows:
// index.js (main application entry)
import Garfish from 'garfish';
Garfish.run({
basename: '/',
domGetter: '#subApp',
apps: [
{
name: 'react',
activeWhen: '/react',
entry: 'http://localhost:3000', // html entry
},
{
name: 'vue',
activeWhen: '/vue',
entry: 'http://localhost:8080/index.js', // js entry
},
],
});
When the Garfish instance is imported and the Garfish.run method is executed, Garfish immediately enables route hijacking, listens for browser route changes, and executes matching logic.
When the current path matches the sub-application logic, it will automatically mount the application to the specified dom node, and during this process, it will sequentially trigger the lifecycle hooks of sub-application loading and rendering.
For the Sub-application
Adjust the build configuration of the sub-application (the configuration exported in webpack.config.js or vite.config.js).
Export the provider function.
- You can use the
@garfish/bridge-reactmentioned in the official documentation. - You can customize the export function (below is the official example). You must provide a
render functionand adestroy function, so that the sub-application can be rendered and destroyed when entering or exiting a route.
import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'; export const provider = () => ({ // render function, must be provided render: ({ dom, basename }) => { ReactDOM.render( <React.StrictMode> <App basename={basename} /> </React.StrictMode>, dom.querySelector('#root'), ); }, // destroy function, must be provided destroy: ({ dom, basename }) => { ReactDOM.unmountComponentAtNode( dom ? dom.querySelector('#root') : document.querySelector('#root'), ); }, });- You can use the
Set the basename of the route.
- If the sub-application has its own routes, in the micro frontend scenario, the basename must be used as the base path of the sub-application. Without a base route, the sub-application routes may conflict with the main application or other applications.
Why?
- Currently, the main application is accessed at
garfish.bytedance.com, so the currentbasenameis/. The sub-application vue can be accessed atgarfish.bytedance.com/vue. - If the main application changes
basenameto/site, then the main application access path becomesgarfish.bytedance.com/site, and the sub-application vue access path becomesgarfish.bytedance.com/site/vue. - Therefore, it is recommended that sub-applications directly use the
basenamepassed inprovideras the base route of their own application, ensuring that when the main application changes its route, the relative path of the sub-application still follows the overall change.
- Currently, the main application is accessed at
Simple Summary
Main Application Setup
- Register basic information of sub-applications
- Use Garfish to schedule and manage sub-applications in the main application
Sub-application Modification
Add corresponding build configuration
Export the
providerfunction by wrapping the sub-application with the function provided by the@garfish/bridge-reactpackageAdd basename settings for different framework types:
- React: pass
basenameintoBrowserRouterโsbasenameproperty in the root component - Vue: pass
basenameintoVueRouterโsbasenameproperty
- React: pass