Skip to content

marko-js/micro-frame

Repository files navigation


@micro-frame/marko
Styled with prettier NPM Version Downloads

A Marko tag for building SSR friendly micro frontends.

Installation

npm install @micro-frame/marko

How it works

This package exposes a <micro-frame> Marko component that in many ways is similar to a traditional <iframe>. However, unlike an iframe, the content from the src is loaded, with streaming support, directly into the existing document.

On the server

When this component is rendered server side, it will make a request to load the embedded html resource. The response is then streamed along side the content for the host page.

Internally make-fetch-happen is used to perform the requests from the server. These means you can also leverage HTTP Cache-Control.

In the browser

When rendered client side a normal fetch request is made to load the embedded html resource. The content of the response will be rendered within the page as if it was a server side render. This includes full streaming support.

Any time the src attribute is changed, a new request will be made to load updated html content.

Why

This module allows for embedded micro frontends with the following benefits:

  1. Can take full advantage of streaming, if loaded server side or in the browser.
  2. Both the host, and embedded applications simply respond with HTML.
  3. Framework agnostic, the child can respond with HTML generated by any tool/framework.

Specifically in comparison to iframes it offers the following advantages:

  • Usability
    • Control over loading & error state rendering.
    • Does not break navigation / back button.
    • Does not appear differently to screen readers.
    • Does not cause issues using native browser API’s that are sometimes restricted in iframes.
    • Content can rendered with the rest of the page
      • No resizing issues.
      • Flows with page content / layout.
      • Can escape it’s container, eg for modals
  • Performance
    • Shares single connection with host (no round trip once iframe makes it to the browser).
    • Does not impact SEO (sometimes iframes are not indexed by search engines).
    • iframes receive lower priority than other assets on the page, this does not.
    • Avoids additional window / browser context (less memory used).
    • Avoids boilerplate html, just send fragments (no <html>, <head>, etc).
    • Caches in both the client and host server.

Why not

This module works best when you have applications that are independently developed, potentially with different technology stacks, that you want to glue together.

  • Applications broken up this way in general are harder to optimize, deploy, etc.
  • Embedded apps should be served from the same origin/TLD to prevent CORS issues. You should not embed untrusted applications, you should consider the embedded application a part of the host page.
  • There will always be overhead in this approach, or really any naive micro-frontend setup. This module does not dictate how assets are loaded or shared across applications. If necessary that must be orchestrated between the applications separately. Solutions like Module federation, native ES modules & globally available modules should work fine with micro-frame.

Example

<micro-frame src="my-nested-app">
  <@loading>
    We're still loading...
  </@loading>
  <@catch|err|>
    Uh-oh! ${err.message}
  </@catch>
</micro-frame>

API

src

A (required) path to the embedded html application. This is resolved from the origin of the of the host application.

<micro-frame src="my-nested-app"/>

With the above, assuming the host application is rendered at https://ebay.com/n/all-categories, the embedded application will resolve to https://ebay.com/my-nested-app.

headers

Optionally provide additional http headers to send. Only the object form shown below is supported.

<micro-frame src="..." headers={
  "X-My-Header": "Hello",
  "X-Another-Header": "World"
}/>

Note that be default on the server side headers are copied from the current incoming request, the headers option will be merged with existing headers.

cache

Mirrors the Request.cache options (works on both server and client renders).

<!--
This example will always show cached content if available
and fallback to the network otherwise
-->
<micro-frame src="..." cache="force-cache"/>

fetch

Optionally provide function to override default fetch logic.

<micro-frame src="..." name="..." fetch(url, options, fetch) {
  // The 3rd parameter allows us to continue to use micro-frames fetch implementation (which is different server/browser).
  // We can use this override to do things like a POST request, eg:
  return fetch(url, {
    ...options,
    method: "POST",
    headers: {
      ...headers,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ "some": "json" })
  });
}  />

timeout

A timeout in ms (defaults to 30s) that will prematurely abort the request. This will trigger the <@catch> if provided. If set to 0 the request will not time out.

<!--
This example will disable the default 30s timeout.
-->
<micro-frame src="..." timeout=0/>

<@catch|err|>

An attribute tag rendered when there is a network error or timeout. If there is no @catch handler the error will be emitted to the stream, similar to the <await> tag.

<micro-frame src="...">
  <@catch|err|>
    <!-- Displays if request to service fails or times out -->
    error: ${err.message}
  </@catch>
</micro-frame>

<@loading>

An attribute tag rendered when while the request is still being streamed. It is removed after the request has either errored, or successfully loaded.

<micro-frame src="...">
  <@loading>
    We are loading the nested app...
    <my-spinner/>
  </@loading>
</micro-frame>

class

Optional class attribute which works the same way as Marko class attribute.

<micro-frame src="..." class="a c"/>
<micro-frame src="..." class={ a:true, b:false, c:true }/>
<micro-frame src="..." class=["a", null, { c:true }]/>

style

Optional style attribute which works the same way as Marko style attribute.

<micro-frame src="..." style="display:block;margin-right:16px"/>
<micro-frame src="..." style={ display: "block", color: false, marginRight: 16 }/>
<micro-frame src="..." style=["display:block", null, { marginRight: 16 }]/>

client-reorder

Similar to the tag client-reorder attribute this tells the micro-frame to avoid blocking content later in the document.

Note when this is used the micro-frame will be buffered instead of streamed and inserted once it's ready.

<!--
This example will disable the default 30s timeout.
-->
<micro-frame src="..." timeout=0/>

Communicating between host and child

Communicating with the embedded application happens primarily in one of two ways, either you want to do a full reload of and get new HTML, or you want to orchestrate a client side rendered update.

Full reload

To perform a full reload of the embedded application it works best to pass a query string in the src attribute. Whenever src updates, a full reload will happen automatically.

class {
  onCreate() {
    this.state = { page: 0 };
  }

  nextPage() {
    this.state.page++;
  }
}

<micro-frame src=`my-nested-app?page=${state.page}`/>

<button onClick("nextPage")>Next Page</button>

With the above, any time state.page changes the my-nested-app content will be re-loaded.

Client side update

Client side communication between the host and child application can be done through a number of mechanisms. You can use a global store, store data on the dom (perhaps even use web components) or other creative options.

You can do this relatively simply by having a contract between the host and child application. Below is an example using a global exposed by the nested application.

class {
  onCreate() {
    this.state = { page: 0 };
  }

  openModal() {
    if (window.nestedApp) {
      window.nestedApp.openModal();
    }
  }
}

<micro-frame src="my-nested-app"/>

<button onClick("openModal")>Open nested app modal</button>

Code of Conduct

This project adheres to the eBay Code of Conduct. By participating in this project you agree to abide by its terms.