UNPKG

@micro-frame/marko

Version:

A Marko tag for building SSR friendly micro frontends.

278 lines (204 loc) 9.7 kB
<h1 align="center"> <!-- Logo --> <br/> @micro-frame/marko <br/> <!-- Format --> <a href="https://github.com/prettier/prettier"> <img src="https://img.shields.io/badge/styled_with-prettier-ff69b4.svg" alt="Styled with prettier"/> </a> <!-- Coverage --> <a href="https://codecov.io/gh/marko-js/micro-frame"> <img src="https://codecov.io/gh/marko-js/micro-frame/branch/main/graph/badge.svg?token=cSvMDikbE4"/> </a> <!-- NPM Version --> <a href="https://npmjs.org/package/@micro-frame/marko"> <img src="https://img.shields.io/npm/v/@micro-frame/marko.svg" alt="NPM Version"/> </a> <!-- Downloads --> <a href="https://npmjs.org/package/@micro-frame/marko"> <img src="https://img.shields.io/npm/dm/@micro-frame/marko.svg" alt="Downloads"/> </a> </h1> <p align="center"> A Marko tag for building SSR friendly micro frontends. </p> # Installation ```console 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](https://github.com/zkat/make-fetch-happen) is used to perform the requests from the server. These means you can also leverage [HTTP Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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](https://webpack.js.org/concepts/module-federation/), native ES modules & globally available modules should work fine with `micro-frame`. # Example ```marko <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`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin) of the of the host application. ```marko <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. ```marko <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](https://developer.mozilla.org/en-US/docs/Web/API/Request/cache) (works on both server and client renders). ```marko <!-- 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. ```marko <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. ```marko <!-- This example will disable the default 30s timeout. --> <micro-frame src="..." timeout=0/> ``` ## `<@catch|err|>` An [attribute tag](https://markojs.com/docs/syntax/#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>`](https://markojs.com/docs/core-tags/#await) tag. ```marko <micro-frame src="..."> <@catch|err|> <!-- Displays if request to service fails or times out --> error: ${err.message} </@catch> </micro-frame> ``` ## `<@loading>` An [attribute tag](https://markojs.com/docs/syntax/#attribute-tag) rendered when while the request is still being streamed. It is removed after the request has either errored, or successfully loaded. ```marko <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](https://markojs.com/docs/syntax/#class-attribute). ```marko <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](https://markojs.com/docs/syntax/#style-attribute). ```marko <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 [`<await>` tag client-reorder attribute](https://markojs.com/docs/core-tags/#await), 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. # 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. ```marko 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. ```marko 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](/.github/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.