From bf626b45841722b3b18b4caee0636ff8be6aa7bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:03:53 +0000 Subject: [PATCH] deploy: 39565190b95b2c3d2ad8e8440038a2f88f6445d8 --- 404.html | 8 +++--- assets/js/0a789486.7b83bf5c.js | 1 + assets/js/0ae76a47.c19f6ce3.js | 1 + assets/js/0ae76a47.ed3fef86.js | 1 - assets/js/3082ef22.1e205034.js | 1 + assets/js/814f3328.531f8715.js | 1 + assets/js/814f3328.b433f0b5.js | 1 - assets/js/a6e5e601.02784cb2.js | 1 + assets/js/a6e5e601.cc84ae5b.js | 1 - ...675dd.c3937a6c.js => b2b675dd.5939c463.js} | 2 +- assets/js/b2f554cd.0fb48443.js | 1 - assets/js/b2f554cd.3d02c0a6.js | 1 + assets/js/main.4cb5badb.js | 2 ++ ...CENSE.txt => main.4cb5badb.js.LICENSE.txt} | 0 assets/js/main.fe52a37c.js | 2 -- assets/js/runtime~main.714a2b72.js | 1 - assets/js/runtime~main.d434432e.js | 1 + .../a-case-for-soa-in-the-browser/index.html | 10 +++---- .../index.html | 10 +++---- .../single-spa-parcels-explained/index.html | 10 +++---- .../02/20/single-spa-inspector/index.html | 10 +++---- blog/2020/02/24/single-spa-5/index.html | 10 +++---- .../single-spa-core-is-expanding/index.html | 10 +++---- .../index.html | 10 +++---- .../11/07/core-team-meeting-notes/index.html | 10 +++---- blog/2023/11/27/single-spa-6/index.html | 10 +++---- .../12/12/core-team-meeting-notes/index.html | 26 +++++++++++++++++++ blog/archive/index.html | 10 +++---- blog/atom.xml | 10 ++++++- blog/index.html | 10 +++---- blog/rss.xml | 10 ++++++- contributors/index.html | 8 +++--- docs/4.x/api/index.html | 8 +++--- docs/4.x/building-applications/index.html | 8 +++--- docs/4.x/code-of-conduct/index.html | 8 +++--- docs/4.x/configuration/index.html | 8 +++--- docs/4.x/contributing-overview/index.html | 8 +++--- docs/4.x/ecosystem-angular/index.html | 8 +++--- docs/4.x/ecosystem-angularjs/index.html | 8 +++--- docs/4.x/ecosystem-backbone/index.html | 8 +++--- docs/4.x/ecosystem-cycle/index.html | 8 +++--- docs/4.x/ecosystem-ember/index.html | 8 +++--- .../ecosystem-html-web-components/index.html | 8 +++--- docs/4.x/ecosystem-inferno/index.html | 8 +++--- docs/4.x/ecosystem-leaked-globals/index.html | 8 +++--- docs/4.x/ecosystem-preact/index.html | 8 +++--- docs/4.x/ecosystem-react/index.html | 8 +++--- docs/4.x/ecosystem-riot/index.html | 8 +++--- docs/4.x/ecosystem-svelte/index.html | 8 +++--- docs/4.x/ecosystem-vue/index.html | 8 +++--- docs/4.x/ecosystem/index.html | 8 +++--- docs/4.x/examples/index.html | 8 +++--- docs/4.x/faq/index.html | 8 +++--- docs/4.x/getting-started-overview/index.html | 8 +++--- docs/4.x/glossary/index.html | 8 +++--- .../migrating-angularJS-tutorial/index.html | 8 +++--- docs/4.x/migrating-existing-spas/index.html | 8 +++--- docs/4.x/migrating-react-tutorial/index.html | 8 +++--- docs/4.x/parcels-api/index.html | 8 +++--- docs/4.x/parcels-overview/index.html | 8 +++--- docs/4.x/separating-applications/index.html | 8 +++--- docs/4.x/starting-from-scratch/index.html | 8 +++--- docs/5.x/api/index.html | 8 +++--- docs/5.x/building-applications/index.html | 8 +++--- docs/5.x/code-of-conduct/index.html | 8 +++--- docs/5.x/configuration/index.html | 8 +++--- docs/5.x/contributing-overview/index.html | 8 +++--- docs/5.x/create-single-spa/index.html | 8 +++--- docs/5.x/devtools/index.html | 8 +++--- docs/5.x/ecosystem-alpinejs/index.html | 8 +++--- docs/5.x/ecosystem-angular/index.html | 8 +++--- docs/5.x/ecosystem-angularjs/index.html | 8 +++--- docs/5.x/ecosystem-backbone/index.html | 8 +++--- docs/5.x/ecosystem-css/index.html | 8 +++--- docs/5.x/ecosystem-cycle/index.html | 8 +++--- docs/5.x/ecosystem-dojo/index.html | 8 +++--- docs/5.x/ecosystem-ember/index.html | 8 +++--- .../ecosystem-html-web-components/index.html | 8 +++--- docs/5.x/ecosystem-inferno/index.html | 8 +++--- docs/5.x/ecosystem-leaked-globals/index.html | 8 +++--- docs/5.x/ecosystem-preact/index.html | 8 +++--- docs/5.x/ecosystem-react/index.html | 8 +++--- docs/5.x/ecosystem-riot/index.html | 8 +++--- docs/5.x/ecosystem-snowpack/index.html | 8 +++--- docs/5.x/ecosystem-svelte/index.html | 8 +++--- docs/5.x/ecosystem-vite/index.html | 8 +++--- docs/5.x/ecosystem-vue/index.html | 8 +++--- docs/5.x/ecosystem/index.html | 8 +++--- docs/5.x/examples/index.html | 8 +++--- docs/5.x/faq/index.html | 8 +++--- docs/5.x/getting-started-overview/index.html | 8 +++--- docs/5.x/glossary/index.html | 8 +++--- docs/5.x/layout-api/index.html | 8 +++--- docs/5.x/layout-definition/index.html | 8 +++--- docs/5.x/layout-overview/index.html | 8 +++--- docs/5.x/microfrontends-concept/index.html | 8 +++--- docs/5.x/migrating-existing-spas/index.html | 8 +++--- docs/5.x/module-types/index.html | 8 +++--- docs/5.x/parcels-api/index.html | 8 +++--- docs/5.x/parcels-overview/index.html | 8 +++--- docs/5.x/recommended-setup/index.html | 8 +++--- docs/5.x/separating-applications/index.html | 8 +++--- docs/5.x/shared-webpack-configs/index.html | 8 +++--- docs/5.x/single-spa-playground/index.html | 8 +++--- docs/5.x/ssr-overview/index.html | 8 +++--- docs/5.x/testing/e2e/index.html | 8 +++--- docs/5.x/testing/units/index.html | 8 +++--- docs/5.x/videos/index.html | 8 +++--- docs/api/index.html | 8 +++--- docs/building-applications/index.html | 8 +++--- docs/code-of-conduct/index.html | 8 +++--- docs/configuration/index.html | 8 +++--- docs/contributing-overview/index.html | 8 +++--- docs/create-single-spa/index.html | 8 +++--- docs/devtools/index.html | 8 +++--- docs/ecosystem-alpinejs/index.html | 8 +++--- docs/ecosystem-angular/index.html | 8 +++--- docs/ecosystem-angularjs/index.html | 8 +++--- docs/ecosystem-backbone/index.html | 8 +++--- docs/ecosystem-css/index.html | 8 +++--- docs/ecosystem-cycle/index.html | 8 +++--- docs/ecosystem-dojo/index.html | 8 +++--- docs/ecosystem-ember/index.html | 8 +++--- docs/ecosystem-html-web-components/index.html | 8 +++--- docs/ecosystem-inferno/index.html | 8 +++--- docs/ecosystem-leaked-globals/index.html | 8 +++--- docs/ecosystem-preact/index.html | 8 +++--- docs/ecosystem-react/index.html | 8 +++--- docs/ecosystem-riot/index.html | 8 +++--- docs/ecosystem-snowpack/index.html | 8 +++--- docs/ecosystem-svelte/index.html | 8 +++--- docs/ecosystem-vite/index.html | 8 +++--- docs/ecosystem-vue/index.html | 8 +++--- docs/ecosystem/index.html | 8 +++--- docs/examples/index.html | 8 +++--- docs/faq/index.html | 8 +++--- docs/getting-started-overview/index.html | 8 +++--- docs/glossary/index.html | 8 +++--- docs/index.html | 8 +++--- docs/layout-api/index.html | 8 +++--- docs/layout-definition/index.html | 8 +++--- docs/layout-overview/index.html | 8 +++--- docs/microfrontends-concept/index.html | 8 +++--- docs/migrating-existing-spas/index.html | 8 +++--- docs/module-types/index.html | 8 +++--- docs/next/index.html | 8 +++--- docs/parcels-api/index.html | 8 +++--- docs/parcels-overview/index.html | 8 +++--- docs/recommended-setup/index.html | 8 +++--- docs/separating-applications/index.html | 8 +++--- docs/shared-webpack-configs/index.html | 8 +++--- docs/single-spa-playground/index.html | 8 +++--- docs/ssr-overview/index.html | 8 +++--- docs/testing/e2e/index.html | 8 +++--- docs/testing/units/index.html | 8 +++--- docs/videos/index.html | 8 +++--- error/index.html | 8 +++--- help/index.html | 8 +++--- index.html | 8 +++--- languages/index.html | 8 +++--- search/index.html | 8 +++--- sitemap.xml | 2 +- sponsors/index.html | 8 +++--- users/index.html | 8 +++--- versions/index.html | 8 +++--- 165 files changed, 646 insertions(+), 602 deletions(-) create mode 100644 assets/js/0a789486.7b83bf5c.js create mode 100644 assets/js/0ae76a47.c19f6ce3.js delete mode 100644 assets/js/0ae76a47.ed3fef86.js create mode 100644 assets/js/3082ef22.1e205034.js create mode 100644 assets/js/814f3328.531f8715.js delete mode 100644 assets/js/814f3328.b433f0b5.js create mode 100644 assets/js/a6e5e601.02784cb2.js delete mode 100644 assets/js/a6e5e601.cc84ae5b.js rename assets/js/{b2b675dd.c3937a6c.js => b2b675dd.5939c463.js} (54%) delete mode 100644 assets/js/b2f554cd.0fb48443.js create mode 100644 assets/js/b2f554cd.3d02c0a6.js create mode 100644 assets/js/main.4cb5badb.js rename assets/js/{main.fe52a37c.js.LICENSE.txt => main.4cb5badb.js.LICENSE.txt} (100%) delete mode 100644 assets/js/main.fe52a37c.js delete mode 100644 assets/js/runtime~main.714a2b72.js create mode 100644 assets/js/runtime~main.d434432e.js create mode 100644 blog/2023/12/12/core-team-meeting-notes/index.html diff --git a/404.html b/404.html index ef405290e..86c265e46 100644 --- a/404.html +++ b/404.html @@ -14,13 +14,13 @@ - - + +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- - + + \ No newline at end of file diff --git a/assets/js/0a789486.7b83bf5c.js b/assets/js/0a789486.7b83bf5c.js new file mode 100644 index 000000000..9575a4906 --- /dev/null +++ b/assets/js/0a789486.7b83bf5c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[2273],{3905:function(e,t,n){n.d(t,{Zo:function(){return u},kt:function(){return f}});var r=n(7294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=r.createContext({}),c=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},u=function(e){var t=c(e.components);return r.createElement(s.Provider,{value:t},e.children)},p="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),p=c(n),d=a,f=p["".concat(s,".").concat(d)]||p[d]||m[d]||o;return n?r.createElement(f,i(i({ref:t},u),{},{components:n})):r.createElement(f,i({ref:t},u))}));function f(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,i=new Array(o);i[0]=d;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[p]="string"==typeof e?e:a,i[1]=l;for(var c=2;c ESM migration"),(0,o.kt)("li",{parentName:"ul"},"create-single-spa update (",(0,o.kt)("a",{parentName:"li",href:"https://github.com/cruft/cruft"},"https://github.com/cruft/cruft"),")"),(0,o.kt)("li",{parentName:"ul"},"Release single-spa 6 as latest \u2705"),(0,o.kt)("li",{parentName:"ul"},"Improve shared dependencies management"),(0,o.kt)("li",{parentName:"ul"},"Autopublishes to npm"),(0,o.kt)("li",{parentName:"ul"},"Server rendering enhancements?"),(0,o.kt)("li",{parentName:"ul"},"Support for NextJS, NuxtJS, Remix, create-react-app, and other build tools"),(0,o.kt)("li",{parentName:"ul"},"Consolidate example projects \ud83d\udea7"),(0,o.kt)("li",{parentName:"ul"},"Feature voting"),(0,o.kt)("li",{parentName:"ul"},"Automated integration tests for popular frameworks"),(0,o.kt)("li",{parentName:"ul"},"Update create-single-spa dependencies \ud83d\udea7")))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0ae76a47.c19f6ce3.js b/assets/js/0ae76a47.c19f6ce3.js new file mode 100644 index 000000000..2b988a395 --- /dev/null +++ b/assets/js/0ae76a47.c19f6ce3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[3288],{3905:function(e,t,n){n.d(t,{Zo:function(){return u},kt:function(){return h}});var a=n(7294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function l(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var r=a.createContext({}),p=function(e){var t=a.useContext(r),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},u=function(e){var t=p(e.components);return a.createElement(r.Provider,{value:t},e.children)},m="mdxType",g={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},c=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,i=e.originalType,r=e.parentName,u=o(e,["components","mdxType","originalType","parentName"]),m=p(n),c=s,h=m["".concat(r,".").concat(c)]||m[c]||g[c]||i;return n?a.createElement(h,l(l({ref:t},u),{},{components:n})):a.createElement(h,l({ref:t},u))}));function h(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var i=n.length,l=new Array(i);l[0]=c;var o={};for(var r in t)hasOwnProperty.call(t,r)&&(o[r]=t[r]);o.originalType=e,o[m]="string"==typeof e?e:s,l[1]=o;for(var p=2;p95% of single-spa users will be able to safely upgrade with no code changes. The breaking changes below are largely related to advanced features that most single-spa users do not use."),(0,i.kt)("h3",{id:"steps"},"Steps"),(0,i.kt)("ol",null,(0,i.kt)("li",{parentName:"ol"},"If using SystemJS + import maps, change the URL of single-spa in your import map:")),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-diff"},' - - + +
-

A Case for SOA in the Browser

· 6 min read

So you are a web-developer. You write a lot of JavaScript. You have a large single-page application (SPA) with features to add and bugs to maintain. Over time the application grows in size and complexity. It becomes more difficult to modify one portion of the SPA without breaking another portion.

The company is growing and you are looking for ways to scale the team and code-base. You add unit tests. You add a linter. You add continuous integration. You modularize the code with ES2015 modules, webpack, and npm. Eventually you even introduce new, independent SPAs with each SPA being owned and deployed by independent squads. Congratulations, you have successfully introduced service-oriented architecture on the front-end, or have you?

What is Service-oriented Architecture?

The fundamental concept behind service-oriented architecture is a service. A service is an isolated piece of code which can only be interacted with through its API. Unlike a shared library, a service itself can be deployed independently of its consumers. Think of a back-end API. The API is the service and the browser is the consumer. The API is deployed independently of the front-end application. There is also only one deployed version of the API available at a URL.

Contrast a service to a shared library. A shared library is a piece of code that is bundled and deployed with your code. For example, libraries such as Express, Lodash, and React are all shared libraries included in your application’s distributable. Upgrading a version of a shared library requires a new deployment of that distributable.

Service-oriented architecture is an approach to building software where the application is composed of many independent and isolated services. Those services are independently deployable, generally non-versioned, and auto discoverable.

Why Service-oriented Architecture on the Front-end?

The benefits of SOA can be illustrated with this real life example from Canopy. At Canopy we have multiple single page applications. The first application is external to the customers and the second is internal, yet both applications share common functionality. That functionality includes among other things, authentication and error logging.

cdn-images-1

Shared libraries between two separate applications. App 1 depends upon shared libs a, b, and c. App 2 depends upon only shared libs a and b.

Overall the design looks good. The code is modularized and shared. The complexities arrive when we start to upgrade the code to different versions. For example, after a short period of time, App 2 (being internal only) is upgraded to a new beta version of the shared lib b. Because the shared a also depends upon b (and we don’t want multiple versions of b bundled) we also create a new version of a. This one change causes a rebuild and deploy of three separate pieces of code: App 2 and shared libs a and b. Our dependency structure is no longer quite so simple.

cdn-images-2

In reality, a duplicate instance of lib a and b exist in both apps. Each app does not point to the same instance of the shared libraries, even when they are the same version. This is more noticeable when the shared libraries have separate versions.

Now imagine a bug in both versions of shared lib b. In order to fix the problem, you will have to republish both versions of a and b as well as c. Also App 1 and App 2 will have to be re-deployed. That is five new versions to publish and two apps to redeploy, all to fix one bug. All downstream dependencies have to be redeployed when a single library is changed. This is deploy dependency hell.

Service oriented architecture avoids these problems in a couple ways. Instead of bundling common dependencies, common code is shared through independent services. Services are not bundled, but rather loaded at run time. This also means that front-end services are not versioned (just like a back-end API). Both App 1 and App 2 load the exact same code for a front-end service.

Introducing sofe

Built upon the new ECMAScript module specification, sofe is a JavaScript library that enables independently deployable JavaScript services to be retrieved at run-time in the browser. Because the new module specification isn’t available within today’s browsers, sofe relies upon System.js to load services at run-time.

You can load a sofe service either with static or asynchronous imports.

// Static imports
import auth from 'auth-service!sofe';
const user = auth.getLoggedInUser();
// Asynchronous imports
System.import('auth-service!sofe').then(auth => auth.getLoggedInUser());

The real power behind sofe is that services are resolved at run-time, making them unversioned. If auth-service is redeployed, it is immediately made available to all upstream dependencies. The above scenario becomes much easier to resolve because there is only one version of each shared library as services. This is powerful because it allows you to deploy once, update everywhere. Also because the code is loaded at run-time, we can also enable developer tools to override what service is loaded into your application. Or in other words, you can test code on production without actually deploying to production.

cdn-images-2

The common dependencies are now services that are independent from the application code. Because services are unversioned, the dependency structure is again flat. Each service can individually be deployed and be available to every upstream dependency.

Obviously not all front-end code should be a service. Services have their own challenges. Specifically your code has to stay backwards compatible. But code can’t always be backwards compatible. Sometimes there needs to be breaking changes. The same problem exists for back-end services. A back-end API has to stay backwards compatible. Breaking changes on the back-end are generally solved by either creating an entirely new (versioned) API or implementing feature toggles within the API itself. The same solution applies to sofe services. An entirely new sofe service can be deployed or feature toggles can exist inside the front-end service. However it is solved, the key point is that services exist outside your application within their own distributable.

Another potential problem for sofe services is performance. Because they are loaded at run-time, performance can become a concern if you synchronously load too many services during bootstrap. Performance degradation can be mitigated by asynchronously loading larger services after the application bootstraps. Despite these challenges, there are many benefits to services on the front-end. The most exciting thing about sofe is there is now an option for services in the browser. You can decide what should and shouldn’t be a service.

Getting started with sofe requires only System.js. But to help you get started we have built sofe to work with a variety of technologies, including webpack, Babel, jspm, and the Chrome Developer Tools. Sofe is also actively used in production at Canopy Tax. We would love feedback on sofe and a number of open source projects that have been built around it. As you approach your next front-end project or look to improve your existing app, consider how it might benefit from service oriented architecture.

Read more about how to get started with sofe here.

- - +

A Case for SOA in the Browser

· 6 min read

So you are a web-developer. You write a lot of JavaScript. You have a large single-page application (SPA) with features to add and bugs to maintain. Over time the application grows in size and complexity. It becomes more difficult to modify one portion of the SPA without breaking another portion.

The company is growing and you are looking for ways to scale the team and code-base. You add unit tests. You add a linter. You add continuous integration. You modularize the code with ES2015 modules, webpack, and npm. Eventually you even introduce new, independent SPAs with each SPA being owned and deployed by independent squads. Congratulations, you have successfully introduced service-oriented architecture on the front-end, or have you?

What is Service-oriented Architecture?

The fundamental concept behind service-oriented architecture is a service. A service is an isolated piece of code which can only be interacted with through its API. Unlike a shared library, a service itself can be deployed independently of its consumers. Think of a back-end API. The API is the service and the browser is the consumer. The API is deployed independently of the front-end application. There is also only one deployed version of the API available at a URL.

Contrast a service to a shared library. A shared library is a piece of code that is bundled and deployed with your code. For example, libraries such as Express, Lodash, and React are all shared libraries included in your application’s distributable. Upgrading a version of a shared library requires a new deployment of that distributable.

Service-oriented architecture is an approach to building software where the application is composed of many independent and isolated services. Those services are independently deployable, generally non-versioned, and auto discoverable.

Why Service-oriented Architecture on the Front-end?

The benefits of SOA can be illustrated with this real life example from Canopy. At Canopy we have multiple single page applications. The first application is external to the customers and the second is internal, yet both applications share common functionality. That functionality includes among other things, authentication and error logging.

cdn-images-1

Shared libraries between two separate applications. App 1 depends upon shared libs a, b, and c. App 2 depends upon only shared libs a and b.

Overall the design looks good. The code is modularized and shared. The complexities arrive when we start to upgrade the code to different versions. For example, after a short period of time, App 2 (being internal only) is upgraded to a new beta version of the shared lib b. Because the shared a also depends upon b (and we don’t want multiple versions of b bundled) we also create a new version of a. This one change causes a rebuild and deploy of three separate pieces of code: App 2 and shared libs a and b. Our dependency structure is no longer quite so simple.

cdn-images-2

In reality, a duplicate instance of lib a and b exist in both apps. Each app does not point to the same instance of the shared libraries, even when they are the same version. This is more noticeable when the shared libraries have separate versions.

Now imagine a bug in both versions of shared lib b. In order to fix the problem, you will have to republish both versions of a and b as well as c. Also App 1 and App 2 will have to be re-deployed. That is five new versions to publish and two apps to redeploy, all to fix one bug. All downstream dependencies have to be redeployed when a single library is changed. This is deploy dependency hell.

Service oriented architecture avoids these problems in a couple ways. Instead of bundling common dependencies, common code is shared through independent services. Services are not bundled, but rather loaded at run time. This also means that front-end services are not versioned (just like a back-end API). Both App 1 and App 2 load the exact same code for a front-end service.

Introducing sofe

Built upon the new ECMAScript module specification, sofe is a JavaScript library that enables independently deployable JavaScript services to be retrieved at run-time in the browser. Because the new module specification isn’t available within today’s browsers, sofe relies upon System.js to load services at run-time.

You can load a sofe service either with static or asynchronous imports.

// Static imports
import auth from 'auth-service!sofe';
const user = auth.getLoggedInUser();
// Asynchronous imports
System.import('auth-service!sofe').then(auth => auth.getLoggedInUser());

The real power behind sofe is that services are resolved at run-time, making them unversioned. If auth-service is redeployed, it is immediately made available to all upstream dependencies. The above scenario becomes much easier to resolve because there is only one version of each shared library as services. This is powerful because it allows you to deploy once, update everywhere. Also because the code is loaded at run-time, we can also enable developer tools to override what service is loaded into your application. Or in other words, you can test code on production without actually deploying to production.

cdn-images-2

The common dependencies are now services that are independent from the application code. Because services are unversioned, the dependency structure is again flat. Each service can individually be deployed and be available to every upstream dependency.

Obviously not all front-end code should be a service. Services have their own challenges. Specifically your code has to stay backwards compatible. But code can’t always be backwards compatible. Sometimes there needs to be breaking changes. The same problem exists for back-end services. A back-end API has to stay backwards compatible. Breaking changes on the back-end are generally solved by either creating an entirely new (versioned) API or implementing feature toggles within the API itself. The same solution applies to sofe services. An entirely new sofe service can be deployed or feature toggles can exist inside the front-end service. However it is solved, the key point is that services exist outside your application within their own distributable.

Another potential problem for sofe services is performance. Because they are loaded at run-time, performance can become a concern if you synchronously load too many services during bootstrap. Performance degradation can be mitigated by asynchronously loading larger services after the application bootstraps. Despite these challenges, there are many benefits to services on the front-end. The most exciting thing about sofe is there is now an option for services in the browser. You can decide what should and shouldn’t be a service.

Getting started with sofe requires only System.js. But to help you get started we have built sofe to work with a variety of technologies, including webpack, Babel, jspm, and the Chrome Developer Tools. Sofe is also actively used in production at Canopy Tax. We would love feedback on sofe and a number of open source projects that have been built around it. As you approach your next front-end project or look to improve your existing app, consider how it might benefit from service oriented architecture.

Read more about how to get started with sofe here.

+ + \ No newline at end of file diff --git a/blog/2016/12/16/a-step-by-step-guide-to-single-spa/index.html b/blog/2016/12/16/a-step-by-step-guide-to-single-spa/index.html index 8f50ed992..f52d119d3 100644 --- a/blog/2016/12/16/a-step-by-step-guide-to-single-spa/index.html +++ b/blog/2016/12/16/a-step-by-step-guide-to-single-spa/index.html @@ -14,13 +14,13 @@ - - + +
-

A step-by-step guide to single-spa

· 10 min read

Running Angular 1, React, Angular 2, and Vue.js side by side sounds pretty cool. And it seems appealing to have multiple applications coexisting on the same page, each lazily loaded.

But using single-spa for the first time can be tricky because you’ll come across terms like “application lifecycles”, “root application”, “loading function”, “child application”, and “activity function.”

This blog post will take you through setting things up and what choices you have when using single-spa. It’s based on what I’ve seen at Canopy Tax where we went from an Angular 1 monolith to an Angular 1, React, and Svelte polyglot.

If you’d like to jump straight to a fully working, self contained code example, check out this webpack single-spa starter project.

Step One: choose a module loader.

Your module loader / bundler is the library you’ll use to lazy load code. I recommend either Webpack or JSPM, if you’re starting from scratch.

If you go with Webpack, try to use Webpack 2 if you can, since it has support for promise-based lazy loading. This will make things easier for you later on, since single-spa requires that your loading functions return promises. If you can’t use Webpack 2, getting single-spa to lazy load your code with Webpack 1 will require some boilerplate code.

JSPM/SystemJS has worse documentation than Webpack, but is a great solution for module loading if you can get past that. I recommend using jspm@0.17 — it’s still in beta but has been worked on for over a year and at Canopy we find it stable enough to use in production.

If you’re struggling to decide between the two, then ask yourself the following: Do I want multiple completely separate bundles? If you don’t, I recommend Webpack because it has better docs, a larger community, and fewer gotchas. Otherwise, I’d go with JSPM, since Webpack has no plans to support dynamic runtime loading (See tweet below from Mr. Larkin, himself).

Step Two: create a brand new HTML file

The next step is to create what single-spa calls your “root application.” Really your root application is just the stuff that initializes single-spa, and it starts with an HTML file.

Even if you’ve got an existing project that already has it’s own HTML file, I recommend starting fresh with a new HTML file. That way, there is a clear distinction between what is in your root application (shared between all apps) and what is in a child application (not shared with everything).

You’ll want to keep your root application as small as possible, since it’s sort of the master controller of everything and could become a bottleneck. You don’t want to be constantly changing both the root application and the child applications.

So for now, just have a <script> to a single JavaScript file (root-application.js), which will be explained in Step Three.

Since Webpack is probably the more common use case, my code examples from here on will assume that you’re using Webpack 2. The equivalent Webpack 1 or JSPM code has all the same concepts and only some minor code differences.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>A single-spa application</title>
</head>
<body>
<div id="cool-app"></div>
<script src="root-application.js"></script>
</body>
</html>

Step Three: register an “application”

Now it’s time to finish up your root application by writing your “root-application.js” file. The primary purpose of root-application.js is to call singleSpa.registerApplication(..) for each of the applications that will be managed by single-spa.

If you’re into analogies, you can think of single-spa as the operating system for your single page application, managing which “processes” (or “child applications”) are running at any given time. At any moment, some of the child applications will be active on the DOM and others will not. As the user navigates throughout the app, some applications will be unmounting from the DOM and others will be mounting to the DOM.

Another way to look at it is that single-spa is a master router on top of your other routers.

To do this, first npm install single-spa and then call the registerApplication function:

import {registerApplication, start} from 'single-spa';

// Register your first application with single-spa. More apps will be registered as you create them
registerApplication('cool-app', loadCoolApp, isCoolAppActive);

// Tell single-spa that you're ready for it to mount your application to the DOM
start();

// This is a "loading function"
function loadCoolApp() {
return import('./cool-app/cool.app.js');
}

// This is an "activity function"
function isCoolAppActive() {
return window.location.hash.startsWith('#/cool');
}

Because single-spa is so very cool, we’ve created an app called “cool-app” that will be lazy loaded and mounted to the DOM whenever the url hash starts with #/cool.

The loadCoolApp function is what single-spa calls a loading function. Inside of it, the import introduces a code splitting point — Webpack will create separate code chunks that will be lazy loaded by single-spa.

For your specific project, you probably won’t have a hash prefix of “cool”, but I recommend establishing some kind of convention that makes it easy to determine which apps are active. This will simplify the maintenance of your activity functions, as you add more and more child applications.

If you’re going to start out with just one child application, then it might make sense to implement the activity function as () => true. You can worry about getting fancier once you have more than one application.

The last thing is to call start(). This is something you must do for things to work. The purpose is to give control over timing and performance. But until that is a concern, start is just one of those things you do, and then maybe read about it later if you ever need to.

Step Four: create “.app.js” file

When you open up your index.html file in the browser, you’ll now see….. a blank screen! We’re really close, but there’s one crucial step left: building your app.js file.

After that, you’ll have everything working for your first single-spa application.

An app.js file is a configuration file that you create for each child application. It is the code that is lazy loaded when your activity function returns true.

There are three things that you need to implement in the app.js file:

  1. A bootstrap lifecycle
  2. A mount lifecycle
  3. An unmount lifecycle

A “lifecycle” is a function or array of functions that will be called by single-spa; you export these from the app.js file. Each function must return a Promise so that single-spa knows when it is completed.

Here is a simple example:

// single-spa will import this file and call the exported lifecyle functions

let user;

export function bootstrap() {
return fetch('/api/users/0')
.then(response => response.json())
.then(json => (user = json));
}

export function mount() {
/* This is normally where you would have your framework-specific code like
* ReactDOM.render or angular.bootstrap(). The fact that you can put *anything*
* into this function is what makes single-spa so powerful -- any framework
* can implement a "mount" and "unmount" to become a single-spa application.
*/
return Promise.resolve().then(() => {
document.getElementById('user-app').innerHTML = `
<div>
Hello ${user.name}!
<div>
`;
});
}

export function unmount() {
/* Real world use cases would be something like ReactDOM.unmountComponentAtNode()
* or vue.$destroy()
*/
return Promise.resolve().then(() => {
document.getElementById('user-app').innerHTML = '';
});
}

At this point, you might be seeing the document.getElementById and innerHTML = and worry that you’ve been duped — maybe single-spa is really just a poor excuse for a ui component framework.

And really, don’t we already have a lot of different ways to write UI components?

Getting all of those frameworks to work together.

Using multiple frameworks is where single-spa really shines. It is not a ui framework itself, but a framework for using other frameworks.

Each child application can be written in any framework, so long as it implements application lifecycle functions. Then the mini-apps cooperate to form the entire single page application.

So going back to our previous example, we could choose to write our “cool.app.js” as an Angular 1 app, and choose something else for future apps:

import singleSpaAngularJS from 'single-spa-angularjs';
import angular from 'angular';
import './app.module.js';
import './routes.js';

const domElementGetter = () => document.getElementById('cool-app');

const angularLifecycles = singleSpaAngularJS({
angular,
domElementGetter,
mainAngularModule: 'single-spa-app',
uiRouter: true,
preserveGlobal: true,
});

export const bootstrap = [
aboutToBootstrap,
angularLifecycles.bootstrap,
doneBootstrapping,
];

export const mount = [angularLifecycles.mount];

export const unmount = [angularLifecycles.unmount];

function aboutToBootstrap() {
console.log('about to bootstrapping');
return Promise.resolve();
}

function doneBootstrap() {
console.log('finished bootstrapping');
return Promise.resolve();
}

In this example, we use a helper library called single-spa-angularjs which abstracts away the specifics of initializing Angular 1 apps. This blogpost doesn’t show you the app.module.js or routes.js files, but you can see an example implementation here.

The pattern is to call singleSpaAngularJS at the very beginning, which returns bootstrap, mount, and unmount lifecycle functions for you.

You might notice that this time the lifecycles are exported as arrays of functions instead of just functions — you can choose whichever works best for you.

The advantage of exporting an array of functions is that you can add in your own custom behavior (like aboutToBootstrap and doneBootstrap) that will run before or after the Angular 1 lifecycles. When you export an array, each item in the array must be a function that returns a promise. Single-spa will wait for each promise to resolve, in order, before calling the next function in the array.

To learn more about single-spa helper libraries, check out these github projects:

You can also see a fully working example of an angular app coexisting with other apps at the single-spa-examples repo or the live demo.

Step Five: test it out!

Refresh your page and you should now have a functioning single-spa application!

Try navigating to a url that your child app is active for (#/cool) and then navigating away from it. When you do so, the page will not refresh but you should see your application mount itself to the DOM and then unmount.

If you run into problems, try to narrow down whether the problem is in the root application or in the child application. Is your root application being executed? Are the declareChildApplication calls being made? Have you called start()? Is there a network request to download the code for your child application? Is your child application's bootstrap lifecycle being called? What about mount?

cdn-images-1

It may be helpful to add a navigation menu, so you can verify everything mounts and unmounts to the DOM correctly. If you want to level up your single-spa skills even more, make the navigation menu an entire child application whose activity function is () => true. An example that does just that is found here and here.

While you are verifying that everything is working, keep in mind that each application goes through five phases:

an applications's lifecycle

Conclusion

As you get your feet wet, you’ll probably run into some (hopefully small) hiccups setting things up. When this tutorial is not enough, there are other resources on Github and here in the docs.

Single-spa is still a relatively new thing, and we’d love to hear your feedback and questions. We welcome contributions from everyone.

If you’re excited about the possibilities, feel free to contact me on twitter (@joelbdenning). And if you are not excited, then still feel free to contact me, but only after you leave some nasty comments :)

- - +

A step-by-step guide to single-spa

· 10 min read

Running Angular 1, React, Angular 2, and Vue.js side by side sounds pretty cool. And it seems appealing to have multiple applications coexisting on the same page, each lazily loaded.

But using single-spa for the first time can be tricky because you’ll come across terms like “application lifecycles”, “root application”, “loading function”, “child application”, and “activity function.”

This blog post will take you through setting things up and what choices you have when using single-spa. It’s based on what I’ve seen at Canopy Tax where we went from an Angular 1 monolith to an Angular 1, React, and Svelte polyglot.

If you’d like to jump straight to a fully working, self contained code example, check out this webpack single-spa starter project.

Step One: choose a module loader.

Your module loader / bundler is the library you’ll use to lazy load code. I recommend either Webpack or JSPM, if you’re starting from scratch.

If you go with Webpack, try to use Webpack 2 if you can, since it has support for promise-based lazy loading. This will make things easier for you later on, since single-spa requires that your loading functions return promises. If you can’t use Webpack 2, getting single-spa to lazy load your code with Webpack 1 will require some boilerplate code.

JSPM/SystemJS has worse documentation than Webpack, but is a great solution for module loading if you can get past that. I recommend using jspm@0.17 — it’s still in beta but has been worked on for over a year and at Canopy we find it stable enough to use in production.

If you’re struggling to decide between the two, then ask yourself the following: Do I want multiple completely separate bundles? If you don’t, I recommend Webpack because it has better docs, a larger community, and fewer gotchas. Otherwise, I’d go with JSPM, since Webpack has no plans to support dynamic runtime loading (See tweet below from Mr. Larkin, himself).

Step Two: create a brand new HTML file

The next step is to create what single-spa calls your “root application.” Really your root application is just the stuff that initializes single-spa, and it starts with an HTML file.

Even if you’ve got an existing project that already has it’s own HTML file, I recommend starting fresh with a new HTML file. That way, there is a clear distinction between what is in your root application (shared between all apps) and what is in a child application (not shared with everything).

You’ll want to keep your root application as small as possible, since it’s sort of the master controller of everything and could become a bottleneck. You don’t want to be constantly changing both the root application and the child applications.

So for now, just have a <script> to a single JavaScript file (root-application.js), which will be explained in Step Three.

Since Webpack is probably the more common use case, my code examples from here on will assume that you’re using Webpack 2. The equivalent Webpack 1 or JSPM code has all the same concepts and only some minor code differences.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>A single-spa application</title>
</head>
<body>
<div id="cool-app"></div>
<script src="root-application.js"></script>
</body>
</html>

Step Three: register an “application”

Now it’s time to finish up your root application by writing your “root-application.js” file. The primary purpose of root-application.js is to call singleSpa.registerApplication(..) for each of the applications that will be managed by single-spa.

If you’re into analogies, you can think of single-spa as the operating system for your single page application, managing which “processes” (or “child applications”) are running at any given time. At any moment, some of the child applications will be active on the DOM and others will not. As the user navigates throughout the app, some applications will be unmounting from the DOM and others will be mounting to the DOM.

Another way to look at it is that single-spa is a master router on top of your other routers.

To do this, first npm install single-spa and then call the registerApplication function:

import {registerApplication, start} from 'single-spa';

// Register your first application with single-spa. More apps will be registered as you create them
registerApplication('cool-app', loadCoolApp, isCoolAppActive);

// Tell single-spa that you're ready for it to mount your application to the DOM
start();

// This is a "loading function"
function loadCoolApp() {
return import('./cool-app/cool.app.js');
}

// This is an "activity function"
function isCoolAppActive() {
return window.location.hash.startsWith('#/cool');
}

Because single-spa is so very cool, we’ve created an app called “cool-app” that will be lazy loaded and mounted to the DOM whenever the url hash starts with #/cool.

The loadCoolApp function is what single-spa calls a loading function. Inside of it, the import introduces a code splitting point — Webpack will create separate code chunks that will be lazy loaded by single-spa.

For your specific project, you probably won’t have a hash prefix of “cool”, but I recommend establishing some kind of convention that makes it easy to determine which apps are active. This will simplify the maintenance of your activity functions, as you add more and more child applications.

If you’re going to start out with just one child application, then it might make sense to implement the activity function as () => true. You can worry about getting fancier once you have more than one application.

The last thing is to call start(). This is something you must do for things to work. The purpose is to give control over timing and performance. But until that is a concern, start is just one of those things you do, and then maybe read about it later if you ever need to.

Step Four: create “.app.js” file

When you open up your index.html file in the browser, you’ll now see….. a blank screen! We’re really close, but there’s one crucial step left: building your app.js file.

After that, you’ll have everything working for your first single-spa application.

An app.js file is a configuration file that you create for each child application. It is the code that is lazy loaded when your activity function returns true.

There are three things that you need to implement in the app.js file:

  1. A bootstrap lifecycle
  2. A mount lifecycle
  3. An unmount lifecycle

A “lifecycle” is a function or array of functions that will be called by single-spa; you export these from the app.js file. Each function must return a Promise so that single-spa knows when it is completed.

Here is a simple example:

// single-spa will import this file and call the exported lifecyle functions

let user;

export function bootstrap() {
return fetch('/api/users/0')
.then(response => response.json())
.then(json => (user = json));
}

export function mount() {
/* This is normally where you would have your framework-specific code like
* ReactDOM.render or angular.bootstrap(). The fact that you can put *anything*
* into this function is what makes single-spa so powerful -- any framework
* can implement a "mount" and "unmount" to become a single-spa application.
*/
return Promise.resolve().then(() => {
document.getElementById('user-app').innerHTML = `
<div>
Hello ${user.name}!
<div>
`;
});
}

export function unmount() {
/* Real world use cases would be something like ReactDOM.unmountComponentAtNode()
* or vue.$destroy()
*/
return Promise.resolve().then(() => {
document.getElementById('user-app').innerHTML = '';
});
}

At this point, you might be seeing the document.getElementById and innerHTML = and worry that you’ve been duped — maybe single-spa is really just a poor excuse for a ui component framework.

And really, don’t we already have a lot of different ways to write UI components?

Getting all of those frameworks to work together.

Using multiple frameworks is where single-spa really shines. It is not a ui framework itself, but a framework for using other frameworks.

Each child application can be written in any framework, so long as it implements application lifecycle functions. Then the mini-apps cooperate to form the entire single page application.

So going back to our previous example, we could choose to write our “cool.app.js” as an Angular 1 app, and choose something else for future apps:

import singleSpaAngularJS from 'single-spa-angularjs';
import angular from 'angular';
import './app.module.js';
import './routes.js';

const domElementGetter = () => document.getElementById('cool-app');

const angularLifecycles = singleSpaAngularJS({
angular,
domElementGetter,
mainAngularModule: 'single-spa-app',
uiRouter: true,
preserveGlobal: true,
});

export const bootstrap = [
aboutToBootstrap,
angularLifecycles.bootstrap,
doneBootstrapping,
];

export const mount = [angularLifecycles.mount];

export const unmount = [angularLifecycles.unmount];

function aboutToBootstrap() {
console.log('about to bootstrapping');
return Promise.resolve();
}

function doneBootstrap() {
console.log('finished bootstrapping');
return Promise.resolve();
}

In this example, we use a helper library called single-spa-angularjs which abstracts away the specifics of initializing Angular 1 apps. This blogpost doesn’t show you the app.module.js or routes.js files, but you can see an example implementation here.

The pattern is to call singleSpaAngularJS at the very beginning, which returns bootstrap, mount, and unmount lifecycle functions for you.

You might notice that this time the lifecycles are exported as arrays of functions instead of just functions — you can choose whichever works best for you.

The advantage of exporting an array of functions is that you can add in your own custom behavior (like aboutToBootstrap and doneBootstrap) that will run before or after the Angular 1 lifecycles. When you export an array, each item in the array must be a function that returns a promise. Single-spa will wait for each promise to resolve, in order, before calling the next function in the array.

To learn more about single-spa helper libraries, check out these github projects:

You can also see a fully working example of an angular app coexisting with other apps at the single-spa-examples repo or the live demo.

Step Five: test it out!

Refresh your page and you should now have a functioning single-spa application!

Try navigating to a url that your child app is active for (#/cool) and then navigating away from it. When you do so, the page will not refresh but you should see your application mount itself to the DOM and then unmount.

If you run into problems, try to narrow down whether the problem is in the root application or in the child application. Is your root application being executed? Are the declareChildApplication calls being made? Have you called start()? Is there a network request to download the code for your child application? Is your child application's bootstrap lifecycle being called? What about mount?

cdn-images-1

It may be helpful to add a navigation menu, so you can verify everything mounts and unmounts to the DOM correctly. If you want to level up your single-spa skills even more, make the navigation menu an entire child application whose activity function is () => true. An example that does just that is found here and here.

While you are verifying that everything is working, keep in mind that each application goes through five phases:

an applications's lifecycle

Conclusion

As you get your feet wet, you’ll probably run into some (hopefully small) hiccups setting things up. When this tutorial is not enough, there are other resources on Github and here in the docs.

Single-spa is still a relatively new thing, and we’d love to hear your feedback and questions. We welcome contributions from everyone.

If you’re excited about the possibilities, feel free to contact me on twitter (@joelbdenning). And if you are not excited, then still feel free to contact me, but only after you leave some nasty comments :)

+ + \ No newline at end of file diff --git a/blog/2018/06/19/single-spa-parcels-explained/index.html b/blog/2018/06/19/single-spa-parcels-explained/index.html index 4bf22461f..ed3341ae6 100644 --- a/blog/2018/06/19/single-spa-parcels-explained/index.html +++ b/blog/2018/06/19/single-spa-parcels-explained/index.html @@ -14,13 +14,13 @@ - - + +
-

single-spa parcels, explained

· 4 min read

Ever since single-spa@1.0.0, the single-spa team has been dedicated to bringing microservices to the frontend. We have made it possible for AngularJS, React, Angular, Vue, and other frameworks to coexist side by side in the same page.

And with the release of version 4, I’m pleased to announce that single-spa is expanding that effort so that individual components written with different frameworks can interoperate. It is new terrain for the single-spa community, which previously had focused on getting large applications to interoperate with each other, instead of the individual components.

Another way to do framework agnostic components?

For those familiar with web components and custom elements, you may be wondering why a JavaScript library would try to do what browsers are starting natively to do.

And as one of the contributors to the custom elements polyfill, let me be the first one to say that we did not make this decision lightly.

If you’re interested in diving into the details, check out One Company’s Relationship With Custom Elements, which explains some of the difficulties we’ve been through with web components and custom elements.

TLDR: React and some other frameworks don’t interop with custom elements very well. Additionally dealing with inner HTML, attributes vs properties, and customized builtins can be a pain.

Okay but you haven’t told me what a single-spa parcel is

A parcel is single-spa’s way of building a component in one framework and using it in another.

To implement a parcel, just create a JavaScript object that has 3–4 functions on it. We call this JavaScript object a parcel config and there are three required functions to implement: bootstrap, mount, and unmount. A fourth function, update, is optional.

Each of the functions will be called by single-spa at the right time, but the parcel config will control what happens. In other words, single-spa controls the “when,” but the parcel config controls the “what” and the “how.”

Once you’ve implemented the parcel config, simply call singleSpa.mountRootParcel(parcelConfig, parcelProps) to mount it. This is the key to what makes parcels framework agnostic — regardless of whether the parcel config is implemented with React, Angular, Vue, or anything else, to use the parcel you always just call mountRootParcel().

A few more specifics

We’ve glossed over a few things that I want to touch on real quick:

  • How do you implement the lifecycle functions on the parcel config?

            Use a helper library for your framework of choice. [single-spa-react](https://github.com/single-spa/single-spa-react), [single-spa-angular](https://github.com/single-spa/single-spa-angular) (for angular@2+), [single-spa-angularjs](https://github.com/single-spa/single-spa-angularjs), [single-spa-vue](https://github.com/single-spa/single-spa-vue), and [others](https://github.com/single-spa/single-spa/blob/master/docs/single-spa-ecosystem.md) will implement the entire parcel config for you.
  • What are the props you pass to mountRootParcel()?

            The props passed as the second argument to singleSpa.mountRootParcel(parcelConfig, parcelProps) are an object with one required prop and as many custom props as you’d like. The required prop is domElement, which tells the parcel where to mount. And the custom props get passed through to the parcel config lifecycle functions.
  • How do you re-render and unmount a parcel?

            The singleSpa.mountRootParcel() function returns a parcel object that lets you re-render and unmount the parcel whenever you’d like to.

    <iframe src="https://medium.com/media/b2d981b380b937009f7ce84e1cc2d753" frameBorder="0" />

Syntactic sugar makes this easier

Calling all of those functions manually might get annoying. So let’s make it easier. Here’s an example of some syntactic sugar for React. Similar features will be added soon for Angular, Vue, and other frameworks.

How hard is it to try this out?

You can get started with parcels immediately, without using the rest of single-spa. To do so, either npm install or script tag single-spa, then call mountRootParcel with your first parcel config.

You can also check out this codepen example to start out.

And if you are already a user of single-spa applications, parcels mean that your applications can mount and unmount shared functionality whenever you want them to. Since parcels don’t have activity functions, you don’t have to set up routes for them.

Let us know what you think!

We’d love to get your feedback on parcels. What do you think of this new way of framework interop? Is the implementation easy to understand? Are parcels useful for you or do they not quite fit into what you’re trying to accomplish?How hard was it for you to try out?

Check out the official docs for more examples, explanations, and api documentation.

And let us know your thoughts in the single-spa Slack channel, a Github issue, or on Twitter!

- - +

single-spa parcels, explained

· 4 min read

Ever since single-spa@1.0.0, the single-spa team has been dedicated to bringing microservices to the frontend. We have made it possible for AngularJS, React, Angular, Vue, and other frameworks to coexist side by side in the same page.

And with the release of version 4, I’m pleased to announce that single-spa is expanding that effort so that individual components written with different frameworks can interoperate. It is new terrain for the single-spa community, which previously had focused on getting large applications to interoperate with each other, instead of the individual components.

Another way to do framework agnostic components?

For those familiar with web components and custom elements, you may be wondering why a JavaScript library would try to do what browsers are starting natively to do.

And as one of the contributors to the custom elements polyfill, let me be the first one to say that we did not make this decision lightly.

If you’re interested in diving into the details, check out One Company’s Relationship With Custom Elements, which explains some of the difficulties we’ve been through with web components and custom elements.

TLDR: React and some other frameworks don’t interop with custom elements very well. Additionally dealing with inner HTML, attributes vs properties, and customized builtins can be a pain.

Okay but you haven’t told me what a single-spa parcel is

A parcel is single-spa’s way of building a component in one framework and using it in another.

To implement a parcel, just create a JavaScript object that has 3–4 functions on it. We call this JavaScript object a parcel config and there are three required functions to implement: bootstrap, mount, and unmount. A fourth function, update, is optional.

Each of the functions will be called by single-spa at the right time, but the parcel config will control what happens. In other words, single-spa controls the “when,” but the parcel config controls the “what” and the “how.”

Once you’ve implemented the parcel config, simply call singleSpa.mountRootParcel(parcelConfig, parcelProps) to mount it. This is the key to what makes parcels framework agnostic — regardless of whether the parcel config is implemented with React, Angular, Vue, or anything else, to use the parcel you always just call mountRootParcel().

A few more specifics

We’ve glossed over a few things that I want to touch on real quick:

  • How do you implement the lifecycle functions on the parcel config?

            Use a helper library for your framework of choice. [single-spa-react](https://github.com/single-spa/single-spa-react), [single-spa-angular](https://github.com/single-spa/single-spa-angular) (for angular@2+), [single-spa-angularjs](https://github.com/single-spa/single-spa-angularjs), [single-spa-vue](https://github.com/single-spa/single-spa-vue), and [others](https://github.com/single-spa/single-spa/blob/master/docs/single-spa-ecosystem.md) will implement the entire parcel config for you.
  • What are the props you pass to mountRootParcel()?

            The props passed as the second argument to singleSpa.mountRootParcel(parcelConfig, parcelProps) are an object with one required prop and as many custom props as you’d like. The required prop is domElement, which tells the parcel where to mount. And the custom props get passed through to the parcel config lifecycle functions.
  • How do you re-render and unmount a parcel?

            The singleSpa.mountRootParcel() function returns a parcel object that lets you re-render and unmount the parcel whenever you’d like to.

    <iframe src="https://medium.com/media/b2d981b380b937009f7ce84e1cc2d753" frameBorder="0" />

Syntactic sugar makes this easier

Calling all of those functions manually might get annoying. So let’s make it easier. Here’s an example of some syntactic sugar for React. Similar features will be added soon for Angular, Vue, and other frameworks.

How hard is it to try this out?

You can get started with parcels immediately, without using the rest of single-spa. To do so, either npm install or script tag single-spa, then call mountRootParcel with your first parcel config.

You can also check out this codepen example to start out.

And if you are already a user of single-spa applications, parcels mean that your applications can mount and unmount shared functionality whenever you want them to. Since parcels don’t have activity functions, you don’t have to set up routes for them.

Let us know what you think!

We’d love to get your feedback on parcels. What do you think of this new way of framework interop? Is the implementation easy to understand? Are parcels useful for you or do they not quite fit into what you’re trying to accomplish?How hard was it for you to try out?

Check out the official docs for more examples, explanations, and api documentation.

And let us know your thoughts in the single-spa Slack channel, a Github issue, or on Twitter!

+ + \ No newline at end of file diff --git a/blog/2019/02/20/single-spa-inspector/index.html b/blog/2019/02/20/single-spa-inspector/index.html index 39fb3d573..051a761fe 100644 --- a/blog/2019/02/20/single-spa-inspector/index.html +++ b/blog/2019/02/20/single-spa-inspector/index.html @@ -14,13 +14,13 @@ - - + +
-

single-spa Inspector and 4.1

· 2 min read

Background

For a long time, Canopy has had the benefit of using a tool called sofe inspector (note: this is an out-of-date version of it) to list, override, and interact with single-spa applications. There has always been a desire to figure out how to share this tool so others can benefit as well.

With that in mind, I'm proud to announce an initial release for single-spa Inspector! single-spa Inspector is a Firefox and Chrome extension, much like React/Vue devtools, that allows you see and interact with your single-spa applications and configuration.

Current Inspector Features

  • List registered applications
  • Show application status
  • Force an app to mount or unmount
  • Hover over an app name to have an "inspect element"-like view of your apps (Overlays)

(Note: Overlays require a small update to your code, but should hopefully be simple! See how to configure app overlays)

The single-spa Inspector will only work with single-spa versions 4.1 and higher, since we had to expose and add some functionality to the single-spa library itself in order to implement these features.

single-spa 4.1

single-spa 4.1 was released, which includes a couple of key updates:

  1. Support for single-spa Inspector
  2. ESM bundle output
  3. Simpmlified test configuration for developers/contributors to single-spa

For most people, ESM (EcmaScript Module) support shouldn't affect how you use single-spa, but for those looking to play around with modules or other advanced Javascript things, it's a welcome addition.

We also changed our test suite to purely use Jest instead of Saucelabs, and hopefully false positive "failing" tests on pull requests will be a thing of the past.

Help Wanted!

If you would like to suggest a new feature for single-spa Inspector, report a bug, improve our (admittedly horrible and hopefully temporary) UI/UX, or add features, please see the github repo and hack away!

We also hope to update some of our example repos to the lastest single-spa so that anyone with the extension installed can test out the features and see how to implement overlays. But this process will go faster if someone wants to help out. :)

Thank you!

- - +

single-spa Inspector and 4.1

· 2 min read

Background

For a long time, Canopy has had the benefit of using a tool called sofe inspector (note: this is an out-of-date version of it) to list, override, and interact with single-spa applications. There has always been a desire to figure out how to share this tool so others can benefit as well.

With that in mind, I'm proud to announce an initial release for single-spa Inspector! single-spa Inspector is a Firefox and Chrome extension, much like React/Vue devtools, that allows you see and interact with your single-spa applications and configuration.

Current Inspector Features

  • List registered applications
  • Show application status
  • Force an app to mount or unmount
  • Hover over an app name to have an "inspect element"-like view of your apps (Overlays)

(Note: Overlays require a small update to your code, but should hopefully be simple! See how to configure app overlays)

The single-spa Inspector will only work with single-spa versions 4.1 and higher, since we had to expose and add some functionality to the single-spa library itself in order to implement these features.

single-spa 4.1

single-spa 4.1 was released, which includes a couple of key updates:

  1. Support for single-spa Inspector
  2. ESM bundle output
  3. Simpmlified test configuration for developers/contributors to single-spa

For most people, ESM (EcmaScript Module) support shouldn't affect how you use single-spa, but for those looking to play around with modules or other advanced Javascript things, it's a welcome addition.

We also changed our test suite to purely use Jest instead of Saucelabs, and hopefully false positive "failing" tests on pull requests will be a thing of the past.

Help Wanted!

If you would like to suggest a new feature for single-spa Inspector, report a bug, improve our (admittedly horrible and hopefully temporary) UI/UX, or add features, please see the github repo and hack away!

We also hope to update some of our example repos to the lastest single-spa so that anyone with the extension installed can test out the features and see how to implement overlays. But this process will go faster if someone wants to help out. :)

Thank you!

+ + \ No newline at end of file diff --git a/blog/2020/02/24/single-spa-5/index.html b/blog/2020/02/24/single-spa-5/index.html index 1efbd9b7e..7da39ccee 100644 --- a/blog/2020/02/24/single-spa-5/index.html +++ b/blog/2020/02/24/single-spa-5/index.html @@ -14,13 +14,13 @@ - - + +
-

single-spa 5

· 5 min read

Today we released single-spa@5.0.0.

Here are the highlights:

Release notes here

Migration from 4 to 5

For every user we're aware of, you do not need to change anything in your code in order to upgrade to single-spa@5. The breaking changes listed in the release notes are the removal of features that were originally used by Canopy Tax, but were never documented.

If installing from npm, you can simply npm install --save single-spa@5.0.0 or yarn add single-spa@5.0.0.

Alternatively, single-spa is available on cdnjs, jsdelivr, and unpkg.

The single-spa core team is committed to treating our users well, which includes not introducing massive breaking changes. The core single-spa API has not seen massive breaking changes since single-spa@3 in August 2016. We have added features and improved things, but single-spa is a stable technology. We are committed to maintaining it, documenting it, and adjusting it as technologies like in-browser modules become more and more popular and viable.

Performance improvements

The ESM version of single-spa@4 was 23.8kb (7.2kb gzipped). That was improved in single-spa@5 to 15.5kb (5.1kb gzipped). We did this by optimizing our build process and removing unused features.

single-spa CLI

Since single-spa's inception, bundler configuration has been a huge source of user pain. We have heard this pain and implemented create-single-spa, which creates (and sometimes can update) repositories that are ready to be used as single-spa microfrontends. For Angular and Vue, the official CLIs are used with a few extra plugins automatically installed. For React, a default webpack config with decent eslint / prettier defaults is set up.

Additionally, we have added a lot of documentation for webpack in The Recommended Setup.

Tutorial videos

We understand that single-spa is more than just a library - it is an architecture. The single-spa library itself is the core, but the surrounding ecosystem of concepts and libraries are equally important to successfully migrating to single-spa and having it work for you. As such, we have created a Youtube playlist, currently consisting of seven videos, to help you get started.

Youtube playlist / Bilibili space

The videos currently cover the following topics:

  • What are Microfrontends?
  • In-browser vs build-time JavaScript modules
  • Import Maps
  • Local Development with single-spa and import maps
  • Deploying Microfrontends / Continuous Integration (CI)
  • SystemJS intro
  • Lazy Loading
  • Bundlers, webpack, and rollup.

New example repositories

What started out as Canopy Tax's special sauce for independently deployed frontend microservices is now fully accessible to the public with our new set of example repos. We have a React example, a Vue example, and a polyglot (multiple framework) example. We hope to add an Angular example, after we achieve support for Angular 9. These example repositories are actively watched and maintained by the single-spa core team, and reflect our current opinions on the best, production-viable way to do microfrontends.

Furthermore, we have deployed each of the examples to our new domains:

Documentation overhaul

We removed several dated documentation pages, and added several that were very much lacking. Here are a few pages that give you the most bang for your buck:

Development builds and error codes

Taking inspiration from the react development and production builds, we now publish to NPM both development and production builds in the following formats: UMD, ESM, and System.register.

You can see the published build files here. The .dev.js files provide full debugging information in the browser console, whereas the .min.js files give you a numeric error code and a link to a documentation page that explains the error. We hope that these error codes and documentation for them will improve discoverability of relevant documentation when you're setting up single-spa.

An example of these new documentation pages for error codes is found here.

Governance

Some of you may have noticed that we recently moved all github repos from https://github.com/CanopyTax to https://github.com/single-spa. Canopy Tax was the company where single-spa was first authored, but as a core team we asked to move ownership and governance of the projects to an organization fully managed by the open source community. In agreement with Canopy, we made that change.

This change does not mean anything drastic for single-spa. Its license was and is MIT, and we have no plans to do anything with the project besides make it better.

Where next?

We are actively translating the single-spa documentation to Chinese, and hope to add other languages soon. We will add full Angular 9 support soon, and hope to add server rendering in an upcoming release.

Please contribute to our code and ecosystem, join our single-spa slack channel, follow our official Twitter account, and contribute to our open collective. The single-spa core team all have full-time jobs and maintain this project on a volunteer basis.

- - +

single-spa 5

· 5 min read

Today we released single-spa@5.0.0.

Here are the highlights:

Release notes here

Migration from 4 to 5

For every user we're aware of, you do not need to change anything in your code in order to upgrade to single-spa@5. The breaking changes listed in the release notes are the removal of features that were originally used by Canopy Tax, but were never documented.

If installing from npm, you can simply npm install --save single-spa@5.0.0 or yarn add single-spa@5.0.0.

Alternatively, single-spa is available on cdnjs, jsdelivr, and unpkg.

The single-spa core team is committed to treating our users well, which includes not introducing massive breaking changes. The core single-spa API has not seen massive breaking changes since single-spa@3 in August 2016. We have added features and improved things, but single-spa is a stable technology. We are committed to maintaining it, documenting it, and adjusting it as technologies like in-browser modules become more and more popular and viable.

Performance improvements

The ESM version of single-spa@4 was 23.8kb (7.2kb gzipped). That was improved in single-spa@5 to 15.5kb (5.1kb gzipped). We did this by optimizing our build process and removing unused features.

single-spa CLI

Since single-spa's inception, bundler configuration has been a huge source of user pain. We have heard this pain and implemented create-single-spa, which creates (and sometimes can update) repositories that are ready to be used as single-spa microfrontends. For Angular and Vue, the official CLIs are used with a few extra plugins automatically installed. For React, a default webpack config with decent eslint / prettier defaults is set up.

Additionally, we have added a lot of documentation for webpack in The Recommended Setup.

Tutorial videos

We understand that single-spa is more than just a library - it is an architecture. The single-spa library itself is the core, but the surrounding ecosystem of concepts and libraries are equally important to successfully migrating to single-spa and having it work for you. As such, we have created a Youtube playlist, currently consisting of seven videos, to help you get started.

Youtube playlist / Bilibili space

The videos currently cover the following topics:

  • What are Microfrontends?
  • In-browser vs build-time JavaScript modules
  • Import Maps
  • Local Development with single-spa and import maps
  • Deploying Microfrontends / Continuous Integration (CI)
  • SystemJS intro
  • Lazy Loading
  • Bundlers, webpack, and rollup.

New example repositories

What started out as Canopy Tax's special sauce for independently deployed frontend microservices is now fully accessible to the public with our new set of example repos. We have a React example, a Vue example, and a polyglot (multiple framework) example. We hope to add an Angular example, after we achieve support for Angular 9. These example repositories are actively watched and maintained by the single-spa core team, and reflect our current opinions on the best, production-viable way to do microfrontends.

Furthermore, we have deployed each of the examples to our new domains:

Documentation overhaul

We removed several dated documentation pages, and added several that were very much lacking. Here are a few pages that give you the most bang for your buck:

Development builds and error codes

Taking inspiration from the react development and production builds, we now publish to NPM both development and production builds in the following formats: UMD, ESM, and System.register.

You can see the published build files here. The .dev.js files provide full debugging information in the browser console, whereas the .min.js files give you a numeric error code and a link to a documentation page that explains the error. We hope that these error codes and documentation for them will improve discoverability of relevant documentation when you're setting up single-spa.

An example of these new documentation pages for error codes is found here.

Governance

Some of you may have noticed that we recently moved all github repos from https://github.com/CanopyTax to https://github.com/single-spa. Canopy Tax was the company where single-spa was first authored, but as a core team we asked to move ownership and governance of the projects to an organization fully managed by the open source community. In agreement with Canopy, we made that change.

This change does not mean anything drastic for single-spa. Its license was and is MIT, and we have no plans to do anything with the project besides make it better.

Where next?

We are actively translating the single-spa documentation to Chinese, and hope to add other languages soon. We will add full Angular 9 support soon, and hope to add server rendering in an upcoming release.

Please contribute to our code and ecosystem, join our single-spa slack channel, follow our official Twitter account, and contribute to our open collective. The single-spa core team all have full-time jobs and maintain this project on a volunteer basis.

+ + \ No newline at end of file diff --git a/blog/2023/08/22/single-spa-core-is-expanding/index.html b/blog/2023/08/22/single-spa-core-is-expanding/index.html index d14fcc396..3e772bba7 100644 --- a/blog/2023/08/22/single-spa-core-is-expanding/index.html +++ b/blog/2023/08/22/single-spa-core-is-expanding/index.html @@ -14,13 +14,13 @@ - - + +
-

The single-spa core team is expanding

· 2 min read

We are happy to announce that the single-spa core team is opening applications for two new members! As a core team member, you will help us maintain and guide the growing single-spa ecosystem by responding to Github and Slack issues, implementing new features, and helping to create the roadmap for the future of single-spa.

Our current core team consists of Joel Denning, Carlos Filoteo, and Anthony Frehner. We also thank Justin McMurdie and Bret Little for their valuable contributions during their time with the single-spa core team.

Single-spa owes its success to the people and companies who support it. If you want to contribute but aren’t able to join the core team, consider applying to be a maintainer. Maintainers are volunteers who receive Github write access to specific subprojects within the ecosystem. If you’re a manager at a company that uses single-spa, we encourage you to allow your developers to volunteer their time as maintainers or core team members so that the project can continue to grow and develop.

The single-spa core team and maintenance are unpaid volunteer positions. Looking ahead, if any core team members or maintainers are interested in new paid roles, Convex Cooperative (the company Joel works for) will be considering hiring new developers from the single-spa team. It’s important to state that Convex will not poach developers away from companies who allow their developers to help us maintain single-spa—our priority is nurturing the growth and development of the single-spa ecosystem.

If interested in applying to be a single-spa maintainer or core team member, please fill out the following Google Form before September 1, 2023:

https://docs.google.com/forms/d/1zIfP2kYjNBCi-qjKf9T1-sNT1VLSXTB9twwX_mxKYxU/edit

- - +

The single-spa core team is expanding

· 2 min read

We are happy to announce that the single-spa core team is opening applications for two new members! As a core team member, you will help us maintain and guide the growing single-spa ecosystem by responding to Github and Slack issues, implementing new features, and helping to create the roadmap for the future of single-spa.

Our current core team consists of Joel Denning, Carlos Filoteo, and Anthony Frehner. We also thank Justin McMurdie and Bret Little for their valuable contributions during their time with the single-spa core team.

Single-spa owes its success to the people and companies who support it. If you want to contribute but aren’t able to join the core team, consider applying to be a maintainer. Maintainers are volunteers who receive Github write access to specific subprojects within the ecosystem. If you’re a manager at a company that uses single-spa, we encourage you to allow your developers to volunteer their time as maintainers or core team members so that the project can continue to grow and develop.

The single-spa core team and maintenance are unpaid volunteer positions. Looking ahead, if any core team members or maintainers are interested in new paid roles, Convex Cooperative (the company Joel works for) will be considering hiring new developers from the single-spa team. It’s important to state that Convex will not poach developers away from companies who allow their developers to help us maintain single-spa—our priority is nurturing the growth and development of the single-spa ecosystem.

If interested in applying to be a single-spa maintainer or core team member, please fill out the following Google Form before September 1, 2023:

https://docs.google.com/forms/d/1zIfP2kYjNBCi-qjKf9T1-sNT1VLSXTB9twwX_mxKYxU/edit

+ + \ No newline at end of file diff --git a/blog/2023/10/11/introducing-single-spa-new-core-team-members/index.html b/blog/2023/10/11/introducing-single-spa-new-core-team-members/index.html index ca9ff0f71..e399d1ba0 100644 --- a/blog/2023/10/11/introducing-single-spa-new-core-team-members/index.html +++ b/blog/2023/10/11/introducing-single-spa-new-core-team-members/index.html @@ -14,13 +14,13 @@ - - + +
-

Introducing Single-Spa's New Core Team Members

· 2 min read

Please welcome the latest additions to the Single-Spa core team. These talented individuals bring fresh energy and a wealth of expertise and are poised to revitalize and expand the Single-Spa project.

Meet the Team

Artur Androsovych

Artur Androsovych is a Google Developer Expert in Angular and an open-source contributor who has been focusing on runtime performance and teaching teams about Angular internals for the past few years. He has maintained the single-spa-angular project for years, and we're excited for him to join the core team.

Roberto Mosca

Roberto is a Principal Software Engineer at TravelPerk (www.travelperk.com), where he is the go-to person for all things related to their frontend platform. With a full-stack background, he's got his hands on a variety of tech — from Python to NodeJS, React, Webpack... Before diving into the world of business travel, he was immersed in biomedical research as a bioinformatician. When he is not in front of a screen, you’ll find him swimming, cooking up some Italian delicacies, or tinkering with his Rubik’s cube-solving robots.

Milan Kovacic

Milan Kovacic is a seasoned software consultant with deep expertise in various development domains. On the frontend side, his work is marked by proficiency in React, TypeScript, and the integration of microfrontends. For backend solutions, he mostly relies on the .NET framework, C#—often utilizing microservices architecture. Additionally, Milan is experienced with cloud technologies, regularly working with platforms like AWS and Azure. Beyond traditional development, he places great emphasis on ensuring a smooth developer experience and promotes the use of automation and efficient DevOps practices to streamline development workflows.

These three new members will be joining current members Joel Denning, Carlos Filoteo, and Anthony Frehner to manage GitHub and Slack issues and actively develop new features. We’d also like to give special recognition to contributors from Qiankun for their extensive support over the years.

As part of this team update, we’ll also be creating a public roadmap, providing transparency about Single-Spa's future direction. To maintain transparency and encourage collaboration, we're establishing monthly meetings where the core team will discuss progress, challenges, and ideas. Meeting notes will be publicly available, ensuring that the community is well-informed and able to participate in our discussions.

So stay tuned and join us on this journey shaping the future of microfrontends.

- - +

Introducing Single-Spa's New Core Team Members

· 2 min read

Please welcome the latest additions to the Single-Spa core team. These talented individuals bring fresh energy and a wealth of expertise and are poised to revitalize and expand the Single-Spa project.

Meet the Team

Artur Androsovych

Artur Androsovych is a Google Developer Expert in Angular and an open-source contributor who has been focusing on runtime performance and teaching teams about Angular internals for the past few years. He has maintained the single-spa-angular project for years, and we're excited for him to join the core team.

Roberto Mosca

Roberto is a Principal Software Engineer at TravelPerk (www.travelperk.com), where he is the go-to person for all things related to their frontend platform. With a full-stack background, he's got his hands on a variety of tech — from Python to NodeJS, React, Webpack... Before diving into the world of business travel, he was immersed in biomedical research as a bioinformatician. When he is not in front of a screen, you’ll find him swimming, cooking up some Italian delicacies, or tinkering with his Rubik’s cube-solving robots.

Milan Kovacic

Milan Kovacic is a seasoned software consultant with deep expertise in various development domains. On the frontend side, his work is marked by proficiency in React, TypeScript, and the integration of microfrontends. For backend solutions, he mostly relies on the .NET framework, C#—often utilizing microservices architecture. Additionally, Milan is experienced with cloud technologies, regularly working with platforms like AWS and Azure. Beyond traditional development, he places great emphasis on ensuring a smooth developer experience and promotes the use of automation and efficient DevOps practices to streamline development workflows.

These three new members will be joining current members Joel Denning, Carlos Filoteo, and Anthony Frehner to manage GitHub and Slack issues and actively develop new features. We’d also like to give special recognition to contributors from Qiankun for their extensive support over the years.

As part of this team update, we’ll also be creating a public roadmap, providing transparency about Single-Spa's future direction. To maintain transparency and encourage collaboration, we're establishing monthly meetings where the core team will discuss progress, challenges, and ideas. Meeting notes will be publicly available, ensuring that the community is well-informed and able to participate in our discussions.

So stay tuned and join us on this journey shaping the future of microfrontends.

+ + \ No newline at end of file diff --git a/blog/2023/11/07/core-team-meeting-notes/index.html b/blog/2023/11/07/core-team-meeting-notes/index.html index bb35c7a9e..498f59f37 100644 --- a/blog/2023/11/07/core-team-meeting-notes/index.html +++ b/blog/2023/11/07/core-team-meeting-notes/index.html @@ -14,13 +14,13 @@ - - + +
-

November 2023 Core Team Meeting

· One min read

Attendees: Joel Denning, Milan Kovacic, Jake Hayes

November 2023 Roadmap

  1. Release single-spa@6 as latest
  2. Consolidate and update the example projects

Meeting Notes

  • Open pull requests, pressing Github issues
  • Core team vacancy
    • Add Jake Hayes to the #core-team channel, but not officially a core team member
    • Joel will reach out to Ian about joining the core team.
  • Single-spa roadmap
  • Maintenance update/plan

Roadmap Brainstorm

  • Adding unit tests to existing projects
  • SystemJS -> ESM migration
  • create-single-spa update (https://github.com/cruft/cruft)
  • Release single-spa 6 as latest
  • Improve shared dependencies management
  • Autopublishes to npm
  • Server rendering enhancements?
  • Support for NextJS, NuxtJS, Remix, create-react-app, and other build tools
  • Consolidate example projects
  • Feature voting
  • Automated integration tests for popular frameworks
  • Update create-single-spa dependencies
- - +

November 2023 Core Team Meeting

· One min read

Attendees: Joel Denning, Milan Kovacic, Jake Hayes

November 2023 Roadmap

  1. Release single-spa@6 as latest
  2. Consolidate and update the example projects

Meeting Notes

  • Open pull requests, pressing Github issues
  • Core team vacancy
    • Add Jake Hayes to the #core-team channel, but not officially a core team member
    • Joel will reach out to Ian about joining the core team.
  • Single-spa roadmap
  • Maintenance update/plan

Roadmap Brainstorm

  • Adding unit tests to existing projects
  • SystemJS -> ESM migration
  • create-single-spa update (https://github.com/cruft/cruft)
  • Release single-spa 6 as latest
  • Improve shared dependencies management
  • Autopublishes to npm
  • Server rendering enhancements?
  • Support for NextJS, NuxtJS, Remix, create-react-app, and other build tools
  • Consolidate example projects
  • Feature voting
  • Automated integration tests for popular frameworks
  • Update create-single-spa dependencies
+ + \ No newline at end of file diff --git a/blog/2023/11/27/single-spa-6/index.html b/blog/2023/11/27/single-spa-6/index.html index e4be0d9cb..4425a0dc8 100644 --- a/blog/2023/11/27/single-spa-6/index.html +++ b/blog/2023/11/27/single-spa-6/index.html @@ -14,13 +14,13 @@ - - + +
-

single-spa 6

· 10 min read

We have released single-spa@6! The single-spa core team is committed to a stable single-spa runtime with very few breaking changes. We expect over 95% of single-spa users can upgrade easily without altering their changes!

Note from single-spa's creator

On September 22, 2015, I pushed single-spa's initial commit. 8 years later, I am releasing single-spa@6. I am committed to this project and hope to improve and grow it for many years to come. The single-spa core team recently started monthly calls where we discuss important pull requests and roadmap (see first meeting notes). The future of microfrontends still includes single-spa.

On December 11, 2023, the company I founded is launching a product called Baseplate Cloud that offers single-spa hosting. Baseplate Cloud is the single-spa hosting solution born from 4+ years of consulting with various companies who were implementing single-spa. Baseplate CDN is a Cloudflare worker that proxies JS files from cloud storage, provides edge-side import map processing, auto-implements single-spa root configs, and more. Cloudflare edge-side processing provides many performance advantages while retaining the impressive scalability and security of Cloudflare. As a sign of good will towards the open source and single-spa communities, I have kept the baseplate-cloudflare-worker public, with a "Commons Clause" + AGPL 3.0 license. Companies can use and contribute to baseplate-cloudflare-worker freely, as long as they don't try to sell it in competition to Baseplate Cloud. I would appreciate the community's backing and hope Baseplate becomes the way to financially sustain the single-spa project in the long term.

It would mean the world to me if your company or organization considered signing up for Baseplate's paid, private beta launch. We used the revenue the community gave us for single-spa-workshop.com to create Baseplate, and plan to use Baseplate revenue to fund maintenance of single-spa's open source ecosystem. The Baseplate team is proudly a worker-owned business that has not sacrificed ownership of the company to outside investors. We built Baseplate with the revenue the community gave us via single-spa-workshop.com as the initial funding, which is only a few thousand dollars a month. The best Christmas present I could receive this year is a few dozen companies signing up immediately and putting Baseplate on their Q1 or Q2 roadmaps for 2024. I am asking for corporate partners to ensure long term financial sustainability so that single-spa will remain a good microfrontends solution for years to come.

Contact Baseplate Sales

Summary

single-spa@6 provides advanced features such as async navigation cancelation and parcel-only mode (via patchHistoryApi), while also updating the package configuration to use modern NodeJS and bundler features like package entrypoints and "type": "module". We also updated urlRerouteOnly to default to true, as it boosts performance and we consider it to be a best practice to author applications that work when urlRerouteOnly is set to true.

Browser Support / IE11

We still support IE11 and the same versions of NodeJS (at least as old as Node 10). However, we've changed our defaults such that IE11 is only supported with some additional configuration, as described in the IE11 Migration section below.

For the single-spa npm package, we plan single-spa@6 to be the last major version that supports IE11. In general, we've released a major version of single-spa every 1-2 years. We do not have a concrete timeline or plans for single-spa@7, and will accept community contributions for any important patches to single-spa@6 after single-spa@7 is released.

For all surrounding packages (single-spa-react, single-spa-vue, systemjs-webpack-interop, single-spa-angular, etc) we may begin dropping IE11 support sooner than the single-spa@7 release. As we do so, we'll create documentation that lists the latest versions of packages that support IE11, and also accept community contributions to those versions for important updates.

The motivation for dropping IE11 support over time is to allow our core team to spend less time on supporting a nine year old browser and more time pushing the single-spa ecosystem forward. Also, we believe that the majority of end users should not suffer performance penalties because of a small percentage of users still using IE11. Slowly dropping IE11 also will also allow us to focus on a systemjs-less future for single-spa, where native browser modules are used rather than systemjs modules.

We understand and sympathize with companies who cannot drop IE11 support now, which is why we're supporting IE11 even though much of the ecosystem has already dropped it (Vue 3 does not support ie11, Angular only does after some effort, and even webpack 5 requires extra configuration to get it working in IE11). Many of single-spa's users are large financial institutions, large ecommerce companies, and other very large companies, where dropping IE11 support is a long process that involves many dozens of people from separate departments. We encourage single-spa users to start the conversations at your companies about dropping IE11 support, even if they take a long time or result in a decision to drop IE11 years in the future.

Migration

We think that >95% of single-spa users will be able to safely upgrade with no code changes. The breaking changes below are largely related to advanced features that most single-spa users do not use.

Steps

  1. If using SystemJS + import maps, change the URL of single-spa in your import map:
<script type="systemjs-importmap">
{
"imports": {
- "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.2/lib/system/single-spa.min.js"
+ "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es2015/system/single-spa.min.js"
}
}
</script>
  1. In all your microfrontends (including root config), upgrade single-spa:
npm install single-spa@6
yarn add single-spa@6
pnpm install single-spa@6
  1. Test things out.
  2. If your app is not re-rendering during route transitions like it should (this is most common for Angular apps, but can happen in other frameworks to if you do a route transition without modifying the URL), change your root config's call to start()
import { start } from 'single-spa';

// DO NOT MAKE THIS CHANGE unless you are experiencing problems.
- start();
+ start({
+ urlRerouteOnly: false
+ });

That's it! If you experience other issues, let us know in Github issues.

IE11

If you support IE11, use the es5 version of single-spa rather than es2015. The method of doing this depends on your configuration - some common ones are explained below:

Import Maps

<script type="systemjs-importmap">
{
"imports": {
- "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es2015/system/single-spa.min.js"
+ "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es5/system/single-spa.min.js"
}
}
</script>

Webpack

If externalizing single-spa in your webpack config (very common, and the default with webpack-config-single-spa), you do not need to make the changes below. Otherwise, though, these changes will ensure you use the IE11-compatible version of single-spa.

// webpack.config.js
module.exports = {
resolve: {
"alias": {
"single-spa": require.resolve("single-spa/lib/es5/esm/single-spa.min.js")
}
}
}

Rollup

Use https://www.npmjs.com/package/@rollup/plugin-alias to alias the import similarly to webpack

Import

If you do not wish to change bundler configuration, you can modify your import statements.

- import { start } from 'single-spa';
+ import { start } from 'single-spa/lib/es5/esm/single-spa.min.js';

Breaking Changes

  • single-spa's package.json's "main" field now points to an es2015 version of single-spa that does not work in IE11. To support IE11, see section below.
  • Delay patching history api until start() or patchHistoryApi() is called. Github link
  • Do not show start() warning when registerApplication hasn't been called (for parcel-only use cases) Github link
  • Change urlRerouteOnly default value to true. This is a performance boost for most applications, but for some situations can result in single-spa not triggering mounts/unmounts when it should. You can read more about it at https://single-spa.js.org/docs/api#start and https://github.com/single-spa/single-spa/issues/484. Angular users may want to set it to false due to nuanced behavior of angular router. To do so, call start({urlRerouteOnly: false}) in your root config. Github link
  • cancelNavigation(val) no longer cancels navigation if val is falsy Github link
  • single-spa's package.json "type" is now set to "module". This only impacts NodeJS usage of single-spa (not webpack/rollup/browser). Read more at official nodejs docs.
  • single-spa's package.json now has "exports". This changes which of single-spa's bundles are used by NodeJS, webpack, and rollup. The main change is that webpack / rollup will now use an IE11-incompatible (es2015) version of single-spa. See IE11 section below for more details. Additionally, an ESM version of single-spa will now be used when loaded via import('single-spa') or import 'single-spa';, whereas a UMD version will be used when calling require("single-spa'). To avoid the dual package hazard, only use import or require to load single-spa in your NodeJS code - do not use both. You can see the package exports at https://github.com/single-spa/single-spa/blob/dea22f1aac39777a07252897ae625ab1d8313e9d/package.json#L8-L25.
  • The published umd builds now have .cjs extensions rather than .js, since all .js files in the single-spa package are assumed to be ESM.
File structure published to npm

# Before
# See https://www.jsdelivr.com/package/npm/single-spa?path=lib&version=5.9.3
lib/
# IE11
umd/
single-spa.dev.js
single-spa.min.js
# IE11
esm/
single-spa.dev.js
single-spa.min.js
# IE11
system/
single-spa.dev.js
single-spa.min.js
# No IE11
es2015/
single-spa.dev.js
single-spa.min.js

# After
# See https://www.jsdelivr.com/package/npm/single-spa?path=lib&version=6.0.0
lib
# IE11
es5/
umd/
single-spa.dev.js
single-spa.min.js
system/
single-spa.dev.js
single-spa.min.js
esm/
single-spa.dev.js
single-spa.min.js
# No IE11
es2015/
umd/
single-spa.dev.cjs
single-spa.min.cjs
system/
single-spa.dev.js
single-spa.min.js
esm/
single-spa.dev.js
single-spa.min.js

Features

Async navigation cancelation

We've added support for async navigation cancelation. To use it, call cancelNavigation(promise) with a promise as an argument. Single-spa will wait until that promise resolves/rejects before proceeding with navigation. If the promise resolves with a truthy value, navigation is canceled. If the promise resolves with a falsy value or rejects, navigation is not canceled. Github link

window.addEventListener("single-spa:before-routing-event", evt => {
if (evt.detail.oldUrl === "/settings") {
evt.detail.cancelNavigation(checkSettingsOkay())
}
})

async function checkSettingsOkay() {
const response = await fetch('/api/settings-okay')
if (response.ok) {
return true;
} else {
alert("Please fix your settings before leaving the page");
return false
}
}

New patchHistoryApi

We've exposed a new patchHistoryApi() api. This lets you use single-spa's modified implementations of pushState/replaceState/popstate/hashchange without using single-spa applications. This is intended to be used by single-spa users who exclusively use single-spa parcels, rather than applications Github link

import { patchHistoryApi, mountRootParcel } from 'single-spa';

patchHistoryApi({
urlRerouteOnly: true
})

// now you don't need to call start() if you're only using parcels

mountRootParcel(parcelConfig, parcelProps)

New profiler

We've implemented a profiler that is available within the dev builds of single-spa@6. A visualization of the profiler is available in the single-spa-inspector@0.6.0 browser extension:

  • Implement profiler for lifecycle events. by @joeldenning in #868
  • Implement parcel and routing profiler events. by @joeldenning in #903
Screenshot of single-spa-inspector profiler tab

Fixes

  • Fix regression with parcel name Github link
  • Ensure all reroute promises (triggerAppChange() return value) resolve even during cancelation Github link
  • fix(types): fix SingleSpaAppsByNewStatus interface by @FelixGraf in #988
  • Ensure app is unmounted during unregisterApplication. Resolves #871 by @joeldenning in #875

Maintenance

  • Switch from yarn -> pnpm Github link
  • Upgrade all dependencies Github link
  • Upgrade husky from v4 to v7. Use pinst to avoid issues with yarn 2 users as described in https://typicode.github.io/husky/#/?id=yarn-2 Github link
  • Upgrade to Jest 27. Fix tests to work with Jest 27. Github link
  • Upgrade to Node 16. Fix tests to work with Node 16 (Fix tests in Node 15 #652) Github link
  • Run upgraded version of prettier on all files. Github link
  • Remove unused babel-eslint dependency Github link
  • Upgrade to new package names for all rollup plugins. (@rollup/plugin-node-resolve instead of rollup-plugin-node-resolve) Github link
  • Remove file size impact workflow since it's broken. by @joeldenning in #879
  • Remove console.log in test by @joeldenning in #878
- - +

single-spa 6

· 10 min read

We have released single-spa@6! The single-spa core team is committed to a stable single-spa runtime with very few breaking changes. We expect over 95% of single-spa users can upgrade easily without altering their changes!

Note from single-spa's creator

On September 22, 2015, I pushed single-spa's initial commit. 8 years later, I am releasing single-spa@6. I am committed to this project and hope to improve and grow it for many years to come. The single-spa core team recently started monthly calls where we discuss important pull requests and roadmap (see first meeting notes). The future of microfrontends still includes single-spa.

On December 11, 2023, the company I founded is launching a product called Baseplate Cloud that offers single-spa hosting. Baseplate Cloud is the single-spa hosting solution born from 4+ years of consulting with various companies who were implementing single-spa. Baseplate CDN is a Cloudflare worker that proxies JS files from cloud storage, provides edge-side import map processing, auto-implements single-spa root configs, and more. Cloudflare edge-side processing provides many performance advantages while retaining the impressive scalability and security of Cloudflare. As a sign of good will towards the open source and single-spa communities, I have kept the baseplate-cloudflare-worker public, with a "Commons Clause" + AGPL 3.0 license. Companies can use and contribute to baseplate-cloudflare-worker freely, as long as they don't try to sell it in competition to Baseplate Cloud. I would appreciate the community's backing and hope Baseplate becomes the way to financially sustain the single-spa project in the long term.

It would mean the world to me if your company or organization considered signing up for Baseplate's paid, private beta launch. We used the revenue the community gave us for single-spa-workshop.com to create Baseplate, and plan to use Baseplate revenue to fund maintenance of single-spa's open source ecosystem. The Baseplate team is proudly a worker-owned business that has not sacrificed ownership of the company to outside investors. We built Baseplate with the revenue the community gave us via single-spa-workshop.com as the initial funding, which is only a few thousand dollars a month. The best Christmas present I could receive this year is a few dozen companies signing up immediately and putting Baseplate on their Q1 or Q2 roadmaps for 2024. I am asking for corporate partners to ensure long term financial sustainability so that single-spa will remain a good microfrontends solution for years to come.

Contact Baseplate Sales

Summary

single-spa@6 provides advanced features such as async navigation cancelation and parcel-only mode (via patchHistoryApi), while also updating the package configuration to use modern NodeJS and bundler features like package entrypoints and "type": "module". We also updated urlRerouteOnly to default to true, as it boosts performance and we consider it to be a best practice to author applications that work when urlRerouteOnly is set to true.

Browser Support / IE11

We still support IE11 and the same versions of NodeJS (at least as old as Node 10). However, we've changed our defaults such that IE11 is only supported with some additional configuration, as described in the IE11 Migration section below.

For the single-spa npm package, we plan single-spa@6 to be the last major version that supports IE11. In general, we've released a major version of single-spa every 1-2 years. We do not have a concrete timeline or plans for single-spa@7, and will accept community contributions for any important patches to single-spa@6 after single-spa@7 is released.

For all surrounding packages (single-spa-react, single-spa-vue, systemjs-webpack-interop, single-spa-angular, etc) we may begin dropping IE11 support sooner than the single-spa@7 release. As we do so, we'll create documentation that lists the latest versions of packages that support IE11, and also accept community contributions to those versions for important updates.

The motivation for dropping IE11 support over time is to allow our core team to spend less time on supporting a nine year old browser and more time pushing the single-spa ecosystem forward. Also, we believe that the majority of end users should not suffer performance penalties because of a small percentage of users still using IE11. Slowly dropping IE11 also will also allow us to focus on a systemjs-less future for single-spa, where native browser modules are used rather than systemjs modules.

We understand and sympathize with companies who cannot drop IE11 support now, which is why we're supporting IE11 even though much of the ecosystem has already dropped it (Vue 3 does not support ie11, Angular only does after some effort, and even webpack 5 requires extra configuration to get it working in IE11). Many of single-spa's users are large financial institutions, large ecommerce companies, and other very large companies, where dropping IE11 support is a long process that involves many dozens of people from separate departments. We encourage single-spa users to start the conversations at your companies about dropping IE11 support, even if they take a long time or result in a decision to drop IE11 years in the future.

Migration

We think that >95% of single-spa users will be able to safely upgrade with no code changes. The breaking changes below are largely related to advanced features that most single-spa users do not use.

Steps

  1. If using SystemJS + import maps, change the URL of single-spa in your import map:
<script type="systemjs-importmap">
{
"imports": {
- "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.2/lib/system/single-spa.min.js"
+ "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es2015/system/single-spa.min.js"
}
}
</script>
  1. In all your microfrontends (including root config), upgrade single-spa:
npm install single-spa@6
yarn add single-spa@6
pnpm install single-spa@6
  1. Test things out.
  2. If your app is not re-rendering during route transitions like it should (this is most common for Angular apps, but can happen in other frameworks to if you do a route transition without modifying the URL), change your root config's call to start()
import { start } from 'single-spa';

// DO NOT MAKE THIS CHANGE unless you are experiencing problems.
- start();
+ start({
+ urlRerouteOnly: false
+ });

That's it! If you experience other issues, let us know in Github issues.

IE11

If you support IE11, use the es5 version of single-spa rather than es2015. The method of doing this depends on your configuration - some common ones are explained below:

Import Maps

<script type="systemjs-importmap">
{
"imports": {
- "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es2015/system/single-spa.min.js"
+ "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es5/system/single-spa.min.js"
}
}
</script>

Webpack

If externalizing single-spa in your webpack config (very common, and the default with webpack-config-single-spa), you do not need to make the changes below. Otherwise, though, these changes will ensure you use the IE11-compatible version of single-spa.

// webpack.config.js
module.exports = {
resolve: {
"alias": {
"single-spa": require.resolve("single-spa/lib/es5/esm/single-spa.min.js")
}
}
}

Rollup

Use https://www.npmjs.com/package/@rollup/plugin-alias to alias the import similarly to webpack

Import

If you do not wish to change bundler configuration, you can modify your import statements.

- import { start } from 'single-spa';
+ import { start } from 'single-spa/lib/es5/esm/single-spa.min.js';

Breaking Changes

  • single-spa's package.json's "main" field now points to an es2015 version of single-spa that does not work in IE11. To support IE11, see section below.
  • Delay patching history api until start() or patchHistoryApi() is called. Github link
  • Do not show start() warning when registerApplication hasn't been called (for parcel-only use cases) Github link
  • Change urlRerouteOnly default value to true. This is a performance boost for most applications, but for some situations can result in single-spa not triggering mounts/unmounts when it should. You can read more about it at https://single-spa.js.org/docs/api#start and https://github.com/single-spa/single-spa/issues/484. Angular users may want to set it to false due to nuanced behavior of angular router. To do so, call start({urlRerouteOnly: false}) in your root config. Github link
  • cancelNavigation(val) no longer cancels navigation if val is falsy Github link
  • single-spa's package.json "type" is now set to "module". This only impacts NodeJS usage of single-spa (not webpack/rollup/browser). Read more at official nodejs docs.
  • single-spa's package.json now has "exports". This changes which of single-spa's bundles are used by NodeJS, webpack, and rollup. The main change is that webpack / rollup will now use an IE11-incompatible (es2015) version of single-spa. See IE11 section below for more details. Additionally, an ESM version of single-spa will now be used when loaded via import('single-spa') or import 'single-spa';, whereas a UMD version will be used when calling require("single-spa'). To avoid the dual package hazard, only use import or require to load single-spa in your NodeJS code - do not use both. You can see the package exports at https://github.com/single-spa/single-spa/blob/dea22f1aac39777a07252897ae625ab1d8313e9d/package.json#L8-L25.
  • The published umd builds now have .cjs extensions rather than .js, since all .js files in the single-spa package are assumed to be ESM.
File structure published to npm

# Before
# See https://www.jsdelivr.com/package/npm/single-spa?path=lib&version=5.9.3
lib/
# IE11
umd/
single-spa.dev.js
single-spa.min.js
# IE11
esm/
single-spa.dev.js
single-spa.min.js
# IE11
system/
single-spa.dev.js
single-spa.min.js
# No IE11
es2015/
single-spa.dev.js
single-spa.min.js

# After
# See https://www.jsdelivr.com/package/npm/single-spa?path=lib&version=6.0.0
lib
# IE11
es5/
umd/
single-spa.dev.js
single-spa.min.js
system/
single-spa.dev.js
single-spa.min.js
esm/
single-spa.dev.js
single-spa.min.js
# No IE11
es2015/
umd/
single-spa.dev.cjs
single-spa.min.cjs
system/
single-spa.dev.js
single-spa.min.js
esm/
single-spa.dev.js
single-spa.min.js

Features

Async navigation cancelation

We've added support for async navigation cancelation. To use it, call cancelNavigation(promise) with a promise as an argument. Single-spa will wait until that promise resolves/rejects before proceeding with navigation. If the promise resolves with a truthy value, navigation is canceled. If the promise resolves with a falsy value or rejects, navigation is not canceled. Github link

window.addEventListener("single-spa:before-routing-event", evt => {
if (evt.detail.oldUrl === "/settings") {
evt.detail.cancelNavigation(checkSettingsOkay())
}
})

async function checkSettingsOkay() {
const response = await fetch('/api/settings-okay')
if (response.ok) {
return true;
} else {
alert("Please fix your settings before leaving the page");
return false
}
}

New patchHistoryApi

We've exposed a new patchHistoryApi() api. This lets you use single-spa's modified implementations of pushState/replaceState/popstate/hashchange without using single-spa applications. This is intended to be used by single-spa users who exclusively use single-spa parcels, rather than applications Github link

import { patchHistoryApi, mountRootParcel } from 'single-spa';

patchHistoryApi({
urlRerouteOnly: true
})

// now you don't need to call start() if you're only using parcels

mountRootParcel(parcelConfig, parcelProps)

New profiler

We've implemented a profiler that is available within the dev builds of single-spa@6. A visualization of the profiler is available in the single-spa-inspector@0.6.0 browser extension:

  • Implement profiler for lifecycle events. by @joeldenning in #868
  • Implement parcel and routing profiler events. by @joeldenning in #903
Screenshot of single-spa-inspector profiler tab

Fixes

  • Fix regression with parcel name Github link
  • Ensure all reroute promises (triggerAppChange() return value) resolve even during cancelation Github link
  • fix(types): fix SingleSpaAppsByNewStatus interface by @FelixGraf in #988
  • Ensure app is unmounted during unregisterApplication. Resolves #871 by @joeldenning in #875

Maintenance

  • Switch from yarn -> pnpm Github link
  • Upgrade all dependencies Github link
  • Upgrade husky from v4 to v7. Use pinst to avoid issues with yarn 2 users as described in https://typicode.github.io/husky/#/?id=yarn-2 Github link
  • Upgrade to Jest 27. Fix tests to work with Jest 27. Github link
  • Upgrade to Node 16. Fix tests to work with Node 16 (Fix tests in Node 15 #652) Github link
  • Run upgraded version of prettier on all files. Github link
  • Remove unused babel-eslint dependency Github link
  • Upgrade to new package names for all rollup plugins. (@rollup/plugin-node-resolve instead of rollup-plugin-node-resolve) Github link
  • Remove file size impact workflow since it's broken. by @joeldenning in #879
  • Remove console.log in test by @joeldenning in #878
+ + \ No newline at end of file diff --git a/blog/2023/12/12/core-team-meeting-notes/index.html b/blog/2023/12/12/core-team-meeting-notes/index.html new file mode 100644 index 000000000..7f0b851c9 --- /dev/null +++ b/blog/2023/12/12/core-team-meeting-notes/index.html @@ -0,0 +1,26 @@ + + + + + +December 2023 Core Team Meeting | single-spa + + + + + + + + + + + + + + +
+

December 2023 Core Team Meeting

· One min read

Attendees: Artur Androsovych, Ian Bacher, Jake Hayes, Milan Kovacic

Achievements and Progress from November

  • Successfully released single-spa version 6, marking a significant milestone in the project's development

December 2023 Roadmap

  1. Update dependencies, and set up automation, starting with create-single-spa
  2. Continue work on the consolidation and updates of the example projects

Meeting Notes

  • Welcomed Ian Bacher as a new addition to the core team.
  • Reviewed single-spa roadmap

Initiatives and Goals

  • Adding unit tests to existing projects
  • SystemJS -> ESM migration
  • create-single-spa update (https://github.com/cruft/cruft)
  • Release single-spa 6 as latest ✅
  • Improve shared dependencies management
  • Autopublishes to npm
  • Server rendering enhancements?
  • Support for NextJS, NuxtJS, Remix, create-react-app, and other build tools
  • Consolidate example projects 🚧
  • Feature voting
  • Automated integration tests for popular frameworks
  • Update create-single-spa dependencies 🚧
+ + + + \ No newline at end of file diff --git a/blog/archive/index.html b/blog/archive/index.html index 78e3e988f..43e545336 100644 --- a/blog/archive/index.html +++ b/blog/archive/index.html @@ -14,13 +14,13 @@ - - + + - - + + + \ No newline at end of file diff --git a/blog/atom.xml b/blog/atom.xml index daff2ba20..e3adc6a8a 100644 --- a/blog/atom.xml +++ b/blog/atom.xml @@ -2,11 +2,19 @@ https://single-spa.js.org/blog single-spa Blog - 2023-11-27T00:00:00.000Z + 2023-12-12T00:00:00.000Z https://github.com/jpmonette/feed single-spa Blog https://single-spa.js.org/img/single-spa-mark-magenta.svg + + <![CDATA[December 2023 Core Team Meeting]]> + https://single-spa.js.org/blog/2023/12/12/core-team-meeting-notes + + 2023-12-12T00:00:00.000Z + + Attendees: Artur Androsovych, Ian Bacher, Jake Hayes, Milan Kovacic

Achievements and Progress from November

  • Successfully released single-spa version 6, marking a significant milestone in the project's development

December 2023 Roadmap

  1. Update dependencies, and set up automation, starting with create-single-spa
  2. Continue work on the consolidation and updates of the example projects

Meeting Notes

  • Welcomed Ian Bacher as a new addition to the core team.
  • Reviewed single-spa roadmap

Initiatives and Goals

  • Adding unit tests to existing projects
  • SystemJS -> ESM migration
  • create-single-spa update (https://github.com/cruft/cruft)
  • Release single-spa 6 as latest ✅
  • Improve shared dependencies management
  • Autopublishes to npm
  • Server rendering enhancements?
  • Support for NextJS, NuxtJS, Remix, create-react-app, and other build tools
  • Consolidate example projects 🚧
  • Feature voting
  • Automated integration tests for popular frameworks
  • Update create-single-spa dependencies 🚧
]]>
+
<![CDATA[single-spa 6]]> https://single-spa.js.org/blog/2023/11/27/single-spa-6 diff --git a/blog/index.html b/blog/index.html index 7ca9788a3..34c3705df 100644 --- a/blog/index.html +++ b/blog/index.html @@ -14,13 +14,13 @@ - - + +
-

· 10 min read

We have released single-spa@6! The single-spa core team is committed to a stable single-spa runtime with very few breaking changes. We expect over 95% of single-spa users can upgrade easily without altering their changes!

Note from single-spa's creator

On September 22, 2015, I pushed single-spa's initial commit. 8 years later, I am releasing single-spa@6. I am committed to this project and hope to improve and grow it for many years to come. The single-spa core team recently started monthly calls where we discuss important pull requests and roadmap (see first meeting notes). The future of microfrontends still includes single-spa.

On December 11, 2023, the company I founded is launching a product called Baseplate Cloud that offers single-spa hosting. Baseplate Cloud is the single-spa hosting solution born from 4+ years of consulting with various companies who were implementing single-spa. Baseplate CDN is a Cloudflare worker that proxies JS files from cloud storage, provides edge-side import map processing, auto-implements single-spa root configs, and more. Cloudflare edge-side processing provides many performance advantages while retaining the impressive scalability and security of Cloudflare. As a sign of good will towards the open source and single-spa communities, I have kept the baseplate-cloudflare-worker public, with a "Commons Clause" + AGPL 3.0 license. Companies can use and contribute to baseplate-cloudflare-worker freely, as long as they don't try to sell it in competition to Baseplate Cloud. I would appreciate the community's backing and hope Baseplate becomes the way to financially sustain the single-spa project in the long term.

It would mean the world to me if your company or organization considered signing up for Baseplate's paid, private beta launch. We used the revenue the community gave us for single-spa-workshop.com to create Baseplate, and plan to use Baseplate revenue to fund maintenance of single-spa's open source ecosystem. The Baseplate team is proudly a worker-owned business that has not sacrificed ownership of the company to outside investors. We built Baseplate with the revenue the community gave us via single-spa-workshop.com as the initial funding, which is only a few thousand dollars a month. The best Christmas present I could receive this year is a few dozen companies signing up immediately and putting Baseplate on their Q1 or Q2 roadmaps for 2024. I am asking for corporate partners to ensure long term financial sustainability so that single-spa will remain a good microfrontends solution for years to come.

Contact Baseplate Sales

Summary

single-spa@6 provides advanced features such as async navigation cancelation and parcel-only mode (via patchHistoryApi), while also updating the package configuration to use modern NodeJS and bundler features like package entrypoints and "type": "module". We also updated urlRerouteOnly to default to true, as it boosts performance and we consider it to be a best practice to author applications that work when urlRerouteOnly is set to true.

Browser Support / IE11

We still support IE11 and the same versions of NodeJS (at least as old as Node 10). However, we've changed our defaults such that IE11 is only supported with some additional configuration, as described in the IE11 Migration section below.

For the single-spa npm package, we plan single-spa@6 to be the last major version that supports IE11. In general, we've released a major version of single-spa every 1-2 years. We do not have a concrete timeline or plans for single-spa@7, and will accept community contributions for any important patches to single-spa@6 after single-spa@7 is released.

For all surrounding packages (single-spa-react, single-spa-vue, systemjs-webpack-interop, single-spa-angular, etc) we may begin dropping IE11 support sooner than the single-spa@7 release. As we do so, we'll create documentation that lists the latest versions of packages that support IE11, and also accept community contributions to those versions for important updates.

The motivation for dropping IE11 support over time is to allow our core team to spend less time on supporting a nine year old browser and more time pushing the single-spa ecosystem forward. Also, we believe that the majority of end users should not suffer performance penalties because of a small percentage of users still using IE11. Slowly dropping IE11 also will also allow us to focus on a systemjs-less future for single-spa, where native browser modules are used rather than systemjs modules.

We understand and sympathize with companies who cannot drop IE11 support now, which is why we're supporting IE11 even though much of the ecosystem has already dropped it (Vue 3 does not support ie11, Angular only does after some effort, and even webpack 5 requires extra configuration to get it working in IE11). Many of single-spa's users are large financial institutions, large ecommerce companies, and other very large companies, where dropping IE11 support is a long process that involves many dozens of people from separate departments. We encourage single-spa users to start the conversations at your companies about dropping IE11 support, even if they take a long time or result in a decision to drop IE11 years in the future.

Migration

We think that >95% of single-spa users will be able to safely upgrade with no code changes. The breaking changes below are largely related to advanced features that most single-spa users do not use.

Steps

  1. If using SystemJS + import maps, change the URL of single-spa in your import map:
<script type="systemjs-importmap">
{
"imports": {
- "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.2/lib/system/single-spa.min.js"
+ "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es2015/system/single-spa.min.js"
}
}
</script>
  1. In all your microfrontends (including root config), upgrade single-spa:
npm install single-spa@6
yarn add single-spa@6
pnpm install single-spa@6
  1. Test things out.
  2. If your app is not re-rendering during route transitions like it should (this is most common for Angular apps, but can happen in other frameworks to if you do a route transition without modifying the URL), change your root config's call to start()
import { start } from 'single-spa';

// DO NOT MAKE THIS CHANGE unless you are experiencing problems.
- start();
+ start({
+ urlRerouteOnly: false
+ });

That's it! If you experience other issues, let us know in Github issues.

IE11

If you support IE11, use the es5 version of single-spa rather than es2015. The method of doing this depends on your configuration - some common ones are explained below:

Import Maps

<script type="systemjs-importmap">
{
"imports": {
- "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es2015/system/single-spa.min.js"
+ "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es5/system/single-spa.min.js"
}
}
</script>

Webpack

If externalizing single-spa in your webpack config (very common, and the default with webpack-config-single-spa), you do not need to make the changes below. Otherwise, though, these changes will ensure you use the IE11-compatible version of single-spa.

// webpack.config.js
module.exports = {
resolve: {
"alias": {
"single-spa": require.resolve("single-spa/lib/es5/esm/single-spa.min.js")
}
}
}

Rollup

Use https://www.npmjs.com/package/@rollup/plugin-alias to alias the import similarly to webpack

Import

If you do not wish to change bundler configuration, you can modify your import statements.

- import { start } from 'single-spa';
+ import { start } from 'single-spa/lib/es5/esm/single-spa.min.js';

Breaking Changes

  • single-spa's package.json's "main" field now points to an es2015 version of single-spa that does not work in IE11. To support IE11, see section below.
  • Delay patching history api until start() or patchHistoryApi() is called. Github link
  • Do not show start() warning when registerApplication hasn't been called (for parcel-only use cases) Github link
  • Change urlRerouteOnly default value to true. This is a performance boost for most applications, but for some situations can result in single-spa not triggering mounts/unmounts when it should. You can read more about it at https://single-spa.js.org/docs/api#start and https://github.com/single-spa/single-spa/issues/484. Angular users may want to set it to false due to nuanced behavior of angular router. To do so, call start({urlRerouteOnly: false}) in your root config. Github link
  • cancelNavigation(val) no longer cancels navigation if val is falsy Github link
  • single-spa's package.json "type" is now set to "module". This only impacts NodeJS usage of single-spa (not webpack/rollup/browser). Read more at official nodejs docs.
  • single-spa's package.json now has "exports". This changes which of single-spa's bundles are used by NodeJS, webpack, and rollup. The main change is that webpack / rollup will now use an IE11-incompatible (es2015) version of single-spa. See IE11 section below for more details. Additionally, an ESM version of single-spa will now be used when loaded via import('single-spa') or import 'single-spa';, whereas a UMD version will be used when calling require("single-spa'). To avoid the dual package hazard, only use import or require to load single-spa in your NodeJS code - do not use both. You can see the package exports at https://github.com/single-spa/single-spa/blob/dea22f1aac39777a07252897ae625ab1d8313e9d/package.json#L8-L25.
  • The published umd builds now have .cjs extensions rather than .js, since all .js files in the single-spa package are assumed to be ESM.
File structure published to npm

# Before
# See https://www.jsdelivr.com/package/npm/single-spa?path=lib&version=5.9.3
lib/
# IE11
umd/
single-spa.dev.js
single-spa.min.js
# IE11
esm/
single-spa.dev.js
single-spa.min.js
# IE11
system/
single-spa.dev.js
single-spa.min.js
# No IE11
es2015/
single-spa.dev.js
single-spa.min.js

# After
# See https://www.jsdelivr.com/package/npm/single-spa?path=lib&version=6.0.0
lib
# IE11
es5/
umd/
single-spa.dev.js
single-spa.min.js
system/
single-spa.dev.js
single-spa.min.js
esm/
single-spa.dev.js
single-spa.min.js
# No IE11
es2015/
umd/
single-spa.dev.cjs
single-spa.min.cjs
system/
single-spa.dev.js
single-spa.min.js
esm/
single-spa.dev.js
single-spa.min.js

Features

Async navigation cancelation

We've added support for async navigation cancelation. To use it, call cancelNavigation(promise) with a promise as an argument. Single-spa will wait until that promise resolves/rejects before proceeding with navigation. If the promise resolves with a truthy value, navigation is canceled. If the promise resolves with a falsy value or rejects, navigation is not canceled. Github link

window.addEventListener("single-spa:before-routing-event", evt => {
if (evt.detail.oldUrl === "/settings") {
evt.detail.cancelNavigation(checkSettingsOkay())
}
})

async function checkSettingsOkay() {
const response = await fetch('/api/settings-okay')
if (response.ok) {
return true;
} else {
alert("Please fix your settings before leaving the page");
return false
}
}

New patchHistoryApi

We've exposed a new patchHistoryApi() api. This lets you use single-spa's modified implementations of pushState/replaceState/popstate/hashchange without using single-spa applications. This is intended to be used by single-spa users who exclusively use single-spa parcels, rather than applications Github link

import { patchHistoryApi, mountRootParcel } from 'single-spa';

patchHistoryApi({
urlRerouteOnly: true
})

// now you don't need to call start() if you're only using parcels

mountRootParcel(parcelConfig, parcelProps)

New profiler

We've implemented a profiler that is available within the dev builds of single-spa@6. A visualization of the profiler is available in the single-spa-inspector@0.6.0 browser extension:

  • Implement profiler for lifecycle events. by @joeldenning in #868
  • Implement parcel and routing profiler events. by @joeldenning in #903
Screenshot of single-spa-inspector profiler tab

Fixes

  • Fix regression with parcel name Github link
  • Ensure all reroute promises (triggerAppChange() return value) resolve even during cancelation Github link
  • fix(types): fix SingleSpaAppsByNewStatus interface by @FelixGraf in #988
  • Ensure app is unmounted during unregisterApplication. Resolves #871 by @joeldenning in #875

Maintenance

  • Switch from yarn -> pnpm Github link
  • Upgrade all dependencies Github link
  • Upgrade husky from v4 to v7. Use pinst to avoid issues with yarn 2 users as described in https://typicode.github.io/husky/#/?id=yarn-2 Github link
  • Upgrade to Jest 27. Fix tests to work with Jest 27. Github link
  • Upgrade to Node 16. Fix tests to work with Node 16 (Fix tests in Node 15 #652) Github link
  • Run upgraded version of prettier on all files. Github link
  • Remove unused babel-eslint dependency Github link
  • Upgrade to new package names for all rollup plugins. (@rollup/plugin-node-resolve instead of rollup-plugin-node-resolve) Github link
  • Remove file size impact workflow since it's broken. by @joeldenning in #879
  • Remove console.log in test by @joeldenning in #878

· One min read

Attendees: Joel Denning, Milan Kovacic, Jake Hayes

November 2023 Roadmap

  1. Release single-spa@6 as latest
  2. Consolidate and update the example projects

Meeting Notes

  • Open pull requests, pressing Github issues
  • Core team vacancy
    • Add Jake Hayes to the #core-team channel, but not officially a core team member
    • Joel will reach out to Ian about joining the core team.
  • Single-spa roadmap
  • Maintenance update/plan

Roadmap Brainstorm

  • Adding unit tests to existing projects
  • SystemJS -> ESM migration
  • create-single-spa update (https://github.com/cruft/cruft)
  • Release single-spa 6 as latest
  • Improve shared dependencies management
  • Autopublishes to npm
  • Server rendering enhancements?
  • Support for NextJS, NuxtJS, Remix, create-react-app, and other build tools
  • Consolidate example projects
  • Feature voting
  • Automated integration tests for popular frameworks
  • Update create-single-spa dependencies

· 2 min read

Please welcome the latest additions to the Single-Spa core team. These talented individuals bring fresh energy and a wealth of expertise and are poised to revitalize and expand the Single-Spa project.

Meet the Team

Artur Androsovych

Artur Androsovych is a Google Developer Expert in Angular and an open-source contributor who has been focusing on runtime performance and teaching teams about Angular internals for the past few years. He has maintained the single-spa-angular project for years, and we're excited for him to join the core team.

Roberto Mosca

Roberto is a Principal Software Engineer at TravelPerk (www.travelperk.com), where he is the go-to person for all things related to their frontend platform. With a full-stack background, he's got his hands on a variety of tech — from Python to NodeJS, React, Webpack... Before diving into the world of business travel, he was immersed in biomedical research as a bioinformatician. When he is not in front of a screen, you’ll find him swimming, cooking up some Italian delicacies, or tinkering with his Rubik’s cube-solving robots.

Milan Kovacic

Milan Kovacic is a seasoned software consultant with deep expertise in various development domains. On the frontend side, his work is marked by proficiency in React, TypeScript, and the integration of microfrontends. For backend solutions, he mostly relies on the .NET framework, C#—often utilizing microservices architecture. Additionally, Milan is experienced with cloud technologies, regularly working with platforms like AWS and Azure. Beyond traditional development, he places great emphasis on ensuring a smooth developer experience and promotes the use of automation and efficient DevOps practices to streamline development workflows.

These three new members will be joining current members Joel Denning, Carlos Filoteo, and Anthony Frehner to manage GitHub and Slack issues and actively develop new features. We’d also like to give special recognition to contributors from Qiankun for their extensive support over the years.

As part of this team update, we’ll also be creating a public roadmap, providing transparency about Single-Spa's future direction. To maintain transparency and encourage collaboration, we're establishing monthly meetings where the core team will discuss progress, challenges, and ideas. Meeting notes will be publicly available, ensuring that the community is well-informed and able to participate in our discussions.

So stay tuned and join us on this journey shaping the future of microfrontends.

· 2 min read

We are happy to announce that the single-spa core team is opening applications for two new members! As a core team member, you will help us maintain and guide the growing single-spa ecosystem by responding to Github and Slack issues, implementing new features, and helping to create the roadmap for the future of single-spa.

Our current core team consists of Joel Denning, Carlos Filoteo, and Anthony Frehner. We also thank Justin McMurdie and Bret Little for their valuable contributions during their time with the single-spa core team.

Single-spa owes its success to the people and companies who support it. If you want to contribute but aren’t able to join the core team, consider applying to be a maintainer. Maintainers are volunteers who receive Github write access to specific subprojects within the ecosystem. If you’re a manager at a company that uses single-spa, we encourage you to allow your developers to volunteer their time as maintainers or core team members so that the project can continue to grow and develop.

The single-spa core team and maintenance are unpaid volunteer positions. Looking ahead, if any core team members or maintainers are interested in new paid roles, Convex Cooperative (the company Joel works for) will be considering hiring new developers from the single-spa team. It’s important to state that Convex will not poach developers away from companies who allow their developers to help us maintain single-spa—our priority is nurturing the growth and development of the single-spa ecosystem.

If interested in applying to be a single-spa maintainer or core team member, please fill out the following Google Form before September 1, 2023:

https://docs.google.com/forms/d/1zIfP2kYjNBCi-qjKf9T1-sNT1VLSXTB9twwX_mxKYxU/edit

· 5 min read

Today we released single-spa@5.0.0.

Here are the highlights:

Release notes here

Migration from 4 to 5

For every user we're aware of, you do not need to change anything in your code in order to upgrade to single-spa@5. The breaking changes listed in the release notes are the removal of features that were originally used by Canopy Tax, but were never documented.

If installing from npm, you can simply npm install --save single-spa@5.0.0 or yarn add single-spa@5.0.0.

Alternatively, single-spa is available on cdnjs, jsdelivr, and unpkg.

The single-spa core team is committed to treating our users well, which includes not introducing massive breaking changes. The core single-spa API has not seen massive breaking changes since single-spa@3 in August 2016. We have added features and improved things, but single-spa is a stable technology. We are committed to maintaining it, documenting it, and adjusting it as technologies like in-browser modules become more and more popular and viable.

Performance improvements

The ESM version of single-spa@4 was 23.8kb (7.2kb gzipped). That was improved in single-spa@5 to 15.5kb (5.1kb gzipped). We did this by optimizing our build process and removing unused features.

single-spa CLI

Since single-spa's inception, bundler configuration has been a huge source of user pain. We have heard this pain and implemented create-single-spa, which creates (and sometimes can update) repositories that are ready to be used as single-spa microfrontends. For Angular and Vue, the official CLIs are used with a few extra plugins automatically installed. For React, a default webpack config with decent eslint / prettier defaults is set up.

Additionally, we have added a lot of documentation for webpack in The Recommended Setup.

Tutorial videos

We understand that single-spa is more than just a library - it is an architecture. The single-spa library itself is the core, but the surrounding ecosystem of concepts and libraries are equally important to successfully migrating to single-spa and having it work for you. As such, we have created a Youtube playlist, currently consisting of seven videos, to help you get started.

Youtube playlist / Bilibili space

The videos currently cover the following topics:

  • What are Microfrontends?
  • In-browser vs build-time JavaScript modules
  • Import Maps
  • Local Development with single-spa and import maps
  • Deploying Microfrontends / Continuous Integration (CI)
  • SystemJS intro
  • Lazy Loading
  • Bundlers, webpack, and rollup.

New example repositories

What started out as Canopy Tax's special sauce for independently deployed frontend microservices is now fully accessible to the public with our new set of example repos. We have a React example, a Vue example, and a polyglot (multiple framework) example. We hope to add an Angular example, after we achieve support for Angular 9. These example repositories are actively watched and maintained by the single-spa core team, and reflect our current opinions on the best, production-viable way to do microfrontends.

Furthermore, we have deployed each of the examples to our new domains:

Documentation overhaul

We removed several dated documentation pages, and added several that were very much lacking. Here are a few pages that give you the most bang for your buck:

Development builds and error codes

Taking inspiration from the react development and production builds, we now publish to NPM both development and production builds in the following formats: UMD, ESM, and System.register.

You can see the published build files here. The .dev.js files provide full debugging information in the browser console, whereas the .min.js files give you a numeric error code and a link to a documentation page that explains the error. We hope that these error codes and documentation for them will improve discoverability of relevant documentation when you're setting up single-spa.

An example of these new documentation pages for error codes is found here.

Governance

Some of you may have noticed that we recently moved all github repos from https://github.com/CanopyTax to https://github.com/single-spa. Canopy Tax was the company where single-spa was first authored, but as a core team we asked to move ownership and governance of the projects to an organization fully managed by the open source community. In agreement with Canopy, we made that change.

This change does not mean anything drastic for single-spa. Its license was and is MIT, and we have no plans to do anything with the project besides make it better.

Where next?

We are actively translating the single-spa documentation to Chinese, and hope to add other languages soon. We will add full Angular 9 support soon, and hope to add server rendering in an upcoming release.

Please contribute to our code and ecosystem, join our single-spa slack channel, follow our official Twitter account, and contribute to our open collective. The single-spa core team all have full-time jobs and maintain this project on a volunteer basis.

· 2 min read

Background

For a long time, Canopy has had the benefit of using a tool called sofe inspector (note: this is an out-of-date version of it) to list, override, and interact with single-spa applications. There has always been a desire to figure out how to share this tool so others can benefit as well.

With that in mind, I'm proud to announce an initial release for single-spa Inspector! single-spa Inspector is a Firefox and Chrome extension, much like React/Vue devtools, that allows you see and interact with your single-spa applications and configuration.

Current Inspector Features

  • List registered applications
  • Show application status
  • Force an app to mount or unmount
  • Hover over an app name to have an "inspect element"-like view of your apps (Overlays)

(Note: Overlays require a small update to your code, but should hopefully be simple! See how to configure app overlays)

The single-spa Inspector will only work with single-spa versions 4.1 and higher, since we had to expose and add some functionality to the single-spa library itself in order to implement these features.

single-spa 4.1

single-spa 4.1 was released, which includes a couple of key updates:

  1. Support for single-spa Inspector
  2. ESM bundle output
  3. Simpmlified test configuration for developers/contributors to single-spa

For most people, ESM (EcmaScript Module) support shouldn't affect how you use single-spa, but for those looking to play around with modules or other advanced Javascript things, it's a welcome addition.

We also changed our test suite to purely use Jest instead of Saucelabs, and hopefully false positive "failing" tests on pull requests will be a thing of the past.

Help Wanted!

If you would like to suggest a new feature for single-spa Inspector, report a bug, improve our (admittedly horrible and hopefully temporary) UI/UX, or add features, please see the github repo and hack away!

We also hope to update some of our example repos to the lastest single-spa so that anyone with the extension installed can test out the features and see how to implement overlays. But this process will go faster if someone wants to help out. :)

Thank you!

· 4 min read

Ever since single-spa@1.0.0, the single-spa team has been dedicated to bringing microservices to the frontend. We have made it possible for AngularJS, React, Angular, Vue, and other frameworks to coexist side by side in the same page.

And with the release of version 4, I’m pleased to announce that single-spa is expanding that effort so that individual components written with different frameworks can interoperate. It is new terrain for the single-spa community, which previously had focused on getting large applications to interoperate with each other, instead of the individual components.

Another way to do framework agnostic components?

For those familiar with web components and custom elements, you may be wondering why a JavaScript library would try to do what browsers are starting natively to do.

And as one of the contributors to the custom elements polyfill, let me be the first one to say that we did not make this decision lightly.

If you’re interested in diving into the details, check out One Company’s Relationship With Custom Elements, which explains some of the difficulties we’ve been through with web components and custom elements.

TLDR: React and some other frameworks don’t interop with custom elements very well. Additionally dealing with inner HTML, attributes vs properties, and customized builtins can be a pain.

Okay but you haven’t told me what a single-spa parcel is

A parcel is single-spa’s way of building a component in one framework and using it in another.

To implement a parcel, just create a JavaScript object that has 3–4 functions on it. We call this JavaScript object a parcel config and there are three required functions to implement: bootstrap, mount, and unmount. A fourth function, update, is optional.

Each of the functions will be called by single-spa at the right time, but the parcel config will control what happens. In other words, single-spa controls the “when,” but the parcel config controls the “what” and the “how.”

Once you’ve implemented the parcel config, simply call singleSpa.mountRootParcel(parcelConfig, parcelProps) to mount it. This is the key to what makes parcels framework agnostic — regardless of whether the parcel config is implemented with React, Angular, Vue, or anything else, to use the parcel you always just call mountRootParcel().

A few more specifics

We’ve glossed over a few things that I want to touch on real quick:

  • How do you implement the lifecycle functions on the parcel config?

            Use a helper library for your framework of choice. [single-spa-react](https://github.com/single-spa/single-spa-react), [single-spa-angular](https://github.com/single-spa/single-spa-angular) (for angular@2+), [single-spa-angularjs](https://github.com/single-spa/single-spa-angularjs), [single-spa-vue](https://github.com/single-spa/single-spa-vue), and [others](https://github.com/single-spa/single-spa/blob/master/docs/single-spa-ecosystem.md) will implement the entire parcel config for you.
  • What are the props you pass to mountRootParcel()?

            The props passed as the second argument to singleSpa.mountRootParcel(parcelConfig, parcelProps) are an object with one required prop and as many custom props as you’d like. The required prop is domElement, which tells the parcel where to mount. And the custom props get passed through to the parcel config lifecycle functions.
  • How do you re-render and unmount a parcel?

            The singleSpa.mountRootParcel() function returns a parcel object that lets you re-render and unmount the parcel whenever you’d like to.

    <iframe src="https://medium.com/media/b2d981b380b937009f7ce84e1cc2d753" frameBorder="0" />

Syntactic sugar makes this easier

Calling all of those functions manually might get annoying. So let’s make it easier. Here’s an example of some syntactic sugar for React. Similar features will be added soon for Angular, Vue, and other frameworks.

How hard is it to try this out?

You can get started with parcels immediately, without using the rest of single-spa. To do so, either npm install or script tag single-spa, then call mountRootParcel with your first parcel config.

You can also check out this codepen example to start out.

And if you are already a user of single-spa applications, parcels mean that your applications can mount and unmount shared functionality whenever you want them to. Since parcels don’t have activity functions, you don’t have to set up routes for them.

Let us know what you think!

We’d love to get your feedback on parcels. What do you think of this new way of framework interop? Is the implementation easy to understand? Are parcels useful for you or do they not quite fit into what you’re trying to accomplish?How hard was it for you to try out?

Check out the official docs for more examples, explanations, and api documentation.

And let us know your thoughts in the single-spa Slack channel, a Github issue, or on Twitter!

· 10 min read

Running Angular 1, React, Angular 2, and Vue.js side by side sounds pretty cool. And it seems appealing to have multiple applications coexisting on the same page, each lazily loaded.

But using single-spa for the first time can be tricky because you’ll come across terms like “application lifecycles”, “root application”, “loading function”, “child application”, and “activity function.”

This blog post will take you through setting things up and what choices you have when using single-spa. It’s based on what I’ve seen at Canopy Tax where we went from an Angular 1 monolith to an Angular 1, React, and Svelte polyglot.

If you’d like to jump straight to a fully working, self contained code example, check out this webpack single-spa starter project.

Step One: choose a module loader.

Your module loader / bundler is the library you’ll use to lazy load code. I recommend either Webpack or JSPM, if you’re starting from scratch.

If you go with Webpack, try to use Webpack 2 if you can, since it has support for promise-based lazy loading. This will make things easier for you later on, since single-spa requires that your loading functions return promises. If you can’t use Webpack 2, getting single-spa to lazy load your code with Webpack 1 will require some boilerplate code.

JSPM/SystemJS has worse documentation than Webpack, but is a great solution for module loading if you can get past that. I recommend using jspm@0.17 — it’s still in beta but has been worked on for over a year and at Canopy we find it stable enough to use in production.

If you’re struggling to decide between the two, then ask yourself the following: Do I want multiple completely separate bundles? If you don’t, I recommend Webpack because it has better docs, a larger community, and fewer gotchas. Otherwise, I’d go with JSPM, since Webpack has no plans to support dynamic runtime loading (See tweet below from Mr. Larkin, himself).

Step Two: create a brand new HTML file

The next step is to create what single-spa calls your “root application.” Really your root application is just the stuff that initializes single-spa, and it starts with an HTML file.

Even if you’ve got an existing project that already has it’s own HTML file, I recommend starting fresh with a new HTML file. That way, there is a clear distinction between what is in your root application (shared between all apps) and what is in a child application (not shared with everything).

You’ll want to keep your root application as small as possible, since it’s sort of the master controller of everything and could become a bottleneck. You don’t want to be constantly changing both the root application and the child applications.

So for now, just have a <script> to a single JavaScript file (root-application.js), which will be explained in Step Three.

Since Webpack is probably the more common use case, my code examples from here on will assume that you’re using Webpack 2. The equivalent Webpack 1 or JSPM code has all the same concepts and only some minor code differences.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>A single-spa application</title>
</head>
<body>
<div id="cool-app"></div>
<script src="root-application.js"></script>
</body>
</html>

Step Three: register an “application”

Now it’s time to finish up your root application by writing your “root-application.js” file. The primary purpose of root-application.js is to call singleSpa.registerApplication(..) for each of the applications that will be managed by single-spa.

If you’re into analogies, you can think of single-spa as the operating system for your single page application, managing which “processes” (or “child applications”) are running at any given time. At any moment, some of the child applications will be active on the DOM and others will not. As the user navigates throughout the app, some applications will be unmounting from the DOM and others will be mounting to the DOM.

Another way to look at it is that single-spa is a master router on top of your other routers.

To do this, first npm install single-spa and then call the registerApplication function:

import {registerApplication, start} from 'single-spa';

// Register your first application with single-spa. More apps will be registered as you create them
registerApplication('cool-app', loadCoolApp, isCoolAppActive);

// Tell single-spa that you're ready for it to mount your application to the DOM
start();

// This is a "loading function"
function loadCoolApp() {
return import('./cool-app/cool.app.js');
}

// This is an "activity function"
function isCoolAppActive() {
return window.location.hash.startsWith('#/cool');
}

Because single-spa is so very cool, we’ve created an app called “cool-app” that will be lazy loaded and mounted to the DOM whenever the url hash starts with #/cool.

The loadCoolApp function is what single-spa calls a loading function. Inside of it, the import introduces a code splitting point — Webpack will create separate code chunks that will be lazy loaded by single-spa.

For your specific project, you probably won’t have a hash prefix of “cool”, but I recommend establishing some kind of convention that makes it easy to determine which apps are active. This will simplify the maintenance of your activity functions, as you add more and more child applications.

If you’re going to start out with just one child application, then it might make sense to implement the activity function as () => true. You can worry about getting fancier once you have more than one application.

The last thing is to call start(). This is something you must do for things to work. The purpose is to give control over timing and performance. But until that is a concern, start is just one of those things you do, and then maybe read about it later if you ever need to.

Step Four: create “.app.js” file

When you open up your index.html file in the browser, you’ll now see….. a blank screen! We’re really close, but there’s one crucial step left: building your app.js file.

After that, you’ll have everything working for your first single-spa application.

An app.js file is a configuration file that you create for each child application. It is the code that is lazy loaded when your activity function returns true.

There are three things that you need to implement in the app.js file:

  1. A bootstrap lifecycle
  2. A mount lifecycle
  3. An unmount lifecycle

A “lifecycle” is a function or array of functions that will be called by single-spa; you export these from the app.js file. Each function must return a Promise so that single-spa knows when it is completed.

Here is a simple example:

// single-spa will import this file and call the exported lifecyle functions

let user;

export function bootstrap() {
return fetch('/api/users/0')
.then(response => response.json())
.then(json => (user = json));
}

export function mount() {
/* This is normally where you would have your framework-specific code like
* ReactDOM.render or angular.bootstrap(). The fact that you can put *anything*
* into this function is what makes single-spa so powerful -- any framework
* can implement a "mount" and "unmount" to become a single-spa application.
*/
return Promise.resolve().then(() => {
document.getElementById('user-app').innerHTML = `
<div>
Hello ${user.name}!
<div>
`;
});
}

export function unmount() {
/* Real world use cases would be something like ReactDOM.unmountComponentAtNode()
* or vue.$destroy()
*/
return Promise.resolve().then(() => {
document.getElementById('user-app').innerHTML = '';
});
}

At this point, you might be seeing the document.getElementById and innerHTML = and worry that you’ve been duped — maybe single-spa is really just a poor excuse for a ui component framework.

And really, don’t we already have a lot of different ways to write UI components?

Getting all of those frameworks to work together.

Using multiple frameworks is where single-spa really shines. It is not a ui framework itself, but a framework for using other frameworks.

Each child application can be written in any framework, so long as it implements application lifecycle functions. Then the mini-apps cooperate to form the entire single page application.

So going back to our previous example, we could choose to write our “cool.app.js” as an Angular 1 app, and choose something else for future apps:

import singleSpaAngularJS from 'single-spa-angularjs';
import angular from 'angular';
import './app.module.js';
import './routes.js';

const domElementGetter = () => document.getElementById('cool-app');

const angularLifecycles = singleSpaAngularJS({
angular,
domElementGetter,
mainAngularModule: 'single-spa-app',
uiRouter: true,
preserveGlobal: true,
});

export const bootstrap = [
aboutToBootstrap,
angularLifecycles.bootstrap,
doneBootstrapping,
];

export const mount = [angularLifecycles.mount];

export const unmount = [angularLifecycles.unmount];

function aboutToBootstrap() {
console.log('about to bootstrapping');
return Promise.resolve();
}

function doneBootstrap() {
console.log('finished bootstrapping');
return Promise.resolve();
}

In this example, we use a helper library called single-spa-angularjs which abstracts away the specifics of initializing Angular 1 apps. This blogpost doesn’t show you the app.module.js or routes.js files, but you can see an example implementation here.

The pattern is to call singleSpaAngularJS at the very beginning, which returns bootstrap, mount, and unmount lifecycle functions for you.

You might notice that this time the lifecycles are exported as arrays of functions instead of just functions — you can choose whichever works best for you.

The advantage of exporting an array of functions is that you can add in your own custom behavior (like aboutToBootstrap and doneBootstrap) that will run before or after the Angular 1 lifecycles. When you export an array, each item in the array must be a function that returns a promise. Single-spa will wait for each promise to resolve, in order, before calling the next function in the array.

To learn more about single-spa helper libraries, check out these github projects:

You can also see a fully working example of an angular app coexisting with other apps at the single-spa-examples repo or the live demo.

Step Five: test it out!

Refresh your page and you should now have a functioning single-spa application!

Try navigating to a url that your child app is active for (#/cool) and then navigating away from it. When you do so, the page will not refresh but you should see your application mount itself to the DOM and then unmount.

If you run into problems, try to narrow down whether the problem is in the root application or in the child application. Is your root application being executed? Are the declareChildApplication calls being made? Have you called start()? Is there a network request to download the code for your child application? Is your child application's bootstrap lifecycle being called? What about mount?

cdn-images-1

It may be helpful to add a navigation menu, so you can verify everything mounts and unmounts to the DOM correctly. If you want to level up your single-spa skills even more, make the navigation menu an entire child application whose activity function is () => true. An example that does just that is found here and here.

While you are verifying that everything is working, keep in mind that each application goes through five phases:

an applications's lifecycle

Conclusion

As you get your feet wet, you’ll probably run into some (hopefully small) hiccups setting things up. When this tutorial is not enough, there are other resources on Github and here in the docs.

Single-spa is still a relatively new thing, and we’d love to hear your feedback and questions. We welcome contributions from everyone.

If you’re excited about the possibilities, feel free to contact me on twitter (@joelbdenning). And if you are not excited, then still feel free to contact me, but only after you leave some nasty comments :)

· 6 min read

So you are a web-developer. You write a lot of JavaScript. You have a large single-page application (SPA) with features to add and bugs to maintain. Over time the application grows in size and complexity. It becomes more difficult to modify one portion of the SPA without breaking another portion.

The company is growing and you are looking for ways to scale the team and code-base. You add unit tests. You add a linter. You add continuous integration. You modularize the code with ES2015 modules, webpack, and npm. Eventually you even introduce new, independent SPAs with each SPA being owned and deployed by independent squads. Congratulations, you have successfully introduced service-oriented architecture on the front-end, or have you?

What is Service-oriented Architecture?

The fundamental concept behind service-oriented architecture is a service. A service is an isolated piece of code which can only be interacted with through its API. Unlike a shared library, a service itself can be deployed independently of its consumers. Think of a back-end API. The API is the service and the browser is the consumer. The API is deployed independently of the front-end application. There is also only one deployed version of the API available at a URL.

Contrast a service to a shared library. A shared library is a piece of code that is bundled and deployed with your code. For example, libraries such as Express, Lodash, and React are all shared libraries included in your application’s distributable. Upgrading a version of a shared library requires a new deployment of that distributable.

Service-oriented architecture is an approach to building software where the application is composed of many independent and isolated services. Those services are independently deployable, generally non-versioned, and auto discoverable.

Why Service-oriented Architecture on the Front-end?

The benefits of SOA can be illustrated with this real life example from Canopy. At Canopy we have multiple single page applications. The first application is external to the customers and the second is internal, yet both applications share common functionality. That functionality includes among other things, authentication and error logging.

cdn-images-1

Shared libraries between two separate applications. App 1 depends upon shared libs a, b, and c. App 2 depends upon only shared libs a and b.

Overall the design looks good. The code is modularized and shared. The complexities arrive when we start to upgrade the code to different versions. For example, after a short period of time, App 2 (being internal only) is upgraded to a new beta version of the shared lib b. Because the shared a also depends upon b (and we don’t want multiple versions of b bundled) we also create a new version of a. This one change causes a rebuild and deploy of three separate pieces of code: App 2 and shared libs a and b. Our dependency structure is no longer quite so simple.

cdn-images-2

In reality, a duplicate instance of lib a and b exist in both apps. Each app does not point to the same instance of the shared libraries, even when they are the same version. This is more noticeable when the shared libraries have separate versions.

Now imagine a bug in both versions of shared lib b. In order to fix the problem, you will have to republish both versions of a and b as well as c. Also App 1 and App 2 will have to be re-deployed. That is five new versions to publish and two apps to redeploy, all to fix one bug. All downstream dependencies have to be redeployed when a single library is changed. This is deploy dependency hell.

Service oriented architecture avoids these problems in a couple ways. Instead of bundling common dependencies, common code is shared through independent services. Services are not bundled, but rather loaded at run time. This also means that front-end services are not versioned (just like a back-end API). Both App 1 and App 2 load the exact same code for a front-end service.

Introducing sofe

Built upon the new ECMAScript module specification, sofe is a JavaScript library that enables independently deployable JavaScript services to be retrieved at run-time in the browser. Because the new module specification isn’t available within today’s browsers, sofe relies upon System.js to load services at run-time.

You can load a sofe service either with static or asynchronous imports.

// Static imports
import auth from 'auth-service!sofe';
const user = auth.getLoggedInUser();
// Asynchronous imports
System.import('auth-service!sofe').then(auth => auth.getLoggedInUser());

The real power behind sofe is that services are resolved at run-time, making them unversioned. If auth-service is redeployed, it is immediately made available to all upstream dependencies. The above scenario becomes much easier to resolve because there is only one version of each shared library as services. This is powerful because it allows you to deploy once, update everywhere. Also because the code is loaded at run-time, we can also enable developer tools to override what service is loaded into your application. Or in other words, you can test code on production without actually deploying to production.

cdn-images-2

The common dependencies are now services that are independent from the application code. Because services are unversioned, the dependency structure is again flat. Each service can individually be deployed and be available to every upstream dependency.

Obviously not all front-end code should be a service. Services have their own challenges. Specifically your code has to stay backwards compatible. But code can’t always be backwards compatible. Sometimes there needs to be breaking changes. The same problem exists for back-end services. A back-end API has to stay backwards compatible. Breaking changes on the back-end are generally solved by either creating an entirely new (versioned) API or implementing feature toggles within the API itself. The same solution applies to sofe services. An entirely new sofe service can be deployed or feature toggles can exist inside the front-end service. However it is solved, the key point is that services exist outside your application within their own distributable.

Another potential problem for sofe services is performance. Because they are loaded at run-time, performance can become a concern if you synchronously load too many services during bootstrap. Performance degradation can be mitigated by asynchronously loading larger services after the application bootstraps. Despite these challenges, there are many benefits to services on the front-end. The most exciting thing about sofe is there is now an option for services in the browser. You can decide what should and shouldn’t be a service.

Getting started with sofe requires only System.js. But to help you get started we have built sofe to work with a variety of technologies, including webpack, Babel, jspm, and the Chrome Developer Tools. Sofe is also actively used in production at Canopy Tax. We would love feedback on sofe and a number of open source projects that have been built around it. As you approach your next front-end project or look to improve your existing app, consider how it might benefit from service oriented architecture.

Read more about how to get started with sofe here.

- - +

· One min read

Attendees: Artur Androsovych, Ian Bacher, Jake Hayes, Milan Kovacic

Achievements and Progress from November

  • Successfully released single-spa version 6, marking a significant milestone in the project's development

December 2023 Roadmap

  1. Update dependencies, and set up automation, starting with create-single-spa
  2. Continue work on the consolidation and updates of the example projects

Meeting Notes

  • Welcomed Ian Bacher as a new addition to the core team.
  • Reviewed single-spa roadmap

Initiatives and Goals

  • Adding unit tests to existing projects
  • SystemJS -> ESM migration
  • create-single-spa update (https://github.com/cruft/cruft)
  • Release single-spa 6 as latest ✅
  • Improve shared dependencies management
  • Autopublishes to npm
  • Server rendering enhancements?
  • Support for NextJS, NuxtJS, Remix, create-react-app, and other build tools
  • Consolidate example projects 🚧
  • Feature voting
  • Automated integration tests for popular frameworks
  • Update create-single-spa dependencies 🚧

· 10 min read

We have released single-spa@6! The single-spa core team is committed to a stable single-spa runtime with very few breaking changes. We expect over 95% of single-spa users can upgrade easily without altering their changes!

Note from single-spa's creator

On September 22, 2015, I pushed single-spa's initial commit. 8 years later, I am releasing single-spa@6. I am committed to this project and hope to improve and grow it for many years to come. The single-spa core team recently started monthly calls where we discuss important pull requests and roadmap (see first meeting notes). The future of microfrontends still includes single-spa.

On December 11, 2023, the company I founded is launching a product called Baseplate Cloud that offers single-spa hosting. Baseplate Cloud is the single-spa hosting solution born from 4+ years of consulting with various companies who were implementing single-spa. Baseplate CDN is a Cloudflare worker that proxies JS files from cloud storage, provides edge-side import map processing, auto-implements single-spa root configs, and more. Cloudflare edge-side processing provides many performance advantages while retaining the impressive scalability and security of Cloudflare. As a sign of good will towards the open source and single-spa communities, I have kept the baseplate-cloudflare-worker public, with a "Commons Clause" + AGPL 3.0 license. Companies can use and contribute to baseplate-cloudflare-worker freely, as long as they don't try to sell it in competition to Baseplate Cloud. I would appreciate the community's backing and hope Baseplate becomes the way to financially sustain the single-spa project in the long term.

It would mean the world to me if your company or organization considered signing up for Baseplate's paid, private beta launch. We used the revenue the community gave us for single-spa-workshop.com to create Baseplate, and plan to use Baseplate revenue to fund maintenance of single-spa's open source ecosystem. The Baseplate team is proudly a worker-owned business that has not sacrificed ownership of the company to outside investors. We built Baseplate with the revenue the community gave us via single-spa-workshop.com as the initial funding, which is only a few thousand dollars a month. The best Christmas present I could receive this year is a few dozen companies signing up immediately and putting Baseplate on their Q1 or Q2 roadmaps for 2024. I am asking for corporate partners to ensure long term financial sustainability so that single-spa will remain a good microfrontends solution for years to come.

Contact Baseplate Sales

Summary

single-spa@6 provides advanced features such as async navigation cancelation and parcel-only mode (via patchHistoryApi), while also updating the package configuration to use modern NodeJS and bundler features like package entrypoints and "type": "module". We also updated urlRerouteOnly to default to true, as it boosts performance and we consider it to be a best practice to author applications that work when urlRerouteOnly is set to true.

Browser Support / IE11

We still support IE11 and the same versions of NodeJS (at least as old as Node 10). However, we've changed our defaults such that IE11 is only supported with some additional configuration, as described in the IE11 Migration section below.

For the single-spa npm package, we plan single-spa@6 to be the last major version that supports IE11. In general, we've released a major version of single-spa every 1-2 years. We do not have a concrete timeline or plans for single-spa@7, and will accept community contributions for any important patches to single-spa@6 after single-spa@7 is released.

For all surrounding packages (single-spa-react, single-spa-vue, systemjs-webpack-interop, single-spa-angular, etc) we may begin dropping IE11 support sooner than the single-spa@7 release. As we do so, we'll create documentation that lists the latest versions of packages that support IE11, and also accept community contributions to those versions for important updates.

The motivation for dropping IE11 support over time is to allow our core team to spend less time on supporting a nine year old browser and more time pushing the single-spa ecosystem forward. Also, we believe that the majority of end users should not suffer performance penalties because of a small percentage of users still using IE11. Slowly dropping IE11 also will also allow us to focus on a systemjs-less future for single-spa, where native browser modules are used rather than systemjs modules.

We understand and sympathize with companies who cannot drop IE11 support now, which is why we're supporting IE11 even though much of the ecosystem has already dropped it (Vue 3 does not support ie11, Angular only does after some effort, and even webpack 5 requires extra configuration to get it working in IE11). Many of single-spa's users are large financial institutions, large ecommerce companies, and other very large companies, where dropping IE11 support is a long process that involves many dozens of people from separate departments. We encourage single-spa users to start the conversations at your companies about dropping IE11 support, even if they take a long time or result in a decision to drop IE11 years in the future.

Migration

We think that >95% of single-spa users will be able to safely upgrade with no code changes. The breaking changes below are largely related to advanced features that most single-spa users do not use.

Steps

  1. If using SystemJS + import maps, change the URL of single-spa in your import map:
<script type="systemjs-importmap">
{
"imports": {
- "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.2/lib/system/single-spa.min.js"
+ "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es2015/system/single-spa.min.js"
}
}
</script>
  1. In all your microfrontends (including root config), upgrade single-spa:
npm install single-spa@6
yarn add single-spa@6
pnpm install single-spa@6
  1. Test things out.
  2. If your app is not re-rendering during route transitions like it should (this is most common for Angular apps, but can happen in other frameworks to if you do a route transition without modifying the URL), change your root config's call to start()
import { start } from 'single-spa';

// DO NOT MAKE THIS CHANGE unless you are experiencing problems.
- start();
+ start({
+ urlRerouteOnly: false
+ });

That's it! If you experience other issues, let us know in Github issues.

IE11

If you support IE11, use the es5 version of single-spa rather than es2015. The method of doing this depends on your configuration - some common ones are explained below:

Import Maps

<script type="systemjs-importmap">
{
"imports": {
- "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es2015/system/single-spa.min.js"
+ "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.0/lib/es5/system/single-spa.min.js"
}
}
</script>

Webpack

If externalizing single-spa in your webpack config (very common, and the default with webpack-config-single-spa), you do not need to make the changes below. Otherwise, though, these changes will ensure you use the IE11-compatible version of single-spa.

// webpack.config.js
module.exports = {
resolve: {
"alias": {
"single-spa": require.resolve("single-spa/lib/es5/esm/single-spa.min.js")
}
}
}

Rollup

Use https://www.npmjs.com/package/@rollup/plugin-alias to alias the import similarly to webpack

Import

If you do not wish to change bundler configuration, you can modify your import statements.

- import { start } from 'single-spa';
+ import { start } from 'single-spa/lib/es5/esm/single-spa.min.js';

Breaking Changes

  • single-spa's package.json's "main" field now points to an es2015 version of single-spa that does not work in IE11. To support IE11, see section below.
  • Delay patching history api until start() or patchHistoryApi() is called. Github link
  • Do not show start() warning when registerApplication hasn't been called (for parcel-only use cases) Github link
  • Change urlRerouteOnly default value to true. This is a performance boost for most applications, but for some situations can result in single-spa not triggering mounts/unmounts when it should. You can read more about it at https://single-spa.js.org/docs/api#start and https://github.com/single-spa/single-spa/issues/484. Angular users may want to set it to false due to nuanced behavior of angular router. To do so, call start({urlRerouteOnly: false}) in your root config. Github link
  • cancelNavigation(val) no longer cancels navigation if val is falsy Github link
  • single-spa's package.json "type" is now set to "module". This only impacts NodeJS usage of single-spa (not webpack/rollup/browser). Read more at official nodejs docs.
  • single-spa's package.json now has "exports". This changes which of single-spa's bundles are used by NodeJS, webpack, and rollup. The main change is that webpack / rollup will now use an IE11-incompatible (es2015) version of single-spa. See IE11 section below for more details. Additionally, an ESM version of single-spa will now be used when loaded via import('single-spa') or import 'single-spa';, whereas a UMD version will be used when calling require("single-spa'). To avoid the dual package hazard, only use import or require to load single-spa in your NodeJS code - do not use both. You can see the package exports at https://github.com/single-spa/single-spa/blob/dea22f1aac39777a07252897ae625ab1d8313e9d/package.json#L8-L25.
  • The published umd builds now have .cjs extensions rather than .js, since all .js files in the single-spa package are assumed to be ESM.
File structure published to npm

# Before
# See https://www.jsdelivr.com/package/npm/single-spa?path=lib&version=5.9.3
lib/
# IE11
umd/
single-spa.dev.js
single-spa.min.js
# IE11
esm/
single-spa.dev.js
single-spa.min.js
# IE11
system/
single-spa.dev.js
single-spa.min.js
# No IE11
es2015/
single-spa.dev.js
single-spa.min.js

# After
# See https://www.jsdelivr.com/package/npm/single-spa?path=lib&version=6.0.0
lib
# IE11
es5/
umd/
single-spa.dev.js
single-spa.min.js
system/
single-spa.dev.js
single-spa.min.js
esm/
single-spa.dev.js
single-spa.min.js
# No IE11
es2015/
umd/
single-spa.dev.cjs
single-spa.min.cjs
system/
single-spa.dev.js
single-spa.min.js
esm/
single-spa.dev.js
single-spa.min.js

Features

Async navigation cancelation

We've added support for async navigation cancelation. To use it, call cancelNavigation(promise) with a promise as an argument. Single-spa will wait until that promise resolves/rejects before proceeding with navigation. If the promise resolves with a truthy value, navigation is canceled. If the promise resolves with a falsy value or rejects, navigation is not canceled. Github link

window.addEventListener("single-spa:before-routing-event", evt => {
if (evt.detail.oldUrl === "/settings") {
evt.detail.cancelNavigation(checkSettingsOkay())
}
})

async function checkSettingsOkay() {
const response = await fetch('/api/settings-okay')
if (response.ok) {
return true;
} else {
alert("Please fix your settings before leaving the page");
return false
}
}

New patchHistoryApi

We've exposed a new patchHistoryApi() api. This lets you use single-spa's modified implementations of pushState/replaceState/popstate/hashchange without using single-spa applications. This is intended to be used by single-spa users who exclusively use single-spa parcels, rather than applications Github link

import { patchHistoryApi, mountRootParcel } from 'single-spa';

patchHistoryApi({
urlRerouteOnly: true
})

// now you don't need to call start() if you're only using parcels

mountRootParcel(parcelConfig, parcelProps)

New profiler

We've implemented a profiler that is available within the dev builds of single-spa@6. A visualization of the profiler is available in the single-spa-inspector@0.6.0 browser extension:

  • Implement profiler for lifecycle events. by @joeldenning in #868
  • Implement parcel and routing profiler events. by @joeldenning in #903
Screenshot of single-spa-inspector profiler tab

Fixes

  • Fix regression with parcel name Github link
  • Ensure all reroute promises (triggerAppChange() return value) resolve even during cancelation Github link
  • fix(types): fix SingleSpaAppsByNewStatus interface by @FelixGraf in #988
  • Ensure app is unmounted during unregisterApplication. Resolves #871 by @joeldenning in #875

Maintenance

  • Switch from yarn -> pnpm Github link
  • Upgrade all dependencies Github link
  • Upgrade husky from v4 to v7. Use pinst to avoid issues with yarn 2 users as described in https://typicode.github.io/husky/#/?id=yarn-2 Github link
  • Upgrade to Jest 27. Fix tests to work with Jest 27. Github link
  • Upgrade to Node 16. Fix tests to work with Node 16 (Fix tests in Node 15 #652) Github link
  • Run upgraded version of prettier on all files. Github link
  • Remove unused babel-eslint dependency Github link
  • Upgrade to new package names for all rollup plugins. (@rollup/plugin-node-resolve instead of rollup-plugin-node-resolve) Github link
  • Remove file size impact workflow since it's broken. by @joeldenning in #879
  • Remove console.log in test by @joeldenning in #878

· One min read

Attendees: Joel Denning, Milan Kovacic, Jake Hayes

November 2023 Roadmap

  1. Release single-spa@6 as latest
  2. Consolidate and update the example projects

Meeting Notes

  • Open pull requests, pressing Github issues
  • Core team vacancy
    • Add Jake Hayes to the #core-team channel, but not officially a core team member
    • Joel will reach out to Ian about joining the core team.
  • Single-spa roadmap
  • Maintenance update/plan

Roadmap Brainstorm

  • Adding unit tests to existing projects
  • SystemJS -> ESM migration
  • create-single-spa update (https://github.com/cruft/cruft)
  • Release single-spa 6 as latest
  • Improve shared dependencies management
  • Autopublishes to npm
  • Server rendering enhancements?
  • Support for NextJS, NuxtJS, Remix, create-react-app, and other build tools
  • Consolidate example projects
  • Feature voting
  • Automated integration tests for popular frameworks
  • Update create-single-spa dependencies

· 2 min read

Please welcome the latest additions to the Single-Spa core team. These talented individuals bring fresh energy and a wealth of expertise and are poised to revitalize and expand the Single-Spa project.

Meet the Team

Artur Androsovych

Artur Androsovych is a Google Developer Expert in Angular and an open-source contributor who has been focusing on runtime performance and teaching teams about Angular internals for the past few years. He has maintained the single-spa-angular project for years, and we're excited for him to join the core team.

Roberto Mosca

Roberto is a Principal Software Engineer at TravelPerk (www.travelperk.com), where he is the go-to person for all things related to their frontend platform. With a full-stack background, he's got his hands on a variety of tech — from Python to NodeJS, React, Webpack... Before diving into the world of business travel, he was immersed in biomedical research as a bioinformatician. When he is not in front of a screen, you’ll find him swimming, cooking up some Italian delicacies, or tinkering with his Rubik’s cube-solving robots.

Milan Kovacic

Milan Kovacic is a seasoned software consultant with deep expertise in various development domains. On the frontend side, his work is marked by proficiency in React, TypeScript, and the integration of microfrontends. For backend solutions, he mostly relies on the .NET framework, C#—often utilizing microservices architecture. Additionally, Milan is experienced with cloud technologies, regularly working with platforms like AWS and Azure. Beyond traditional development, he places great emphasis on ensuring a smooth developer experience and promotes the use of automation and efficient DevOps practices to streamline development workflows.

These three new members will be joining current members Joel Denning, Carlos Filoteo, and Anthony Frehner to manage GitHub and Slack issues and actively develop new features. We’d also like to give special recognition to contributors from Qiankun for their extensive support over the years.

As part of this team update, we’ll also be creating a public roadmap, providing transparency about Single-Spa's future direction. To maintain transparency and encourage collaboration, we're establishing monthly meetings where the core team will discuss progress, challenges, and ideas. Meeting notes will be publicly available, ensuring that the community is well-informed and able to participate in our discussions.

So stay tuned and join us on this journey shaping the future of microfrontends.

· 2 min read

We are happy to announce that the single-spa core team is opening applications for two new members! As a core team member, you will help us maintain and guide the growing single-spa ecosystem by responding to Github and Slack issues, implementing new features, and helping to create the roadmap for the future of single-spa.

Our current core team consists of Joel Denning, Carlos Filoteo, and Anthony Frehner. We also thank Justin McMurdie and Bret Little for their valuable contributions during their time with the single-spa core team.

Single-spa owes its success to the people and companies who support it. If you want to contribute but aren’t able to join the core team, consider applying to be a maintainer. Maintainers are volunteers who receive Github write access to specific subprojects within the ecosystem. If you’re a manager at a company that uses single-spa, we encourage you to allow your developers to volunteer their time as maintainers or core team members so that the project can continue to grow and develop.

The single-spa core team and maintenance are unpaid volunteer positions. Looking ahead, if any core team members or maintainers are interested in new paid roles, Convex Cooperative (the company Joel works for) will be considering hiring new developers from the single-spa team. It’s important to state that Convex will not poach developers away from companies who allow their developers to help us maintain single-spa—our priority is nurturing the growth and development of the single-spa ecosystem.

If interested in applying to be a single-spa maintainer or core team member, please fill out the following Google Form before September 1, 2023:

https://docs.google.com/forms/d/1zIfP2kYjNBCi-qjKf9T1-sNT1VLSXTB9twwX_mxKYxU/edit

· 5 min read

Today we released single-spa@5.0.0.

Here are the highlights:

Release notes here

Migration from 4 to 5

For every user we're aware of, you do not need to change anything in your code in order to upgrade to single-spa@5. The breaking changes listed in the release notes are the removal of features that were originally used by Canopy Tax, but were never documented.

If installing from npm, you can simply npm install --save single-spa@5.0.0 or yarn add single-spa@5.0.0.

Alternatively, single-spa is available on cdnjs, jsdelivr, and unpkg.

The single-spa core team is committed to treating our users well, which includes not introducing massive breaking changes. The core single-spa API has not seen massive breaking changes since single-spa@3 in August 2016. We have added features and improved things, but single-spa is a stable technology. We are committed to maintaining it, documenting it, and adjusting it as technologies like in-browser modules become more and more popular and viable.

Performance improvements

The ESM version of single-spa@4 was 23.8kb (7.2kb gzipped). That was improved in single-spa@5 to 15.5kb (5.1kb gzipped). We did this by optimizing our build process and removing unused features.

single-spa CLI

Since single-spa's inception, bundler configuration has been a huge source of user pain. We have heard this pain and implemented create-single-spa, which creates (and sometimes can update) repositories that are ready to be used as single-spa microfrontends. For Angular and Vue, the official CLIs are used with a few extra plugins automatically installed. For React, a default webpack config with decent eslint / prettier defaults is set up.

Additionally, we have added a lot of documentation for webpack in The Recommended Setup.

Tutorial videos

We understand that single-spa is more than just a library - it is an architecture. The single-spa library itself is the core, but the surrounding ecosystem of concepts and libraries are equally important to successfully migrating to single-spa and having it work for you. As such, we have created a Youtube playlist, currently consisting of seven videos, to help you get started.

Youtube playlist / Bilibili space

The videos currently cover the following topics:

  • What are Microfrontends?
  • In-browser vs build-time JavaScript modules
  • Import Maps
  • Local Development with single-spa and import maps
  • Deploying Microfrontends / Continuous Integration (CI)
  • SystemJS intro
  • Lazy Loading
  • Bundlers, webpack, and rollup.

New example repositories

What started out as Canopy Tax's special sauce for independently deployed frontend microservices is now fully accessible to the public with our new set of example repos. We have a React example, a Vue example, and a polyglot (multiple framework) example. We hope to add an Angular example, after we achieve support for Angular 9. These example repositories are actively watched and maintained by the single-spa core team, and reflect our current opinions on the best, production-viable way to do microfrontends.

Furthermore, we have deployed each of the examples to our new domains:

Documentation overhaul

We removed several dated documentation pages, and added several that were very much lacking. Here are a few pages that give you the most bang for your buck:

Development builds and error codes

Taking inspiration from the react development and production builds, we now publish to NPM both development and production builds in the following formats: UMD, ESM, and System.register.

You can see the published build files here. The .dev.js files provide full debugging information in the browser console, whereas the .min.js files give you a numeric error code and a link to a documentation page that explains the error. We hope that these error codes and documentation for them will improve discoverability of relevant documentation when you're setting up single-spa.

An example of these new documentation pages for error codes is found here.

Governance

Some of you may have noticed that we recently moved all github repos from https://github.com/CanopyTax to https://github.com/single-spa. Canopy Tax was the company where single-spa was first authored, but as a core team we asked to move ownership and governance of the projects to an organization fully managed by the open source community. In agreement with Canopy, we made that change.

This change does not mean anything drastic for single-spa. Its license was and is MIT, and we have no plans to do anything with the project besides make it better.

Where next?

We are actively translating the single-spa documentation to Chinese, and hope to add other languages soon. We will add full Angular 9 support soon, and hope to add server rendering in an upcoming release.

Please contribute to our code and ecosystem, join our single-spa slack channel, follow our official Twitter account, and contribute to our open collective. The single-spa core team all have full-time jobs and maintain this project on a volunteer basis.

· 2 min read

Background

For a long time, Canopy has had the benefit of using a tool called sofe inspector (note: this is an out-of-date version of it) to list, override, and interact with single-spa applications. There has always been a desire to figure out how to share this tool so others can benefit as well.

With that in mind, I'm proud to announce an initial release for single-spa Inspector! single-spa Inspector is a Firefox and Chrome extension, much like React/Vue devtools, that allows you see and interact with your single-spa applications and configuration.

Current Inspector Features

  • List registered applications
  • Show application status
  • Force an app to mount or unmount
  • Hover over an app name to have an "inspect element"-like view of your apps (Overlays)

(Note: Overlays require a small update to your code, but should hopefully be simple! See how to configure app overlays)

The single-spa Inspector will only work with single-spa versions 4.1 and higher, since we had to expose and add some functionality to the single-spa library itself in order to implement these features.

single-spa 4.1

single-spa 4.1 was released, which includes a couple of key updates:

  1. Support for single-spa Inspector
  2. ESM bundle output
  3. Simpmlified test configuration for developers/contributors to single-spa

For most people, ESM (EcmaScript Module) support shouldn't affect how you use single-spa, but for those looking to play around with modules or other advanced Javascript things, it's a welcome addition.

We also changed our test suite to purely use Jest instead of Saucelabs, and hopefully false positive "failing" tests on pull requests will be a thing of the past.

Help Wanted!

If you would like to suggest a new feature for single-spa Inspector, report a bug, improve our (admittedly horrible and hopefully temporary) UI/UX, or add features, please see the github repo and hack away!

We also hope to update some of our example repos to the lastest single-spa so that anyone with the extension installed can test out the features and see how to implement overlays. But this process will go faster if someone wants to help out. :)

Thank you!

· 4 min read

Ever since single-spa@1.0.0, the single-spa team has been dedicated to bringing microservices to the frontend. We have made it possible for AngularJS, React, Angular, Vue, and other frameworks to coexist side by side in the same page.

And with the release of version 4, I’m pleased to announce that single-spa is expanding that effort so that individual components written with different frameworks can interoperate. It is new terrain for the single-spa community, which previously had focused on getting large applications to interoperate with each other, instead of the individual components.

Another way to do framework agnostic components?

For those familiar with web components and custom elements, you may be wondering why a JavaScript library would try to do what browsers are starting natively to do.

And as one of the contributors to the custom elements polyfill, let me be the first one to say that we did not make this decision lightly.

If you’re interested in diving into the details, check out One Company’s Relationship With Custom Elements, which explains some of the difficulties we’ve been through with web components and custom elements.

TLDR: React and some other frameworks don’t interop with custom elements very well. Additionally dealing with inner HTML, attributes vs properties, and customized builtins can be a pain.

Okay but you haven’t told me what a single-spa parcel is

A parcel is single-spa’s way of building a component in one framework and using it in another.

To implement a parcel, just create a JavaScript object that has 3–4 functions on it. We call this JavaScript object a parcel config and there are three required functions to implement: bootstrap, mount, and unmount. A fourth function, update, is optional.

Each of the functions will be called by single-spa at the right time, but the parcel config will control what happens. In other words, single-spa controls the “when,” but the parcel config controls the “what” and the “how.”

Once you’ve implemented the parcel config, simply call singleSpa.mountRootParcel(parcelConfig, parcelProps) to mount it. This is the key to what makes parcels framework agnostic — regardless of whether the parcel config is implemented with React, Angular, Vue, or anything else, to use the parcel you always just call mountRootParcel().

A few more specifics

We’ve glossed over a few things that I want to touch on real quick:

  • How do you implement the lifecycle functions on the parcel config?

            Use a helper library for your framework of choice. [single-spa-react](https://github.com/single-spa/single-spa-react), [single-spa-angular](https://github.com/single-spa/single-spa-angular) (for angular@2+), [single-spa-angularjs](https://github.com/single-spa/single-spa-angularjs), [single-spa-vue](https://github.com/single-spa/single-spa-vue), and [others](https://github.com/single-spa/single-spa/blob/master/docs/single-spa-ecosystem.md) will implement the entire parcel config for you.
  • What are the props you pass to mountRootParcel()?

            The props passed as the second argument to singleSpa.mountRootParcel(parcelConfig, parcelProps) are an object with one required prop and as many custom props as you’d like. The required prop is domElement, which tells the parcel where to mount. And the custom props get passed through to the parcel config lifecycle functions.
  • How do you re-render and unmount a parcel?

            The singleSpa.mountRootParcel() function returns a parcel object that lets you re-render and unmount the parcel whenever you’d like to.

    <iframe src="https://medium.com/media/b2d981b380b937009f7ce84e1cc2d753" frameBorder="0" />

Syntactic sugar makes this easier

Calling all of those functions manually might get annoying. So let’s make it easier. Here’s an example of some syntactic sugar for React. Similar features will be added soon for Angular, Vue, and other frameworks.

How hard is it to try this out?

You can get started with parcels immediately, without using the rest of single-spa. To do so, either npm install or script tag single-spa, then call mountRootParcel with your first parcel config.

You can also check out this codepen example to start out.

And if you are already a user of single-spa applications, parcels mean that your applications can mount and unmount shared functionality whenever you want them to. Since parcels don’t have activity functions, you don’t have to set up routes for them.

Let us know what you think!

We’d love to get your feedback on parcels. What do you think of this new way of framework interop? Is the implementation easy to understand? Are parcels useful for you or do they not quite fit into what you’re trying to accomplish?How hard was it for you to try out?

Check out the official docs for more examples, explanations, and api documentation.

And let us know your thoughts in the single-spa Slack channel, a Github issue, or on Twitter!

· 10 min read

Running Angular 1, React, Angular 2, and Vue.js side by side sounds pretty cool. And it seems appealing to have multiple applications coexisting on the same page, each lazily loaded.

But using single-spa for the first time can be tricky because you’ll come across terms like “application lifecycles”, “root application”, “loading function”, “child application”, and “activity function.”

This blog post will take you through setting things up and what choices you have when using single-spa. It’s based on what I’ve seen at Canopy Tax where we went from an Angular 1 monolith to an Angular 1, React, and Svelte polyglot.

If you’d like to jump straight to a fully working, self contained code example, check out this webpack single-spa starter project.

Step One: choose a module loader.

Your module loader / bundler is the library you’ll use to lazy load code. I recommend either Webpack or JSPM, if you’re starting from scratch.

If you go with Webpack, try to use Webpack 2 if you can, since it has support for promise-based lazy loading. This will make things easier for you later on, since single-spa requires that your loading functions return promises. If you can’t use Webpack 2, getting single-spa to lazy load your code with Webpack 1 will require some boilerplate code.

JSPM/SystemJS has worse documentation than Webpack, but is a great solution for module loading if you can get past that. I recommend using jspm@0.17 — it’s still in beta but has been worked on for over a year and at Canopy we find it stable enough to use in production.

If you’re struggling to decide between the two, then ask yourself the following: Do I want multiple completely separate bundles? If you don’t, I recommend Webpack because it has better docs, a larger community, and fewer gotchas. Otherwise, I’d go with JSPM, since Webpack has no plans to support dynamic runtime loading (See tweet below from Mr. Larkin, himself).

Step Two: create a brand new HTML file

The next step is to create what single-spa calls your “root application.” Really your root application is just the stuff that initializes single-spa, and it starts with an HTML file.

Even if you’ve got an existing project that already has it’s own HTML file, I recommend starting fresh with a new HTML file. That way, there is a clear distinction between what is in your root application (shared between all apps) and what is in a child application (not shared with everything).

You’ll want to keep your root application as small as possible, since it’s sort of the master controller of everything and could become a bottleneck. You don’t want to be constantly changing both the root application and the child applications.

So for now, just have a <script> to a single JavaScript file (root-application.js), which will be explained in Step Three.

Since Webpack is probably the more common use case, my code examples from here on will assume that you’re using Webpack 2. The equivalent Webpack 1 or JSPM code has all the same concepts and only some minor code differences.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>A single-spa application</title>
</head>
<body>
<div id="cool-app"></div>
<script src="root-application.js"></script>
</body>
</html>

Step Three: register an “application”

Now it’s time to finish up your root application by writing your “root-application.js” file. The primary purpose of root-application.js is to call singleSpa.registerApplication(..) for each of the applications that will be managed by single-spa.

If you’re into analogies, you can think of single-spa as the operating system for your single page application, managing which “processes” (or “child applications”) are running at any given time. At any moment, some of the child applications will be active on the DOM and others will not. As the user navigates throughout the app, some applications will be unmounting from the DOM and others will be mounting to the DOM.

Another way to look at it is that single-spa is a master router on top of your other routers.

To do this, first npm install single-spa and then call the registerApplication function:

import {registerApplication, start} from 'single-spa';

// Register your first application with single-spa. More apps will be registered as you create them
registerApplication('cool-app', loadCoolApp, isCoolAppActive);

// Tell single-spa that you're ready for it to mount your application to the DOM
start();

// This is a "loading function"
function loadCoolApp() {
return import('./cool-app/cool.app.js');
}

// This is an "activity function"
function isCoolAppActive() {
return window.location.hash.startsWith('#/cool');
}

Because single-spa is so very cool, we’ve created an app called “cool-app” that will be lazy loaded and mounted to the DOM whenever the url hash starts with #/cool.

The loadCoolApp function is what single-spa calls a loading function. Inside of it, the import introduces a code splitting point — Webpack will create separate code chunks that will be lazy loaded by single-spa.

For your specific project, you probably won’t have a hash prefix of “cool”, but I recommend establishing some kind of convention that makes it easy to determine which apps are active. This will simplify the maintenance of your activity functions, as you add more and more child applications.

If you’re going to start out with just one child application, then it might make sense to implement the activity function as () => true. You can worry about getting fancier once you have more than one application.

The last thing is to call start(). This is something you must do for things to work. The purpose is to give control over timing and performance. But until that is a concern, start is just one of those things you do, and then maybe read about it later if you ever need to.

Step Four: create “.app.js” file

When you open up your index.html file in the browser, you’ll now see….. a blank screen! We’re really close, but there’s one crucial step left: building your app.js file.

After that, you’ll have everything working for your first single-spa application.

An app.js file is a configuration file that you create for each child application. It is the code that is lazy loaded when your activity function returns true.

There are three things that you need to implement in the app.js file:

  1. A bootstrap lifecycle
  2. A mount lifecycle
  3. An unmount lifecycle

A “lifecycle” is a function or array of functions that will be called by single-spa; you export these from the app.js file. Each function must return a Promise so that single-spa knows when it is completed.

Here is a simple example:

// single-spa will import this file and call the exported lifecyle functions

let user;

export function bootstrap() {
return fetch('/api/users/0')
.then(response => response.json())
.then(json => (user = json));
}

export function mount() {
/* This is normally where you would have your framework-specific code like
* ReactDOM.render or angular.bootstrap(). The fact that you can put *anything*
* into this function is what makes single-spa so powerful -- any framework
* can implement a "mount" and "unmount" to become a single-spa application.
*/
return Promise.resolve().then(() => {
document.getElementById('user-app').innerHTML = `
<div>
Hello ${user.name}!
<div>
`;
});
}

export function unmount() {
/* Real world use cases would be something like ReactDOM.unmountComponentAtNode()
* or vue.$destroy()
*/
return Promise.resolve().then(() => {
document.getElementById('user-app').innerHTML = '';
});
}

At this point, you might be seeing the document.getElementById and innerHTML = and worry that you’ve been duped — maybe single-spa is really just a poor excuse for a ui component framework.

And really, don’t we already have a lot of different ways to write UI components?

Getting all of those frameworks to work together.

Using multiple frameworks is where single-spa really shines. It is not a ui framework itself, but a framework for using other frameworks.

Each child application can be written in any framework, so long as it implements application lifecycle functions. Then the mini-apps cooperate to form the entire single page application.

So going back to our previous example, we could choose to write our “cool.app.js” as an Angular 1 app, and choose something else for future apps:

import singleSpaAngularJS from 'single-spa-angularjs';
import angular from 'angular';
import './app.module.js';
import './routes.js';

const domElementGetter = () => document.getElementById('cool-app');

const angularLifecycles = singleSpaAngularJS({
angular,
domElementGetter,
mainAngularModule: 'single-spa-app',
uiRouter: true,
preserveGlobal: true,
});

export const bootstrap = [
aboutToBootstrap,
angularLifecycles.bootstrap,
doneBootstrapping,
];

export const mount = [angularLifecycles.mount];

export const unmount = [angularLifecycles.unmount];

function aboutToBootstrap() {
console.log('about to bootstrapping');
return Promise.resolve();
}

function doneBootstrap() {
console.log('finished bootstrapping');
return Promise.resolve();
}

In this example, we use a helper library called single-spa-angularjs which abstracts away the specifics of initializing Angular 1 apps. This blogpost doesn’t show you the app.module.js or routes.js files, but you can see an example implementation here.

The pattern is to call singleSpaAngularJS at the very beginning, which returns bootstrap, mount, and unmount lifecycle functions for you.

You might notice that this time the lifecycles are exported as arrays of functions instead of just functions — you can choose whichever works best for you.

The advantage of exporting an array of functions is that you can add in your own custom behavior (like aboutToBootstrap and doneBootstrap) that will run before or after the Angular 1 lifecycles. When you export an array, each item in the array must be a function that returns a promise. Single-spa will wait for each promise to resolve, in order, before calling the next function in the array.

To learn more about single-spa helper libraries, check out these github projects:

You can also see a fully working example of an angular app coexisting with other apps at the single-spa-examples repo or the live demo.

Step Five: test it out!

Refresh your page and you should now have a functioning single-spa application!

Try navigating to a url that your child app is active for (#/cool) and then navigating away from it. When you do so, the page will not refresh but you should see your application mount itself to the DOM and then unmount.

If you run into problems, try to narrow down whether the problem is in the root application or in the child application. Is your root application being executed? Are the declareChildApplication calls being made? Have you called start()? Is there a network request to download the code for your child application? Is your child application's bootstrap lifecycle being called? What about mount?

cdn-images-1

It may be helpful to add a navigation menu, so you can verify everything mounts and unmounts to the DOM correctly. If you want to level up your single-spa skills even more, make the navigation menu an entire child application whose activity function is () => true. An example that does just that is found here and here.

While you are verifying that everything is working, keep in mind that each application goes through five phases:

an applications's lifecycle

Conclusion

As you get your feet wet, you’ll probably run into some (hopefully small) hiccups setting things up. When this tutorial is not enough, there are other resources on Github and here in the docs.

Single-spa is still a relatively new thing, and we’d love to hear your feedback and questions. We welcome contributions from everyone.

If you’re excited about the possibilities, feel free to contact me on twitter (@joelbdenning). And if you are not excited, then still feel free to contact me, but only after you leave some nasty comments :)

· 6 min read

So you are a web-developer. You write a lot of JavaScript. You have a large single-page application (SPA) with features to add and bugs to maintain. Over time the application grows in size and complexity. It becomes more difficult to modify one portion of the SPA without breaking another portion.

The company is growing and you are looking for ways to scale the team and code-base. You add unit tests. You add a linter. You add continuous integration. You modularize the code with ES2015 modules, webpack, and npm. Eventually you even introduce new, independent SPAs with each SPA being owned and deployed by independent squads. Congratulations, you have successfully introduced service-oriented architecture on the front-end, or have you?

What is Service-oriented Architecture?

The fundamental concept behind service-oriented architecture is a service. A service is an isolated piece of code which can only be interacted with through its API. Unlike a shared library, a service itself can be deployed independently of its consumers. Think of a back-end API. The API is the service and the browser is the consumer. The API is deployed independently of the front-end application. There is also only one deployed version of the API available at a URL.

Contrast a service to a shared library. A shared library is a piece of code that is bundled and deployed with your code. For example, libraries such as Express, Lodash, and React are all shared libraries included in your application’s distributable. Upgrading a version of a shared library requires a new deployment of that distributable.

Service-oriented architecture is an approach to building software where the application is composed of many independent and isolated services. Those services are independently deployable, generally non-versioned, and auto discoverable.

Why Service-oriented Architecture on the Front-end?

The benefits of SOA can be illustrated with this real life example from Canopy. At Canopy we have multiple single page applications. The first application is external to the customers and the second is internal, yet both applications share common functionality. That functionality includes among other things, authentication and error logging.

cdn-images-1

Shared libraries between two separate applications. App 1 depends upon shared libs a, b, and c. App 2 depends upon only shared libs a and b.

Overall the design looks good. The code is modularized and shared. The complexities arrive when we start to upgrade the code to different versions. For example, after a short period of time, App 2 (being internal only) is upgraded to a new beta version of the shared lib b. Because the shared a also depends upon b (and we don’t want multiple versions of b bundled) we also create a new version of a. This one change causes a rebuild and deploy of three separate pieces of code: App 2 and shared libs a and b. Our dependency structure is no longer quite so simple.

cdn-images-2

In reality, a duplicate instance of lib a and b exist in both apps. Each app does not point to the same instance of the shared libraries, even when they are the same version. This is more noticeable when the shared libraries have separate versions.

Now imagine a bug in both versions of shared lib b. In order to fix the problem, you will have to republish both versions of a and b as well as c. Also App 1 and App 2 will have to be re-deployed. That is five new versions to publish and two apps to redeploy, all to fix one bug. All downstream dependencies have to be redeployed when a single library is changed. This is deploy dependency hell.

Service oriented architecture avoids these problems in a couple ways. Instead of bundling common dependencies, common code is shared through independent services. Services are not bundled, but rather loaded at run time. This also means that front-end services are not versioned (just like a back-end API). Both App 1 and App 2 load the exact same code for a front-end service.

Introducing sofe

Built upon the new ECMAScript module specification, sofe is a JavaScript library that enables independently deployable JavaScript services to be retrieved at run-time in the browser. Because the new module specification isn’t available within today’s browsers, sofe relies upon System.js to load services at run-time.

You can load a sofe service either with static or asynchronous imports.

// Static imports
import auth from 'auth-service!sofe';
const user = auth.getLoggedInUser();
// Asynchronous imports
System.import('auth-service!sofe').then(auth => auth.getLoggedInUser());

The real power behind sofe is that services are resolved at run-time, making them unversioned. If auth-service is redeployed, it is immediately made available to all upstream dependencies. The above scenario becomes much easier to resolve because there is only one version of each shared library as services. This is powerful because it allows you to deploy once, update everywhere. Also because the code is loaded at run-time, we can also enable developer tools to override what service is loaded into your application. Or in other words, you can test code on production without actually deploying to production.

cdn-images-2

The common dependencies are now services that are independent from the application code. Because services are unversioned, the dependency structure is again flat. Each service can individually be deployed and be available to every upstream dependency.

Obviously not all front-end code should be a service. Services have their own challenges. Specifically your code has to stay backwards compatible. But code can’t always be backwards compatible. Sometimes there needs to be breaking changes. The same problem exists for back-end services. A back-end API has to stay backwards compatible. Breaking changes on the back-end are generally solved by either creating an entirely new (versioned) API or implementing feature toggles within the API itself. The same solution applies to sofe services. An entirely new sofe service can be deployed or feature toggles can exist inside the front-end service. However it is solved, the key point is that services exist outside your application within their own distributable.

Another potential problem for sofe services is performance. Because they are loaded at run-time, performance can become a concern if you synchronously load too many services during bootstrap. Performance degradation can be mitigated by asynchronously loading larger services after the application bootstraps. Despite these challenges, there are many benefits to services on the front-end. The most exciting thing about sofe is there is now an option for services in the browser. You can decide what should and shouldn’t be a service.

Getting started with sofe requires only System.js. But to help you get started we have built sofe to work with a variety of technologies, including webpack, Babel, jspm, and the Chrome Developer Tools. Sofe is also actively used in production at Canopy Tax. We would love feedback on sofe and a number of open source projects that have been built around it. As you approach your next front-end project or look to improve your existing app, consider how it might benefit from service oriented architecture.

Read more about how to get started with sofe here.

+ + \ No newline at end of file diff --git a/blog/rss.xml b/blog/rss.xml index aae5a8cec..e4a954c80 100644 --- a/blog/rss.xml +++ b/blog/rss.xml @@ -4,10 +4,18 @@ single-spa Blog https://single-spa.js.org/blog single-spa Blog - Mon, 27 Nov 2023 00:00:00 GMT + Tue, 12 Dec 2023 00:00:00 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed en + + <![CDATA[December 2023 Core Team Meeting]]> + https://single-spa.js.org/blog/2023/12/12/core-team-meeting-notes + https://single-spa.js.org/blog/2023/12/12/core-team-meeting-notes + Tue, 12 Dec 2023 00:00:00 GMT + + Attendees: Artur Androsovych, Ian Bacher, Jake Hayes, Milan Kovacic

Achievements and Progress from November

  • Successfully released single-spa version 6, marking a significant milestone in the project's development

December 2023 Roadmap

  1. Update dependencies, and set up automation, starting with create-single-spa
  2. Continue work on the consolidation and updates of the example projects

Meeting Notes

  • Welcomed Ian Bacher as a new addition to the core team.
  • Reviewed single-spa roadmap

Initiatives and Goals

  • Adding unit tests to existing projects
  • SystemJS -> ESM migration
  • create-single-spa update (https://github.com/cruft/cruft)
  • Release single-spa 6 as latest ✅
  • Improve shared dependencies management
  • Autopublishes to npm
  • Server rendering enhancements?
  • Support for NextJS, NuxtJS, Remix, create-react-app, and other build tools
  • Consolidate example projects 🚧
  • Feature voting
  • Automated integration tests for popular frameworks
  • Update create-single-spa dependencies 🚧
]]>
+
<![CDATA[single-spa 6]]> https://single-spa.js.org/blog/2023/11/27/single-spa-6 diff --git a/contributors/index.html b/contributors/index.html index 9f80f2b3e..7ee9ec0ad 100644 --- a/contributors/index.html +++ b/contributors/index.html @@ -14,13 +14,13 @@ - - + + - - + + \ No newline at end of file diff --git a/docs/4.x/api/index.html b/docs/4.x/api/index.html index f34582223..2bac3e3fe 100644 --- a/docs/4.x/api/index.html +++ b/docs/4.x/api/index.html @@ -14,8 +14,8 @@ - - + +
@@ -23,7 +23,7 @@ This means importing must happen in one of two ways:

import { registerApplication, start } from 'single-spa';
// or
import * as singleSpa from 'single-spa';

registerApplication

singleSpa.registerApplication(
'appName',
() => System.import('appName'),
location => location.pathname.startsWith('appName'),
);

registerApplication is the most important api your root config will use. Use this function to register any application within single-spa.

Note that if an application is registered from within another application, that no hierarchy will be maintained between the applications.

note

It is described in detail inside of the Configuration docs

arguments

appName: string
App name that single-spa will register and reference this application with, and will be labelled with in dev tools.
applicationOrLoadingFn: () => <Function | Promise>
Must be a loading function that either returns the resolved application or a promise.
activityFn: (location) => boolean
Must be a pure function. The function is called with window.location as the first argument !-- TODO: any only? --> and should return a truthy value whenever the application should be active.
customProps?: Object =
Will be passed to the application during each lifecycle method.

returns

undefined

start

singleSpa.start();

Must be called by your single spa config. Before start is called, applications will be loaded, but will never be bootstrapped, mounted or unmounted. The reason for start is to give you control over the performance of your single page application. For example, you may want to declare registered applications immediately (to start downloading the code for the active ones), but not actually mount the registered applications until an initial AJAX request (maybe to get information about the logged in user) has been completed. In that case, the best performance is achieved by calling registerApplication immediately, but calling start after the AJAX request is completed.

arguments

none

returns

undefined

triggerAppChange

singleSpa.triggerAppChange();

Returns a Promise that will resolve/reject when all apps have mounted/unmounted. This was built for testing single-spa and is likely not needed in a production application.

arguments

none

returns

Promise
Returns a Promise that will resolve/reject when all apps have mounted.
// Three ways of using navigateToUrl
singleSpa.navigateToUrl('/new-url');
singleSpa.navigateToUrl(document.querySelector('a'));
document.querySelector('a').addEventListener(singleSpa.navigateToUrl);
<!-- A fourth way to use navigateToUrl, this one inside of your HTML -->
<a href="/new-url" onclick="singleSpaNavigate()">My link</a>

Use this utility function to easily perform url navigation between registered applications without needing to deal with event.preventDefault(), pushState, triggerAppChange(), etc.

arguments

navigationObj: string | context | DOMEvent
navigationObj must be one of the following types:
  • a url string.
  • a context / thisArg that has an href property; useful for calling singleSpaNavigate.call(anchorElement) with a reference to the anchor element or other context.
  • a DOMEvent object for a click event on a DOMElement that has an href attribute; ideal for the <a onclick="singleSpaNavigate"></a> use case.

returns

undefined

getMountedApps

const mountedAppNames = singleSpa.getMountedApps();
console.log(mountedAppNames); // ['app1', 'app2', 'navbar']

arguments

none

returns

appNames: string[]
Each string is the name of a registered application that is currently MOUNTED.

getAppNames

const appNames = singleSpa.getAppNames();
console.log(appNames); // ['app1', 'app2', 'app3', 'navbar']

arguments

none

returns

appNames: string[]
Each string is the name of a registered application regardless of app status.

getAppStatus

const status = singleSpa.getAppStatus('app1');
console.log(status); // one of many statuses (see list below). e.g. MOUNTED

arguments

appName: string
Registered application name.

returns

appStatus: <string | null>
Will be one of the following string constants, or null if the app does not exist.
NOT_LOADED
app has been registered with single-spa but has not yet been loaded.
LOADING_SOURCE_CODE
's source code is being fetched.
NOT_BOOTSTRAPPED
app has been loaded but not bootstrapped.
BOOTSTRAPPING
the bootstrap lifecycle function has been called but has not finished.
NOT_MOUNTED
app has been loaded and bootstrapped but not yet mounted.
MOUNTING
app is being mounted but not finished.
MOUNTED
app is currently active and is mounted to the DOM.
UNMOUNTING
app is currently being unmounted but has not finished.
UNLOADING
app is currently being unloaded but has not finished.
SKIP_BECAUSE_BROKEN
app threw an error during load, bootstrap, mount, or unmount and has been siloed because it is misbehaving and has been skipped. Other apps will continue on normally.
LOAD_ERROR
The app's loading function returned a promise that rejected. This is often due to a network error attempting to download the JavaScript bundle for the application. Single-spa will retry loading the application after the user navigates away from the current route and then comes back to it.

Note about LOAD_ERROR status

Note that if you're using SystemJS to load your bundles, you need to add the following code to get SystemJS to re-attempt the network request when your loading function calls System.import() on an application in LOAD_ERROR status.

singleSpa.addErrorHandler(err => {
if (singleSpa.getAppStatus(err.appOrParcelName) === singleSpa.LOAD_ERROR) {
System.delete(System.resolve(err.appOrParcelName));
}
});

unloadApplication

// Unload the application right now, without waiting for it to naturally unmount.
singleSpa.unloadApplication('app1');

// Unload the application only after it naturally unmounts due to a route change.
singleSpa.unloadApplication('app1', { waitForUnmount: true });

The purpose of unloading a registered application is to set it back to to a NOT_LOADED status, which means that it will be re-bootstrapped the next time it needs to mount. The main use-case for this was to allow for the hot-reloading of entire registered applications, but unloadApplication can be useful whenever you want to re-bootstrap your application.

Single-spa performs the following steps when unloadApplication is called.

  1. Call the unload lifecyle on the registered application that is being unloaded.
  2. Set the app status to NOT_LOADED
  3. Trigger a reroute, during which single-spa will potentially mount the application that was just unloaded.

Because a registered application might be mounted when unloadApplication is called, you can specify whether you want to immediately unload or if you want to wait until the application is no longer mounted. This is done with the waitForUnmount option.

arguments

appName: string
Registered application name.
options?: {waitForUnmount: boolean = false}
The options must be an object that has a waitForUnmount property. When `waitForUnmount` is `false`, single-spa immediately unloads the specified registered application even if the app is currently mounted. If it is true, single-spa will unload the registered application as soon as it is safe to do so (when the app status is not MOUNTED).

returns

Promise
This promise will be resolved when the registered application has been successfully removed.

checkActivityFunctions

const appsThatShouldBeActive = singleSpa.checkActivityFunctions();
console.log(appsThatShouldBeActive); // ['app1']

const appsForACertainRoute = singleSpa.checkActivityFunctions({
pathname: '/app2',
});
console.log(appsForACertainRoute); // ['app2']

Will call every app's activity function with the mockWindowLocation and give you list of which applications should be mounted with that location.

arguments

mockWindowLocation: string
A string representing a window.location that will be used when calling every application's activity function to test if they return true.

returns

appNames: string[]
Each string is the name of a registered application that matches the provided mockWindowLocation.

addErrorHandler

singleSpa.addErrorHandler(err => {
console.log(err);
console.log(err.appOrParcelName);
console.log(singleSpa.getAppStatus(err.appOrParcelName));
});

Adds a handler that will be called every time an application throws an error during a lifecycle function or activity function. When there are no error handlers, single-spa throws the error to the window.

errorHandler: Function(error: Error)
Must be a function. Will be called with an Error object that additionally has a message and appOrParcelName property.

returns

undefined

removeErrorHandler

singleSpa.addErrorHandler(handleErr);
singleSpa.removeErrorHandler(handleErr);

function handleErr(err) {
console.log(err);
}

Removes the given error handler function.

arguments

errorHandler: Function
Reference to the error handling function.

returns

boolean
true if the error handler was removed, and false if it was not.

mountRootParcel

// Synchronous mounting
const parcel = singleSpa.mountRootParcel(parcelConfig, {
prop1: 'value1',
domElement: document.getElementById('a-div'),
});
parcel.mountPromise.then(() => {
console.log('finished mounting the parcel!');
});

// Asynchronous mounting. Feel free to use webpack code splits or SystemJS dynamic loading
const parcel2 = singleSpa.mountRootParcel(() => import('./some-parcel.js'), {
prop1: 'value1',
domElement: document.getElementById('a-div'),
});

Will create and mount a single-spa parcel.

caution

Parcels do not automatically unmount. Unmounting will need to be triggered manually.

arguments

parcelConfig: Object or Loading Function
[parcelConfig](/docs/4.x/parcels-api#parcel-configuration)
parcelProps: Object with a domElement property
[parcelProps](/docs/4.x/parcels-api#parcel-props)

returns

Parcel object
See Parcels API for more detail.

ensureJQuerySupport

singleSpa.ensureJQuerySupport(jQuery);

jQuery uses event delegation so single-spa must monkey-patch each version of jQuery on the page. single-spa will attempt to do this automatically by looking for window.jQuery or window.$. Use this explicit method if multiple versions are included on your page or if jQuery is bound to a different global variable.

arguments

jQuery?: JQueryFn = window.jQuery
A reference to the global variable that jQuery has been bound to.

returns

undefined

setBootstrapMaxTime

// After three seconds, show a console warning while continuing to wait.
singleSpa.setBootstrapMaxTime(3000);

// After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
singleSpa.setBootstrapMaxTime(3000, true);

Sets the global configuration for bootstrap timeouts.

arguments

millis: number
Number of milliseconds to wait for bootstrap to complete before timing out.
dieOnTimeout: boolean = false

If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

Each registered application can override this behavior for itself.

returns

undefined

setMountMaxTime

// After three seconds, show a console warning while continuing to wait.
singleSpa.setMountMaxTime(3000);

// After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
singleSpa.setMountMaxTime(3000, true);

Sets the global configuration for mount timeouts.

arguments

millis: number
Number of milliseconds to wait for mount to complete before timing out.
dieOnTimeout: boolean = false

If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

Each registered application can override this behavior for itself.

returns

undefined

setUnmountMaxTime

// After three seconds, show a console warning while continuing to wait.
singleSpa.setUnmountMaxTime(3000);

// After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
singleSpa.setUnmountMaxTime(3000, true);

Sets the global configuration for unmount timeouts.

arguments

millis: number
Number of milliseconds to wait for unmount to complete before timing out.
dieOnTimeout: boolean = false

If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

Each registered application can override this behavior for itself.

returns

undefined

setUnloadMaxTime

// After three seconds, show a console warning while continuing to wait.
singleSpa.setUnloadMaxTime(3000);

// After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
singleSpa.setUnloadMaxTime(3000, true);

// don't do a console warning for slow lifecycles until 10 seconds have elapsed
singleSpa.setUnloadMaxTime(3000, true, 10000);

Sets the global configuration for unload timeouts.

arguments

millis: number
Number of milliseconds to wait for unload to complete before timing out.
dieOnTimeout: boolean = false

If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

Each registered application can override this behavior for itself.

warningMillis: number = 1000
Number of milliseconds to wait between console warnings that occur before the final timeout.

returns

undefined


Events

All of the following are custom events fired by single-spa on the window. The event detail property contains the native DOM event that triggered the reroute, such as a PopStateEvent or HashChangeEvent. These events can be handled by using addEventListener, like so:

window.addEventListener('single-spa:before-routing-event', evt => {
const originalEvent = evt.detail;
console.log('single-spa event', originalEvent);
});

before routing event

window.addEventListener('single-spa:before-routing-event', () => {
console.log('single-spa is about to mount/unmount applications!');
});

A single-spa:before-routing-event event is fired before every routing event occurs, which is after each hashchange, popstate, or triggerAppChange, even if no changes to registered applications were necessary.

routing event

window.addEventListener('single-spa:routing-event', () => {
console.log('single-spa finished mounting/unmounting applications!');
});

A single-spa:routing-event event is fired every time that a routing event has occurred, which is after each hashchange, popstate, or triggerAppChange, even if no changes to registered applications were necessary; and after single-spa verified that all apps were correctly loaded, bootstrapped, mounted, and unmounted.

app-change event

window.addEventListener('single-spa:app-change', () => {
console.log(
'A routing event occurred where at least one application was mounted/unmounted',
);
});

A single-spa:app-change event is fired every time that one or more apps were loaded, bootstrapped, mounted, unmounted, or unloaded. It is similar to single-spa:routing-event except that it will not fire unless one or more apps were actually loaded, bootstrapped, mounted, or unmounted. A hashchange, popstate, or triggerAppChange that does not result in one of those changes will not cause the event to be fired.

no-app-change event

window.addEventListener('single-spa:before-routing-event', () => {
console.log(
'A routing event occurred where zero applications were mounted/unmounted',
);
});

When no applications were loaded, bootstrapped, mounted, unmounted, or unloaded, single-spa fires a single-spa:no-app-change event. This is the inverse of the single-spa:app-change event. Only one will be fired for each routing event.

before-first-mount

window.addEventListener('single-spa:before-first-mount', () => {
console.log(
'single-spa is about to mount the very first application for the first time',
);
});

Before the first of any single-spa applications is mounted, single-spa fires a single-spa:before-first-mount event; therefore it will only be fired once ever. More specifically, it fires after the application is already loaded but before mounting.

Suggested use case

remove a loader bar that the user is seeing right before the first app will be mounted.

first-mount

window.addEventListener('single-spa:first-mount', () => {
console.log('single-spa just mounted the very first application');
});

After the first of any single-spa applications is mounted, single-spa fires a single-spa:first-mount event; therefore it will only be fired once ever.

Suggested use case:

log the time it took before the user sees any of the apps mounted.

- - + + \ No newline at end of file diff --git a/docs/4.x/building-applications/index.html b/docs/4.x/building-applications/index.html index f2a0492d3..fd3fac401 100644 --- a/docs/4.x/building-applications/index.html +++ b/docs/4.x/building-applications/index.html @@ -14,8 +14,8 @@ - - + +
@@ -24,7 +24,7 @@ but will not be bootstrapped, mounted or unmounted.
info

Framework-specific helper libraries exist in the single-spa ecosystem to implement these required lifecycle methods. This documentation is helpful for understanding what those helpers are doing, or for implementing your own.

Lifecyle props

Lifecycle functions are called with a props argument, which is an object with some guaranteed information and additional custom information.

function bootstrap(props) {
const {
name, // The name of the application
singleSpa, // The singleSpa instance
mountParcel, // Function for manually mounting
customProps, // Additional custom information
} = props; // Props are given to every lifecycle
return Promise.resolve();
}

Built-in props

Each lifecycle function is guranteed to be called with the following props:

  • name: The string name that was registered to single-spa.
  • singleSpa: A reference to the singleSpa instance, itself. This is intended to allow applications and helper libraries to call singleSpa APIs without having to import it. This is useful in situations where there are multiple webpack configs that are not set up to ensure that only one instance of singleSpa is loaded.
  • mountParcel: The mountParcel function.

Custom props

In addition to the built-in props that are provided by single-spa, you may optionally specify custom props to be passed to an application by providing a fourth argument to registerApplication. These customProps will be passed into each lifecycle method.

root.application.js
singleSpa.registerApplication(
'app1',
() => {},
() => {},
{ authToken: 'd83jD63UdZ6RS6f70D0' },
);
app1.js
export function mount(props) {
console.log(props.customProps.authToken); // do something with the common authToken in app1
return reactLifecycles.mount(props);
}

Some use cases could be to:

  • share a common access token with all child apps
  • pass down some initialization information, like the rendering target
  • pass a reference to a common event bus so each app may talk to each other

Note that when no customProps are provided during registration, props.customProps defaults to an empty object.

Lifecycle helpers

Some helper libraries that implement lifecycle functions for ease of use are available for many popular frameworks/libraries. Learn more on the Ecosystem page.

Load

When registered applications are being lazily loaded, this refers to when the code for a registered application is fetched from the server and executed. This will happen once the registered application's activity function returns a truthy value for the first time. It is best practice to do as little as possible / nothing at all during load, but instead to wait until the bootstrap lifecycle function to do anything. If you need to do something during load, simply put the code into a registered application's main entry point, but not inside of an exported function. For example:

console.log("The registered application has been loaded!");

export async function bootstrap(props) {...}
export async function mount(props) {...}
export async function unmount(props) {...}

Bootstrap

This lifecycle function will be called once, right before the registered application is mounted for the first time.

export function bootstrap(props) {
return Promise.resolve().then(() => {
// One-time initialization code goes here
console.log('bootstrapped!');
});
}

Mount

This lifecycle function will be called whenever the registered application is not mounted, but its activity function returns a truthy value. When called, this function should look at the URL to determine the active route and then create DOM elements, DOM event listeners, etc. to render content to the user. Any subsequent routing events (such as hashchange and popstate) will not trigger more calls to mount, but instead should be handled by the application itself.

export function mount(props) {
return Promise.resolve().then(() => {
// Do framework UI rendering here
console.log('mounted!');
});
}

Unmount

This lifecycle function will be called whenever the registered application is mounted, but its activity function returns a falsy value. When called, this function should clean up all DOM elements, DOM event listeners, leaked memory, globals, observable subscriptions, etc. that were created at any point when the registered application was mounted.

export function unmount(props) {
return Promise.resolve().then(() => {
// Do framework UI unrendering here
console.log('unmounted!');
});
}

Unload

The unload lifecycle is an optionally implemented lifecycle function. It will be called whenever an application should be unloaded. This will not ever happen unless someone calls the unloadApplication API. If a registered application does not implement the unload lifecycle, then it assumed that unloading the app is a no-op.

The purpose of the unload lifecycle is to perform logic right before a single-spa application is unloaded. Once the application is unloaded, the application status will be NOT_LOADED and the application will be re-bootstrapped.

The motivation for unload was to implement the hot-loading of entire registered applications, but it is useful in other scenarios as well when you want to re-bootstrap applications, but perform some logic before applications are re-bootstrapped.

export function unload(props) {
return Promise.resolve().then(() => {
// Hot-reloading implementation goes here
console.log('unloaded!');
});
}

Timeouts

By default, registered applications obey the global dieOnTimeout configuration, but can override that behavior for their specific application. This is done by exporting a timeouts object from the main entry point of the registered application. Example:

app-1.js
export function bootstrap(props) {...}
export function mount(props) {...}
export function unmount(props) {...}

export const timeouts = {
bootstrap: {
millis: 5000,
dieOnTimeout: true,
},
mount: {
millis: 5000,
dieOnTimeout: false,
},
unmount: {
millis: 5000,
dieOnTimeout: true,
},
unload: {
millis: 5000,
dieOnTimeout: true,
},
};

Transitioning between applications

If you find yourself wanting to add transitions as applications are mounted and unmounted, then you'll probably want to tie into the bootstrap, mount, and unmount lifecycle methods. This single-spa transitions repo is a small proof-of-concept of how you can tie into these lifecycle methods to add transitions as your apps mount and unmount.

Transitions for pages within a mounted application can be handled entirely by the application itself. For example, using react-transition-group for React-based projects.

- - + + \ No newline at end of file diff --git a/docs/4.x/code-of-conduct/index.html b/docs/4.x/code-of-conduct/index.html index 217912ef8..4b8d690a3 100644 --- a/docs/4.x/code-of-conduct/index.html +++ b/docs/4.x/code-of-conduct/index.html @@ -14,13 +14,13 @@ - - + +
Version: 4.x

Code of Conduct

This code of conduct outlines our expectations for participants within the single-spa community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community.

Our open source community strives to:

  • Be friendly and patient.
  • Be welcoming: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
  • Be considerate: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into *account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language.
  • Be respectful: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration *to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one.
  • Be careful in the words that you choose: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other *exclusionary behavior aren’t acceptable. This includes, but is not limited to:
    • Violent threats or language directed against another person.
    • Discriminatory jokes and language.
    • Posting sexually explicit or violent material.
    • Posting (or threatening to post) other people’s personally identifying information (“doxing”).
    • Personal insults, especially those using racist or sexist terms.
    • Unwelcome sexual attention.
    • Advocating for, or encouraging, any of the above behavior.
    • Repeated harassment of others. In general, if someone asks you to stop, then stop.
    • When we disagree, try to understand why: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively.
    • Remember that we’re different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.

This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter.

Diversity Statement

We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong.

Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities.

Reporting Issues

If you experience or witness unacceptable behavior—or have any other concerns—please report it by sending a Twitter DM to https://twitter.com/Single_spa. All reports will be handled with discretion. In your report please include:

  • Your contact information.
  • Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link.
  • Any additional information that may be helpful.

After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse.

Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning.

This Code Of Conduct follows the template established by the TODO Group.

- - + + \ No newline at end of file diff --git a/docs/4.x/configuration/index.html b/docs/4.x/configuration/index.html index 050663118..6551312fe 100644 --- a/docs/4.x/configuration/index.html +++ b/docs/4.x/configuration/index.html @@ -14,8 +14,8 @@ - - + +
@@ -40,7 +40,7 @@ the AJAX request is completed.

//single-spa-config.js
import { start } from 'single-spa';

/* Calling start before registering apps means that single-spa can immediately mount apps, without
* waiting for any initial setup of the single page app.
*/
start();

// Register applications....

Two registered applications simultaneously??

Yep, it's possible. And it's actually not that scary if you do it right. And once you do, it's really really powerful. One approach to do this is to create a <div id="app-name"></div> for each app, so that they never try to modify the same DOM at the same time.

- - + + \ No newline at end of file diff --git a/docs/4.x/contributing-overview/index.html b/docs/4.x/contributing-overview/index.html index ea1111bce..0c588986e 100644 --- a/docs/4.x/contributing-overview/index.html +++ b/docs/4.x/contributing-overview/index.html @@ -14,13 +14,13 @@ - - + +
Version: 4.x

Contributing to Single-spa

Thanks for checking out single-spa! We're excited to hear and learn from you.

We've put together the following guidelines to help you figure out where you can best be helpful.

Table of Contents

  1. Types of contributions we're looking for
  2. Ground rules & expectations
  3. How to contribute
  4. Setting up your environment
  5. Community

Types of contributions we're looking for

There are many ways you can directly contribute to the guides (in descending order of need):

  • Examples
  • Helper Libraries (like single-spa-react) for missing frameworks
  • Bug fixes
  • Answering questions in the slack channel
  • new helper packages for frameworks

Interested in making a contribution? Read on!

Ground rules & expectations

Before we get started, here are a few things we expect from you (and that you should expect from others):

  • Be kind and thoughtful in your conversations around this project. We all come from different backgrounds and projects, which means we likely have different perspectives on "how open source is done." Try to listen to others rather than convince them that your way is correct.
  • Please read the single-spa Contributor Code of Conduct. By participating in this project, you agree to abide by its terms.
  • If you open a pull request, please ensure that your contribution passes all tests. If there are test failures, you will need to address them before we can merge your contribution.
  • When adding content, please consider if it is widely valuable. Please don't add references or links to things you or your employer have created as others will do so if they appreciate it.

How to contribute

If you'd like to contribute, start by searching through the issues and pull requests to see whether someone else has raised a similar idea or question.

If you don't see your idea listed, and you think it fits into the goals of this guide, do one of the following:

  • If your contribution is minor, such as a small typo or bug fix, open a pull request.
  • If your contribution is major, such as a new feature, start by opening an issue first. That way, other people can weigh in on the discussion before you do any work.

Setting up your environment

Prerequisites

  1. Git
  2. Node: install version 8.4 or greater
  3. Yarn: See Yarn website for installation instructions
  4. A fork of the single-spa repo
  5. A clone of the repo on your local machine

Installation

  1. cd single-spa to go into the project root
  2. yarn to install single-spa's dependencies

Create a branch

  1. git checkout master from any folder in your local single-spa repository
  2. git pull origin master to ensure you have the latest main code
  3. git checkout -b the-name-of-my-branch (replacing the-name-of-my-branch with a suitable name) to create a branch

Test the change

  1. Run yarn test from the project root.

Push it

  1. git add . && git commit -m "My message" (replacing My message with a commit message, such as Fixed application lifecycles) to stage and commit your changes
  2. git push my-fork-name the-name-of-my-branch
  3. Go to the single-spa repo and you should see recently pushed branches.
  4. Follow GitHub's instructions to submit a new Pull Request.

Community

Discussions about single-spa take place on the single-spa repository's Issues and Pull Requests sections. Anybody is welcome to join these conversations. There is also a slack community for regular updates.

Wherever possible, do not take these conversations to private channels, including contacting the maintainers directly. Keeping communication public means everybody can benefit and learn from the conversation.

- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-angular/index.html b/docs/4.x/ecosystem-angular/index.html index 26545f338..fdef31d36 100644 --- a/docs/4.x/ecosystem-angular/index.html +++ b/docs/4.x/ecosystem-angular/index.html @@ -14,8 +14,8 @@ - - + +
@@ -91,7 +91,7 @@ polyfills once in the root HTML file, instead of once per application.

To load polyfills, you'll need to follow the instructions in the Angular documentation for non-CLI users. Even if you are using Angular CLI, you will need to follow those instructions, since your single-spa root HTML file is not using Angular CLI and that's where the polyfills need to go.

If you're looking for a quick one-liner, try adding this line near the top of your index.html.

<script src="https://unpkg.com/core-js-bundle/minified.js"></script>

Internet Explorer

If you need to support IE11 or older, do the following:

Full example commit to get IE11 support

- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-angularjs/index.html b/docs/4.x/ecosystem-angularjs/index.html index 09b6f0200..1751c6165 100644 --- a/docs/4.x/ecosystem-angularjs/index.html +++ b/docs/4.x/ecosystem-angularjs/index.html @@ -14,8 +14,8 @@ - - + +
@@ -26,7 +26,7 @@ loading function should be System.import('name-of-app'). Make sure to add name-of-app to your import map.

As a global variable

window.myAngularApp = singleSpaAngularJS({
angular: angular,
mainAngularModule: 'app',
uiRouter: true,
preserveGlobal: false,
template: '<my-component />',
})

Your loading function should just be the global variable itself. For example:

singleSpa.registerApplication('my-angular-app', myAngularApp, () => true);

Options

All options are passed to single-spa-angularjs via the opts parameter when calling singleSpaAngularJS(opts). The following options are available:

  • angular: (required) The main angular object, which is generally either exposed onto the window or is available via require('angular') or import angular from 'angular'.
  • domElementGetter: (optional) A function that takes in no arguments and returns a DOMElement. This dom element is where the angular application will be bootstrapped, mounted, and unmounted. If not provided, the default is to create a div and append it to document.body.
  • mainAngularModule: (required) A string that is the name of the angular module that will be bootstrapped by angular. See angular docs for angular.bootstrap().
  • uiRouter: (optional) If you are using angular-ui-router, set this option to either true or to a string value. The string value will be the value of the ui-view HTML attribute. For example, uiRouter: 'core' will be <div ui-view="core" /> whereas uiRouter: true turns into <div ui-view />.
  • preserveGlobal: (optional) A boolean that defaults to false. Set if you want to keep angular on the global even after an app unmounts.
  • elementId: (optional) A string which will be used to identify the element appended to the DOM and bootstrapped by Angular.
  • strictDi: (optional - part of the bootstrap config object) A boolean that defaults to false. Set if you want to enable StrictDi mode
  • template: (optional) An HTML string that will be inserted into the DOM when the app is mounted. The template goes inside of the element returned by domElementGetter. If not provided, no template will be inserted. When using angular-ui-router, you often do not need to use this since ui-router will be putting a template onto the dom for you.

ES5 Example

Check out this example repo

- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-backbone/index.html b/docs/4.x/ecosystem-backbone/index.html index 3aa5a6f5e..a3f3fc8e6 100644 --- a/docs/4.x/ecosystem-backbone/index.html +++ b/docs/4.x/ecosystem-backbone/index.html @@ -14,15 +14,15 @@ - - + +
Version: 4.x

single-spa-backbone

A single-spa helper library which provides lifecycle events for building single-spa applications using Backbone.

npm Package License

There are mostly three styles of creating backbone applications

  1. Using RequireJS which will loads the application and all it's dependencies, including the templates loaded using Handlebars, RequireJS:Text or any other engine.

    If your application is written using this style, then you will have to pass the AppWithRequire parameter as options in the plugin, and choose the flavour to load the app, either through data-main attribute or without it.

  2. Using Backbone and ApplicationPath (Entry point of application) directly as script elements and optionally loading other dependencies.

  3. Loading a single application bundle which includes application dependencies like i.e. Backbone, Require, Underscore, Jquery etc.

NPM package

npm install --save @emtecinc/single-spa-backbone

The npm package can be found here.

Quickstart

Option 1: Using RequireJS with data-main

First, in the single-spa application, run npm install --save @emtec/single-spa-backbone. Then, create an entry file for application like below, assuming the application has some BasePath with AppPath and `RequireJsPath' being relative to the base path.

import singleSpaBackbone from '@emtecinc/single-spa-backbone';

const backBoneLifecycles = singleSpaBackbone({
BasePath: 'appBasePath',
AppWithRequire:
{
IsDataMain: true,
AppPath: 'src/app',
RequireJsPath: 'lib/require.js'
},
DomElementSetter: domElementSetter
});

export const bootstrap = [
backBoneLifecycles.bootstrap,
];

export const mount = [
backBoneLifecycles.mount,
];

export const unmount = [
backBoneLifecycles.unmount,
];


function domElementSetter() {

//use the same element id to render into, in the backbone app
let el = document.getElementById('shell-container');
if (!el) {
el = document.createElement('div');
el.id = 'shell-container';
document.body.appendChild(el);
}

}

DomElementSetter gives you a provision to hook up your callback, and can be used to create a container element in the dom which will be used to load the app.

Please note that this hook up is just a setter and don't expect you to return a value. You need to explicitly use the same element #id in the backbone application to use it as app region or container.

Option 2: Using RequireJS without data-main

IsDataMain will be set to false in this case

import singleSpaBackbone from '@emtecinc/single-spa-backbone';

const backBoneLifecycles = singleSpaBackbone({
BasePath: 'appBasePath',
AppWithBackboneJs:
{
AppPath: 'src/app',
BackboneJsPath: 'lib/backbone.js'
},
DomElementSetter: domElementSetter
});

export const bootstrap = backBoneLifecycles.bootstrap;

export const mount = backBoneLifecycles.mount;

export const unmount = backBoneLifecycles.unmount;

function domElementSetter() {

//use the same element id to render into, in the backbone app
let el = document.getElementById('shell-container');
if (!el) {
el = document.createElement('div');
el.id = 'shell-container';
document.body.appendChild(el);
}

}

Option 3: Load Backbone app using production build

import singleSpaBackbone from '@emtecinc/single-spa-backbone';

const backBoneLifecycles = singleSpaBackbone({
BasePath: 'appBasePath',
App:
{
AppPath: 'src/app'
},
DomElementSetter: domElementSetter
});

export const bootstrap = backBoneLifecycles.bootstrap;

export const mount = backBoneLifecycles.mount;

export const unmount = backBoneLifecycles.unmount;


function domElementSetter() {

//use the same element id to render into, in the backbone app
let el = document.getElementById('shell-container');
if (!el) {
el = document.createElement('div');
el.id = 'shell-container';
document.body.appendChild(el);
}

}

Options

All options are passed to single-spa-backbone via the userOptions parameter when calling singleSpaBackbone(userOptions). The following properties are available:

  • BasePath (required) : The base path of the backbone application. Mostly it will be the public path from where the app is server and other paths will be relative to this. This parameter expects a string type. optional

  • AppWithRequire (required) : This parameter takes an object and expects below properties:

    • IsDataMain (optional) : This parameter takes a boolean value and is used to specify whether require js will use data-main to load the app.
    • AppPath (required) : This parameter takes a string value and specifies the path of JavaScript file, which is entry point of your application and will be booted up using RequireJs. The path is relative to BasePath.
    • RequireJsPath (required) : This parameter takes a string value and takes the path of the RequireJs file and is relative to BasePath.
    • DependenciesJsPaths (optional) : This is an optional parameter takes an array of strings. It can be used to optionally provide a list of JavaScript paths which you want to load in the browser.
  • AppWithBackboneJs (optional) : This parameter takes an object and expects below properties:

    • AppPath (required) : This parameter takes a string value and specifies the path of JavaScript file, which is entry point of your application and will be booted up using Backbone Js. The path is relative to BasePath.
    • BackboneJsPath (required) : This parameter takes a string value and takes the path of the Backbone Js file and is relative to BasePath.
    • DependenciesJsPaths (optional) : This is an optional parameter takes an array of strings. It can be used to optionally provide a list of JavaScript paths which you want to load in the browser.
  • App (optional) : This parameter takes an object and expects below properties:

    • AppPath (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is the production build of your backbone application. The path is relative to BasePath.

Note : Out of AppWithRequire, AppWithBackboneJs and App only one is required

  • DomElementSetter (optional) : This is an optional parameter and can be mostly used to create a dom element, whose id can be later used in the backbone app to load the application. However, you can freely use this callback for any other purpose. It is called before anything else.
- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-cycle/index.html b/docs/4.x/ecosystem-cycle/index.html index 7f63833d1..c23202538 100644 --- a/docs/4.x/ecosystem-cycle/index.html +++ b/docs/4.x/ecosystem-cycle/index.html @@ -14,13 +14,13 @@ - - + +
Version: 4.x

single-spa-cycle

single-spa-cycle is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Cycle.js. Check out the single-spa-cycle github.

Installation

npm install --save @pcmnac/single-spa-cycle

Quickstart

In your project's entry file, add the following:


import {run} from '@cycle/run'
import {makeDOMDriver} from '@cycle/dom'
import singleSpaCycle from '@pcmnac/single-spa-cycle';
import rootComponent from './root.component.js';

const cycleLifecycles = singleSpaCycle({
run,
rootComponent,
drivers: { DOM: makeDOMDriver(document.getElementById('main-content'))}, // or { DOM: makeDOMDriver('#main-content')}
});

export const bootstrap = cycleLifecycles.bootstrap;
export const mount = cycleLifecycles.mount;
export const unmount = cycleLifecycles.unmount;

Options

All options are passed to single-spa-cycle via the opts parameter when calling singleSpaCycle(opts). The following options are available:

  • run: (required) Cycle.js run function.
  • drivers: (required) Drivers (including DOM Driver) to be used by your Cycle.js root component.
  • rootComponent: (required) The top level Cycle.js component which will be rendered
- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-ember/index.html b/docs/4.x/ecosystem-ember/index.html index 16b2978b0..c218c3061 100644 --- a/docs/4.x/ecosystem-ember/index.html +++ b/docs/4.x/ecosystem-ember/index.html @@ -14,14 +14,14 @@ - - + +
Version: 4.x

single-spa-ember

single-spa-ember is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Ember.js. Check out the single-spa-ember github.

It is available on npm as single-spa-ember, and also available on bower as single-spa-ember in case you want to use it with ember cli and need to use bower.

Overview

When you are building an ember application that you want to work as a single-spa application, there are five things you need to implement:

Single-spa-ember will help you implement all of those except for the activity function.

Note that the loading and activity functions are part of the single-spa root application, whereas the bootstrap, mount, and unmount functions are part of a single-spa application

API

loadEmberApp

loadEmberApp(appName, appUrl, vendorUrl) is a function that helps you implement the loading function for your ember application. appName and appUrl are both strings and both required, whereas vendorUrl is an optional string.

// In the single-spa root application

import {registerApplication} from 'single-spa';
import {loadEmberApp} from 'single-spa-ember';

const appName = 'ember-app';
const loadingFunction = () => loadEmberApp(appName, '/dist/ember-app/assets/ember-app.js', '/dist/ember-app/assets/vendor.js');
const activityFunction = location => location.hash.startsWith('ember');

registerApplication(appName, loadingFunction, activityFunction);

singleSpaEmber

Single-spa-ember will implement the single-spa lifecyle functions for you. To use it, you call the default export as a function with a configuration object, which returns an object that has bootstrap, mount, and unmount lifecycle functions on it. The provided configuration object has the following options:

// In the ember application
import singleSpaEmber from 'single-spa-ember/src/single-spa-ember';

const emberLifecycles = singleSpaEmber({
appName: 'ember-app', // required
createOpts: { // See https://www.emberjs.com/api/ember/2.14.1/classes/Ember.Application
rootElement: '#ember-app',
},
});

export const bootstrap = emberLifecycles.bootstrap;
export const mount = emberLifecycles.mount;
export const unmount = emberLifecycles.unmount;

Usage with ember cli

For the most part, you can get applications that use ember cli to work pretty seamlessly with single-spa. Maybe the biggest thing you'll have to worry about is that ember-cli assumes that it controls the entire HTML page, whereas a single-spa application does not. However, usually we can achieve equivalent behavior by just loading the vendor and app bundles into the HTML page dynamically, instead of baking them right into the HTML page. Below is a description of the known things you should do when setting up an ember-cli application with single-spa:

First, since the ember cli only supports dependencies from bower, you'll need to do:

  • bower init
  • bower install single-spa-ember --save

Add the following options to your ember-cli-build.js file:

/* eslint-env node */
'use strict';

const EmberApp = require('ember-cli/lib/broccoli/ember-app');

module.exports = function(defaults) {
let app = new EmberApp(defaults, {
autoRun: false, // Set autoRun to false, because we only want the ember app to render to the DOM when single-spa tells it to.
storeConfigInMeta: false, // We're making a single-spa application, which doesn't exclusively own the HTML file. So we don't want to have to have a `<meta>` tag for the ember environment to be initialized.
fingerprint: {
customHash: null, // This is optional, just will make it easier for you to have the same url every time you do an ember build.
},
// Add options here
});

// Tell ember how to use the single-spa-ember library
app.import('bower_components/single-spa-ember/amd/single-spa-ember.js', {
using: [
{transformation: 'amd', as: 'single-spa-ember'},
],
});

return app.toTree();
};

In your single-spa root application (which is separate from anything generated by ember cli):

// root-application.js
import * as singleSpa from 'single-spa';
import {loadEmberApp} from 'single-spa-ember';

singleSpa.registerApplication('ember-app', loadingFunction, activityFunction);

function activityFunction(location) {
// Only render the ember app when the url hash starts with ember
return location.hash.startsWith('ember');
}

// single-spa-ember helps us load the script tags and give the ember app module to single-spa.
function loadingFunction() {
const appName = 'ember-app';
const appUrl = '/dist/ember-app/assets/ember-app.js';
const vendorUrl = '/dist/ember-app/assets/vendor.js'; // Optional if you have one vendor bundle used for many different ember apps
return loadEmberApp(appName, appUrl, vendorUrl);
}

In your app.js file (that is generated by ember cli)

// app.js (the ember application)
import Ember from 'ember';
import Resolver from './resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';
import singleSpaEmber from 'single-spa-ember';

// This part is generated by the ember cli
const App = Ember.Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver
});

loadInitializers(App, config.modulePrefix);

export default App;

// This is the single-spa part
const emberLifecycles = singleSpaEmber({
App, // required
appName: 'ember-app', // required
createOpts: { // optional
rootElement: '#ember-app',
},
})

// Single-spa lifecycles.
export const bootstrap = emberLifecycles.bootstrap;
export const mount = emberLifecycles.mount;
export const unmount = emberLifecycles.unmount;
- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-html-web-components/index.html b/docs/4.x/ecosystem-html-web-components/index.html index 49a29be8c..f65379a8e 100644 --- a/docs/4.x/ecosystem-html-web-components/index.html +++ b/docs/4.x/ecosystem-html-web-components/index.html @@ -14,8 +14,8 @@ - - + +
@@ -23,7 +23,7 @@ single-spa applications or parcels.

Installation

npm install --save single-spa-html

# or
yarn add single-spa-html

Alternatively, you can use single-spa-html from a CDN as a global variable:

<script src="https://cdn.jsdelivr.net/npm/single-spa-html"></script>

Note that you might want to lock down the package to a specific version. See here for how to do that.

Usage

Via npm

import singleSpaHtml from 'single-spa-html';

const htmlLifecycles = singleSpaHtml({
template: '<x-my-web-component></x-my-web-component>',
})

export const bootstrap = htmlLifecycles.bootstrap;
export const mount = htmlLifecycles.mount;
export const unmount = htmlLifecycles.unmount;

Via cdn

Example usage when installed via CDN:

const webComponentApp = window.singleSpaHtml.default({
template: props => `<x-my-web-component attr="${props.attr}"></x-my-web-component>`,
})

singleSpa.registerApplication('name', webComponentApp, () => true)

API / Options

single-spa-html is called with an object that has the following properties:

  • template (required): An HTML string or a function that returns a string. The function will be called with the single-spa custom props. The returned string is injected into the DOM during the single-spa mount lifecycle.
  • domElementGetter (optional): A function that returns the dom element container into which the HTML will be injected. If omitted, a default implementation is provided that wraps the template in a <div> that is appended to document.body.
- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-inferno/index.html b/docs/4.x/ecosystem-inferno/index.html index a1a8773f3..b9f5db80d 100644 --- a/docs/4.x/ecosystem-inferno/index.html +++ b/docs/4.x/ecosystem-inferno/index.html @@ -14,13 +14,13 @@ - - + +
Version: 4.x

single-spa-inferno

single-spa-inferno is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Inferno. Check out the single-spa-inferno github.

Quickstart

First, in the application, run npm install --save single-spa-inferno. Then, add the following to your application's entry file.

import Inferno from 'inferno';
import rootComponent from './path-to-root-component.js';
import singleSpaInferno from 'single-spa-inferno';

const infernoLifecycles = singleSpaInferno({
Inferno,
createElement,
rootComponent,
domElementGetter: () => document.getElementById('main-content'),
});

export const bootstrap = infernoLifecyles.bootstrap;
export const mount = infernoLifecyles.mount;
export const unmount = infernoLifecyles.unmount;

Options

All options are passed to single-spa-inferno via the opts parameter when calling singleSpaInferno(opts). The following options are available:

  • inferno: (required) The main Inferno object, which is generally either exposed onto the window or is available via require('inferno') or import Inferno from 'inferno'.
  • createElement: (required) The default export from Inferno's inferno-create-element package.
  • rootComponent: (required) The top level Inferno component which will be rendered.
  • domElementGetter: (required) A function that takes in no arguments and returns a DOMElement. This dom element is where the Inferno application will be bootstrapped, mounted, and unmounted.
- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-leaked-globals/index.html b/docs/4.x/ecosystem-leaked-globals/index.html index ec029d6bd..1296ef8b1 100644 --- a/docs/4.x/ecosystem-leaked-globals/index.html +++ b/docs/4.x/ecosystem-leaked-globals/index.html @@ -14,8 +14,8 @@ - - + +
@@ -41,7 +41,7 @@ export an array for your lifecycle functions instead of exporting just a function.

import singleSpaLeakedGlobals from 'single-spa-leaked-globals';

// Use single-spa-angularjs, single-spa-backbone, etc to get your framework specific lifecycles
const frameworkLifecycles = ...

const leakedGlobalsLifecycles = singleSpaLeakedGlobals({
globalVariableNames: ['$', 'jQuery', '_'],
})

export const bootstrap = [
leakedGlobalsLifecycles.bootstrap,
frameworkLifecycles.bootstrap,
]

export const mount = [
// Make sure leaked globals lifecycles' mount function is **before** other lifecycles' mount
// This is so the global vars are available when the framework mounts
leakedGlobalsLifecycles.mount,
frameworkLifecycles.mount,
]

export const unmount = [
leakedGlobalsLifecycles.mount,
// Make sure leaked globals lifecycles' unmount function is **after** other lifecycles' unmount
// This is so the global vars are still available during the framework unmount lifecycle function.
frameworkLifecycles.unmount,
]

If you're using single-spa-leaked-globals as a global variable itself:

const leakedGlobalsLifecycles = window.singleSpaLeakedGlobals.default({
globalVariableNames: ['_'],
})

API / Options

single-spa-leaked-globals is called with an object with the following properties:

  • globalVariableNames (required): An array of strings. Each string is the name of a global variable that should be removed when the application is unmounted, and added back when the application is mounted.
- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-preact/index.html b/docs/4.x/ecosystem-preact/index.html index 82af5ebf5..2e8a8445a 100644 --- a/docs/4.x/ecosystem-preact/index.html +++ b/docs/4.x/ecosystem-preact/index.html @@ -14,13 +14,13 @@ - - + +
Version: 4.x

single-spa-preact

single-spa-preact is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Preact. Check out the single-spa-preact github.

Installation

npm install --save preact

Quickstart

In your project's entry file, add the following:

import preact from 'preact';
import rootComponent from './path-to-root-component.js';
import singleSpaPreact from 'single-spa-preact';

const preactLifecycles = singleSpaPreact({
preact,
rootComponent,
domElementGetter: () => document.getElementById('main-content'),
});

export const bootstrap = preactLifecycles.bootstrap;
export const mount = preactLifecycles.mount;
export const unmount = preactLifecycles.unmount;

Options

All options are passed to single-spa-preact via the opts parameter when calling singleSpaPreact(opts). The following options are available:

  • preact: (required) The main Preact object, which is generally either exposed onto the window or is available via require('preact') or import preact from 'preact'.
  • rootComponent: (required) The top level preact component which will be rendered
  • domElementGetter: (required) A function that takes in no arguments and returns a DOMElement. This dom element is where the Preact application will be bootstrapped, mounted, and unmounted.
- - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-react/index.html b/docs/4.x/ecosystem-react/index.html index 6f3285161..637699d0c 100644 --- a/docs/4.x/ecosystem-react/index.html +++ b/docs/4.x/ecosystem-react/index.html @@ -14,8 +14,8 @@ - - + +
@@ -33,7 +33,7 @@ single-spa's root mountParcel function, so that single-spa can keep track of the parent-child relationship and automatically unmount the application's parcels when the application unmounts. Note that if the <Parcel> component is being rendered by a single-spa application that uses single-spa-react, it is unnecessary to pass in the prop, since <Parcel> can get the prop from SingleSpaContext
  • handleError (optional): A function that will be called with errors thrown by the parcel. If not provided, errors will be thrown on the window, by default.
  • parcelDidMount (optional): A function that will be called when the parcel finishes loading and mounting.
  • Examples

    import Parcel from 'single-spa-react/parcel'
    import * as parcelConfig from './my-parcel.js'

    // config is required. The parcel will be mounted inside of the
    // of a div inside of the react component tree
    <Parcel
    config={parcelConfig}

    wrapWith="div"
    handleError={err => console.error(err)}

    customProp1="customPropValue2"
    customProp2="customPropValue2"
    />

    // If you pass in an appendTo prop, the parcel will be mounted there instead of
    // to a dom node inside of the current react component tree
    <Parcel>
    config={parcelConfig}
    wrapWith="div"
    appendTo={document.body}
    />

    // You can also pass in a "loading function" as the config.
    // The loading function must return a promise that resolves with the parcel config.
    // The parcel will be mounted once the promise resolves.
    <Parcel
    config={() => import('./my-parcel.js')}
    wrapWith="div"
    />

    // If you are rendering the Parcel component from a single-spa application, you do not need to pass a mountParcel prop.
    // But if you have a separate react component tree that is not rendered by single-spa-react, you **must** pass in a mountParcel prop
    // In general, it is preferred to use an application's mountParcel function instead of the single-spa's root mountParcel function,
    // so that single-spa can keep track of the parent-child relationship and automatically unmount the application's parcels when the application
    // unmounts
    <Parcel
    mountParcel={singleSpa.mountParcel}
    config={parcelConfig}
    wrapWith="div"
    />

    Create React App

    See FAQ for CRA

    - - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-riot/index.html b/docs/4.x/ecosystem-riot/index.html index 8c7674eb5..0fd7acd6d 100644 --- a/docs/4.x/ecosystem-riot/index.html +++ b/docs/4.x/ecosystem-riot/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 4.x

    single-spa-riot

    single-spa-riot is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with riot. Check out the single-spa-riot github.

    NPM

    Build Status

    minified

    Installation

    npm install --save single-spa-riot

    Usage

    import * as Riot from 'riot';
    import singleSpaRiot from 'single-spa-riot';
    import App from './App.riot'

    const riotLifecycles = singleSpaRiot({
    rootComponent: Riot.component(App),
    domElementGetter: () => document.getElementById('#app')
    });

    export const bootstrap = riotLifecycles.bootstrap;

    export const mount = riotLifecycles.mount;

    export const unmount = riotLifecycles.unmount;

    Options

    All options are passed to single-spa-riot via the opts parameter when calling singleSpaRiot(opts). The following options are available:

    • domElementGetter: (required) the callback to get root component mount element.
    • rootComponent: (optional and replaces appOptions.loadRootComponent) the root riot component.
    • loadRootComponent: (optional and replaces appOptions.rootComponent) A promise that resolves with your root component. This is useful for lazy loading.
    - - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-svelte/index.html b/docs/4.x/ecosystem-svelte/index.html index d0e956e61..dbc24000d 100644 --- a/docs/4.x/ecosystem-svelte/index.html +++ b/docs/4.x/ecosystem-svelte/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 4.x

    single-spa-svelte

    single-spa-svelte is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with svelte. Check out the single-spa-svelte github.

    Quickstart

    First, in the single-spa application, run npm install --save single-spa-svelte. Then, create an entry file with the following.

    import singleSpaSvelte from 'single-spa-svelte';
    import myRootSvelteComponent from 'my-root-svelte-component.js';

    const svelteLifecycles = singleSpaSvelte({
    component: myRootSvelteComponent,
    domElementGetter: () => document.getElementById('svelte-app'),
    data: { someData: 'data' }
    });

    export const bootstrap = svelteLifecycles.bootstrap;
    export const mount = svelteLifecycles.mount;
    export const unmount = svelteLifecycles.unmount;

    Options

    All options are passed to single-spa-svelte via the opts parameter when calling singleSpaSvelte(opts). The following options are available:

    • component: (required) The root component that will be rendered. This component should be compiled by svelte and not an iife.
    • domElementGetter: (required) A function which will return a dom element. The root component will be mounted in this element.

    Svelte-specific options

    • anchor: (optional) A child of the dom element identified by domElementGetter to render the component immediately before
    • hydrate: (optional) See the svelte Creating a component documentation
    • intro: (optional) If true, will play transitions on initial render, rather than waiting for subsequent state changes
    • props: (optional) An object of properties to supply to the component
    - - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem-vue/index.html b/docs/4.x/ecosystem-vue/index.html index c7fe0bc78..91648ea71 100644 --- a/docs/4.x/ecosystem-vue/index.html +++ b/docs/4.x/ecosystem-vue/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -34,7 +34,7 @@ option should be provided exactly the same as in the example above.

    Custom Props

    single-spa custom props are added to your App component as appOptions.data, and are accessible via vm.$data. See this Vue documentation for more information on appOptions.data.

    - - + + \ No newline at end of file diff --git a/docs/4.x/ecosystem/index.html b/docs/4.x/ecosystem/index.html index b267d01cb..71219025b 100644 --- a/docs/4.x/ecosystem/index.html +++ b/docs/4.x/ecosystem/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -27,7 +27,7 @@ loading functions.

    import {registerApplication} from 'single-spa';

    registerApplication('app-name', () => import('./my-app.js'), activeWhen);

    function activeWhen() {
    return window.location.pathname.indexOf('/my-app') === 0;
    }

    SystemJS

    Since SystemJS is a Promise-based loader, the way to lazy load your registered applications is straightforward:

    import {registerApplication} from 'single-spa';

    // Import the registered application with a SystemJS.import call
    registerApplication('app-name-1', () => SystemJS.import('./my-app.js'), activeWhen);

    // Alternatively, use the more out-of-date System.import (instead of SystemJS.import)
    registerApplication('app-name-2', () => System.import('./my-other-app.js'), activeWhen);

    function activeWhen() {
    return window.location.pathname.indexOf('/my-app') === 0;
    }

    Webpack 1

    With webpack 1, there is no support for Promise-based code splitting. Instead, we have to either wrap a require.ensure in a Promise, or just give up on lazy loading completely.

    import {registerApplication} from 'single-spa';
    import app1 from './app1'; // Not lazy loading with code splitting :(

    // Giving up on lazy loading and code splitting :(
    registerApplication('app-1', () => Promise.resolve(app1), activeWhen);

    // Still doing code splitting! But verbose :(
    registerApplication('app-2', app2InPromise, activeWhen);

    /* Unfortunately, this logic cannot be abstracted into a generic
    * function that handles wrapping require.ensure in a promise for
    * any dynamically imported module. This is because webpack needs to
    * be able to statically analyze the code and find all of the require.ensure
    * calls at build-time, so you can't pass variables into require.ensure.
    */
    function app2InPromise() {
    return new Promise((resolve, reject) => {
    require.ensure(['./app-2.js'], require => {
    try {
    resolve(require('./app-2.js'));
    } catch(err) {
    reject(err);
    }
    });
    });
    }

    function activeWhen() {
    return window.location.pathname.indexOf('/my-app') === 0;
    }
    - - + + \ No newline at end of file diff --git a/docs/4.x/examples/index.html b/docs/4.x/examples/index.html index f6509efc4..ceee55cd9 100644 --- a/docs/4.x/examples/index.html +++ b/docs/4.x/examples/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 4.x

    Single-spa Examples

    There are a variety of single-spa example repositories, each for different use cases:

    Have your own starter repo? Submit a PR to add yours to this list.

    - - + + \ No newline at end of file diff --git a/docs/4.x/faq/index.html b/docs/4.x/faq/index.html index 3704a6530..5567a9921 100644 --- a/docs/4.x/faq/index.html +++ b/docs/4.x/faq/index.html @@ -14,14 +14,14 @@ - - + +
    Version: 4.x

    Frequently Asked Questions

    What does single-spa do?

    single-spa is a top level router. When a route is active, it downloads and executes the code for that route.

    The code for a route is called an "application", and each can (optionally) be in its own git repository, have its own CI process, and be separately deployed. The applications can all be written in the same framework, or they can be implemented in different frameworks.

    We recommend a setup that uses in-browser ES modules + import maps (or SystemJS to polyfill these if you need better browser support). This setup has several advantages:

    1. Common libraries are easy to manage, and are only downloaded once. If you're using SystemJS, you can also preload them for a speed boost as well.
    2. Sharing code / functions / variables is as easy as import/export, just like in a monolithic setup
    3. Lazy loading applications is easy, which enables you to speed up initial load times
    4. Each application (AKA microservice, AKA ES module) can be independently developed and deployed. Teams are enabled to work at their own speed, experiment (within reason as defined by the organization), QA, and deploy on thier own schedules. This usually also means that release cycles can be decreased to days instead of weeks or months
    5. A great developer experience (DX): go to your dev environment and add an import map that points the application's url to your localhost. See "What is the DX like?" for more details.

    What is the impact to performance?

    When setup in the recommended way, your code performance and bundle size will be nearly identical to a single application that has been code-split. The major differences will be the addition of the single-spa library (and SystemJS if you chose to use it). Other differences mainly come down to the difference between one (webpack / rollup / etc.) code bundle and in-browser ES modules.

    Can I have only one version of (React, Vue, Angular, etc.) loaded?

    Yes, and it's highly recommended you do so! Using the recommended setup, you configure your import map so that your library is defined only once. Then, tell each application to not bundle that library; instead, the library will be given to you at runtime in the browser. See webpack’s externals (and other bundlers have similar options) for how to do this.

    You do have the option of not excluding those libraries (for example if you want to experiment with a newer version or a different library) but be aware of the effect that will have on user's bundle sizes and application speed.

    What are import maps?

    Import maps improve the developer experience of in-browser ES modules by allowing you to write something like import React from "react" instead of needing to use an absolute or relative URL for your import statement. The same is also true of importing from other single-spa applications, e.g. import {MyButton} from "styleguide". The import-map spec is currently in the process of being accepted as a web standard and at the time of writing has been implemented in Chrome, and a polyfill for browsers >= IE11 has been implemented by SystemJS >= 3.0. Also see the recommended setup

    How can I share application state between applications?

    In general, we recommend trying to avoid this — it couples those apps together. If you find yourself doing this frequently between apps, you may want to consider that those separate apps should actually just be one app.

    Generally, it’s better to just make an API request for the data that each app needs, even if parts of it have been requested by other apps. For this situation, placing IDs in the URL (e.g. /contact/{contactId}) is probably the best and easiest way to share this information.

    In practice, if you’ve designed your application boundaries correctly, there will end up being very little application state that is truly shared — for example, your friends list has different data requirements than your social feed.

    However, that doesn’t mean it can’t be done. Here are several ways:

    1. Create a shared API request library that can cache requests and their responses. If somone hits an API, and then that API is hit again by another application, it just uses the cache
    2. Expose the shared state as an export, and other libraries can import it. Observables (like RxJS) are useful here since they can stream new values to subscribers
    3. Use custom browser events to communicate
    4. Use cookies, local/session storage, or other similar methods for storing and reading that state. These methods work best with things that don't change often, e.g. logged-in user info.

    Please note that this is just talking about sharing application state: sharing functions, components, etc. is as easy as an export in one project and an import in the other. See import map documentation for more details

    Should I use frontend microservices?

    If you’ve ran into some of the headaches a monolithic repo has, then you should really consider it.

    In addition, if your organization is setup in a Spotify-type model (e.g. where there are autonomous squads that own full-stack features) then microservices on the frontend will fit very well into your setup.

    However, if you’re just starting off and have a small project or a small team, we would recommend you stick with a monolith (i.e. not microservices) until you get to the point that scaling (e.g. organizational scaling, feature scaling, etc.) is getting hard. Don’t worry, we’ll be here to help you migrate when you get there.

    Can I use more than one framework?

    Yes. However, it’s something you’ll want to consider hard because it splits your front-end organization into specialities that aren’t compatible (e.g. a React specialist may have problems working in an Angular app), and also causes more code to be shipped to your users.

    However, it is great for migrations away from an older or unwanted library, which allows you to slowly rip out the code in the old application and replace it with new code in the new library (see Google results for the strangler pattern).

    It also is a way to allow large organizations to experiment on different libraries without a strong commitment to them.

    Just be conscious of the effect it has on your users and their experience using your app.

    What is the developer experience (DX) like?

    If you're using the recommended setup for single-spa, you'll simply be able to go to your development website, add an import map that points to your locally-running code, and refresh the page.

    There's a library that you can use, or you can even just do it yourself - you'll note that the source code is pretty simple. The main takeaway is that you can have multiple import maps and the latest one wins - you add an import map that overrides the default URL for an application to point to your localhost.

    We're also looking at providing this functionality as part of the Chrome/Firefox browser extension.

    Finally, this setup also enables you to do overrides in your production environment. It obviously should be used with caution, but it does enable a powerful way of debugging problems and validating solutions.

    As a point of reference, nearly all developers we've worked with prefer the developer experience of microservices + single-spa over a monolithic setup.

    Can each single-spa application have its own git repo?

    Yes! You can even give them their own package.json, webpack config, and CI/CD process, using SystemJS to bring them all together in the browser.

    Can single-spa applications be deployed independently?

    Yes! See next section about CI/CD.

    What does the CI/CD process look like?

    In other words, how do I build and deploy a single-spa application?

    With the recommended setup, the process generally flows like this:

    1. Bundle your code and upload it to a CDN.
    2. Update your dev environment's import map to point to the that new URL. In other words, your import map used to say "styleguide": "cdn.com/styleguide/v1.js" and now it should say "styleguide": "cdn.com/styleguide/v2.js"

    Some options on how to update your import map include:

    • Server render your index.html with the import map inlined. This does not mean that your DOM elements need to all be server rendered, but just the <script type="systemjs-importmap> element. Provide an API that either updates a database table or a file local to the server.
    • Have your import map itself on a CDN, and use import-map-deployer or similar to update the import map during your CI process. This method has a small impact on performance, but is generally easier to setup if you don't have a server-rendered setup already. (You can also preload the import map file to help provide a small speed boost). See example travis.yml. Other CI tools work, too.

    Create React App

    Currently Create React App (CRA) requires ejecting or using a tool to modify the webpack config. You can also consider some of the popular alternatives to CRA.

    When you use the recommended setup the following things need to change (as of CRA v3.0.1):

    1. Remove Webpack optimizations block, because they add multiple webpack chunks that don't load each other
    2. Remove html-webpack plugin
    3. Change output.libraryTarget to System, UMD, or AMD.

    CRA does not allow you to change those items without ejecting or using another tool.

    Code splits

    Single spa supports code splits. There are so many ways to code split we won't be able to cover them all, but if you're using the recommended setup with webpack you'll need to do at least two things:

    1. Set the __webpack_public_path__ dynamically so webpack knows where to fetch your code splits (webpack assumes they are located at the root of the server and that isn't always true in a single-spa application). Both solutions below should be the very first import of your application in order to work.

      import { setPublicPath } from 'systemjs-webpack-interop';

      setPublicPath('name-of-module-in-import-map');
      • For SystemJS 2-5: Find a code example here
    2. Set either output.jsonpFunction or output.library to ensure that each app's webpack doesn't collide with other apps' webpack. jsonpFunction is preferred.

    Does single-spa require additional security considerations?

    No. single-spa does not add, deviate, or attempt to bypass any browser JavaScript security measures. The security needs of your applications are the same as if you did not use single-spa.

    Outside of that, web applications may use the following resources that have their own security considerations that you may need to become familiar with:

    - - + + \ No newline at end of file diff --git a/docs/4.x/getting-started-overview/index.html b/docs/4.x/getting-started-overview/index.html index 305ed432b..b3068bf86 100644 --- a/docs/4.x/getting-started-overview/index.html +++ b/docs/4.x/getting-started-overview/index.html @@ -14,15 +14,15 @@ - - + +
    Version: 4.x

    Getting Started with single-spa

    JavaScript Microfrontends

    single-spa is a framework for bringing together multiple JavaScript microfrontends in a frontend application. Architecting your frontend using single-spa enables many benefits, such as:

    Demo and Examples

    Visit the live demo for an example that highlights single-spa usage. The source code is available in the single-spa-examples repository.

    Also, you can check out a simple webpack starter project which is simpler, and may be easier to understand and get started with.

    Architectural Overview

    single-spa takes inspiration from modern framework component lifecycles by applying lifecycles to entire applications. It was born out of Canopy's desire to use React + react-router instead of being forever stuck with our AngularJS + ui-router application, and now single-spa supports almost any framework. Since JavaScript is notorious for the short life of its many frameworks, we decided to make it easy to use whichever frameworks you want.

    single-spa apps consist of the following:

    1. Applications, each of which is an entire SPA itself (sort of). Each application can respond to url routing events and must know how to bootstrap, mount, and unmount itself from the DOM. The main difference between a traditional SPA and single-spa applications is that they must be able to coexist with other applications, and they do not each have their own HTML page.

      For example, your React or Angular SPAs are applications. When active, they listen to url routing events and put content on the DOM. When inactive, they do not listen to url routing events and are totally removed from the DOM.

    2. A single-spa-config, which is the HTML page and the JavaScript that registers applications with single-spa. Each application is registered with three things:

      • A name
      • A function to load the application's code
      • A function that determines when the application is active/inactive

    How hard will it be to use single-spa?

    single-spa works with ES5, ES6+, TypeScript, Webpack, SystemJS, Gulp, Grunt, Bower, ember-cli, or really any build system available. You can npm install it, jspm install it, or even just use a <script> tag if you prefer.

    Our objective is to make using single-spa as easy as possible. But we should also point out that this is an advanced architecture that is different from how front-end applications are typically done.

    If you're not starting your application from scratch, you'll have to migrate your SPA to become a single-spa application.

    single-spa works in Chrome, Firefox, Safari, IE11, and Edge.

    Isn't single-spa sort of a redundant name?

    Yep.

    Documentation

    The documentation is divided into several sections:

    You can help improve the single-spa website by sending pull requests to the single-spa.js.org repository.

    Simple Usage

    For a full example, check out this simple webpack example or this starting from scratch tutorial.

    To create a single-spa application, you will need to do three things:

    1. Create an HTML file.
    <html>
    <body>
    <script src="single-spa-config.js"></script>
    </body>
    </html>
    1. Create a single-spa-config. Check out the docs for more detail.
    import * as singleSpa from 'single-spa';

    const appName = 'app1';

    /* The loading function is a function that returns a promise that resolves with the JavaScript application module.
    * The purpose of it is to facilitate lazy loading -- single-spa will not download the code for a application until it needs to.
    * In this example, import() is supported in webpack and returns a Promise, but single-spa works with any loading function that returns a Promise.
    */
    const loadingFunction = () => import('./app1/app1.js');

    /* single-spa does some top-level routing to determine which application is active for any url. You can implement this routing any way you'd like.
    * One useful convention might be to prefix the url with the name of the app that is active, to keep your top-level routing simple.
    */
    const activityFunction = location => location.pathname.startsWith('/app1');

    singleSpa.registerApplication(appName, loadingFunction, activityFunction);
    singleSpa.start();
    1. Create an application. Check out the docs for more detail.
    //app1.js

    let domEl;

    export function bootstrap(props) {
    return Promise
    .resolve()
    .then(() => {
    domEl = document.createElement('div');
    domEl.id = 'app1';
    document.body.appendChild(domEl);
    });
    }

    export function mount(props) {
    return Promise
    .resolve()
    .then(() => {
    // This is where you would normally use a framework to mount some ui to the dom. See https://single-spa.js.org/docs/ecosystem.html.
    domEl.textContent = 'App 1 is mounted!'
    });
    }

    export function unmount(props) {
    return Promise
    .resolve()
    .then(() => {
    // This is normally where you would tell the framework to unmount the ui from the dom. See https://single-spa.js.org/docs/ecosystem.html
    domEl.textContent = '';
    })
    }

    API

    Read more at single-spa API and application api.

    Contributing

    The main purpose of this repository is to continue to evolve single-spa, making it better and easier to use. Development of single-spa, and the single-spa ecosystem happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving single-spa.

    Code of Conduct

    single-spa has adopted a Code of Conduct that we expect project participants to adhere to. Please read the full text so that you can understand what actions will and will not be tolerated.

    Contributing Guide

    Read our contributing guide to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to single-spa.

    Who's Using This?

    See user showcase.

    Is your company or project using single-spa? Let us know by submitting a PR to this section!

    - - + + \ No newline at end of file diff --git a/docs/4.x/glossary/index.html b/docs/4.x/glossary/index.html index bf441b782..2c904c842 100644 --- a/docs/4.x/glossary/index.html +++ b/docs/4.x/glossary/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 4.x

    Glossary

    Activity Function
    Application
    Application
    Helpers
    are a library that already implements single-spa lifecycle functions for a specific framework.
    Lifecycles
    module loader
    Microservices
    registerApplication
    - - + + \ No newline at end of file diff --git a/docs/4.x/migrating-angularJS-tutorial/index.html b/docs/4.x/migrating-angularJS-tutorial/index.html index 00cc18700..822b13cb3 100644 --- a/docs/4.x/migrating-angularJS-tutorial/index.html +++ b/docs/4.x/migrating-angularJS-tutorial/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 4.x

    Migrating an Existing AngularJS Project

    Project Setup

    You can find the code needed to follow along here. You can find the completed code for this tutorial here.

    Since this is an older project, there are a few dependencies you might not currently have installed on your machine. This project requires that you have Grunt, Bower, and Sass installed to get up and running.

    npm install -g grunt-cli
    npm install -g bower
    npm install -g sass

    With those dependencies installed, you can get started by typing the following command into your terminal:

    git clone git@github.com:alocke12992/single-spa-angular-migration-starter.git
    cd single-spa-angular-migration-starter
    yarn
    cd public/assets
    bower install

    Run grunt in the root directory to fire up a server at http://localhost:8080.

    Step One: Create a single-spa config

    The single spa config consists of all code that is not part of a registered application. Ideally, this only includes an HTML file and a JavaScript file that registers single-spa applications. It is best practice to keep your single spa config as small as possible and to simply defer to single-spa to manage all of the applications. The single spa config should not be doing client-side HTML rendering nor should it be responding to routing events such as hashchange or popstate. Instead, all of that functionality should be taken care of either by single-spa itself or by a single-spa application.

    It is required to register applications with single-spa. This enables single-spa to know how and when to bootstrap, mount and unmount an application. We will be creating a single-spa.config.js file to house all of our single-spa logic.

    Inside the public/ folder, create a single-spa-config.js file.

    touch public/single-spa.config.js

    a) importing without using import

    Since we are using an older version of Angular, and we do not have access to babel, we cannot use import or even require() to obtain access to the single-spa library. One way around this is to include a <script> tag in the project's HTML file which will provide us access to the library. Single-spa is hosted on https://unpkg.com/ and when called, creates a global variable.

    In public/index.html, add the following script tag at the bottom of the <head>

    <head>
    <!-- ... -->
    <script src="https://unpkg.com/single-spa"></script>
    </head>

    b) Connect the config file

    To get single-spa connected, we will need to include a script tag connecting the HTML file to single-spa.config.js (we will be building the single-spa.config.js file in the next step).

    Add the following <script> at the bottom of index.html

    <body>
    <!-- ... -->
    <script src="/assets/js/angular_drums.min.js"></script>
    <script src="single-spa.config.js"></script>
    </body>

    Step Two: Register the application

    Now that our application has access to the single-spa library, we can use window.singleSpa to call specific functions within the library. In order to register an application with single-spa we call the registerApplication() api and include the application name, a loadingFunction and an activityFunction.

    Finally, the start() api must be called by your single spa config in order for applications to actually be mounted. Before start() is called, applications will be loaded, but not bootstrapped/mounted/unmounted.

    Note that since we are not using Babel, we cannot use the ES6 const, let, or arrow functions.

    Start by stubbing out the registration function by adding the following in public/single-spa.config.js:

    public/single-spa.config.js
    window.singleSpa.registerApplication(
    'drum-machine',
    loadingFunction,
    function activityFunction() {
    return true;
    },
    );

    window.singleSpa.start();

    The second argument in registerApplication, loadingFunction, must be a function that returns a promise (or an "async function"). The function will be called with no arguments when it's time to load the application for the first time. The returned promise must be resolved with the application. We will be creating this in the next step.

    The third argument, activityFunction, must be a pure function. The function is provided window.location as the first argument, and returns a truthy value whenever the application should be active. In this case we have set the activity function to return true. This will set our SPA to always be mounted regardless of the location. Later, if we wanted to add other SPAs to our single-spa web application, we can change the activity function to return based on location.hash.startsWith('#/someRoute').

    Step Three: Setup Lifecycle Functions

    Since we have registered our application, single-spa will be listening for the application to bootstrap and mount. We can use the single-spa-angularjs helper library which will handle generic lifecycle hooks (bootstrap, mount and unmount) for registered angularjs applications.

    To gain access to the single-spa-angularjs library, we will need to include another <script> tag in our HTML file.

    Add the following in public/index.html at the very bottom of the <head>.

    <head>
    ...
    <script src="https://unpkg.com/single-spa"></script>
    <script src="https://unpkg.com/single-spa-angularjs"></script>
    </head>
    info

    You may want to read more about the importance of <script> tag order.

    Now that our application has access to the single-spa-angularjs library, we can set up the application lifecycle. Add the following code:

    public/single-spa.config.js
    var drumMachineApp = window.singleSpaAngularjs.default({
    angular: window.angular,
    domElementGetter: function() {
    // A div with this id will be added to our index.html later, in step four
    return document.getElementById('drum-machine')
    },
    mainAngularModule: 'AngularDrumMachine',
    uiRouter: false,
    preserveGlobal: true,
    // This template will be built in step four
    template: '<display-machine />',
    })
    ...

    With our app's lifecycle function defined, we can now include it in our registerApplication function.

    public/single-spa.config.js
    ...
    window.singleSpa.registerApplication(
    'drum-machine',
    drumMachineApp,
    function activityFunction(location) {
    return true;
    }
    )

    window.singleSpa.start();

    Step Four: Adjust your HTML file

    Since most existing SPAs are used to having control of an index.html file for their css, fonts, third party script-tags, etc., it's likely that you'll have to do some work to make sure all of those keep on working when your SPA becomes an html-less application.

    In this case, we are going to have to make a few adjustments to the current index.html to make sure that the SPA is not mounted until single-spa tells it to.

    a) Prevent auto bootstrapping

    Currently, our index.html contains two hurdles we will need to overcome to allow single-spa to control the DOM. The first is the auto-bootstrap directive ng-app at the top of the HTML file. If left in the HTML file, ng-app will force the entire application to automatically bootstrap and render, overriding the single-spa lifecycle functions. To fix this, we simply need to remove ng-app from the HTML file and then allow single-spa-angularjs to call the bootstrap function instead (recall that we set this up in Step Three).

    In index.html remove ng-app="AngularDrumMachine.

    <!DOCTYPE html>
    <!-- Remove ng-app -->
    <html lang="en-us">
    <!-- ... -->
    </html>

    b) Create a Template

    The second challenge is that the index.html currently holds the entire application template. Since HTML will automatically render anything in the file, we will need to pull all of the SPAs logic out of the HTML file and replace it with a new <div /> containing the id single-spa will use to mount the application. To do this, we will create a new template that we can then provide to the single-spa-angularjs lifecycle function.

    Create a new directory inside of public/assets called templates/. Then create a new template called display-machine.template.html.

    mkdir public/assets/templates
    touch public/assets/templates/display-machine.template.html

    Then, remove lines 24 - 83 from index.html and paste them inside of display-machine.template.html. You will also need to remove the ng-view directive in the <body> tag. Once removed, add a new <div> containing the id single-spa will use to mount the SPA.

    index.html should now look like this:

    <body>
    <!-- Notice that this id was set when we built the lifecycle functions in step three -->
    <div id="drum-machine" />
    <script src="/assets/js/angular_drums.min.js"></script>
    <script src="single-spa.config.js"></script>
    </body>

    The new template display-machine.template.html should look like this:

    <a class="show-for-medium-up" href="https://github.com/dougjohnston/angular-drum-machine">
    <img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png"
    alt="Fork me on GitHub">
    </a>
    <div id="container" class="row">
    <div class="large-12 large-centered columns" ng-controller="DrumMachineCtrl as dm">
    <header>
    <h1>Angular Drum Machine</h1>
    <div id="loading-wrap" ng-show="loading">
    <div class="loading loading-outer">
    <div class="loading loading-inner"></div>
    </div>
    <h3>loading...</h3>
    </div>
    </header>

    <div id="controls" ng-hide="loading">
    <button id="play" class="button small success radius" ng-click="playLoop()">Play</button>
    <button id="pause" class="button small alert radius" ng-click="stopLoop()">Stop</button>
    <button id="reset" class="button small radius" ng-click="resetLoop()">Clear</button>
    <div id="readout">
    <span id="tempo">
    <input type="range" min="60" max="180" ng-change="updateTempo()" ng-model="tempo">
    <input id="bpmEdit" type="text" min="60" max="180" ng-change="updateTempo()" ng-blur="CloseEdit()" ng-model="tempo" style="width:50px;display:none;">
    <span id="bpm" ng-click="EditBPM()">{{tempo}} bpm</span>
    </span>
    </div>
    </div>

    <ul id="dm-grid" ng-hide="loading">
    <li class="dm-header">
    <ul>
    <li class='instrument-name'></li>
    <li class='beat-num' data-ng-repeat="i in [] | range:machine.gridLength()">
    <div ng-class="{true: 'current-beat'}[($index + 1) === machine.currentBeat()]">{{$index + 1}}</div>
    </li>
    </ul>
    </li>
    <li class="dm-row" ng-repeat="row in machine.rows()">
    <ul class="instrument">
    <li class='instrument-name'>
    {{row.getInstrument().getName()}}
    <br>
    <span>{{row.getInstrument().getDescription()}}
    <span>
    </li>
    <li data-ng-repeat="beat in row.getBeats()">
    <button class="btn" ng-class="{'btn-on':beat.isActive()}" ng-click="beat.toggle()">
    <div></div>
    </button>
    </li>
    </ul>
    </li>
    </ul>

    <footer ng-hide="loading">
    Developed by
    <a href="http://www.dojosto.com">Doug Johnston</a> using
    <a href="http://angularjs.org/">AngularJS</a>.
    <br>Drum loops provided by
    <a href="http://www.musicradar.com/news/tech/free-music-samples-download-loops-hits-and-multis-217833/65">Music Radar</a>.
    </footer>

    <aside class="msg-play show-for-large-up" ng-class="{'faded': fade_msg_play}" ng-hide="loading"></aside>
    </div>

    c) Create a Directive

    Per the AngularJS conventions, we will need to create a directive in order to "compile" our new HTML template. Let's start by creating a new directives/ folder inside public/app to house a new display-machine.directive.js

    mkdir public/app/directives
    touch public/app/directives/display-machine.directive.js

    Then, inside of display-machine.directive.js we will register our new directive on the "AngularDrumMachine" module, restrict the directive to be triggered by a class name using the E option, and tell it to load our template using the templateUrl option.

    Add the following code to public/app/directives/display-machine.directive.js`*:

    'use strict';

    angular.module('AngularDrumMachine').directive('displayMachine', [
    function() {
    return {
    restrict: 'E',
    templateUrl: 'assets/templates/display-machine.template.html',
    };
    },
    ]);

    That's it

    Head back to the console and start up the server on http://localhost:8080 by running grunt from the root directory.

    Inspect the page and notice that our drum-machine app is now being rendered inside of the <div id="drum-machine"/> we created. Technically, we are back to square one, with a fully functioning SPA. However, now that our SPA is a registered single-spa application we can take advantage of single-spa's functionality by building additional applications to mount side by side with our current AngularJS SPA.

    Feel free to start using that new JavaScript framework everyone has been talking about.

    - - + + \ No newline at end of file diff --git a/docs/4.x/migrating-existing-spas/index.html b/docs/4.x/migrating-existing-spas/index.html index 3fc9d0c1b..48dadef0a 100644 --- a/docs/4.x/migrating-existing-spas/index.html +++ b/docs/4.x/migrating-existing-spas/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -37,7 +37,7 @@ application. It is best to try to put all that you can into the JavaScript bundle, but your escape hatch is to put the things you need into your single spa config.

    - - + + \ No newline at end of file diff --git a/docs/4.x/migrating-react-tutorial/index.html b/docs/4.x/migrating-react-tutorial/index.html index c358b8430..43abd1e2c 100644 --- a/docs/4.x/migrating-react-tutorial/index.html +++ b/docs/4.x/migrating-react-tutorial/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 4.x

    Migrating an Existing React Project

    Project Setup

    You can find the code needed to follow along here. You can find the completed code for this tutorial here.

    Get started by cloning the starter pack, moving into the project and initializing the package manager of your choice so we can install the single-spa library. For this tutorial, we will be using yarn.

    git clone git@github.com:alocke12992/migrating-to-single-spa-react-starter.git
    cd migrating-to-single-spa-react-starter
    yarn # or npm install
    yarn add single-spa # or npm install --save single-spa

    Run yarn start from the root directory to fire up the server at http://localhost:3000.

    Step One: Set up the single-spa config

    The single-spa config consists of all code that is not part of a registered application. Ideally, this only includes an HTML file and a JavaScript file that registers single-spa applications. It is best practice to keep your single spa config as small as possible and to simply defer to single-spa to manage all of the applications.

    Usually, when using webpack with React, we recommend setting your single-spa config as the entry point in your webpack.config.js (see also the "Setup Webpack" example). However, this application was built using create-react-app, so we don't have access to the webpack.config.js without ejecting.

    To avoid having to eject, we are going to hijack the current entry point, src/index.js so we can use it to register our SPA as a single-spa application.

    Start by removing everything except registerServiceWorker.

    src/index.js
    import registerServiceWorker from './registerServiceWorker';

    registerServiceWorker();

    Step Two: Register the Application

    Now that we have prepared index.js to function as our single-spa config, we can begin to register the application. It is required to register applications with single-spa. This enables single-spa to know how and when to bootstrap, mount and unmount an application.

    In order to register an application with single-spa we call the registerApplication() api and include the application name, a loadingFunction and an activityFunction.

    Finally, the start() api must be called by your single spa config in order for applications to actually be mounted. Before start() is called, applications will be loaded, but not bootstrapped/mounted/unmounted.

    In src/index.js, start by importing the registerApplication and start functions:

    src/index.js
    import registerServiceWorker from './registerServiceWorker';
    import { registerApplication, start } from 'single-spa';

    registerServiceWorker();

    With our functions imported, we can now register an application with single-spa and call start():

    src/index.js
    import registerServiceWorker from './registerServiceWorker';
    import { registerApplication, start } from 'single-spa';

    registerApplication(
    'root', // Name of this single-spa application
    loadingFunction, // Our loading function
    activityFunction, // Our activity function
    );

    start();
    registerServiceWorker();

    The second argument in registerApplication, loadingFunction, must be a function that returns a promise (or an "async function"). The function will be called with no arguments when it's time to load the application for the first time. The returned promise must be resolved with the application. We will be creating this in the next step.

    The third argument, activityFunction, must be a pure function. The function is provided window.location as the first argument, and returns a truthy value whenever the application should be active. In this case we have set the activity function to return true. This will set our SPA to always be mounted regardless of the location. Later, if we wanted to add other SPAs to our single-spa web application, we can change the activity function to return based on location.hash.startsWith('#/someRoute'). See the "Starting From Scratch" tutorial for an example of how to set up routing between multiple SPAs.

    Step Three: Setup Lifecycle Functions

    Since we have registered our application, single-spa will be listening for the application to bootstrap and mount. We can use the single-spa-react helper library to make use of the generic React lifecycle hooks. See the registered application lifecycle docs to learn more about each lifecycle function.

    For this tutorial, we will be implementing the required lifecycle functions in a new root.app.js file within the src/ folder. From the root directory, run the following code to install the single-spa-react helper library and create the new file:

    yarn add single-spa-react
    touch src/root.app.js

    During this process, we need to establish a rootComponent, which is the top level React component to be rendered. In this case src/containers/App.js has already been designated as the top level component. If you recall, we removed this from the index.js file so we could set up our single-spa config.

    Finally, we will use the domElementGetter() function to return a DOMElement where the application will be bootstrapped, mounted, and unmounted. Notice that our SPA already has an HTML file in the public/ folder containing a <div /> with and id of root.

    Set up the registered application lifecycle functions by adding the following to src/root.app.js:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import singleSpaReact from 'single-spa-react';
    import App from './containers/App.js';

    const reactLifecycles = singleSpaReact({
    React,
    ReactDOM,
    rootComponent: App,
    domElementGetter,
    });

    export const bootstrap = [reactLifecycles.bootstrap];

    export const mount = [reactLifecycles.mount];

    export const unmount = [reactLifecycles.unmount];

    function domElementGetter() {
    // This is where single-spa will mount our application
    return document.getElementById('root');
    }

    Step Four: Connect to single-spa Config

    Head back to the single-spa config in src/index.js to add a loading function for the registered application by importing root.app.js.

    note

    It is important to note that you do not have to use a loading function and instead can simply pass in the application config object directly to the registerApplication function. However, with Webpack 2+, we can take advantage of its support for code splitting with import() in order to easily lazy-load registered applications when they are needed. Think about your project's build when deciding which route to take.

    src/index.js
    import registerServiceWorker from './registerServiceWorker';
    import { registerApplication, start } from 'single-spa';

    registerApplication(
    'root',
    () => import('./root.app.js'),
    () => true,
    );

    start();
    registerServiceWorker();

    That's it

    Head back to the console and start up the server on http://localhost:3000 by running yarn start from the root directory.

    Inspect the page and notice that our SPA is now being rendered inside of the <div id="root"/>. Technically, we are back to square one, with a fully functioning SPA. However, now that our SPA is a registered single-spa application we can take advantage of single-spa's functionality by building additional applications to mount side by side with our current React SPA.

    Feel free to start using that new Javacript framework everyone has been talking about.

    - - + + \ No newline at end of file diff --git a/docs/4.x/parcels-api/index.html b/docs/4.x/parcels-api/index.html index 6da6bb66b..13bb5b972 100644 --- a/docs/4.x/parcels-api/index.html +++ b/docs/4.x/parcels-api/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ They both return a parcel object. The parcel object contains all additional exposed methods.

    Parcel Props

    When mounting a parcel the second argument is props, a JavaScript object of properties to be passed to the parcel. This object must have a domElement prop, which is the dom node that the parcel will mount into.

    const parcelProps = {
    customerId: 7,
    numberOfTasks: 42,
    domElement: document.createElement('div')
    }

    mountParcel

    applicationProps.mountParcel(parcelConfig, parcelProps). Each application is provided a mountParcel function. The main advantage to using an applications mountParcel function is that parcels mounted via an applications mountParcel will be automatically unmounted when the application is unmounted.

    The first argument may be either an object or a function that returns a promise that resolves with the object (a loading function).

    // Synchronous mounting
    const parcel1 = applicationProps.mountParcel(parcelConfig, parcelProps);

    // Asynchronous mounting. Feel free to use webpack code splits or SystemJS dynamic loading
    const parcel2 = applicationProps.mountParcel(() => import('./some-parcel'), parcelProps);

    mountRootParcel

    The mountRootParcel method will mount the parcel but unmount must be called manually.

    Parcel Object

    The parcel object contains the following functions and methods:

    unmount

    parcel.unmount() returns a promise that resolves once the parcel is successfully unmounted. The promise may throw an error which needs to be handled.

    mount

    parcel.mount() returns a promise that resolves once the parcel is successfully mounted. The promise can throw an error which needs to be handled.

    update

    parcel.update(props) allows you to change the props passed into a parcel. Note that not all parcels support being updated. The update function returns a promise that resolves when the parcel is finished updating. See other documentation and example for more information.

    const parcel = singleSpa.mountRootParcel(parcelConfig, parcelProps);
    parcel.update(newParcelProps);

    getStatus

    parcel.getStatus() returns a string of that parcels status. The string status is one of the following:

    • NOT_BOOTSTRAPPED: The parcel has not been bootstrapped
    • BOOTSTRAPPING: The parcel is bootstrapping but has not finished
    • NOT_MOUNTED: The parcel has bootstrapped, but is not mounted
    • MOUNTED: The parcel is currently active and mounted to the DOM
    • UNMOUNTING: The parcel is unmounting, but has not finished
    • UPDATING: The parcel is currently being updated, but has not finished
    • SKIP_BECAUSE_BROKEN: The parcel threw an error during bootstrap, mount, unmount, or update. Other parcels may continue normally, but this one will be skipped.

    loadPromise

    parcel.loadPromise() returns a promise that will resolve once the parcel has been loaded.

    bootstrapPromise

    parcel.bootstrapPromise() returns a promise that will resolve once the parcel has been bootstrapped.

    mountPromise

    parcel.mountPromise() returns a promise that will resolve once the parcel has been mounted. This is helpful for knowing exactly when a parcel has been appended to the DOM

    unmountPromise

    parcel.unmountPromise() returns a promise that will resolve once the parcel has been unmounted.

    - - + + \ No newline at end of file diff --git a/docs/4.x/parcels-overview/index.html b/docs/4.x/parcels-overview/index.html index c45c15e83..d9fe36722 100644 --- a/docs/4.x/parcels-overview/index.html +++ b/docs/4.x/parcels-overview/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -34,7 +34,7 @@ We could do any number of things to share the functionality between application 1 and 2:

    • If both are written in the same framework we could export/import components.
    • We could reimplement creating a contact (loss of cohesion)
    • We could use single-spa parcels.

    Exporting a parcel from App1 that wraps the createContact modal component gives us the ability to share components and behavior across disparate frameworks, without losing application cohesion. App1 can export a modal as a single-spa parcel and App2 can import the parcel and use it easily. One major advantage is that in the below example the parcel/modal from App1 that is being used by App2 will also be unmounted, without unmounting/mounting of App1.

    // App1
    export const AddContactParcel = {
    bootstrap: bootstrapFn,
    mount: mountFn,
    unmount: unmountFn,
    }

    // App2
    // get the parcel configuration in this case I'm using systemJS and react
    ...
    componentDidMount() {
    SystemJS.import('App1').then(App1 => {
    const domElement = document.body
    App2MountProps.mountParcel(App1.AddContactParcel, {domElement})
    })
    }
    ...

    mountRootParcel vs mountParcel

    Single spa exposes two APIs for working with parcels. These API's are differentiated primarily by the context in which the parcel is created and how to access the API's

    mountRootParcelmountParcel
    contextsingleSpaapplication
    unmount conditionmanual onlymanual + application unmount
    api locationsingleSpa named exportprovided in lifecycle prop

    Which should I use?

    In general we suggest using the application-aware mountParcel API. mountParcel allows you to treat the parcel just like a component inside your application without considering what framework it was written in and being forced to remember to call unmount.

    How do I get the mountParcel API?

    In order to keep the function contextually bound to an application it is provided to the application as a lifecycle prop. You will need to store and manage that function yourself in your application.

    Example of storing the application specific mountParcel API:

    // App1
    let mountParcel
    export const bootstrap = [
    (props) => {
    mountParcel = props.mountParcel
    return Promise.resolve()
    },
    // more bootstrap lifecycles if necessary
    ]
    ...

    note: some libraries (such as react) support a framework specific context that makes it easy to store/manage. In those cases we've written some helper methods to abstract away the need to manage and store the mountParcel method.

    - - + + \ No newline at end of file diff --git a/docs/4.x/separating-applications/index.html b/docs/4.x/separating-applications/index.html index 611e57429..78e8e914d 100644 --- a/docs/4.x/separating-applications/index.html +++ b/docs/4.x/separating-applications/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -33,7 +33,7 @@ padding-left: 1em; }
    Comparison of front-end system architectures
    MonorepoNPM modulesModule loading
    Difficulty to set upEasyMediumHard
    Separate code repositoriesNoNo
    Separate buildsNo
    Separate deploymentsNo
    Examples
    - - + + \ No newline at end of file diff --git a/docs/4.x/starting-from-scratch/index.html b/docs/4.x/starting-from-scratch/index.html index 4b1d22d90..7d67c6bae 100644 --- a/docs/4.x/starting-from-scratch/index.html +++ b/docs/4.x/starting-from-scratch/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 4.x

    Starting From Scratch

    single-spa allows you to build micro frontends that coexist and can each be written with their own framework. This will allow you and your team to:

    1. Use multiple frameworks on the same page. See the single-spa ecosystem for more info
    2. Write code using a new framework, without rewriting your existing application
    3. Lazy load code for improved initial load time.

    Single-spa can be used with just about any build system or JavaScript framework, but this tutorial will focus on creating a web app with Webpack, React, and AngularJS. Our tutorial puts everything into a single code repository, but it is also possible to have separate code repositories for each of your applications.

    For this tutorial we will be creating the following applications to showcase the power and usefulness of single-spa:

    1. home: a React app using React Router
    2. navBar: a React app that always displays top-level navigation
    3. angularJS: an AngularJS app using angular-ui-router

    The complete code for this example is in the single-spa-simple-example repository.

    note

    We encourage you to read through all the single-spa docs to become familiar with the entire single-spa setup. Visit the single-spa Github, the help section, or our community Slack channel for more support.

    1. Initial setup

    info

    For this tutorial, we will be using yarn but npm has its own equivalent commands and can be used almost interchangibly.

    Create a new folder for this project and navigate into it. Initialize a new project using your package manager, and then install single-spa as a dependency. Then create a src/ folder to hold all of our micro-service applications, with each in their own folder.

    mkdir single-spa-simple-example && cd single-spa-simple-example
    yarn init # or npm init
    yarn add single-spa # or npm install --save single-spa
    mkdir src

    1.a Setup Babel

    We will be using Babel to compile our code. Install it and some additional dependencies using:

    yarn add --dev @babel/core @babel/preset-env @babel/preset-react @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-object-rest-spread

    Next create a .babelrc file and paste in the following:

    .babelrc
    {
    "presets": [
    ["@babel/preset-env", {
    "targets": {
    "browsers": ["last 2 versions"]
    }
    }],
    ["@babel/preset-react"]
    ],
    "plugins": [
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-object-rest-spread"
    ]
    }

    Learn more about what each of these packages do by visiting the Babel docs.

    1.b Setup Webpack

    note

    It is important to point out that you do not have to use Webpack in order use single-spa. Learn more about Separating applications and the different ways you can use single-spa for your specific build.

    Run the following commands to add Webpack, Webpack plugins, and loaders.

    # Webpack core
    yarn add webpack webpack-dev-server webpack-cli --dev
    # Webpack plugins
    yarn add clean-webpack-plugin --dev
    # Webpack loaders
    yarn add style-loader css-loader html-loader babel-loader --dev

    Learn more about these Webpack plugins and loaders at their respective documentation pages.

    In the root of your project create a new file name webpack.config.js and paste in the following code:

    webpack.config.js
    const path = require('path');
    const webpack = require('webpack');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');

    module.exports = {
    mode: 'development',
    entry: {
    // Set the single-spa config as the project entry point
    'single-spa.config': './single-spa.config.js',
    },
    output: {
    publicPath: '/dist/',
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
    },
    module: {
    rules: [
    {
    // Webpack style loader added so we can use materialize
    test: /\.css$/,
    use: ['style-loader', 'css-loader'],
    },
    {
    test: /\.js$/,
    exclude: [path.resolve(__dirname, 'node_modules')],
    loader: 'babel-loader',
    },
    {
    // This plugin will allow us to use AngularJS HTML templates
    test: /\.html$/,
    exclude: /node_modules/,
    loader: 'html-loader',
    },
    ],
    },
    node: {
    fs: 'empty',
    },
    resolve: {
    modules: [path.resolve(__dirname, 'node_modules')],
    },
    plugins: [
    // A webpack plugin to remove/clean the output folder before building
    new CleanWebpackPlugin(),
    ],
    devtool: 'source-map',
    externals: [],
    devServer: {
    historyApiFallback: true,
    },
    };

    1.c Add npm scripts

    The last step in our project set up is to include a couple scripts in our package.json to run webpack-dev-server and to create a production build. Add the following to your package.json:

    package.json
    "scripts": {
    "start": "webpack-dev-server --open",
    "build": "webpack --config webpack.config.js -p"
    },

    2. Create the HTML file

    Our goal in this step will be to create a single-spa config. The single-spa config file is where your applications are initialized, and an HTML page will request this config.

    You’ll want to keep your single-spa config as small as possible since it is the master controller and could easily become a maintenance bottleneck. You don’t want to be constantly changing both the single-spa config and the child applications.

    2.a Create index.html

    Create an index.html file the root directory. Inside this file, we'll be adding a div element for each application, each with a unique ID. Mounting each application to a different point allows us to maintain them completely separated and so that they never try to modify the same DOM.

    Paste in the following HTML markup:

    index.html
    <html>
    <head></head>
    <body>
    <div id="navBar"></div>
    <div id="home"></div>
    <div id="angularJS"></div>
    </body>
    </html>

    2.b Include scripts and stylesheets

    For styling, we will be using the Materialize framework. We can enable all of our applications to access the Materialize library by including the styles and scripts in index.html.

    Additionally, to enable single-spa, we will need to include a script tag that references single-spa.config.js in index.html. We will be adding and populating this file in the next step. Webpack outputs our built code to dist/ so that will be the path of single-spa.config.js.

    index.html
    <html>
    <head>
    <!-- Materialize -->
    <link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/css/materialize.min.css"
    />
    <link
    href="https://fonts.googleapis.com/icon?family=Material+Icons"
    rel="stylesheet"
    />
    </head>
    <body>
    <div id="navBar"></div>
    <div id="home"></div>
    <div id="angularJS"></div>

    <!-- jQuery -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
    <!-- Materialize -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/js/materialize.min.js"></script>
    <!-- import the single-spa config file -->
    <script src="/dist/single-spa.config.js"></script>
    </body>
    </html>

    3. Registering applications

    Registering applications is how we tell single-spa when and how to bootstrap, mount, and unmount an application.

    Create a new file called single-spa.config.js in the root directory. Let's start by registering the home application.

    single-spa.config.js
    import { registerApplication, start } from 'single-spa';

    registerApplication(
    // Name of our single-spa application
    'home',
    // loadingFunction
    () => {},
    // activityFunction
    location =>
    location.pathname === '' ||
    location.pathname === '/' ||
    location.pathname.startsWith('/home'),
    );

    start();

    The above code needs explanation. In order to register an application with single-spa, import and call the registerApplication function; and include the application name, a loadingFunction, and an activityFunction as parameters.

    loadingFunction must be a function that returns a Promise (or an async function). The function will be called with no arguments when loading the application for the first time. The returned promise must resolve with the application code. We will come back to this in Step 4.d after creating the home application.

    activityFunction must be a function that returns a truthy value that represents whether the application should be active, and must be a pure function. The function is provided window.location as the first argument. The most common scenario is to determine if an application is active by looking at window.location, but not always. In this case, home will be our root application so it will be shown at the root url paths as well as and url pathname that begins with /home.

    Lastly, we also import the start function from the single-spa package and call it in order for applications be mounted. Before start is called, applications will be loaded into the browser but not bootstrapped/mounted/unmounted. Learn more about the start() api here.

    4. Create the home application

    4.a Setup home

    Start by adding a home/ folder inside of the src/ directory. Then inside of home/ we will create two files: home.app.js and root.component.js.

    mkdir src/home && cd src/home
    touch home.app.js root.component.js

    The home application will use React with React Router animated transitions. Using your package manager, add react, react-dom, react-router-dom, and single-spa-react as dependencies.

    single-spa-react is a helper library that already implements single-spa lifecycle functions for React, so you don't have to implement these yourself.

    yarn add react react-dom single-spa-react react-router-dom react-transition-group

    Your file tree should now look similar to this:

    .
    ├── node_modules
    ├── package.json
    ├── .gitignore
    ├── src
    │ └── home
    │ ├── home.app.js
    │ └── root.component.js
    ├── .babelrc
    ├── index.html
    ├── single-spa.config.js
    ├── webpack.config.js
    ├── yarn-error.log
    ├── yarn.lock
    └── README.md

    4.b Define home application lifecycles

    Since we have registered our application, single-spa will be listening for the home application to bootstrap and mount. home app will be responsible for this. We will set this up in home.app.js.

    single-spa-react provides the generic React lifecycle hooks for registering a singe-spa application, which we'll import as singleSpaReact.

    singleSpaReact requires 4 parameters: the instance of React, the instance of ReactDOM, the rootComponent to be rendered (in this case, the Home component), and a domElementGetter function that return a DOMElement where the Home application will be bootstrapped, mounted, and unmounted by single-spa.

    home.app.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import singleSpaReact from 'single-spa-react';
    import Home from './root.component.js';

    function domElementGetter() {
    return document.getElementById('home');
    }

    const reactLifecycles = singleSpaReact({
    React,
    ReactDOM,
    rootComponent: Home,
    domElementGetter,
    });

    export const bootstrap = [reactLifecycles.bootstrap];

    export const mount = [reactLifecycles.mount];

    export const unmount = [reactLifecycles.unmount];

    4.c Build the React app

    Now that we have the home application registered, let us build the React app. We've reproduced the code from react-router's Animated Transitions below with two modifications, which are highlighted below. The first change is to add /home as the basename prop for Router, since in Step 3 we had configured this application to handle routing at the /home path. The second change is to the top-most div's styles so that home appears beneath the navBar that we'll create later.

    root.component.js
    import React from 'react';
    import { TransitionGroup, CSSTransition } from 'react-transition-group';
    import {
    BrowserRouter as Router,
    Switch,
    Route,
    Link,
    Redirect,
    } from 'react-router-dom';

    /* you'll need this CSS somewhere
    .fade-enter {
    opacity: 0;
    z-index: 1;
    }

    .fade-enter.fade-enter-active {
    opacity: 1;
    transition: opacity 250ms ease-in;
    }
    */

    const AnimationExample = () => (
    <Router basename="/home">
    <Route
    render={({ location }) => (
    <div style={{ position: 'relative', height: '100%' }}>
    <Route
    exact
    path="/"
    render={() => <Redirect to="/hsl/10/90/50" />}
    />

    <ul style={styles.nav}>
    <NavLink to="/hsl/10/90/50">Red</NavLink>
    <NavLink to="/hsl/120/100/40">Green</NavLink>
    <NavLink to="/rgb/33/150/243">Blue</NavLink>
    <NavLink to="/rgb/240/98/146">Pink</NavLink>
    </ul>

    <div style={styles.content}>
    <TransitionGroup>
    {/* no different than other usage of
    CSSTransition, just make sure to pass
    `location` to `Switch` so it can match
    the old location as it animates out
    */}
    <CSSTransition key={location.key} classNames="fade" timeout={300}>
    <Switch location={location}>
    <Route exact path="/hsl/:h/:s/:l" component={HSL} />
    <Route exact path="/rgb/:r/:g/:b" component={RGB} />
    {/* Without this `Route`, we would get errors during
    the initial transition from `/` to `/hsl/10/90/50`
    */}
    <Route render={() => <div>Not Found</div>} />
    </Switch>
    </CSSTransition>
    </TransitionGroup>
    </div>
    </div>
    )}
    />
    </Router>
    );

    const NavLink = props => (
    <li style={styles.navItem}>
    <Link {...props} style={{ color: 'inherit' }} />
    </li>
    );

    const HSL = ({ match: { params } }) => (
    <div
    style={{
    ...styles.fill,
    ...styles.hsl,
    background: `hsl(${params.h}, ${params.s}%, ${params.l}%)`,
    }}>
    hsl({params.h}, {params.s}%, {params.l}%)
    </div>
    );

    const RGB = ({ match: { params } }) => (
    <div
    style={{
    ...styles.fill,
    ...styles.rgb,
    background: `rgb(${params.r}, ${params.g}, ${params.b})`,
    }}>
    rgb({params.r}, {params.g}, {params.b})
    </div>
    );

    const styles = {};

    styles.fill = {
    position: 'absolute',
    left: 0,
    right: 0,
    top: 0,
    bottom: 0,
    };

    styles.content = {
    ...styles.fill,
    top: '40px',
    textAlign: 'center',
    };

    styles.nav = {
    padding: 0,
    margin: 0,
    position: 'absolute',
    top: 0,
    height: '40px',
    width: '100%',
    display: 'flex',
    };

    styles.navItem = {
    textAlign: 'center',
    flex: 1,
    listStyleType: 'none',
    padding: '10px',
    };

    styles.hsl = {
    ...styles.fill,
    color: 'white',
    paddingTop: '20px',
    fontSize: '30px',
    };

    styles.rgb = {
    ...styles.fill,
    color: 'white',
    paddingTop: '20px',
    fontSize: '30px',
    };

    export default AnimationExample;

    4.d Define the loading function

    We will now define the loading function for home in single-spa.config.js.

    One way of doing this is by simply passing in an application config object (the reactLifecycles functions we built in Step 4.b are an example of this) directly to the registerApplication function.

    However, to encourage best practices, we will leverage code splitting using Webpack to easily lazy-load registered applications on-demand. Think about your project's needs when deciding which route to take.

    single-spa.config.js
    import { registerApplication, start } from 'single-spa';

    registerApplication(
    // Name of our single-spa application
    'home',
    // Our loading function
    () => import('./src/home/home.app.js'),
    // Our activity function
    () =>
    location.pathname === '' ||
    location.pathname === '/' ||
    location.pathname.startsWith('/home'),
    );

    start();

    We are now ready to test out our first application.

    Run yarn start in the root directory to start up the webpack-dev-server.

    5. Create the navBar application

    Creating and registering our navBar application will be very similar to the process we used to create our home application. The main difference is that navBar will export as an object with lifecycle methods and use dynamic imports (a Webpack 2+ feature) to obtain the application object.

    note

    You may wish to revisit Step 3 for a more detailed explanation on how to register an application.

    5.a Register navBar

    Just as before, register the navBar application using the registerApplication in single-spa.config.js. Two items should be called out here:

    • Notice that we are using .then() after our import in the loadingFunction. This is because this application is returning an application config object, and we access the actual navBar application as a property and return it.
    • Recall that the activityFunction should return a truthy value when the application should be active. Since we want our navBar to be always be displayed, regardless of any other displayed applications, we define a function that will always return true.
    single-spa.config.js
    import {registerApplication, start} from 'single-spa'

    registerApplication(
    'navBar',
    () => import('./src/navBar/navBar.app.js').then(module => module.navBar),
    () => true
    );

    ...
    caution

    Don't forget to define a corresponding mount point for every newly registered application in your root HTML file. We did this already in Step 2.a so just remember to do so for each new application in the future.

    5.b Setup NavBar

    Now that we have registered our application, let's create a new navBar/ folder in the src/ directory to contain navBar.app.js and root.component.js files.

    From the root directory:

    mkdir src/navBar
    touch src/navBar/navBar.app.js src/navBar/root.component.js

    5.c Define NavBar application lifecycles

    In navbar.app.js add the following application lifecycles. This is slightly different from how we accomplished this in Step 4.b. For this application we are going to demonstrate how you can export an object which contains the required lifecycle methods using single-spa-react.

    navbar.app.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import singleSpaReact from 'single-spa-react';
    import NavBar from './root.component.js';

    function domElementGetter() {
    return document.getElementById('navBar');
    }

    export const navBar = singleSpaReact({
    React,
    ReactDOM,
    rootComponent: NavBar,
    domElementGetter,
    });

    5.d Build the navBar

    Recall that Materialize is included so we can use the class names it provides inside of the navBar component. Include the following in root.component.js:

    root.component.js
    import React from 'react';

    const NavBar = () => (
    <nav>
    <div className="nav-wrapper">
    <a className="brand-logo">single-spa</a>
    <ul id="nav-mobile" className="right hide-on-med-and-down">
    <li>
    <a>Home</a>
    </li>
    <li>
    <a>AngularJS</a>
    </li>
    </ul>
    </div>
    </nav>
    );

    export default NavBar;

    5.e Set up navigation

    With single-spa, there are a number of options that will allow us to navigate between our separate SPAs. single-spa provides navigateToUrl, a utility function that allows for easy url navigation between registered applications.

    tip

    An alternative method would be to call pushState(), which navigateToUrl does internally. This method could be used in conjunction with other client-side libraries but there are some additional considerations when using pushState.

    To use the function, we simply need to import it and call it with a click event, passing in each application's url (as designated by the activityFunction set in single-spa.config.js) as a string to the anchor tag's href.

    root.component.js
    import React from 'react';
    import { navigateToUrl } from 'single-spa';

    const NavBar = () => (
    <nav>
    <div className="nav-wrapper">
    <a href="/" onClick={navigateToUrl} className="brand-logo">
    single-spa
    </a>
    <ul id="nav-mobile" className="right hide-on-med-and-down">
    <li>
    <a href="/" onClick={navigateToUrl}>
    Home
    </a>
    </li>
    <li>
    <a href="/angularJS" onClick={navigateToUrl}>
    AngularJS
    </a>
    </li>
    </ul>
    </div>
    </nav>
    );

    export default NavBar;
    info

    We have yet to build the AngularJS application that corresponds to the /angularJS URL so navigating to it at this point will fail.

    6. Create the angularJS application

    6.a Setup angularJS

    Create a new folder in the src directory to contain the angularJS application files. There are quite a few to create.

    mkdir src/angularJS
    cd src/angularJS
    touch angularJS.app.js root.component.js root.template.html routes.js app.module.js gifs.component.js gifs.template.html

    To demonstrate the ability to use client-side routing within applications, our AngularJS application will make use of angular-ui-router.

    Using your package manager, add angular, angular-ui-router, and single-spa-angularjs (the single-spa AngularJS helper) as dependencies, like so:

    yarn add angular angular-ui-router single-spa-angularjs
    info

    Within the single-spa ecosystem there is a growing number of projects that help you bootstrap, mount, and unmount your applications that are written with popular frameworks.

    6.b Register angularJS as an application

    Just as we did for the home and navBar applications, we start by registering the angularJS application in single-spa.config.js. Add the following:

    single-spa.config.js
    registerApplication(
    'angularJS',
    () => import('./src/angularJS/angularJS.app.js'),
    () => {},
    );

    Hard-coding the activityFunction begins to get tedious so let us add a function that will simplify the matching logic for our application configuration. To do this, we've created a function that takes a string that represents the path prefix and returns a function that accepts location and matches whether the location starts with the path prefix.

    single-spa.config.js
    ...

    function pathPrefix(prefix) {
    return function(location) {
    return location.pathname.startsWith(prefix);
    }
    }

    registerApplication(
    'angularJS',
    () => import ('./src/angularJS/angularJS.app.js'),
    pathPrefix('/angularJS')
    ));

    start();

    6.c Set up Application Lifecycles

    single-spa-angularjs another helper library that implements the necessary lifecycle hooks, which simplifies the configuration. Learn more about the single-spa-angularjs options.

    Just as we did for our home and navBar applications, set up the lifecycle hooks for the angularJS in the angularJS.app.js file.

    angularJS.app.js
    import singleSpaAngularJS from 'single-spa-angularjs';
    import angular from 'angular';
    import './app.module.js';
    import './routes.js';

    const domElementGetter = () => document.getElementById('angularJS');

    const angularLifecycles = singleSpaAngularJS({
    angular,
    domElementGetter,
    mainAngularModule: 'angularJS-app',
    uiRouter: true,
    preserveGlobal: false,
    });

    export const bootstrap = [angularLifecycles.bootstrap];

    export const mount = [angularLifecycles.mount];

    export const unmount = [angularLifecycles.unmount];

    6.d Set up the AngularJS application

    Now that we have registered our application and set up the lifecycle methods pointing to our main Angular module, we can begin to flesh out the application.

    To start, we will build app.module.js followed by root.component.js which will set the root of the angularJS application using root.template.html as the template.

    app.module.js
    import angular from 'angular';
    import 'angular-ui-router';

    angular.module('angularJS-app', ['ui.router']);
    root.component.js
    import angular from 'angular';
    import template from './root.template.html';

    angular.module('angularJS-app').component('root', {
    template,
    });
    root.template.html
    <div ng-style="vm.styles">
    <div class="container">
    <div class="row">
    <h4 class="light">
    Angular 1 example
    </h4>
    <p class="caption">
    This is a sample application written with Angular 1.5 and
    angular-ui-router.
    </p>
    </div>
    <div>
    <!-- These Routes will be set up in the routes.js file -->
    <a
    class="waves-effect waves-light btn-large"
    href="/angularJS/gifs"
    style="margin-right: 10px"
    >
    Show me cat gifs
    </a>
    <a
    class="waves-effect waves-light btn-large"
    href="/angularJS"
    style="margin-right: 10px"
    >
    Take me home
    </a>
    </div>
    <div class="row">
    <ui-view />
    </div>
    </div>
    </div>

    Next we will add a basic Gif Component and import it in the root component.

    gifs.component.js
    import angular from 'angular';
    import template from './gifs.template.html';

    angular.module('angularJS-app').component('gifs', {
    template,
    controllerAs: 'vm',
    controller($http) {
    const vm = this;

    $http
    .get('https://api.giphy.com/v1/gifs/search?q=cat&api_key=dc6zaTOxFJmzC')
    .then(response => {
    vm.gifs = response.data.data;
    })
    .catch(err => {
    setTimeout(() => {
    throw err;
    }, 0);
    });
    },
    });
    gifs.template.html
    <div style="padding-top: 20px">
    <h4 class="light">
    Cat Gifs gifs
    </h4>
    <p></p>
    <div ng-repeat="gif in vm.gifs" style="margin: 5px;">
    <img ng-src="{{gif.images.downsized_medium.url}}" class="col l3" />
    </div>
    </div>

    6.e Set up in-app routing

    Now that we have each of our components built out, all we have left to do is connect them. We will do this by importing both into routes.js.

    routes.js
    import angular from 'angular';
    import './root.component.js';
    import './gifs.component.js';

    angular.module('angularJS-app').config(($stateProvider, $locationProvider) => {
    $locationProvider.html5Mode({
    enabled: true,
    requireBase: false,
    });

    $stateProvider
    .state('root', {
    url: '/angularJS',
    template: '<root />',
    })

    .state('root.gifs', {
    url: '/gifs',
    template: '<gifs />',
    });
    });

    Finished!

    From the root directory run yarn start to check out your new single-spa project.

    We hope this tutorial gives you experience building and implementing micro frontends using single-spa. Review this guide periodically and use it as a reference in your projects. If you still have questions about how to use single-spa with your specific build, check out the migration tutorials: for AngularJS and React.

    As always, there is more to be learned. If you want to learn how to use single-spa with Angular, Vue, or other frameworks/build systems, checkout the single-spa-examples repo. Lastly, you may also want to study how to separate applications using single-spa.

    - - + + \ No newline at end of file diff --git a/docs/5.x/api/index.html b/docs/5.x/api/index.html index 8369e876d..bccfc46a6 100644 --- a/docs/5.x/api/index.html +++ b/docs/5.x/api/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ This means importing must happen in one of two ways:

    import { registerApplication, start } from 'single-spa';
    // or
    import * as singleSpa from 'single-spa';

    registerApplication

    registerApplication is the most important API your root config will use. Use this function to register any application within single-spa. Note that if an application is registered from within another application, that no hierarchy will be maintained between the applications.

    There are two ways of registering your application:

    Simple arguments

    singleSpa.registerApplication(
    'appName',
    () => System.import('appName'),
    location => location.pathname.startsWith('appName'),
    );

    arguments

    appName: string
    App name that single-spa will register and reference this application with, and will be labelled with in dev tools.
    applicationOrLoadingFn: () => <Function | Promise>
    Must be a loading function that either returns the resolved application or a promise.
    activityFn: (location) => boolean
    Must be a pure function. The function is called with window.location as the first argument and should return a truthy value whenever the application should be active.
    customProps?: Object | () => Object
    Will be passed to the application during each lifecycle method.

    returns

    undefined

    Configuration object

    singleSpa.registerApplication({
    name: 'appName',
    app: () => System.import('appName'),
    activeWhen: '/appName',
    customProps: {
    authToken: 'xc67f6as87f7s9d'
    }
    })

    singleSpa.registerApplication({
    name: 'appName',
    app: () => System.import('appName'),
    activeWhen: '/appName',
    // Dynamic custom props that can change based on route
    customProps(appName, location) {
    return {
    authToken: 'xc67f6as87f7s9d'
    }
    }
    })

    arguments

    name: string
    App name that single-spa will register and reference this application with, and will be labelled with in dev tools.
    app: Application | () => Application | Promise<Application>
    Application object or a function that returns the resolved application (Promise or not)
    activeWhen: string | (location) => boolean | (string | (location) => boolean)[]
    Can be a path prefix which will match every URL starting with this path, an activity function (as described in the simple arguments) or an array containing both of them. If any of the criteria is true, it will keep the application active. The path prefix also accepts dynamic values (they must start with ':'), as some paths would receive url params and should still trigger your application. Examples:
    '/app1'
    ✅ https://app.com/app1
    ✅ https://app.com/app1/anything/everything
    🚫 https://app.com/app2
    '/users/:userId/profile'
    ✅ https://app.com/users/123/profile
    ✅ https://app.com/users/123/profile/sub-profile/
    🚫 https://app.com/users//profile/sub-profile/
    🚫 https://app.com/users/profile/sub-profile/
    '/pathname/#/hash'
    ✅ https://app.com/pathname/#/hash
    ✅ https://app.com/pathname/#/hash/route/nested
    🚫 https://app.com/pathname#/hash/route/nested
    🚫 https://app.com/pathname#/another-hash
    ['/pathname/#/hash', '/app1']
    ✅ https://app.com/pathname/#/hash/route/nested
    ✅ https://app.com/app1/anything/everything
    🚫 https://app.com/pathname/app1
    🚫 https://app.com/app2
    customProps?: Object | () => Object
    Will be passed to the application during each lifecycle method.

    returns

    undefined

    note

    It is described in detail inside of the Configuration docs

    start

    singleSpa.start();

    // Optionally, you can provide configuration
    singleSpa.start({
    urlRerouteOnly: true,
    });

    Must be called by your single spa config. Before start is called, applications will be loaded, but will never be bootstrapped, mounted or unmounted. The reason for start is to give you control over the performance of your single page application. For example, you may want to declare registered applications immediately (to start downloading the code for the active ones), but not actually mount the registered applications until an initial AJAX request (maybe to get information about the logged in user) has been completed. In that case, the best performance is achieved by calling registerApplication immediately, but calling start after the AJAX request is completed.

    arguments

    The start(opts) api optionally accepts a single opts object, with the following properties. If the opts object is omitted, all defaults will be applied.

    • urlRerouteOnly: A boolean that defaults to false. If set to true, calls to history.pushState() and history.replaceState() will not trigger a single-spa reroute unless the client side route was changed. Setting this to true can be better for performance in some situations. For more information, read original issue.

    returns

    undefined

    triggerAppChange

    singleSpa.triggerAppChange();

    Returns a Promise that will resolve/reject when all apps have mounted/unmounted. This was built for testing single-spa and is likely not needed in a production application.

    arguments

    none

    returns

    Promise
    Returns a Promise that will resolve/reject when all apps have mounted.
    // Three ways of using navigateToUrl
    singleSpa.navigateToUrl('/new-url');
    singleSpa.navigateToUrl(document.querySelector('a'));
    document.querySelector('a').addEventListener(singleSpa.navigateToUrl);
    <!-- A fourth way to use navigateToUrl, this one inside of your HTML -->
    <a href="/new-url" onclick="singleSpaNavigate(event)">My link</a>

    Use this utility function to easily perform url navigation between registered applications without needing to deal with event.preventDefault(), pushState, triggerAppChange(), etc.

    arguments

    navigationObj: string | context | DOMEvent
    navigationObj must be one of the following types:
    • a url string.
    • a context / thisArg that has an href property; useful for calling singleSpaNavigate.call(anchorElement) with a reference to the anchor element or other context.
    • a DOMEvent object for a click event on a DOMElement that has an href attribute; ideal for the <a onclick="singleSpaNavigate"></a> use case.

    returns

    undefined

    getMountedApps

    const mountedAppNames = singleSpa.getMountedApps();
    console.log(mountedAppNames); // ['app1', 'app2', 'navbar']

    arguments

    none

    returns

    appNames: string[]
    Each string is the name of a registered application that is currently MOUNTED.

    getAppNames

    const appNames = singleSpa.getAppNames();
    console.log(appNames); // ['app1', 'app2', 'app3', 'navbar']

    arguments

    none

    returns

    appNames: string[]
    Each string is the name of a registered application regardless of app status.

    getAppStatus

    const status = singleSpa.getAppStatus('app1');
    console.log(status); // one of many statuses (see list below). e.g. MOUNTED

    arguments

    appName: string
    Registered application name.

    returns

    appStatus: <string | null>
    Will be one of the following string constants, or null if the app does not exist.
    NOT_LOADED
    app has been registered with single-spa but has not yet been loaded.
    LOADING_SOURCE_CODE
    app's source code is being fetched.
    NOT_BOOTSTRAPPED
    app has been loaded but not bootstrapped.
    BOOTSTRAPPING
    the bootstrap lifecycle function has been called but has not finished.
    NOT_MOUNTED
    app has been loaded and bootstrapped but not yet mounted.
    MOUNTING
    app is being mounted but not finished.
    MOUNTED
    app is currently active and is mounted to the DOM.
    UNMOUNTING
    app is currently being unmounted but has not finished.
    UNLOADING
    app is currently being unloaded but has not finished.
    SKIP_BECAUSE_BROKEN
    app threw an error during load, bootstrap, mount, or unmount and has been siloed because it is misbehaving and has been skipped. Other apps will continue on normally.
    LOAD_ERROR
    The app's loading function returned a promise that rejected. This is often due to a network error attempting to download the JavaScript bundle for the application. Single-spa will retry loading the application after the user navigates away from the current route and then comes back to it.

    Handling LOAD_ERROR status to retry module

    If a module fails to load (for example, due to network error), single-spa will handle the error but SystemJS will not automatically retry to download the module later. To do so, add a single-spa errorHandler that deletes the module from the SystemJS registry and re-attempt to download the module when System.import() on an application in LOAD_ERROR status is called again.

    singleSpa.addErrorHandler(err => {
    if (singleSpa.getAppStatus(err.appOrParcelName) === singleSpa.LOAD_ERROR) {
    System.delete(System.resolve(err.appOrParcelName));
    }
    });

    unloadApplication

    // Unload the application right now, without waiting for it to naturally unmount.
    singleSpa.unloadApplication('app1');

    // Unload the application only after it naturally unmounts due to a route change.
    singleSpa.unloadApplication('app1', { waitForUnmount: true });

    The purpose of unloading a registered application is to set it back to a NOT_LOADED status, which means that it will be re-bootstrapped the next time it needs to mount. The main use-case for this was to allow for the hot-reloading of entire registered applications, but unloadApplication can be useful whenever you want to re-bootstrap your application.

    Single-spa performs the following steps when unloadApplication is called.

    1. Call the unload lifecyle on the registered application that is being unloaded.
    2. Set the app status to NOT_LOADED
    3. Trigger a reroute, during which single-spa will potentially mount the application that was just unloaded.

    Because a registered application might be mounted when unloadApplication is called, you can specify whether you want to immediately unload or if you want to wait until the application is no longer mounted. This is done with the waitForUnmount option.

    arguments

    appName: string
    Registered application name.
    options?: {waitForUnmount: boolean = false}
    The options must be an object that has a waitForUnmount property. When waitForUnmount is false, single-spa immediately unloads the specified registered application even if the app is currently mounted. If it is true, single-spa will unload the registered application as soon as it is safe to do so (when the app status is not MOUNTED).

    returns

    Promise
    This promise will be resolved when the registered application has been successfully unloaded.

    unregisterApplication

    import { unregisterApplication } from 'single-spa';

    unregisterApplication('app1').then(() => {
    console.log('app1 is now unmounted, unloaded, and no longer registered!');
    });

    The unregisterApplication function will unmount, unload, and unregister an application. Once it is no longer registered, the application will never again be mounted.

    This api was introduced in single-spa@5.8.0. A few notes about this api:

    • Unregistering an application does not delete it from the SystemJS module registry.
    • Unregistering an application does not delete its code or javascript frameworks from browser memory.
    • An alternative to unregistering applications is to perform permission checks inside of the application's activity function. This has a similar effect of preventing the application from ever mounting.

    arguments

    appName: string

    returns

    Promise
    This promise will be resolved when the application has been successfully unregistered.

    checkActivityFunctions

    const appsThatShouldBeActive = singleSpa.checkActivityFunctions();
    console.log(appsThatShouldBeActive); // ['app1']

    const appsForACertainRoute = singleSpa.checkActivityFunctions(new URL('/app2', window.location.href));
    console.log(appsForACertainRoute); // ['app2']

    Will call every app's activity function with url and give you list of which applications should be mounted with that location.

    arguments

    location: Location
    A Location object that will be used instead of window.location when calling every application's activity function to test if they return true.

    returns

    appNames: string[]
    Each string is the name of a registered application that matches the provided url.

    addErrorHandler

    singleSpa.addErrorHandler(err => {
    console.log(err);
    console.log(err.appOrParcelName);
    console.log(singleSpa.getAppStatus(err.appOrParcelName));
    });

    Adds a handler that will be called every time an application throws an error during a lifecycle function or activity function. When there are no error handlers, single-spa throws the error to the window.

    errorHandler: Function(error: Error)
    Must be a function. Will be called with an Error object that additionally has a message and appOrParcelName property.

    returns

    undefined

    removeErrorHandler

    singleSpa.addErrorHandler(handleErr);
    singleSpa.removeErrorHandler(handleErr);

    function handleErr(err) {
    console.log(err);
    }

    Removes the given error handler function.

    arguments

    errorHandler: Function
    Reference to the error handling function.

    returns

    boolean
    true if the error handler was removed, and false if it was not.

    mountRootParcel

    // Synchronous mounting
    const parcel = singleSpa.mountRootParcel(parcelConfig, {
    prop1: 'value1',
    domElement: document.getElementById('a-div'),
    });
    parcel.mountPromise.then(() => {
    console.log('finished mounting the parcel!');
    });

    // Asynchronous mounting. Feel free to use webpack code splits or SystemJS dynamic loading
    const parcel2 = singleSpa.mountRootParcel(() => import('./some-parcel.js'), {
    prop1: 'value1',
    domElement: document.getElementById('a-div'),
    });

    Will create and mount a single-spa parcel.

    Parcels do not automatically unmount

    Unmounting will need to be triggered manually.

    arguments

    parcelConfig: Object or Loading Function
    [parcelConfig](/docs/5.x/parcels-api#parcel-configuration)
    parcelProps: Object with a domElement property
    [parcelProps](/docs/5.x/parcels-api#parcel-props)

    returns

    Parcel object
    See Parcels API for more detail.

    pathToActiveWhen

    The pathToActiveWhen function converts a string URL path into an activity function. The string path may contain route parameters that single-spa will match any characters to. By default, pathToActiveWhen assumes that the string provided is a prefix; however, this can be altered with the exactMatch parameter.

    This function is used by single-spa when a string is passed into registerApplication as the activeWhen argument.

    Arguments

    1. path (string): The URL prefix that.
    2. exactMatch (boolean, optional, defaults to false, requires single-spa@>=5.9.0): A boolean that controls whether trailing characters after the path should be allowed. When false, trailing characters are allowed. When true, trailing characters are not allowed.

    Return Value

    (url: URL) => boolean

    A function that accepts a URL object as an argument and returns a boolean indicating whether the path matches that URL.

    Examples:

    let activeWhen = singleSpa.pathToActiveWhen('/settings');
    activeWhen(new URL('http://localhost/settings')); // true
    activeWhen(new URL('http://localhost/settings/password')); // true
    activeWhen(new URL('http://localhost/')); // false

    activeWhen = singleSpa.pathToActiveWhen('/users/:id/settings');
    activeWhen(new URL('http://localhost/users/6f7dsdf8g9df8g9dfg/settings')); // true
    activeWhen(new URL('http://localhost/users/1324/settings')); // true
    activeWhen(new URL('http://localhost/users/1324/settings/password')); // true
    activeWhen(new URL('http://localhost/users/1/settings')); // true
    activeWhen(new URL('http://localhost/users/1')); // false
    activeWhen(new URL('http://localhost/settings')); // false
    activeWhen(new URL('http://localhost/')); // false

    activeWhen = singleSpa.pathToActiveWhen('/page#/hash');
    activeWhen(new URL('http://localhost/page#/hash')); // true
    activeWhen(new URL('http://localhost/#/hash')); // false
    activeWhen(new URL('http://localhost/page')); // false

    ensureJQuerySupport

    singleSpa.ensureJQuerySupport(jQuery);

    jQuery uses event delegation so single-spa must monkey-patch each version of jQuery on the page. single-spa will attempt to do this automatically by looking for window.jQuery or window.$. Use this explicit method if multiple versions are included on your page or if jQuery is bound to a different global variable.

    arguments

    jQuery?: JQueryFn = window.jQuery
    A reference to the global variable that jQuery has been bound to.

    returns

    undefined

    setBootstrapMaxTime

    // After three seconds, show a console warning while continuing to wait.
    singleSpa.setBootstrapMaxTime(3000);

    // After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
    singleSpa.setBootstrapMaxTime(3000, true);

    // don't do a console warning for slow lifecycles until 10 seconds have elapsed
    singleSpa.setBootstrapMaxTime(3000, true, 10000);

    Sets the global configuration for bootstrap timeouts.

    arguments

    millis: number
    Number of milliseconds to wait for bootstrap to complete before timing out.
    dieOnTimeout: boolean = false

    If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

    If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

    Each registered application can override this behavior for itself.

    warningMillis: number = 1000
    Number of milliseconds to wait between console warnings that occur before the final timeout.

    returns

    undefined

    setMountMaxTime

    // After three seconds, show a console warning while continuing to wait.
    singleSpa.setMountMaxTime(3000);

    // After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
    singleSpa.setMountMaxTime(3000, true);

    // don't do a console warning for slow lifecycles until 10 seconds have elapsed
    singleSpa.setMountMaxTime(3000, true, 10000);

    Sets the global configuration for mount timeouts.

    arguments

    millis: number
    Number of milliseconds to wait for mount to complete before timing out.
    dieOnTimeout: boolean = false

    If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

    If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

    Each registered application can override this behavior for itself.

    warningMillis: number = 1000
    Number of milliseconds to wait between console warnings that occur before the final timeout.

    returns

    undefined

    setUnmountMaxTime

    // After three seconds, show a console warning while continuing to wait.
    singleSpa.setUnmountMaxTime(3000);

    // After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
    singleSpa.setUnmountMaxTime(3000, true);

    // don't do a console warning for slow lifecycles until 10 seconds have elapsed
    singleSpa.setUnmountMaxTime(3000, true, 10000);

    Sets the global configuration for unmount timeouts.

    arguments

    millis: number
    Number of milliseconds to wait for unmount to complete before timing out.
    dieOnTimeout: boolean = false

    If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

    If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

    Each registered application can override this behavior for itself.

    warningMillis: number = 1000
    Number of milliseconds to wait between console warnings that occur before the final timeout.

    returns

    undefined


    setUnloadMaxTime

    // After three seconds, show a console warning while continuing to wait.
    singleSpa.setUnloadMaxTime(3000);

    // After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
    singleSpa.setUnloadMaxTime(3000, true);

    // don't do a console warning for slow lifecycles until 10 seconds have elapsed
    singleSpa.setUnloadMaxTime(3000, true, 10000);

    Sets the global configuration for unload timeouts.

    arguments

    millis: number
    Number of milliseconds to wait for unload to complete before timing out.
    dieOnTimeout: boolean = false

    If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

    If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

    Each registered application can override this behavior for itself.

    warningMillis: number = 1000
    Number of milliseconds to wait between console warnings that occur before the final timeout.

    returns

    undefined

    Events

    single-spa fires Events to the window as a way for your code to hook into URL transitions.

    PopStateEvent

    single-spa fires PopStateEvent events when it wants to instruct all active applications to re-render. This occurs when one application calls history.pushState, history.replaceState, or triggerAppChange. Single-spa deviates from the browser's default behavior in some cases, as described in this Github issue.

    window.addEventListener('popstate', evt => {
    if (evt.singleSpa) {
    console.log(
    'This event was fired by single-spa to forcibly trigger a re-render',
    );
    console.log(evt.singleSpaTrigger); // pushState | replaceState
    } else {
    console.log('This event was fired by native browser behavior');
    }
    });

    Canceling navigation

    Canceling navigation refers to the URL changing and then immediately changing back to what it was before. This is done before any mounting, unmounting, or loading that would otherwise take place. This can be used in conjunction with Vue router and Angular router's built-in navigation guards that allow for cancelation of a navigation event.

    To cancel a navigation event, listen to the single-spa:before-routing-event event:

    window.addEventListener(
    'single-spa:before-routing-event',
    ({ detail: { oldUrl, newUrl, cancelNavigation } }) => {
    if (
    new URL(oldUrl).pathname === '/route1' &&
    new URL(newUrl).pathname === '/route2'
    ) {
    cancelNavigation();
    }
    },
    );

    When a navigation is canceled, no applications will be mounted, unmounted, loaded, or unloaded. All single-spa routing events will fire for the canceled navigation, but they will each have the navigationIsCanceled property set to true on the event.detail (Details below in Custom Events section).

    Navigation cancelation is sometimes used as a mechanism for preventing users from accessing routes for which they are unauthorized. However, we generally recommend permission checks on each route as the proper way to guard routes, instead of navigation cancelation.

    Custom Events

    single-spa fires a series of custom events whenever it reroutes. A reroute occurs whenever the browser URL changes in any way or a triggerAppChange is called. The custom events are fired to the window. Each custom event has a detail property with the following properties:

    window.addEventListener('single-spa:before-routing-event', evt => {
    const {
    originalEvent,
    newAppStatuses,
    appsByNewStatus,
    totalAppChanges,
    oldUrl,
    newUrl,
    navigationIsCanceled,
    cancelNavigation,
    } = evt.detail;
    console.log(
    'original event that triggered this single-spa event',
    originalEvent,
    ); // PopStateEvent | HashChangeEvent | undefined
    console.log(
    'the new status for all applications after the reroute finishes',
    newAppStatuses,
    ); // { app1: MOUNTED, app2: NOT_MOUNTED }
    console.log(
    'the applications that changed, grouped by their status',
    appsByNewStatus,
    ); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] }
    console.log(
    'number of applications that changed status so far during this reroute',
    totalAppChanges,
    ); // 2
    console.log('the URL before the navigationEvent', oldUrl); // http://localhost:8080/old-route
    console.log('the URL after the navigationEvent', newUrl); // http://localhost:8080/new-route
    console.log('has the navigation been canceled', navigationIsCanceled); // false

    // The cancelNavigation function is only defined in the before-routing-event
    evt.detail.cancelNavigation();
    });

    The following table shows the order in which the custom events are fired during a reroute:

    Event orderEvent NameCondition for firing
    1single-spa:before-app-change or single-spa:before-no-app-changeWill any applications change status?
    2single-spa:before-routing-event
    3single-spa:before-mount-routing-event
    4single-spa:before-first-mountIs this the first time any application is mounting?
    5single-spa:first-mountIs this the first time any application was mounted?
    6single-spa:app-change or single-spa:no-app-changeDid any applications change status?
    7single-spa:routing-event

    before-app-change event

    window.addEventListener('single-spa:before-app-change', evt => {
    console.log('single-spa is about to mount/unmount applications!');
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { app1: MOUNTED }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 1
    });

    A single-spa:before-app-change event is fired before reroutes that will result in at least one application changing status.

    before-no-app-change

    window.addEventListener('single-spa:before-no-app-change', evt => {
    console.log('single-spa is about to do a no-op reroute');
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: [], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 0
    });

    A single-spa:before-no-app-change event is fired before reroutes that will result in zero applications changing status.

    before-routing-event

    window.addEventListener('single-spa:before-routing-event', evt => {
    console.log('single-spa is about to mount/unmount applications!');
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: [], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 0
    });

    A single-spa:before-routing-event event is fired before every routing event occurs, which is after each hashchange, popstate, or triggerAppChange, even if no changes to registered applications were necessary.

    before-mount-routing-event

    window.addEventListener('single-spa:before-mount-routing-event', evt => {
    console.log('single-spa is about to mount/unmount applications!');
    console.log(evt.detail);
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { app1: MOUNTED }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 1
    });

    A single-spa:before-mount-routing-event event is fired after before-routing-event and before routing-event. It is guaranteed to fire after all single-spa applications have been unmounted, but before any new applications have been mounted.

    before-first-mount

    window.addEventListener('single-spa:before-first-mount', () => {
    console.log(
    'single-spa is about to mount the very first application for the first time',
    );
    });

    Before the first of any single-spa applications is mounted, single-spa fires a single-spa:before-first-mount event; therefore it will only be fired once ever. More specifically, it fires after the application is already loaded but before mounting.

    Suggested use case

    remove a loader bar that the user is seeing right before the first app will be mounted.

    first-mount

    window.addEventListener('single-spa:first-mount', () => {
    console.log('single-spa just mounted the very first application');
    });

    After the first of any single-spa applications is mounted, single-spa fires a single-spa:first-mount event; therefore it will only be fired once ever.

    Suggested use case

    log the time it took before the user sees any of the apps mounted.

    app-change event

    window.addEventListener('single-spa:app-change', evt => {
    console.log(
    'A routing event occurred where at least one application was mounted/unmounted',
    );
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { app1: MOUNTED, app2: NOT_MOUNTED }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] }
    console.log(evt.detail.totalAppChanges); // 2
    });

    A single-spa:app-change event is fired every time that one or more apps were loaded, bootstrapped, mounted, unmounted, or unloaded. It is similar to single-spa:routing-event except that it will not fire unless one or more apps were actually loaded, bootstrapped, mounted, or unmounted. A hashchange, popstate, or triggerAppChange that does not result in one of those changes will not cause the event to be fired.

    no-app-change event

    window.addEventListener('single-spa:no-app-change', evt => {
    console.log(
    'A routing event occurred where zero applications were mounted/unmounted',
    );
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: [], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 0
    });

    When no applications were loaded, bootstrapped, mounted, unmounted, or unloaded, single-spa fires a single-spa:no-app-change event. This is the inverse of the single-spa:app-change event. Only one will be fired for each routing event.

    routing-event

    window.addEventListener('single-spa:routing-event', evt => {
    console.log('single-spa finished mounting/unmounting applications!');
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { app1: MOUNTED, app2: NOT_MOUNTED }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] }
    console.log(evt.detail.totalAppChanges); // 2
    });

    A single-spa:routing-event event is fired every time that a routing event has occurred, which is after each hashchange, popstate, or triggerAppChange, even if no changes to registered applications were necessary; and after single-spa verified that all apps were correctly loaded, bootstrapped, mounted, and unmounted.

    - - + + \ No newline at end of file diff --git a/docs/5.x/building-applications/index.html b/docs/5.x/building-applications/index.html index c5da27c58..490c65de1 100644 --- a/docs/5.x/building-applications/index.html +++ b/docs/5.x/building-applications/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -24,7 +24,7 @@ but will not be bootstrapped, mounted or unmounted.
    info

    Framework-specific helper libraries exist in the single-spa ecosystem to implement these required lifecycle methods. This documentation is helpful for understanding what those helpers are doing, or for implementing your own.

    Lifecycle props

    Lifecycle functions are called with a props argument, which is an object with some guaranteed information and additional custom information.

    function bootstrap(props) {
    const {
    name, // The name of the application
    singleSpa, // The singleSpa instance
    mountParcel, // Function for manually mounting
    customProps, // Additional custom information
    } = props; // Props are given to every lifecycle
    return Promise.resolve();
    }

    Built-in props

    Each lifecycle function is guaranteed to be called with the following props:

    • name: The string name that was registered to single-spa.
    • singleSpa: A reference to the singleSpa instance, itself. This is intended to allow applications and helper libraries to call singleSpa APIs without having to import it. This is useful in situations where there are multiple webpack configs that are not set up to ensure that only one instance of singleSpa is loaded.
    • mountParcel: The mountParcel function.

    Custom props

    In addition to the built-in props that are provided by single-spa, you may optionally specify custom props to be passed to an application. These customProps will be passed into each lifecycle method. The custom props are an object, and you can provide either the object or a function that returns the object. Custom prop functions are called with the application name and current window.location as arguments.

    root-config.js
    singleSpa.registerApplication({
    name: 'app1',
    activeWhen,
    app,
    customProps: { authToken: 'd83jD63UdZ6RS6f70D0' },
    });

    singleSpa.registerApplication({
    name: 'app1',
    activeWhen,
    app,
    customProps: (name, location) => {
    return { authToken: 'd83jD63UdZ6RS6f70D0' };
    },
    });
    app1.js
    export function mount(props) {
    // do something with the common authToken in app1
    console.log(props.authToken);
    return reactLifecycles.mount(props);
    }

    Some use cases could be to:

    • share a common access token with all child apps
    • pass down some initialization information, like the rendering target
    • pass a reference to a common event bus so each app may talk to each other

    Note that when no customProps are provided during registration, props.customProps defaults to an empty object.

    Lifecycle helpers

    Some helper libraries that implement lifecycle functions for ease of use are available for many popular frameworks/libraries. Learn more on the Ecosystem page.

    Load

    When registered applications are being lazily loaded, this refers to when the code for a registered application is fetched from the server and executed. This will happen once the registered application's activity function returns a truthy value for the first time. It is best practice to do as little as possible / nothing at all during load, but instead to wait until the bootstrap lifecycle function to do anything. If you need to do something during load, simply put the code into a registered application's main entry point, but not inside of an exported function. For example:

    console.log("The registered application has been loaded!");

    export async function bootstrap(props) {...}
    export async function mount(props) {...}
    export async function unmount(props) {...}

    Bootstrap

    This lifecycle function will be called once, right before the registered application is mounted for the first time.

    export function bootstrap(props) {
    return Promise.resolve().then(() => {
    // One-time initialization code goes here
    console.log('bootstrapped!');
    });
    }

    Mount

    This lifecycle function will be called whenever the registered application is not mounted, but its activity function returns a truthy value. When called, this function should look at the URL to determine the active route and then create DOM elements, DOM event listeners, etc. to render content to the user. Any subsequent routing events (such as hashchange and popstate) will not trigger more calls to mount, but instead should be handled by the application itself.

    export function mount(props) {
    return Promise.resolve().then(() => {
    // Do framework UI rendering here
    console.log('mounted!');
    });
    }

    Unmount

    This lifecycle function will be called whenever the registered application is mounted, but its activity function returns a falsy value. When called, this function should clean up all DOM elements, DOM event listeners, leaked memory, globals, observable subscriptions, etc. that were created at any point when the registered application was mounted.

    export function unmount(props) {
    return Promise.resolve().then(() => {
    // Do framework UI unrendering here
    console.log('unmounted!');
    });
    }

    Unload

    The unload lifecycle is an optionally implemented lifecycle function. It will be called whenever an application should be unloaded. This will not ever happen unless someone calls the unloadApplication API. If a registered application does not implement the unload lifecycle, then it assumed that unloading the app is a no-op.

    The purpose of the unload lifecycle is to perform logic right before a single-spa application is unloaded. Once the application is unloaded, the application status will be NOT_LOADED and the application will be re-bootstrapped.

    The motivation for unload was to implement the hot-loading of entire registered applications, but it is useful in other scenarios as well when you want to re-bootstrap applications, but perform some logic before applications are re-bootstrapped.

    export function unload(props) {
    return Promise.resolve().then(() => {
    // Hot-reloading implementation goes here
    console.log('unloaded!');
    });
    }

    Timeouts

    By default, registered applications obey the global timeout configuration, but can override that behavior for their specific application. This is done by exporting a timeouts object from the main entry point of the registered application. Example:

    app-1.js
    export function bootstrap(props) {...}
    export function mount(props) {...}
    export function unmount(props) {...}

    export const timeouts = {
    bootstrap: {
    millis: 5000,
    dieOnTimeout: true,
    warningMillis: 2500,
    },
    mount: {
    millis: 5000,
    dieOnTimeout: false,
    warningMillis: 2500,
    },
    unmount: {
    millis: 5000,
    dieOnTimeout: true,
    warningMillis: 2500,
    },
    unload: {
    millis: 5000,
    dieOnTimeout: true,
    warningMillis: 2500,
    },
    };

    Note that millis refers to the number of milliseconds for the final console warning, and warningMillis refers to the number of milliseconds at which a warning will be printed to the console (on an interval) leading up to the final console warning.

    Transitioning between applications

    If you find yourself wanting to add transitions as applications are mounted and unmounted, then you'll probably want to tie into the bootstrap, mount, and unmount lifecycle methods. This single-spa transitions repo is a small proof-of-concept of how you can tie into these lifecycle methods to add transitions as your apps mount and unmount.

    Transitions for pages within a mounted application can be handled entirely by the application itself. For example, using react-transition-group for React-based projects.

    - - + + \ No newline at end of file diff --git a/docs/5.x/code-of-conduct/index.html b/docs/5.x/code-of-conduct/index.html index 875ab8a0d..d1e8b0669 100644 --- a/docs/5.x/code-of-conduct/index.html +++ b/docs/5.x/code-of-conduct/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Code of Conduct

    This code of conduct outlines our expectations for participants within the single-spa community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community.

    Our open source community strives to:

    • Be friendly and patient.
    • Be welcoming: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
    • Be considerate: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into *account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language.
    • Be respectful: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration *to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one.
    • Be careful in the words that you choose: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other *exclusionary behavior aren’t acceptable. This includes, but is not limited to:
      • Violent threats or language directed against another person.
      • Discriminatory jokes and language.
      • Posting sexually explicit or violent material.
      • Posting (or threatening to post) other people’s personally identifying information (“doxing”).
      • Personal insults, especially those using racist or sexist terms.
      • Unwelcome sexual attention.
      • Advocating for, or encouraging, any of the above behavior.
      • Repeated harassment of others. In general, if someone asks you to stop, then stop.
      • When we disagree, try to understand why: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively.
      • Remember that we’re different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.

    This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter.

    Diversity Statement

    We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong.

    Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities.

    Reporting Issues

    If you experience or witness unacceptable behavior—or have any other concerns—please report it by sending a Twitter DM to https://twitter.com/Single_spa. All reports will be handled with discretion. In your report please include:

    • Your contact information.
    • Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link.
    • Any additional information that may be helpful.

    After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse.

    Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning.

    This Code Of Conduct follows the template established by the TODO Group.

    - - + + \ No newline at end of file diff --git a/docs/5.x/configuration/index.html b/docs/5.x/configuration/index.html index 730217034..0dda4b237 100644 --- a/docs/5.x/configuration/index.html +++ b/docs/5.x/configuration/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -45,7 +45,7 @@ so that they never try to modify the same DOM at the same time.

    The <div> will need an id starting with the prefix single-spa-application: and then your app name. For example, if you had an app called app-name, you'd make a <div> with the id single-spa-application:app-name.

    An example with multiple applications would look like this:

    <div id="single-spa-application:app-name"></div>
    <div id="single-spa-application:other-app"></div>
    - - + + \ No newline at end of file diff --git a/docs/5.x/contributing-overview/index.html b/docs/5.x/contributing-overview/index.html index 7c0e85ae8..2caa806f3 100644 --- a/docs/5.x/contributing-overview/index.html +++ b/docs/5.x/contributing-overview/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Contributing to Single-spa

    List of current contributors

    Thanks for checking out single-spa! We're excited to hear and learn from you.

    We've put together the following guidelines to help you figure out where you can best be helpful.

    Table of Contents

    1. Types of contributions we're looking for
    2. Ground rules & expectations
    3. How to contribute
    4. Setting up your environment
    5. Community

    Types of contributions we're looking for

    There are many ways you can directly contribute to the guides (in descending order of need):

    • Examples
    • Helper Libraries (like single-spa-react) for missing frameworks
    • Bug fixes
    • Answering questions in the slack channel
    • new helper packages for frameworks

    Interested in making a contribution? Read on!

    Ground rules & expectations

    Before we get started, here are a few things we expect from you (and that you should expect from others):

    • Be kind and thoughtful in your conversations around this project. We all come from different backgrounds and projects, which means we likely have different perspectives on "how open source is done." Try to listen to others rather than convince them that your way is correct.
    • Please read the single-spa Contributor Code of Conduct. By participating in this project, you agree to abide by its terms.
    • If you open a pull request, please ensure that your contribution passes all tests. If there are test failures, you will need to address them before we can merge your contribution.
    • When adding content, please consider if it is widely valuable. Please don't add references or links to things you or your employer have created as others will do so if they appreciate it.

    How to contribute

    If you'd like to contribute, start by searching through the issues and pull requests to see whether someone else has raised a similar idea or question.

    If you don't see your idea listed, and you think it fits into the goals of this guide, do one of the following:

    • If your contribution is minor, such as a small typo or bug fix, open a pull request.
    • If your contribution is major, such as a new feature, start by opening an issue first. That way, other people can weigh in on the discussion before you do any work.

    Setting up your environment

    Prerequisites

    1. Git
    2. Node: install version 8.4 or greater
    3. Yarn: See Yarn website for installation instructions
    4. A fork of the single-spa repo
    5. A clone of the repo on your local machine

    Installation

    1. cd single-spa to go into the project root
    2. yarn to install single-spa's dependencies

    Create a branch

    1. git checkout master from any folder in your local single-spa repository
    2. git pull origin master to ensure you have the latest main code
    3. git checkout -b the-name-of-my-branch (replacing the-name-of-my-branch with a suitable name) to create a branch

    Test the change

    1. Run yarn test from the project root.

    Push it

    1. git add . && git commit -m "My message" (replacing My message with a commit message, such as Fixed application lifecycles) to stage and commit your changes
    2. git push my-fork-name the-name-of-my-branch
    3. Go to the single-spa repo and you should see recently pushed branches.
    4. Follow GitHub's instructions to submit a new Pull Request.

    Community

    Discussions about single-spa take place on the single-spa repository's Issues and Pull Requests sections. Anybody is welcome to join these conversations. There is also a Slack workspace for regular updates.

    Wherever possible, do not take these conversations to private channels, including contacting the maintainers directly. Keeping communication public means everybody can benefit and learn from the conversation.

    - - + + \ No newline at end of file diff --git a/docs/5.x/create-single-spa/index.html b/docs/5.x/create-single-spa/index.html index 3601f5797..12b8c0ea9 100644 --- a/docs/5.x/create-single-spa/index.html +++ b/docs/5.x/create-single-spa/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    create-single-spa

    Single-spa offers a CLI for those who prefer autogenerated and managed configurations for webpack, babel, jest, etc. You do not have to use the CLI in order to use single-spa.

    The CLI is called create-single-spa (Github link). It is primarily intended for the creation of new projects, but may also be useful for migrating existing projects (especially migrating CRA or frameworkless projects).

    Installation and Usage

    If you wish to have create-single-spa globally available, run the following in a terminal

    npm install --global create-single-spa

    # or
    yarn global add create-single-spa

    Then run the following:

    create-single-spa

    Alternatively, you may use create-single-spa without global installation:

    npm init single-spa

    # or
    npx create-single-spa

    # or
    yarn create single-spa

    This will open up a CLI prompt asking you what kind of project you want to create or update.

    CLI arguments

    You may pass arguments to create-single-spa like so:

    # Different ways of doing the same thing
    create-single-spa --framework react
    npm init single-spa --framework react
    npx create-single-spa --framework react
    yarn create single-spa --framework react

    Here are the available CLI options:

    --dir

    You may specify which directory create-single-spa runs in through either of the following ways:

    # Two ways of doing the same thing
    create-single-spa my-dir
    create-single-spa --dir my-dir

    --moduleType

    You can specify which kind of microfrontend you are creating with the --moduleType CLI argument:

    create-single-spa --moduleType root-config
    create-single-spa --moduleType app-parcel
    create-single-spa --moduleType util-module

    --framework

    You can specify which framework you're using with the --framework CLI argument. Note that if you specify a framework that you may omit the --moduleType, as it is inferred to be app-parcel.

    create-single-spa --framework react
    create-single-spa --framework vue
    create-single-spa --framework angular

    --layout

    When generating a root config, the --layout CLI argument indicates that you want to use single-spa-layout in your root config.

    --skipInstall

    This option skips npm/yarn/pnpm installation during project creation.

    Project types

    create-single-spa asks you if you'd like to create a single-spa application, a utility module, or a root-config. All three module types assume that you are using the recommended setup.

    If you select that you'd like to create a single-spa application, you will be prompted for which framework you'd like to choose. React is implemented with premade configurations for babel + webpack + jest. Angular is implemented with Angular CLI and single-spa-angular. Vue is implemented with Vue CLI and vue-cli-plugin-single-spa.

    NPM packages

    Within the create-single-spa repo, there are several NPM packages. The following sections document each package:

    create-single-spa

    Github project

    The core CLI, which invokes generator-single-spa.

    generator-single-spa

    Github project

    A Yeoman generator that prompts the user and then creates files. This is primarily invoked via the create-single-spa CLI, but can also be composed if you'd like to customize it.

    single-spa-web-server-utils

    The single-spa-web-server-utils package is a collection of functions that help when implementing a web server for an index.html file. This package can be used to inline import maps into an HTML, which helps with the performance of your application. Additionally, it can be used to modify a browser import map so that it's suitable for usage in NodeJS for dynamic module loading and server rendering (Dynamic Module Loading and Server Rendering)).

    The web server utils poll the import map from a URL and generate a browserImportMap and nodeImportMap from the response.

    Installation

    npm install --save single-spa-web-server-utils

    # alternatively
    yarn add single-spa-web-server-utils

    getImportMaps

    The getImportMaps function accepts an object parameter and returns a promise that resolves with an object with two import maps: browserImportMap and nodeImportMap. Note that import maps are polled at the specified interval forever until either reset() or clearAllIntervals() is called. Import Maps are stored in memory in a javascript variable that exists outside of the getImportMaps function, so subsequent calls to getImportMaps will all use the same cache.

    const { getImportMaps } = require('single-spa-web-server-utils');
    const http = require('http');
    const ejs = require('ejs');
    const fs = require('fs');
    const path = require('path');

    const htmlTemplate = ejs.compile(
    fs.readFileSync(path.resolve(process.cwd(), 'views/index.html'), 'utf-8'),
    );

    http.createServer((req, res) => {
    getImportMaps({
    // required
    // The URL at which the server
    url: 'https://my-cdn.com/live.importmap',

    // optional - defaults to 30000
    // The ms to wait when polling the import map
    pollInterval: 30000,

    // optional - defaults to false
    // Whether to allow for import-map-overrides via cookies sent in the request.
    // More details about overrides via cookies at
    // https://github.com/joeldenning/import-map-overrides/blob/master/docs/api.md#node
    allowOverrides: true,

    // optional - only needed when allowOverrides is true
    // The IncomingMessage from an http server. This is used to gather
    // cookies for import-map-overrides
    req,

    // optional
    // This allows you to remove entries from the downloaded import map
    // from the returned `nodeImportMap`. This is useful for customizing
    // an import map that is used in the browser so that it can be used
    // for dynamic NodeJS module loading. Each key is a string import specifier.
    // Keys that you return `true` for are preserved in the nodeImportMap.
    nodeKeyFilter(key) {
    return true;
    },
    }).then(({ browserImportMap, nodeImportMap }) => {
    console.log(browserImportMap, nodeImportMap);

    // Example of how to inline a browser import map
    const htmlWithInlinedImportMap = htmlTemplate({
    importMap: browserImportMap,
    });
    res.setResponseHeader('Content-Type', 'text/html');
    res.status(200).send(htmlWithInlinedImportMap);

    // Example of how to apply a NodeJS import map
    // More info at https://github.com/node-loader/node-loader-import-maps
    global.nodeLoader.setImportMapPromise(Promise.resolve(nodeImportMap));
    import('module-in-import-map');
    });
    });

    clearAllIntervals

    This clears all import map polling intervals that were created via setInterval() inside of getImportMaps(). This is useful for tests and for cleaning up memory.

    import { clearAllIntervals } from 'single-spa-web-server-utils';

    clearAllIntervals();

    reset

    This clears all intervals (see clearAllIntervals), and also clears the in-memory cache of all import maps. In other words, after reset() is called, getImportMaps() will always result in a new network request to fetch the import map.

    import { reset } from 'single-spa-web-server-utils';

    reset();
    - - + + \ No newline at end of file diff --git a/docs/5.x/devtools/index.html b/docs/5.x/devtools/index.html index 0d5a95e7d..28ab3381d 100644 --- a/docs/5.x/devtools/index.html +++ b/docs/5.x/devtools/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    single-spa-inspector

    The single-spa-inspector is a Firefox/Chrome devtools extension to provide utilities for helping with single-spa applications. Github project.

    Requires >= single-spa@4.1.

    Note: you can also build and run this locally. See how to contribute.

    Features

    • List all registered applications (mounted at top)
    • Show all application statuses (statii)
    • Force mount and unmount an application
    • Show app overlays (see configuring app overlays to enable this feature)
    • Provides an interface for adding import-map overrides

    Configuring app overlays

    App overlays allow you to hover over a mounted app's name and have an "inspect element" type experience which shows where the app is in the DOM. This is especially useful for when multiple apps are mounted at the same time (e.g. in some places Canopy has upwards of 4 apps mounted on a single page/URL!).

    To add app overlays, find the file where you export your lifecycle functions (e.g. bootstrap, mount, unmount) and add another exported object with the following shape:

    // must be called "devtools"
    export const devtools = {
    overlays: {
    // selectors is required for overlays to work
    selectors: [
    // an array of CSS selector strings, meant to be unique ways to identify the outermost container of your app
    // you can have more than one, for cases like parcels or different containers for differet views
    '#my-app',
    '.some-container .app',
    ],
    // options is optional
    options: {
    // these options allow you some control over how the overlay div looks/behaves
    // the listed values below are the defaults

    width: '100%',
    height: '100%',
    zIndex: 40,
    position: 'absolute',
    top: 0,
    left: 0,
    color: '#000', // the default for this is actually based on the app's name, so it's dynamic. can be a hex or a CSS color name
    background: '#000', // the default for this is actually based on the app's name, so it's dynamic. can be a hex or a CSS color name
    textBlocks: [
    // allows you to add additional text to the overlay. for example, you can add the name of the team/squad that owns this app
    // each string in this array will be in a new div
    // 'blue squad', 'is awesome'
    // turns into:
    // <div>blue squad</div><div>is awesome</div>
    ],
    },
    },
    };

    import-map-overrides

    If your environment uses import-maps, single-spa Inspector provides an interface for adding import-map overrides when utilizing the import-map-overrides library. Once the installation requirements for import-map-overrides are completed, you can add, remove, and refresh the page with your overrides.

    Example of single-spa Inspector extension with import-maps overrides

    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-alpinejs/index.html b/docs/5.x/ecosystem-alpinejs/index.html index 9af51319e..4886ba252 100644 --- a/docs/5.x/ecosystem-alpinejs/index.html +++ b/docs/5.x/ecosystem-alpinejs/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ single-spa applications or parcels.

    Installation

    npm install --save single-spa-alpinejs

    # or
    yarn add single-spa-alpinejs

    Alternatively, you can use single-spa-alpinejs from a CDN as a global variable:

    <script src="https://cdn.jsdelivr.net/npm/single-spa-alpinejs"></script>

    Note that you might want to lock down the package to a specific version. See here for how to do that.

    Usage

    • There are three ways the you can define AlpineJS components as single-spa applications or parcels.

    1 - Template Only

    • The simplest way where the template contains all the required data and initialization logic (including x-data and x-init) as part of the dom. The template is provided via the options attribute template

    2 - Template with externally defined x-data

    • You could also provide x-data externally and the helper will add it to the component.
      • The x-data can be provided in the following forms (via the options attribute xData)
        • an object
        • a function that returns an object
        • a function that returns a promise which resolves with an object.

    3 - Template with externally defined x-data with x-init

    • You can also provide x-init externally along with the x-data and the helper will add it to the component.

    • The x-init can be provided in the following forms (via the options attribute xInit) and needs to be a function.

    • Please note the xData attribute must be provided otherwise the xInit attribute will be ignored.

    • The sample below references the example from the Alpine Toolbox - Alpine JS and fetch() and demonstrates how you can use the xInit and xData attributes to create an AlpineJS application .

    Usage Examples

    1 - Template Only

    import singleSpaAlpinejs from 'single-spa-alpinejs';

    const alpinejslifecycles = singleSpaAlpinejs({
    template: `
    <div class="rounded overflow-hidden shadow-lg font-sans p-1 m-1"
    x-data="{ open: false }">
    <div class="font-bold p-1">Example for x-show attribute</div>
    <button class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold
    hover:text-white py-2 px-4 border border-blue-500
    hover:border-transparent rounded"
    @click="open = !open">Open/Close</button>
    <div x-show="open" class="text-4xl">
    Hey, I'm open
    </div>
    </div>`,
    });

    export const bootstrap = alpinejslifecycles.bootstrap;
    export const mount = alpinejslifecycles.mount;
    export const unmount = alpinejslifecycles.unmount;

    Via cdn

    Example usage when installed via CDN:

    • The usage is similar and once the library is loaded it will be available globally and accessed via the window object.
    const alpinejsApp = window.singleSpaAlpinejs({
    template: `
    <div class="rounded overflow-hidden shadow-lg font-sans p-1 m-1"
    x-data="{ open: false }">
    <div class="font-bold p-1">Example for x-show attribute</div>
    <button class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold
    hover:text-white py-2 px-4 border border-blue-500
    hover:border-transparent rounded" @click="open = !open">Open/Close</button>
    <div x-show="open" class="text-4xl">
    Hey, I'm open
    </div>
    </div>`,
    });

    singleSpa.registerApplication({
    name: 'name',
    app: alpinejsApp,
    activeWhen: () => true,
    });

    2 - Template with externally defined x-data

    import singleSpaAlpinejs from 'single-spa-alpinejs';

    const alpinejslifecycles = singleSpaAlpinejs({
    template: `
    <div class="rounded overflow-hidden shadow-lg font-sans p-1 m-1">
    <div class="font-bold p-1">Example for x-show attribute</div>
    <button class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold
    hover:text-white py-2 px-4 border border-blue-500
    hover:border-transparent rounded" @click="open = !open">Open/Close</button>
    <div x-show="open" class="text-4xl">
    Hey, I'm open
    </div>
    </div>`,
    xData: { open: false },
    });

    export const bootstrap = alpinejslifecycles.bootstrap;
    export const mount = alpinejslifecycles.mount;
    export const unmount = alpinejslifecycles.unmount;

    3 - Template with externally defined x-data with x-init

    import singleSpaAlpinejs from 'single-spa-alpinejs';

    const appTemplate = `
    <div class="w-full h-full text-gray-800">
    <h1 class="mt-0 mb-3 font-light text-3xl" x-text="title"><!-- title text --></h1>
    <p class="text-xl text-gray-600 font-light mb-4" x-html="intro"><!-- intro text --></p>
    <div class="flex flex-wrap -mx-2 pb-8">
    <!-- begin: user card -->
    <template x-for="user in users" :key="user.id">
    <div class="w-full md:w-1/2 lg:w-1/3 xl:w-1/4 h-auto font-light">
    <div class="flex bg-white rounded-lg shadow-md m-2 border-l-4
    border-white hover:shadow-2xl hover:border-pink-500
    cursor-pointer relative">
    <div class="p-4 pr-6 leading-normal">
    <div class="font-medium text-xl truncate" x-text="user.name"></div>
    <div class="truncate uppercase text-xs text-gray-500 font-semibold
    pb-2 tracking-widest" x-text="user.company.name"></div>
    <div class="" x-text="user.phone"></div>
    <a class="text-blue-600 hover:text-blue-700 mr-4 block"
    x-bind:href="'mailto:' + user.email" x-text="user.email"></a>
    <a class="text-blue-600 hover:text-blue-700 block"
    x-bind:href="'https://' + user.website" x-text="user.website"></a>
    </div>
    </div>
    </div>
    </template>
    <!-- end: user card -->
    </div>
    </div>
    `;

    const appDataFn = ({ title, name }) => ({
    title,
    intro:
    'Implement a simple <code class="text-md text-pink-600">fetch()</code> request to render a list of items using Alpine.js :)',
    users: [],
    open: false,
    name,
    });

    const appXInitFn = (id) => {
    return fetch('https://jsonplaceholder.typicode.com/users')
    .then((response) => response.json())
    .then((data) => (document.querySelector(`#${id}`).__x.$data.users = data));
    };

    const opts = {
    template: appTemplate,
    xData: (data) => appDataFn(data), // pass props to x-data
    xInit: appXInitFn,
    };

    const alpinejslifecycles = singleSpaAlpinejs(opts);

    export const bootstrap = alpinejslifecycles.bootstrap;
    export const mount = alpinejslifecycles.mount;
    export const unmount = alpinejslifecycles.unmount;

    API / Options

    single-spa-html is called with an object that has the following properties:

    • template (required): An HTML string or a function that returns a string. The function will be called with the single-spa custom props. The returned string is injected into the DOM during the single-spa mount lifecycle.
    • domElementGetter (optional): A function that returns the dom element container into which the HTML will be injected. If omitted, a default implementation is provided that wraps the template in a <div> that is appended to document.body.
    • xData (optional): An object or a function or a function that returns a promise.The returned string is injected into the DOM as the x-data attribute during the single-spa mount lifecycle.
    • xInit (optional): A function or a function that returns a promise. The function provided is added to the global scope and the function initiation along with the root dom element id as a parameter is injected into the DOM as the x-init attribute during the single-spa mount lifecycle. Please note the xData attribute must be provided otherwise the xInit attribute will be ignored. The function you provide xInit

    xData and xInit Handling

    • This section covers the details of how xData and xInit option attributes are processed by the single spa helper.

    • Consider the example below

    const appDataFn = () => { open: false, loading: true }
    const appXInitFn = (domId) => {
    console.log('Hello from appXInitFn');
    // domId provides access to the parent dom element where x-data and x-init are defined
    document.querySelector(`#${domId}`).__x.$data.loading = false
    }

    const opts = {
    template: appTemplate, // base template
    xData: (data) => appDataFn(data), // pass props to x-data
    xInit: appXInitFn, // x-Init function
    };

    const alpinejsApp = singleSpaAlpinejs(opts);

    singleSpa.registerApplication({
    name: 'myapp',
    app: alpinejsApp,
    activeWhen: () => true,
    });

    • The helper does the following

      • Adds the template to the dom wrapped in parent dom element with and id that has a prefix of alpine. In this case it will be id='alpine-myapp'
      • Attaches a resolved xData as a string x-data="{ "name": "myapp" ,"open": false }" to the parent dom element.
      • It will make the user defined appXInitFn available globally as an attribute of window.singleSpaAlpineXInit and will be accessible via variable window.singleSpaAlpineXInit.myapp
      • Attaches a resolved xInit as a string that calls the globally defined variable x-init="singleSpaAlpineXInit.myapp('alpine-myapp')" to the parent dom element.
      • Note that this also passes id of the parent dom element which can then be used to access the alpine data elements to update the state as required.

      Special characters in the application names

      • You may have special characters in the application name for example @my/app. See the example below
          singleSpa.registerApplication({
      name: '@my/app',
      app: alpinejsApp,
      activeWhen: () => true,
      });
      • The single spa helper converts these to valid global function names by replacing all the special characters with underscores (_). This does not require any special handling from the user as the helper takes care of this internally

      • In the above case the xInit dom element would look like the following x-init="singleSpaAlpineXInit._my_app('alpine-@my/app')" where the xInit function is available as a global variable _my_app.

    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-angular/index.html b/docs/5.x/ecosystem-angular/index.html index 62be3abc8..c6a20c10a 100644 --- a/docs/5.x/ecosystem-angular/index.html +++ b/docs/5.x/ecosystem-angular/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -111,7 +111,7 @@ asynchronous method that fetches the component.

    Since some versions of webpack use SystemJS under the hood, you'll need to reference the global version.

    // Inside of src/app/app.component.ts
    import { ChangeDetectionStrategy, Component } from '@angular/core';
    import { mountRootParcel } from 'single-spa';

    @Component({
    selector: 'app-root',
    template:
    '<parcel [config]="config" [mountParcel]="mountRootParcel"></parcel>',
    changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class AppComponent {
    async config() {
    return window.System.import('@org/react-component');
    }
    mountRootParcel = mountRootParcel;
    }

    Passing Custom Props

    You can pass any custom props to the parcel by passing an object of props using the customProps attribute.

    For example, if you're rendering a React parcel from an Angular component, you can pass a click handler from Angular into the React parcel:

    // Inside of src/app/ReactWidget/ReactWidget.tsx
    import * as React from 'react';
    import * as ReactDOM from 'react-dom';
    import singleSpaReact from 'single-spa-react';

    const ReactWidget = ({ handleClick }) => (
    <button onClick={handleClick}>Click Me!</button>
    );

    export const parcelConfig = singleSpaReact({
    React,
    ReactDOM,
    rootComponent: ReactWidget,
    });

    You can pass a function (or any other value) as a custom prop. To ensure that the functions you pass to the parcel are bound with the correct javascript context, use the handleClick = () => { syntax when defining your functions.

    // Inside of src/app/app.component.ts
    import { ChangeDetectionStrategy, Component } from '@angular/core';
    import { mountRootParcel } from 'single-spa';

    import { config } from './ReactWidget/ReactWidget';

    @Component({
    selector: 'app-root',
    template:
    '<parcel [config]="config" [customProps]="parcelProps" [mountParcel]="mountRootParcel"></parcel>',
    changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class AppComponent {
    config = config;
    mountRootParcel = mountRootParcel;

    handleClick = () => {
    alert('Hello World');
    };

    parcelProps = {
    handleClick = this.handleClick,
    };
    }

    Zone-less applications

    info

    This feature is available starting from single-spa-angular@4.1.

    It's possible to develop Angular applications that don't rely on zone.js library, these applications are called zone-less. You have to run change detection manually in zone-less applications through ApplicationRef.tick() or ChangeDetectorRef.detectChanges(). You can find more info in Angular NoopZone docs.

    The point is that you do not need to load zone.js library in your root HTML file. As Angular docs mention that you should have a comprehensive knowledge of change detection to develop such applications. Let's start by nooping zone when bootstrapping module:

    import { singleSpaAngular } from 'single-spa-angular';

    const lifecycles = singleSpaAngular({
    bootstrapFunction: () =>
    platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' }),
    template: '<app-root />',
    NgZone: 'noop',
    });
    caution

    We must specify noop twice: when bootstrapping AppModule, and setting NgZone property to noop. This tells Angular and single-spa-angular that we're not going to use zones.

    Routing in zone-less applications

    Since routing is managed by single-spa and there is no zone that tells Angular that some asynchronous event has occured, then we need to tell Angular when to run change detection if routing occurs. Let's look at the below code:

    import { ApplicationRef } from '@angular/core';
    import { Router, NavigationStart } from '@angular/router';
    import { singleSpaAngular } from 'single-spa-angular';

    const lifecycles = singleSpaAngular({
    bootstrapFunction: async () => {
    const ngModuleRef = await platformBrowserDynamic().bootstrapModule(
    AppModule,
    { ngZone: 'noop' },
    );

    const appRef = ngModuleRef.injector.get(ApplicationRef);
    const listener = () => appRef.tick();
    window.addEventListener('popstate', listener);

    ngModuleRef.onDestroy(() => {
    window.removeEventListener('popstate', listener);
    });

    return ngModuleRef;
    },
    template: '<app-root />',
    NgZone: 'noop',
    Router,
    NavigationStart,
    });
    caution

    single-spa-angular@4.x requires calling getSingleSpaExtraProviders function in applications that have routing. Do not call this function in zone-less application.

    Inter-app communication via RxJS

    First of all, check out this Inter-app communication guide.

    It's possible to setup a communication between microfrontends via RxJS using cross microfrontend imports.

    We can not create complex abstractions, but simply export the Subject:

    // Inside of @org/api
    import { ReplaySubject } from 'rxjs';
    import { User } from '@org/models';

    // `1` means that we want to buffer the last emitted value
    export const userSubject$ = new ReplaySubject<User>(1);

    And then you just need to import this Subject into the microfrontend application:

    // Inside of @org/app1 single-spa application
    import { userSubject$ } from '@org/api';
    import { User } from '@org/models';

    userSubject$.subscribe(user => {
    // ...
    });

    userSubject$.next(newUser);

    Also, you should remember that @org/api should be an "isolated" dependency, for example the Nrwl Nx library, where each library is in the "libs" folder and you import it via TypeScript paths.

    Every application that uses this library should add it to its Webpack config as an external dependency:

    const singleSpaAngularWebpack = require('single-spa-angular/lib/webpack')
    .default;

    module.exports = (config, options) => {
    const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options);
    singleSpaWebpackConfig.externals = [/^@org\/api$/];
    return singleSpaWebpackConfig;
    };

    But this library should be part of root application bundle and shared with import maps, for example:

    {
    "imports": {
    "@org/api": "http://localhost:8080/api.js"
    }
    }
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-angularjs/index.html b/docs/5.x/ecosystem-angularjs/index.html index 3829214c8..3c9c755a9 100644 --- a/docs/5.x/ecosystem-angularjs/index.html +++ b/docs/5.x/ecosystem-angularjs/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -26,7 +26,7 @@ loading function should be System.import('name-of-app'). Make sure to add name-of-app to your import map.

    As a global variable

    // note that "js" is not capitalized in the name of the global variable.
    window.myAngularApp = window.singleSpaAngularjs.default({
    angular: angular,
    mainAngularModule: 'app',
    uiRouter: true,
    preserveGlobal: false,
    template: '<my-component />',
    })

    Your loading function should just be the global variable itself. For example:

    singleSpa.registerApplication({
    name: 'my-angular-app',
    app: myAngularApp,
    activeWhen: () => true
    });

    Options

    All options are passed to single-spa-angularjs via the opts parameter when calling singleSpaAngularJS(opts). The following options are available:

    • angular: (required) The main angular object, which is generally either exposed onto the window or is available via require('angular') or import angular from 'angular'.
    • domElementGetter: (optional) A function that takes in the props parameter and returns a DOMElement. This dom element is where the angular application will be bootstrapped, mounted, and unmounted. If not provided, the default is to create a div and append it to document.body.
    • mainAngularModule: (required) A string that is the name of the angular module that will be bootstrapped by angular. See angular docs for angular.bootstrap().
    • uiRouter: (optional) If you are using angular-ui-router, set this option to either true or to a string value. The string value will be the value of the ui-view HTML attribute. For example, uiRouter: 'core' will be <div ui-view="core" /> whereas uiRouter: true turns into <div ui-view />.
    • ngRoute: (optional) If you are using ngRoute, set this option to true to have an <ng-view> element automatically injected into the DOM during mount.
    • preserveGlobal: (optional) A boolean that defaults to false. Set if you want to keep angular on the global even after an app unmounts.
    • elementId: (optional) A string which will be used to identify the element appended to the DOM and bootstrapped by Angular.
    • strictDi: (optional - part of the bootstrap config object) A boolean that defaults to false. Set if you want to enable StrictDi mode
    • template: (optional) An HTML string that will be inserted into the DOM when the app is mounted. The template goes inside of the element returned by domElementGetter. If not provided, no template will be inserted. When using angular-ui-router, you often do not need to use this since ui-router will be putting a template onto the dom for you.

    Custom Props

    single-spa custom props are made available as $rootScope.singleSpaProps. In templates, you can access custom props via $root.singleSpaProps. For example:

    <div>{{ $root.singleSpaProps.token }}</div>

    Parcels

    Creating AngularJS parcels

    The singleSpaAngularJs() function returns an object that can serve as either a single-spa application or single-spa parcel.

    Rendering parcels in AngularJS

    To render a single-spa parcel inside of your AngularJS application, you can use the <single-spa-parcel> directive. To do so, first add the "single-spa-angularjs" module as a dependency of your application:

    import 'single-spa-angularjs/lib/parcel.js';

    angular.module('myMainModule', [
    'single-spa-angularjs'
    ])

    Then you can use the <single-spa-parcel> directive in your templates:

    <single-spa-parcel
    parcel-config="parcelConfig"
    props="parcelProps"
    mount-parcel="mountRootParcel"
    />

    In your controller, set the corresponding values on the $scope:

    import { mountRootParcel } from 'single-spa';

    // The parcelConfig binding is required. It must be an object or loading function that resolves with an object.
    $scope.parcelConfig = {async mount() {}, async unmount() {}}

    // You can retrieve parcels from other microfrontends via cross-microfrontend imports
    // See https://single-spa.js.org/docs/recommended-setup#cross-microfrontend-imports
    // $scope.parcelConfig = () => System.import('@org/settings-modal');

    // The props binding is optional, defaulting to no custom props being passed into the parcel
    $scope.props = {
    extra: 'info can be passed here'
    }

    // As long as you're using <single-spa-parcel> inside of another single-spa application or parcel,
    // the mountParcel binding is not needed. However, it is needed otherwise.
    $scope.mountParcel = mountRootParcel

    If you run into issues related to singleSpaProps not being available for injection, this is likely caused by using <single-spa-parcel> outside of a single-spa application or parcel. It is okay to do so, but you'll need to manually provide the singleSpaProps value:

    import { mountRootParcel } from 'single-spa';

    angular.module('single-spa-angularjs').config(['$provide', ($provide) => {
    // This can be an empty object, you just need the DI to not fail
    const props = {};

    // Alternatively, you can provide a mountParcel function that will be used as the default value for the mount-parcel attribute
    // const props = {mountParcel: mountRootParcel}

    $provide.value('singleSpaProps', props);
    }])

    Migrating

    Migrating an existing AngularJS application to single-spa can be a tricky. Here are some recommendations.

    High level approach

    1. Convert the angularjs application to be a single-spa application via global variables.
    2. Switch the angularjs application from being a global variable to being SystemJS in-browser module.
    3. Add a new single-spa application (doesn't need to be angularjs)

    Step 1: Convert to global variable

    1. Load single-spa and single-spa-angularjs as global variables in your main HTML file:
    <!--
    Consider upgrading the versions of these libraries
    They likely have had updates since this documentation was written
    -->
    <script src="https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/umd/single-spa.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/single-spa-angularjs@4.2.1/lib/single-spa-angularjs.min.js"></script>
    1. Change your angularjs application to not mount to the DOM. This is generally done removing the ng-app attribute in your main html file.
    2. In one of the first / main scripts loaded for your angularjs application, create your single-spa application as a global variable. See this code.
    3. In your main HTML file, add the following:
    <script>
    window.singleSpa.registerApplication({
    name: "legacyAngularjsApp",
    app: window.legacyAngularjsApp,
    activeWhen: ['/']
    })
    window.singleSpa.start();
    </script>
    1. Confirm that your application now is mounting again and works properly. Also, check that it's in MOUNTED status as a single-spa microfrontend:
    // in the browser console, check that it's in `MOUNTED` status
    console.log('legacyAngularjsApp status', singleSpa.getAppStatus('legacyAngularjsApp'));

    Step 2: Convert to SystemJS module:

    This step is not required unless you want to do cross microfrontend imports between your angularjs microfrontend and other microfrontends.

    1. Add systemjs to your index.html file:
    <!-- consider checking/upgrading systemjs version -->
    <script type="systemjs-importmap">
    {
    "imports": {
    "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/system/single-spa.min.js",
    "@org/legacyAngularjsApp": "/main-angular-app.js"
    }
    }
    </script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.10.2/dist/system.min.js"></script>
    1. Remove the global single-spa script:
    - <script src="https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/umd/single-spa.min.js"></script>
    1. Modify your main / first angularjs script file to create a systemjs module instead of global variable. See this code.

    2. Remove the <script> for loading that main / first angularjs script file. Replace it with a System.import.

    - <script src="/main-angular-app.js"></script>
    1. Modify the <script> in your main HTML file to load the angularjs app as a systemjs module instead of global variable:
     window.singleSpa.registerApplication({
    name: "legacyAngularjsApp",
    - app: window.legacyAngularjsApp,
    + app: function() { return System.import('@org/legacyAngularjsApp'); },
    activeWhen: ['/']
    })
    1. Verify that the app continues working.

    Step 3: Add new microfrontend

    In single-spa, it's encouraged to split microfrontends by route. During the migration/transition period, you may need to have the legacy angularjs application always active to show navigation menus, even for routes that are controlled by new microfrontends.

    It's recommended to create new microfrontends via the single-spa CLI.

    1. Add a new call to registerApplication() to your index.html file.
    window.singleSpa.registerApplication({
    name: "new-microfrontend",
    app: function () { return System.import("new-microfrontend"); },
    activeWen: ["/route-for-new-microfrontend"]
    })
    1. Add the new microfrontend to your import map in the index.html file.
    <script type="systemjs-importmap">
    {
    "imports": {
    "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/system/single-spa.min.js",
    "@org/legacyAngularjsApp": "/main-angular-app.js",
    "@org/new-microfrontend": "http://localhost:8080/new-microfrontend.js"
    }
    }
    </script>
    1. Start a new microfrontend on the port in the import map. Go to the route for the new microfrontend and verify it is loaded.

    Examples

    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-backbone/index.html b/docs/5.x/ecosystem-backbone/index.html index d96ee9814..d57e18b3c 100644 --- a/docs/5.x/ecosystem-backbone/index.html +++ b/docs/5.x/ecosystem-backbone/index.html @@ -14,15 +14,15 @@ - - + +
    Version: 5.x

    single-spa-backbone

    A single-spa helper library which provides lifecycle events for building single-spa applications using Backbone.

    npm Package License

    There are mostly three styles of creating backbone applications

    1. Using RequireJS which will loads the application and all it's dependencies, including the templates loaded using Handlebars, RequireJS:Text or any other engine.

      If your application is written using this style, then you will have to pass the AppWithRequire parameter as options in the plugin, and choose the flavour to load the app, either through data-main attribute or without it.

    2. Using Backbone and ApplicationPath (Entry point of application) directly as script elements and optionally loading other dependencies.

    3. Loading a single application bundle which includes application dependencies like i.e. Backbone, Require, Underscore, Jquery etc.

    NPM package

    npm install --save @emtecinc/single-spa-backbone

    The npm package can be found here.

    Quickstart

    Option 1: Using RequireJS with data-main

    First, in the single-spa application, run npm install --save @emtec/single-spa-backbone. Then, create an entry file for application like below, assuming the application has some BasePath with AppPath and `RequireJsPath' being relative to the base path.

    import singleSpaBackbone from '@emtecinc/single-spa-backbone';

    const backBoneLifecycles = singleSpaBackbone({
    BasePath: 'appBasePath',
    AppWithRequire:
    {
    IsDataMain: true,
    AppPath: 'src/app',
    RequireJsPath: 'lib/require.js'
    },
    DomElementSetter: domElementSetter
    });

    export const bootstrap = [
    backBoneLifecycles.bootstrap,
    ];

    export const mount = [
    backBoneLifecycles.mount,
    ];

    export const unmount = [
    backBoneLifecycles.unmount,
    ];


    function domElementSetter() {

    //use the same element id to render into, in the backbone app
    let el = document.getElementById('shell-container');
    if (!el) {
    el = document.createElement('div');
    el.id = 'shell-container';
    document.body.appendChild(el);
    }

    }

    DomElementSetter gives you a provision to hook up your callback, and can be used to create a container element in the dom which will be used to load the app.

    Please note that this hook up is just a setter and don't expect you to return a value. You need to explicitly use the same element #id in the backbone application to use it as app region or container.

    Option 2: Using RequireJS without data-main

    IsDataMain will be set to false in this case

    import singleSpaBackbone from '@emtecinc/single-spa-backbone';

    const backBoneLifecycles = singleSpaBackbone({
    BasePath: 'appBasePath',
    AppWithBackboneJs:
    {
    AppPath: 'src/app',
    BackboneJsPath: 'lib/backbone.js'
    },
    DomElementSetter: domElementSetter
    });

    export const bootstrap = backBoneLifecycles.bootstrap;

    export const mount = backBoneLifecycles.mount;

    export const unmount = backBoneLifecycles.unmount;

    function domElementSetter() {

    //use the same element id to render into, in the backbone app
    let el = document.getElementById('shell-container');
    if (!el) {
    el = document.createElement('div');
    el.id = 'shell-container';
    document.body.appendChild(el);
    }

    }

    Option 3: Load Backbone app using production build

    import singleSpaBackbone from '@emtecinc/single-spa-backbone';

    const backBoneLifecycles = singleSpaBackbone({
    BasePath: 'appBasePath',
    App:
    {
    AppPath: 'src/app'
    },
    DomElementSetter: domElementSetter
    });

    export const bootstrap = backBoneLifecycles.bootstrap;

    export const mount = backBoneLifecycles.mount;

    export const unmount = backBoneLifecycles.unmount;


    function domElementSetter() {

    //use the same element id to render into, in the backbone app
    let el = document.getElementById('shell-container');
    if (!el) {
    el = document.createElement('div');
    el.id = 'shell-container';
    document.body.appendChild(el);
    }

    }

    Options

    All options are passed to single-spa-backbone via the userOptions parameter when calling singleSpaBackbone(userOptions). The following properties are available:

    • BasePath (required) : The base path of the backbone application. Mostly it will be the public path from where the app is server and other paths will be relative to this. This parameter expects a string type. optional

    • AppWithRequire (required) : This parameter takes an object and expects below properties:

      • IsDataMain (optional) : This parameter takes a boolean value and is used to specify whether require js will use data-main to load the app.
      • AppPath (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is entry point of your application and will be booted up using RequireJs. The path is relative to BasePath.
      • RequireJsPath (required) : This parameter takes a string value and takes the path of the RequireJs file and is relative to BasePath.
      • DependenciesJsPaths (optional) : This is an optional parameter takes an array of strings. It can be used to optionally provide a list of JavaScript paths which you want to load in the browser.
    • AppWithBackboneJs (optional) : This parameter takes an object and expects below properties:

      • AppPath (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is entry point of your application and will be booted up using Backbone Js. The path is relative to BasePath.
      • BackboneJsPath (required) : This parameter takes a string value and takes the path of the Backbone Js file and is relative to BasePath.
      • DependenciesJsPaths (optional) : This is an optional parameter takes an array of strings. It can be used to optionally provide a list of JavaScript paths which you want to load in the browser.
    • App (optional) : This parameter takes an object and expects below properties:

      • AppPath (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is the production build of your backbone application. The path is relative to BasePath.

    Note : Out of AppWithRequire, AppWithBackboneJs and App only one is required

    • DomElementSetter (optional) : This is an optional parameter and can be mostly used to create a dom element, whose id can be later used in the backbone app to load the application. However, you can freely use this callback for any other purpose. It is called before anything else.
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-css/index.html b/docs/5.x/ecosystem-css/index.html index 4a6a730ae..89aa92e49 100644 --- a/docs/5.x/ecosystem-css/index.html +++ b/docs/5.x/ecosystem-css/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ With single-spa, each application is wrapped in a <div id="single-spa-application:@org-name/project-name"></div>, which can be used as a prefix to all CSS classes and selectors. Run CSS.escape("single-spa-application:@org-name/project-name") in the browser console to make sure the HTML id is escaped, then prefix it with # so that it matches the id. The resulting string is what you pass into postcss-prefix-selector.

    The example code above in the Scoped CSS section shows the mechanics of how selector prefixing can accomplish scoping, and postcss-prefix-selector can do this automatically to all of your CSS. Below is an example PostCSS configuration file:

    // postcss.config.js
    const prefixer = require('postcss-prefix-selector');

    module.exports = {
    plugins: [
    prefixer({
    prefix: "#single-spa-application\\:\\@org-name\\/project-name"
    })
    ]
    }

    Shadow DOM

    Shadow DOM is a browser API for scoping CSS. It is designed to be used by Web Components, and is mentioned here as another viable option for scoping CSS.

    Below are some notes about Shadow DOM that may be relevant to microfrontends:

    • Shadow DOM prevents any global CSS from cascading into the Shadow Root, which means you can't easily have global, shared CSS.
    • CSS custom properties from outside the Shadow Root can be used within the Shadow Root.
    • The HTML elements within the Shadow DOM are not reachable by CSS selectors outside of the Shadow Root.
    • Events that propagate from a Shadow Root are retargeted at each shadow boundary.

    Lazy Loading

    "Loading" CSS refers to downloading the CSS by inserting a <link rel="stylesheet" href="/my-file.css"> element into the DOM, or by downloading a Javascript file that inserts a <style></style> element into the DOM.

    "Lazy Loading" refers to only inserting the <link> or <style> elements into the DOM once they are needed, instead of all at once. In single-spa, this is during the load or mount lifecycle functions.

    Each microfrontend should only load its CSS into the DOM after its Javascript is downloaded. Single-spa lazy loads the Javascript for each microfrontend, by default; therefore, the CSS for the microfrontends will only be loaded as needed.

    Unmounting CSS

    In large systems with dozens of microfrontends, it can become important for performance to unmount CSS as you navigate between pages. This is accomplished by removing <style> and <link> elements from the DOM.

    By default, most tooling will load and mount the CSS one time and leave it there indefinitely (it never unmounts!). However, some resources exist for unmounting CSS that is no longer being used, and remounting it once it's needed again.

    To accomplish this, single-spa applications and parcels should remove <link> and <style> elements inside of their unmount lifecycle function:

    // This code is an example of the mechanics of mounting + unmounting + remounting CSS.
    // In practice, this is often done via tools like css-loader, style-loader, or
    // single-spa-css (rather than manually).
    const style = document.createElement('style');
    style.textContent = `.settings {color: blue;}`;

    export const mount = [
    async () => {
    document.head.appendChild(styleElement);
    },
    reactLifecycles.mount,
    ]

    export const unmount = [
    reactLifecycles.unmount,
    async () => {
    styleElement.remove();
    }
    ]

    To help you accomplish this, this single-spa-css library implements mount and unmount functions for you.

    SASS, PostCSS, Less, Stylus, etc

    SASS, PostCSS, Less, Stylus, and other CSS build tools are all compatible with single-spa and microfrontends.

    These tools run at build-time to produce vanilla CSS files. All of the documentation on this page applies to the output CSS files created by SASS and other CSS preprocessors.

    Since each microfrontend has its own build, this means that there are multiple SASS (or other preprocessor) builds occurring - one per microfrontend. As a result, SASS variables are not shareable via cross microfrontend imports, since cross microfrontend imports occur at runtime. Instead, to share SASS variables, you'll need to publish them to an NPM registry and install them individually into each microfrontend. Since npm packages are not independently deployable (separately from the packages that use them), changes to the variables will need to be updated and deployed in each microfrontend individually. One thing to note is that the browser's implementation of CSS custom properties occurs at runtime and so native CSS custom properties are inherently shareable between microfrontends.

    SASS and other build tools often produce global CSS rather than scoped CSS. This behavior can be undesirable in a microfrontends architecture because it can result in CSS class name collisions between your microfrontends. To avoid this, you can use SASS modules (or similar) to scope the CSS.

    Webpack CSS resources

    Below is a list of commonly used Webpack loaders and plugins that can help with loading CSS:

    • css-loader facilitates using CSS Modules and properly handling @import() within CSS files.
    • style-loader facilitates mounting CSS via <style> elements. This is often used in development mode, but not production.
    • postcss-loader is similar to CSS modules, but for more advanced use cases that require PostCSS.
    • sass-loader can be used to compile SASS to CSS.
    • single-spa-css can be used to automatically detect which CSS files to load during the mount lifecycle function of your single-spa application or parcel.

    single-spa-css

    The single-spa-css npm package implements helper functions for loading, mounting, and unmounting CSS. It does this by adding <link rel="stylesheet"> elements to the DOM to mount the CSS, and removing the <link> from the DOM when it's time to unmount the CSS.

    Installation

    npm install single-spa-css

    pnpm install single-spa-css

    yarn add single-spa-css

    Usage

    import singleSpaCss from 'single-spa-css';

    const cssLifecycles = singleSpaCss({
    // required: a list of CSS URLs to load
    // can be omitted if webpackExtractedCss is set to true, do not specify Webpack extracted css files here
    cssUrls: ['https://example.com/main.css'],

    // optional: defaults to false. This controls whether extracted CSS files from Webpack
    // will automatically be loaded. This requires using the ExposeRuntimeCssAssetsPlugin,
    // which is documented below.
    webpackExtractedCss: false,

    // optional: defaults to true. Indicates whether the <link> element for the CSS will be
    // unmounted when the single-spa microfrontend is unmounted.
    shouldUnmount: true,

    // optional: defaults to 5000. The number of milliseconds to wait on the <link> to load
    // before failing the mount lifecycle.
    timeout: 5000,

    // optional: defaults to a standard <link rel="stylesheet" href="/main.css"> element
    // Customize the creation of the link element that is used by single-spa-css by providing a
    // function. For example, for setting the cross-origin or other HTML attributes on the <link>
    createLink(url) {
    const linkEl = document.createElement('link');
    linkEl.rel = 'stylesheet';
    linkEl.href = url;
    return linkEl;
    },
    });

    const reactLifecycles = singleSpaReact({...})

    // Export an array of lifecycles to integrate with a framework's
    // single-spa lifecycles. The order matters.
    export const bootstrap = [
    cssLifecycles.bootstrap,
    reactLifecycles.bootstrap
    ]

    export const mount = [
    // The CSS lifecycles should be before your framework's mount lifecycle,
    // to avoid a Flash of Unstyled Content (FOUC)
    cssLifecycles.mount,
    reactLifecycles.mount
    ]

    export const unmount = [
    // The CSS lifecycles should be after your framework's unmount lifecycle,
    // to avoid a Flash of Unstyled Content (FOUC)
    reactLifecycles.unmount,
    cssLifecycles.unmount
    ]

    If you want some CSS files to unmount, but others to stay mounted, use the following syntax:

    const cssLifecycles = singleSpaCss({
    cssUrls: [
    {
    href: "https://example.com/main.css",
    shouldUnmount: true,
    },
    {
    href: "https://example.com/other.css",
    shouldUnmount: false,
    },
    ],
    });

    Webpack Plugin

    This plugin currently only supports webpack 5. See issue 7 to track webpack 4 support.

    single-spa-css includes a Webpack plugin that integrates with mini-css-extract-plugin, which allows you to load CSS files that are extracted and otherwise would not be loaded. The Webpack plugin exposes the names of the extracted CSS files to your bundle under the __webpack_require__.cssAssets and __webpack_require__.cssAssetFileName variables. The cssAssets variable contains the name of the Webpack chunk, and the cssAssetFileName function converts the chunk name into the extracted CSS asset's file name. These can be used manually, or you can specify the webpackExtractedCss option in single-spa-css to have it automatically mount and unmount those CSS files.

    Usage

    In your Webpack config, add the following:

    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    const ExposeRuntimeCssAssetsPlugin = require("single-spa-css/ExposeRuntimeCssAssetsPlugin.cjs");

    module.exports = {
    plugins: [
    new MiniCssExtractPlugin({
    filename: "[name].css",
    }),
    new ExposeRuntimeCssAssetsPlugin({
    // The filename here must match the filename for the MiniCssExtractPlugin
    filename: "[name].css",
    }),
    ],
    };
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-cycle/index.html b/docs/5.x/ecosystem-cycle/index.html index 542f1d457..4cde2b365 100644 --- a/docs/5.x/ecosystem-cycle/index.html +++ b/docs/5.x/ecosystem-cycle/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    single-spa-cycle

    single-spa-cycle is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Cycle.js. Check out the single-spa-cycle github.

    Installation

    npm install --save @pcmnac/single-spa-cycle

    Quickstart

    In your project's entry file, add the following:


    import {run} from '@cycle/run'
    import {makeDOMDriver} from '@cycle/dom'
    import singleSpaCycle from '@pcmnac/single-spa-cycle';
    import rootComponent from './root.component.js';

    const cycleLifecycles = singleSpaCycle({
    run,
    rootComponent,
    drivers: { DOM: makeDOMDriver(document.getElementById('main-content'))}, // or { DOM: makeDOMDriver('#main-content')}
    });

    export const bootstrap = cycleLifecycles.bootstrap;
    export const mount = cycleLifecycles.mount;
    export const unmount = cycleLifecycles.unmount;

    Options

    All options are passed to single-spa-cycle via the opts parameter when calling singleSpaCycle(opts). The following options are available:

    • run: (required) Cycle.js run function.
    • drivers: (required) Drivers (including DOM Driver) to be used by your Cycle.js root component.
    • rootComponent: (required) The top level Cycle.js component which will be rendered
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-dojo/index.html b/docs/5.x/ecosystem-dojo/index.html index 552d5d483..ce4d0c013 100644 --- a/docs/5.x/ecosystem-dojo/index.html +++ b/docs/5.x/ecosystem-dojo/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    single-spa-dojo

    Build Status

    single-spa-dojo is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for use with Dojo. Check out the single-spa-dojo github.

    Installation

    npm install --save single-spa-dojo

    # or
    yarn add single-spa-dojo

    Quickstart

    Your bundler's "entry file" should look like this, which allows your application to be downloaded as an in-browser ES module.

    import { renderer } from '@dojo/framework/core/vdom';
    import { v, w } from '@dojo/framework/widget-core/d';
    import singleSpaDojo from 'single-spa-dojo';
    import App from './app';

    const dojoLifecycles = singleSpaDojo({
    // required
    renderer,

    // required
    v,

    // required
    w,

    // required
    appComponent: App,

    // optional - see https://dojo.io/learn/creating-widgets/rendering-widgets#mountoptions-properties
    mountOptions: {
    // optional
    registry: myRegistry,

    // optional - one will be provided by single-spa automatically
    domNode: document.getElementById('myContainer'),

    // optional
    sync: true
    }
    });

    export const bootstrap = dojoLifecycles.bootstrap;
    export const mount = dojoLifecycles.mount;
    export const unmount = dojoLifecycles.unmount;

    Options

    All options are passed to single-spa-dojo via the opts parameter when calling singleSpaDojo(opts). The following options are available:

    • renderer (required): The renderer function imported from Dojo. See https://dojo.io/learn/creating-widgets/rendering-widgets#rendering-to-the-dom.
    • v (required): The function used to render dom elements in Dojo. Often JSX hides this function from you, but it can be found at import { v } from '@dojo/framework/widget-core/d'.
    • w (required): The function used to render dom elements in Dojo. Often JSX hides this function from you, but it can be found at import { w } from '@dojo/framework/widget-core/d'.
    • appComponent (required): The class or function for your root Dojo component.
    • mountOptions (optional): An object of Dojo MountOptions. Note that a domNode will be provided by single-spa-dojo, if one is not provided.
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-ember/index.html b/docs/5.x/ecosystem-ember/index.html index c6783cc0f..dcb7f6c01 100644 --- a/docs/5.x/ecosystem-ember/index.html +++ b/docs/5.x/ecosystem-ember/index.html @@ -14,15 +14,15 @@ - - + +
    Version: 5.x

    single-spa-ember

    single-spa-ember is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for use with Ember.js. Check out the single-spa-ember github.

    It is available on npm as single-spa-ember, and also available on bower as single-spa-ember in case you want to use it with ember cli and need to use bower.

    Overview

    When you are building an ember application that you want to work as a single-spa application, there are five things you need to implement:

    Single-spa-ember will help you implement all of those except for the activity function.

    Note that the loading and activity functions are part of the single-spa root application, whereas the bootstrap, mount, and unmount functions are part of a single-spa application

    API

    loadEmberApp

    loadEmberApp(appName, appUrl, vendorUrl) is a function that helps you implement the loading function for your ember application. appName and appUrl are both strings and both required, whereas vendorUrl is an optional string.

    // In the single-spa root application

    import {registerApplication} from 'single-spa';
    import {loadEmberApp} from 'single-spa-ember';

    const name = 'ember-app';
    const app = () => loadEmberApp(name, '/dist/ember-app/assets/ember-app.js', '/dist/ember-app/assets/vendor.js');
    const activeWhen = location => location.hash.startsWith('ember');

    registerApplication({ name, app, activeWhen });

    singleSpaEmber

    Single-spa-ember will implement the single-spa lifecyle functions for you. To use it, you call the default export as a function with a configuration object, which returns an object that has bootstrap, mount, and unmount lifecycle functions on it. The provided configuration object has the following options:

    // In the ember application
    import singleSpaEmber from 'single-spa-ember/src/single-spa-ember';

    const emberLifecycles = singleSpaEmber({
    appName: 'ember-app', // required
    createOpts: { // See https://www.emberjs.com/api/ember/2.14.1/classes/Ember.Application
    rootElement: '#ember-app',
    },
    });

    export const bootstrap = emberLifecycles.bootstrap;
    export const mount = emberLifecycles.mount;
    export const unmount = emberLifecycles.unmount;

    Usage with ember cli

    For the most part, you can get applications that use ember cli to work pretty seamlessly with single-spa. Maybe the biggest thing you'll have to worry about is that ember-cli assumes that it controls the entire HTML page, whereas a single-spa application does not. However, usually we can achieve equivalent behavior by just loading the vendor and app bundles into the HTML page dynamically, instead of baking them right into the HTML page. Below is a description of the known things you should do when setting up an ember-cli application with single-spa:

    First, you'll need to add single-spa-ember as a dependency to the ember project. This can be done with npm, yarn, or bower. For example:

    • npm init
    • npm install single-spa-ember or
    • bower init
    • bower install single-spa-ember --save

    Add the following options to your ember-cli-build.js file:

    /* eslint-env node */
    'use strict';

    const EmberApp = require('ember-cli/lib/broccoli/ember-app');

    module.exports = function(defaults) {
    let app = new EmberApp(defaults, {
    autoRun: false, // Set autoRun to false, because we only want the ember app to render to the DOM when single-spa tells it to.
    storeConfigInMeta: false, // We're making a single-spa application, which doesn't exclusively own the HTML file. So we don't want to have to have a `<meta>` tag for the ember environment to be initialized.
    fingerprint: {
    customHash: null, // This is optional, just will make it easier for you to have the same url every time you do an ember build.
    },
    // Add options here
    });

    // Tell ember how to use the single-spa-ember library - pick one of the following
    // if you used npm or yarn
    app.import('node_modules/single-spa-ember/amd/single-spa-ember.js', {
    using: [
    {transformation: 'amd', as: 'single-spa-ember'},
    ],
    });

    // **or** if you used bower
    app.import('bower_components/single-spa-ember/amd/single-spa-ember.js', {
    using: [
    {transformation: 'amd', as: 'single-spa-ember'},
    ],
    });


    return app.toTree();
    };

    In your single-spa root application (which is separate from anything generated by ember cli):

    // root-application.js
    import * as singleSpa from 'single-spa';
    import {loadEmberApp} from 'single-spa-ember';

    singleSpa.registerApplication('ember-app', loadingFunction, activityFunction);

    function activityFunction(location) {
    // Only render the ember app when the url hash starts with ember
    return location.hash.startsWith('ember');
    }

    // single-spa-ember helps us load the script tags and give the ember app module to single-spa.
    function loadingFunction() {
    const appName = 'ember-app';
    const appUrl = '/dist/ember-app/assets/ember-app.js';
    const vendorUrl = '/dist/ember-app/assets/vendor.js'; // Optional if you have one vendor bundle used for many different ember apps
    return loadEmberApp(appName, appUrl, vendorUrl);
    }

    In your app.js file (that is generated by ember cli)

    // app.js (the ember application)
    import Ember from 'ember';
    import Resolver from './resolver';
    import loadInitializers from 'ember-load-initializers';
    import config from './config/environment';
    import singleSpaEmber from 'single-spa-ember';

    // This part is generated by the ember cli
    const App = Ember.Application.extend({
    modulePrefix: config.modulePrefix,
    podModulePrefix: config.podModulePrefix,
    Resolver
    });

    loadInitializers(App, config.modulePrefix);

    export default App;

    // This is the single-spa part
    const emberLifecycles = singleSpaEmber({
    App, // required
    appName: 'ember-app', // required
    createOpts: { // optional
    rootElement: '#ember-app',
    },
    })

    // Single-spa lifecycles.
    export const bootstrap = emberLifecycles.bootstrap;
    export const mount = emberLifecycles.mount;
    export const unmount = emberLifecycles.unmount;
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-html-web-components/index.html b/docs/5.x/ecosystem-html-web-components/index.html index 13746fdcd..eb9a91680 100644 --- a/docs/5.x/ecosystem-html-web-components/index.html +++ b/docs/5.x/ecosystem-html-web-components/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ single-spa applications or parcels.

    Installation

    npm install --save single-spa-html

    # or
    yarn add single-spa-html

    Alternatively, you can use single-spa-html from a CDN as a global variable:

    <script src="https://cdn.jsdelivr.net/npm/single-spa-html"></script>

    Note that you might want to lock down the package to a specific version. See here for how to do that.

    Usage

    Via npm

    import singleSpaHtml from 'single-spa-html';

    const htmlLifecycles = singleSpaHtml({
    template: '<x-my-web-component></x-my-web-component>',
    })

    export const bootstrap = htmlLifecycles.bootstrap;
    export const mount = htmlLifecycles.mount;
    export const unmount = htmlLifecycles.unmount;

    Via cdn

    Example usage when installed via CDN:

    const webComponentApp = window.singleSpaHtml.default({
    template: props => `<x-my-web-component attr="${props.attr}"></x-my-web-component>`,
    })

    singleSpa.registerApplication({
    name: 'name',
    app: webComponentApp,
    activeWhen: () => true
    })

    API / Options

    single-spa-html is called with an object that has the following properties:

    • template (required): An HTML string or a function that returns a string or promise that resolves a string. The function will be called with the single-spa custom props. The returned string is injected into the DOM during the single-spa mount lifecycle.
    • domElementGetter (optional): A function that is given the single-spa props and returns the dom element container into which the HTML will be injected. If omitted, a default implementation is provided that wraps the template in a <div> that is appended to document.body.
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-inferno/index.html b/docs/5.x/ecosystem-inferno/index.html index 11573630b..9b20d946e 100644 --- a/docs/5.x/ecosystem-inferno/index.html +++ b/docs/5.x/ecosystem-inferno/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    single-spa-inferno

    single-spa-inferno is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Inferno. Check out the single-spa-inferno github.

    Quickstart

    First, in the application, run npm install --save single-spa-inferno. Then, add the following to your application's entry file.

    import Inferno from 'inferno';
    import rootComponent from './path-to-root-component.js';
    import singleSpaInferno from 'single-spa-inferno';

    const infernoLifecycles = singleSpaInferno({
    Inferno,
    createElement,
    rootComponent,
    domElementGetter: () => document.getElementById('main-content'),
    });

    export const bootstrap = infernoLifecyles.bootstrap;
    export const mount = infernoLifecyles.mount;
    export const unmount = infernoLifecyles.unmount;

    Options

    All options are passed to single-spa-inferno via the opts parameter when calling singleSpaInferno(opts). The following options are available:

    • inferno: (required) The main Inferno object, which is generally either exposed onto the window or is available via require('inferno') or import Inferno from 'inferno'.
    • createElement: (required) The default export from Inferno's inferno-create-element package.
    • rootComponent: (required) The top level Inferno component which will be rendered.
    • domElementGetter: (required) A function that takes in no arguments and returns a DOMElement. This dom element is where the Inferno application will be bootstrapped, mounted, and unmounted.
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-leaked-globals/index.html b/docs/5.x/ecosystem-leaked-globals/index.html index d67db922f..ea64e3bee 100644 --- a/docs/5.x/ecosystem-leaked-globals/index.html +++ b/docs/5.x/ecosystem-leaked-globals/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -41,7 +41,7 @@ export an array for your lifecycle functions instead of exporting just a function.

    import singleSpaLeakedGlobals from 'single-spa-leaked-globals';

    // Use single-spa-angularjs, single-spa-backbone, etc to get your framework specific lifecycles
    const frameworkLifecycles = ...

    const leakedGlobalsLifecycles = singleSpaLeakedGlobals({
    globalVariableNames: ['$', 'jQuery', '_'],
    })

    export const bootstrap = [
    leakedGlobalsLifecycles.bootstrap,
    frameworkLifecycles.bootstrap,
    ]

    export const mount = [
    // Make sure leaked globals lifecycles' mount function is **before** other lifecycles' mount
    // This is so the global vars are available when the framework mounts
    leakedGlobalsLifecycles.mount,
    frameworkLifecycles.mount,
    ]

    export const unmount = [
    leakedGlobalsLifecycles.unmount,
    // Make sure leaked globals lifecycles' unmount function is **after** other lifecycles' unmount
    // This is so the global vars are still available during the framework unmount lifecycle function.
    frameworkLifecycles.unmount,
    ]

    If you're using single-spa-leaked-globals as a global variable itself:

    const leakedGlobalsLifecycles = window.singleSpaLeakedGlobals.default({
    globalVariableNames: ['_'],
    })

    API / Options

    single-spa-leaked-globals is called with an object with the following properties:

    • globalVariableNames (required): An array of strings. Each string is the name of a global variable that should be removed when the application is unmounted, and added back when the application is mounted.
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-preact/index.html b/docs/5.x/ecosystem-preact/index.html index 7e69ece08..39190d228 100644 --- a/docs/5.x/ecosystem-preact/index.html +++ b/docs/5.x/ecosystem-preact/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    single-spa-preact

    single-spa-preact is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Preact. Check out the single-spa-preact github.

    Installation

    npm install --save preact

    Quickstart

    In your project's entry file, add the following:

    import preact from 'preact';
    import rootComponent from './path-to-root-component.js';
    import singleSpaPreact from 'single-spa-preact';

    const preactLifecycles = singleSpaPreact({
    preact,
    rootComponent,
    domElementGetter: () => document.getElementById('main-content'),
    });

    export const bootstrap = preactLifecycles.bootstrap;
    export const mount = preactLifecycles.mount;
    export const unmount = preactLifecycles.unmount;

    Options

    All options are passed to single-spa-preact via the opts parameter when calling singleSpaPreact(opts). The following options are available:

    • preact: (required) The main Preact object, which is generally either exposed onto the window or is available via require('preact') or import preact from 'preact'.
    • rootComponent: (required) The top level preact component which will be rendered
    • domElementGetter: (optional) A function that is given the single-spa props and returns a DOMElement. This dom element is where the Preact application will be bootstrapped, mounted, and unmounted. If omitted, a div will be created and appended to the body.
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-react/index.html b/docs/5.x/ecosystem-react/index.html index dc9e7f8d7..73992a22d 100644 --- a/docs/5.x/ecosystem-react/index.html +++ b/docs/5.x/ecosystem-react/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -33,7 +33,7 @@ single-spa's root mountParcel function, so that single-spa can keep track of the parent-child relationship and automatically unmount the application's parcels when the application unmounts. Note that if the <Parcel> component is being rendered by a single-spa application that uses single-spa-react, it is unnecessary to pass in the prop, since <Parcel> can get the prop from SingleSpaContext
  • handleError (optional): A function that will be called with errors thrown by the parcel. If not provided, errors will be thrown on the window, by default.
  • parcelDidMount (optional): A function that will be called when the parcel finishes loading and mounting.
  • Examples

    // Use this import path in environments that support package.json exports
    // See https://nodejs.org/dist/latest-v14.x/docs/api/packages.html#packages_package_entry_points
    // and see https://github.com/single-spa/single-spa-react/releases/tag/v3.0.0
    // Use this in Webpack 5 and recent versions of Node
    import Parcel from 'single-spa-react/parcel'

    // Use this import path in environments that don't support package.json exports
    // See https://nodejs.org/dist/latest-v14.x/docs/api/packages.html#packages_package_entry_points
    // and see https://github.com/single-spa/single-spa-react/releases/tag/v3.0.0
    // Use this in Webpack 4 and older versions of Node
    import Parcel from 'single-spa-react/lib/esm/parcel'


    import * as parcelConfig from './my-parcel.js'

    // config is required. The parcel will be mounted inside of the
    // of a div inside of the react component tree
    <Parcel
    config={parcelConfig}

    wrapWith="div"
    handleError={err => console.error(err)}

    customProp1="customPropValue2"
    customProp2="customPropValue2"
    />

    // If you pass in an appendTo prop, the parcel will be mounted there instead of
    // to a dom node inside of the current react component tree
    <Parcel
    config={parcelConfig}
    wrapWith="div"
    appendTo={document.body}
    />

    // You can also pass in a "loading function" as the config.
    // The loading function must return a promise that resolves with the parcel config.
    // The parcel will be mounted once the promise resolves.
    <Parcel
    config={() => import('./my-parcel.js')}
    wrapWith="div"
    />

    // If you are rendering the Parcel component from a single-spa application, you do not need to pass a mountParcel prop.
    // But if you have a separate react component tree that is not rendered by single-spa-react, you **must** pass in a mountParcel prop
    // In general, it is preferred to use an application's mountParcel function instead of the single-spa's root mountParcel function,
    // so that single-spa can keep track of the parent-child relationship and automatically unmount the application's parcels when the application
    // unmounts
    <Parcel
    mountParcel={singleSpa.mountParcel}
    config={parcelConfig}
    wrapWith="div"
    />

    // Add styles to wrapWith element.
    <Parcel
    config={parcelConfig}
    wrapWith="div"
    wrapStyle={{ background: 'black' }}
    />

    // Add classNames to wrapWith element.
    <Parcel
    config={parcelConfig}
    wrapWith="div"
    wrapClassName="wrapper"
    />

    Create React App

    See FAQ for CRA.

    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-riot/index.html b/docs/5.x/ecosystem-riot/index.html index 15a77a2ab..7d46e54be 100644 --- a/docs/5.x/ecosystem-riot/index.html +++ b/docs/5.x/ecosystem-riot/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    single-spa-riot

    single-spa-riot is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with riot. Check out the single-spa-riot github.

    NPM

    Build Status

    minified

    Installation

    npm install --save single-spa-riot

    Usage

    import * as Riot from 'riot';
    import singleSpaRiot from 'single-spa-riot';
    import App from './App.riot'

    const riotLifecycles = singleSpaRiot({
    rootComponent: Riot.component(App),
    domElementGetter: () => document.getElementById('#app')
    });

    export const bootstrap = riotLifecycles.bootstrap;

    export const mount = riotLifecycles.mount;

    export const unmount = riotLifecycles.unmount;

    Options

    All options are passed to single-spa-riot via the opts parameter when calling singleSpaRiot(opts). The following options are available:

    • domElementGetter: (required) the callback to get root component mount element.
    • rootComponent: (optional and replaces appOptions.loadRootComponent) the root riot component.
    • loadRootComponent: (optional and replaces appOptions.rootComponent) A promise that resolves with your root component. This is useful for lazy loading.
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-snowpack/index.html b/docs/5.x/ecosystem-snowpack/index.html index b6167434b..9491edfd9 100644 --- a/docs/5.x/ecosystem-snowpack/index.html +++ b/docs/5.x/ecosystem-snowpack/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Snowpack

    Snowpack is a tool for both local development and the building of applications. It uses in-browser ES modules during development, and then bundles with webpack (or other build tools) for production.

    Example repo

    https://github.com/joeldenning/snowpack-single-spa-example

    Overview

    Snowpack uses ES modules in local development, but not in production. This works well with single-spa, which encourages using in-browser modules as the interface for each microfrontend. To use snowpack with single-spa, you must export the single-spa lifecycle functions from your Snowpack project's index.js file and then make a few modifications to the snowpack.config.js file.

    Configuration

    Modify the index.js file to not mount your app immediately, but rather to export the single-spa lifecycles. If using Vue, for example, see https://single-spa.js.org/docs/ecosystem-vue#usage.

    The following Snowpack config can be used as the basis for a single-spa + Snowpack setup. It requires installing systemjs-webpack-interop and @snowpack/plugin-webpack:

    npm install --save-dev systemjs-webpack-interop @snowpack/plugin-webpack

    yarn add --dev systemjs-webpack-interop @snowpack/plugin-webpack

    pnpm install --save-dev systemjs-webpack-interop @snowpack/plugin-webpack
    const { merge } = require("webpack-merge");
    const SystemJSPublicPathWebpackPlugin = require("systemjs-webpack-interop/SystemJSPublicPathWebpackPlugin");

    /** @type {import("snowpack").SnowpackUserConfig } */
    module.exports = {
    mount: {
    /* ... */
    },
    plugins: [
    [
    "@snowpack/plugin-webpack",
    {
    extendConfig(config) {
    delete config.optimization.runtimeChunk;
    delete config.optimization.splitChunks;

    return merge(config, {
    mode: "development",
    module: {
    rules: [
    // This rule is necessary in webpack 4, but breaks things in webpack 5
    // At the time of writing this documentation, @snowpack/plugin-webpack uses webpack 4.
    {
    parser: {
    system: false,
    },
    },
    ],
    },
    output: {
    libraryTarget: "system",
    },
    plugins: [
    new SystemJSPublicPathWebpackPlugin({
    systemjsModuleName: "snowpack-test",
    rootDirectoryLevel: 2,
    }),
    ],
    });
    },
    },
    ],
    ],
    routes: [
    /* Enable an SPA Fallback in development: */
    // {"match": "routes", "src": ".*", "dest": "/index.html"},
    ],
    optimize: {
    /* Example: Bundle your final build: */
    // "bundle": true,
    },
    packageOptions: {
    /* ... */
    },
    devOptions: {},
    buildOptions: {
    baseUrl: "http://localhost:8080/",
    },
    };

    Local development

    Snowpack works well with development via import map overrides. You should use http://localhost:8080/index.js as the URL for your import map override.

    caution

    Static Assets currently do not load from the correct URL in development, pending a PR to Snowpack: https://github.com/snowpackjs/snowpack/pull/2407. However, static assets do load from the correct URL in production, due to systemjs-webpack-interop.

    Native Modules vs SystemJS

    single-spa works well with native modules, systemjs, or even both. With Snowpack + single-spa, a general recommendation is to use native modules during local development, but SystemJS in production (since browser support for Import Maps is still pending). Doing this is nice because it matches Snowpack's development workflow; however, mixing native and systemjs modules also can have some caveats:

    • The browser and SystemJS maintain separate module registries. This means that you can't share imports between SystemJS and native modules. So if you are doing an import map override for a Snowpack application on a page that also uses SystemJS, you may end up with multiple instances of Vue or React (and other shared libraries), which is different than how things will work in production. This is generally okay, except for situations where the Vue instance is modified via Vue.use().
    • This PR to SystemJS shows how you can populate native modules into the SystemJS registry, allowing for one-way sharing of modules between the two registries. The PR was closed due to some edge cases, but it generally works. Even though the PR is closed, you can paste the ESM extra into your root config and it will work. If you have interest in driving forward better SystemJS + ESM compatibility, comment on Github or Slack with your interest.
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-svelte/index.html b/docs/5.x/ecosystem-svelte/index.html index 9de920ab9..a95dda5be 100644 --- a/docs/5.x/ecosystem-svelte/index.html +++ b/docs/5.x/ecosystem-svelte/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    single-spa-svelte

    single-spa-svelte is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with svelte. Check out the single-spa-svelte github.

    Quickstart

    First, in the single-spa application, run npm install --save single-spa-svelte. Then, create an entry file with the following.

    import singleSpaSvelte from 'single-spa-svelte';
    import myRootSvelteComponent from 'my-root-svelte-component.js';

    const svelteLifecycles = singleSpaSvelte({
    component: myRootSvelteComponent,
    domElementGetter: () => document.getElementById('svelte-app'),
    props: { someData: 'data' }
    });

    export const bootstrap = svelteLifecycles.bootstrap;
    export const mount = svelteLifecycles.mount;
    export const unmount = svelteLifecycles.unmount;

    Options

    All options are passed to single-spa-svelte via the opts parameter when calling singleSpaSvelte(opts). The following options are available:

    • component: (required) The root component that will be rendered. This component should be compiled by svelte and not an iife.
    • domElementGetter: (optional) A function which will return a dom element. The root component will be mounted in this element. If not provided, a default dom element will be provided.

    Svelte-specific options

    • anchor: (optional) A child of the dom element identified by domElementGetter to render the component immediately before
    • hydrate: (optional) See the svelte Creating a component documentation
    • intro: (optional) If true, will play transitions on initial render, rather than waiting for subsequent state changes
    • props: (optional) An object of properties to supply to the component

    single-spa props

    All single-spa props are passed to the svelte component as props. The props provided to singleSpaSvelte({props: {...}}) are merged with the single-spa props.

    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-vite/index.html b/docs/5.x/ecosystem-vite/index.html index bf6fe2317..b1a3c7536 100644 --- a/docs/5.x/ecosystem-vite/index.html +++ b/docs/5.x/ecosystem-vite/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Vite

    Vite is a tool for both local development and the building of applications. It was created within the Vue ecosystem, but can be used with other UI frameworks, too.

    Example repo

    https://github.com/joeldenning/vite-single-spa-example

    Overview

    By default, Vite uses ES modules in local development, but not in production. This works well with single-spa, which encourages using in-browser modules as the interface for each microfrontend. To use vite with single-spa, you must export the single-spa lifecycle functions from your Vite's main.js file and then make a few modifications to the vite.config.js file.

    Configuration

    Modify the src/main.js file to not mount your app immediately, but rather to export the single-spa lifecycles. For Vue apps, see https://single-spa.js.org/docs/ecosystem-vue#usage.

    The following Vite config can be used as the basis for a single-spa + Vite setup:

    import vue from '@vitejs/plugin-vue'

    export default {
    rollupOptions: {
    input: 'src/main.js',
    format: 'system',
    preserveEntrySignatures: true
    },
    base: 'http://localhost:3000',
    plugins: [vue({
    template: {
    transformAssetUrls: {
    base: '/src'
    }
    }
    })],
    }

    Local development

    Vite works well with development via import map overrides. You should use http://localhost:3000/src/main.js as the URL for your import map override. It is important to note, however, that assets such as images and fonts won't load. The import map is only used to load JavaScript, not media files. The import map does not affect asset URL's. Asset URL's are affected by Vite's base configuration property, and Vite doesn't respect full URL's in said property while in serve mode (npm run dev). While in serve mode, a base with a full URL is stripped down to its path. Therefore, the asset URL's don't really get the correct host URL. The author of vite-plugin-single-spa has opened a discussion in Vite's GitHub that you can opt to support by upvoting it.

    Native Modules vs SystemJS

    single-spa works well with native modules, systemjs, or even both. With Vite + single-spa, a general recommendation is to use native modules during local development, but SystemJS in production (since browser support for Import Maps is still pending). Doing this is nice because it matches Vite's development workflow; however, mixing native and systemjs modules also can have some caveats:

    • The browser and SystemJS maintain separate module registries. This means that you can't share imports between SystemJS and native modules. So if you are doing an import map override for a Vite application on a page that also uses SystemJS, you may end up with multiple instances of Vue (and other shared libraries), which is different than how things will work in production. This is generally okay, except for situations where the Vue instance is modified via Vue.use().
    • This PR to SystemJS shows how you can populate native modules into the SystemJS registry, allowing for one-way sharing of modules between the two registries. The PR was closed due to some edge cases, but it generally works. Even though the PR is closed, you can paste the ESM extra into your root config and it will work. If you have interest in driving forward better SystemJS + ESM compatibility, comment on Github or Slack with your interest.

    vite-plugin-single-spa

    This is a new entry that is currently in the early stages of development, but shows significant progress (view in GitHub). It claims to be able to convert out-of-the-box Vite projects (regardless of the framework) into single-spa micro-frontend projects and even root config projects. While the single-spa team discourages the use of UI frameworks in root configs, it is indeed an alternative that may interest people.

    To convert a Vite project to a root config project, all that is needed is install vite-plugin-single-spa, and then use it in vite.config.ts. This is a Vite + Vue example:

    import vitePluginSingleSpa from 'vite-plugin-single-spa';

    // https://vitejs.dev/config/
    export default defineConfig({
    plugins: [vue(), vitePluginSingleSpa({
    type: 'root'
    }
    })]
    });

    To convert a Vite project to a micro-frontend project, a similarly minimalistic configuration is needed, plus a file that exports the single-spa lifecycle functions.

    // vite.config.ts for a Vite + React project

    import react from '@vitejs/plugin-react'
    import vitePluginSingleSpa from 'vite-plugin-single-spa';

    // https://vitejs.dev/config/
    export default defineConfig({
    plugins: [react(), vitePluginSingleSpa({
    serverPort: 4101,
    spaEntryPoint: 'src/spa.tsx'
    })],
    });
    // src/spa.tsx

    import React from 'react';
    import ReactDOMClient from 'react-dom/client';
    // @ts-ignore
    import singleSpaReact from 'single-spa-react';
    import App from './App';
    import { cssLifecycle } from 'vite-plugin-single-spa/ex';

    const lc = singleSpaReact({
    React,
    ReactDOMClient,
    rootComponent: App,
    errorBoundary(err: any, _info: any, _props: any) {
    return <div>Error: {err}</div>
    }
    });

    export const bootstrap = [cssLifecycle.bootstrap, lc.bootstrap];
    export const mount = [cssLifecycle.mount, lc.mount];
    export const unmount = [cssLifecycle.unmount, lc.unmount];

    Main Features

    • Supports stock Vite projects, regardless of framework.
    • Micro-frontend projects behave dually while in serve mode: The micro-frontend can be previewed as a standalone web application with its server URL, or it can be served as a single-spa micro-frontend.
    • As seen in the example above, it provides an extra module that automatically mounts and unmounts the CSS referenced by the lifecycle-exporting module (src/spa.tsx in the example). COMING SOON
    • Automatically picks up import maps from src/importMap.dev.json and src/importMap.json.
    • Automatically adds the import-map-overrides NPM package, user interface included.

    IMPORTANT: The author of this plug-in does not believe in creating dedicated root config projects. Furthermore, this package will, by default, create import maps for native modules. We at single-spa recommend SystemJS modules. Yes, single-spa is perfectly capable of working with native modules as well.

    The opinions of the author of this plug-in in no way represent those of single-spa, and it is an independent work. We present it here as one more option in the Vite ecosystem.

    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem-vue/index.html b/docs/5.x/ecosystem-vue/index.html index 61c4d0c24..2b57f52d4 100644 --- a/docs/5.x/ecosystem-vue/index.html +++ b/docs/5.x/ecosystem-vue/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -28,7 +28,7 @@ checkout coexisting-vue-microfrontends' index.html file.

    Sharing a single instance of Vue and other common libraries is highly recommended. See the recommended setup for single-spa for more details on why.

    Shared deps with Vue CLI

    // vue.config.js
    module.exports = {
    chainWebpack: config => {
    config.externals(['vue', 'vue-router']);
    },
    };

    Shared deps without Vue CLI

    // webpack.config.js
    module.exports = {
    externals: ['vue', 'vue-router'],
    };

    Options

    All options are passed to single-spa-vue via the opts parameter when calling singleSpaVue(opts). The following options are available:

    • Vue: (required) The main Vue object, which is generally either exposed onto the window or is available via require('vue') import Vue from 'vue'.
    • appOptions: (required) An object or async function which will be used to instantiate your Vue.js application. appOptions will pass directly through to new Vue(appOptions). Note that if you do not provide an el to appOptions, that a div will be created and appended to the DOM as a default container for your Vue application. When appOptions is an async function, it receives the single-spa props as an argument (as of single-spa-vue@2.4.0).
    • loadRootComponent: (optional and replaces appOptions.render) A promise that resolves with your root component. This is useful for lazy loading.
    • handleInstance: (optional) A method can be used to handle Vue instance. Vue 3 brings new instance API, and you can access the app instance from this, like handleInstance: (app, props) => app.use(router). For Vue 2 users, a Vue instance can be accessed. The handleInstance(app, props) function receives the instance as its first argument, and single-spa props as its second argument. If handleInstance returns a promise, single-spa-vue will wait to resolve the app / parcel's mount lifecycle until the handleInstance promise resolves.
    • replaceMode: (optional, defaults to false) A boolean that determines whether your root Vue component will entirely replace the container element it's mounted to. The Vue library always replaces, so to implement replaceMode: false a temporary <div class="single-spa-container"> element is created inside of the container, so that Vue replaces that element rather than the container. Introduced in single-spa-vue@2.3.0.

    To configure which dom element the single-spa application is mounted to, use appOptions.el:

    const vueLifecycles = singleSpaVue({
    Vue,
    appOptions: {
    render: h => h(App),
    el: '#a-special-container',
    },
    });

    To configure options asynchronously return a promise from appOptions function:

    const vueLifecycles = singleSpaVue({
    Vue,
    async appOptions() {
    return {
    router: await routerFactory(),
    render: h => h(App)
    }
    },
    });

    Custom Props

    single-spa custom props are available in the render() function in your main file. They can be passed as custom props to your App component.

    const vueLifecycles = singleSpaVue({
    Vue,
    appOptions: {
    render(h) {
    return h(App, {
    props: {
    // single-spa props are available on the "this" object. Forward them to your component as needed.
    // https://single-spa.js.org/docs/building-applications#lifecycle-props
    name: this.name,
    mountParcel: this.mountParcel,
    singleSpa: this.singleSpa,
    },
    });
    },
    },
    });

    Parcels

    Creating a parcel

    A parcel config is an object that represents a component implemented in Vue, React, Angular, or any other framework.

    To create a VueJS single-spa parcel config object, simply omit the el option from your appOptions, since the dom element will be specified by the user of the Parcel. Every other option should be provided exactly the same as in the example above.

    const parcelConfig = singleSpaVue({...});

    Rendering a parcel

    To render a parcel config object in Vue, you can use single-spa-vue's Parcel component:

    <template>
    <Parcel
    v-on:parcelMounted="parcelMounted()"
    v-on:parcelUpdated="parcelUpdated()"
    :config="parcelConfig"
    :mountParcel="mountParcel"
    :wrapWith="wrapWith"
    :wrapClass="wrapClass"
    :wrapStyle="wrapStyle"
    :parcelProps="getParcelProps()"
    />
    </template>

    <script>
    // For old versions of webpack
    import Parcel from 'single-spa-vue/dist/esm/parcel'
    // For new versions of webpack
    import Parcel from 'single-spa-vue/parcel'

    import { mountRootParcel } from 'single-spa'

    export default {
    components: {
    Parcel
    },
    data() {
    return {
    /*
    parcelConfig (object, required)

    The parcelConfig is an object, or a promise that resolves with a parcel config object.
    The object can originate from within the current project, or from a different
    microfrontend via cross microfrontend imports. It can represent a Vue component,
    or a React / Angular component.
    https://single-spa.js.org/docs/recommended-setup#cross-microfrontend-imports

    Vanilla js object:
    parcelConfig: {
    async mount(props) {},
    async unmount(props) {}
    }

    // React component
    parcelConfig: singleSpaReact({...})

    // cross microfrontend import is shown below
    */
    parcelConfig: System.import('@org/other-microfrontend').then(ns => ns.Widget),


    /*
    mountParcel (function, required)

    The mountParcel function can be either the current Vue application's mountParcel prop or
    the globally available mountRootParcel function. More info at
    http://localhost:3000/docs/parcels-api#mountparcel
    */
    mountParcel: mountRootParcel,

    /*
    wrapWith (string, optional)

    The wrapWith string determines what kind of dom element will be provided to the parcel.
    Defaults to 'div'
    */
    wrapWith: 'div'

    /*
    wrapClass (string, optional)

    The wrapClass string is applied to as the CSS class for the dom element that is provided to the parcel
    */
    wrapClass: "bg-red"

    /*
    wrapStyle (object, optional)

    The wrapStyle object is applied to the dom element container for the parcel as CSS styles
    */
    wrapStyle: {
    outline: '1px solid red'
    },
    }
    },
    methods: {
    // These are the props passed into the parcel
    getParcelProps() {
    return {
    text: `Hello world`
    }
    },
    // Parcels mount asynchronously, so this will be called once the parcel finishes mounting
    parcelMounted() {
    console.log("parcel mounted");
    },
    parcelUpdated() {
    console.log("parcel updated");
    }
    }
    }
    </script>

    Webpack Public Path

    vue-cli-plugin-single-spa sets the webpack public path via SystemJSPublicPathWebpackPlugin. By default, the public path is set to match the following output directory structure:

    dist/
    js/
    app.js
    css/
    main.css

    With this directory structure (which is the Vue CLI default), the public path should not include the js folder. This is accomplished by setting rootDirectoryLevel to be 2. If this doesn't match your directory structure or setup, you can change the rootDirectoryLevel with the following code in your vue.config.js or webpack.config.js:

    // vue.config.js
    module.exports = {
    chainWebpack(config) {
    config.plugin('SystemJSPublicPathWebpackPlugin').tap((args) => {
    args[0].rootDirectoryLevel = 1;
    return args;
    });
    }
    }
    - - + + \ No newline at end of file diff --git a/docs/5.x/ecosystem/index.html b/docs/5.x/ecosystem/index.html index 1ed8eea82..935f42034 100644 --- a/docs/5.x/ecosystem/index.html +++ b/docs/5.x/ecosystem/index.html @@ -14,15 +14,15 @@ - - + +
    Version: 5.x

    The single-spa ecosystem

    The single-spa ecosystem is quickly growing to support as many frameworks and build tools as possible.

    Help for frameworks

    There is a growing number of projects that help you bootstrap, mount, and unmount your applications that are written with popular frameworks. Feel free to contribute to this list with your own project:

    Other helpful libraries

    - - + + \ No newline at end of file diff --git a/docs/5.x/examples/index.html b/docs/5.x/examples/index.html index 2c9738fef..2805262a4 100644 --- a/docs/5.x/examples/index.html +++ b/docs/5.x/examples/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Single-spa Examples

    Core team examples

    Actively maintained

    Older examples

    Community examples

    Have your own example or starter repo? Submit a PR to add yours to this list.

    - - + + \ No newline at end of file diff --git a/docs/5.x/faq/index.html b/docs/5.x/faq/index.html index fab10df74..ec87c5889 100644 --- a/docs/5.x/faq/index.html +++ b/docs/5.x/faq/index.html @@ -14,14 +14,14 @@ - - + +
    Version: 5.x

    Frequently Asked Questions

    What does single-spa do?

    single-spa is a top level router. When a route is active, it downloads and executes the code for that route.

    The code for a route is called an "application", and each can (optionally) be in its own git repository, have its own CI process, and be separately deployed. The applications can all be written in the same framework, or they can be implemented in different frameworks.

    Yes, here is the documentation for our recommended setup.

    Should I have a parent/root app and children apps?

    No. We strongly encourage that your single-spa-config or root application does not use any JavaScript UI frameworks (React, Angular, Angularjs, Vue, etc). In our experience a plain JavaScript module is best for the single-spa-config and only the registered applications actually use UI frameworks (Angular, React, Vue, etc).

    Why? You end up creating a structure that has all the disadvantages of microservices without any of the advantages: your apps are now coupled together and you have to change multiple apps at the same time in order to make updates. Good microservices are completely independent, and this pattern breaks that.

    What is the impact to performance?

    When setup in the recommended way, your code performance and bundle size will be nearly identical to a single application that has been code-split. The major differences will be the addition of the single-spa library (and SystemJS if you chose to use it). Other differences mainly come down to the difference between one (webpack / rollup / etc.) code bundle and in-browser ES modules.

    Can I have only one version of (React, Vue, Angular, etc.) loaded?

    Yes, and it's highly recommended you do so! Using the recommended setup, you configure your import map so that your library is defined only once. Then, tell each application to not bundle that library; instead, the library will given to you at runtime in the browser. See webpack’s externals (and other bundlers have similar options) for how to do this.

    You do have the option of not excluding those libraries (for example if you want to experiment with a newer version or a different library) but be aware of the effect that will have on user's bundle sizes and application speed.

    What are import maps?

    Import maps improve the developer experience of in-browser ES modules by allowing you to write something like import React from "react" instead of needing to use an absolute or relative URL for your import statement. The same is also true of importing from other single-spa applications, e.g. import {MyButton} from "styleguide". The import-map spec is currently in the process of being accepted as a web standard and at the time of writing has been implemented in Chrome, and a polyfill for browsers >= IE11 has been implemented by SystemJS >= 3.0. Also see the recommended setup

    How can I share application state between applications?

    The primary means of communicating between applications is cross microfrontend imports. This allows you define a public interface for a microfrontend that others can use. You may expose functions, data, components, stores, or anything else from any microfrontend to be available in any other.

    We recommend that each application manage as much of its own state as possible so that your applications remain independently deployable without the risk of breaking each other. Generally, it’s better to make an API request for the data that each app needs, even if parts of it have been requested by other apps. If you've split your applications well, there will end up being very little application state that is truly shared — for example, your friends list has different data requirements than your social feed.

    The list below shows some common practices:

    1. Create a shared API utility microfrontend that caches fetch/XHR requests and their responses. All microfrontends call into the API microfrontend when making a request, so that the microfrontend can control whether to refetch the data or not.
    2. Create a shared Auth utility microfrontend that exposes a userCanAccess function for other microfrontends to use when checking permissions. The auth module may also include other exports such as the logged in user object, auth tokens, etc.
    3. Export shared state from the public interface of your microfrontend so that libraries can import it. For values that change over time, Observables (RxJS docs) can be useful. Create a ReplaySubject so that you can push new values out to all subscribers at any time.
    4. Use custom browser events to communicate. Fire them on the window in one microfrontend, and listen to the event in a different microfrontend.
    5. Use cookies, local/session storage, or other similar methods for storing and reading that state. These methods work best with things that don't change often, e.g. logged-in user info.

    Should I use frontend microservices?

    If you’ve ran into some of the headaches a monolithic repo has, then you should really consider it.

    In addition, if your organization is setup in a Spotify-type model (e.g. where there are autonomous squads that own full-stack features) then microservices on the frontend will fit very well into your setup.

    However, if you’re just starting off and have a small project or a small team, we would recommend you stick with a monolith (i.e. not microservices) until you get to the point that scaling (e.g. organizational scaling, feature scaling, etc.) is getting hard. Don’t worry, we’ll be here to help you migrate when you get there.

    Can I use more than one framework?

    Yes. However, it’s something you’ll want to consider hard because it splits your front-end organization into specialities that aren’t compatible (e.g. a React specialist may have problems working in an Angular app), and also causes more code to be shipped to your users.

    However, it is great for migrations away from an older or unwanted library, which allows you to slowly rip out the code in the old application and replace it with new code in the new library (see Google results for the strangler pattern).

    It also is a way to allow large organizations to experiment on different libraries without a strong commitment to them.

    Just be conscious of the effect it has on your users and their experience using your app.

    What is the developer experience (DX) like?

    If you're using the recommended setup for single-spa, you'll simply be able to go to your development website, add an import map that points to your locally-running code, and refresh the page.

    There's a library that you can use, or you can even just do it yourself - you'll note that the source code is pretty simple. The main takeaway is that you can have multiple import maps and the latest one wins - you add an import map that overrides the default URL for an application to point to your localhost.

    We're also looking at providing this functionality as part of the Chrome/Firefox browser extension.

    Finally, this setup also enables you to do overrides in your production environment. It obviously should be used with caution, but it does enable a powerful way of debugging problems and validating solutions.

    As a point of reference, nearly all developers we've worked with prefer the developer experience of microservices + single-spa over a monolithic setup.

    Can each single-spa application have its own git repo?

    Yes! You can even give them their own package.json, webpack config, and CI/CD process, using SystemJS to bring them all together in the browser.

    Can single-spa applications be deployed independently?

    Yes! See next section about CI/CD.

    What does the CI/CD process look like?

    In other words, how do I build and deploy a single-spa application?

    With the recommended setup, the process generally flows like this:

    1. Bundle your code and upload it to a CDN.
    2. Update your dev environment's import map to point to the new URL. In other words, your import map used to say "styleguide": "cdn.com/styleguide/v1.js" and now it should say "styleguide": "cdn.com/styleguide/v2.js"

    Some options on how to update your import map include:

    • Server render your index.html with the import map inlined. This does not mean that your DOM elements need to all be server rendered, but just the <script type="systemjs-importmap"> element. Provide an API that either updates a database table or a file local to the server.
    • Have your import map itself on a CDN, and use import-map-deployer or similar to update the import map during your CI process. This method has a small impact on performance, but is generally easier to setup if you don't have a server-rendered setup already. (You can also preload the import map file to help provide a small speed boost). See example travis.yml. Other CI tools work, too.

    Create React App

    Tutorial video: Youtube / Bilibili

    If you are starting from scratch, prefer using create-single-spa instead of create-react-app. A project using CRA must be altered before it can be used as a single-spa application. CRA provides many features out of the box, and outputs "monolith" apps by default. CRA does not support extending its configuration so we cannot provide official support for using it with single-spa. In order to continue using CRA, your options are to:

    1. Eject from CRA

    2. Use a third-party tool to extend CRA, such as:

    3. Remove react-scripts and then run create-single-spa on your project to merge create-single-spa's package.json with yours. Run yarn start and fix webpack configuration errors until it's working.

    4. Generate a project with create-single-spa and migrate the React code over.

    caution

    The single-spa webpack configs only provide basic functionality and not all the same features that CRA does. You may be required to add & configure more options, plugins, or loaders for non-JavaScript files. This may require advanced knowledge across bundlers, toolchains, plugins, etc.

    For additional reference here is a list of some changes you will need to make to your webpack config:

    1. Remove Webpack optimizations block, because they add multiple webpack chunks that don't load each other
    2. Remove html-webpack plugin
    3. Change output.libraryTarget to System, UMD, or AMD.

    Code splits

    Single spa supports code splits. There are so many ways to code split we won't be able to cover them all, but if you're using the recommended setup with webpack you'll need to do at least two things:

    1. Set the __webpack_public_path__ dynamically so webpack knows where to fetch your code splits (webpack assumes they are located at the root of the server and that isn't always true in a single-spa application). Both solutions below should be the very first import of your application in order to work.

      import { setPublicPath } from 'systemjs-webpack-interop';

      setPublicPath('name-of-module-in-import-map');
      • For SystemJS 2-5: Find a code example here
    2. Set either output.jsonpFunction or output.library to ensure that each app's webpack doesn't collide with other apps' webpack. jsonpFunction is preferred.

    For more information about webpack configuration and single-spa, see the recommended setup.

    Does single-spa require additional security considerations?

    No. single-spa does not add, deviate, or attempt to bypass any browser JavaScript security measures. The security needs of your applications are the same as if you did not use single-spa.

    Outside of that, web applications may use the following resources that have their own security considerations that you may need to become familiar with:

    How do I write tests for single-spa?

    See the documentation for unit and E2E tests.

    - - + + \ No newline at end of file diff --git a/docs/5.x/getting-started-overview/index.html b/docs/5.x/getting-started-overview/index.html index 574760345..b18e8ca63 100644 --- a/docs/5.x/getting-started-overview/index.html +++ b/docs/5.x/getting-started-overview/index.html @@ -14,15 +14,15 @@ - - + +
    Version: 5.x

    Getting Started with single-spa

    JavaScript Microfrontends

    Join the chat on Slack

    single-spa is a framework for bringing together multiple JavaScript microfrontends in a frontend application. Architecting your frontend using single-spa enables many benefits, such as:

    Demos and Examples

    See our examples page.

    Architectural Overview

    single-spa takes inspiration from modern framework component lifecycles by abstracting lifecycles for entire applications. Born out of Canopy's desire to use React + react-router instead of being forever stuck with our AngularJS + ui-router application, single-spa is now a mature library that enables frontend microservices architecture aka "microfrontends". Microfrontends enable many benefits such as independent deployments, migration and experimentation, and resilient applications.

    single-spa apps consist of the following:

    1. A single-spa root config, which renders the HTML page and the JavaScript that registers applications. Each application is registered with three things:

      • A name
      • A function to load the application's code
      • A function that determines when the application is active/inactive
    2. Applications which can be thought of as single-page applications packaged up into modules. Each application must know how to bootstrap, mount, and unmount itself from the DOM. The main difference between a traditional SPA and single-spa applications is that they must be able to coexist with other applications as they do not each have their own HTML page.

      For example, your React or Angular SPAs are applications. When active, they can listen to url routing events and put content on the DOM. When inactive, they do not listen to url routing events and are totally removed from the DOM.

    The single-spa core team has put together documentation, tools, and videos showing the currently encouraged best practices with single-spa. Check out these docs for more information.

    How hard will it be to use single-spa?

    single-spa works with ES5, ES6+, TypeScript, Webpack, SystemJS, Gulp, Grunt, Bower, ember-cli, or really any build system available. You can npm install it or even just use a <script> tag if you prefer.

    While our objective is to make using single-spa as easy as possible, we should also note that this is an advanced architecture that is different from how front-end applications are typically done. This will require changes to existing paradigms as well as understanding of underlying tools.

    If you're not starting your application from scratch, you'll have to migrate your SPA to become a single-spa application.

    single-spa works in Chrome, Firefox, Safari, Edge, and IE11 (with polyfills).

    Isn't single-spa sort of a redundant name?

    Yep.

    Documentation

    The documentation is divided into several sections:

    You can help improve the single-spa website by sending pull requests to the single-spa.js.org repository.

    Quick start

    To help beginners to single-spa get started quickly we have developed create-single-spa, a utility for generating starter code. This guide will cover creating the root-config and your first single-spa application. Let's get started.

    note

    Once you've gotten some of the basics down, refer to these other single-spa examples to see more advanced usage.

    Create a root config

    1. Invoke create-single-spa to generate a root-config by running:

      npx create-single-spa --moduleType root-config

      Follow the remaining prompts with a few things in mind:

    2. Once created, navigate into the newly created root-config folder

    3. Run the start script using your preferred package manager

    4. Navigate to http://localhost:9000/ in your browser

    5. You now have a working root-config!

    Be sure to review the comments inside the generated code as well as the information in the Welcome application even if some of the content is duplicated in this guide.

    tip

    single-spa-playground.org is an alternative guide to run an application without needing to create your own root-config.

    Create a single-spa application

    1. Invoke create-single-spa to generate a single-spa application by running:

      npx create-single-spa --moduleType app-parcel

      Follow the remaining prompts to generate a single-spa application using your framework of choice

    2. Once created, navigate into the newly created application folder

    3. Run the start script using your preferred package manager

    Add shared dependencies

    Shared dependencies are used to improve performance by sharing a module in the browser through import maps declared in the root-config. Adding these at this point is conditionally optional, depending on if the generated application expects any shared dependencies.

    For example, if using React the generated Webpack config already expects React and ReactDOM to be shared dependencies, so you must add these to the import map. Vue, Angular, and Svelte don't require shared dependencies at this time.

    "react": "https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js",
    "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"

    As your architecture matures, you may add more shared dependencies in the future so don't stress about leveraging these perfectly at first.

    Register the application

    1. Return to the root-config and add your application to the import map in src/index.ejs

      The application's package.json name field is recommended
    2. Register as a single-spa application

      if not using single-spa Layout Engine

      • Open src/root-config.js
      • Remove the code for registering @single-spa/welcome as an application
      • Uncomment the sample registerApplication code and update it with the module name of your application

      if using single-spa Layout Engine

      • Remove the existing <application name="@single-spa/welcome"></application> element
      • Add your own <application name=""></application> element using the name the module name used in the import map from the previous step

    Thats it! Your first single-spa application should now be running in your root-config.


    API

    Read more at single-spa API and application api.

    Contributing

    The main purpose of this repository is to continue to evolve single-spa, making it better and easier to use. Development of single-spa, and the single-spa ecosystem happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving single-spa.

    Code of Conduct

    single-spa has adopted a Code of Conduct that we expect project participants to adhere to. Please read the full text so that you can understand what actions will and will not be tolerated.

    Contributing Guide

    Read our contributing guide to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to single-spa.

    Who's Using This?

    See user showcase.

    Is your company or project using single-spa? Let us know by submitting a PR to this section!

    - - + + \ No newline at end of file diff --git a/docs/5.x/glossary/index.html b/docs/5.x/glossary/index.html index adeab51df..b77d75e5b 100644 --- a/docs/5.x/glossary/index.html +++ b/docs/5.x/glossary/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Glossary

    Activity Function
    Application
    Application
    Helpers
    are a library that already implements single-spa lifecycle functions for a specific framework.
    Lifecycles
    module loader
    Microservices
    registerApplication
    - - + + \ No newline at end of file diff --git a/docs/5.x/layout-api/index.html b/docs/5.x/layout-api/index.html index cfb413af8..511224ced 100644 --- a/docs/5.x/layout-api/index.html +++ b/docs/5.x/layout-api/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Layout Engine API

    The single-spa-layout library exposes several javascript functions as a public API.

    Browser

    In the browser, single-spa-layout exports the following functions as named exports.

    constructRoutes

    The constructRoutes API transforms your Layout Definition into an opaque "resolved routes" object. We call it "opaque" because the shape of the object is irrelevant, as you will only use it when calling other APIs within single-spa-layout.

    import { constructRoutes } from 'single-spa-layout';

    const htmlTemplate = document.querySelector('#single-spa-template')
    const layoutData = {
    props: {
    authToken: "78sf9d0fds89-0fysdiuf6sf8",
    loggedInUser: fetch('/api/user')
    },
    loaders: {
    mainContent: `<img src="loading.gif">`,
    // A single-spa parcel config
    topNav: singleSpaReact({...})
    }
    };

    const resolvedRoutes = constructRoutes(htmlTemplate, layoutData)

    Arguments

    • routesConfig (required): Routes config is a JSON Layout Definition, an HTMLElement, or a parse5 HTML element. If it is an HTMLElement, it must be a <single-spa-router> element or a <template> that contains a single-spa-router element.
    • layoutData (optional): Layout data is an optionally provided object that defines props and loaders for HTML Layouts. You can omit it if using a JSON Layout or if you do not need to define props or loaders in your HTML Layout. The layoutData object should have top level properties props and loaders that are each objects. Each of those objects' keys is the name of a prop or loader and its corresponding value.

    Return value

    An opaque resolvedRoutes object. It is opaque because you will only use the object when calling other single-spa-layout APIs and do not need to read or modify the resolvedRoutes.

    constructApplications

    The constructApplications API transforms your resolvedRoutes into single-spa application registration objects. These application registration objects are then used to call singleSpa.registerApplication().

    import { constructRoutes, constructApplications } from 'single-spa-layout';
    import { registerApplication } from 'single-spa';

    const resolvedRoutes = constructRoutes(...)
    const applications = constructApplications({
    routes: resolvedRoutes,
    loadApp: (app) => System.import(app.name)
    })
    applications.forEach(registerApplication);

    Arguments

    constructApplications accepts a single object as an argument, with the following properties:

    • routes (required): The opaque resolvedRoutes object returned from constructRoutes.
    • loadApp (required): A function that is given an application object and must return a loading function.

    Return value

    constructApplications returns an array of single-spa registration objects.

    constructLayoutEngine

    The constructLayoutEngine API transforms your resolvedRoutes and applications into a layoutEngine object. The layout engine is responsible for creating, destroying, and rearranging dom elements during route transitions.

    import { constructRoutes, constructApplications, constructLayoutEngine } from 'single-spa-layout';
    import { registerApplication, start } from 'single-spa';

    const resolvedRoutes = constructRoutes(...);
    const applications = constructApplications(...);
    const layoutEngine = constructLayoutEngine({routes: resolvedRoutes, applications: applications});

    layoutEngine.isActive(); // true
    layoutEngine.deactivate();
    layoutEngine.activate();

    applications.forEach(registerApplication);
    start();

    Arguments

    constructLayoutEngine accepts a single object as an argument, with the following properties:

    • routes (required): The opaque resolvedRoutes object returned from constructRoutes.
    • applications (required): The array of application registration objects returned from constructApplications.
    • active (optional): A boolean that indicates whether the layout engine should start out active or not. Defaults to true.

    Return Value

    A layoutEngine object, with the following properties:

    • isActive: a function that accepts no arguments and returns a boolean indicating whether the layout engine is active or not. When active, the layout engine will change the DOM during route transitions.

    • activate: a function that accepts no arguments and returns undefined. Calling this function activates the layout engine, which includes setting up routing event listeners so that the layout engine can change the DOM during route transitions.

    • deactivate: a function that accepts no arguments and returns undefined. Calling this function deactivates the layout engine, which includes tearing down all routing event listeners so that the layout engine no longer changes the DOM during route transitions.

    matchRoute

    The matchRoute API primarily exists for server rendering. It returns a filtered resolvedRoutes object that contains only the routes that match a particular string path.

    import { constructRoutes, matchRoute } from 'single-spa-layout';

    const resolvedRoutes = constructRoutes(...);

    const settingsRoutes = matchRoute(resolvedRoutes, "/settings")
    const dashboardRoutes = matchRoute(resolvedRoutes, "/dashboard")

    Arguments

    • routes (required): The opaque resolvedRoutes object returned from constructRoutes.
    • path (required): A string path representing the URL fragment to match the routes with. Note that the path is not a full URL - it only is the pathname part of a browser's URL. In server rendering contexts, this is often available as req.url.

    Return Value

    An opaque resolvedRoutes object. It is opaque because you will only use the object when calling other single-spa-layout APIs and do not need to read or modify the resolvedRoutes.

    Server

    In NodeJS, single-spa-layout exports the following functions as named exports. Note that the code is published in ESM and therefore won't work in old versions of Node. Additionally, single-spa-layout uses package entry points, which are only supported in newer versions of Node.

    // Works in newer versions of NodeJS
    import 'single-spa-layout';

    // Works in older versions of NodeJS
    import 'single-spa-layout/dist/esm/single-spa-layout-server.min.js';

    constructServerLayout

    The constructServerLayout api parses an HTML file and prepares it for rendering. This should be done once when the NodeJS server boots up, so the same serverLayout can be reused for all incoming HTTP requests.

    import { constructServerLayout } from 'single-spa-layout/server';

    const serverLayout = constructServerLayout({
    // filepath is resolved relative to the cwd (current working directory)
    // of the NodeJS process.
    filePath: "server/views/index.html"
    })

    // Alternatively, provide the html as a string
    const serverLayout = constructServerLayout({
    html: `
    <html>
    <head>
    <single-spa-router>
    <application name="nav"></application>
    </single-spa-router>
    </head>
    </html>
    `
    })

    Arguments

    constructServerLayout accepts a single object argument, with the following properties:

    • filePath (optional): A string file path to the HTML template file. Relative paths are resolved relative to process.cwd(). If filePath is omitted, html must be provided.
    • html (optional): An HTML string containing the HTML template. If html is omitted, filePath must be provided.

    Return Value

    constructServerLayout returns an opaque ServerLayout object. This object is then provided to sendLayoutHTTPResponse.

    sendLayoutHTTPResponse

    The sendLayoutHTTPResponse api sends HTTP headers and HTML content to the browser. It streams a full HTML file to the browser, so that the browser shows content as soon as it is available, instead of waiting for the entire HTML document. This is done by providing a ServerResponse object, or res to sendLayoutHTTPResponse .

    import { constructServerLayout, sendLayoutHTTPResponse } from 'single-spa-layout';
    import http from 'http';

    const serverLayout = constructServerLayout({...})

    http.createServer((req, res) => {
    sendLayoutHTTPResponse({
    res,
    serverLayout,
    urlPath: req.path,
    nonce: "yourNonceHere",
    async renderApplication({ appName, propsPromise }) {
    return {
    assets: `<link rel="stylesheet" href="/my-styles.css">`,
    content: `<button>${appName} app</button>`
    }
    },
    async retrieveApplicationHeaders({ appName, propsPromise }) {
    return {
    'x-custom-header': 'value'
    }
    },
    async renderFragment(fragmentName) {
    return `<script type="systemjs-importmap">{"imports": {}}</script>`;
    },
    async retrieveProp(propName) {
    return "prop value";
    },
    assembleFinalHeaders(allHeaders) {
    allHeaders.forEach(({appProps, appHeaders}) => {
    })

    return {}
    }
    })
    })

    Arguments

    sendLayoutHTTPResponse accepts one object argument, with the following properties:

    • res (required): A ServerResponse object. Express res objects (and likely other framework-specific objects) are supported.
    • serverLayout (required): The opaque server layout object returned from constructServerLayout.
    • urlPath (required): A string url path that will be used as the current route. Example: /settings
    • assembleFinalHeaders (required): A function that is passed all application headers and returns the final HTTP headers sent to the browser. The application headers are collected from the retrieveApplicationHeaders function into an array of AppHeaders objects. Each AppHeaders object has an appName and appHeaders object, where the appName is a string and the appHeaders is a headers object. assembleFinalHeaders must return a headers object.
    • renderApplication (optional): A function that is given information about a single-spa application and should return the HTML content (and, optionally, the assets) for that application. This function is required if a single-spa application matches the current route. The argument passed to the renderApplication function is an object with an appName string and a propsPromise promise. The propsPromise resolves with the props for the application. The function can return an object, string, Readable stream, or a Promise. Returned objects must be of format type ApplicationRenderResult = { assets: Readable | Promise<Readable> | string | Promise<string>, content: Readable | Promise<Readable> | string | Promise<string> }. Returned promises must resolve with an ApplicationRenderResult object, string or Readable stream. The assets returned from renderApplication are rendered into the <assets> element in your layout definition.
    • retrieveApplicationHeaders (optional): A function that is given information about a single-spa application and should return the HTTP response headers for that application. This function is required if a single-spa application matches the current route. The argument passed to the retrieveApplicationHeaders function is an object with an appName string and a propsPromise promise. The propsPromise resolves with the props for the application. The function can a headers object or a Promise that resolves with a headers object.
    • renderFragment (optional): A function that is given a fragment name and returns the HTML content for that fragment. This corresponds to <fragment> elements in the layout definition, and is required if the the layout definition contains a <fragment> element. The renderFragment function can return a string, Readable stream, or a Promise. Returned promises must resolve with a string or Readable stream.
    • retrieveProp (optional): A function that is given a propName and returns the prop's value. This function is required if any rendered applications have props. retrieveProp can return a value, or a promise that resolves with a value.
    • nonce (optional): A string nonce that is added to the <script> sent with the singleSpaLayoutData global variable.

    Return Value

    A promise that resolves when headers (but not necessarily HTTP response body) are sent.

    - - + + \ No newline at end of file diff --git a/docs/5.x/layout-definition/index.html b/docs/5.x/layout-definition/index.html index 79b93829c..1c42cba9b 100644 --- a/docs/5.x/layout-definition/index.html +++ b/docs/5.x/layout-definition/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Layout Definition

    A layout is a combination of HTMLElements, routes, and single-spa applications. Layout is defined statically in your root config to handle your top level routes and dom elements. Single-spa-layout should not be used outside of the root config; instead, a UI framework (React, Angular, Vue) should handle layouts within the applications.

    You may define layouts as either HTML templates or JSON objects. Defining in JSON is supported for organizations who prefer storing their layout definitions in a database instead of code. Both HTML and JSON layouts have the same feature set. However, storing layouts in code is generally preferred and encouraged by default. If you're just getting started with single-spa-layout, we encourage using an HTML template.

    Once you define your layout, you should constructRoutes, constructApplications, and constructLayoutEngine.

    HTML Layouts

    You may define HTML layouts either within your root config's index.html file, or within a javascript string that is parsed as HTML. We generally encourage defining the layout within your root config's index.html file.

    To define a layout within your index.html file, create a <template id="single-spa-layout"> element that contains your layout. Within the template, add a <single-spa-router> element, along with any routes, applications, and dom elements.

    Note that HTMLElements defined in your layout are static - there is no way to forcibly re-render or change them.

    <!-- index.ejs -->
    <html>
    <head>
    <template>
    <single-spa-router>
    <div class="main-content">
    <route path="settings">
    <application name="settings"></application>
    </route>
    </div>
    </single-spa-router>
    </template>
    </head>
    </html>
    // You can pass in an HTML string, too, in the browser
    const routes = constructRoutes(`
    <single-spa-router>
    <div class="main-content">
    <route path="settings">
    <application name="settings"></application>
    </route>
    </div>
    </single-spa-router>
    `);
    // With a properly configured bundler, you can import the html as a string from another file
    import layout from './microfrontends-layout.html';

    const routes = constructRoutes(layout);

    JSON Layouts

    You may define your layout as JSON, including routes, applications, and arbitrary dom elements.

    const routes = constructRoutes({
    "routes": [
    { "type": "route", "path": "settings", "routes": [
    { "type": "application", "name": "settings" }
    ]}
    ]
    });

    Layout Elements

    A layout element is an HTMLElement or JSON object that represents either a dom node, route, or application.

    <template>

    The template element is only used when defining the layout as HTML. Its purpose is to prevent its contents from being displayed by the browser, since the layout definition should not be visible to user.

    <template>
    <!-- Define your layout here -->
    <single-spa-router></single-spa-router>
    </template>

    Note that <template> elements are not fully supported in IE11. However, you do not need to polyfill template elements in order to use them in single-spa-layout. Instead, simply add style="display: none;" to the template to prevent its contents from being displayed in IE11.

    <template style="display: none;">
    <!-- Define your layout here -->
    <single-spa-router></single-spa-router>
    </template>

    <single-spa-router>

    The single-spa-router element is required as the top level container of your layout. All attributes are optional.

    <single-spa-router mode="hash|history" base="/" disableWarnings></single-spa-router>
    {
    "mode": "hash|history",
    "base": "/",
    "disableWarnings": false,
    "containerEl": "#container",
    "routes": []
    }

    Attributes

    • mode (optional): A string that must be hash or history that defaults to history. This indicates whether the routes should be matched against the Location pathname or hash.
    • base (optional): A string URL prefix that will be considered when matching route paths.
    • disableWarnings (optional): A boolean that turns of single-spa-layout's console warnings when the elements provided are incorrect.
    • containerEl (optional): A string CSS Selector or HTMLElement that is used as the container for all single-spa dom elements. Defaults to body.

    <route>

    The route element is used to control which applications and dom elements are shown for a top-level URL route. It may contain HTMLElements, applications, or other routes. Note that the route path is a URL prefix, not an exact match.

    <route path="clients">
    <application name="clients"></application>
    </route>

    <route default>
    <application name="clients"></application>
    </route>
    {
    "type": "route",
    "path": "clients",
    "routes": [
    { "type": "application", "name": "clients" }
    ],
    "default": false
    }

    Attributes

    Routes must either have a path or be a default route.

    • routes (required): An array of children elements that will be displayed when the route is active
    • path (optional): A path that will be matched against the browser's URL. The path is relative to its parent route (or the base URL). Leading and trailing / characters are unnecessary and are automatically applied. Paths may contain "dynamic segments" by using the : character ("clients/:id/reports"). Single-spa-layout uses single-spa's pathToActiveWhen function to convert the path string to an activity function. By default, the path is a prefix because it will match when any subroutes of the path match. See the exact attribute for exact matching.
    • default (optional): A boolean that determines whether this route will match all remaining URLs that have not been defined by sibling routes. This is useful for 404 Not Found pages. A sibling route is defined as any route with the same nearest-parent-route.
    • exact (optional, defaults to false): A boolean that determines whether the path should be treated as a prefix or exact match. When true the route does not activate if there are trailing characters in the URL path that are not specified in the path attribute.
    • props: An object of single-spa custom props that will be provided to the application when it is mounted. Note that these can be defined differently for the same application on different routes. You can read more about defining props within your HTML in the docs below.

    <application>

    The application element is used to render a single-spa application. Applications may be contained within route elements, or may exist at the top level as applications that will always be rendered. A container HTMLElement will be created by single-spa-layout when the application is rendered. The container HTMLElement is created with an id attribute of single-spa-application:appName such that your framework helpers will automatically use it when mounting the application.

    The same application may appear multiple times in your layout, under different routes. However, each application can only be defined once per-route.

    <!-- Basic usage -->
    <application name="appName"></application>

    <!-- Use a named loader that is defined in javascript -->
    <application name="appName" loader="mainContentLoader"></application>

    <!-- Add single-spa custom props to the application. The value of the prop is defined in javascript -->
    <application name="appName" props="myProp,authToken"></application>
    // Basic usage
    {
    "type": "application",
    "name": "appName"
    }

    // Use a single-spa parcel as a loading UI
    // You may also use Angular, Vue, etc.
    const parcelConfig = singleSpaReact({...})
    {
    "type": "application",
    "name": "appName",
    "loader": parcelConfig
    }

    // Use an HTML string as a loading UI
    {
    "type": "application",
    "name": "appName",
    "loader": "<img src='loading.gif'>"
    }

    // Add single-spa custom props
    {
    "type": "application",
    "name": "appName",
    "props": {
    "myProp": "some-value"
    }
    }

    Attributes

    • name (required): The string application name.
    • loader (optional): An HTML string or single-spa parcel config object. The loader will be mounted to the DOM while waiting for the application's loading function to resolve. You can read more about defining loaders in the docs below
    • props: An object of single-spa custom props that will be provided to the application when it is mounted. Note that these can be defined differently for the same application on different routes. You can read more about defining props within your HTML in the docs below.
    • class / className: The CSS class to apply to the container HTML element for this single-spa application. In JSON layouts, use className. In HTML layouts, use class.

    <fragment>

    The fragment element is used to specify a dynamic server-rendered portion of the template. Fragments are commonly used to inline import maps, add dynamic CSS / fonts, or customize the HTML <head> metadata. See sendLayoutHTTPResponse for more information about how fragments are rendered. Note that <fragment> elements only have meaning in server templates, not browser-only templates.

    <fragment name="importmap"></fragment>

    <fragment name="head-metadata"></fragment>

    <assets>

    The <assets> element is used to specify the location of server-rendered application assets, including CSS and fonts. When server-side rendered, the <assets> element is replaced by all the assets from the active applications on the page. Applications specify their assets as part of the renderApplication function provided to the sendLayoutHTTPResponse function.

    <assets></assets>

    <redirect>

    The <redirect> element is used to specify route redirects. On the server side, this is done with res.redirect(), which results in an HTTP 302 being sent to the browser. Within the browser, this is done by canceling navigation and then calling navigateToUrl().

    Redirects are always defined with absolute paths. This means that nesting a <redirect> inside of a route will not behave any differently than placing the redirect at the top level. All redirects should have full paths. Leading slashes are optional in those full paths.

    <redirect from="/" to="/login"></redirect>
    <redirect from="/old-settings" to="/login-settings"></redirect>

    In JSON, redirects are defined as a top-level property:

    {
    "routes": [],
    "redirects": {
    "/": "/login",
    "/old-settings": "/settings"
    }
    }

    DOM elements

    Arbitrary HTMLElements may be placed anywhere in your layout. You may define arbirary dom elements in both HTML and JSON.

    single-spa-layout only supports updating DOM elements during route transitions. Arbitrary re-renders and updates are not supported.

    DOM elements defined within a route will be mounted/unmounted as the route becomes active/inactive. If you define the same DOM element twice under different routes, it will be destroyed and recreated when navigating between the routes.

    <nav class="topnav"></nav>
    <div class="main-content">
    <button>A button</button>
    </div>

    JSON DOM Nodes

    The format of dom nodes in JSON is largely based on the parse5 format.

    HTMLElement

    Elements are defined with their nodeName as the type. HTML attributes are specified as the attrs array, where each item is an object with name and value properties.

    {
    "type": "div",
    "attrs": [
    {
    "name": "class",
    "value": "blue"
    }
    ]
    }

    Child nodes are specified via the "routes" property.

    {
    "type": "div",
    "routes": [
    {
    "type": "button"
    }
    ]
    }
    Text Nodes

    Text Nodes are defined separately from the parent containers, as separate objects with type set to #text:

    {
    "type": "#text",
    "value": "The displayed text"
    }

    Button with text:

    {
    "type": "button",
    "routes": [
    {
    "type": "#text",
    "value": "The button text"
    }
    ]
    }

    Note that text nodes may not have routes (children).

    Comment Nodes

    Comment Nodes are defined as objects whose type is #comment:

    {
    "type": "#comment",
    "value": "The comment text"
    }

    Note that comments may not have routes (children).

    Props

    Single-spa custom props may be defined on both route and application elements. Any route props will be merged together with the application props to create the final props that are passed to the single-spa lifecycle functions.

    JSON

    In a JSON layout definition, you can define props with the props property on your applications and routes:

    import { constructRoutes } from 'single-spa-layout';

    constructRoutes({
    routes: [
    { type: "application", name: "nav", props: { title: "Title" } },
    { type: "route", path: "settings", props: { otherProp: "Some value" } },
    ]
    })

    HTML

    Defining props on JSON objects is straightforward, as they are an object that can contain strings, numbers, booleans, objects, arrays, etc. However, defining complex data types in HTML is not as straightforward, since HTML attributes are always strings. To work around this, single-spa-layout allows you to name your props in the HTML, but define their values in javascript.

    <application name="settings" props="authToken,loggedInUser"></application>
    import { constructRoutes } from 'single-spa-layout';

    const data = {
    props: {
    authToken: "fds789dsfyuiosodusfd",
    loggedInUser: fetch('/api/logged-in-user').then(r => r.json())
    }
    }

    const routes = constructRoutes(document.querySelector('#single-spa-template'), data)

    The full API documentation for the constructRoutes API explains the data object in detail.

    Loading UIs

    It is often desireable to show a loading UI when waiting for an application's code to download and execute. Single-spa-layout allows you to define per-application loaders that will be mounted to the DOM while the application's loading function is pending. It is possible to share the same loading UI for multiple applications.

    A loading UI is defined as either an HTML string or as a parcel config object. HTML strings are best for static, non-interactive loaders, whereas parcels are best when you want to use a framework (Vue, React, Angular, etc) to dynamically render the loader.

    Defining loaders via javascript objects is straightforward, as they are an object that can contain strings, numbers, booleans, objects, arrays, etc. However, defining complex data types in HTML is not as straightforward, since HTML attributes are always strings. To work around this, single-spa-layout allows you to name your loaders in the HTML, but define their values in javascript.

    <application name="topnav" loader="topNav"></application>
    <application name="topnav" loader="settings"></application>
    import { constructRoutes } from 'single-spa-layout';

    // You may also use Angular, Vue, etc.
    const settingsLoader = singleSpaReact({...})

    const data = {
    loaders: {
    topNav: `<nav class="placeholder"></nav>`,
    settings: settingsLoader
    }
    }

    const routes = constructRoutes(document.querySelector('#single-spa-template'), data)

    The full API documentation for the constructRoutes API explains the data object in detail.

    Transitions

    Support for route transitions is planned, but not yet implemented. If you have interest in this feature, please provide use cases, upvotes, and feedback in this tracking issue.

    Default Routes (404 Not Found)

    Default routes are routes that activate when no other sibling routes match the current URL. They do not have a URL path and may contain any combination of DOM elements and single-spa applications.

    <single-spa-router>
    <route path="cart"></route>
    <route path="product-detail"></route>
    <route default>
    <h1>404 Not Found</h1>
    </route>
    </single-spa-router>

    Default routes are matched against their sibling routes, which allows for nesting:

    <single-spa-router>
    <route path="cart"></route>
    <route path="product-detail/:productId">
    <route path="reviews"></route>
    <route path="images"></route>
    <route default>
    <h1>Unknown product page</h1>
    </route>
    </route>
    <route default>
    <h1>404 Not Found</h1>
    </route>
    </single-spa-router>

    Sibling routes are defined as those that share a "nearest parent route." This means that they do not have to be direct siblings in your HTML/JSON, but can be nested within DOM elements:

    <single-spa-router>
    <route path="product-detail/:productId">
    <div class="product-content">
    <route path="reviews"></route>
    <route path="images"></route>
    </div>
    <!-- The reviews and images routes are siblings, since they share a nearest parent route -->
    <!-- The default route will activate when the URL does not match reviews or images -->
    <route default>
    <h1>Unknown product page</h1>
    </route>
    </route>
    </single-spa-router>

    Error UIs

    When a single-spa application fails to load, mount, or unmount, it moves to SKIP_BECAUSE_BROKEN or LOAD_ERROR status. When in SKIP_BECAUSE_BROKEN status, often nothing is visible to the user and they won't understand why the application is not showing. You can call unloadApplication to move the application back to NOT_LOADED status, which will cause single-spa to re-attempt downloading and mounting it. However, it is often desireable to show an error state when the application errors.

    An error UI is defined as either an HTML string or as a parcel config object. HTML strings are best for static, non-interactive error states, whereas parcels are best when you want to use a framework (Vue, React, Angular, etc) to dynamically render the error state. The error UI will be shown whenever the application's status is SKIP_BECAUSE_BROKEN or LOAD_ERROR.

    Note that Error UI parcels are given a prop called error that is the Error that caused the application to fail in loading/mounting.

    Defining error uis via javascript objects is straightforward, as the string or parcel can be defined in an application object via the error property:

    {
    "type": "application",
    "name": "nav",
    "error": "<h1>Oops! The navbar isn't working right now</h1>"
    }
    const myErrorParcel = singleSpaReact({...})

    {
    "type": "application",
    "name": "nav",
    "error": myErrorParcel
    }

    However, defining error uis in HTML is less straightforward, since HTML attributes are always strings and therefore can't be a parcel config object. To work around this, error UIs are named in the HTML, but defined in javascript:

    <template id="single-spa-layout">
    <single-spa-router>
    <application name="nav" error="navError"></application>
    </single-spa-router>
    </template>
    const myErrorParcel = singleSpaReact({...})

    const routes = constructRoutes(document.querySelector('#single-spa-layout'), {
    errors: {
    navError: myErrorParcel
    // alternatively:
    // navError: "<h1>Oops! The navbar isn't working right now</h1>"
    }
    })
    - - + + \ No newline at end of file diff --git a/docs/5.x/layout-overview/index.html b/docs/5.x/layout-overview/index.html index 517937cfb..7e706e2a6 100644 --- a/docs/5.x/layout-overview/index.html +++ b/docs/5.x/layout-overview/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Layout Engine

    Introduction

    Github repository

    The single-spa-layout npm package is an optional add-on to single-spa. The layout engine provides a routing API that controls your top level routes, applications, and dom elements. Using single-spa-layout makes it easier to accomplish the following:

    • DOM placement and ordering of applications.
    • Loading UIs when applications are downloaded.
    • Default routes for Not Found / 404 pages.
    • Transitions between routes (implementation pending).
    • Server side rendering of single-spa applications
    • Error pages

    In the browser, the layout engine performs two major tasks:

    1. Generate single-spa registration config from an HTML Element and/or JSON object.
    2. Listen to routing events to ensure that all DOM elements are laid out correctly before the single-spa applications are mounted.

    On the server, the layout engine performs two tasks:

    1. Construct a server layout object from an HTML template.
    2. Send an HTML document (HTTP response headers and body) to the browser, based on the server layout object and current route.

    single-spa-layout is 3.2kb gzipped (9kb ungzipped).

    Installation

    You only need to install the layout engine into your root config (not in any other application).

    npm install --save single-spa-layout

    # or
    yarn add single-spa-layout

    Browser / NodeJS support

    single-spa-layout works in all browsers supported by single-spa, including IE11. On the server, all NodeJS versions that support ESM are supported.

    Requirements

    You must use single-spa@>=5.4.0 in order for the layout engine to work. Additionally, you may not provide custom domElementGetter functions for any of your single-spa applications, as those override the configuration within single-spa-layout.

    Basic usage

    In your root html file, add a <template> element to the head. It should have a <single-spa-router> element that contains <route> elements, <application> elements, and any other dom elements:

    <html>
    <head>
    <template id="single-spa-layout">
    <single-spa-router>
    <nav class="topnav">
    <application name="@organization/nav"></application>
    </nav>
    <div class="main-content">
    <route path="settings">
    <application name="@organization/settings"></application>
    </route>
    <route path="clients">
    <application name="@organization/clients"></application>
    </route>
    </div>
    <footer>
    <application name="@organization/footer"></application>
    </footer>
    </single-spa-router>
    </template>
    </head>
    </html>

    Then inside of your root-config's JavaScript code, add the following:

    import { registerApplication, start } from 'single-spa';
    import {
    constructApplications,
    constructRoutes,
    constructLayoutEngine,
    } from 'single-spa-layout';

    const routes = constructRoutes(document.querySelector('#single-spa-layout'));
    const applications = constructApplications({
    routes,
    loadApp({ name }) {
    return System.import(name);
    },
    });
    const layoutEngine = constructLayoutEngine({ routes, applications });

    applications.forEach(registerApplication);
    start();
    - - + + \ No newline at end of file diff --git a/docs/5.x/microfrontends-concept/index.html b/docs/5.x/microfrontends-concept/index.html index 3f5dad66b..1a8e7d1b4 100644 --- a/docs/5.x/microfrontends-concept/index.html +++ b/docs/5.x/microfrontends-concept/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Concept: Microfrontends

    Tutorial video: Youtube / Bilibili

    A microfrontend is a microservice that exists within a browser.

    Microfrontends are sections of your UI, often consisting of dozens of components, that use frameworks like React, Vue, and Angular to render their components. Each microfrontend can be managed by a different team and may be implemented using its own framework. It is practical and suggested to use just one framework for all your microfrontends, although you may add additional frameworks when migrating or when experimenting.

    Each microfrontend has its own git repository, its own package.json file, and its own build tool configuration. As a result, each microfrontend has an independent build process and an independent deploy / CI. This generally means that each repo has fast build times.

    Comparison to Microservices

    Microservices are backend services that run in their own operating system process, control their own databases, and communicate with each other over the network.

    Compare that to microfrontends that all exist within a single browser tab: all browser JavaScript within a tab exists in a single operating system process (and even thread!). Browser JavaScript generally does not directly access databases, and communication within a browser tab happens in-memory instead of over the network.

    So what do they have in common???

    Independent builds and deployments. Think of the DOM as the shared resource that your microfrontends are owning. One microfrontend's DOM should not be touched by another microfrontend, similar to how one backend microservice's database should not be touched by any microservice except the one that owns/controls it.

    Concrete Technical Definition

    In the context of single-spa, a microfrontend is often an in-browser JavaScript module. You can read more about this in the recommended setup.

    Types of Microfrontends

    In the context of single-spa, there are three kinds of microfrontends:

    1. single-spa applications: Microfrontends that render components for a set of specific routes.
    2. single-spa parcels: Microfrontends that render components without controlling routes.
    3. utility modules: Microfrontends that export shared JavaScript logic without rendering components.

    A web app may include one or more types of microfrontends. See an in-depth comparison and our recommendations for choosing between microfrontend types.

    Communication between Microfrontends

    import { thing } from 'other-microfrontend' is the preferred way to communicate between microfrontends. Here is some documentation that goes over this in more detail.

    Relationship to single-spa

    single-spa is a small, 5kb (gzipped) npm package that orchestrates the mounting and unmounting of your microfrontends. It knows when to mount the applications based on activity functions and can do so in a framework agnostic way with the help of small adapter libraries.

    Performance

    Microfrontends are often more performant than the monoliths from which they originate. This is due to built-in lazy loading (via loading functions) and other performance-related best practices. Your monolith likely has "skeletons in its closet" - microfrontends gives you a migration path that will expose and resolve the problems caused by those skeletons. One important performance consideration is to share a single instance of large libraries (such as React, Vue, or Angular), which is highly encouraged. To do so, see our recommended setup.

    - - + + \ No newline at end of file diff --git a/docs/5.x/migrating-existing-spas/index.html b/docs/5.x/migrating-existing-spas/index.html index 05058eff9..cb1b0a7e3 100644 --- a/docs/5.x/migrating-existing-spas/index.html +++ b/docs/5.x/migrating-existing-spas/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -37,7 +37,7 @@ application. It is best to try to put all that you can into the JavaScript bundle, but your escape hatch is to put the things you need into your single spa config.

    - - + + \ No newline at end of file diff --git a/docs/5.x/module-types/index.html b/docs/5.x/module-types/index.html index 25ab37730..93a617438 100644 --- a/docs/5.x/module-types/index.html +++ b/docs/5.x/module-types/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -28,7 +28,7 @@ For example: Authorization. How does each application know which user is logged in? You could have each application ask the server or read a JWT but that creates duplicate work in each application. Using the utility module pattern would allow you to create one module that implements the authorization logic. This module would export any needed methods, and then your other single-spa applications could use those authorization methods by importing them. This approach also works well for data fetching.

    Examples of Utility Microfrontends

    The following are commonly implemented as a Utility Microfrontend:

    • Notification service
    • Styleguide/component library
    • Error tracking service
    • Authorization service
    • Data fetching
    - - + + \ No newline at end of file diff --git a/docs/5.x/parcels-api/index.html b/docs/5.x/parcels-api/index.html index 846563179..099c34577 100644 --- a/docs/5.x/parcels-api/index.html +++ b/docs/5.x/parcels-api/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ They both return a parcel object. The parcel object contains all additional exposed methods.

    Parcel Props

    When mounting a parcel the second argument is props, a JavaScript object of properties to be passed to the parcel. This object must have a domElement prop, which is the dom node that the parcel will mount into.

    const parcelProps = {
    customerId: 7,
    numberOfTasks: 42,
    domElement: document.createElement('div')
    }

    mountParcel

    applicationProps.mountParcel(parcelConfig, parcelProps). Each application is provided a mountParcel function. The main advantage to using an applications mountParcel function is that parcels mounted via an applications mountParcel will be automatically unmounted when the application is unmounted.

    The first argument may be either an object or a function that returns a promise that resolves with the object (a loading function).

    // Synchronous mounting
    const parcel1 = applicationProps.mountParcel(parcelConfig, parcelProps);

    // Asynchronous mounting. Feel free to use webpack code splits or SystemJS dynamic loading
    const parcel2 = applicationProps.mountParcel(() => import('./some-parcel'), parcelProps);

    mountRootParcel

    The mountRootParcel method will mount the parcel but unmount must be called manually.

    Parcel Object

    The parcel object contains the following functions and methods:

    unmount

    parcel.unmount() returns a promise that resolves once the parcel is successfully unmounted. The promise may throw an error which needs to be handled.

    mount

    parcel.mount() returns a promise that resolves once the parcel is successfully mounted. The promise can throw an error which needs to be handled.

    update

    parcel.update(props) allows you to change the props passed into a parcel. Note that not all parcels support being updated. The update function returns a promise that resolves when the parcel is finished updating. See other documentation and example for more information.

    const parcel = singleSpa.mountRootParcel(parcelConfig, parcelProps);
    parcel.update(newParcelProps);

    getStatus

    parcel.getStatus() returns a string of that parcels status. The string status is one of the following:

    • NOT_BOOTSTRAPPED: The parcel has not been bootstrapped
    • BOOTSTRAPPING: The parcel is bootstrapping but has not finished
    • NOT_MOUNTED: The parcel has bootstrapped, but is not mounted
    • MOUNTED: The parcel is currently active and mounted to the DOM
    • UNMOUNTING: The parcel is unmounting, but has not finished
    • UPDATING: The parcel is currently being updated, but has not finished
    • SKIP_BECAUSE_BROKEN: The parcel threw an error during bootstrap, mount, unmount, or update. Other parcels may continue normally, but this one will be skipped.

    loadPromise

    parcel.loadPromise() returns a promise that will resolve once the parcel has been loaded.

    bootstrapPromise

    parcel.bootstrapPromise() returns a promise that will resolve once the parcel has been bootstrapped.

    mountPromise

    parcel.mountPromise() returns a promise that will resolve once the parcel has been mounted. This is helpful for knowing exactly when a parcel has been appended to the DOM

    unmountPromise

    parcel.unmountPromise() returns a promise that will resolve once the parcel has been unmounted.

    - - + + \ No newline at end of file diff --git a/docs/5.x/parcels-overview/index.html b/docs/5.x/parcels-overview/index.html index d0300f0d6..06af31966 100644 --- a/docs/5.x/parcels-overview/index.html +++ b/docs/5.x/parcels-overview/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -34,7 +34,7 @@ We could do any number of things to share the functionality between application 1 and 2:

    • If both are written in the same framework we could export/import components.
    • We could reimplement creating a contact (loss of cohesion)
    • We could use single-spa parcels.

    Exporting a parcel from App1 that wraps the createContact modal component gives us the ability to share components and behavior across disparate frameworks, without losing application cohesion. App1 can export a modal as a single-spa parcel and App2 can import the parcel and use it easily. One major advantage is that in the below example the parcel/modal from App1 that is being used by App2 will also be unmounted, without unmounting/mounting of App1.

    // App1
    export const AddContactParcel = {
    bootstrap: bootstrapFn,
    mount: mountFn,
    unmount: unmountFn,
    }

    // App2
    // get the parcel configuration in this case I'm using systemJS and react
    ...
    componentDidMount() {
    SystemJS.import('App1').then(App1 => {
    const domElement = document.body
    App2MountProps.mountParcel(App1.AddContactParcel, {domElement})
    })
    }
    ...

    mountRootParcel vs mountParcel

    Single spa exposes two APIs for working with parcels. These API's are differentiated primarily by the context in which the parcel is created and how to access the API's

    mountRootParcelmountParcel
    contextsingleSpaapplication
    unmount conditionmanual onlymanual + application unmount
    api locationsingleSpa named exportprovided in lifecycle prop

    Which should I use?

    In general we suggest using the application-aware mountParcel API. mountParcel allows you to treat the parcel just like a component inside your application without considering what framework it was written in and being forced to remember to call unmount.

    How do I get the mountParcel API?

    In order to keep the function contextually bound to an application it is provided to the application as a lifecycle prop. You will need to store and manage that function yourself in your application.

    Example of storing the application specific mountParcel API:

    // App1
    let mountParcel
    export const bootstrap = [
    (props) => {
    mountParcel = props.mountParcel
    return Promise.resolve()
    },
    // more bootstrap lifecycles if necessary
    ]
    ...

    note: some libraries (such as react) support a framework specific context that makes it easy to store/manage. In those cases we've written some helper methods to abstract away the need to manage and store the mountParcel method.

    - - + + \ No newline at end of file diff --git a/docs/5.x/recommended-setup/index.html b/docs/5.x/recommended-setup/index.html index 9fb1d17c8..dd6802338 100644 --- a/docs/5.x/recommended-setup/index.html +++ b/docs/5.x/recommended-setup/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -26,7 +26,7 @@ An import specifier is the string indicating which module to load. Examples:

    // ./thing.js is the import specifier
    import thing from './thing.js';

    // react is the import specifier
    import React from 'react';

    Specifiers that are not a URL are called "bare specifiers," such as import 'react'. Being able to alias bare specifiers to a URL is crucial to being able to use in-browser modules, which is why import maps exist.

    Import Maps are not supported in all browsers. See https://caniuse.com/import-maps for more detail. You can use SystemJS or es-module-shims to polyfill support for import maps.

    Module Federation

    Module Federation is a webpack-specific technique for sharing build-time modules. It involves each microfrontend bundling all of its dependencies, even the shared ones. This means that there are multiple copies of each shared dependency - one per microfrontend. In the browser, the first copy of the shared dependency will be downloaded, but subsequent microfrontends will reuse that shared dependency without downloading their copy of it.

    Note that Module Federation is a new feature (at the time of this writing) and requires that you use webpack@>=5. It is still an evolving technology.

    single-spa is a way of structuring your routes for microfrontends. Module Federation is a performance technique for microfrontends. They complement each other well and can be used together. Here is a YouTube video by a community member that talks about using single-spa and module federation together.

    With module federation, you must choose how you wish to load the microfrontends themselves. The single-spa core team recommends using SystemJS + import maps as a module loader for the microfrontends. Alternatively, you may use global variables and <script> elements. An example of using SystemJS to load microfrontends with module federation can be found at https://github.com/ScriptedAlchemy/mfe-webpack-demo/pull/2.

    The single-spa core team recommends choosing either import maps or module federation for your shared, third-party dependencies. We do not recommend sharing some third-party dependencies via import map and others via module federation. When choosing between the two approaches, we have a preference towards import maps, but no objection to module federation. See the shared dependencies section for a comparison.

    SystemJS

    Tutorial video: Youtube / Bilibili

    SystemJS provides polyfill-like behavior for import maps and in-browser modules. It is not a true polyfill of import maps, due to limitations of the JavaScript language in polyfilling the resolution of bare import specifiers to URLs.

    Since SystemJS is only polyfill-like, you'll need to compile your applications into System.register format instead of to ESM format. This allows for in-browser modules to be fully emulated in environments that don't support modules or import maps.

    To compile your code to System.register format, set webpack's output.libraryTarget to "system", or set rollup's format to "system".

    Shared dependencies like React, Vue, and Angular, do not publish System.register versions of their libraries. However, you can find System.register versions of the libraries in the esm-bundle project (blog post). Alternatively, SystemJS is capable of loading them via global loading or the AMD and named-exports extras.

    Another resource for sharing dependencies is the self-hosted-shared-dependencies project.

    An alternative to SystemJS that provides polyfill behavior for import maps is es-module-shims. This has the advantage of using truly native ES modules. However, it is not the single-spa core team's recommended approach for production applications, since it requires less-performant in browser parsing and modification of all your bundles.

    Lazy loading

    Tutorial video: Youtube / Bilibili

    Lazy loading is when you only download JavaScript code that the user needs for the current page, instead of all JavaScript up front. It is a technique for improving the performance of your application by decreasing the time-to-meaningful-render when you initially load the page. If you use single-spa loading functions, you already have built-in lazy loading for your applications and parcels. Since an application is an "in-browser module," this means that you are only downloading the in-browser modules in your import map when you need them.

    Often, the route-based lazy loading provided by single-spa loading functions is all that you need to ensure great performance. However, it is also possible to do lazy loading via "code splits" with your bundler (webpack or rollup). For documentation on webpack code splits, see these docs. It is recommended to use dynamic import (import()) instead of multiple entry points for code splits in a single-spa application. For code splits to work properly, you'll need to dynamically set your public path. A tool exists to help you set your public path correctly for use with systemjs - https://github.com/joeldenning/systemjs-webpack-interop.

    Local development

    Tutorial video: Youtube / Bilibili

    In contrast to monolithic frontend applications, local development with single-spa encourages only running the one microfrontend you're working on, while using deployed versions of all other microfrontends. This is important because running every single-spa microfrontend every time you want to do anything is unwieldy and cumbersome.

    To accomplish local development of only one microfrontend at a time, we can customize the URL for that microfrontend within the import map. For example, the following import map is set up for local development of the navbar application, since that's the only one pointing to a local web server. The planets and things applications are pointing to deployed (already hosted) versions of the applications.

    {
    "imports": {
    "@react-mf/navbar": "https://localhost:8080/react-mf-navbar.js",
    "@react-mf/planets": "https://react.microfrontends.app/planets/2717466e748e53143474beb6baa38e3e5320edd7/react-mf-planets.js",
    "@react-mf/things": "https://react.microfrontends.app/things/7f209a1ed9ac9690835c57a3a8eb59c17114bb1d/react-mf-things.js"
    }
    }

    A tool called import-map-overrides exists to customize your import map through an in-browser UI. This tool will automatically let you toggle one or more microfrontends between your localhost and the deployed version.

    Alternatively, you can use standalone-single-spa-webpack-plugin, which allows you to develop each application in standalone mode. Another alternative is to always run the single-spa root config locally, in addition to whichever microfrontends you're developing.

    The single-spa core team recommends development on deployed environments via import-map-overrides, as we find that to be the best developer experience, since it allows you to only start one project at a time while also ensuring there's no difference between the local environment and fully-integrated deployed environment. However, there are cases when running the root config locally or using standalone-single-spa-webpack-plugin can be useful.

    Build tools (Webpack / Rollup)

    Tutorial video: Youtube / Bilibili

    It is highly encouraged to use a bundler such as webpack, rollup, parceljs, pikapack, etc. Webpack is an industry-standard for compiling many JavaScript source files into one or more production JavaScript bundles.

    Below are some tips for configuring your bundler to be consumable by SystemJS and single-spa. Note that if you're using create-single-spa that these are all set up for you. We leave these instructions here not to overwhelm you with webpack configuration hell, but rather to help you if you choose not to use create-single-spa.

    1. Set the output target to system. In webpack, this is done via output.libraryTarget
    2. Use a single entry point, with dynamic imports for any code splitting that you'd like to accomplish. This best matches the "one bundled project = one in-browser module" paradigm encouraged by the single-spa core team.
    3. Do not use webpack's optimization configuration options, as they make it harder to load the outputted JavaScript files as a single in-browser JavaScript module. Doing so does not make your bundle less optimized - dynamic imports are a viable strategy for accomplishing optimized bundles.
    4. Follow the systemjs docs for webpack.
    5. Consider using systemjs-webpack-interop to create or verify your webpack config.
    6. Use systemjs-webpack-interop to set your webpack public path "on the fly".
    7. Do not set webpack output.library. SystemJS does not need a name, and in fact does not support named modules without additional configuration.
    8. Consider turning off webpack hashing for both entry and code split bundles. It is often easier to add in a commit hash during deployment of your microfrontend via your CI environment variables.
    9. Configure webpack-dev-server to not do host checks. (docs).
    10. Configure webpack-dev-server for CORS by setting {headers: {'Access-Control-Allow-Origin': '*'}}. (docs)
    11. If developing on https, configure webpack-dev-server for HTTPS. Also consider trusting SSL certificates from localhost.
    12. Make sure that your webpack externals are correctly configured for any shared, in-browser modules that you are importing.
    13. Set output.jsonpFunction to be a unique string for this project. Since you'll have multiple webpack bundles running in the same browser tab, a collision of the jsonpFunction could result in webpack modules getting mixed between bundles.
    14. Set sockPort, sockPath, and sockHost inside of your devServer configuration.
    15. For webpack, set output.devtoolNamespace to your MFE's name. This helps namespace your sourcemaps to each MFE.

    For a bit more information specific to webpack code splits, see the code splits FAQ.

    Utility modules (styleguide, API, etc)

    A "utility module" is an in-browser JavaScript module that is not a single-spa application or parcel. In other words, it's only purpose is to export functionality for other microfrontends to import.

    Common examples of utility modules include styleguides, authentication helpers, and API helpers. These modules do not need to be registered with single-spa, but are important for maintaining consistency between several single-spa applications and parcels.

    Example code in a utility module:

    // In a repo called "api", you may export functions from the repo's entry file.
    // These functions will be available to single-spa application, parcels, and other in-browser modules
    // via an import statement.

    export function authenticatedFetch(url, init) {
    return fetch(url, init).then(r => {
    // Maybe do some auth stuff here
    return r.json()
    })
    }

    Example code in a single-spa application that is using the utility module:

    // Inside of a single-spa application, you can import the functions from the 'api' repo
    import React from 'react'
    import { authenticatedFetch } from '@org-name/api';

    export function Foo(props) {
    React.useEffect(() => {
    const abortController = new AbortController()
    authenticatedFetch(`/api/clients/${props.clientId}`, {signal: abortController.signal})
    .then(client => {
    console.log(client)
    })

    return () => {
    abortController.abort()
    }
    }, [props.clientId])

    return null
    }

    To make utility modules work, you must ensure that your webpack externals and import map are properly configured. An example of a working styleguide may be found at https://github.com/vue-microfrontends/styleguide.

    Cross microfrontend imports

    Example - exporting a shared component, importing a shared component, and required webpack config.

    You can import and export functions, components, logic, data, event emitters, and environment variables between your microfrontends that are in different git repos and JavaScript bundles. Each microfrontend should have a single entry file that serves as the "public interface" that controls what is exposed outside of the microfrontend.

    To make cross microfrontend imports possible, configure your bundler so that the microfrontends are treated as "externals" (webpack docs / rollup docs). Marking them as externals ensures that they are treated as in-browser modules instead of build-time modules.

    // Inside of the "entry file" for a utility module called @org-name/auth,
    // expose your public interface that other microfrontends can access.
    // Often this is within the main.js or main.single-spa.js file.

    export function userHasAccess(permission) {
    return loggedInUser.permissions.some(p => p === permission);
    }
    import { userHasAccess } from '@org-name/auth'

    // Inside of a single-spa application, import and use a util function from a different microfrontend
    const showLinkToInvoiceFeature = userHasAccess('invoicing');
    // In your webpack config, mark @org-name auth as a webpack external
    module.exports = {
    externals: ['@org-name/auth'],

    // Alternatively, mark *all* org-name packages as externals
    // externals: [/^@org-name\/.+/]
    }

    Shared dependencies

    For performance, it is crucial that your web app loads large JavaScript libraries only once. Your framework of choice (React, Vue, Angular, etc) should only be loaded on the page a single time.

    It is not advisable to make everything a shared dependency, because shared dependencies must be upgraded at once for every microfrontend that uses them. For small libraries, it is likely acceptable to duplicate them in each microfrontend that uses them. For example, react-router is likely small enough to duplicate, which is nice when you want to upgrade your routing one microfrontend at a time. However, for large libraries like react, momentjs, rxjs, etc, you may consider making them shared dependencies.

    There are two approaches to sharing dependencies:

    1. In-browser modules with import maps
    2. Build-time modules with module federation

    You may use either one, or both. We currently recommend only using import maps, although we have no objection to module federation.

    Comparison of approaches

    ApproachShare dependenciesBundler requirementsManaging dependencies
    Import MapsFully supportedAny bundlershared dependecies repo
    Module FederationFully supportedOnly webpack@>=5multiple webpack configs

    Sharing with Import Maps

    To share a dependency between microfrontends with Import Maps, you should use webpack externals, rollup externals, or similar. Marking libraries as external tells your bundler to not use the version in your node_modules, but rather to expect the library to exist as an in-browser module.

    To make the shared dependencies available as in-browser modules, they must be present in your import map. A good way of managing them is to create a repository called shared-dependencies that has a partial import map in it. The CI process for that repository updates your deployed import map. Upgrading the shared dependencies can then be achieved by making a pull request to that repository.

    Not all libraries publish their code in a suitable format for SystemJS consumption. In those cases, check https://github.com/esm-bundle for a SystemJS version of those libraries. Alternatively, you may use SystemJS extras to support UMD bundles, which are often available.

    Another option for finding a suitable version of a library for your import map is to use the JSPM CDN, which provides precompiled SystemJS versions of every package on npm (example: https://system-cdn.jspm.io/npm:@material-ui/core@4.11.3/index.js). See https://jspm.org/docs/cdn for more info. You can generate an import map for your shared dependencies at https://generator.jspm.io/.

    Another option for hosting shared dependencies is self-hosted-shared-dependencies, which generates a directory of third party packages that you can self host on your server / CDN.

    An example of a shared-dependencies repo, along with a functioning CI process for it, can be found at https://github.com/polyglot-microfrontends/shared-dependencies.

    Sharing with Module Federation

    At the time of this writing, module federation is new and still changing. Check out this example repo which uses systemjs to load the microfrontends, but module federation to share react, react-dom, and react-router.

    Deployment and Continuous Integration (CI)

    Tutorial video (Part 1): Youtube / Bilibili

    Tutorial video (Part 2): Youtube / Bilibili

    Example CI configuration files

    Microfrontends are built and deployed completely independently. This means that the git repository, CI, build, and deployments all occur without going through a centralized repository. For this reason, monorepos are not encouraged for microfrontends. CI for monorepos can be configured to only build and deploy the packages that have changed but it is often more complex. Modern CI platforms such as AWS Amplify and Vercel are starting to have built-in support for monorepos however.

    There are two steps to deploying a microfrontend.

    1. Uploading production JavaScript bundles to a web server / CDN. It is encouraged to use a CDN such as AWS S3 + Cloudfront, Google Cloud Storage, Microsoft Azure Storage, Digital Ocean Spaces, etc because of their superior availability, caching, and performance due to edge locations. The JavaScript files that you upload are completely static. It is encouraged to always write new files to the CDN instead of overwriting files.
    2. Updating your import map to point to the newly deployed file.

    The implementation of Step 1 is dependent on the infrastructure you're using for your CDN. The AWS CLI (aws s3 sync), Google gsutil (gsutil cp), etc are easy ways of accomplishing this.

    If you prefer or require using docker containers rather than Cloud Storage like S3, see https://github.com/single-spa/docker-import-maps-mfe-server

    For the implementation of Step 2, you have a choice:

    a) Your CI makes a curl HTTP call to a running instance of import-map-deployer, which updates the import map in a concurrent-safe way. b) Your CI runner pulls down the import map, modify it, and reuploads it.

    The advantage of a) is that it is concurrent-safe for multiple, simultaneous deployments. Without a concurrent-safe solution, there might be multiple processes pulling down and reuploading the import map at the same time, which could result in a race condition where one CI process thinks it successfully updated the import map when in reality the other CI process wrote the import map later, having based its changes on a stale version of the import map.

    The advantage of b) is that it doesn't require running the import-map-deployer in your production environment. Ultimately, you should choose whichever option makes sense for your organization.

    Another option of deploying and making sure the latests javascript files are beeing used is making use of redirect (HTTP status code 302). The single-spa import map uses the unhashed url of the javascript file. eg: dist/app.js. Then when the requests for this file comes to the server it is redirected to the actual deployed file eg. dist/app.123abc.js and this is then served to the client. This way the import map never has to be updated and the microfrontend can be separately deployed.

    Applications versus parcels versus utility modules

    Single-spa has different categories of microfrontends. It is up to you where and how you use each of them. However, the single-spa core team recommends the following:

    Many route-based single-spa applications, very few single-spa parcels

    1. Prefer splitting microfrontends by route, instead of by components within a route. This means preferring single-spa applications over single-spa parcels whenever possible. The reason for this is that transitions between routes often involve destroying and recreating most UI state, which means your single-spa applications on different routes do not need to ever share UI state.
    2. Move fixed navigation menus into their own single-spa applications. Implement their activity functions to be active by default, only unmounting for the login page.
    3. Create utility modules for your core component library / styleguide, for shared authentication / authorization code, and for global error handling.
    4. If you are only using one framework, prefer framework components (i.e. React, Vue, and Angular components) over single-spa parcels. This is because framework components interop easier with each other than when there is an intermediate layer of single-spa parcels. You can import components between single-spa applications You should only create a single-spa parcel if you need it to work with multiple frameworks.

    Inter-app communication

    A good architecture is one in which microfrontends are decoupled and do not need to frequently communicate. Following the guidelines above about applications versus parcels helps you keep your microfrontends decoupled. Route-based single-spa applications inherently require less inter-app communication.

    There are three things that microfrontends might need to share / communicate:

    1. Functions, components, logic, and environment variables.
    2. API data
    3. UI state

    Functions, components, logic, and environment variables

    We recommend using cross microfrontend imports to share functions, components, logic, and environment variables.

    API Data

    Example - exporting a fetchWithCache function and importing the function.

    API data often does not need to be shared between microfrontends, since each single-spa application controls different routes and different routes often have different data. However, occasionally you do need to share API data between microfrontends. An in-memory JavaScript cache of API objects is a solution used by several companies to solve this. For React users, this is similar to Data Fetching with Suspense, where the fetching logic for routes is split out from the component code that uses the data.

    // Inside of your api utility module, you can lazily fetch data either when another microfrontend calls your exported
    // functions, or eagerly fetch it when the route changes.
    let loggedInUserPromise = fetch('...').then(r => {
    if (r.ok) {
    return r.json()
    } else {
    throw Error(`Error getting user, server responded with HTTP ${r.status}`)
    }
    })

    export function getLoggedInUser() {
    return loggedInUserPromise;
    }
    import { getLoggedInUser } from '@org-name/api';

    // Inside of app1, you can import something from an "api" utility module
    getLoggedInUser().then(user => {
    console.log('user', user);
    });

    UI State

    If two microfrontends are frequently passing state between each other, consider merging them. The disadvantages of microfrontends are enhanced when your microfrontends are not isolated modules.

    UI State, such as "is the modal open," "what's the current value of that input," etc. largely does not need to be shared between microfrontends. If you find yourself needing constant sharing of UI state, your microfrontends are likely more coupled than they should be. Consider merging them into a single microfrontend.

    Under the rare circumstances where you do need to share UI state between single-spa applications, an event emitter may be used to do so. Below are a few examples of event emitters that might help you out.

    1. Observables / Subjects (rxjs) - one microfrontend emits new values to a stream that can be consumed by any other microfrontend. It exports the observable to all microfrontends from its in-browser module, so that others may import it.
    2. CustomEvents - browsers have a built-in event emitter system that allows you to fire custom events. Check out this documentation for more information. Firing the events with window.dispatchEvent allows you to subscribe in any other microfrontend with window.addEventListener.
    3. Any other pub/sub event emitter system.

    State management

    The single-spa core team cautions against using redux, mobx, and other global state management libraries. However, if you'd like to use a state management library, we recommend keeping the state management tool specific to a single repository / microfrontend instead of a single store for all of your microfrontends. The reason is that microfrontends are not truly decoupled or framework agnostic if they all must use a global store. You cannot independently deploy a microfrontend if it relies on the global store's state to be a specific shape or have specific actions fired by other microfrontends - to do so you'd have to think really hard about whether your changes to the global store are backwards and forwards compatible with all other microfrontends. Additionally, managing global state during route transitions is hard enough without the complexity of multiple microfrontends contributing to and consuming the global state.

    Instead of a global store, the single-spa core team recommends using local component state for your components, or a store for each of your microfrontends. See the above section "Inter-app communication" for more related information.

    - - + + \ No newline at end of file diff --git a/docs/5.x/separating-applications/index.html b/docs/5.x/separating-applications/index.html index e0151a9ae..7586ff41f 100644 --- a/docs/5.x/separating-applications/index.html +++ b/docs/5.x/separating-applications/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ This allows for separate builds and deployment without having separate code repositories.

    Option 4: Dynamic Module Loading

    Create a root application which can allow single-spa applications to deploy themselves separately. To do so, create a manifest file that the single-spa applications update during their deployment process, which controls which versions of the single-spa applications are "live". Then change which JavaScript file is loaded based on the manifest.

    Changing which JavaScript file is loaded for each child application can be done in many ways.

    1. Web server: have your webserver create a dynamic script tag for the "live" version of each single-spa application.
    2. Use a module loader such as SystemJS that can download and execute JavaScript code in the browser from dynamic urls.

    Comparison

    Separate code repositories possibleIndependent CI buildsSeparate deploymentsExamples
    NPM Packages1
    Monorepo111
    Module loading1 2 3
    - - + + \ No newline at end of file diff --git a/docs/5.x/shared-webpack-configs/index.html b/docs/5.x/shared-webpack-configs/index.html index 38cb9a350..b9fce099e 100644 --- a/docs/5.x/shared-webpack-configs/index.html +++ b/docs/5.x/shared-webpack-configs/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Shared Webpack configs

    single-spa also publishes a few shared Webpack config packages that include basics needed for creating single-spa applications. These configs are used by applications generated with create-single-spa, and can be used as a starting point to extend/modify a custom configuration for your organization/team.

    These packages are housed within the create-single-spa repository.

    webpack-config-single-spa

    Github project

    A shareable, customizable webpack config that is used for single-spa modules.

    Installation

    npm install --save-dev webpack-config-single-spa webpack-merge

    # or
    yarn add --dev webpack-config-single-spa webpack-merge

    Usage

    const singleSpaDefaults = require('webpack-config-single-spa');
    const { merge } = require('webpack-merge'); // webpack-merge@5.0.3+

    module.exports = (webpackConfigEnv, argv) => {
    const defaultConfig = singleSpaDefaults({
    // The name of the organization this application is written for
    orgName: 'name-of-company',
    // The name of the current project. This usually matches the git repo's name
    projectName: 'name-of-project',
    // See https://webpack.js.org/guides/environment-variables/#root for explanation of webpackConfigEnv
    webpackConfigEnv,
    // The CLI commands in the package.json script that triggered this build
    argv,
    // optional
    // This changes whether package names that start with @your-org-name are
    // treated as webpack externals or not. Defaults to true
    orgPackagesAsExternal: true,

    // optional, defaults to 1
    // This is the rootDirectoryLevel that is passed to https://github.com/joeldenning/systemjs-webpack-interop
    rootDirectoryLevel: 1,

    // optional, defaults to false
    // Disable html-webpack-plugin (and standalone-single-spa-webpack-plugin) entirely
    // This is intended for root configs, but can be used in other cases, too
    disableHtmlGeneration: false,
    });

    return merge(defaultConfig, {
    // modify the webpack config however you'd like to by adding to this object
    });
    };

    webpack-config-single-spa-react

    Github project

    A shareable, customizable webpack config that adds react-specific configuration to webpack-config-single-spa.

    Installation

    npm install --save-dev webpack-config-single-spa-react webpack-merge

    # or
    yarn add --dev webpack-config-single-spa-react webpack-merge

    Usage

    const singleSpaDefaults = require('webpack-config-single-spa-react');
    const webpackMerge = require('webpack-merge');

    module.exports = (webpackConfigEnv, argv) => {
    const defaultConfig = singleSpaDefaults({
    // The name of the organization this application is written for
    orgName: 'name-of-company',
    // The name of the current project. This usually matches the git repo's name
    projectName: 'name-of-project',
    // See https://webpack.js.org/guides/environment-variables/#root for explanation of webpackConfigEnv
    webpackConfigEnv,
    // The CLI commands in the package.json script that triggered this build
    argv,
    // optional
    // This changes whether package names that start with @your-org-name are
    // treated as webpack externals or not. Defaults to true
    orgPackagesAsExternal: true,

    // optional, defaults to 1
    // This is the rootDirectoryLevel that is passed to https://github.com/joeldenning/systemjs-webpack-interop
    rootDirectoryLevel: 1,

    // optional, defaults to {}
    // This controls the options given to standalone-single-spa-webpack-plugin
    // See https://github.com/single-spa/standalone-single-spa-webpack-plugin#usage
    standaloneOptions: {},
    });

    // modify the webpack config however you'd like to by adding to this object
    });
    };

    webpack-config-single-spa-ts

    Github project

    A shareable, customizable webpack config that adds typescript-specific configuration to webpack-config-single-spa. Note that webpack-config-single-spa-ts has a peerDependency on typescript.

    Installation

    npm install --save-dev webpack-config-single-spa-ts webpack-merge

    # or
    yarn add --dev webpack-config-single-spa-ts webpack-merge

    Usage

    const webpackMerge = require('webpack-merge');
    const singleSpaDefaults = require('webpack-config-single-spa-ts');

    module.exports = (webpackConfigEnv, argv) => {
    const defaultConfig = singleSpaDefaults({
    // The name of the organization this application is written for
    orgName: 'name-of-company',
    // The name of the current project. This usually matches the git repo's name
    projectName: 'name-of-project',
    // See https://webpack.js.org/guides/environment-variables/#root for explanation of webpackConfigEnv
    webpackConfigEnv,
    // The CLI commands in the package.json script that triggered this build
    argv,
    });

    return webpackMerge.smart(defaultConfig, {
    // modify the webpack config however you'd like to by adding to this object
    });
    };
    const singleSpaTs = require('webpack-config-single-spa-ts');

    // Alternatively, you may modify a webpack config directly
    const myOtherWebpackConfig = {
    /* ... */
    };
    const finalConfig = singleSpaDefaults.modifyConfig(myOtherWebpackConfig);

    webpack-config-single-spa-react-ts

    Github project

    A shareable, customizable webpack config that creates a webpack config that works with both react and typescript. Note that webpack-config-single-spa-react-ts simply merges the config from webpack-config-single-spa-react with that of webpack-config-single-spa-ts.

    Installation

    npm install --save-dev webpack-config-single-spa-react-ts webpack-merge

    # or
    yarn add --dev webpack-config-single-spa-react-ts webpack-merge

    Usage

    const webpackMerge = require('webpack-merge');
    const singleSpaDefaults = require('webpack-config-single-spa-react-ts');

    module.exports = (webpackConfigEnv, argv) => {
    const defaultConfig = singleSpaDefaults({
    // The name of the organization this application is written for
    orgName: 'name-of-company',

    // The name of the current project. This usually matches the git repo's name
    projectName: 'name-of-project',

    // optional
    // This changes whether package names that start with @your-org-name are
    // treated as webpack externals or not. Defaults to true
    orgPackagesAsExternal: true,

    // See https://webpack.js.org/guides/environment-variables/#root for explanation of webpackConfigEnv
    webpackConfigEnv,

    // The CLI commands in the package.json script that triggered this build
    argv,

    // optional, defaults to 1
    // This is the rootDirectoryLevel that is passed to https://github.com/joeldenning/systemjs-webpack-interop
    rootDirectoryLevel: 1,

    // optional, defaults to false.
    // When true, this removes html-webpack-plugin and standalone-single-spa-webpack-plugin
    disableHtmlGeneration: false,
    });

    return webpackMerge.smart(defaultConfig, {
    // modify the webpack config however you'd like to by adding to this object
    });
    };

    Custom Webpack configuration

    Our shared Webpack configs are intended to be extensible to fit the requirements of your applications. These custom config options can be made in each project's webpack.config.js file generated by create-single-spa, or used as the basis for a tailored shared config for your organization.

    • Use require.resolve to reuse loaders that are included as dependencies
      • this is especially useful for reusing any of webpack-config-single-spa-*'s dependencies
    • webpack-merge does not support merging configs exported as a function, which may be relevant if creating shared config packages. webpack-config-single-spa-* configs require webpackConfigEnv and argv parameters which necesitates exporting a config function but return a plain object. This makes it compatible with webpack-merge.
    • Use webpack-merge's mergeWithRules function to merge and de-duplicate webpack rules

    Example: load SVGs as components

    You must also install @svgr/webpack.

    const singleSpaDefaults = require('webpack-config-single-spa');
    const { mergeWithRules } = require('webpack-merge');

    const merge = mergeWithRules({
    module: {
    rules: {
    // replace the entire `rule` if the `test` property matches
    test: 'match',
    use: 'replace',
    },
    },
    });

    module.exports = (env, argv) => {
    const defaultConfig = singleSpaDefaults({
    orgName: 'abcde',
    projectName: 'fghij',
    webpackConfigEnv: env,
    argv,
    });

    const config = merge(defaultConfig, {
    module: {
    rules: [
    {
    test: /\.svg$/i,
    use: [{ loader: '@svgr/webpack' }],
    },
    ],
    },
    });

    // console.dir(config, null, 2) // useful for debugging
    return config;
    };

    Replacing plugins

    Use webpack-merge's mergeWithCustomize to resolve duplicate plugins or replace instances. Duplicate plugins often result in cryptic errors!

    When referencing a loader that is installed as a dependency of webpack-config-single-spa, use require.resolve to ensure its path is resolved correctly.

    Example: replace HtmlWebpackPlugin instance

    const { mergeWithCustomize, unique } = require('webpack-merge');
    const singleSpaDefaults = require('webpack-config-single-spa');
    const HtmlWebpackPlugin = require('html-webpack-plugin');

    const merge = mergeWithCustomize({
    customizeArray: unique(
    'plugins',
    ['HtmlWebpackPlugin'],
    plugin => plugin.constructor && plugin.constructor.name,
    ),
    });

    module.exports = (env, argv) => {
    const orgName = 'example';
    const myEnv = process.env.NODE_ENV || 'development';

    const defaultConfig = singleSpaDefaults({
    orgName,
    projectName: 'custom-root-config',
    webpackConfigEnv: env,
    argv,
    });

    const config = merge(defaultConfig, {
    plugins: [
    new HtmlWebpackPlugin({
    inject: false,
    template: 'src/custom.ejs',
    templateParameters: {
    isLocal: env?.isLocal,
    // additional templateParameters can now be supplied
    orgName,
    environment: myEnv,
    },
    }),
    ],
    });

    // console.dir(config, null, 2) // useful for debugging
    return config;
    };
    - - + + \ No newline at end of file diff --git a/docs/5.x/single-spa-playground/index.html b/docs/5.x/single-spa-playground/index.html index d57882d6a..ca052a3df 100644 --- a/docs/5.x/single-spa-playground/index.html +++ b/docs/5.x/single-spa-playground/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    single-spa-playground

    Playground website

    Github project

    The single-spa playground is an interactive tutorial that lets you test your code live in the browser to verify it can work with single-spa.

    - - + + \ No newline at end of file diff --git a/docs/5.x/ssr-overview/index.html b/docs/5.x/ssr-overview/index.html index cd3907d5a..4d65ab929 100644 --- a/docs/5.x/ssr-overview/index.html +++ b/docs/5.x/ssr-overview/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Server Side Rendering

    Intro to SSR

    In the context of single page applications (SPAs), server-side rendering (SSR) refers to dynamic generation of the HTML page that is sent from web server to browser. In a single page application, the server only generates the very first page that the user requests, leaving all subsequent pages to be rendered by the browser.

    To accomplish server-side rendering of an SPA, javascript code is executed in NodeJS to generate the initial HTML. In the browser, the same javascript code is executed during a "hydration" process, which attaches event listeners to the HTML. Most popular UI Frameworks (Vue, React, Angular, etc) are capable of executing in both NodeJS and the browser, and offer APIs for both generating the server HTML and hydrating it in the browser. Additionally, there are popular frameworks such as NextJS and Nuxt which simplify the developer experience of server-side rendering.

    In the context of microfrontends, server-side rendering refers to assembling the HTML from multiple, separate microfrontends. Each microfrontend controls a fragment of the HTML sent from web server to browser, and hydrate their fragment once initialized in the browser.

    Purpose

    A primary purpose of server-side rendering is improved performance. Server rendered pages often display their content to users faster than their static counterparts, since the user is presented with the content before javascript resources have been initialized. Other reasons for SSR include improved search engine optimization (SEO).

    Server rendered applications are generally harder to build and maintain, since the code has to work on both client and server. Additionally, SSR often complicates the infrastructure needed to run your application, since many SPA + SSR solutions require NodeJS, which is not required in production for client-only SPAs.

    Example

    The isomorphic-microfrontends example shows React server-rendered microfrontends. You can view the live demo of the code at https://isomorphic.microfrontends.app.

    Implementation Overview

    The ultimate goal of server-side rendering is to generate an HTTP response that the browser will display to the user while javascript is hydrating. Most microfrontend server-side rendering implementations, including single-spa's recommended approach, do this with the following steps:

    1. Layout - Identify which microfrontends to render for the incoming HTTP request, and where within the HTML they will be placed. This is usually route based.
    2. Fetch - Begin rendering the HTML for each microfrontend to a stream.
    3. Headers - Retrieve HTTP response headers from each microfrontend. Merge them together and send the result as the HTTP response headers to the browser.
    4. Body - Send the HTTP response body to the browser, which is an HTML document consisting of static and dynamic parts. This involves waiting for each microfrontend's stream to end before proceeding to the next portion of HTML.
    5. Hydrate - Within the browser, download all javascript needed and then hydrate the HTML.

    1. Layout

    To define an HTML template that lays out your page, first choose a "microfrontend layout middleware":

    1. single-spa-layout: The official layout engine for single-spa.
    2. Tailor: A popular, battle tested layout engine that predates single-spa-layout and is not officially affiliated with single-spa.
    3. TailorX: An actively maintained fork of Tailor that is used by Namecheap in their single-spa website. The single-spa core team collaborated with the creators of TailorX when authoring single-spa-layout, taking some inspiration from it.

    We generally recommend single-spa-layout, although choosing one of the other options might make sense for your situation, since single-spa-layout is newer and has been used less than Tailor/TailorX.

    With single-spa-layout, you define a single template that handles all routes. Full documentation.

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Isomorphic Microfrontends</title>
    <meta
    name="importmap-type"
    content="systemjs-importmap"
    server-cookie
    server-only
    />
    <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.0.0/dist/import-map-overrides.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.6.1/dist/system.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.6.1/dist/extras/amd.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.6.1/dist/extras/named-exports.min.js"></script>
    </head>
    <body>
    <template id="single-spa-layout">
    <single-spa-router>
    <nav>
    <application name="@org-name/navbar"></application>
    </nav>
    <main>
    <route path="settings">
    <application name="@org-name/settings"></application>
    </route>
    <route path="home">
    <application name="@org-name/home"></application>
    </route>
    </main>
    </single-spa-router>
    </template>
    <fragment name="importmap"></fragment>
    <script>
    System.import('@org-name/root-config');
    </script>
    <import-map-overrides-full
    show-when-local-storage="devtools"
    dev-libs
    ></import-map-overrides-full>
    </body>
    </html>

    2. Fetch

    Your microfrontend layout middleware (see Layout section) determines which microfrontends match the HTTP request's route. The middleware then fetches the HTTP response headers and HTML content for each microfrontend.

    When using single-spa-layout, fetching each microfrontend is handled by the renderApplication function that is provided to renderServerResponseBody.

    The method of fetching the headers and HTML content can vary, since single-spa-layout allows for any arbitrary, custom method of fetching. However, in practice, there are two popular approaches, which are described below. We generally recommend dynamic module loading as the primary method, since it requires less infrastructure to set up and has arguably (slightly) better performance. However, HTTP requests have some advantages, too, and it's also possible for different microfrontends to be implemented with different fetch methods.

    A. Module loading

    Module loading refers to loading javascript code using import and import(). Using module loading, the implementation of fetching the headers and content for each microfrontend is done purely within a single web server and operating system process:

    import('@org-name/navbar/server.js').then(navbar => {
    const headers = navbar.getResponseHeaders(props);
    const htmlStream = navbar.serverRender(props);
    });

    In the context of single-spa-layout, this is done inside of the renderApplication function:

    import {
    constructServerLayout,
    sendLayoutHTTPResponse,
    } from 'single-spa-layout/server';
    import http from 'http';

    const serverLayout = constructServerLayout({
    filePath: 'server/views/index.html',
    });

    http
    .createServer((req, res) => {
    const { bodyStream } = sendLayoutHTTPResponse({
    res,
    serverLayout,
    urlPath: req.path,
    async renderApplication({ appName, propsPromise }) {
    const [app, props] = await Promise.all([
    import(`${props.name}/server.mjs`, propsPromise),
    ]);
    return app.serverRender(props);
    },
    async retrieveApplicationHeaders({ appName, propsPromise }) {
    const [app, props] = await Promise.all([
    import(`${props.name}/server.mjs`, propsPromise),
    ]);
    return app.getResponseHeaders(props);
    },
    async retrieveProp(propName) {
    return 'prop value';
    },
    assembleFinalHeaders(appHeaders) {
    return Object.assign(
    {},
    ...Object.values(allHeaders).map(a => a.appHeaders),
    );
    },
    renderFragment(name) {
    // not relevant to the docs here
    },
    });

    bodyStream.pipe(res);
    })
    .listen(9000);

    To facilitate independent deployments of our microfrontends, such that the web server does not have to reboot/redeploy every time we update every microfrontend, we can use dynamic module loading. Dynamic module loading refers to loading a module from a dynamic location - often from somewhere on disk or over the network. By default, NodeJS will only load modules from relative URLs or from the node_modules directory, but dynamic module loading allows you to load modules from any arbitrary file path or URL.

    A pattern to facilitate independent deployments via dynamic module loading is for each microfrontend's deployment to upload one or more javascript files to a trusted CDN, and then use dynamic module loading to load a certain version of the code on the CDN. The web server polls for new versions of each microfrontend and downloads the newer versions as they are deployed.

    To accomplish dynamic module loading, we can use NodeJS module loaders. Specifically, @node-loader/import-maps and @node-loader/http allow us to control where the module is located and how to download it over the network. The code below shows how a server-side import map facilitates dynamic module loading

    Before deployment of navbar:

    {
    "imports": {
    "@org-name/navbar/": "https://cdn.example.com/navbar/v1/"
    }
    }

    After deployment of navbar:

    {
    "imports": {
    "@org-name/navbar/": "https://cdn.example.com/navbar/v2/"
    }
    }

    The import map itself is hosted on the CDN, so that deployments may occur without restarting the web server. An example of this setup is shown here.

    B. HTTP Request

    It is also possible to implement the fetching of HTML content and HTTP headers from microfrontends using HTTP requests. In this setup, each microfrontend must run as a deployed web server. The root web server (responsible for responding to the browser) makes an HTTP call to each of the microfrontends' web servers. Each microfrontend web server responds with an HTML page as response body, along with its HTTP response headers. The response body is streamed to the root web server so that it can send the bytes as soon as possible to the browser.

    In the context of single-spa-layout, this is done with the renderApplication function:

    import {
    constructServerLayout,
    sendLayoutHTTPResponse,
    } from 'single-spa-layout/server';
    import http from 'http';
    import fetch from 'node-fetch';

    const serverLayout = constructServerLayout({
    filePath: 'server/views/index.html',
    });

    http
    .createServer((req, res) => {
    const fetchPromises = {};

    sendLayoutHTTPResponse(serverLayout, {
    res,
    serverLayout,
    urlPath: req.path,
    async renderApplication({ appName, propsPromise }) {
    const props = await propsPromise;
    const fetchPromise =
    fetchPromises[appName] ||
    (fetchPromises[appName] = fetchMicrofrontend(props));
    const response = await fetchPromise;
    // r.body is a Readable stream when you use node-fetch,
    // which is best for performance when using single-spa-layout
    return response.body;
    },
    async retrieveApplicationHeaders({ appName, propsPromise }) {
    const props = await propsPromise;
    const fetchPromise =
    fetchPromises[appName] ||
    (fetchPromises[appName] = fetchMicrofrontend(props));
    const response = await fetchPromise;
    return response.headers;
    },
    async retrieveProp(propName) {
    return 'prop value';
    },
    assembleFinalHeaders(allHeaders) {
    return Object.assign({}, ...Object.values(allHeaders));
    },
    renderFragment(name) {
    // not relevant to the docs here
    },
    });

    bodyStream.pipe(res);
    })
    .listen(9000);

    async function fetchMicrofrontend(props) {
    fetch(`http://${props.name}`, {
    headers: props,
    }).then(r => {
    if (r.ok) {
    return r;
    } else {
    throw Error(
    `Received http response ${r.status} from microfrontend ${appName}`,
    );
    }
    });
    }

    3. HTTP Response Headers

    The HTTP response headers sent to the browser are a combination of default headers and the headers retrieved from each microfrontend. Your method of fetching microfrontends does not change how the final headers are merged and assembled for the browser.

    Tailor and TailorX have built-in methods of merging headers. Single-spa-layout allows for custom merging via the assembleFinalHeaders option:

    import {
    constructServerLayout,
    sendLayoutHTTPResponse,
    } from 'single-spa-layout/server';
    import http from 'http';

    const serverLayout = constructServerLayout({
    filePath: 'server/views/index.html',
    });

    http
    .createServer((req, res) => {
    const { bodyStream } = sendLayoutHTTPResponse({
    res,
    serverLayout,
    urlPath: req.path,
    async renderApplication({ appName, propsPromise }) {
    const [app, props] = await Promise.all([
    import(`${props.name}/server.mjs`, propsPromise),
    ]);
    return app.serverRender(props);
    },
    async retrieveApplicationHeaders({ appName, propsPromise }) {
    const [app, props] = await Promise.all([
    import(`${props.name}/server.mjs`, propsPromise),
    ]);
    return app.getResponseHeaders(props);
    },
    async retrieveProp(propName) {
    return 'prop value';
    },
    assembleFinalHeaders(allHeaders) {
    // appHeaders contains all the application names, props, and headers for
    return Object.assign(
    {},
    ...Object.values(allHeaders).map(a => a.appHeaders),
    );
    },
    renderFragment(name) {
    // not relevant to the docs here
    },
    });

    bodyStream.pipe(res);
    })
    .listen(9000);

    4. HTTP Response Body

    The HTTP Response body sent from the web server to the browser must be streamed, byte by byte, in order to maximize performance. NodeJS Readable streams make this possible by acting as a buffer that sends each byte as received, instead of all bytes at once.

    All microfrontend layout middlewares mentioned in this document stream the HTML response body to the browser. In the context of single-spa-layout, this is done by calling sendLayoutHTTPResponse

    import { sendLayoutHTTPResponse } from 'single-spa-layout/server';
    import http from 'http';

    const serverLayout = constructServerLayout({
    filePath: 'server/views/index.html',
    });

    http
    .createServer((req, res) => {
    sendLayoutHTTPResponse({
    res,
    // Add all other needed options here, too
    });
    })
    .listen(9000);

    5. Hydrate

    Hydration (or rehydration) refers to browser Javascript initializing and attaching event listeners to the HTML sent by the server. There are several variants, including progressive rehydration and partial rehydration.

    info

    See also "Rendering on the Web" by Google.

    In the context of microfrontends, hydration is done by the underlying UI framework of the microfrontend (React, Vue, Angular, etc). For example, in React, this is done by calling ReactDOM.hydrate(). The single-spa adapter libraries allow you to specify whether you are hydrating or mounting for the first time (see single-spa-react's renderType option).

    The role of single-spa-layout is to determine which microfrontends should hydrate which parts of the DOM. This is done automatically when you call constructLayoutEngine and singleSpa.start(). If using TailorX instead of single-spa-layout, the Isomorphic Layout Composer Project serves a similar purpose as constructLayoutEngine.

    - - + + \ No newline at end of file diff --git a/docs/5.x/testing/e2e/index.html b/docs/5.x/testing/e2e/index.html index e6b689ef8..ebcc89c9c 100644 --- a/docs/5.x/testing/e2e/index.html +++ b/docs/5.x/testing/e2e/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    E2E testing

    info

    As microfrontends gain widespread adoption, testing tools will catch up and the testing story will improve.

    End to End (E2E) testing a single-spa application, parcel, or utility is very similar to E2E testing in other architectures. Because you are testing in the browser you can even use tools like import-map-overrides to run your tests in a production or production like environment with an override before deploying to that environment.

    In general we suggest only using E2E tests to test integration points between microfrontends and core functionality following principles of either the testing pyramid or the testing trophy.

    Testing Options

    In single-spa, there are more ways to test your code in a browser using tools like cypress. Two common approaches are to test individual applications by using standalone mode and testing everything together, both provide value in different ways.

    "E2E" testing with "standalone" mode

    While not perfect standalone mode offers a way to run individual single-spa applications and can be used to test a single-spa application. If the microfrontend relies upon configuration or initialization happening in your single-spa root-config you cannot test those areas in standalone mode without mocking. Standalone mode works by creating a custom single-spa root-config on the fly that will just render the one application, so the code is the same as if it were running in production but the configuration is different.

    Testing everything together

    Much like E2E tests run in traditional SPA applications you can open a brower and run assertions using tools like Cypress. Taking this approach is mirroring a full end to end test. You are running the exact same code that is in the environment. With some configuration and tools like import-map-overrides you can set-up your testing environment to work with overrides to the import map and can run end-to-end tests before deploying your code changes to an environment.

    Configuring E2E tests to work with overrides

    At a high level you will need to do the following before your environment can utilize overrides in E2E tests

    1. Use a tool like import-map-overrides
    2. Get the built code on a publically accessible domain. Similar to a "review app"
    3. Configure your E2E testing environment to set the override
    4. Run the E2E tests
    - - + + \ No newline at end of file diff --git a/docs/5.x/testing/units/index.html b/docs/5.x/testing/units/index.html index 86d874a42..d9943450a 100644 --- a/docs/5.x/testing/units/index.html +++ b/docs/5.x/testing/units/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 5.x

    Unit testing

    info

    As microfrontends gain widespread adoption, testing tools will catch up and the testing story will improve.

    Unit testing a single-spa application, parcel, or utility is very similar to unit testing in the framework you are using, with two notable exceptions:

    • Importing microfrontends
    • System.import

    In general we recommend following the principle of only testing the units that need to be tested and aren't covered by other tests. Testing library code like single-spa.registerApplication is usually unnecessary because those are covered by the library's unit tests.

    Importing microfrontends

    It is fairly common in microfrontends to have one microfrontend import and rely upon a component from another microfrontend. Reliance on another microfrontend can be challenging to test because unit tests generally run locally and you won't have access to other microfrontends. When this occurs we generally recommend mocking the other microfrontend for the unit test.

    An example of this can be found in the Button component exported by the styleguide in react.microfrontends.app. Because that component is imported and used by the planets application you will need to make it available to the test environment by mocking the dependency. This is necessary because the test environment cannot dynamically import other microfrontends, like the browser can. Given the wide variety of unit testing tools you will need to follow the pattern established by the test environment you are using for mocking other microfrontends.

    note

    We suggest mocks over installing microfrontends for local tests (for example via NPM modules) because mocks are easier to maintain and avoid several potential incompatiblity issues such as version mismatch, module format incompatibility, environment differences, and more.

    System.import

    Occasionally you will choose to interop with another microfrontend asynchronously by explicitly calling System.import. Testing in this scenario may require mocking both SystemJS and the module you're importing. Additionally because System.import returns a promise your tests in that area will need to be asynchronous and wait for promises to resolve.

    An example of this can be found in people and planets applications from react.microfrontends.app. The People application exports a function that resolves with a component. The Planets Application imports and uses that component asynchronously with React.lazy. Testing this component would necessitate mocking both SystemJS and People.

    Shared Mocks

    If each project mocks every other microfrontend it is possible that the mocks will eventually become out of sync with the actual deployed microfrontend. One way to prevent this is to share mocks so that keeping the mocks in sync only requires one change instead of updating mocks in many different repositories.

    Testing examples

    Jest

    In the above examples I showed how People imports a component from styleguide. In order to unit test the component in people with Jest you will need to configure jest and mock the styleguide MFE. In jest configuration is done via multiple areas.

    1. Create a jest config file
    2. Setup a mocks directory at the root
    3. Add a setupFile

    Jest config

    Jest is configured with a configuration file. Below is an example configuration using some of the options. See more options on Jest's official documentation site.

    module.exports = {
    collectCoverageFrom: ['src/**/*.js'],
    modulePathIgnorePatterns: ['/cache', '/dist'],
    transform: {
    '^.+\\.(j|t)sx?$': 'babel-jest',
    },
    setupFilesAfterEnv: ['./jest.setup.js'],
    moduleNameMapper: {
    // Note this is only needed if you don't match the module name directly
    // an alternative would be to place the mock in
    // <rootDir>/__mocks__/@react-mf/styleguide.js and it would be autodetected
    '@react-mf/styleguide': '<rootDir>/__mocks__/styleguide.js',
    },
    }

    mocks directory

    Jest will detect folders named __mocks__ and if the naming convention is exact or the modules have been mapped using moduleNameMapper then Jest will use those mocks in place of an import. This structure is essential for other microfrontends where you don't have the code locally. See more information on jest's official documentation

    .
    ├── __mocks__
    │ └── styleguide.js
    ├── src
    │ ├── react-mf-people.js
    │ └── ...
    ├── node_modules
    ├── jest.setup.js
    ├── ...
    └── jest.config.js

    setup file

    Jest uses a setup file to create globals mocks that can be utilized by every test or otherwise configure the test environment. If you were mocking localStorage or SystemJS this is a good place to configure those mocks. See more use-cases for a set-up file on Jest's offical documentation

    // jest.setup.js
    // import Mocks for SystemJS mock below
    import peopleApplication from '@react-mf/people'
    // Mock SystemJS
    global.System = {
    import: jest.fn(mockImport)
    }

    function mockImport (importName) {
    // What you do in mock import will depend a lot on how you use SystemJS in the project and components you wish to test

    /* If I had already created a mock for `@react-mf/people` and I wanted to test this component:
    * https://github.com/react-microfrontends/planets/blob/main/src/planets-page/selected-planet/selected-planet.component.js#L5
    * I would want `System.import('@react-mf/people')` to resovle to my mock one way to accomplish this would be the following
    */
    if (importName === '@react-mf/people') {
    return Promise.resolve(peopleApplication)
    } else {
    console.warn('No mock module found')
    return Promise.resolve({})
    }
    }

    - - + + \ No newline at end of file diff --git a/docs/5.x/videos/index.html b/docs/5.x/videos/index.html index ac077c1a2..77e5d1e63 100644 --- a/docs/5.x/videos/index.html +++ b/docs/5.x/videos/index.html @@ -14,13 +14,13 @@ - - + +
    - - + + \ No newline at end of file diff --git a/docs/api/index.html b/docs/api/index.html index e4e1dfd4e..3b9a61078 100644 --- a/docs/api/index.html +++ b/docs/api/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ This means importing must happen in one of two ways:

    import { registerApplication, start } from 'single-spa';
    // or
    import * as singleSpa from 'single-spa';

    registerApplication

    registerApplication is the most important API your root config will use. Use this function to register any application within single-spa. Note that if an application is registered from within another application, that no hierarchy will be maintained between the applications.

    There are two ways of registering your application:

    Simple arguments

    singleSpa.registerApplication(
    'appName',
    () => System.import('appName'),
    location => location.pathname.startsWith('appName'),
    );

    arguments

    appName: string
    App name that single-spa will register and reference this application with, and will be labelled with in dev tools.
    applicationOrLoadingFn: () => <Function | Promise>
    Must be a loading function that either returns the resolved application or a promise.
    activityFn: (location) => boolean
    Must be a pure function. The function is called with window.location as the first argument and should return a truthy value whenever the application should be active.
    customProps?: Object | () => Object
    Will be passed to the application during each lifecycle method.

    returns

    undefined

    Configuration object

    singleSpa.registerApplication({
    name: 'appName',
    app: () => System.import('appName'),
    activeWhen: '/appName',
    customProps: {
    authToken: 'xc67f6as87f7s9d'
    }
    })

    singleSpa.registerApplication({
    name: 'appName',
    app: () => System.import('appName'),
    activeWhen: '/appName',
    // Dynamic custom props that can change based on route
    customProps(appName, location) {
    return {
    authToken: 'xc67f6as87f7s9d'
    }
    }
    })

    arguments

    name: string
    App name that single-spa will register and reference this application with, and will be labelled with in dev tools.
    app: Application | () => Application | Promise<Application>
    Application object or a function that returns the resolved application (Promise or not)
    activeWhen: string | (location) => boolean | (string | (location) => boolean)[]
    Can be a path prefix which will match every URL starting with this path, an activity function (as described in the simple arguments) or an array containing both of them. If any of the criteria is true, it will keep the application active. The path prefix also accepts dynamic values (they must start with ':'), as some paths would receive url params and should still trigger your application. Examples:
    '/app1'
    ✅ https://app.com/app1
    ✅ https://app.com/app1/anything/everything
    🚫 https://app.com/app2
    '/users/:userId/profile'
    ✅ https://app.com/users/123/profile
    ✅ https://app.com/users/123/profile/sub-profile/
    🚫 https://app.com/users//profile/sub-profile/
    🚫 https://app.com/users/profile/sub-profile/
    '/pathname/#/hash'
    ✅ https://app.com/pathname/#/hash
    ✅ https://app.com/pathname/#/hash/route/nested
    🚫 https://app.com/pathname#/hash/route/nested
    🚫 https://app.com/pathname#/another-hash
    ['/pathname/#/hash', '/app1']
    ✅ https://app.com/pathname/#/hash/route/nested
    ✅ https://app.com/app1/anything/everything
    🚫 https://app.com/pathname/app1
    🚫 https://app.com/app2
    customProps?: Object | () => Object
    Will be passed to the application during each lifecycle method.

    returns

    undefined

    note

    It is described in detail inside of the Configuration docs

    start

    singleSpa.start();

    // Optionally, you can provide configuration
    singleSpa.start({
    urlRerouteOnly: true,
    });

    Must be called by your single spa config. Before start is called, applications will be loaded, but will never be bootstrapped, mounted or unmounted. The reason for start is to give you control over the performance of your single page application. For example, you may want to declare registered applications immediately (to start downloading the code for the active ones), but not actually mount the registered applications until an initial AJAX request (maybe to get information about the logged in user) has been completed. In that case, the best performance is achieved by calling registerApplication immediately, but calling start after the AJAX request is completed.

    arguments

    The start(opts) api optionally accepts a single opts object, with the following properties. If the opts object is omitted, all defaults will be applied.

    • urlRerouteOnly: A boolean that defaults to true. If set to true, calls to history.pushState() and history.replaceState() will not trigger a single-spa reroute unless the client side route was changed. Setting this to true can be better for performance in some situations. For more information, read original issue.

    returns

    undefined

    triggerAppChange

    singleSpa.triggerAppChange();

    Returns a Promise that will resolve/reject when all apps have mounted/unmounted. This was built for testing single-spa and is likely not needed in a production application.

    arguments

    none

    returns

    Promise
    Returns a Promise that will resolve/reject when all apps have mounted.
    // Three ways of using navigateToUrl
    singleSpa.navigateToUrl('/new-url');
    singleSpa.navigateToUrl(document.querySelector('a'));
    document.querySelector('a').addEventListener(singleSpa.navigateToUrl);
    <!-- A fourth way to use navigateToUrl, this one inside of your HTML -->
    <a href="/new-url" onclick="singleSpaNavigate(event)">My link</a>

    Use this utility function to easily perform url navigation between registered applications without needing to deal with event.preventDefault(), pushState, triggerAppChange(), etc.

    arguments

    navigationObj: string | context | DOMEvent
    navigationObj must be one of the following types:
    • a url string.
    • a context / thisArg that has an href property; useful for calling singleSpaNavigate.call(anchorElement) with a reference to the anchor element or other context.
    • a DOMEvent object for a click event on a DOMElement that has an href attribute; ideal for the <a onclick="singleSpaNavigate"></a> use case.

    returns

    undefined

    getMountedApps

    const mountedAppNames = singleSpa.getMountedApps();
    console.log(mountedAppNames); // ['app1', 'app2', 'navbar']

    arguments

    none

    returns

    appNames: string[]
    Each string is the name of a registered application that is currently MOUNTED.

    getAppNames

    const appNames = singleSpa.getAppNames();
    console.log(appNames); // ['app1', 'app2', 'app3', 'navbar']

    arguments

    none

    returns

    appNames: string[]
    Each string is the name of a registered application regardless of app status.

    getAppStatus

    const status = singleSpa.getAppStatus('app1');
    console.log(status); // one of many statuses (see list below). e.g. MOUNTED

    arguments

    appName: string
    Registered application name.

    returns

    appStatus: <string | null>
    Will be one of the following string constants, or null if the app does not exist.
    NOT_LOADED
    app has been registered with single-spa but has not yet been loaded.
    LOADING_SOURCE_CODE
    app's source code is being fetched.
    NOT_BOOTSTRAPPED
    app has been loaded but not bootstrapped.
    BOOTSTRAPPING
    the bootstrap lifecycle function has been called but has not finished.
    NOT_MOUNTED
    app has been loaded and bootstrapped but not yet mounted.
    MOUNTING
    app is being mounted but not finished.
    MOUNTED
    app is currently active and is mounted to the DOM.
    UNMOUNTING
    app is currently being unmounted but has not finished.
    UNLOADING
    app is currently being unloaded but has not finished.
    SKIP_BECAUSE_BROKEN
    app threw an error during load, bootstrap, mount, or unmount and has been siloed because it is misbehaving and has been skipped. Other apps will continue on normally.
    LOAD_ERROR
    The app's loading function returned a promise that rejected. This is often due to a network error attempting to download the JavaScript bundle for the application. Single-spa will retry loading the application after the user navigates away from the current route and then comes back to it.

    Handling LOAD_ERROR status to retry module

    If a module fails to load (for example, due to network error), single-spa will handle the error but SystemJS will not automatically retry to download the module later. To do so, add a single-spa errorHandler that deletes the module from the SystemJS registry and re-attempt to download the module when System.import() on an application in LOAD_ERROR status is called again.

    singleSpa.addErrorHandler(err => {
    if (singleSpa.getAppStatus(err.appOrParcelName) === singleSpa.LOAD_ERROR) {
    System.delete(System.resolve(err.appOrParcelName));
    }
    });

    unloadApplication

    // Unload the application right now, without waiting for it to naturally unmount.
    singleSpa.unloadApplication('app1');

    // Unload the application only after it naturally unmounts due to a route change.
    singleSpa.unloadApplication('app1', { waitForUnmount: true });

    The purpose of unloading a registered application is to set it back to a NOT_LOADED status, which means that it will be re-bootstrapped the next time it needs to mount. The main use-case for this was to allow for the hot-reloading of entire registered applications, but unloadApplication can be useful whenever you want to re-bootstrap your application.

    Single-spa performs the following steps when unloadApplication is called.

    1. Call the unload lifecyle on the registered application that is being unloaded.
    2. Set the app status to NOT_LOADED
    3. Trigger a reroute, during which single-spa will potentially mount the application that was just unloaded.

    Because a registered application might be mounted when unloadApplication is called, you can specify whether you want to immediately unload or if you want to wait until the application is no longer mounted. This is done with the waitForUnmount option.

    arguments

    appName: string
    Registered application name.
    options?: {waitForUnmount: boolean = false}
    The options must be an object that has a waitForUnmount property. When waitForUnmount is false, single-spa immediately unloads the specified registered application even if the app is currently mounted. If it is true, single-spa will unload the registered application as soon as it is safe to do so (when the app status is not MOUNTED).

    returns

    Promise
    This promise will be resolved when the registered application has been successfully unloaded.

    unregisterApplication

    import { unregisterApplication } from 'single-spa';

    unregisterApplication('app1').then(() => {
    console.log('app1 is now unmounted, unloaded, and no longer registered!');
    });

    The unregisterApplication function will unmount, unload, and unregister an application. Once it is no longer registered, the application will never again be mounted.

    This api was introduced in single-spa@5.8.0. A few notes about this api:

    • Unregistering an application does not delete it from the SystemJS module registry.
    • Unregistering an application does not delete its code or javascript frameworks from browser memory.
    • An alternative to unregistering applications is to perform permission checks inside of the application's activity function. This has a similar effect of preventing the application from ever mounting.

    arguments

    appName: string

    returns

    Promise
    This promise will be resolved when the application has been successfully unregistered.

    checkActivityFunctions

    const appsThatShouldBeActive = singleSpa.checkActivityFunctions();
    console.log(appsThatShouldBeActive); // ['app1']

    const appsForACertainRoute = singleSpa.checkActivityFunctions(new URL('/app2', window.location.href));
    console.log(appsForACertainRoute); // ['app2']

    Will call every app's activity function with url and give you list of which applications should be mounted with that location.

    arguments

    location: Location
    A Location object that will be used instead of window.location when calling every application's activity function to test if they return true.

    returns

    appNames: string[]
    Each string is the name of a registered application that matches the provided url.

    addErrorHandler

    singleSpa.addErrorHandler(err => {
    console.log(err);
    console.log(err.appOrParcelName);
    console.log(singleSpa.getAppStatus(err.appOrParcelName));
    });

    Adds a handler that will be called every time an application throws an error during a lifecycle function or activity function. When there are no error handlers, single-spa throws the error to the window.

    errorHandler: Function(error: Error)
    Must be a function. Will be called with an Error object that additionally has a message and appOrParcelName property.

    returns

    undefined

    removeErrorHandler

    singleSpa.addErrorHandler(handleErr);
    singleSpa.removeErrorHandler(handleErr);

    function handleErr(err) {
    console.log(err);
    }

    Removes the given error handler function.

    arguments

    errorHandler: Function
    Reference to the error handling function.

    returns

    boolean
    true if the error handler was removed, and false if it was not.

    mountRootParcel

    // Synchronous mounting
    const parcel = singleSpa.mountRootParcel(parcelConfig, {
    prop1: 'value1',
    domElement: document.getElementById('a-div'),
    });
    parcel.mountPromise.then(() => {
    console.log('finished mounting the parcel!');
    });

    // Asynchronous mounting. Feel free to use webpack code splits or SystemJS dynamic loading
    const parcel2 = singleSpa.mountRootParcel(() => import('./some-parcel.js'), {
    prop1: 'value1',
    domElement: document.getElementById('a-div'),
    });

    Will create and mount a single-spa parcel.

    Parcels do not automatically unmount

    Unmounting will need to be triggered manually.

    arguments

    parcelConfig: Object or Loading Function
    [parcelConfig](/docs/parcels-api#parcel-configuration)
    parcelProps: Object with a domElement property
    [parcelProps](/docs/parcels-api#parcel-props)

    returns

    Parcel object
    See Parcels API for more detail.

    pathToActiveWhen

    The pathToActiveWhen function converts a string URL path into an activity function. The string path may contain route parameters that single-spa will match any characters to. By default, pathToActiveWhen assumes that the string provided is a prefix; however, this can be altered with the exactMatch parameter.

    This function is used by single-spa when a string is passed into registerApplication as the activeWhen argument.

    Arguments

    1. path (string): The URL prefix that.
    2. exactMatch (boolean, optional, defaults to false, requires single-spa@>=5.9.0): A boolean that controls whether trailing characters after the path should be allowed. When false, trailing characters are allowed. When true, trailing characters are not allowed.

    Return Value

    (url: URL) => boolean

    A function that accepts a URL object as an argument and returns a boolean indicating whether the path matches that URL.

    Examples:

    let activeWhen = singleSpa.pathToActiveWhen('/settings');
    activeWhen(new URL('http://localhost/settings')); // true
    activeWhen(new URL('http://localhost/settings/password')); // true
    activeWhen(new URL('http://localhost/')); // false

    activeWhen = singleSpa.pathToActiveWhen('/users/:id/settings');
    activeWhen(new URL('http://localhost/users/6f7dsdf8g9df8g9dfg/settings')); // true
    activeWhen(new URL('http://localhost/users/1324/settings')); // true
    activeWhen(new URL('http://localhost/users/1324/settings/password')); // true
    activeWhen(new URL('http://localhost/users/1/settings')); // true
    activeWhen(new URL('http://localhost/users/1')); // false
    activeWhen(new URL('http://localhost/settings')); // false
    activeWhen(new URL('http://localhost/')); // false

    activeWhen = singleSpa.pathToActiveWhen('/page#/hash');
    activeWhen(new URL('http://localhost/page#/hash')); // true
    activeWhen(new URL('http://localhost/#/hash')); // false
    activeWhen(new URL('http://localhost/page')); // false

    ensureJQuerySupport

    singleSpa.ensureJQuerySupport(jQuery);

    jQuery uses event delegation so single-spa must monkey-patch each version of jQuery on the page. single-spa will attempt to do this automatically by looking for window.jQuery or window.$. Use this explicit method if multiple versions are included on your page or if jQuery is bound to a different global variable.

    arguments

    jQuery?: JQueryFn = window.jQuery
    A reference to the global variable that jQuery has been bound to.

    returns

    undefined

    setBootstrapMaxTime

    // After three seconds, show a console warning while continuing to wait.
    singleSpa.setBootstrapMaxTime(3000);

    // After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
    singleSpa.setBootstrapMaxTime(3000, true);

    // don't do a console warning for slow lifecycles until 10 seconds have elapsed
    singleSpa.setBootstrapMaxTime(3000, true, 10000);

    Sets the global configuration for bootstrap timeouts.

    arguments

    millis: number
    Number of milliseconds to wait for bootstrap to complete before timing out.
    dieOnTimeout: boolean = false

    If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

    If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

    Each registered application can override this behavior for itself.

    warningMillis: number = 1000
    Number of milliseconds to wait between console warnings that occur before the final timeout.

    returns

    undefined

    setMountMaxTime

    // After three seconds, show a console warning while continuing to wait.
    singleSpa.setMountMaxTime(3000);

    // After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
    singleSpa.setMountMaxTime(3000, true);

    // don't do a console warning for slow lifecycles until 10 seconds have elapsed
    singleSpa.setMountMaxTime(3000, true, 10000);

    Sets the global configuration for mount timeouts.

    arguments

    millis: number
    Number of milliseconds to wait for mount to complete before timing out.
    dieOnTimeout: boolean = false

    If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

    If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

    Each registered application can override this behavior for itself.

    warningMillis: number = 1000
    Number of milliseconds to wait between console warnings that occur before the final timeout.

    returns

    undefined

    setUnmountMaxTime

    // After three seconds, show a console warning while continuing to wait.
    singleSpa.setUnmountMaxTime(3000);

    // After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
    singleSpa.setUnmountMaxTime(3000, true);

    // don't do a console warning for slow lifecycles until 10 seconds have elapsed
    singleSpa.setUnmountMaxTime(3000, true, 10000);

    Sets the global configuration for unmount timeouts.

    arguments

    millis: number
    Number of milliseconds to wait for unmount to complete before timing out.
    dieOnTimeout: boolean = false

    If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

    If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

    Each registered application can override this behavior for itself.

    warningMillis: number = 1000
    Number of milliseconds to wait between console warnings that occur before the final timeout.

    returns

    undefined


    setUnloadMaxTime

    // After three seconds, show a console warning while continuing to wait.
    singleSpa.setUnloadMaxTime(3000);

    // After three seconds, move the application to SKIP_BECAUSE_BROKEN status.
    singleSpa.setUnloadMaxTime(3000, true);

    // don't do a console warning for slow lifecycles until 10 seconds have elapsed
    singleSpa.setUnloadMaxTime(3000, true, 10000);

    Sets the global configuration for unload timeouts.

    arguments

    millis: number
    Number of milliseconds to wait for unload to complete before timing out.
    dieOnTimeout: boolean = false

    If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

    If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

    Each registered application can override this behavior for itself.

    warningMillis: number = 1000
    Number of milliseconds to wait between console warnings that occur before the final timeout.

    returns

    undefined

    Events

    single-spa fires Events to the window as a way for your code to hook into URL transitions.

    PopStateEvent

    single-spa fires PopStateEvent events when it wants to instruct all active applications to re-render. This occurs when one application calls history.pushState, history.replaceState, or triggerAppChange. Single-spa deviates from the browser's default behavior in some cases, as described in this Github issue.

    window.addEventListener('popstate', evt => {
    if (evt.singleSpa) {
    console.log(
    'This event was fired by single-spa to forcibly trigger a re-render',
    );
    console.log(evt.singleSpaTrigger); // pushState | replaceState
    } else {
    console.log('This event was fired by native browser behavior');
    }
    });

    Canceling navigation

    Canceling navigation refers to the URL changing and then immediately changing back to what it was before. This is done before any mounting, unmounting, or loading that would otherwise take place. This can be used in conjunction with Vue router and Angular router's built-in navigation guards that allow for cancelation of a navigation event.

    To cancel a navigation event, listen to the single-spa:before-routing-event event:

    window.addEventListener(
    'single-spa:before-routing-event',
    ({ detail: { oldUrl, newUrl, cancelNavigation } }) => {
    if (
    new URL(oldUrl).pathname === '/route1' &&
    new URL(newUrl).pathname === '/route2'
    ) {
    cancelNavigation();
    }
    },
    );

    The cancelNavigation(val = true) function optionally accepts a single argument. If the argument (val) is falsy, navigation will not be canceled. If the argument is a promise, single-spa will wait for the promise to resolve or reject before canceling navigation. If the promise resolves with a falsy value, navigation will not be canceled.

    When a navigation is canceled, no applications will be mounted, unmounted, loaded, or unloaded. All single-spa routing events will fire for the canceled navigation, but they will each have the navigationIsCanceled property set to true on the event.detail (Details below in Custom Events section).

    Navigation cancelation is sometimes used as a mechanism for preventing users from accessing routes for which they are unauthorized. However, we generally recommend permission checks on each route as the proper way to guard routes, instead of navigation cancelation.

    Custom Events

    single-spa fires a series of custom events whenever it reroutes. A reroute occurs whenever the browser URL changes in any way or a triggerAppChange is called. The custom events are fired to the window. Each custom event has a detail property with the following properties:

    window.addEventListener('single-spa:before-routing-event', evt => {
    const {
    originalEvent,
    newAppStatuses,
    appsByNewStatus,
    totalAppChanges,
    oldUrl,
    newUrl,
    navigationIsCanceled,
    cancelNavigation,
    } = evt.detail;
    console.log(
    'original event that triggered this single-spa event',
    originalEvent,
    ); // PopStateEvent | HashChangeEvent | undefined
    console.log(
    'the new status for all applications after the reroute finishes',
    newAppStatuses,
    ); // { app1: MOUNTED, app2: NOT_MOUNTED }
    console.log(
    'the applications that changed, grouped by their status',
    appsByNewStatus,
    ); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] }
    console.log(
    'number of applications that changed status so far during this reroute',
    totalAppChanges,
    ); // 2
    console.log('the URL before the navigationEvent', oldUrl); // http://localhost:8080/old-route
    console.log('the URL after the navigationEvent', newUrl); // http://localhost:8080/new-route
    console.log('has the navigation been canceled', navigationIsCanceled); // false

    // The cancelNavigation function is only defined in the before-routing-event
    evt.detail.cancelNavigation();
    });

    The following table shows the order in which the custom events are fired during a reroute:

    Event orderEvent NameCondition for firing
    1single-spa:before-app-change or single-spa:before-no-app-changeWill any applications change status?
    2single-spa:before-routing-event
    3single-spa:before-mount-routing-event
    4single-spa:before-first-mountIs this the first time any application is mounting?
    5single-spa:first-mountIs this the first time any application was mounted?
    6single-spa:app-change or single-spa:no-app-changeDid any applications change status?
    7single-spa:routing-event

    before-app-change event

    window.addEventListener('single-spa:before-app-change', evt => {
    console.log('single-spa is about to mount/unmount applications!');
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { app1: MOUNTED }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 1
    });

    A single-spa:before-app-change event is fired before reroutes that will result in at least one application changing status.

    before-no-app-change

    window.addEventListener('single-spa:before-no-app-change', evt => {
    console.log('single-spa is about to do a no-op reroute');
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: [], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 0
    });

    A single-spa:before-no-app-change event is fired before reroutes that will result in zero applications changing status.

    before-routing-event

    window.addEventListener('single-spa:before-routing-event', evt => {
    console.log('single-spa is about to mount/unmount applications!');
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: [], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 0
    });

    A single-spa:before-routing-event event is fired before every routing event occurs, which is after each hashchange, popstate, or triggerAppChange, even if no changes to registered applications were necessary.

    before-mount-routing-event

    window.addEventListener('single-spa:before-mount-routing-event', evt => {
    console.log('single-spa is about to mount/unmount applications!');
    console.log(evt.detail);
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { app1: MOUNTED }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 1
    });

    A single-spa:before-mount-routing-event event is fired after before-routing-event and before routing-event. It is guaranteed to fire after all single-spa applications have been unmounted, but before any new applications have been mounted.

    before-first-mount

    window.addEventListener('single-spa:before-first-mount', () => {
    console.log(
    'single-spa is about to mount the very first application for the first time',
    );
    });

    Before the first of any single-spa applications is mounted, single-spa fires a single-spa:before-first-mount event; therefore it will only be fired once ever. More specifically, it fires after the application is already loaded but before mounting.

    Suggested use case

    remove a loader bar that the user is seeing right before the first app will be mounted.

    first-mount

    window.addEventListener('single-spa:first-mount', () => {
    console.log('single-spa just mounted the very first application');
    });

    After the first of any single-spa applications is mounted, single-spa fires a single-spa:first-mount event; therefore it will only be fired once ever.

    Suggested use case

    log the time it took before the user sees any of the apps mounted.

    app-change event

    window.addEventListener('single-spa:app-change', evt => {
    console.log(
    'A routing event occurred where at least one application was mounted/unmounted',
    );
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { app1: MOUNTED, app2: NOT_MOUNTED }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] }
    console.log(evt.detail.totalAppChanges); // 2
    });

    A single-spa:app-change event is fired every time that one or more apps were loaded, bootstrapped, mounted, unmounted, or unloaded. It is similar to single-spa:routing-event except that it will not fire unless one or more apps were actually loaded, bootstrapped, mounted, or unmounted. A hashchange, popstate, or triggerAppChange that does not result in one of those changes will not cause the event to be fired.

    no-app-change event

    window.addEventListener('single-spa:no-app-change', evt => {
    console.log(
    'A routing event occurred where zero applications were mounted/unmounted',
    );
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: [], NOT_MOUNTED: [] }
    console.log(evt.detail.totalAppChanges); // 0
    });

    When no applications were loaded, bootstrapped, mounted, unmounted, or unloaded, single-spa fires a single-spa:no-app-change event. This is the inverse of the single-spa:app-change event. Only one will be fired for each routing event.

    routing-event

    window.addEventListener('single-spa:routing-event', evt => {
    console.log('single-spa finished mounting/unmounting applications!');
    console.log(evt.detail.originalEvent); // PopStateEvent
    console.log(evt.detail.newAppStatuses); // { app1: MOUNTED, app2: NOT_MOUNTED }
    console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] }
    console.log(evt.detail.totalAppChanges); // 2
    });

    A single-spa:routing-event event is fired every time that a routing event has occurred, which is after each hashchange, popstate, or triggerAppChange, even if no changes to registered applications were necessary; and after single-spa verified that all apps were correctly loaded, bootstrapped, mounted, and unmounted.

    - - + + \ No newline at end of file diff --git a/docs/building-applications/index.html b/docs/building-applications/index.html index de4a4eb4f..aca6d4b11 100644 --- a/docs/building-applications/index.html +++ b/docs/building-applications/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -24,7 +24,7 @@ but will not be bootstrapped, mounted or unmounted.
    info

    Framework-specific helper libraries exist in the single-spa ecosystem to implement these required lifecycle methods. This documentation is helpful for understanding what those helpers are doing, or for implementing your own.

    Lifecycle props

    Lifecycle functions are called with a props argument, which is an object with some guaranteed information and additional custom information.

    function bootstrap(props) {
    const {
    name, // The name of the application
    singleSpa, // The singleSpa instance
    mountParcel, // Function for manually mounting
    customProps, // Additional custom information
    } = props; // Props are given to every lifecycle
    return Promise.resolve();
    }

    Built-in props

    Each lifecycle function is guaranteed to be called with the following props:

    • name: The string name that was registered to single-spa.
    • singleSpa: A reference to the singleSpa instance, itself. This is intended to allow applications and helper libraries to call singleSpa APIs without having to import it. This is useful in situations where there are multiple webpack configs that are not set up to ensure that only one instance of singleSpa is loaded.
    • mountParcel: The mountParcel function.

    Custom props

    In addition to the built-in props that are provided by single-spa, you may optionally specify custom props to be passed to an application. These customProps will be passed into each lifecycle method. The custom props are an object, and you can provide either the object or a function that returns the object. Custom prop functions are called with the application name and current window.location as arguments.

    root-config.js
    singleSpa.registerApplication({
    name: 'app1',
    activeWhen,
    app,
    customProps: { authToken: 'd83jD63UdZ6RS6f70D0' },
    });

    singleSpa.registerApplication({
    name: 'app1',
    activeWhen,
    app,
    customProps: (name, location) => {
    return { authToken: 'd83jD63UdZ6RS6f70D0' };
    },
    });
    app1.js
    export function mount(props) {
    // do something with the common authToken in app1
    console.log(props.authToken);
    return reactLifecycles.mount(props);
    }

    Some use cases could be to:

    • share a common access token with all child apps
    • pass down some initialization information, like the rendering target
    • pass a reference to a common event bus so each app may talk to each other

    Note that when no customProps are provided during registration, props.customProps defaults to an empty object.

    Lifecycle helpers

    Some helper libraries that implement lifecycle functions for ease of use are available for many popular frameworks/libraries. Learn more on the Ecosystem page.

    Load

    When registered applications are being lazily loaded, this refers to when the code for a registered application is fetched from the server and executed. This will happen once the registered application's activity function returns a truthy value for the first time. It is best practice to do as little as possible / nothing at all during load, but instead to wait until the bootstrap lifecycle function to do anything. If you need to do something during load, simply put the code into a registered application's main entry point, but not inside of an exported function. For example:

    console.log("The registered application has been loaded!");

    export async function bootstrap(props) {...}
    export async function mount(props) {...}
    export async function unmount(props) {...}

    Bootstrap

    This lifecycle function will be called once, right before the registered application is mounted for the first time.

    export function bootstrap(props) {
    return Promise.resolve().then(() => {
    // One-time initialization code goes here
    console.log('bootstrapped!');
    });
    }

    Mount

    This lifecycle function will be called whenever the registered application is not mounted, but its activity function returns a truthy value. When called, this function should look at the URL to determine the active route and then create DOM elements, DOM event listeners, etc. to render content to the user. Any subsequent routing events (such as hashchange and popstate) will not trigger more calls to mount, but instead should be handled by the application itself.

    export function mount(props) {
    return Promise.resolve().then(() => {
    // Do framework UI rendering here
    console.log('mounted!');
    });
    }

    Unmount

    This lifecycle function will be called whenever the registered application is mounted, but its activity function returns a falsy value. When called, this function should clean up all DOM elements, DOM event listeners, leaked memory, globals, observable subscriptions, etc. that were created at any point when the registered application was mounted.

    export function unmount(props) {
    return Promise.resolve().then(() => {
    // Do framework UI unrendering here
    console.log('unmounted!');
    });
    }

    Unload

    The unload lifecycle is an optionally implemented lifecycle function. It will be called whenever an application should be unloaded. This will not ever happen unless someone calls the unloadApplication API. If a registered application does not implement the unload lifecycle, then it assumed that unloading the app is a no-op.

    The purpose of the unload lifecycle is to perform logic right before a single-spa application is unloaded. Once the application is unloaded, the application status will be NOT_LOADED and the application will be re-bootstrapped.

    The motivation for unload was to implement the hot-loading of entire registered applications, but it is useful in other scenarios as well when you want to re-bootstrap applications, but perform some logic before applications are re-bootstrapped.

    export function unload(props) {
    return Promise.resolve().then(() => {
    // Hot-reloading implementation goes here
    console.log('unloaded!');
    });
    }

    Timeouts

    By default, registered applications obey the global timeout configuration, but can override that behavior for their specific application. This is done by exporting a timeouts object from the main entry point of the registered application. Example:

    app-1.js
    export function bootstrap(props) {...}
    export function mount(props) {...}
    export function unmount(props) {...}

    export const timeouts = {
    bootstrap: {
    millis: 5000,
    dieOnTimeout: true,
    warningMillis: 2500,
    },
    mount: {
    millis: 5000,
    dieOnTimeout: false,
    warningMillis: 2500,
    },
    unmount: {
    millis: 5000,
    dieOnTimeout: true,
    warningMillis: 2500,
    },
    unload: {
    millis: 5000,
    dieOnTimeout: true,
    warningMillis: 2500,
    },
    };

    Note that millis refers to the number of milliseconds for the final console warning, and warningMillis refers to the number of milliseconds at which a warning will be printed to the console (on an interval) leading up to the final console warning.

    Transitioning between applications

    If you find yourself wanting to add transitions as applications are mounted and unmounted, then you'll probably want to tie into the bootstrap, mount, and unmount lifecycle methods. This single-spa transitions repo is a small proof-of-concept of how you can tie into these lifecycle methods to add transitions as your apps mount and unmount.

    Transitions for pages within a mounted application can be handled entirely by the application itself. For example, using react-transition-group for React-based projects.

    - - + + \ No newline at end of file diff --git a/docs/code-of-conduct/index.html b/docs/code-of-conduct/index.html index 420e7c502..0cb8f1bff 100644 --- a/docs/code-of-conduct/index.html +++ b/docs/code-of-conduct/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Code of Conduct

    This code of conduct outlines our expectations for participants within the single-spa community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community.

    Our open source community strives to:

    • Be friendly and patient.
    • Be welcoming: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
    • Be considerate: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into *account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language.
    • Be respectful: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration *to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one.
    • Be careful in the words that you choose: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other *exclusionary behavior aren’t acceptable. This includes, but is not limited to:
      • Violent threats or language directed against another person.
      • Discriminatory jokes and language.
      • Posting sexually explicit or violent material.
      • Posting (or threatening to post) other people’s personally identifying information (“doxing”).
      • Personal insults, especially those using racist or sexist terms.
      • Unwelcome sexual attention.
      • Advocating for, or encouraging, any of the above behavior.
      • Repeated harassment of others. In general, if someone asks you to stop, then stop.
      • When we disagree, try to understand why: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively.
      • Remember that we’re different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.

    This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter.

    Diversity Statement

    We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong.

    Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities.

    Reporting Issues

    If you experience or witness unacceptable behavior—or have any other concerns—please report it by sending a Twitter DM to https://twitter.com/Single_spa. All reports will be handled with discretion. In your report please include:

    • Your contact information.
    • Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link.
    • Any additional information that may be helpful.

    After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse.

    Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning.

    This Code Of Conduct follows the template established by the TODO Group.

    - - + + \ No newline at end of file diff --git a/docs/configuration/index.html b/docs/configuration/index.html index a367df837..cee8e73b9 100644 --- a/docs/configuration/index.html +++ b/docs/configuration/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -45,7 +45,7 @@ so that they never try to modify the same DOM at the same time.

    The <div> will need an id starting with the prefix single-spa-application: and then your app name. For example, if you had an app called app-name, you'd make a <div> with the id single-spa-application:app-name.

    An example with multiple applications would look like this:

    <div id="single-spa-application:app-name"></div>
    <div id="single-spa-application:other-app"></div>
    - - + + \ No newline at end of file diff --git a/docs/contributing-overview/index.html b/docs/contributing-overview/index.html index cb7e45460..252ef14d8 100644 --- a/docs/contributing-overview/index.html +++ b/docs/contributing-overview/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Contributing to Single-spa

    List of current contributors

    Thanks for checking out single-spa! We're excited to hear and learn from you.

    We've put together the following guidelines to help you figure out where you can best be helpful.

    Table of Contents

    1. Types of contributions we're looking for
    2. Ground rules & expectations
    3. How to contribute
    4. Setting up your environment
    5. Community

    Types of contributions we're looking for

    There are many ways you can directly contribute to the guides (in descending order of need):

    • Examples
    • Helper Libraries (like single-spa-react) for missing frameworks
    • Bug fixes
    • Answering questions in the slack channel
    • new helper packages for frameworks

    Interested in making a contribution? Read on!

    Ground rules & expectations

    Before we get started, here are a few things we expect from you (and that you should expect from others):

    • Be kind and thoughtful in your conversations around this project. We all come from different backgrounds and projects, which means we likely have different perspectives on "how open source is done." Try to listen to others rather than convince them that your way is correct.
    • Please read the single-spa Contributor Code of Conduct. By participating in this project, you agree to abide by its terms.
    • If you open a pull request, please ensure that your contribution passes all tests. If there are test failures, you will need to address them before we can merge your contribution.
    • When adding content, please consider if it is widely valuable. Please don't add references or links to things you or your employer have created as others will do so if they appreciate it.

    How to contribute

    If you'd like to contribute, start by searching through the issues and pull requests to see whether someone else has raised a similar idea or question.

    If you don't see your idea listed, and you think it fits into the goals of this guide, do one of the following:

    • If your contribution is minor, such as a small typo or bug fix, open a pull request.
    • If your contribution is major, such as a new feature, start by opening an issue first. That way, other people can weigh in on the discussion before you do any work.

    Setting up your environment

    Prerequisites

    1. Git
    2. Node: install version 8.4 or greater
    3. Yarn: See Yarn website for installation instructions
    4. A fork of the single-spa repo
    5. A clone of the repo on your local machine

    Installation

    1. cd single-spa to go into the project root
    2. yarn to install single-spa's dependencies

    Create a branch

    1. git checkout master from any folder in your local single-spa repository
    2. git pull origin master to ensure you have the latest main code
    3. git checkout -b the-name-of-my-branch (replacing the-name-of-my-branch with a suitable name) to create a branch

    Test the change

    1. Run yarn test from the project root.

    Push it

    1. git add . && git commit -m "My message" (replacing My message with a commit message, such as Fixed application lifecycles) to stage and commit your changes
    2. git push my-fork-name the-name-of-my-branch
    3. Go to the single-spa repo and you should see recently pushed branches.
    4. Follow GitHub's instructions to submit a new Pull Request.

    Community

    Discussions about single-spa take place on the single-spa repository's Issues and Pull Requests sections. Anybody is welcome to join these conversations. There is also a Slack workspace for regular updates.

    Wherever possible, do not take these conversations to private channels, including contacting the maintainers directly. Keeping communication public means everybody can benefit and learn from the conversation.

    - - + + \ No newline at end of file diff --git a/docs/create-single-spa/index.html b/docs/create-single-spa/index.html index a87046159..c6481f9d5 100644 --- a/docs/create-single-spa/index.html +++ b/docs/create-single-spa/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    create-single-spa

    Single-spa offers a CLI for those who prefer autogenerated and managed configurations for webpack, babel, jest, etc. You do not have to use the CLI in order to use single-spa.

    The CLI is called create-single-spa (Github link). It is primarily intended for the creation of new projects, but may also be useful for migrating existing projects (especially migrating CRA or frameworkless projects).

    Installation and Usage

    If you wish to have create-single-spa globally available, run the following in a terminal

    npm install --global create-single-spa

    # or
    yarn global add create-single-spa

    Then run the following:

    create-single-spa

    Alternatively, you may use create-single-spa without global installation:

    npm init single-spa

    # or
    npx create-single-spa

    # or
    yarn create single-spa

    This will open up a CLI prompt asking you what kind of project you want to create or update.

    CLI arguments

    You may pass arguments to create-single-spa like so:

    # Different ways of doing the same thing
    create-single-spa --framework react
    npm init single-spa --framework react
    npx create-single-spa --framework react
    yarn create single-spa --framework react

    Here are the available CLI options:

    --dir

    You may specify which directory create-single-spa runs in through either of the following ways:

    # Two ways of doing the same thing
    create-single-spa my-dir
    create-single-spa --dir my-dir

    --moduleType

    You can specify which kind of microfrontend you are creating with the --moduleType CLI argument:

    create-single-spa --moduleType root-config
    create-single-spa --moduleType app-parcel
    create-single-spa --moduleType util-module

    --framework

    You can specify which framework you're using with the --framework CLI argument. Note that if you specify a framework that you may omit the --moduleType, as it is inferred to be app-parcel.

    create-single-spa --framework react
    create-single-spa --framework vue
    create-single-spa --framework angular

    --layout

    When generating a root config, the --layout CLI argument indicates that you want to use single-spa-layout in your root config.

    --skipInstall

    This option skips npm/yarn/pnpm installation during project creation.

    Project types

    create-single-spa asks you if you'd like to create a single-spa application, a utility module, or a root-config. All three module types assume that you are using the recommended setup.

    If you select that you'd like to create a single-spa application, you will be prompted for which framework you'd like to choose. React is implemented with premade configurations for babel + webpack + jest. Angular is implemented with Angular CLI and single-spa-angular. Vue is implemented with Vue CLI and vue-cli-plugin-single-spa.

    NPM packages

    Within the create-single-spa repo, there are several NPM packages. The following sections document each package:

    create-single-spa

    Github project

    The core CLI, which invokes generator-single-spa.

    generator-single-spa

    Github project

    A Yeoman generator that prompts the user and then creates files. This is primarily invoked via the create-single-spa CLI, but can also be composed if you'd like to customize it.

    single-spa-web-server-utils

    The single-spa-web-server-utils package is a collection of functions that help when implementing a web server for an index.html file. This package can be used to inline import maps into an HTML, which helps with the performance of your application. Additionally, it can be used to modify a browser import map so that it's suitable for usage in NodeJS for dynamic module loading and server rendering (Dynamic Module Loading and Server Rendering)).

    The web server utils poll the import map from a URL and generate a browserImportMap and nodeImportMap from the response.

    Installation

    npm install --save single-spa-web-server-utils

    # alternatively
    yarn add single-spa-web-server-utils

    getImportMaps

    The getImportMaps function accepts an object parameter and returns a promise that resolves with an object with two import maps: browserImportMap and nodeImportMap. Note that import maps are polled at the specified interval forever until either reset() or clearAllIntervals() is called. Import Maps are stored in memory in a javascript variable that exists outside of the getImportMaps function, so subsequent calls to getImportMaps will all use the same cache.

    const { getImportMaps } = require('single-spa-web-server-utils');
    const http = require('http');
    const ejs = require('ejs');
    const fs = require('fs');
    const path = require('path');

    const htmlTemplate = ejs.compile(
    fs.readFileSync(path.resolve(process.cwd(), 'views/index.html'), 'utf-8'),
    );

    http.createServer((req, res) => {
    getImportMaps({
    // required
    // The URL at which the server
    url: 'https://my-cdn.com/live.importmap',

    // optional - defaults to 30000
    // The ms to wait when polling the import map
    pollInterval: 30000,

    // optional - defaults to false
    // Whether to allow for import-map-overrides via cookies sent in the request.
    // More details about overrides via cookies at
    // https://github.com/joeldenning/import-map-overrides/blob/master/docs/api.md#node
    allowOverrides: true,

    // optional - only needed when allowOverrides is true
    // The IncomingMessage from an http server. This is used to gather
    // cookies for import-map-overrides
    req,

    // optional
    // This allows you to remove entries from the downloaded import map
    // from the returned `nodeImportMap`. This is useful for customizing
    // an import map that is used in the browser so that it can be used
    // for dynamic NodeJS module loading. Each key is a string import specifier.
    // Keys that you return `true` for are preserved in the nodeImportMap.
    nodeKeyFilter(key) {
    return true;
    },
    }).then(({ browserImportMap, nodeImportMap }) => {
    console.log(browserImportMap, nodeImportMap);

    // Example of how to inline a browser import map
    const htmlWithInlinedImportMap = htmlTemplate({
    importMap: browserImportMap,
    });
    res.setResponseHeader('Content-Type', 'text/html');
    res.status(200).send(htmlWithInlinedImportMap);

    // Example of how to apply a NodeJS import map
    // More info at https://github.com/node-loader/node-loader-import-maps
    global.nodeLoader.setImportMapPromise(Promise.resolve(nodeImportMap));
    import('module-in-import-map');
    });
    });

    clearAllIntervals

    This clears all import map polling intervals that were created via setInterval() inside of getImportMaps(). This is useful for tests and for cleaning up memory.

    import { clearAllIntervals } from 'single-spa-web-server-utils';

    clearAllIntervals();

    reset

    This clears all intervals (see clearAllIntervals), and also clears the in-memory cache of all import maps. In other words, after reset() is called, getImportMaps() will always result in a new network request to fetch the import map.

    import { reset } from 'single-spa-web-server-utils';

    reset();
    - - + + \ No newline at end of file diff --git a/docs/devtools/index.html b/docs/devtools/index.html index 551b85302..d639f491e 100644 --- a/docs/devtools/index.html +++ b/docs/devtools/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    single-spa-inspector

    The single-spa-inspector is a Firefox/Chrome devtools extension to provide utilities for helping with single-spa applications. Github project.

    Requires >= single-spa@4.1.

    Note: you can also build and run this locally. See how to contribute.

    Features

    • List all registered applications (mounted at top)
    • Show all application statuses (statii)
    • Force mount and unmount an application
    • Show app overlays (see configuring app overlays to enable this feature)
    • Provides an interface for adding import-map overrides

    Configuring app overlays

    App overlays allow you to hover over a mounted app's name and have an "inspect element" type experience which shows where the app is in the DOM. This is especially useful for when multiple apps are mounted at the same time (e.g. in some places Canopy has upwards of 4 apps mounted on a single page/URL!).

    To add app overlays, find the file where you export your lifecycle functions (e.g. bootstrap, mount, unmount) and add another exported object with the following shape:

    // must be called "devtools"
    export const devtools = {
    overlays: {
    // selectors is required for overlays to work
    selectors: [
    // an array of CSS selector strings, meant to be unique ways to identify the outermost container of your app
    // you can have more than one, for cases like parcels or different containers for differet views
    '#my-app',
    '.some-container .app',
    ],
    // options is optional
    options: {
    // these options allow you some control over how the overlay div looks/behaves
    // the listed values below are the defaults

    width: '100%',
    height: '100%',
    zIndex: 40,
    position: 'absolute',
    top: 0,
    left: 0,
    color: '#000', // the default for this is actually based on the app's name, so it's dynamic. can be a hex or a CSS color name
    background: '#000', // the default for this is actually based on the app's name, so it's dynamic. can be a hex or a CSS color name
    textBlocks: [
    // allows you to add additional text to the overlay. for example, you can add the name of the team/squad that owns this app
    // each string in this array will be in a new div
    // 'blue squad', 'is awesome'
    // turns into:
    // <div>blue squad</div><div>is awesome</div>
    ],
    },
    },
    };

    import-map-overrides

    If your environment uses import-maps, single-spa Inspector provides an interface for adding import-map overrides when utilizing the import-map-overrides library. Once the installation requirements for import-map-overrides are completed, you can add, remove, and refresh the page with your overrides.

    Example of single-spa Inspector extension with import-maps overrides

    - - + + \ No newline at end of file diff --git a/docs/ecosystem-alpinejs/index.html b/docs/ecosystem-alpinejs/index.html index 8add552eb..93eb9ca41 100644 --- a/docs/ecosystem-alpinejs/index.html +++ b/docs/ecosystem-alpinejs/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ single-spa applications or parcels.

    Installation

    npm install --save single-spa-alpinejs

    # or
    yarn add single-spa-alpinejs

    Alternatively, you can use single-spa-alpinejs from a CDN as a global variable:

    <script src="https://cdn.jsdelivr.net/npm/single-spa-alpinejs"></script>

    Note that you might want to lock down the package to a specific version. See here for how to do that.

    Usage

    • There are three ways the you can define AlpineJS components as single-spa applications or parcels.

    1 - Template Only

    • The simplest way where the template contains all the required data and initialization logic (including x-data and x-init) as part of the dom. The template is provided via the options attribute template

    2 - Template with externally defined x-data

    • You could also provide x-data externally and the helper will add it to the component.
      • The x-data can be provided in the following forms (via the options attribute xData)
        • an object
        • a function that returns an object
        • a function that returns a promise which resolves with an object.

    3 - Template with externally defined x-data with x-init

    • You can also provide x-init externally along with the x-data and the helper will add it to the component.

    • The x-init can be provided in the following forms (via the options attribute xInit) and needs to be a function.

    • Please note the xData attribute must be provided otherwise the xInit attribute will be ignored.

    • The sample below references the example from the Alpine Toolbox - Alpine JS and fetch() and demonstrates how you can use the xInit and xData attributes to create an AlpineJS application .

    Usage Examples

    1 - Template Only

    import singleSpaAlpinejs from 'single-spa-alpinejs';

    const alpinejslifecycles = singleSpaAlpinejs({
    template: `
    <div class="rounded overflow-hidden shadow-lg font-sans p-1 m-1"
    x-data="{ open: false }">
    <div class="font-bold p-1">Example for x-show attribute</div>
    <button class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold
    hover:text-white py-2 px-4 border border-blue-500
    hover:border-transparent rounded"
    @click="open = !open">Open/Close</button>
    <div x-show="open" class="text-4xl">
    Hey, I'm open
    </div>
    </div>`,
    });

    export const bootstrap = alpinejslifecycles.bootstrap;
    export const mount = alpinejslifecycles.mount;
    export const unmount = alpinejslifecycles.unmount;

    Via cdn

    Example usage when installed via CDN:

    • The usage is similar and once the library is loaded it will be available globally and accessed via the window object.
    const alpinejsApp = window.singleSpaAlpinejs({
    template: `
    <div class="rounded overflow-hidden shadow-lg font-sans p-1 m-1"
    x-data="{ open: false }">
    <div class="font-bold p-1">Example for x-show attribute</div>
    <button class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold
    hover:text-white py-2 px-4 border border-blue-500
    hover:border-transparent rounded" @click="open = !open">Open/Close</button>
    <div x-show="open" class="text-4xl">
    Hey, I'm open
    </div>
    </div>`,
    });

    singleSpa.registerApplication({
    name: 'name',
    app: alpinejsApp,
    activeWhen: () => true,
    });

    2 - Template with externally defined x-data

    import singleSpaAlpinejs from 'single-spa-alpinejs';

    const alpinejslifecycles = singleSpaAlpinejs({
    template: `
    <div class="rounded overflow-hidden shadow-lg font-sans p-1 m-1">
    <div class="font-bold p-1">Example for x-show attribute</div>
    <button class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold
    hover:text-white py-2 px-4 border border-blue-500
    hover:border-transparent rounded" @click="open = !open">Open/Close</button>
    <div x-show="open" class="text-4xl">
    Hey, I'm open
    </div>
    </div>`,
    xData: { open: false },
    });

    export const bootstrap = alpinejslifecycles.bootstrap;
    export const mount = alpinejslifecycles.mount;
    export const unmount = alpinejslifecycles.unmount;

    3 - Template with externally defined x-data with x-init

    import singleSpaAlpinejs from 'single-spa-alpinejs';

    const appTemplate = `
    <div class="w-full h-full text-gray-800">
    <h1 class="mt-0 mb-3 font-light text-3xl" x-text="title"><!-- title text --></h1>
    <p class="text-xl text-gray-600 font-light mb-4" x-html="intro"><!-- intro text --></p>
    <div class="flex flex-wrap -mx-2 pb-8">
    <!-- begin: user card -->
    <template x-for="user in users" :key="user.id">
    <div class="w-full md:w-1/2 lg:w-1/3 xl:w-1/4 h-auto font-light">
    <div class="flex bg-white rounded-lg shadow-md m-2 border-l-4
    border-white hover:shadow-2xl hover:border-pink-500
    cursor-pointer relative">
    <div class="p-4 pr-6 leading-normal">
    <div class="font-medium text-xl truncate" x-text="user.name"></div>
    <div class="truncate uppercase text-xs text-gray-500 font-semibold
    pb-2 tracking-widest" x-text="user.company.name"></div>
    <div class="" x-text="user.phone"></div>
    <a class="text-blue-600 hover:text-blue-700 mr-4 block"
    x-bind:href="'mailto:' + user.email" x-text="user.email"></a>
    <a class="text-blue-600 hover:text-blue-700 block"
    x-bind:href="'https://' + user.website" x-text="user.website"></a>
    </div>
    </div>
    </div>
    </template>
    <!-- end: user card -->
    </div>
    </div>
    `;

    const appDataFn = ({ title, name }) => ({
    title,
    intro:
    'Implement a simple <code class="text-md text-pink-600">fetch()</code> request to render a list of items using Alpine.js :)',
    users: [],
    open: false,
    name,
    });

    const appXInitFn = (id) => {
    return fetch('https://jsonplaceholder.typicode.com/users')
    .then((response) => response.json())
    .then((data) => (document.querySelector(`#${id}`).__x.$data.users = data));
    };

    const opts = {
    template: appTemplate,
    xData: (data) => appDataFn(data), // pass props to x-data
    xInit: appXInitFn,
    };

    const alpinejslifecycles = singleSpaAlpinejs(opts);

    export const bootstrap = alpinejslifecycles.bootstrap;
    export const mount = alpinejslifecycles.mount;
    export const unmount = alpinejslifecycles.unmount;

    API / Options

    single-spa-html is called with an object that has the following properties:

    • template (required): An HTML string or a function that returns a string. The function will be called with the single-spa custom props. The returned string is injected into the DOM during the single-spa mount lifecycle.
    • domElementGetter (optional): A function that returns the dom element container into which the HTML will be injected. If omitted, a default implementation is provided that wraps the template in a <div> that is appended to document.body.
    • xData (optional): An object or a function or a function that returns a promise.The returned string is injected into the DOM as the x-data attribute during the single-spa mount lifecycle.
    • xInit (optional): A function or a function that returns a promise. The function provided is added to the global scope and the function initiation along with the root dom element id as a parameter is injected into the DOM as the x-init attribute during the single-spa mount lifecycle. Please note the xData attribute must be provided otherwise the xInit attribute will be ignored. The function you provide xInit

    xData and xInit Handling

    • This section covers the details of how xData and xInit option attributes are processed by the single spa helper.

    • Consider the example below

    const appDataFn = () => { open: false, loading: true }
    const appXInitFn = (domId) => {
    console.log('Hello from appXInitFn');
    // domId provides access to the parent dom element where x-data and x-init are defined
    document.querySelector(`#${domId}`).__x.$data.loading = false
    }

    const opts = {
    template: appTemplate, // base template
    xData: (data) => appDataFn(data), // pass props to x-data
    xInit: appXInitFn, // x-Init function
    };

    const alpinejsApp = singleSpaAlpinejs(opts);

    singleSpa.registerApplication({
    name: 'myapp',
    app: alpinejsApp,
    activeWhen: () => true,
    });

    • The helper does the following

      • Adds the template to the dom wrapped in parent dom element with and id that has a prefix of alpine. In this case it will be id='alpine-myapp'
      • Attaches a resolved xData as a string x-data="{ "name": "myapp" ,"open": false }" to the parent dom element.
      • It will make the user defined appXInitFn available globally as an attribute of window.singleSpaAlpineXInit and will be accessible via variable window.singleSpaAlpineXInit.myapp
      • Attaches a resolved xInit as a string that calls the globally defined variable x-init="singleSpaAlpineXInit.myapp('alpine-myapp')" to the parent dom element.
      • Note that this also passes id of the parent dom element which can then be used to access the alpine data elements to update the state as required.

      Special characters in the application names

      • You may have special characters in the application name for example @my/app. See the example below
          singleSpa.registerApplication({
      name: '@my/app',
      app: alpinejsApp,
      activeWhen: () => true,
      });
      • The single spa helper converts these to valid global function names by replacing all the special characters with underscores (_). This does not require any special handling from the user as the helper takes care of this internally

      • In the above case the xInit dom element would look like the following x-init="singleSpaAlpineXInit._my_app('alpine-@my/app')" where the xInit function is available as a global variable _my_app.

    - - + + \ No newline at end of file diff --git a/docs/ecosystem-angular/index.html b/docs/ecosystem-angular/index.html index dde0d11a6..ff9fba7fd 100644 --- a/docs/ecosystem-angular/index.html +++ b/docs/ecosystem-angular/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -111,7 +111,7 @@ asynchronous method that fetches the component.

    Since some versions of webpack use SystemJS under the hood, you'll need to reference the global version.

    // Inside of src/app/app.component.ts
    import { ChangeDetectionStrategy, Component } from '@angular/core';
    import { mountRootParcel } from 'single-spa';

    @Component({
    selector: 'app-root',
    template:
    '<parcel [config]="config" [mountParcel]="mountRootParcel"></parcel>',
    changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class AppComponent {
    async config() {
    return window.System.import('@org/react-component');
    }
    mountRootParcel = mountRootParcel;
    }

    Passing Custom Props

    You can pass any custom props to the parcel by passing an object of props using the customProps attribute.

    For example, if you're rendering a React parcel from an Angular component, you can pass a click handler from Angular into the React parcel:

    // Inside of src/app/ReactWidget/ReactWidget.tsx
    import * as React from 'react';
    import * as ReactDOM from 'react-dom';
    import singleSpaReact from 'single-spa-react';

    const ReactWidget = ({ handleClick }) => (
    <button onClick={handleClick}>Click Me!</button>
    );

    export const parcelConfig = singleSpaReact({
    React,
    ReactDOM,
    rootComponent: ReactWidget,
    });

    You can pass a function (or any other value) as a custom prop. To ensure that the functions you pass to the parcel are bound with the correct javascript context, use the handleClick = () => { syntax when defining your functions.

    // Inside of src/app/app.component.ts
    import { ChangeDetectionStrategy, Component } from '@angular/core';
    import { mountRootParcel } from 'single-spa';

    import { config } from './ReactWidget/ReactWidget';

    @Component({
    selector: 'app-root',
    template:
    '<parcel [config]="config" [customProps]="parcelProps" [mountParcel]="mountRootParcel"></parcel>',
    changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class AppComponent {
    config = config;
    mountRootParcel = mountRootParcel;

    handleClick = () => {
    alert('Hello World');
    };

    parcelProps = {
    handleClick = this.handleClick,
    };
    }

    Zone-less applications

    info

    This feature is available starting from single-spa-angular@4.1.

    It's possible to develop Angular applications that don't rely on zone.js library, these applications are called zone-less. You have to run change detection manually in zone-less applications through ApplicationRef.tick() or ChangeDetectorRef.detectChanges(). You can find more info in Angular NoopZone docs.

    The point is that you do not need to load zone.js library in your root HTML file. As Angular docs mention that you should have a comprehensive knowledge of change detection to develop such applications. Let's start by nooping zone when bootstrapping module:

    import { singleSpaAngular } from 'single-spa-angular';

    const lifecycles = singleSpaAngular({
    bootstrapFunction: () =>
    platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' }),
    template: '<app-root />',
    NgZone: 'noop',
    });
    caution

    We must specify noop twice: when bootstrapping AppModule, and setting NgZone property to noop. This tells Angular and single-spa-angular that we're not going to use zones.

    Routing in zone-less applications

    Since routing is managed by single-spa and there is no zone that tells Angular that some asynchronous event has occured, then we need to tell Angular when to run change detection if routing occurs. Let's look at the below code:

    import { ApplicationRef } from '@angular/core';
    import { Router, NavigationStart } from '@angular/router';
    import { singleSpaAngular } from 'single-spa-angular';

    const lifecycles = singleSpaAngular({
    bootstrapFunction: async () => {
    const ngModuleRef = await platformBrowserDynamic().bootstrapModule(
    AppModule,
    { ngZone: 'noop' },
    );

    const appRef = ngModuleRef.injector.get(ApplicationRef);
    const listener = () => appRef.tick();
    window.addEventListener('popstate', listener);

    ngModuleRef.onDestroy(() => {
    window.removeEventListener('popstate', listener);
    });

    return ngModuleRef;
    },
    template: '<app-root />',
    NgZone: 'noop',
    Router,
    NavigationStart,
    });
    caution

    single-spa-angular@4.x requires calling getSingleSpaExtraProviders function in applications that have routing. Do not call this function in zone-less application.

    Inter-app communication via RxJS

    First of all, check out this Inter-app communication guide.

    It's possible to setup a communication between microfrontends via RxJS using cross microfrontend imports.

    We can not create complex abstractions, but simply export the Subject:

    // Inside of @org/api
    import { ReplaySubject } from 'rxjs';
    import { User } from '@org/models';

    // `1` means that we want to buffer the last emitted value
    export const userSubject$ = new ReplaySubject<User>(1);

    And then you just need to import this Subject into the microfrontend application:

    // Inside of @org/app1 single-spa application
    import { userSubject$ } from '@org/api';
    import { User } from '@org/models';

    userSubject$.subscribe(user => {
    // ...
    });

    userSubject$.next(newUser);

    Also, you should remember that @org/api should be an "isolated" dependency, for example the Nrwl Nx library, where each library is in the "libs" folder and you import it via TypeScript paths.

    Every application that uses this library should add it to its Webpack config as an external dependency:

    const singleSpaAngularWebpack = require('single-spa-angular/lib/webpack')
    .default;

    module.exports = (config, options) => {
    const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options);
    singleSpaWebpackConfig.externals = [/^@org\/api$/];
    return singleSpaWebpackConfig;
    };

    But this library should be part of root application bundle and shared with import maps, for example:

    {
    "imports": {
    "@org/api": "http://localhost:8080/api.js"
    }
    }
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-angularjs/index.html b/docs/ecosystem-angularjs/index.html index 78ae4de0e..244879629 100644 --- a/docs/ecosystem-angularjs/index.html +++ b/docs/ecosystem-angularjs/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -26,7 +26,7 @@ loading function should be System.import('name-of-app'). Make sure to add name-of-app to your import map.

    As a global variable

    // note that "js" is not capitalized in the name of the global variable.
    window.myAngularApp = window.singleSpaAngularjs.default({
    angular: angular,
    mainAngularModule: 'app',
    uiRouter: true,
    preserveGlobal: false,
    template: '<my-component />',
    })

    Your loading function should just be the global variable itself. For example:

    singleSpa.registerApplication({
    name: 'my-angular-app',
    app: myAngularApp,
    activeWhen: () => true
    });

    Options

    All options are passed to single-spa-angularjs via the opts parameter when calling singleSpaAngularJS(opts). The following options are available:

    • angular: (required) The main angular object, which is generally either exposed onto the window or is available via require('angular') or import angular from 'angular'.
    • domElementGetter: (optional) A function that takes in the props parameter and returns a DOMElement. This dom element is where the angular application will be bootstrapped, mounted, and unmounted. If not provided, the default is to create a div and append it to document.body.
    • mainAngularModule: (required) A string that is the name of the angular module that will be bootstrapped by angular. See angular docs for angular.bootstrap().
    • uiRouter: (optional) If you are using angular-ui-router, set this option to either true or to a string value. The string value will be the value of the ui-view HTML attribute. For example, uiRouter: 'core' will be <div ui-view="core" /> whereas uiRouter: true turns into <div ui-view />.
    • ngRoute: (optional) If you are using ngRoute, set this option to true to have an <ng-view> element automatically injected into the DOM during mount.
    • preserveGlobal: (optional) A boolean that defaults to false. Set if you want to keep angular on the global even after an app unmounts.
    • elementId: (optional) A string which will be used to identify the element appended to the DOM and bootstrapped by Angular.
    • strictDi: (optional - part of the bootstrap config object) A boolean that defaults to false. Set if you want to enable StrictDi mode
    • template: (optional) An HTML string that will be inserted into the DOM when the app is mounted. The template goes inside of the element returned by domElementGetter. If not provided, no template will be inserted. When using angular-ui-router, you often do not need to use this since ui-router will be putting a template onto the dom for you.

    Custom Props

    single-spa custom props are made available as $rootScope.singleSpaProps. In templates, you can access custom props via $root.singleSpaProps. For example:

    <div>{{ $root.singleSpaProps.token }}</div>

    Parcels

    Creating AngularJS parcels

    The singleSpaAngularJs() function returns an object that can serve as either a single-spa application or single-spa parcel.

    Rendering parcels in AngularJS

    To render a single-spa parcel inside of your AngularJS application, you can use the <single-spa-parcel> directive. To do so, first add the "single-spa-angularjs" module as a dependency of your application:

    import 'single-spa-angularjs/lib/parcel.js';

    angular.module('myMainModule', [
    'single-spa-angularjs'
    ])

    Then you can use the <single-spa-parcel> directive in your templates:

    <single-spa-parcel
    parcel-config="parcelConfig"
    props="parcelProps"
    mount-parcel="mountRootParcel"
    />

    In your controller, set the corresponding values on the $scope:

    import { mountRootParcel } from 'single-spa';

    // The parcelConfig binding is required. It must be an object or loading function that resolves with an object.
    $scope.parcelConfig = {async mount() {}, async unmount() {}}

    // You can retrieve parcels from other microfrontends via cross-microfrontend imports
    // See https://single-spa.js.org/docs/recommended-setup#cross-microfrontend-imports
    // $scope.parcelConfig = () => System.import('@org/settings-modal');

    // The props binding is optional, defaulting to no custom props being passed into the parcel
    $scope.props = {
    extra: 'info can be passed here'
    }

    // As long as you're using <single-spa-parcel> inside of another single-spa application or parcel,
    // the mountParcel binding is not needed. However, it is needed otherwise.
    $scope.mountParcel = mountRootParcel

    If you run into issues related to singleSpaProps not being available for injection, this is likely caused by using <single-spa-parcel> outside of a single-spa application or parcel. It is okay to do so, but you'll need to manually provide the singleSpaProps value:

    import { mountRootParcel } from 'single-spa';

    angular.module('single-spa-angularjs').config(['$provide', ($provide) => {
    // This can be an empty object, you just need the DI to not fail
    const props = {};

    // Alternatively, you can provide a mountParcel function that will be used as the default value for the mount-parcel attribute
    // const props = {mountParcel: mountRootParcel}

    $provide.value('singleSpaProps', props);
    }])

    Migrating

    Migrating an existing AngularJS application to single-spa can be a tricky. Here are some recommendations.

    High level approach

    1. Convert the angularjs application to be a single-spa application via global variables.
    2. Switch the angularjs application from being a global variable to being SystemJS in-browser module.
    3. Add a new single-spa application (doesn't need to be angularjs)

    Step 1: Convert to global variable

    1. Load single-spa and single-spa-angularjs as global variables in your main HTML file:
    <!--
    Consider upgrading the versions of these libraries
    They likely have had updates since this documentation was written
    -->
    <script src="https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/umd/single-spa.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/single-spa-angularjs@4.2.1/lib/single-spa-angularjs.min.js"></script>
    1. Change your angularjs application to not mount to the DOM. This is generally done removing the ng-app attribute in your main html file.
    2. In one of the first / main scripts loaded for your angularjs application, create your single-spa application as a global variable. See this code.
    3. In your main HTML file, add the following:
    <script>
    window.singleSpa.registerApplication({
    name: "legacyAngularjsApp",
    app: window.legacyAngularjsApp,
    activeWhen: ['/']
    })
    window.singleSpa.start();
    </script>
    1. Confirm that your application now is mounting again and works properly. Also, check that it's in MOUNTED status as a single-spa microfrontend:
    // in the browser console, check that it's in `MOUNTED` status
    console.log('legacyAngularjsApp status', singleSpa.getAppStatus('legacyAngularjsApp'));

    Step 2: Convert to SystemJS module:

    This step is not required unless you want to do cross microfrontend imports between your angularjs microfrontend and other microfrontends.

    1. Add systemjs to your index.html file:
    <!-- consider checking/upgrading systemjs version -->
    <script type="systemjs-importmap">
    {
    "imports": {
    "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/system/single-spa.min.js",
    "@org/legacyAngularjsApp": "/main-angular-app.js"
    }
    }
    </script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.10.2/dist/system.min.js"></script>
    1. Remove the global single-spa script:
    - <script src="https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/umd/single-spa.min.js"></script>
    1. Modify your main / first angularjs script file to create a systemjs module instead of global variable. See this code.

    2. Remove the <script> for loading that main / first angularjs script file. Replace it with a System.import.

    - <script src="/main-angular-app.js"></script>
    1. Modify the <script> in your main HTML file to load the angularjs app as a systemjs module instead of global variable:
     window.singleSpa.registerApplication({
    name: "legacyAngularjsApp",
    - app: window.legacyAngularjsApp,
    + app: function() { return System.import('@org/legacyAngularjsApp'); },
    activeWhen: ['/']
    })
    1. Verify that the app continues working.

    Step 3: Add new microfrontend

    In single-spa, it's encouraged to split microfrontends by route. During the migration/transition period, you may need to have the legacy angularjs application always active to show navigation menus, even for routes that are controlled by new microfrontends.

    It's recommended to create new microfrontends via the single-spa CLI.

    1. Add a new call to registerApplication() to your index.html file.
    window.singleSpa.registerApplication({
    name: "new-microfrontend",
    app: function () { return System.import("new-microfrontend"); },
    activeWen: ["/route-for-new-microfrontend"]
    })
    1. Add the new microfrontend to your import map in the index.html file.
    <script type="systemjs-importmap">
    {
    "imports": {
    "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/system/single-spa.min.js",
    "@org/legacyAngularjsApp": "/main-angular-app.js",
    "@org/new-microfrontend": "http://localhost:8080/new-microfrontend.js"
    }
    }
    </script>
    1. Start a new microfrontend on the port in the import map. Go to the route for the new microfrontend and verify it is loaded.

    Examples

    - - + + \ No newline at end of file diff --git a/docs/ecosystem-backbone/index.html b/docs/ecosystem-backbone/index.html index 8e16fe0bc..31fde1b52 100644 --- a/docs/ecosystem-backbone/index.html +++ b/docs/ecosystem-backbone/index.html @@ -14,15 +14,15 @@ - - + +
    Version: 6.x

    single-spa-backbone

    A single-spa helper library which provides lifecycle events for building single-spa applications using Backbone.

    npm Package License

    There are mostly three styles of creating backbone applications

    1. Using RequireJS which will loads the application and all it's dependencies, including the templates loaded using Handlebars, RequireJS:Text or any other engine.

      If your application is written using this style, then you will have to pass the AppWithRequire parameter as options in the plugin, and choose the flavour to load the app, either through data-main attribute or without it.

    2. Using Backbone and ApplicationPath (Entry point of application) directly as script elements and optionally loading other dependencies.

    3. Loading a single application bundle which includes application dependencies like i.e. Backbone, Require, Underscore, Jquery etc.

    NPM package

    npm install --save @emtecinc/single-spa-backbone

    The npm package can be found here.

    Quickstart

    Option 1: Using RequireJS with data-main

    First, in the single-spa application, run npm install --save @emtec/single-spa-backbone. Then, create an entry file for application like below, assuming the application has some BasePath with AppPath and `RequireJsPath' being relative to the base path.

    import singleSpaBackbone from '@emtecinc/single-spa-backbone';

    const backBoneLifecycles = singleSpaBackbone({
    BasePath: 'appBasePath',
    AppWithRequire:
    {
    IsDataMain: true,
    AppPath: 'src/app',
    RequireJsPath: 'lib/require.js'
    },
    DomElementSetter: domElementSetter
    });

    export const bootstrap = [
    backBoneLifecycles.bootstrap,
    ];

    export const mount = [
    backBoneLifecycles.mount,
    ];

    export const unmount = [
    backBoneLifecycles.unmount,
    ];


    function domElementSetter() {

    //use the same element id to render into, in the backbone app
    let el = document.getElementById('shell-container');
    if (!el) {
    el = document.createElement('div');
    el.id = 'shell-container';
    document.body.appendChild(el);
    }

    }

    DomElementSetter gives you a provision to hook up your callback, and can be used to create a container element in the dom which will be used to load the app.

    Please note that this hook up is just a setter and don't expect you to return a value. You need to explicitly use the same element #id in the backbone application to use it as app region or container.

    Option 2: Using RequireJS without data-main

    IsDataMain will be set to false in this case

    import singleSpaBackbone from '@emtecinc/single-spa-backbone';

    const backBoneLifecycles = singleSpaBackbone({
    BasePath: 'appBasePath',
    AppWithBackboneJs:
    {
    AppPath: 'src/app',
    BackboneJsPath: 'lib/backbone.js'
    },
    DomElementSetter: domElementSetter
    });

    export const bootstrap = backBoneLifecycles.bootstrap;

    export const mount = backBoneLifecycles.mount;

    export const unmount = backBoneLifecycles.unmount;

    function domElementSetter() {

    //use the same element id to render into, in the backbone app
    let el = document.getElementById('shell-container');
    if (!el) {
    el = document.createElement('div');
    el.id = 'shell-container';
    document.body.appendChild(el);
    }

    }

    Option 3: Load Backbone app using production build

    import singleSpaBackbone from '@emtecinc/single-spa-backbone';

    const backBoneLifecycles = singleSpaBackbone({
    BasePath: 'appBasePath',
    App:
    {
    AppPath: 'src/app'
    },
    DomElementSetter: domElementSetter
    });

    export const bootstrap = backBoneLifecycles.bootstrap;

    export const mount = backBoneLifecycles.mount;

    export const unmount = backBoneLifecycles.unmount;


    function domElementSetter() {

    //use the same element id to render into, in the backbone app
    let el = document.getElementById('shell-container');
    if (!el) {
    el = document.createElement('div');
    el.id = 'shell-container';
    document.body.appendChild(el);
    }

    }

    Options

    All options are passed to single-spa-backbone via the userOptions parameter when calling singleSpaBackbone(userOptions). The following properties are available:

    • BasePath (required) : The base path of the backbone application. Mostly it will be the public path from where the app is server and other paths will be relative to this. This parameter expects a string type. optional

    • AppWithRequire (required) : This parameter takes an object and expects below properties:

      • IsDataMain (optional) : This parameter takes a boolean value and is used to specify whether require js will use data-main to load the app.
      • AppPath (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is entry point of your application and will be booted up using RequireJs. The path is relative to BasePath.
      • RequireJsPath (required) : This parameter takes a string value and takes the path of the RequireJs file and is relative to BasePath.
      • DependenciesJsPaths (optional) : This is an optional parameter takes an array of strings. It can be used to optionally provide a list of JavaScript paths which you want to load in the browser.
    • AppWithBackboneJs (optional) : This parameter takes an object and expects below properties:

      • AppPath (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is entry point of your application and will be booted up using Backbone Js. The path is relative to BasePath.
      • BackboneJsPath (required) : This parameter takes a string value and takes the path of the Backbone Js file and is relative to BasePath.
      • DependenciesJsPaths (optional) : This is an optional parameter takes an array of strings. It can be used to optionally provide a list of JavaScript paths which you want to load in the browser.
    • App (optional) : This parameter takes an object and expects below properties:

      • AppPath (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is the production build of your backbone application. The path is relative to BasePath.

    Note : Out of AppWithRequire, AppWithBackboneJs and App only one is required

    • DomElementSetter (optional) : This is an optional parameter and can be mostly used to create a dom element, whose id can be later used in the backbone app to load the application. However, you can freely use this callback for any other purpose. It is called before anything else.
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-css/index.html b/docs/ecosystem-css/index.html index 12bb85e61..c7a4fa5ca 100644 --- a/docs/ecosystem-css/index.html +++ b/docs/ecosystem-css/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ With single-spa, each application is wrapped in a <div id="single-spa-application:@org-name/project-name"></div>, which can be used as a prefix to all CSS classes and selectors. Run CSS.escape("single-spa-application:@org-name/project-name") in the browser console to make sure the HTML id is escaped, then prefix it with # so that it matches the id. The resulting string is what you pass into postcss-prefix-selector.

    The example code above in the Scoped CSS section shows the mechanics of how selector prefixing can accomplish scoping, and postcss-prefix-selector can do this automatically to all of your CSS. Below is an example PostCSS configuration file:

    // postcss.config.js
    const prefixer = require('postcss-prefix-selector');

    module.exports = {
    plugins: [
    prefixer({
    prefix: "#single-spa-application\\:\\@org-name\\/project-name"
    })
    ]
    }

    Shadow DOM

    Shadow DOM is a browser API for scoping CSS. It is designed to be used by Web Components, and is mentioned here as another viable option for scoping CSS.

    Below are some notes about Shadow DOM that may be relevant to microfrontends:

    • Shadow DOM prevents any global CSS from cascading into the Shadow Root, which means you can't easily have global, shared CSS.
    • CSS custom properties from outside the Shadow Root can be used within the Shadow Root.
    • The HTML elements within the Shadow DOM are not reachable by CSS selectors outside of the Shadow Root.
    • Events that propagate from a Shadow Root are retargeted at each shadow boundary.

    Lazy Loading

    "Loading" CSS refers to downloading the CSS by inserting a <link rel="stylesheet" href="/my-file.css"> element into the DOM, or by downloading a Javascript file that inserts a <style></style> element into the DOM.

    "Lazy Loading" refers to only inserting the <link> or <style> elements into the DOM once they are needed, instead of all at once. In single-spa, this is during the load or mount lifecycle functions.

    Each microfrontend should only load its CSS into the DOM after its Javascript is downloaded. Single-spa lazy loads the Javascript for each microfrontend, by default; therefore, the CSS for the microfrontends will only be loaded as needed.

    Unmounting CSS

    In large systems with dozens of microfrontends, it can become important for performance to unmount CSS as you navigate between pages. This is accomplished by removing <style> and <link> elements from the DOM.

    By default, most tooling will load and mount the CSS one time and leave it there indefinitely (it never unmounts!). However, some resources exist for unmounting CSS that is no longer being used, and remounting it once it's needed again.

    To accomplish this, single-spa applications and parcels should remove <link> and <style> elements inside of their unmount lifecycle function:

    // This code is an example of the mechanics of mounting + unmounting + remounting CSS.
    // In practice, this is often done via tools like css-loader, style-loader, or
    // single-spa-css (rather than manually).
    const style = document.createElement('style');
    style.textContent = `.settings {color: blue;}`;

    export const mount = [
    async () => {
    document.head.appendChild(styleElement);
    },
    reactLifecycles.mount,
    ]

    export const unmount = [
    reactLifecycles.unmount,
    async () => {
    styleElement.remove();
    }
    ]

    To help you accomplish this, this single-spa-css library implements mount and unmount functions for you.

    SASS, PostCSS, Less, Stylus, etc

    SASS, PostCSS, Less, Stylus, and other CSS build tools are all compatible with single-spa and microfrontends.

    These tools run at build-time to produce vanilla CSS files. All of the documentation on this page applies to the output CSS files created by SASS and other CSS preprocessors.

    Since each microfrontend has its own build, this means that there are multiple SASS (or other preprocessor) builds occurring - one per microfrontend. As a result, SASS variables are not shareable via cross microfrontend imports, since cross microfrontend imports occur at runtime. Instead, to share SASS variables, you'll need to publish them to an NPM registry and install them individually into each microfrontend. Since npm packages are not independently deployable (separately from the packages that use them), changes to the variables will need to be updated and deployed in each microfrontend individually. One thing to note is that the browser's implementation of CSS custom properties occurs at runtime and so native CSS custom properties are inherently shareable between microfrontends.

    SASS and other build tools often produce global CSS rather than scoped CSS. This behavior can be undesirable in a microfrontends architecture because it can result in CSS class name collisions between your microfrontends. To avoid this, you can use SASS modules (or similar) to scope the CSS.

    Webpack CSS resources

    Below is a list of commonly used Webpack loaders and plugins that can help with loading CSS:

    • css-loader facilitates using CSS Modules and properly handling @import() within CSS files.
    • style-loader facilitates mounting CSS via <style> elements. This is often used in development mode, but not production.
    • postcss-loader is similar to CSS modules, but for more advanced use cases that require PostCSS.
    • sass-loader can be used to compile SASS to CSS.
    • single-spa-css can be used to automatically detect which CSS files to load during the mount lifecycle function of your single-spa application or parcel.

    single-spa-css

    The single-spa-css npm package implements helper functions for loading, mounting, and unmounting CSS. It does this by adding <link rel="stylesheet"> elements to the DOM to mount the CSS, and removing the <link> from the DOM when it's time to unmount the CSS.

    Installation

    npm install single-spa-css

    pnpm install single-spa-css

    yarn add single-spa-css

    Usage

    import singleSpaCss from 'single-spa-css';

    const cssLifecycles = singleSpaCss({
    // required: a list of CSS URLs to load
    // can be omitted if webpackExtractedCss is set to true, do not specify Webpack extracted css files here
    cssUrls: ['https://example.com/main.css'],

    // optional: defaults to false. This controls whether extracted CSS files from Webpack
    // will automatically be loaded. This requires using the ExposeRuntimeCssAssetsPlugin,
    // which is documented below.
    webpackExtractedCss: false,

    // optional: defaults to true. Indicates whether the <link> element for the CSS will be
    // unmounted when the single-spa microfrontend is unmounted.
    shouldUnmount: true,

    // optional: defaults to 5000. The number of milliseconds to wait on the <link> to load
    // before failing the mount lifecycle.
    timeout: 5000,

    // optional: defaults to a standard <link rel="stylesheet" href="/main.css"> element
    // Customize the creation of the link element that is used by single-spa-css by providing a
    // function. For example, for setting the cross-origin or other HTML attributes on the <link>
    createLink(url) {
    const linkEl = document.createElement('link');
    linkEl.rel = 'stylesheet';
    linkEl.href = url;
    return linkEl;
    },
    });

    const reactLifecycles = singleSpaReact({...})

    // Export an array of lifecycles to integrate with a framework's
    // single-spa lifecycles. The order matters.
    export const bootstrap = [
    cssLifecycles.bootstrap,
    reactLifecycles.bootstrap
    ]

    export const mount = [
    // The CSS lifecycles should be before your framework's mount lifecycle,
    // to avoid a Flash of Unstyled Content (FOUC)
    cssLifecycles.mount,
    reactLifecycles.mount
    ]

    export const unmount = [
    // The CSS lifecycles should be after your framework's unmount lifecycle,
    // to avoid a Flash of Unstyled Content (FOUC)
    reactLifecycles.unmount,
    cssLifecycles.unmount
    ]

    If you want some CSS files to unmount, but others to stay mounted, use the following syntax:

    const cssLifecycles = singleSpaCss({
    cssUrls: [
    {
    href: "https://example.com/main.css",
    shouldUnmount: true,
    },
    {
    href: "https://example.com/other.css",
    shouldUnmount: false,
    },
    ],
    });

    Webpack Plugin

    This plugin currently only supports webpack 5. See issue 7 to track webpack 4 support.

    single-spa-css includes a Webpack plugin that integrates with mini-css-extract-plugin, which allows you to load CSS files that are extracted and otherwise would not be loaded. The Webpack plugin exposes the names of the extracted CSS files to your bundle under the __webpack_require__.cssAssets and __webpack_require__.cssAssetFileName variables. The cssAssets variable contains the name of the Webpack chunk, and the cssAssetFileName function converts the chunk name into the extracted CSS asset's file name. These can be used manually, or you can specify the webpackExtractedCss option in single-spa-css to have it automatically mount and unmount those CSS files.

    Usage

    In your Webpack config, add the following:

    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    const ExposeRuntimeCssAssetsPlugin = require("single-spa-css/ExposeRuntimeCssAssetsPlugin.cjs");

    module.exports = {
    plugins: [
    new MiniCssExtractPlugin({
    filename: "[name].css",
    }),
    new ExposeRuntimeCssAssetsPlugin({
    // The filename here must match the filename for the MiniCssExtractPlugin
    filename: "[name].css",
    }),
    ],
    };
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-cycle/index.html b/docs/ecosystem-cycle/index.html index ac25749a4..1857ced1e 100644 --- a/docs/ecosystem-cycle/index.html +++ b/docs/ecosystem-cycle/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    single-spa-cycle

    single-spa-cycle is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Cycle.js. Check out the single-spa-cycle github.

    Installation

    npm install --save @pcmnac/single-spa-cycle

    Quickstart

    In your project's entry file, add the following:


    import {run} from '@cycle/run'
    import {makeDOMDriver} from '@cycle/dom'
    import singleSpaCycle from '@pcmnac/single-spa-cycle';
    import rootComponent from './root.component.js';

    const cycleLifecycles = singleSpaCycle({
    run,
    rootComponent,
    drivers: { DOM: makeDOMDriver(document.getElementById('main-content'))}, // or { DOM: makeDOMDriver('#main-content')}
    });

    export const bootstrap = cycleLifecycles.bootstrap;
    export const mount = cycleLifecycles.mount;
    export const unmount = cycleLifecycles.unmount;

    Options

    All options are passed to single-spa-cycle via the opts parameter when calling singleSpaCycle(opts). The following options are available:

    • run: (required) Cycle.js run function.
    • drivers: (required) Drivers (including DOM Driver) to be used by your Cycle.js root component.
    • rootComponent: (required) The top level Cycle.js component which will be rendered
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-dojo/index.html b/docs/ecosystem-dojo/index.html index da0cca247..a9cebc079 100644 --- a/docs/ecosystem-dojo/index.html +++ b/docs/ecosystem-dojo/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    single-spa-dojo

    Build Status

    single-spa-dojo is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for use with Dojo. Check out the single-spa-dojo github.

    Installation

    npm install --save single-spa-dojo

    # or
    yarn add single-spa-dojo

    Quickstart

    Your bundler's "entry file" should look like this, which allows your application to be downloaded as an in-browser ES module.

    import { renderer } from '@dojo/framework/core/vdom';
    import { v, w } from '@dojo/framework/widget-core/d';
    import singleSpaDojo from 'single-spa-dojo';
    import App from './app';

    const dojoLifecycles = singleSpaDojo({
    // required
    renderer,

    // required
    v,

    // required
    w,

    // required
    appComponent: App,

    // optional - see https://dojo.io/learn/creating-widgets/rendering-widgets#mountoptions-properties
    mountOptions: {
    // optional
    registry: myRegistry,

    // optional - one will be provided by single-spa automatically
    domNode: document.getElementById('myContainer'),

    // optional
    sync: true
    }
    });

    export const bootstrap = dojoLifecycles.bootstrap;
    export const mount = dojoLifecycles.mount;
    export const unmount = dojoLifecycles.unmount;

    Options

    All options are passed to single-spa-dojo via the opts parameter when calling singleSpaDojo(opts). The following options are available:

    • renderer (required): The renderer function imported from Dojo. See https://dojo.io/learn/creating-widgets/rendering-widgets#rendering-to-the-dom.
    • v (required): The function used to render dom elements in Dojo. Often JSX hides this function from you, but it can be found at import { v } from '@dojo/framework/widget-core/d'.
    • w (required): The function used to render dom elements in Dojo. Often JSX hides this function from you, but it can be found at import { w } from '@dojo/framework/widget-core/d'.
    • appComponent (required): The class or function for your root Dojo component.
    • mountOptions (optional): An object of Dojo MountOptions. Note that a domNode will be provided by single-spa-dojo, if one is not provided.
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-ember/index.html b/docs/ecosystem-ember/index.html index c9faeb2a8..a41fcb178 100644 --- a/docs/ecosystem-ember/index.html +++ b/docs/ecosystem-ember/index.html @@ -14,15 +14,15 @@ - - + +
    Version: 6.x

    single-spa-ember

    single-spa-ember is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for use with Ember.js. Check out the single-spa-ember github.

    It is available on npm as single-spa-ember, and also available on bower as single-spa-ember in case you want to use it with ember cli and need to use bower.

    Overview

    When you are building an ember application that you want to work as a single-spa application, there are five things you need to implement:

    Single-spa-ember will help you implement all of those except for the activity function.

    Note that the loading and activity functions are part of the single-spa root application, whereas the bootstrap, mount, and unmount functions are part of a single-spa application

    API

    loadEmberApp

    loadEmberApp(appName, appUrl, vendorUrl) is a function that helps you implement the loading function for your ember application. appName and appUrl are both strings and both required, whereas vendorUrl is an optional string.

    // In the single-spa root application

    import {registerApplication} from 'single-spa';
    import {loadEmberApp} from 'single-spa-ember';

    const name = 'ember-app';
    const app = () => loadEmberApp(name, '/dist/ember-app/assets/ember-app.js', '/dist/ember-app/assets/vendor.js');
    const activeWhen = location => location.hash.startsWith('ember');

    registerApplication({ name, app, activeWhen });

    singleSpaEmber

    Single-spa-ember will implement the single-spa lifecyle functions for you. To use it, you call the default export as a function with a configuration object, which returns an object that has bootstrap, mount, and unmount lifecycle functions on it. The provided configuration object has the following options:

    // In the ember application
    import singleSpaEmber from 'single-spa-ember/src/single-spa-ember';

    const emberLifecycles = singleSpaEmber({
    appName: 'ember-app', // required
    createOpts: { // See https://www.emberjs.com/api/ember/2.14.1/classes/Ember.Application
    rootElement: '#ember-app',
    },
    });

    export const bootstrap = emberLifecycles.bootstrap;
    export const mount = emberLifecycles.mount;
    export const unmount = emberLifecycles.unmount;

    Usage with ember cli

    For the most part, you can get applications that use ember cli to work pretty seamlessly with single-spa. Maybe the biggest thing you'll have to worry about is that ember-cli assumes that it controls the entire HTML page, whereas a single-spa application does not. However, usually we can achieve equivalent behavior by just loading the vendor and app bundles into the HTML page dynamically, instead of baking them right into the HTML page. Below is a description of the known things you should do when setting up an ember-cli application with single-spa:

    First, you'll need to add single-spa-ember as a dependency to the ember project. This can be done with npm, yarn, or bower. For example:

    • npm init
    • npm install single-spa-ember or
    • bower init
    • bower install single-spa-ember --save

    Add the following options to your ember-cli-build.js file:

    /* eslint-env node */
    'use strict';

    const EmberApp = require('ember-cli/lib/broccoli/ember-app');

    module.exports = function(defaults) {
    let app = new EmberApp(defaults, {
    autoRun: false, // Set autoRun to false, because we only want the ember app to render to the DOM when single-spa tells it to.
    storeConfigInMeta: false, // We're making a single-spa application, which doesn't exclusively own the HTML file. So we don't want to have to have a `<meta>` tag for the ember environment to be initialized.
    fingerprint: {
    customHash: null, // This is optional, just will make it easier for you to have the same url every time you do an ember build.
    },
    // Add options here
    });

    // Tell ember how to use the single-spa-ember library - pick one of the following
    // if you used npm or yarn
    app.import('node_modules/single-spa-ember/amd/single-spa-ember.js', {
    using: [
    {transformation: 'amd', as: 'single-spa-ember'},
    ],
    });

    // **or** if you used bower
    app.import('bower_components/single-spa-ember/amd/single-spa-ember.js', {
    using: [
    {transformation: 'amd', as: 'single-spa-ember'},
    ],
    });


    return app.toTree();
    };

    In your single-spa root application (which is separate from anything generated by ember cli):

    // root-application.js
    import * as singleSpa from 'single-spa';
    import {loadEmberApp} from 'single-spa-ember';

    singleSpa.registerApplication('ember-app', loadingFunction, activityFunction);

    function activityFunction(location) {
    // Only render the ember app when the url hash starts with ember
    return location.hash.startsWith('ember');
    }

    // single-spa-ember helps us load the script tags and give the ember app module to single-spa.
    function loadingFunction() {
    const appName = 'ember-app';
    const appUrl = '/dist/ember-app/assets/ember-app.js';
    const vendorUrl = '/dist/ember-app/assets/vendor.js'; // Optional if you have one vendor bundle used for many different ember apps
    return loadEmberApp(appName, appUrl, vendorUrl);
    }

    In your app.js file (that is generated by ember cli)

    // app.js (the ember application)
    import Ember from 'ember';
    import Resolver from './resolver';
    import loadInitializers from 'ember-load-initializers';
    import config from './config/environment';
    import singleSpaEmber from 'single-spa-ember';

    // This part is generated by the ember cli
    const App = Ember.Application.extend({
    modulePrefix: config.modulePrefix,
    podModulePrefix: config.podModulePrefix,
    Resolver
    });

    loadInitializers(App, config.modulePrefix);

    export default App;

    // This is the single-spa part
    const emberLifecycles = singleSpaEmber({
    App, // required
    appName: 'ember-app', // required
    createOpts: { // optional
    rootElement: '#ember-app',
    },
    })

    // Single-spa lifecycles.
    export const bootstrap = emberLifecycles.bootstrap;
    export const mount = emberLifecycles.mount;
    export const unmount = emberLifecycles.unmount;
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-html-web-components/index.html b/docs/ecosystem-html-web-components/index.html index 1fbfe79b9..5f99194f9 100644 --- a/docs/ecosystem-html-web-components/index.html +++ b/docs/ecosystem-html-web-components/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ single-spa applications or parcels.

    Installation

    npm install --save single-spa-html

    # or
    yarn add single-spa-html

    Alternatively, you can use single-spa-html from a CDN as a global variable:

    <script src="https://cdn.jsdelivr.net/npm/single-spa-html"></script>

    Note that you might want to lock down the package to a specific version. See here for how to do that.

    Usage

    Via npm

    import singleSpaHtml from 'single-spa-html';

    const htmlLifecycles = singleSpaHtml({
    template: '<x-my-web-component></x-my-web-component>',
    })

    export const bootstrap = htmlLifecycles.bootstrap;
    export const mount = htmlLifecycles.mount;
    export const unmount = htmlLifecycles.unmount;

    Via cdn

    Example usage when installed via CDN:

    const webComponentApp = window.singleSpaHtml.default({
    template: props => `<x-my-web-component attr="${props.attr}"></x-my-web-component>`,
    })

    singleSpa.registerApplication({
    name: 'name',
    app: webComponentApp,
    activeWhen: () => true
    })

    API / Options

    single-spa-html is called with an object that has the following properties:

    • template (required): An HTML string or a function that returns a string or promise that resolves a string. The function will be called with the single-spa custom props. The returned string is injected into the DOM during the single-spa mount lifecycle.
    • domElementGetter (optional): A function that is given the single-spa props and returns the dom element container into which the HTML will be injected. If omitted, a default implementation is provided that wraps the template in a <div> that is appended to document.body.
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-inferno/index.html b/docs/ecosystem-inferno/index.html index 6e2765834..c17916220 100644 --- a/docs/ecosystem-inferno/index.html +++ b/docs/ecosystem-inferno/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    single-spa-inferno

    single-spa-inferno is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Inferno. Check out the single-spa-inferno github.

    Quickstart

    First, in the application, run npm install --save single-spa-inferno. Then, add the following to your application's entry file.

    import Inferno from 'inferno';
    import rootComponent from './path-to-root-component.js';
    import singleSpaInferno from 'single-spa-inferno';

    const infernoLifecycles = singleSpaInferno({
    Inferno,
    createElement,
    rootComponent,
    domElementGetter: () => document.getElementById('main-content'),
    });

    export const bootstrap = infernoLifecyles.bootstrap;
    export const mount = infernoLifecyles.mount;
    export const unmount = infernoLifecyles.unmount;

    Options

    All options are passed to single-spa-inferno via the opts parameter when calling singleSpaInferno(opts). The following options are available:

    • inferno: (required) The main Inferno object, which is generally either exposed onto the window or is available via require('inferno') or import Inferno from 'inferno'.
    • createElement: (required) The default export from Inferno's inferno-create-element package.
    • rootComponent: (required) The top level Inferno component which will be rendered.
    • domElementGetter: (required) A function that takes in no arguments and returns a DOMElement. This dom element is where the Inferno application will be bootstrapped, mounted, and unmounted.
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-leaked-globals/index.html b/docs/ecosystem-leaked-globals/index.html index 073d50e2e..12a1f22f2 100644 --- a/docs/ecosystem-leaked-globals/index.html +++ b/docs/ecosystem-leaked-globals/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -41,7 +41,7 @@ export an array for your lifecycle functions instead of exporting just a function.

    import singleSpaLeakedGlobals from 'single-spa-leaked-globals';

    // Use single-spa-angularjs, single-spa-backbone, etc to get your framework specific lifecycles
    const frameworkLifecycles = ...

    const leakedGlobalsLifecycles = singleSpaLeakedGlobals({
    globalVariableNames: ['$', 'jQuery', '_'],
    })

    export const bootstrap = [
    leakedGlobalsLifecycles.bootstrap,
    frameworkLifecycles.bootstrap,
    ]

    export const mount = [
    // Make sure leaked globals lifecycles' mount function is **before** other lifecycles' mount
    // This is so the global vars are available when the framework mounts
    leakedGlobalsLifecycles.mount,
    frameworkLifecycles.mount,
    ]

    export const unmount = [
    leakedGlobalsLifecycles.unmount,
    // Make sure leaked globals lifecycles' unmount function is **after** other lifecycles' unmount
    // This is so the global vars are still available during the framework unmount lifecycle function.
    frameworkLifecycles.unmount,
    ]

    If you're using single-spa-leaked-globals as a global variable itself:

    const leakedGlobalsLifecycles = window.singleSpaLeakedGlobals.default({
    globalVariableNames: ['_'],
    })

    API / Options

    single-spa-leaked-globals is called with an object with the following properties:

    • globalVariableNames (required): An array of strings. Each string is the name of a global variable that should be removed when the application is unmounted, and added back when the application is mounted.
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-preact/index.html b/docs/ecosystem-preact/index.html index 3789bcc37..e7d7c1d7a 100644 --- a/docs/ecosystem-preact/index.html +++ b/docs/ecosystem-preact/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    single-spa-preact

    single-spa-preact is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Preact. Check out the single-spa-preact github.

    Installation

    npm install --save preact

    Quickstart

    In your project's entry file, add the following:

    import preact from 'preact';
    import rootComponent from './path-to-root-component.js';
    import singleSpaPreact from 'single-spa-preact';

    const preactLifecycles = singleSpaPreact({
    preact,
    rootComponent,
    domElementGetter: () => document.getElementById('main-content'),
    });

    export const bootstrap = preactLifecycles.bootstrap;
    export const mount = preactLifecycles.mount;
    export const unmount = preactLifecycles.unmount;

    Options

    All options are passed to single-spa-preact via the opts parameter when calling singleSpaPreact(opts). The following options are available:

    • preact: (required) The main Preact object, which is generally either exposed onto the window or is available via require('preact') or import preact from 'preact'.
    • rootComponent: (required) The top level preact component which will be rendered
    • domElementGetter: (optional) A function that is given the single-spa props and returns a DOMElement. This dom element is where the Preact application will be bootstrapped, mounted, and unmounted. If omitted, a div will be created and appended to the body.
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-react/index.html b/docs/ecosystem-react/index.html index 3ae60b36e..b0a8edf83 100644 --- a/docs/ecosystem-react/index.html +++ b/docs/ecosystem-react/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -33,7 +33,7 @@ single-spa's root mountParcel function, so that single-spa can keep track of the parent-child relationship and automatically unmount the application's parcels when the application unmounts. Note that if the <Parcel> component is being rendered by a single-spa application that uses single-spa-react, it is unnecessary to pass in the prop, since <Parcel> can get the prop from SingleSpaContext
  • handleError (optional): A function that will be called with errors thrown by the parcel. If not provided, errors will be thrown on the window, by default.
  • parcelDidMount (optional): A function that will be called when the parcel finishes loading and mounting.
  • Examples

    // Use this import path in environments that support package.json exports
    // See https://nodejs.org/dist/latest-v14.x/docs/api/packages.html#packages_package_entry_points
    // and see https://github.com/single-spa/single-spa-react/releases/tag/v3.0.0
    // Use this in Webpack 5 and recent versions of Node
    import Parcel from 'single-spa-react/parcel'

    // Use this import path in environments that don't support package.json exports
    // See https://nodejs.org/dist/latest-v14.x/docs/api/packages.html#packages_package_entry_points
    // and see https://github.com/single-spa/single-spa-react/releases/tag/v3.0.0
    // Use this in Webpack 4 and older versions of Node
    import Parcel from 'single-spa-react/lib/esm/parcel'


    import * as parcelConfig from './my-parcel.js'

    // config is required. The parcel will be mounted inside of the
    // of a div inside of the react component tree
    <Parcel
    config={parcelConfig}

    wrapWith="div"
    handleError={err => console.error(err)}

    customProp1="customPropValue2"
    customProp2="customPropValue2"
    />

    // If you pass in an appendTo prop, the parcel will be mounted there instead of
    // to a dom node inside of the current react component tree
    <Parcel
    config={parcelConfig}
    wrapWith="div"
    appendTo={document.body}
    />

    // You can also pass in a "loading function" as the config.
    // The loading function must return a promise that resolves with the parcel config.
    // The parcel will be mounted once the promise resolves.
    <Parcel
    config={() => import('./my-parcel.js')}
    wrapWith="div"
    />

    // If you are rendering the Parcel component from a single-spa application, you do not need to pass a mountParcel prop.
    // But if you have a separate react component tree that is not rendered by single-spa-react, you **must** pass in a mountParcel prop
    // In general, it is preferred to use an application's mountParcel function instead of the single-spa's root mountParcel function,
    // so that single-spa can keep track of the parent-child relationship and automatically unmount the application's parcels when the application
    // unmounts
    <Parcel
    mountParcel={singleSpa.mountParcel}
    config={parcelConfig}
    wrapWith="div"
    />

    // Add styles to wrapWith element.
    <Parcel
    config={parcelConfig}
    wrapWith="div"
    wrapStyle={{ background: 'black' }}
    />

    // Add classNames to wrapWith element.
    <Parcel
    config={parcelConfig}
    wrapWith="div"
    wrapClassName="wrapper"
    />

    Create React App

    See FAQ for CRA.

    - - + + \ No newline at end of file diff --git a/docs/ecosystem-riot/index.html b/docs/ecosystem-riot/index.html index c5ec59234..a772e7bad 100644 --- a/docs/ecosystem-riot/index.html +++ b/docs/ecosystem-riot/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    single-spa-riot

    single-spa-riot is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with riot. Check out the single-spa-riot github.

    NPM

    Build Status

    minified

    Installation

    npm install --save single-spa-riot

    Usage

    import * as Riot from 'riot';
    import singleSpaRiot from 'single-spa-riot';
    import App from './App.riot'

    const riotLifecycles = singleSpaRiot({
    rootComponent: Riot.component(App),
    domElementGetter: () => document.getElementById('#app')
    });

    export const bootstrap = riotLifecycles.bootstrap;

    export const mount = riotLifecycles.mount;

    export const unmount = riotLifecycles.unmount;

    Options

    All options are passed to single-spa-riot via the opts parameter when calling singleSpaRiot(opts). The following options are available:

    • domElementGetter: (required) the callback to get root component mount element.
    • rootComponent: (optional and replaces appOptions.loadRootComponent) the root riot component.
    • loadRootComponent: (optional and replaces appOptions.rootComponent) A promise that resolves with your root component. This is useful for lazy loading.
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-snowpack/index.html b/docs/ecosystem-snowpack/index.html index bb5503ad1..d65c96a9c 100644 --- a/docs/ecosystem-snowpack/index.html +++ b/docs/ecosystem-snowpack/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Snowpack

    Snowpack is a tool for both local development and the building of applications. It uses in-browser ES modules during development, and then bundles with webpack (or other build tools) for production.

    Example repo

    https://github.com/joeldenning/snowpack-single-spa-example

    Overview

    Snowpack uses ES modules in local development, but not in production. This works well with single-spa, which encourages using in-browser modules as the interface for each microfrontend. To use snowpack with single-spa, you must export the single-spa lifecycle functions from your Snowpack project's index.js file and then make a few modifications to the snowpack.config.js file.

    Configuration

    Modify the index.js file to not mount your app immediately, but rather to export the single-spa lifecycles. If using Vue, for example, see https://single-spa.js.org/docs/ecosystem-vue#usage.

    The following Snowpack config can be used as the basis for a single-spa + Snowpack setup. It requires installing systemjs-webpack-interop and @snowpack/plugin-webpack:

    npm install --save-dev systemjs-webpack-interop @snowpack/plugin-webpack

    yarn add --dev systemjs-webpack-interop @snowpack/plugin-webpack

    pnpm install --save-dev systemjs-webpack-interop @snowpack/plugin-webpack
    const { merge } = require("webpack-merge");
    const SystemJSPublicPathWebpackPlugin = require("systemjs-webpack-interop/SystemJSPublicPathWebpackPlugin");

    /** @type {import("snowpack").SnowpackUserConfig } */
    module.exports = {
    mount: {
    /* ... */
    },
    plugins: [
    [
    "@snowpack/plugin-webpack",
    {
    extendConfig(config) {
    delete config.optimization.runtimeChunk;
    delete config.optimization.splitChunks;

    return merge(config, {
    mode: "development",
    module: {
    rules: [
    // This rule is necessary in webpack 4, but breaks things in webpack 5
    // At the time of writing this documentation, @snowpack/plugin-webpack uses webpack 4.
    {
    parser: {
    system: false,
    },
    },
    ],
    },
    output: {
    libraryTarget: "system",
    },
    plugins: [
    new SystemJSPublicPathWebpackPlugin({
    systemjsModuleName: "snowpack-test",
    rootDirectoryLevel: 2,
    }),
    ],
    });
    },
    },
    ],
    ],
    routes: [
    /* Enable an SPA Fallback in development: */
    // {"match": "routes", "src": ".*", "dest": "/index.html"},
    ],
    optimize: {
    /* Example: Bundle your final build: */
    // "bundle": true,
    },
    packageOptions: {
    /* ... */
    },
    devOptions: {},
    buildOptions: {
    baseUrl: "http://localhost:8080/",
    },
    };

    Local development

    Snowpack works well with development via import map overrides. You should use http://localhost:8080/index.js as the URL for your import map override.

    caution

    Static Assets currently do not load from the correct URL in development, pending a PR to Snowpack: https://github.com/snowpackjs/snowpack/pull/2407. However, static assets do load from the correct URL in production, due to systemjs-webpack-interop.

    Native Modules vs SystemJS

    single-spa works well with native modules, systemjs, or even both. With Snowpack + single-spa, a general recommendation is to use native modules during local development, but SystemJS in production (since browser support for Import Maps is still pending). Doing this is nice because it matches Snowpack's development workflow; however, mixing native and systemjs modules also can have some caveats:

    • The browser and SystemJS maintain separate module registries. This means that you can't share imports between SystemJS and native modules. So if you are doing an import map override for a Snowpack application on a page that also uses SystemJS, you may end up with multiple instances of Vue or React (and other shared libraries), which is different than how things will work in production. This is generally okay, except for situations where the Vue instance is modified via Vue.use().
    • This PR to SystemJS shows how you can populate native modules into the SystemJS registry, allowing for one-way sharing of modules between the two registries. The PR was closed due to some edge cases, but it generally works. Even though the PR is closed, you can paste the ESM extra into your root config and it will work. If you have interest in driving forward better SystemJS + ESM compatibility, comment on Github or Slack with your interest.
    - - + + \ No newline at end of file diff --git a/docs/ecosystem-svelte/index.html b/docs/ecosystem-svelte/index.html index 4d21c8565..73f528a1b 100644 --- a/docs/ecosystem-svelte/index.html +++ b/docs/ecosystem-svelte/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    single-spa-svelte

    single-spa-svelte is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with svelte. Check out the single-spa-svelte github.

    Quickstart

    First, in the single-spa application, run npm install --save single-spa-svelte. Then, create an entry file with the following.

    import singleSpaSvelte from 'single-spa-svelte';
    import myRootSvelteComponent from 'my-root-svelte-component.js';

    const svelteLifecycles = singleSpaSvelte({
    component: myRootSvelteComponent,
    domElementGetter: () => document.getElementById('svelte-app'),
    props: { someData: 'data' }
    });

    export const bootstrap = svelteLifecycles.bootstrap;
    export const mount = svelteLifecycles.mount;
    export const unmount = svelteLifecycles.unmount;

    Options

    All options are passed to single-spa-svelte via the opts parameter when calling singleSpaSvelte(opts). The following options are available:

    • component: (required) The root component that will be rendered. This component should be compiled by svelte and not an iife.
    • domElementGetter: (optional) A function which will return a dom element. The root component will be mounted in this element. If not provided, a default dom element will be provided.

    Svelte-specific options

    • anchor: (optional) A child of the dom element identified by domElementGetter to render the component immediately before
    • hydrate: (optional) See the svelte Creating a component documentation
    • intro: (optional) If true, will play transitions on initial render, rather than waiting for subsequent state changes
    • props: (optional) An object of properties to supply to the component

    single-spa props

    All single-spa props are passed to the svelte component as props. The props provided to singleSpaSvelte({props: {...}}) are merged with the single-spa props.

    - - + + \ No newline at end of file diff --git a/docs/ecosystem-vite/index.html b/docs/ecosystem-vite/index.html index 4b2d27223..27ca8d4a4 100644 --- a/docs/ecosystem-vite/index.html +++ b/docs/ecosystem-vite/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Vite

    Vite is a tool for both local development and the building of applications. It was created within the Vue ecosystem, but can be used with other UI frameworks, too.

    Example repo

    https://github.com/joeldenning/vite-single-spa-example

    Overview

    By default, Vite uses ES modules in local development, but not in production. This works well with single-spa, which encourages using in-browser modules as the interface for each microfrontend. To use vite with single-spa, you must export the single-spa lifecycle functions from your Vite's main.js file and then make a few modifications to the vite.config.js file.

    Configuration

    Modify the src/main.js file to not mount your app immediately, but rather to export the single-spa lifecycles. For Vue apps, see https://single-spa.js.org/docs/ecosystem-vue#usage.

    The following Vite config can be used as the basis for a single-spa + Vite setup:

    import vue from '@vitejs/plugin-vue'

    export default {
    rollupOptions: {
    input: 'src/main.js',
    format: 'system',
    preserveEntrySignatures: true
    },
    base: 'http://localhost:3000',
    plugins: [vue({
    template: {
    transformAssetUrls: {
    base: '/src'
    }
    }
    })],
    }

    Local development

    Vite works well with development via import map overrides. You should use http://localhost:3000/src/main.js as the URL for your import map override. It is important to note, however, that assets such as images and fonts won't load. The import map is only used to load JavaScript, not media files. The import map does not affect asset URL's. Asset URL's are affected by Vite's base configuration property, and Vite doesn't respect full URL's in said property while in serve mode (npm run dev). While in serve mode, a base with a full URL is stripped down to its path. Therefore, the asset URL's don't really get the correct host URL. The author of vite-plugin-single-spa has opened a discussion in Vite's GitHub that you can opt to support by upvoting it.

    Native Modules vs SystemJS

    single-spa works well with native modules, systemjs, or even both. With Vite + single-spa, a general recommendation is to use native modules during local development, but SystemJS in production (since browser support for Import Maps is still pending). Doing this is nice because it matches Vite's development workflow; however, mixing native and systemjs modules also can have some caveats:

    • The browser and SystemJS maintain separate module registries. This means that you can't share imports between SystemJS and native modules. So if you are doing an import map override for a Vite application on a page that also uses SystemJS, you may end up with multiple instances of Vue (and other shared libraries), which is different than how things will work in production. This is generally okay, except for situations where the Vue instance is modified via Vue.use().
    • This PR to SystemJS shows how you can populate native modules into the SystemJS registry, allowing for one-way sharing of modules between the two registries. The PR was closed due to some edge cases, but it generally works. Even though the PR is closed, you can paste the ESM extra into your root config and it will work. If you have interest in driving forward better SystemJS + ESM compatibility, comment on Github or Slack with your interest.

    vite-plugin-single-spa

    This is a new entry that is currently in the early stages of development, but shows significant progress (view in GitHub). It claims to be able to convert out-of-the-box Vite projects (regardless of the framework) into single-spa micro-frontend projects and even root config projects. While the single-spa team discourages the use of UI frameworks in root configs, it is indeed an alternative that may interest people.

    To convert a Vite project to a root config project, all that is needed is install vite-plugin-single-spa, and then use it in vite.config.ts. This is a Vite + Vue example:

    import vitePluginSingleSpa from 'vite-plugin-single-spa';

    // https://vitejs.dev/config/
    export default defineConfig({
    plugins: [vue(), vitePluginSingleSpa({
    type: 'root'
    }
    })]
    });

    To convert a Vite project to a micro-frontend project, a similarly minimalistic configuration is needed, plus a file that exports the single-spa lifecycle functions.

    // vite.config.ts for a Vite + React project

    import react from '@vitejs/plugin-react'
    import vitePluginSingleSpa from 'vite-plugin-single-spa';

    // https://vitejs.dev/config/
    export default defineConfig({
    plugins: [react(), vitePluginSingleSpa({
    serverPort: 4101,
    spaEntryPoint: 'src/spa.tsx'
    })],
    });
    // src/spa.tsx

    import React from 'react';
    import ReactDOMClient from 'react-dom/client';
    // @ts-ignore
    import singleSpaReact from 'single-spa-react';
    import App from './App';
    import { cssLifecycle } from 'vite-plugin-single-spa/ex';

    const lc = singleSpaReact({
    React,
    ReactDOMClient,
    rootComponent: App,
    errorBoundary(err: any, _info: any, _props: any) {
    return <div>Error: {err}</div>
    }
    });

    export const bootstrap = [cssLifecycle.bootstrap, lc.bootstrap];
    export const mount = [cssLifecycle.mount, lc.mount];
    export const unmount = [cssLifecycle.unmount, lc.unmount];

    Main Features

    • Supports stock Vite projects, regardless of framework.
    • Micro-frontend projects behave dually while in serve mode: The micro-frontend can be previewed as a standalone web application with its server URL, or it can be served as a single-spa micro-frontend.
    • As seen in the example above, it provides an extra module that automatically mounts and unmounts the CSS referenced by the lifecycle-exporting module (src/spa.tsx in the example). COMING SOON
    • Automatically picks up import maps from src/importMap.dev.json and src/importMap.json.
    • Automatically adds the import-map-overrides NPM package, user interface included.

    IMPORTANT: The author of this plug-in does not believe in creating dedicated root config projects. Furthermore, this package will, by default, create import maps for native modules. We at single-spa recommend SystemJS modules. Yes, single-spa is perfectly capable of working with native modules as well.

    The opinions of the author of this plug-in in no way represent those of single-spa, and it is an independent work. We present it here as one more option in the Vite ecosystem.

    - - + + \ No newline at end of file diff --git a/docs/ecosystem-vue/index.html b/docs/ecosystem-vue/index.html index 092e55a55..4993cd909 100644 --- a/docs/ecosystem-vue/index.html +++ b/docs/ecosystem-vue/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -28,7 +28,7 @@ checkout coexisting-vue-microfrontends' index.html file.

    Sharing a single instance of Vue and other common libraries is highly recommended. See the recommended setup for single-spa for more details on why.

    Shared deps with Vue CLI

    // vue.config.js
    module.exports = {
    chainWebpack: config => {
    config.externals(['vue', 'vue-router']);
    },
    };

    Shared deps without Vue CLI

    // webpack.config.js
    module.exports = {
    externals: ['vue', 'vue-router'],
    };

    Options

    All options are passed to single-spa-vue via the opts parameter when calling singleSpaVue(opts). The following options are available:

    • Vue: (required) The main Vue object, which is generally either exposed onto the window or is available via require('vue') import Vue from 'vue'.
    • appOptions: (required) An object or async function which will be used to instantiate your Vue.js application. appOptions will pass directly through to new Vue(appOptions). Note that if you do not provide an el to appOptions, that a div will be created and appended to the DOM as a default container for your Vue application. When appOptions is an async function, it receives the single-spa props as an argument (as of single-spa-vue@2.4.0).
    • loadRootComponent: (optional and replaces appOptions.render) A promise that resolves with your root component. This is useful for lazy loading.
    • handleInstance: (optional) A method can be used to handle Vue instance. Vue 3 brings new instance API, and you can access the app instance from this, like handleInstance: (app, props) => app.use(router). For Vue 2 users, a Vue instance can be accessed. The handleInstance(app, props) function receives the instance as its first argument, and single-spa props as its second argument. If handleInstance returns a promise, single-spa-vue will wait to resolve the app / parcel's mount lifecycle until the handleInstance promise resolves.
    • replaceMode: (optional, defaults to false) A boolean that determines whether your root Vue component will entirely replace the container element it's mounted to. The Vue library always replaces, so to implement replaceMode: false a temporary <div class="single-spa-container"> element is created inside of the container, so that Vue replaces that element rather than the container. Introduced in single-spa-vue@2.3.0.

    To configure which dom element the single-spa application is mounted to, use appOptions.el:

    const vueLifecycles = singleSpaVue({
    Vue,
    appOptions: {
    render: h => h(App),
    el: '#a-special-container',
    },
    });

    To configure options asynchronously return a promise from appOptions function:

    const vueLifecycles = singleSpaVue({
    Vue,
    async appOptions() {
    return {
    router: await routerFactory(),
    render: h => h(App)
    }
    },
    });

    Custom Props

    single-spa custom props are available in the render() function in your main file. They can be passed as custom props to your App component.

    const vueLifecycles = singleSpaVue({
    Vue,
    appOptions: {
    render(h) {
    return h(App, {
    props: {
    // single-spa props are available on the "this" object. Forward them to your component as needed.
    // https://single-spa.js.org/docs/building-applications#lifecycle-props
    name: this.name,
    mountParcel: this.mountParcel,
    singleSpa: this.singleSpa,
    },
    });
    },
    },
    });

    Parcels

    Creating a parcel

    A parcel config is an object that represents a component implemented in Vue, React, Angular, or any other framework.

    To create a VueJS single-spa parcel config object, simply omit the el option from your appOptions, since the dom element will be specified by the user of the Parcel. Every other option should be provided exactly the same as in the example above.

    const parcelConfig = singleSpaVue({...});

    Rendering a parcel

    To render a parcel config object in Vue, you can use single-spa-vue's Parcel component:

    <template>
    <Parcel
    v-on:parcelMounted="parcelMounted()"
    v-on:parcelUpdated="parcelUpdated()"
    :config="parcelConfig"
    :mountParcel="mountParcel"
    :wrapWith="wrapWith"
    :wrapClass="wrapClass"
    :wrapStyle="wrapStyle"
    :parcelProps="getParcelProps()"
    />
    </template>

    <script>
    // For old versions of webpack
    import Parcel from 'single-spa-vue/dist/esm/parcel'
    // For new versions of webpack
    import Parcel from 'single-spa-vue/parcel'

    import { mountRootParcel } from 'single-spa'

    export default {
    components: {
    Parcel
    },
    data() {
    return {
    /*
    parcelConfig (object, required)

    The parcelConfig is an object, or a promise that resolves with a parcel config object.
    The object can originate from within the current project, or from a different
    microfrontend via cross microfrontend imports. It can represent a Vue component,
    or a React / Angular component.
    https://single-spa.js.org/docs/recommended-setup#cross-microfrontend-imports

    Vanilla js object:
    parcelConfig: {
    async mount(props) {},
    async unmount(props) {}
    }

    // React component
    parcelConfig: singleSpaReact({...})

    // cross microfrontend import is shown below
    */
    parcelConfig: System.import('@org/other-microfrontend').then(ns => ns.Widget),


    /*
    mountParcel (function, required)

    The mountParcel function can be either the current Vue application's mountParcel prop or
    the globally available mountRootParcel function. More info at
    http://localhost:3000/docs/parcels-api#mountparcel
    */
    mountParcel: mountRootParcel,

    /*
    wrapWith (string, optional)

    The wrapWith string determines what kind of dom element will be provided to the parcel.
    Defaults to 'div'
    */
    wrapWith: 'div'

    /*
    wrapClass (string, optional)

    The wrapClass string is applied to as the CSS class for the dom element that is provided to the parcel
    */
    wrapClass: "bg-red"

    /*
    wrapStyle (object, optional)

    The wrapStyle object is applied to the dom element container for the parcel as CSS styles
    */
    wrapStyle: {
    outline: '1px solid red'
    },
    }
    },
    methods: {
    // These are the props passed into the parcel
    getParcelProps() {
    return {
    text: `Hello world`
    }
    },
    // Parcels mount asynchronously, so this will be called once the parcel finishes mounting
    parcelMounted() {
    console.log("parcel mounted");
    },
    parcelUpdated() {
    console.log("parcel updated");
    }
    }
    }
    </script>

    Webpack Public Path

    vue-cli-plugin-single-spa sets the webpack public path via SystemJSPublicPathWebpackPlugin. By default, the public path is set to match the following output directory structure:

    dist/
    js/
    app.js
    css/
    main.css

    With this directory structure (which is the Vue CLI default), the public path should not include the js folder. This is accomplished by setting rootDirectoryLevel to be 2. If this doesn't match your directory structure or setup, you can change the rootDirectoryLevel with the following code in your vue.config.js or webpack.config.js:

    // vue.config.js
    module.exports = {
    chainWebpack(config) {
    config.plugin('SystemJSPublicPathWebpackPlugin').tap((args) => {
    args[0].rootDirectoryLevel = 1;
    return args;
    });
    }
    }
    - - + + \ No newline at end of file diff --git a/docs/ecosystem/index.html b/docs/ecosystem/index.html index 957b979f5..68da60ae4 100644 --- a/docs/ecosystem/index.html +++ b/docs/ecosystem/index.html @@ -14,15 +14,15 @@ - - + + - - + + \ No newline at end of file diff --git a/docs/examples/index.html b/docs/examples/index.html index 489198b56..a84a2c5c8 100644 --- a/docs/examples/index.html +++ b/docs/examples/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Single-spa Examples

    Core team examples

    Actively maintained

    Older examples

    Community examples

    Have your own example or starter repo? Submit a PR to add yours to this list.

    - - + + \ No newline at end of file diff --git a/docs/faq/index.html b/docs/faq/index.html index 0b892025c..777aba5e5 100644 --- a/docs/faq/index.html +++ b/docs/faq/index.html @@ -14,14 +14,14 @@ - - + +
    Version: 6.x

    Frequently Asked Questions

    What does single-spa do?

    single-spa is a top level router. When a route is active, it downloads and executes the code for that route.

    The code for a route is called an "application", and each can (optionally) be in its own git repository, have its own CI process, and be separately deployed. The applications can all be written in the same framework, or they can be implemented in different frameworks.

    Yes, here is the documentation for our recommended setup.

    Should I have a parent/root app and children apps?

    No. We strongly encourage that your single-spa-config or root application does not use any JavaScript UI frameworks (React, Angular, Angularjs, Vue, etc). In our experience a plain JavaScript module is best for the single-spa-config and only the registered applications actually use UI frameworks (Angular, React, Vue, etc).

    Why? You end up creating a structure that has all the disadvantages of microservices without any of the advantages: your apps are now coupled together and you have to change multiple apps at the same time in order to make updates. Good microservices are completely independent, and this pattern breaks that.

    What is the impact to performance?

    When setup in the recommended way, your code performance and bundle size will be nearly identical to a single application that has been code-split. The major differences will be the addition of the single-spa library (and SystemJS if you chose to use it). Other differences mainly come down to the difference between one (webpack / rollup / etc.) code bundle and in-browser ES modules.

    Can I have only one version of (React, Vue, Angular, etc.) loaded?

    Yes, and it's highly recommended you do so! Using the recommended setup, you configure your import map so that your library is defined only once. Then, tell each application to not bundle that library; instead, the library will given to you at runtime in the browser. See webpack’s externals (and other bundlers have similar options) for how to do this.

    You do have the option of not excluding those libraries (for example if you want to experiment with a newer version or a different library) but be aware of the effect that will have on user's bundle sizes and application speed.

    What are import maps?

    Import maps improve the developer experience of in-browser ES modules by allowing you to write something like import React from "react" instead of needing to use an absolute or relative URL for your import statement. The same is also true of importing from other single-spa applications, e.g. import {MyButton} from "styleguide". The import-map spec is currently in the process of being accepted as a web standard and at the time of writing has been implemented in Chrome, and a polyfill for browsers >= IE11 has been implemented by SystemJS >= 3.0. Also see the recommended setup

    How can I share application state between applications?

    The primary means of communicating between applications is cross microfrontend imports. This allows you define a public interface for a microfrontend that others can use. You may expose functions, data, components, stores, or anything else from any microfrontend to be available in any other.

    We recommend that each application manage as much of its own state as possible so that your applications remain independently deployable without the risk of breaking each other. Generally, it’s better to make an API request for the data that each app needs, even if parts of it have been requested by other apps. If you've split your applications well, there will end up being very little application state that is truly shared — for example, your friends list has different data requirements than your social feed.

    The list below shows some common practices:

    1. Create a shared API utility microfrontend that caches fetch/XHR requests and their responses. All microfrontends call into the API microfrontend when making a request, so that the microfrontend can control whether to refetch the data or not.
    2. Create a shared Auth utility microfrontend that exposes a userCanAccess function for other microfrontends to use when checking permissions. The auth module may also include other exports such as the logged in user object, auth tokens, etc.
    3. Export shared state from the public interface of your microfrontend so that libraries can import it. For values that change over time, Observables (RxJS docs) can be useful. Create a ReplaySubject so that you can push new values out to all subscribers at any time.
    4. Use custom browser events to communicate. Fire them on the window in one microfrontend, and listen to the event in a different microfrontend.
    5. Use cookies, local/session storage, or other similar methods for storing and reading that state. These methods work best with things that don't change often, e.g. logged-in user info.

    Should I use frontend microservices?

    If you’ve ran into some of the headaches a monolithic repo has, then you should really consider it.

    In addition, if your organization is setup in a Spotify-type model (e.g. where there are autonomous squads that own full-stack features) then microservices on the frontend will fit very well into your setup.

    However, if you’re just starting off and have a small project or a small team, we would recommend you stick with a monolith (i.e. not microservices) until you get to the point that scaling (e.g. organizational scaling, feature scaling, etc.) is getting hard. Don’t worry, we’ll be here to help you migrate when you get there.

    Can I use more than one framework?

    Yes. However, it’s something you’ll want to consider hard because it splits your front-end organization into specialities that aren’t compatible (e.g. a React specialist may have problems working in an Angular app), and also causes more code to be shipped to your users.

    However, it is great for migrations away from an older or unwanted library, which allows you to slowly rip out the code in the old application and replace it with new code in the new library (see Google results for the strangler pattern).

    It also is a way to allow large organizations to experiment on different libraries without a strong commitment to them.

    Just be conscious of the effect it has on your users and their experience using your app.

    What is the developer experience (DX) like?

    If you're using the recommended setup for single-spa, you'll simply be able to go to your development website, add an import map that points to your locally-running code, and refresh the page.

    There's a library that you can use, or you can even just do it yourself - you'll note that the source code is pretty simple. The main takeaway is that you can have multiple import maps and the latest one wins - you add an import map that overrides the default URL for an application to point to your localhost.

    We're also looking at providing this functionality as part of the Chrome/Firefox browser extension.

    Finally, this setup also enables you to do overrides in your production environment. It obviously should be used with caution, but it does enable a powerful way of debugging problems and validating solutions.

    As a point of reference, nearly all developers we've worked with prefer the developer experience of microservices + single-spa over a monolithic setup.

    Can each single-spa application have its own git repo?

    Yes! You can even give them their own package.json, webpack config, and CI/CD process, using SystemJS to bring them all together in the browser.

    Can single-spa applications be deployed independently?

    Yes! See next section about CI/CD.

    What does the CI/CD process look like?

    In other words, how do I build and deploy a single-spa application?

    With the recommended setup, the process generally flows like this:

    1. Bundle your code and upload it to a CDN.
    2. Update your dev environment's import map to point to the new URL. In other words, your import map used to say "styleguide": "cdn.com/styleguide/v1.js" and now it should say "styleguide": "cdn.com/styleguide/v2.js"

    Some options on how to update your import map include:

    • Server render your index.html with the import map inlined. This does not mean that your DOM elements need to all be server rendered, but just the <script type="systemjs-importmap"> element. Provide an API that either updates a database table or a file local to the server.
    • Have your import map itself on a CDN, and use import-map-deployer or similar to update the import map during your CI process. This method has a small impact on performance, but is generally easier to setup if you don't have a server-rendered setup already. (You can also preload the import map file to help provide a small speed boost). See example travis.yml. Other CI tools work, too.

    Create React App

    Tutorial video: Youtube / Bilibili

    If you are starting from scratch, prefer using create-single-spa instead of create-react-app. A project using CRA must be altered before it can be used as a single-spa application. CRA provides many features out of the box, and outputs "monolith" apps by default. CRA does not support extending its configuration so we cannot provide official support for using it with single-spa. In order to continue using CRA, your options are to:

    1. Eject from CRA

    2. Use a third-party tool to extend CRA, such as:

    3. Remove react-scripts and then run create-single-spa on your project to merge create-single-spa's package.json with yours. Run yarn start and fix webpack configuration errors until it's working.

    4. Generate a project with create-single-spa and migrate the React code over.

    caution

    The single-spa webpack configs only provide basic functionality and not all the same features that CRA does. You may be required to add & configure more options, plugins, or loaders for non-JavaScript files. This may require advanced knowledge across bundlers, toolchains, plugins, etc.

    For additional reference here is a list of some changes you will need to make to your webpack config:

    1. Remove Webpack optimizations block, because they add multiple webpack chunks that don't load each other
    2. Remove html-webpack plugin
    3. Change output.libraryTarget to System, UMD, or AMD.

    Code splits

    Single spa supports code splits. There are so many ways to code split we won't be able to cover them all, but if you're using the recommended setup with webpack you'll need to do at least two things:

    1. Set the __webpack_public_path__ dynamically so webpack knows where to fetch your code splits (webpack assumes they are located at the root of the server and that isn't always true in a single-spa application). Both solutions below should be the very first import of your application in order to work.

      import { setPublicPath } from 'systemjs-webpack-interop';

      setPublicPath('name-of-module-in-import-map');
      • For SystemJS 2-5: Find a code example here
    2. Set either output.jsonpFunction or output.library to ensure that each app's webpack doesn't collide with other apps' webpack. jsonpFunction is preferred.

    For more information about webpack configuration and single-spa, see the recommended setup.

    Does single-spa require additional security considerations?

    No. single-spa does not add, deviate, or attempt to bypass any browser JavaScript security measures. The security needs of your applications are the same as if you did not use single-spa.

    Outside of that, web applications may use the following resources that have their own security considerations that you may need to become familiar with:

    How do I write tests for single-spa?

    See the documentation for unit and E2E tests.

    - - + + \ No newline at end of file diff --git a/docs/getting-started-overview/index.html b/docs/getting-started-overview/index.html index 54b387bb7..5a41d22e6 100644 --- a/docs/getting-started-overview/index.html +++ b/docs/getting-started-overview/index.html @@ -14,15 +14,15 @@ - - + +
    Version: 6.x

    Getting Started with single-spa

    JavaScript Microfrontends

    Join the chat on Slack

    single-spa is a framework for bringing together multiple JavaScript microfrontends in a frontend application. Architecting your frontend using single-spa enables many benefits, such as:

    Demos and Examples

    See our examples page.

    Architectural Overview

    single-spa takes inspiration from modern framework component lifecycles by abstracting lifecycles for entire applications. Born out of Canopy's desire to use React + react-router instead of being forever stuck with our AngularJS + ui-router application, single-spa is now a mature library that enables frontend microservices architecture aka "microfrontends". Microfrontends enable many benefits such as independent deployments, migration and experimentation, and resilient applications.

    single-spa apps consist of the following:

    1. A single-spa root config, which renders the HTML page and the JavaScript that registers applications. Each application is registered with three things:

      • A name
      • A function to load the application's code
      • A function that determines when the application is active/inactive
    2. Applications which can be thought of as single-page applications packaged up into modules. Each application must know how to bootstrap, mount, and unmount itself from the DOM. The main difference between a traditional SPA and single-spa applications is that they must be able to coexist with other applications as they do not each have their own HTML page.

      For example, your React or Angular SPAs are applications. When active, they can listen to url routing events and put content on the DOM. When inactive, they do not listen to url routing events and are totally removed from the DOM.

    The single-spa core team has put together documentation, tools, and videos showing the currently encouraged best practices with single-spa. Check out these docs for more information.

    How hard will it be to use single-spa?

    single-spa works with ES5, ES6+, TypeScript, Webpack, SystemJS, Gulp, Grunt, Bower, ember-cli, or really any build system available. You can npm install it or even just use a <script> tag if you prefer.

    While our objective is to make using single-spa as easy as possible, we should also note that this is an advanced architecture that is different from how front-end applications are typically done. This will require changes to existing paradigms as well as understanding of underlying tools.

    If you're not starting your application from scratch, you'll have to migrate your SPA to become a single-spa application.

    single-spa works in Chrome, Firefox, Safari, Edge, and IE11 (with polyfills).

    Isn't single-spa sort of a redundant name?

    Yep.

    Documentation

    The documentation is divided into several sections:

    You can help improve the single-spa website by sending pull requests to the single-spa.js.org repository.

    Quick start

    To help beginners to single-spa get started quickly we have developed create-single-spa, a utility for generating starter code. This guide will cover creating the root-config and your first single-spa application. Let's get started.

    note

    Once you've gotten some of the basics down, refer to these other single-spa examples to see more advanced usage.

    Create a root config

    1. Invoke create-single-spa to generate a root-config by running:

      npx create-single-spa --moduleType root-config

      Follow the remaining prompts with a few things in mind:

    2. Once created, navigate into the newly created root-config folder

    3. Run the start script using your preferred package manager

    4. Navigate to http://localhost:9000/ in your browser

    5. You now have a working root-config!

    Be sure to review the comments inside the generated code as well as the information in the Welcome application even if some of the content is duplicated in this guide.

    tip

    single-spa-playground.org is an alternative guide to run an application without needing to create your own root-config.

    Create a single-spa application

    1. Invoke create-single-spa to generate a single-spa application by running:

      npx create-single-spa --moduleType app-parcel

      Follow the remaining prompts to generate a single-spa application using your framework of choice

    2. Once created, navigate into the newly created application folder

    3. Run the start script using your preferred package manager

    Add shared dependencies

    Shared dependencies are used to improve performance by sharing a module in the browser through import maps declared in the root-config. Adding these at this point is conditionally optional, depending on if the generated application expects any shared dependencies.

    For example, if using React the generated Webpack config already expects React and ReactDOM to be shared dependencies, so you must add these to the import map. Vue, Angular, and Svelte don't require shared dependencies at this time.

    "react": "https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js",
    "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"

    As your architecture matures, you may add more shared dependencies in the future so don't stress about leveraging these perfectly at first.

    Register the application

    1. Return to the root-config and add your application to the import map in src/index.ejs

      The application's package.json name field is recommended
    2. Register as a single-spa application

      if not using single-spa Layout Engine

      • Open src/root-config.js
      • Remove the code for registering @single-spa/welcome as an application
      • Uncomment the sample registerApplication code and update it with the module name of your application

      if using single-spa Layout Engine

      • Remove the existing <application name="@single-spa/welcome"></application> element
      • Add your own <application name=""></application> element using the name the module name used in the import map from the previous step

    Thats it! Your first single-spa application should now be running in your root-config.


    API

    Read more at single-spa API and application api.

    Contributing

    The main purpose of this repository is to continue to evolve single-spa, making it better and easier to use. Development of single-spa, and the single-spa ecosystem happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving single-spa.

    Code of Conduct

    single-spa has adopted a Code of Conduct that we expect project participants to adhere to. Please read the full text so that you can understand what actions will and will not be tolerated.

    Contributing Guide

    Read our contributing guide to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to single-spa.

    Who's Using This?

    See user showcase.

    Is your company or project using single-spa? Let us know by submitting a PR to this section!

    - - + + \ No newline at end of file diff --git a/docs/glossary/index.html b/docs/glossary/index.html index d478affa6..03eb91cd7 100644 --- a/docs/glossary/index.html +++ b/docs/glossary/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Glossary

    Activity Function
    Application
    Application
    Helpers
    are a library that already implements single-spa lifecycle functions for a specific framework.
    Lifecycles
    module loader
    Microservices
    registerApplication
    - - + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index a65637014..6e86d7d8a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -14,13 +14,13 @@ - - + +
    - - + + \ No newline at end of file diff --git a/docs/layout-api/index.html b/docs/layout-api/index.html index a2bc68e59..247696a35 100644 --- a/docs/layout-api/index.html +++ b/docs/layout-api/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Layout Engine API

    The single-spa-layout library exposes several javascript functions as a public API.

    Browser

    In the browser, single-spa-layout exports the following functions as named exports.

    constructRoutes

    The constructRoutes API transforms your Layout Definition into an opaque "resolved routes" object. We call it "opaque" because the shape of the object is irrelevant, as you will only use it when calling other APIs within single-spa-layout.

    import { constructRoutes } from 'single-spa-layout';

    const htmlTemplate = document.querySelector('#single-spa-template')
    const layoutData = {
    props: {
    authToken: "78sf9d0fds89-0fysdiuf6sf8",
    loggedInUser: fetch('/api/user')
    },
    loaders: {
    mainContent: `<img src="loading.gif">`,
    // A single-spa parcel config
    topNav: singleSpaReact({...})
    }
    };

    const resolvedRoutes = constructRoutes(htmlTemplate, layoutData)

    Arguments

    • routesConfig (required): Routes config is a JSON Layout Definition, an HTMLElement, or a parse5 HTML element. If it is an HTMLElement, it must be a <single-spa-router> element or a <template> that contains a single-spa-router element.
    • layoutData (optional): Layout data is an optionally provided object that defines props and loaders for HTML Layouts. You can omit it if using a JSON Layout or if you do not need to define props or loaders in your HTML Layout. The layoutData object should have top level properties props and loaders that are each objects. Each of those objects' keys is the name of a prop or loader and its corresponding value.

    Return value

    An opaque resolvedRoutes object. It is opaque because you will only use the object when calling other single-spa-layout APIs and do not need to read or modify the resolvedRoutes.

    constructApplications

    The constructApplications API transforms your resolvedRoutes into single-spa application registration objects. These application registration objects are then used to call singleSpa.registerApplication().

    import { constructRoutes, constructApplications } from 'single-spa-layout';
    import { registerApplication } from 'single-spa';

    const resolvedRoutes = constructRoutes(...)
    const applications = constructApplications({
    routes: resolvedRoutes,
    loadApp: (app) => System.import(app.name)
    })
    applications.forEach(registerApplication);

    Arguments

    constructApplications accepts a single object as an argument, with the following properties:

    • routes (required): The opaque resolvedRoutes object returned from constructRoutes.
    • loadApp (required): A function that is given an application object and must return a loading function.

    Return value

    constructApplications returns an array of single-spa registration objects.

    constructLayoutEngine

    The constructLayoutEngine API transforms your resolvedRoutes and applications into a layoutEngine object. The layout engine is responsible for creating, destroying, and rearranging dom elements during route transitions.

    import { constructRoutes, constructApplications, constructLayoutEngine } from 'single-spa-layout';
    import { registerApplication, start } from 'single-spa';

    const resolvedRoutes = constructRoutes(...);
    const applications = constructApplications(...);
    const layoutEngine = constructLayoutEngine({routes: resolvedRoutes, applications: applications});

    layoutEngine.isActive(); // true
    layoutEngine.deactivate();
    layoutEngine.activate();

    applications.forEach(registerApplication);
    start();

    Arguments

    constructLayoutEngine accepts a single object as an argument, with the following properties:

    • routes (required): The opaque resolvedRoutes object returned from constructRoutes.
    • applications (required): The array of application registration objects returned from constructApplications.
    • active (optional): A boolean that indicates whether the layout engine should start out active or not. Defaults to true.

    Return Value

    A layoutEngine object, with the following properties:

    • isActive: a function that accepts no arguments and returns a boolean indicating whether the layout engine is active or not. When active, the layout engine will change the DOM during route transitions.

    • activate: a function that accepts no arguments and returns undefined. Calling this function activates the layout engine, which includes setting up routing event listeners so that the layout engine can change the DOM during route transitions.

    • deactivate: a function that accepts no arguments and returns undefined. Calling this function deactivates the layout engine, which includes tearing down all routing event listeners so that the layout engine no longer changes the DOM during route transitions.

    matchRoute

    The matchRoute API primarily exists for server rendering. It returns a filtered resolvedRoutes object that contains only the routes that match a particular string path.

    import { constructRoutes, matchRoute } from 'single-spa-layout';

    const resolvedRoutes = constructRoutes(...);

    const settingsRoutes = matchRoute(resolvedRoutes, "/settings")
    const dashboardRoutes = matchRoute(resolvedRoutes, "/dashboard")

    Arguments

    • routes (required): The opaque resolvedRoutes object returned from constructRoutes.
    • path (required): A string path representing the URL fragment to match the routes with. Note that the path is not a full URL - it only is the pathname part of a browser's URL. In server rendering contexts, this is often available as req.url.

    Return Value

    An opaque resolvedRoutes object. It is opaque because you will only use the object when calling other single-spa-layout APIs and do not need to read or modify the resolvedRoutes.

    Server

    In NodeJS, single-spa-layout exports the following functions as named exports. Note that the code is published in ESM and therefore won't work in old versions of Node. Additionally, single-spa-layout uses package entry points, which are only supported in newer versions of Node.

    // Works in newer versions of NodeJS
    import 'single-spa-layout';

    // Works in older versions of NodeJS
    import 'single-spa-layout/dist/esm/single-spa-layout-server.min.js';

    constructServerLayout

    The constructServerLayout api parses an HTML file and prepares it for rendering. This should be done once when the NodeJS server boots up, so the same serverLayout can be reused for all incoming HTTP requests.

    import { constructServerLayout } from 'single-spa-layout/server';

    const serverLayout = constructServerLayout({
    // filepath is resolved relative to the cwd (current working directory)
    // of the NodeJS process.
    filePath: "server/views/index.html"
    })

    // Alternatively, provide the html as a string
    const serverLayout = constructServerLayout({
    html: `
    <html>
    <head>
    <single-spa-router>
    <application name="nav"></application>
    </single-spa-router>
    </head>
    </html>
    `
    })

    Arguments

    constructServerLayout accepts a single object argument, with the following properties:

    • filePath (optional): A string file path to the HTML template file. Relative paths are resolved relative to process.cwd(). If filePath is omitted, html must be provided.
    • html (optional): An HTML string containing the HTML template. If html is omitted, filePath must be provided.

    Return Value

    constructServerLayout returns an opaque ServerLayout object. This object is then provided to sendLayoutHTTPResponse.

    sendLayoutHTTPResponse

    The sendLayoutHTTPResponse api sends HTTP headers and HTML content to the browser. It streams a full HTML file to the browser, so that the browser shows content as soon as it is available, instead of waiting for the entire HTML document. This is done by providing a ServerResponse object, or res to sendLayoutHTTPResponse .

    import { constructServerLayout, sendLayoutHTTPResponse } from 'single-spa-layout';
    import http from 'http';

    const serverLayout = constructServerLayout({...})

    http.createServer((req, res) => {
    sendLayoutHTTPResponse({
    res,
    serverLayout,
    urlPath: req.path,
    nonce: "yourNonceHere",
    async renderApplication({ appName, propsPromise }) {
    return {
    assets: `<link rel="stylesheet" href="/my-styles.css">`,
    content: `<button>${appName} app</button>`
    }
    },
    async retrieveApplicationHeaders({ appName, propsPromise }) {
    return {
    'x-custom-header': 'value'
    }
    },
    async renderFragment(fragmentName) {
    return `<script type="systemjs-importmap">{"imports": {}}</script>`;
    },
    async retrieveProp(propName) {
    return "prop value";
    },
    assembleFinalHeaders(allHeaders) {
    allHeaders.forEach(({appProps, appHeaders}) => {
    })

    return {}
    }
    })
    })

    Arguments

    sendLayoutHTTPResponse accepts one object argument, with the following properties:

    • res (required): A ServerResponse object. Express res objects (and likely other framework-specific objects) are supported.
    • serverLayout (required): The opaque server layout object returned from constructServerLayout.
    • urlPath (required): A string url path that will be used as the current route. Example: /settings
    • assembleFinalHeaders (required): A function that is passed all application headers and returns the final HTTP headers sent to the browser. The application headers are collected from the retrieveApplicationHeaders function into an array of AppHeaders objects. Each AppHeaders object has an appName and appHeaders object, where the appName is a string and the appHeaders is a headers object. assembleFinalHeaders must return a headers object.
    • renderApplication (optional): A function that is given information about a single-spa application and should return the HTML content (and, optionally, the assets) for that application. This function is required if a single-spa application matches the current route. The argument passed to the renderApplication function is an object with an appName string and a propsPromise promise. The propsPromise resolves with the props for the application. The function can return an object, string, Readable stream, or a Promise. Returned objects must be of format type ApplicationRenderResult = { assets: Readable | Promise<Readable> | string | Promise<string>, content: Readable | Promise<Readable> | string | Promise<string> }. Returned promises must resolve with an ApplicationRenderResult object, string or Readable stream. The assets returned from renderApplication are rendered into the <assets> element in your layout definition.
    • retrieveApplicationHeaders (optional): A function that is given information about a single-spa application and should return the HTTP response headers for that application. This function is required if a single-spa application matches the current route. The argument passed to the retrieveApplicationHeaders function is an object with an appName string and a propsPromise promise. The propsPromise resolves with the props for the application. The function can a headers object or a Promise that resolves with a headers object.
    • renderFragment (optional): A function that is given a fragment name and returns the HTML content for that fragment. This corresponds to <fragment> elements in the layout definition, and is required if the the layout definition contains a <fragment> element. The renderFragment function can return a string, Readable stream, or a Promise. Returned promises must resolve with a string or Readable stream.
    • retrieveProp (optional): A function that is given a propName and returns the prop's value. This function is required if any rendered applications have props. retrieveProp can return a value, or a promise that resolves with a value.
    • nonce (optional): A string nonce that is added to the <script> sent with the singleSpaLayoutData global variable.

    Return Value

    A promise that resolves when headers (but not necessarily HTTP response body) are sent.

    - - + + \ No newline at end of file diff --git a/docs/layout-definition/index.html b/docs/layout-definition/index.html index 5ca702d0c..435503510 100644 --- a/docs/layout-definition/index.html +++ b/docs/layout-definition/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Layout Definition

    A layout is a combination of HTMLElements, routes, and single-spa applications. Layout is defined statically in your root config to handle your top level routes and dom elements. Single-spa-layout should not be used outside of the root config; instead, a UI framework (React, Angular, Vue) should handle layouts within the applications.

    You may define layouts as either HTML templates or JSON objects. Defining in JSON is supported for organizations who prefer storing their layout definitions in a database instead of code. Both HTML and JSON layouts have the same feature set. However, storing layouts in code is generally preferred and encouraged by default. If you're just getting started with single-spa-layout, we encourage using an HTML template.

    Once you define your layout, you should constructRoutes, constructApplications, and constructLayoutEngine.

    HTML Layouts

    You may define HTML layouts either within your root config's index.html file, or within a javascript string that is parsed as HTML. We generally encourage defining the layout within your root config's index.html file.

    To define a layout within your index.html file, create a <template id="single-spa-layout"> element that contains your layout. Within the template, add a <single-spa-router> element, along with any routes, applications, and dom elements.

    Note that HTMLElements defined in your layout are static - there is no way to forcibly re-render or change them.

    <!-- index.ejs -->
    <html>
    <head>
    <template>
    <single-spa-router>
    <div class="main-content">
    <route path="settings">
    <application name="settings"></application>
    </route>
    </div>
    </single-spa-router>
    </template>
    </head>
    </html>
    // You can pass in an HTML string, too, in the browser
    const routes = constructRoutes(`
    <single-spa-router>
    <div class="main-content">
    <route path="settings">
    <application name="settings"></application>
    </route>
    </div>
    </single-spa-router>
    `);
    // With a properly configured bundler, you can import the html as a string from another file
    import layout from './microfrontends-layout.html';

    const routes = constructRoutes(layout);

    JSON Layouts

    You may define your layout as JSON, including routes, applications, and arbitrary dom elements.

    const routes = constructRoutes({
    "routes": [
    { "type": "route", "path": "settings", "routes": [
    { "type": "application", "name": "settings" }
    ]}
    ]
    });

    Layout Elements

    A layout element is an HTMLElement or JSON object that represents either a dom node, route, or application.

    <template>

    The template element is only used when defining the layout as HTML. Its purpose is to prevent its contents from being displayed by the browser, since the layout definition should not be visible to user.

    <template>
    <!-- Define your layout here -->
    <single-spa-router></single-spa-router>
    </template>

    Note that <template> elements are not fully supported in IE11. However, you do not need to polyfill template elements in order to use them in single-spa-layout. Instead, simply add style="display: none;" to the template to prevent its contents from being displayed in IE11.

    <template style="display: none;">
    <!-- Define your layout here -->
    <single-spa-router></single-spa-router>
    </template>

    <single-spa-router>

    The single-spa-router element is required as the top level container of your layout. All attributes are optional.

    <single-spa-router mode="hash|history" base="/" disableWarnings></single-spa-router>
    {
    "mode": "hash|history",
    "base": "/",
    "disableWarnings": false,
    "containerEl": "#container",
    "routes": []
    }

    Attributes

    • mode (optional): A string that must be hash or history that defaults to history. This indicates whether the routes should be matched against the Location pathname or hash.
    • base (optional): A string URL prefix that will be considered when matching route paths.
    • disableWarnings (optional): A boolean that turns of single-spa-layout's console warnings when the elements provided are incorrect.
    • containerEl (optional): A string CSS Selector or HTMLElement that is used as the container for all single-spa dom elements. Defaults to body.

    <route>

    The route element is used to control which applications and dom elements are shown for a top-level URL route. It may contain HTMLElements, applications, or other routes. Note that the route path is a URL prefix, not an exact match.

    <route path="clients">
    <application name="clients"></application>
    </route>

    <route default>
    <application name="clients"></application>
    </route>
    {
    "type": "route",
    "path": "clients",
    "routes": [
    { "type": "application", "name": "clients" }
    ],
    "default": false
    }

    Attributes

    Routes must either have a path or be a default route.

    • routes (required): An array of children elements that will be displayed when the route is active
    • path (optional): A path that will be matched against the browser's URL. The path is relative to its parent route (or the base URL). Leading and trailing / characters are unnecessary and are automatically applied. Paths may contain "dynamic segments" by using the : character ("clients/:id/reports"). Single-spa-layout uses single-spa's pathToActiveWhen function to convert the path string to an activity function. By default, the path is a prefix because it will match when any subroutes of the path match. See the exact attribute for exact matching.
    • default (optional): A boolean that determines whether this route will match all remaining URLs that have not been defined by sibling routes. This is useful for 404 Not Found pages. A sibling route is defined as any route with the same nearest-parent-route.
    • exact (optional, defaults to false): A boolean that determines whether the path should be treated as a prefix or exact match. When true the route does not activate if there are trailing characters in the URL path that are not specified in the path attribute.
    • props: An object of single-spa custom props that will be provided to the application when it is mounted. Note that these can be defined differently for the same application on different routes. You can read more about defining props within your HTML in the docs below.

    <application>

    The application element is used to render a single-spa application. Applications may be contained within route elements, or may exist at the top level as applications that will always be rendered. A container HTMLElement will be created by single-spa-layout when the application is rendered. The container HTMLElement is created with an id attribute of single-spa-application:appName such that your framework helpers will automatically use it when mounting the application.

    The same application may appear multiple times in your layout, under different routes. However, each application can only be defined once per-route.

    <!-- Basic usage -->
    <application name="appName"></application>

    <!-- Use a named loader that is defined in javascript -->
    <application name="appName" loader="mainContentLoader"></application>

    <!-- Add single-spa custom props to the application. The value of the prop is defined in javascript -->
    <application name="appName" props="myProp,authToken"></application>
    // Basic usage
    {
    "type": "application",
    "name": "appName"
    }

    // Use a single-spa parcel as a loading UI
    // You may also use Angular, Vue, etc.
    const parcelConfig = singleSpaReact({...})
    {
    "type": "application",
    "name": "appName",
    "loader": parcelConfig
    }

    // Use an HTML string as a loading UI
    {
    "type": "application",
    "name": "appName",
    "loader": "<img src='loading.gif'>"
    }

    // Add single-spa custom props
    {
    "type": "application",
    "name": "appName",
    "props": {
    "myProp": "some-value"
    }
    }

    Attributes

    • name (required): The string application name.
    • loader (optional): An HTML string or single-spa parcel config object. The loader will be mounted to the DOM while waiting for the application's loading function to resolve. You can read more about defining loaders in the docs below
    • props: An object of single-spa custom props that will be provided to the application when it is mounted. Note that these can be defined differently for the same application on different routes. You can read more about defining props within your HTML in the docs below.
    • class / className: The CSS class to apply to the container HTML element for this single-spa application. In JSON layouts, use className. In HTML layouts, use class.

    <fragment>

    The fragment element is used to specify a dynamic server-rendered portion of the template. Fragments are commonly used to inline import maps, add dynamic CSS / fonts, or customize the HTML <head> metadata. See sendLayoutHTTPResponse for more information about how fragments are rendered. Note that <fragment> elements only have meaning in server templates, not browser-only templates.

    <fragment name="importmap"></fragment>

    <fragment name="head-metadata"></fragment>

    <assets>

    The <assets> element is used to specify the location of server-rendered application assets, including CSS and fonts. When server-side rendered, the <assets> element is replaced by all the assets from the active applications on the page. Applications specify their assets as part of the renderApplication function provided to the sendLayoutHTTPResponse function.

    <assets></assets>

    <redirect>

    The <redirect> element is used to specify route redirects. On the server side, this is done with res.redirect(), which results in an HTTP 302 being sent to the browser. Within the browser, this is done by canceling navigation and then calling navigateToUrl().

    Redirects are always defined with absolute paths. This means that nesting a <redirect> inside of a route will not behave any differently than placing the redirect at the top level. All redirects should have full paths. Leading slashes are optional in those full paths.

    <redirect from="/" to="/login"></redirect>
    <redirect from="/old-settings" to="/login-settings"></redirect>

    In JSON, redirects are defined as a top-level property:

    {
    "routes": [],
    "redirects": {
    "/": "/login",
    "/old-settings": "/settings"
    }
    }

    DOM elements

    Arbitrary HTMLElements may be placed anywhere in your layout. You may define arbirary dom elements in both HTML and JSON.

    single-spa-layout only supports updating DOM elements during route transitions. Arbitrary re-renders and updates are not supported.

    DOM elements defined within a route will be mounted/unmounted as the route becomes active/inactive. If you define the same DOM element twice under different routes, it will be destroyed and recreated when navigating between the routes.

    <nav class="topnav"></nav>
    <div class="main-content">
    <button>A button</button>
    </div>

    JSON DOM Nodes

    The format of dom nodes in JSON is largely based on the parse5 format.

    HTMLElement

    Elements are defined with their nodeName as the type. HTML attributes are specified as the attrs array, where each item is an object with name and value properties.

    {
    "type": "div",
    "attrs": [
    {
    "name": "class",
    "value": "blue"
    }
    ]
    }

    Child nodes are specified via the "routes" property.

    {
    "type": "div",
    "routes": [
    {
    "type": "button"
    }
    ]
    }
    Text Nodes

    Text Nodes are defined separately from the parent containers, as separate objects with type set to #text:

    {
    "type": "#text",
    "value": "The displayed text"
    }

    Button with text:

    {
    "type": "button",
    "routes": [
    {
    "type": "#text",
    "value": "The button text"
    }
    ]
    }

    Note that text nodes may not have routes (children).

    Comment Nodes

    Comment Nodes are defined as objects whose type is #comment:

    {
    "type": "#comment",
    "value": "The comment text"
    }

    Note that comments may not have routes (children).

    Props

    Single-spa custom props may be defined on both route and application elements. Any route props will be merged together with the application props to create the final props that are passed to the single-spa lifecycle functions.

    JSON

    In a JSON layout definition, you can define props with the props property on your applications and routes:

    import { constructRoutes } from 'single-spa-layout';

    constructRoutes({
    routes: [
    { type: "application", name: "nav", props: { title: "Title" } },
    { type: "route", path: "settings", props: { otherProp: "Some value" } },
    ]
    })

    HTML

    Defining props on JSON objects is straightforward, as they are an object that can contain strings, numbers, booleans, objects, arrays, etc. However, defining complex data types in HTML is not as straightforward, since HTML attributes are always strings. To work around this, single-spa-layout allows you to name your props in the HTML, but define their values in javascript.

    <application name="settings" props="authToken,loggedInUser"></application>
    import { constructRoutes } from 'single-spa-layout';

    const data = {
    props: {
    authToken: "fds789dsfyuiosodusfd",
    loggedInUser: fetch('/api/logged-in-user').then(r => r.json())
    }
    }

    const routes = constructRoutes(document.querySelector('#single-spa-template'), data)

    The full API documentation for the constructRoutes API explains the data object in detail.

    Loading UIs

    It is often desireable to show a loading UI when waiting for an application's code to download and execute. Single-spa-layout allows you to define per-application loaders that will be mounted to the DOM while the application's loading function is pending. It is possible to share the same loading UI for multiple applications.

    A loading UI is defined as either an HTML string or as a parcel config object. HTML strings are best for static, non-interactive loaders, whereas parcels are best when you want to use a framework (Vue, React, Angular, etc) to dynamically render the loader.

    Defining loaders via javascript objects is straightforward, as they are an object that can contain strings, numbers, booleans, objects, arrays, etc. However, defining complex data types in HTML is not as straightforward, since HTML attributes are always strings. To work around this, single-spa-layout allows you to name your loaders in the HTML, but define their values in javascript.

    <application name="topnav" loader="topNav"></application>
    <application name="topnav" loader="settings"></application>
    import { constructRoutes } from 'single-spa-layout';

    // You may also use Angular, Vue, etc.
    const settingsLoader = singleSpaReact({...})

    const data = {
    loaders: {
    topNav: `<nav class="placeholder"></nav>`,
    settings: settingsLoader
    }
    }

    const routes = constructRoutes(document.querySelector('#single-spa-template'), data)

    The full API documentation for the constructRoutes API explains the data object in detail.

    Transitions

    Support for route transitions is planned, but not yet implemented. If you have interest in this feature, please provide use cases, upvotes, and feedback in this tracking issue.

    Default Routes (404 Not Found)

    Default routes are routes that activate when no other sibling routes match the current URL. They do not have a URL path and may contain any combination of DOM elements and single-spa applications.

    <single-spa-router>
    <route path="cart"></route>
    <route path="product-detail"></route>
    <route default>
    <h1>404 Not Found</h1>
    </route>
    </single-spa-router>

    Default routes are matched against their sibling routes, which allows for nesting:

    <single-spa-router>
    <route path="cart"></route>
    <route path="product-detail/:productId">
    <route path="reviews"></route>
    <route path="images"></route>
    <route default>
    <h1>Unknown product page</h1>
    </route>
    </route>
    <route default>
    <h1>404 Not Found</h1>
    </route>
    </single-spa-router>

    Sibling routes are defined as those that share a "nearest parent route." This means that they do not have to be direct siblings in your HTML/JSON, but can be nested within DOM elements:

    <single-spa-router>
    <route path="product-detail/:productId">
    <div class="product-content">
    <route path="reviews"></route>
    <route path="images"></route>
    </div>
    <!-- The reviews and images routes are siblings, since they share a nearest parent route -->
    <!-- The default route will activate when the URL does not match reviews or images -->
    <route default>
    <h1>Unknown product page</h1>
    </route>
    </route>
    </single-spa-router>

    Error UIs

    When a single-spa application fails to load, mount, or unmount, it moves to SKIP_BECAUSE_BROKEN or LOAD_ERROR status. When in SKIP_BECAUSE_BROKEN status, often nothing is visible to the user and they won't understand why the application is not showing. You can call unloadApplication to move the application back to NOT_LOADED status, which will cause single-spa to re-attempt downloading and mounting it. However, it is often desireable to show an error state when the application errors.

    An error UI is defined as either an HTML string or as a parcel config object. HTML strings are best for static, non-interactive error states, whereas parcels are best when you want to use a framework (Vue, React, Angular, etc) to dynamically render the error state. The error UI will be shown whenever the application's status is SKIP_BECAUSE_BROKEN or LOAD_ERROR.

    Note that Error UI parcels are given a prop called error that is the Error that caused the application to fail in loading/mounting.

    Defining error uis via javascript objects is straightforward, as the string or parcel can be defined in an application object via the error property:

    {
    "type": "application",
    "name": "nav",
    "error": "<h1>Oops! The navbar isn't working right now</h1>"
    }
    const myErrorParcel = singleSpaReact({...})

    {
    "type": "application",
    "name": "nav",
    "error": myErrorParcel
    }

    However, defining error uis in HTML is less straightforward, since HTML attributes are always strings and therefore can't be a parcel config object. To work around this, error UIs are named in the HTML, but defined in javascript:

    <template id="single-spa-layout">
    <single-spa-router>
    <application name="nav" error="navError"></application>
    </single-spa-router>
    </template>
    const myErrorParcel = singleSpaReact({...})

    const routes = constructRoutes(document.querySelector('#single-spa-layout'), {
    errors: {
    navError: myErrorParcel
    // alternatively:
    // navError: "<h1>Oops! The navbar isn't working right now</h1>"
    }
    })
    - - + + \ No newline at end of file diff --git a/docs/layout-overview/index.html b/docs/layout-overview/index.html index 0145e76df..e7ad58cee 100644 --- a/docs/layout-overview/index.html +++ b/docs/layout-overview/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Layout Engine

    Introduction

    Github repository

    The single-spa-layout npm package is an optional add-on to single-spa. The layout engine provides a routing API that controls your top level routes, applications, and dom elements. Using single-spa-layout makes it easier to accomplish the following:

    • DOM placement and ordering of applications.
    • Loading UIs when applications are downloaded.
    • Default routes for Not Found / 404 pages.
    • Transitions between routes (implementation pending).
    • Server side rendering of single-spa applications
    • Error pages

    In the browser, the layout engine performs two major tasks:

    1. Generate single-spa registration config from an HTML Element and/or JSON object.
    2. Listen to routing events to ensure that all DOM elements are laid out correctly before the single-spa applications are mounted.

    On the server, the layout engine performs two tasks:

    1. Construct a server layout object from an HTML template.
    2. Send an HTML document (HTTP response headers and body) to the browser, based on the server layout object and current route.

    single-spa-layout is 3.2kb gzipped (9kb ungzipped).

    Installation

    You only need to install the layout engine into your root config (not in any other application).

    npm install --save single-spa-layout

    # or
    yarn add single-spa-layout

    Browser / NodeJS support

    single-spa-layout works in all browsers supported by single-spa, including IE11. On the server, all NodeJS versions that support ESM are supported.

    Requirements

    You must use single-spa@>=5.4.0 in order for the layout engine to work. Additionally, you may not provide custom domElementGetter functions for any of your single-spa applications, as those override the configuration within single-spa-layout.

    Basic usage

    In your root html file, add a <template> element to the head. It should have a <single-spa-router> element that contains <route> elements, <application> elements, and any other dom elements:

    <html>
    <head>
    <template id="single-spa-layout">
    <single-spa-router>
    <nav class="topnav">
    <application name="@organization/nav"></application>
    </nav>
    <div class="main-content">
    <route path="settings">
    <application name="@organization/settings"></application>
    </route>
    <route path="clients">
    <application name="@organization/clients"></application>
    </route>
    </div>
    <footer>
    <application name="@organization/footer"></application>
    </footer>
    </single-spa-router>
    </template>
    </head>
    </html>

    Then inside of your root-config's JavaScript code, add the following:

    import { registerApplication, start } from 'single-spa';
    import {
    constructApplications,
    constructRoutes,
    constructLayoutEngine,
    } from 'single-spa-layout';

    const routes = constructRoutes(document.querySelector('#single-spa-layout'));
    const applications = constructApplications({
    routes,
    loadApp({ name }) {
    return System.import(name);
    },
    });
    const layoutEngine = constructLayoutEngine({ routes, applications });

    applications.forEach(registerApplication);
    start();
    - - + + \ No newline at end of file diff --git a/docs/microfrontends-concept/index.html b/docs/microfrontends-concept/index.html index d109cb827..0f4c60676 100644 --- a/docs/microfrontends-concept/index.html +++ b/docs/microfrontends-concept/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Concept: Microfrontends

    Tutorial video: Youtube / Bilibili

    A microfrontend is a microservice that exists within a browser.

    Microfrontends are sections of your UI, often consisting of dozens of components, that use frameworks like React, Vue, and Angular to render their components. Each microfrontend can be managed by a different team and may be implemented using its own framework. It is practical and suggested to use just one framework for all your microfrontends, although you may add additional frameworks when migrating or when experimenting.

    Each microfrontend has its own git repository, its own package.json file, and its own build tool configuration. As a result, each microfrontend has an independent build process and an independent deploy / CI. This generally means that each repo has fast build times.

    Comparison to Microservices

    Microservices are backend services that run in their own operating system process, control their own databases, and communicate with each other over the network.

    Compare that to microfrontends that all exist within a single browser tab: all browser JavaScript within a tab exists in a single operating system process (and even thread!). Browser JavaScript generally does not directly access databases, and communication within a browser tab happens in-memory instead of over the network.

    So what do they have in common???

    Independent builds and deployments. Think of the DOM as the shared resource that your microfrontends are owning. One microfrontend's DOM should not be touched by another microfrontend, similar to how one backend microservice's database should not be touched by any microservice except the one that owns/controls it.

    Concrete Technical Definition

    In the context of single-spa, a microfrontend is often an in-browser JavaScript module. You can read more about this in the recommended setup.

    Types of Microfrontends

    In the context of single-spa, there are three kinds of microfrontends:

    1. single-spa applications: Microfrontends that render components for a set of specific routes.
    2. single-spa parcels: Microfrontends that render components without controlling routes.
    3. utility modules: Microfrontends that export shared JavaScript logic without rendering components.

    A web app may include one or more types of microfrontends. See an in-depth comparison and our recommendations for choosing between microfrontend types.

    Communication between Microfrontends

    import { thing } from 'other-microfrontend' is the preferred way to communicate between microfrontends. Here is some documentation that goes over this in more detail.

    Relationship to single-spa

    single-spa is a small, 5kb (gzipped) npm package that orchestrates the mounting and unmounting of your microfrontends. It knows when to mount the applications based on activity functions and can do so in a framework agnostic way with the help of small adapter libraries.

    Performance

    Microfrontends are often more performant than the monoliths from which they originate. This is due to built-in lazy loading (via loading functions) and other performance-related best practices. Your monolith likely has "skeletons in its closet" - microfrontends gives you a migration path that will expose and resolve the problems caused by those skeletons. One important performance consideration is to share a single instance of large libraries (such as React, Vue, or Angular), which is highly encouraged. To do so, see our recommended setup.

    - - + + \ No newline at end of file diff --git a/docs/migrating-existing-spas/index.html b/docs/migrating-existing-spas/index.html index b60aa668d..c9f8092e1 100644 --- a/docs/migrating-existing-spas/index.html +++ b/docs/migrating-existing-spas/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -37,7 +37,7 @@ application. It is best to try to put all that you can into the JavaScript bundle, but your escape hatch is to put the things you need into your single spa config.

    - - + + \ No newline at end of file diff --git a/docs/module-types/index.html b/docs/module-types/index.html index 702b13e29..cf07c5f1d 100644 --- a/docs/module-types/index.html +++ b/docs/module-types/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -28,7 +28,7 @@ For example: Authorization. How does each application know which user is logged in? You could have each application ask the server or read a JWT but that creates duplicate work in each application. Using the utility module pattern would allow you to create one module that implements the authorization logic. This module would export any needed methods, and then your other single-spa applications could use those authorization methods by importing them. This approach also works well for data fetching.

    Examples of Utility Microfrontends

    The following are commonly implemented as a Utility Microfrontend:

    • Notification service
    • Styleguide/component library
    • Error tracking service
    • Authorization service
    • Data fetching
    - - + + \ No newline at end of file diff --git a/docs/next/index.html b/docs/next/index.html index 96c0895d2..9ac559bd4 100644 --- a/docs/next/index.html +++ b/docs/next/index.html @@ -14,13 +14,13 @@ - - + +
    - - + + \ No newline at end of file diff --git a/docs/parcels-api/index.html b/docs/parcels-api/index.html index e74898f70..5f48ff6de 100644 --- a/docs/parcels-api/index.html +++ b/docs/parcels-api/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ They both return a parcel object. The parcel object contains all additional exposed methods.

    Parcel Props

    When mounting a parcel the second argument is props, a JavaScript object of properties to be passed to the parcel. This object must have a domElement prop, which is the dom node that the parcel will mount into.

    const parcelProps = {
    customerId: 7,
    numberOfTasks: 42,
    domElement: document.createElement('div')
    }

    mountParcel

    applicationProps.mountParcel(parcelConfig, parcelProps). Each application is provided a mountParcel function. The main advantage to using an applications mountParcel function is that parcels mounted via an applications mountParcel will be automatically unmounted when the application is unmounted.

    The first argument may be either an object or a function that returns a promise that resolves with the object (a loading function).

    // Synchronous mounting
    const parcel1 = applicationProps.mountParcel(parcelConfig, parcelProps);

    // Asynchronous mounting. Feel free to use webpack code splits or SystemJS dynamic loading
    const parcel2 = applicationProps.mountParcel(() => import('./some-parcel'), parcelProps);

    mountRootParcel

    The mountRootParcel method will mount the parcel but unmount must be called manually.

    Parcel Object

    The parcel object contains the following functions and methods:

    unmount

    parcel.unmount() returns a promise that resolves once the parcel is successfully unmounted. The promise may throw an error which needs to be handled.

    mount

    parcel.mount() returns a promise that resolves once the parcel is successfully mounted. The promise can throw an error which needs to be handled.

    update

    parcel.update(props) allows you to change the props passed into a parcel. Note that not all parcels support being updated. The update function returns a promise that resolves when the parcel is finished updating. See other documentation and example for more information.

    const parcel = singleSpa.mountRootParcel(parcelConfig, parcelProps);
    parcel.update(newParcelProps);

    getStatus

    parcel.getStatus() returns a string of that parcels status. The string status is one of the following:

    • NOT_BOOTSTRAPPED: The parcel has not been bootstrapped
    • BOOTSTRAPPING: The parcel is bootstrapping but has not finished
    • NOT_MOUNTED: The parcel has bootstrapped, but is not mounted
    • MOUNTED: The parcel is currently active and mounted to the DOM
    • UNMOUNTING: The parcel is unmounting, but has not finished
    • UPDATING: The parcel is currently being updated, but has not finished
    • SKIP_BECAUSE_BROKEN: The parcel threw an error during bootstrap, mount, unmount, or update. Other parcels may continue normally, but this one will be skipped.

    loadPromise

    parcel.loadPromise() returns a promise that will resolve once the parcel has been loaded.

    bootstrapPromise

    parcel.bootstrapPromise() returns a promise that will resolve once the parcel has been bootstrapped.

    mountPromise

    parcel.mountPromise() returns a promise that will resolve once the parcel has been mounted. This is helpful for knowing exactly when a parcel has been appended to the DOM

    unmountPromise

    parcel.unmountPromise() returns a promise that will resolve once the parcel has been unmounted.

    - - + + \ No newline at end of file diff --git a/docs/parcels-overview/index.html b/docs/parcels-overview/index.html index a3974d998..e85d8b3a5 100644 --- a/docs/parcels-overview/index.html +++ b/docs/parcels-overview/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -34,7 +34,7 @@ We could do any number of things to share the functionality between application 1 and 2:

    • If both are written in the same framework we could export/import components.
    • We could reimplement creating a contact (loss of cohesion)
    • We could use single-spa parcels.

    Exporting a parcel from App1 that wraps the createContact modal component gives us the ability to share components and behavior across disparate frameworks, without losing application cohesion. App1 can export a modal as a single-spa parcel and App2 can import the parcel and use it easily. One major advantage is that in the below example the parcel/modal from App1 that is being used by App2 will also be unmounted, without unmounting/mounting of App1.

    // App1
    export const AddContactParcel = {
    bootstrap: bootstrapFn,
    mount: mountFn,
    unmount: unmountFn,
    }

    // App2
    // get the parcel configuration in this case I'm using systemJS and react
    ...
    componentDidMount() {
    SystemJS.import('App1').then(App1 => {
    const domElement = document.body
    App2MountProps.mountParcel(App1.AddContactParcel, {domElement})
    })
    }
    ...

    mountRootParcel vs mountParcel

    Single spa exposes two APIs for working with parcels. These API's are differentiated primarily by the context in which the parcel is created and how to access the API's

    mountRootParcelmountParcel
    contextsingleSpaapplication
    unmount conditionmanual onlymanual + application unmount
    api locationsingleSpa named exportprovided in lifecycle prop

    Which should I use?

    In general we suggest using the application-aware mountParcel API. mountParcel allows you to treat the parcel just like a component inside your application without considering what framework it was written in and being forced to remember to call unmount.

    How do I get the mountParcel API?

    In order to keep the function contextually bound to an application it is provided to the application as a lifecycle prop. You will need to store and manage that function yourself in your application.

    Example of storing the application specific mountParcel API:

    // App1
    let mountParcel
    export const bootstrap = [
    (props) => {
    mountParcel = props.mountParcel
    return Promise.resolve()
    },
    // more bootstrap lifecycles if necessary
    ]
    ...

    note: some libraries (such as react) support a framework specific context that makes it easy to store/manage. In those cases we've written some helper methods to abstract away the need to manage and store the mountParcel method.

    - - + + \ No newline at end of file diff --git a/docs/recommended-setup/index.html b/docs/recommended-setup/index.html index 546b87a89..425fa4908 100644 --- a/docs/recommended-setup/index.html +++ b/docs/recommended-setup/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -26,7 +26,7 @@ An import specifier is the string indicating which module to load. Examples:

    // ./thing.js is the import specifier
    import thing from './thing.js';

    // react is the import specifier
    import React from 'react';

    Specifiers that are not a URL are called "bare specifiers," such as import 'react'. Being able to alias bare specifiers to a URL is crucial to being able to use in-browser modules, which is why import maps exist.

    Import Maps are not supported in all browsers. See https://caniuse.com/import-maps for more detail. You can use SystemJS or es-module-shims to polyfill support for import maps.

    Module Federation

    Module Federation is a webpack-specific technique for sharing build-time modules. It involves each microfrontend bundling all of its dependencies, even the shared ones. This means that there are multiple copies of each shared dependency - one per microfrontend. In the browser, the first copy of the shared dependency will be downloaded, but subsequent microfrontends will reuse that shared dependency without downloading their copy of it.

    Note that Module Federation is a new feature (at the time of this writing) and requires that you use webpack@>=5. It is still an evolving technology.

    single-spa is a way of structuring your routes for microfrontends. Module Federation is a performance technique for microfrontends. They complement each other well and can be used together. Here is a YouTube video by a community member that talks about using single-spa and module federation together.

    With module federation, you must choose how you wish to load the microfrontends themselves. The single-spa core team recommends using SystemJS + import maps as a module loader for the microfrontends. Alternatively, you may use global variables and <script> elements. An example of using SystemJS to load microfrontends with module federation can be found at https://github.com/ScriptedAlchemy/mfe-webpack-demo/pull/2.

    The single-spa core team recommends choosing either import maps or module federation for your shared, third-party dependencies. We do not recommend sharing some third-party dependencies via import map and others via module federation. When choosing between the two approaches, we have a preference towards import maps, but no objection to module federation. See the shared dependencies section for a comparison.

    SystemJS

    Tutorial video: Youtube / Bilibili

    SystemJS provides polyfill-like behavior for import maps and in-browser modules. It is not a true polyfill of import maps, due to limitations of the JavaScript language in polyfilling the resolution of bare import specifiers to URLs.

    Since SystemJS is only polyfill-like, you'll need to compile your applications into System.register format instead of to ESM format. This allows for in-browser modules to be fully emulated in environments that don't support modules or import maps.

    To compile your code to System.register format, set webpack's output.libraryTarget to "system", or set rollup's format to "system".

    Shared dependencies like React, Vue, and Angular, do not publish System.register versions of their libraries. However, you can find System.register versions of the libraries in the esm-bundle project (blog post). Alternatively, SystemJS is capable of loading them via global loading or the AMD and named-exports extras.

    Another resource for sharing dependencies is the self-hosted-shared-dependencies project.

    An alternative to SystemJS that provides polyfill behavior for import maps is es-module-shims. This has the advantage of using truly native ES modules. However, it is not the single-spa core team's recommended approach for production applications, since it requires less-performant in browser parsing and modification of all your bundles.

    Lazy loading

    Tutorial video: Youtube / Bilibili

    Lazy loading is when you only download JavaScript code that the user needs for the current page, instead of all JavaScript up front. It is a technique for improving the performance of your application by decreasing the time-to-meaningful-render when you initially load the page. If you use single-spa loading functions, you already have built-in lazy loading for your applications and parcels. Since an application is an "in-browser module," this means that you are only downloading the in-browser modules in your import map when you need them.

    Often, the route-based lazy loading provided by single-spa loading functions is all that you need to ensure great performance. However, it is also possible to do lazy loading via "code splits" with your bundler (webpack or rollup). For documentation on webpack code splits, see these docs. It is recommended to use dynamic import (import()) instead of multiple entry points for code splits in a single-spa application. For code splits to work properly, you'll need to dynamically set your public path. A tool exists to help you set your public path correctly for use with systemjs - https://github.com/joeldenning/systemjs-webpack-interop.

    Local development

    Tutorial video: Youtube / Bilibili

    In contrast to monolithic frontend applications, local development with single-spa encourages only running the one microfrontend you're working on, while using deployed versions of all other microfrontends. This is important because running every single-spa microfrontend every time you want to do anything is unwieldy and cumbersome.

    To accomplish local development of only one microfrontend at a time, we can customize the URL for that microfrontend within the import map. For example, the following import map is set up for local development of the navbar application, since that's the only one pointing to a local web server. The planets and things applications are pointing to deployed (already hosted) versions of the applications.

    {
    "imports": {
    "@react-mf/navbar": "https://localhost:8080/react-mf-navbar.js",
    "@react-mf/planets": "https://react.microfrontends.app/planets/2717466e748e53143474beb6baa38e3e5320edd7/react-mf-planets.js",
    "@react-mf/things": "https://react.microfrontends.app/things/7f209a1ed9ac9690835c57a3a8eb59c17114bb1d/react-mf-things.js"
    }
    }

    A tool called import-map-overrides exists to customize your import map through an in-browser UI. This tool will automatically let you toggle one or more microfrontends between your localhost and the deployed version.

    Alternatively, you can use standalone-single-spa-webpack-plugin, which allows you to develop each application in standalone mode. Another alternative is to always run the single-spa root config locally, in addition to whichever microfrontends you're developing.

    The single-spa core team recommends development on deployed environments via import-map-overrides, as we find that to be the best developer experience, since it allows you to only start one project at a time while also ensuring there's no difference between the local environment and fully-integrated deployed environment. However, there are cases when running the root config locally or using standalone-single-spa-webpack-plugin can be useful.

    Build tools (Webpack / Rollup)

    Tutorial video: Youtube / Bilibili

    It is highly encouraged to use a bundler such as webpack, rollup, parceljs, pikapack, etc. Webpack is an industry-standard for compiling many JavaScript source files into one or more production JavaScript bundles.

    Below are some tips for configuring your bundler to be consumable by SystemJS and single-spa. Note that if you're using create-single-spa that these are all set up for you. We leave these instructions here not to overwhelm you with webpack configuration hell, but rather to help you if you choose not to use create-single-spa.

    1. Set the output target to system. In webpack, this is done via output.libraryTarget
    2. Use a single entry point, with dynamic imports for any code splitting that you'd like to accomplish. This best matches the "one bundled project = one in-browser module" paradigm encouraged by the single-spa core team.
    3. Do not use webpack's optimization configuration options, as they make it harder to load the outputted JavaScript files as a single in-browser JavaScript module. Doing so does not make your bundle less optimized - dynamic imports are a viable strategy for accomplishing optimized bundles.
    4. Follow the systemjs docs for webpack.
    5. Consider using systemjs-webpack-interop to create or verify your webpack config.
    6. Use systemjs-webpack-interop to set your webpack public path "on the fly".
    7. Do not set webpack output.library. SystemJS does not need a name, and in fact does not support named modules without additional configuration.
    8. Consider turning off webpack hashing for both entry and code split bundles. It is often easier to add in a commit hash during deployment of your microfrontend via your CI environment variables.
    9. Configure webpack-dev-server to not do host checks. (docs).
    10. Configure webpack-dev-server for CORS by setting {headers: {'Access-Control-Allow-Origin': '*'}}. (docs)
    11. If developing on https, configure webpack-dev-server for HTTPS. Also consider trusting SSL certificates from localhost.
    12. Make sure that your webpack externals are correctly configured for any shared, in-browser modules that you are importing.
    13. Set output.jsonpFunction to be a unique string for this project. Since you'll have multiple webpack bundles running in the same browser tab, a collision of the jsonpFunction could result in webpack modules getting mixed between bundles.
    14. Set sockPort, sockPath, and sockHost inside of your devServer configuration.
    15. For webpack, set output.devtoolNamespace to your MFE's name. This helps namespace your sourcemaps to each MFE.

    For a bit more information specific to webpack code splits, see the code splits FAQ.

    Utility modules (styleguide, API, etc)

    A "utility module" is an in-browser JavaScript module that is not a single-spa application or parcel. In other words, it's only purpose is to export functionality for other microfrontends to import.

    Common examples of utility modules include styleguides, authentication helpers, and API helpers. These modules do not need to be registered with single-spa, but are important for maintaining consistency between several single-spa applications and parcels.

    Example code in a utility module:

    // In a repo called "api", you may export functions from the repo's entry file.
    // These functions will be available to single-spa application, parcels, and other in-browser modules
    // via an import statement.

    export function authenticatedFetch(url, init) {
    return fetch(url, init).then(r => {
    // Maybe do some auth stuff here
    return r.json()
    })
    }

    Example code in a single-spa application that is using the utility module:

    // Inside of a single-spa application, you can import the functions from the 'api' repo
    import React from 'react'
    import { authenticatedFetch } from '@org-name/api';

    export function Foo(props) {
    React.useEffect(() => {
    const abortController = new AbortController()
    authenticatedFetch(`/api/clients/${props.clientId}`, {signal: abortController.signal})
    .then(client => {
    console.log(client)
    })

    return () => {
    abortController.abort()
    }
    }, [props.clientId])

    return null
    }

    To make utility modules work, you must ensure that your webpack externals and import map are properly configured. An example of a working styleguide may be found at https://github.com/vue-microfrontends/styleguide.

    Cross microfrontend imports

    Example - exporting a shared component, importing a shared component, and required webpack config.

    You can import and export functions, components, logic, data, event emitters, and environment variables between your microfrontends that are in different git repos and JavaScript bundles. Each microfrontend should have a single entry file that serves as the "public interface" that controls what is exposed outside of the microfrontend.

    To make cross microfrontend imports possible, configure your bundler so that the microfrontends are treated as "externals" (webpack docs / rollup docs). Marking them as externals ensures that they are treated as in-browser modules instead of build-time modules.

    // Inside of the "entry file" for a utility module called @org-name/auth,
    // expose your public interface that other microfrontends can access.
    // Often this is within the main.js or main.single-spa.js file.

    export function userHasAccess(permission) {
    return loggedInUser.permissions.some(p => p === permission);
    }
    import { userHasAccess } from '@org-name/auth'

    // Inside of a single-spa application, import and use a util function from a different microfrontend
    const showLinkToInvoiceFeature = userHasAccess('invoicing');
    // In your webpack config, mark @org-name auth as a webpack external
    module.exports = {
    externals: ['@org-name/auth'],

    // Alternatively, mark *all* org-name packages as externals
    // externals: [/^@org-name\/.+/]
    }

    Shared dependencies

    For performance, it is crucial that your web app loads large JavaScript libraries only once. Your framework of choice (React, Vue, Angular, etc) should only be loaded on the page a single time.

    It is not advisable to make everything a shared dependency, because shared dependencies must be upgraded at once for every microfrontend that uses them. For small libraries, it is likely acceptable to duplicate them in each microfrontend that uses them. For example, react-router is likely small enough to duplicate, which is nice when you want to upgrade your routing one microfrontend at a time. However, for large libraries like react, momentjs, rxjs, etc, you may consider making them shared dependencies.

    There are two approaches to sharing dependencies:

    1. In-browser modules with import maps
    2. Build-time modules with module federation

    You may use either one, or both. We currently recommend only using import maps, although we have no objection to module federation.

    Comparison of approaches

    ApproachShare dependenciesBundler requirementsManaging dependencies
    Import MapsFully supportedAny bundlershared dependecies repo
    Module FederationFully supportedOnly webpack@>=5multiple webpack configs

    Sharing with Import Maps

    To share a dependency between microfrontends with Import Maps, you should use webpack externals, rollup externals, or similar. Marking libraries as external tells your bundler to not use the version in your node_modules, but rather to expect the library to exist as an in-browser module.

    To make the shared dependencies available as in-browser modules, they must be present in your import map. A good way of managing them is to create a repository called shared-dependencies that has a partial import map in it. The CI process for that repository updates your deployed import map. Upgrading the shared dependencies can then be achieved by making a pull request to that repository.

    Not all libraries publish their code in a suitable format for SystemJS consumption. In those cases, check https://github.com/esm-bundle for a SystemJS version of those libraries. Alternatively, you may use SystemJS extras to support UMD bundles, which are often available.

    Another option for finding a suitable version of a library for your import map is to use the JSPM CDN, which provides precompiled SystemJS versions of every package on npm (example: https://system-cdn.jspm.io/npm:@material-ui/core@4.11.3/index.js). See https://jspm.org/docs/cdn for more info. You can generate an import map for your shared dependencies at https://generator.jspm.io/.

    Another option for hosting shared dependencies is self-hosted-shared-dependencies, which generates a directory of third party packages that you can self host on your server / CDN.

    An example of a shared-dependencies repo, along with a functioning CI process for it, can be found at https://github.com/polyglot-microfrontends/shared-dependencies.

    Sharing with Module Federation

    At the time of this writing, module federation is new and still changing. Check out this example repo which uses systemjs to load the microfrontends, but module federation to share react, react-dom, and react-router.

    Deployment and Continuous Integration (CI)

    Tutorial video (Part 1): Youtube / Bilibili

    Tutorial video (Part 2): Youtube / Bilibili

    Example CI configuration files

    Microfrontends are built and deployed completely independently. This means that the git repository, CI, build, and deployments all occur without going through a centralized repository. For this reason, monorepos are not encouraged for microfrontends. CI for monorepos can be configured to only build and deploy the packages that have changed but it is often more complex. Modern CI platforms such as AWS Amplify and Vercel are starting to have built-in support for monorepos however.

    There are two steps to deploying a microfrontend.

    1. Uploading production JavaScript bundles to a web server / CDN. It is encouraged to use a CDN such as AWS S3 + Cloudfront, Google Cloud Storage, Microsoft Azure Storage, Digital Ocean Spaces, etc because of their superior availability, caching, and performance due to edge locations. The JavaScript files that you upload are completely static. It is encouraged to always write new files to the CDN instead of overwriting files.
    2. Updating your import map to point to the newly deployed file.

    The implementation of Step 1 is dependent on the infrastructure you're using for your CDN. The AWS CLI (aws s3 sync), Google gsutil (gsutil cp), etc are easy ways of accomplishing this.

    If you prefer or require using docker containers rather than Cloud Storage like S3, see https://github.com/single-spa/docker-import-maps-mfe-server

    For the implementation of Step 2, you have a choice:

    a) Your CI makes a curl HTTP call to a running instance of import-map-deployer, which updates the import map in a concurrent-safe way. b) Your CI runner pulls down the import map, modify it, and reuploads it.

    The advantage of a) is that it is concurrent-safe for multiple, simultaneous deployments. Without a concurrent-safe solution, there might be multiple processes pulling down and reuploading the import map at the same time, which could result in a race condition where one CI process thinks it successfully updated the import map when in reality the other CI process wrote the import map later, having based its changes on a stale version of the import map.

    The advantage of b) is that it doesn't require running the import-map-deployer in your production environment. Ultimately, you should choose whichever option makes sense for your organization.

    Another option of deploying and making sure the latests javascript files are beeing used is making use of redirect (HTTP status code 302). The single-spa import map uses the unhashed url of the javascript file. eg: dist/app.js. Then when the requests for this file comes to the server it is redirected to the actual deployed file eg. dist/app.123abc.js and this is then served to the client. This way the import map never has to be updated and the microfrontend can be separately deployed.

    Applications versus parcels versus utility modules

    Single-spa has different categories of microfrontends. It is up to you where and how you use each of them. However, the single-spa core team recommends the following:

    Many route-based single-spa applications, very few single-spa parcels

    1. Prefer splitting microfrontends by route, instead of by components within a route. This means preferring single-spa applications over single-spa parcels whenever possible. The reason for this is that transitions between routes often involve destroying and recreating most UI state, which means your single-spa applications on different routes do not need to ever share UI state.
    2. Move fixed navigation menus into their own single-spa applications. Implement their activity functions to be active by default, only unmounting for the login page.
    3. Create utility modules for your core component library / styleguide, for shared authentication / authorization code, and for global error handling.
    4. If you are only using one framework, prefer framework components (i.e. React, Vue, and Angular components) over single-spa parcels. This is because framework components interop easier with each other than when there is an intermediate layer of single-spa parcels. You can import components between single-spa applications You should only create a single-spa parcel if you need it to work with multiple frameworks.

    Inter-app communication

    A good architecture is one in which microfrontends are decoupled and do not need to frequently communicate. Following the guidelines above about applications versus parcels helps you keep your microfrontends decoupled. Route-based single-spa applications inherently require less inter-app communication.

    There are three things that microfrontends might need to share / communicate:

    1. Functions, components, logic, and environment variables.
    2. API data
    3. UI state

    Functions, components, logic, and environment variables

    We recommend using cross microfrontend imports to share functions, components, logic, and environment variables.

    API Data

    Example - exporting a fetchWithCache function and importing the function.

    API data often does not need to be shared between microfrontends, since each single-spa application controls different routes and different routes often have different data. However, occasionally you do need to share API data between microfrontends. An in-memory JavaScript cache of API objects is a solution used by several companies to solve this. For React users, this is similar to Data Fetching with Suspense, where the fetching logic for routes is split out from the component code that uses the data.

    // Inside of your api utility module, you can lazily fetch data either when another microfrontend calls your exported
    // functions, or eagerly fetch it when the route changes.
    let loggedInUserPromise = fetch('...').then(r => {
    if (r.ok) {
    return r.json()
    } else {
    throw Error(`Error getting user, server responded with HTTP ${r.status}`)
    }
    })

    export function getLoggedInUser() {
    return loggedInUserPromise;
    }
    import { getLoggedInUser } from '@org-name/api';

    // Inside of app1, you can import something from an "api" utility module
    getLoggedInUser().then(user => {
    console.log('user', user);
    });

    UI State

    If two microfrontends are frequently passing state between each other, consider merging them. The disadvantages of microfrontends are enhanced when your microfrontends are not isolated modules.

    UI State, such as "is the modal open," "what's the current value of that input," etc. largely does not need to be shared between microfrontends. If you find yourself needing constant sharing of UI state, your microfrontends are likely more coupled than they should be. Consider merging them into a single microfrontend.

    Under the rare circumstances where you do need to share UI state between single-spa applications, an event emitter may be used to do so. Below are a few examples of event emitters that might help you out.

    1. Observables / Subjects (rxjs) - one microfrontend emits new values to a stream that can be consumed by any other microfrontend. It exports the observable to all microfrontends from its in-browser module, so that others may import it.
    2. CustomEvents - browsers have a built-in event emitter system that allows you to fire custom events. Check out this documentation for more information. Firing the events with window.dispatchEvent allows you to subscribe in any other microfrontend with window.addEventListener.
    3. Any other pub/sub event emitter system.

    State management

    The single-spa core team cautions against using redux, mobx, and other global state management libraries. However, if you'd like to use a state management library, we recommend keeping the state management tool specific to a single repository / microfrontend instead of a single store for all of your microfrontends. The reason is that microfrontends are not truly decoupled or framework agnostic if they all must use a global store. You cannot independently deploy a microfrontend if it relies on the global store's state to be a specific shape or have specific actions fired by other microfrontends - to do so you'd have to think really hard about whether your changes to the global store are backwards and forwards compatible with all other microfrontends. Additionally, managing global state during route transitions is hard enough without the complexity of multiple microfrontends contributing to and consuming the global state.

    Instead of a global store, the single-spa core team recommends using local component state for your components, or a store for each of your microfrontends. See the above section "Inter-app communication" for more related information.

    - - + + \ No newline at end of file diff --git a/docs/separating-applications/index.html b/docs/separating-applications/index.html index 8859ea4ab..22b56e67c 100644 --- a/docs/separating-applications/index.html +++ b/docs/separating-applications/index.html @@ -14,8 +14,8 @@ - - + +
    @@ -23,7 +23,7 @@ This allows for separate builds and deployment without having separate code repositories.

    Option 4: Dynamic Module Loading

    Create a root application which can allow single-spa applications to deploy themselves separately. To do so, create a manifest file that the single-spa applications update during their deployment process, which controls which versions of the single-spa applications are "live". Then change which JavaScript file is loaded based on the manifest.

    Changing which JavaScript file is loaded for each child application can be done in many ways.

    1. Web server: have your webserver create a dynamic script tag for the "live" version of each single-spa application.
    2. Use a module loader such as SystemJS that can download and execute JavaScript code in the browser from dynamic urls.

    Comparison

    Separate code repositories possibleIndependent CI buildsSeparate deploymentsExamples
    NPM Packages1
    Monorepo111
    Module loading1 2 3
    - - + + \ No newline at end of file diff --git a/docs/shared-webpack-configs/index.html b/docs/shared-webpack-configs/index.html index 305c3f291..3188ba615 100644 --- a/docs/shared-webpack-configs/index.html +++ b/docs/shared-webpack-configs/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Shared Webpack configs

    single-spa also publishes a few shared Webpack config packages that include basics needed for creating single-spa applications. These configs are used by applications generated with create-single-spa, and can be used as a starting point to extend/modify a custom configuration for your organization/team.

    These packages are housed within the create-single-spa repository.

    webpack-config-single-spa

    Github project

    A shareable, customizable webpack config that is used for single-spa modules.

    Installation

    npm install --save-dev webpack-config-single-spa webpack-merge

    # or
    yarn add --dev webpack-config-single-spa webpack-merge

    Usage

    const singleSpaDefaults = require('webpack-config-single-spa');
    const { merge } = require('webpack-merge'); // webpack-merge@5.0.3+

    module.exports = (webpackConfigEnv, argv) => {
    const defaultConfig = singleSpaDefaults({
    // The name of the organization this application is written for
    orgName: 'name-of-company',
    // The name of the current project. This usually matches the git repo's name
    projectName: 'name-of-project',
    // See https://webpack.js.org/guides/environment-variables/#root for explanation of webpackConfigEnv
    webpackConfigEnv,
    // The CLI commands in the package.json script that triggered this build
    argv,
    // optional
    // This changes whether package names that start with @your-org-name are
    // treated as webpack externals or not. Defaults to true
    orgPackagesAsExternal: true,

    // optional, defaults to 1
    // This is the rootDirectoryLevel that is passed to https://github.com/joeldenning/systemjs-webpack-interop
    rootDirectoryLevel: 1,

    // optional, defaults to false
    // Disable html-webpack-plugin (and standalone-single-spa-webpack-plugin) entirely
    // This is intended for root configs, but can be used in other cases, too
    disableHtmlGeneration: false,
    });

    return merge(defaultConfig, {
    // modify the webpack config however you'd like to by adding to this object
    });
    };

    webpack-config-single-spa-react

    Github project

    A shareable, customizable webpack config that adds react-specific configuration to webpack-config-single-spa.

    Installation

    npm install --save-dev webpack-config-single-spa-react webpack-merge

    # or
    yarn add --dev webpack-config-single-spa-react webpack-merge

    Usage

    const singleSpaDefaults = require('webpack-config-single-spa-react');
    const webpackMerge = require('webpack-merge');

    module.exports = (webpackConfigEnv, argv) => {
    const defaultConfig = singleSpaDefaults({
    // The name of the organization this application is written for
    orgName: 'name-of-company',
    // The name of the current project. This usually matches the git repo's name
    projectName: 'name-of-project',
    // See https://webpack.js.org/guides/environment-variables/#root for explanation of webpackConfigEnv
    webpackConfigEnv,
    // The CLI commands in the package.json script that triggered this build
    argv,
    // optional
    // This changes whether package names that start with @your-org-name are
    // treated as webpack externals or not. Defaults to true
    orgPackagesAsExternal: true,

    // optional, defaults to 1
    // This is the rootDirectoryLevel that is passed to https://github.com/joeldenning/systemjs-webpack-interop
    rootDirectoryLevel: 1,

    // optional, defaults to {}
    // This controls the options given to standalone-single-spa-webpack-plugin
    // See https://github.com/single-spa/standalone-single-spa-webpack-plugin#usage
    standaloneOptions: {},
    });

    // modify the webpack config however you'd like to by adding to this object
    });
    };

    webpack-config-single-spa-ts

    Github project

    A shareable, customizable webpack config that adds typescript-specific configuration to webpack-config-single-spa. Note that webpack-config-single-spa-ts has a peerDependency on typescript.

    Installation

    npm install --save-dev webpack-config-single-spa-ts webpack-merge

    # or
    yarn add --dev webpack-config-single-spa-ts webpack-merge

    Usage

    const webpackMerge = require('webpack-merge');
    const singleSpaDefaults = require('webpack-config-single-spa-ts');

    module.exports = (webpackConfigEnv, argv) => {
    const defaultConfig = singleSpaDefaults({
    // The name of the organization this application is written for
    orgName: 'name-of-company',
    // The name of the current project. This usually matches the git repo's name
    projectName: 'name-of-project',
    // See https://webpack.js.org/guides/environment-variables/#root for explanation of webpackConfigEnv
    webpackConfigEnv,
    // The CLI commands in the package.json script that triggered this build
    argv,
    });

    return webpackMerge.smart(defaultConfig, {
    // modify the webpack config however you'd like to by adding to this object
    });
    };
    const singleSpaTs = require('webpack-config-single-spa-ts');

    // Alternatively, you may modify a webpack config directly
    const myOtherWebpackConfig = {
    /* ... */
    };
    const finalConfig = singleSpaDefaults.modifyConfig(myOtherWebpackConfig);

    webpack-config-single-spa-react-ts

    Github project

    A shareable, customizable webpack config that creates a webpack config that works with both react and typescript. Note that webpack-config-single-spa-react-ts simply merges the config from webpack-config-single-spa-react with that of webpack-config-single-spa-ts.

    Installation

    npm install --save-dev webpack-config-single-spa-react-ts webpack-merge

    # or
    yarn add --dev webpack-config-single-spa-react-ts webpack-merge

    Usage

    const webpackMerge = require('webpack-merge');
    const singleSpaDefaults = require('webpack-config-single-spa-react-ts');

    module.exports = (webpackConfigEnv, argv) => {
    const defaultConfig = singleSpaDefaults({
    // The name of the organization this application is written for
    orgName: 'name-of-company',

    // The name of the current project. This usually matches the git repo's name
    projectName: 'name-of-project',

    // optional
    // This changes whether package names that start with @your-org-name are
    // treated as webpack externals or not. Defaults to true
    orgPackagesAsExternal: true,

    // See https://webpack.js.org/guides/environment-variables/#root for explanation of webpackConfigEnv
    webpackConfigEnv,

    // The CLI commands in the package.json script that triggered this build
    argv,

    // optional, defaults to 1
    // This is the rootDirectoryLevel that is passed to https://github.com/joeldenning/systemjs-webpack-interop
    rootDirectoryLevel: 1,

    // optional, defaults to false.
    // When true, this removes html-webpack-plugin and standalone-single-spa-webpack-plugin
    disableHtmlGeneration: false,
    });

    return webpackMerge.smart(defaultConfig, {
    // modify the webpack config however you'd like to by adding to this object
    });
    };

    Custom Webpack configuration

    Our shared Webpack configs are intended to be extensible to fit the requirements of your applications. These custom config options can be made in each project's webpack.config.js file generated by create-single-spa, or used as the basis for a tailored shared config for your organization.

    • Use require.resolve to reuse loaders that are included as dependencies
      • this is especially useful for reusing any of webpack-config-single-spa-*'s dependencies
    • webpack-merge does not support merging configs exported as a function, which may be relevant if creating shared config packages. webpack-config-single-spa-* configs require webpackConfigEnv and argv parameters which necesitates exporting a config function but return a plain object. This makes it compatible with webpack-merge.
    • Use webpack-merge's mergeWithRules function to merge and de-duplicate webpack rules

    Example: load SVGs as components

    You must also install @svgr/webpack.

    const singleSpaDefaults = require('webpack-config-single-spa');
    const { mergeWithRules } = require('webpack-merge');

    const merge = mergeWithRules({
    module: {
    rules: {
    // replace the entire `rule` if the `test` property matches
    test: 'match',
    use: 'replace',
    },
    },
    });

    module.exports = (env, argv) => {
    const defaultConfig = singleSpaDefaults({
    orgName: 'abcde',
    projectName: 'fghij',
    webpackConfigEnv: env,
    argv,
    });

    const config = merge(defaultConfig, {
    module: {
    rules: [
    {
    test: /\.svg$/i,
    use: [{ loader: '@svgr/webpack' }],
    },
    ],
    },
    });

    // console.dir(config, null, 2) // useful for debugging
    return config;
    };

    Replacing plugins

    Use webpack-merge's mergeWithCustomize to resolve duplicate plugins or replace instances. Duplicate plugins often result in cryptic errors!

    When referencing a loader that is installed as a dependency of webpack-config-single-spa, use require.resolve to ensure its path is resolved correctly.

    Example: replace HtmlWebpackPlugin instance

    const { mergeWithCustomize, unique } = require('webpack-merge');
    const singleSpaDefaults = require('webpack-config-single-spa');
    const HtmlWebpackPlugin = require('html-webpack-plugin');

    const merge = mergeWithCustomize({
    customizeArray: unique(
    'plugins',
    ['HtmlWebpackPlugin'],
    plugin => plugin.constructor && plugin.constructor.name,
    ),
    });

    module.exports = (env, argv) => {
    const orgName = 'example';
    const myEnv = process.env.NODE_ENV || 'development';

    const defaultConfig = singleSpaDefaults({
    orgName,
    projectName: 'custom-root-config',
    webpackConfigEnv: env,
    argv,
    });

    const config = merge(defaultConfig, {
    plugins: [
    new HtmlWebpackPlugin({
    inject: false,
    template: 'src/custom.ejs',
    templateParameters: {
    isLocal: env?.isLocal,
    // additional templateParameters can now be supplied
    orgName,
    environment: myEnv,
    },
    }),
    ],
    });

    // console.dir(config, null, 2) // useful for debugging
    return config;
    };
    - - + + \ No newline at end of file diff --git a/docs/single-spa-playground/index.html b/docs/single-spa-playground/index.html index a66b07b81..dd3427652 100644 --- a/docs/single-spa-playground/index.html +++ b/docs/single-spa-playground/index.html @@ -14,13 +14,13 @@ - - + + - - + + \ No newline at end of file diff --git a/docs/ssr-overview/index.html b/docs/ssr-overview/index.html index 0e5f5feae..2c495016d 100644 --- a/docs/ssr-overview/index.html +++ b/docs/ssr-overview/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Server Side Rendering

    Intro to SSR

    In the context of single page applications (SPAs), server-side rendering (SSR) refers to dynamic generation of the HTML page that is sent from web server to browser. In a single page application, the server only generates the very first page that the user requests, leaving all subsequent pages to be rendered by the browser.

    To accomplish server-side rendering of an SPA, javascript code is executed in NodeJS to generate the initial HTML. In the browser, the same javascript code is executed during a "hydration" process, which attaches event listeners to the HTML. Most popular UI Frameworks (Vue, React, Angular, etc) are capable of executing in both NodeJS and the browser, and offer APIs for both generating the server HTML and hydrating it in the browser. Additionally, there are popular frameworks such as NextJS and Nuxt which simplify the developer experience of server-side rendering.

    In the context of microfrontends, server-side rendering refers to assembling the HTML from multiple, separate microfrontends. Each microfrontend controls a fragment of the HTML sent from web server to browser, and hydrate their fragment once initialized in the browser.

    Purpose

    A primary purpose of server-side rendering is improved performance. Server rendered pages often display their content to users faster than their static counterparts, since the user is presented with the content before javascript resources have been initialized. Other reasons for SSR include improved search engine optimization (SEO).

    Server rendered applications are generally harder to build and maintain, since the code has to work on both client and server. Additionally, SSR often complicates the infrastructure needed to run your application, since many SPA + SSR solutions require NodeJS, which is not required in production for client-only SPAs.

    Example

    The isomorphic-microfrontends example shows React server-rendered microfrontends. You can view the live demo of the code at https://isomorphic.microfrontends.app.

    Implementation Overview

    The ultimate goal of server-side rendering is to generate an HTTP response that the browser will display to the user while javascript is hydrating. Most microfrontend server-side rendering implementations, including single-spa's recommended approach, do this with the following steps:

    1. Layout - Identify which microfrontends to render for the incoming HTTP request, and where within the HTML they will be placed. This is usually route based.
    2. Fetch - Begin rendering the HTML for each microfrontend to a stream.
    3. Headers - Retrieve HTTP response headers from each microfrontend. Merge them together and send the result as the HTTP response headers to the browser.
    4. Body - Send the HTTP response body to the browser, which is an HTML document consisting of static and dynamic parts. This involves waiting for each microfrontend's stream to end before proceeding to the next portion of HTML.
    5. Hydrate - Within the browser, download all javascript needed and then hydrate the HTML.

    1. Layout

    To define an HTML template that lays out your page, first choose a "microfrontend layout middleware":

    1. single-spa-layout: The official layout engine for single-spa.
    2. Tailor: A popular, battle tested layout engine that predates single-spa-layout and is not officially affiliated with single-spa.
    3. TailorX: An actively maintained fork of Tailor that is used by Namecheap in their single-spa website. The single-spa core team collaborated with the creators of TailorX when authoring single-spa-layout, taking some inspiration from it.

    We generally recommend single-spa-layout, although choosing one of the other options might make sense for your situation, since single-spa-layout is newer and has been used less than Tailor/TailorX.

    With single-spa-layout, you define a single template that handles all routes. Full documentation.

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Isomorphic Microfrontends</title>
    <meta
    name="importmap-type"
    content="systemjs-importmap"
    server-cookie
    server-only
    />
    <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.0.0/dist/import-map-overrides.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.6.1/dist/system.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.6.1/dist/extras/amd.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.6.1/dist/extras/named-exports.min.js"></script>
    </head>
    <body>
    <template id="single-spa-layout">
    <single-spa-router>
    <nav>
    <application name="@org-name/navbar"></application>
    </nav>
    <main>
    <route path="settings">
    <application name="@org-name/settings"></application>
    </route>
    <route path="home">
    <application name="@org-name/home"></application>
    </route>
    </main>
    </single-spa-router>
    </template>
    <fragment name="importmap"></fragment>
    <script>
    System.import('@org-name/root-config');
    </script>
    <import-map-overrides-full
    show-when-local-storage="devtools"
    dev-libs
    ></import-map-overrides-full>
    </body>
    </html>

    2. Fetch

    Your microfrontend layout middleware (see Layout section) determines which microfrontends match the HTTP request's route. The middleware then fetches the HTTP response headers and HTML content for each microfrontend.

    When using single-spa-layout, fetching each microfrontend is handled by the renderApplication function that is provided to renderServerResponseBody.

    The method of fetching the headers and HTML content can vary, since single-spa-layout allows for any arbitrary, custom method of fetching. However, in practice, there are two popular approaches, which are described below. We generally recommend dynamic module loading as the primary method, since it requires less infrastructure to set up and has arguably (slightly) better performance. However, HTTP requests have some advantages, too, and it's also possible for different microfrontends to be implemented with different fetch methods.

    A. Module loading

    Module loading refers to loading javascript code using import and import(). Using module loading, the implementation of fetching the headers and content for each microfrontend is done purely within a single web server and operating system process:

    import('@org-name/navbar/server.js').then(navbar => {
    const headers = navbar.getResponseHeaders(props);
    const htmlStream = navbar.serverRender(props);
    });

    In the context of single-spa-layout, this is done inside of the renderApplication function:

    import {
    constructServerLayout,
    sendLayoutHTTPResponse,
    } from 'single-spa-layout/server';
    import http from 'http';

    const serverLayout = constructServerLayout({
    filePath: 'server/views/index.html',
    });

    http
    .createServer((req, res) => {
    const { bodyStream } = sendLayoutHTTPResponse({
    res,
    serverLayout,
    urlPath: req.path,
    async renderApplication({ appName, propsPromise }) {
    const [app, props] = await Promise.all([
    import(`${props.name}/server.mjs`, propsPromise),
    ]);
    return app.serverRender(props);
    },
    async retrieveApplicationHeaders({ appName, propsPromise }) {
    const [app, props] = await Promise.all([
    import(`${props.name}/server.mjs`, propsPromise),
    ]);
    return app.getResponseHeaders(props);
    },
    async retrieveProp(propName) {
    return 'prop value';
    },
    assembleFinalHeaders(appHeaders) {
    return Object.assign(
    {},
    ...Object.values(allHeaders).map(a => a.appHeaders),
    );
    },
    renderFragment(name) {
    // not relevant to the docs here
    },
    });

    bodyStream.pipe(res);
    })
    .listen(9000);

    To facilitate independent deployments of our microfrontends, such that the web server does not have to reboot/redeploy every time we update every microfrontend, we can use dynamic module loading. Dynamic module loading refers to loading a module from a dynamic location - often from somewhere on disk or over the network. By default, NodeJS will only load modules from relative URLs or from the node_modules directory, but dynamic module loading allows you to load modules from any arbitrary file path or URL.

    A pattern to facilitate independent deployments via dynamic module loading is for each microfrontend's deployment to upload one or more javascript files to a trusted CDN, and then use dynamic module loading to load a certain version of the code on the CDN. The web server polls for new versions of each microfrontend and downloads the newer versions as they are deployed.

    To accomplish dynamic module loading, we can use NodeJS module loaders. Specifically, @node-loader/import-maps and @node-loader/http allow us to control where the module is located and how to download it over the network. The code below shows how a server-side import map facilitates dynamic module loading

    Before deployment of navbar:

    {
    "imports": {
    "@org-name/navbar/": "https://cdn.example.com/navbar/v1/"
    }
    }

    After deployment of navbar:

    {
    "imports": {
    "@org-name/navbar/": "https://cdn.example.com/navbar/v2/"
    }
    }

    The import map itself is hosted on the CDN, so that deployments may occur without restarting the web server. An example of this setup is shown here.

    B. HTTP Request

    It is also possible to implement the fetching of HTML content and HTTP headers from microfrontends using HTTP requests. In this setup, each microfrontend must run as a deployed web server. The root web server (responsible for responding to the browser) makes an HTTP call to each of the microfrontends' web servers. Each microfrontend web server responds with an HTML page as response body, along with its HTTP response headers. The response body is streamed to the root web server so that it can send the bytes as soon as possible to the browser.

    In the context of single-spa-layout, this is done with the renderApplication function:

    import {
    constructServerLayout,
    sendLayoutHTTPResponse,
    } from 'single-spa-layout/server';
    import http from 'http';
    import fetch from 'node-fetch';

    const serverLayout = constructServerLayout({
    filePath: 'server/views/index.html',
    });

    http
    .createServer((req, res) => {
    const fetchPromises = {};

    sendLayoutHTTPResponse(serverLayout, {
    res,
    serverLayout,
    urlPath: req.path,
    async renderApplication({ appName, propsPromise }) {
    const props = await propsPromise;
    const fetchPromise =
    fetchPromises[appName] ||
    (fetchPromises[appName] = fetchMicrofrontend(props));
    const response = await fetchPromise;
    // r.body is a Readable stream when you use node-fetch,
    // which is best for performance when using single-spa-layout
    return response.body;
    },
    async retrieveApplicationHeaders({ appName, propsPromise }) {
    const props = await propsPromise;
    const fetchPromise =
    fetchPromises[appName] ||
    (fetchPromises[appName] = fetchMicrofrontend(props));
    const response = await fetchPromise;
    return response.headers;
    },
    async retrieveProp(propName) {
    return 'prop value';
    },
    assembleFinalHeaders(allHeaders) {
    return Object.assign({}, ...Object.values(allHeaders));
    },
    renderFragment(name) {
    // not relevant to the docs here
    },
    });

    bodyStream.pipe(res);
    })
    .listen(9000);

    async function fetchMicrofrontend(props) {
    fetch(`http://${props.name}`, {
    headers: props,
    }).then(r => {
    if (r.ok) {
    return r;
    } else {
    throw Error(
    `Received http response ${r.status} from microfrontend ${appName}`,
    );
    }
    });
    }

    3. HTTP Response Headers

    The HTTP response headers sent to the browser are a combination of default headers and the headers retrieved from each microfrontend. Your method of fetching microfrontends does not change how the final headers are merged and assembled for the browser.

    Tailor and TailorX have built-in methods of merging headers. Single-spa-layout allows for custom merging via the assembleFinalHeaders option:

    import {
    constructServerLayout,
    sendLayoutHTTPResponse,
    } from 'single-spa-layout/server';
    import http from 'http';

    const serverLayout = constructServerLayout({
    filePath: 'server/views/index.html',
    });

    http
    .createServer((req, res) => {
    const { bodyStream } = sendLayoutHTTPResponse({
    res,
    serverLayout,
    urlPath: req.path,
    async renderApplication({ appName, propsPromise }) {
    const [app, props] = await Promise.all([
    import(`${props.name}/server.mjs`, propsPromise),
    ]);
    return app.serverRender(props);
    },
    async retrieveApplicationHeaders({ appName, propsPromise }) {
    const [app, props] = await Promise.all([
    import(`${props.name}/server.mjs`, propsPromise),
    ]);
    return app.getResponseHeaders(props);
    },
    async retrieveProp(propName) {
    return 'prop value';
    },
    assembleFinalHeaders(allHeaders) {
    // appHeaders contains all the application names, props, and headers for
    return Object.assign(
    {},
    ...Object.values(allHeaders).map(a => a.appHeaders),
    );
    },
    renderFragment(name) {
    // not relevant to the docs here
    },
    });

    bodyStream.pipe(res);
    })
    .listen(9000);

    4. HTTP Response Body

    The HTTP Response body sent from the web server to the browser must be streamed, byte by byte, in order to maximize performance. NodeJS Readable streams make this possible by acting as a buffer that sends each byte as received, instead of all bytes at once.

    All microfrontend layout middlewares mentioned in this document stream the HTML response body to the browser. In the context of single-spa-layout, this is done by calling sendLayoutHTTPResponse

    import { sendLayoutHTTPResponse } from 'single-spa-layout/server';
    import http from 'http';

    const serverLayout = constructServerLayout({
    filePath: 'server/views/index.html',
    });

    http
    .createServer((req, res) => {
    sendLayoutHTTPResponse({
    res,
    // Add all other needed options here, too
    });
    })
    .listen(9000);

    5. Hydrate

    Hydration (or rehydration) refers to browser Javascript initializing and attaching event listeners to the HTML sent by the server. There are several variants, including progressive rehydration and partial rehydration.

    info

    See also "Rendering on the Web" by Google.

    In the context of microfrontends, hydration is done by the underlying UI framework of the microfrontend (React, Vue, Angular, etc). For example, in React, this is done by calling ReactDOM.hydrate(). The single-spa adapter libraries allow you to specify whether you are hydrating or mounting for the first time (see single-spa-react's renderType option).

    The role of single-spa-layout is to determine which microfrontends should hydrate which parts of the DOM. This is done automatically when you call constructLayoutEngine and singleSpa.start(). If using TailorX instead of single-spa-layout, the Isomorphic Layout Composer Project serves a similar purpose as constructLayoutEngine.

    - - + + \ No newline at end of file diff --git a/docs/testing/e2e/index.html b/docs/testing/e2e/index.html index 86992a18b..4f1cad48e 100644 --- a/docs/testing/e2e/index.html +++ b/docs/testing/e2e/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    E2E testing

    info

    As microfrontends gain widespread adoption, testing tools will catch up and the testing story will improve.

    End to End (E2E) testing a single-spa application, parcel, or utility is very similar to E2E testing in other architectures. Because you are testing in the browser you can even use tools like import-map-overrides to run your tests in a production or production like environment with an override before deploying to that environment.

    In general we suggest only using E2E tests to test integration points between microfrontends and core functionality following principles of either the testing pyramid or the testing trophy.

    Testing Options

    In single-spa, there are more ways to test your code in a browser using tools like cypress. Two common approaches are to test individual applications by using standalone mode and testing everything together, both provide value in different ways.

    "E2E" testing with "standalone" mode

    While not perfect standalone mode offers a way to run individual single-spa applications and can be used to test a single-spa application. If the microfrontend relies upon configuration or initialization happening in your single-spa root-config you cannot test those areas in standalone mode without mocking. Standalone mode works by creating a custom single-spa root-config on the fly that will just render the one application, so the code is the same as if it were running in production but the configuration is different.

    Testing everything together

    Much like E2E tests run in traditional SPA applications you can open a brower and run assertions using tools like Cypress. Taking this approach is mirroring a full end to end test. You are running the exact same code that is in the environment. With some configuration and tools like import-map-overrides you can set-up your testing environment to work with overrides to the import map and can run end-to-end tests before deploying your code changes to an environment.

    Configuring E2E tests to work with overrides

    At a high level you will need to do the following before your environment can utilize overrides in E2E tests

    1. Use a tool like import-map-overrides
    2. Get the built code on a publically accessible domain. Similar to a "review app"
    3. Configure your E2E testing environment to set the override
    4. Run the E2E tests
    - - + + \ No newline at end of file diff --git a/docs/testing/units/index.html b/docs/testing/units/index.html index 6f2a66bf8..bb076d8ba 100644 --- a/docs/testing/units/index.html +++ b/docs/testing/units/index.html @@ -14,13 +14,13 @@ - - + +
    Version: 6.x

    Unit testing

    info

    As microfrontends gain widespread adoption, testing tools will catch up and the testing story will improve.

    Unit testing a single-spa application, parcel, or utility is very similar to unit testing in the framework you are using, with two notable exceptions:

    • Importing microfrontends
    • System.import

    In general we recommend following the principle of only testing the units that need to be tested and aren't covered by other tests. Testing library code like single-spa.registerApplication is usually unnecessary because those are covered by the library's unit tests.

    Importing microfrontends

    It is fairly common in microfrontends to have one microfrontend import and rely upon a component from another microfrontend. Reliance on another microfrontend can be challenging to test because unit tests generally run locally and you won't have access to other microfrontends. When this occurs we generally recommend mocking the other microfrontend for the unit test.

    An example of this can be found in the Button component exported by the styleguide in react.microfrontends.app. Because that component is imported and used by the planets application you will need to make it available to the test environment by mocking the dependency. This is necessary because the test environment cannot dynamically import other microfrontends, like the browser can. Given the wide variety of unit testing tools you will need to follow the pattern established by the test environment you are using for mocking other microfrontends.

    note

    We suggest mocks over installing microfrontends for local tests (for example via NPM modules) because mocks are easier to maintain and avoid several potential incompatiblity issues such as version mismatch, module format incompatibility, environment differences, and more.

    System.import

    Occasionally you will choose to interop with another microfrontend asynchronously by explicitly calling System.import. Testing in this scenario may require mocking both SystemJS and the module you're importing. Additionally because System.import returns a promise your tests in that area will need to be asynchronous and wait for promises to resolve.

    An example of this can be found in people and planets applications from react.microfrontends.app. The People application exports a function that resolves with a component. The Planets Application imports and uses that component asynchronously with React.lazy. Testing this component would necessitate mocking both SystemJS and People.

    Shared Mocks

    If each project mocks every other microfrontend it is possible that the mocks will eventually become out of sync with the actual deployed microfrontend. One way to prevent this is to share mocks so that keeping the mocks in sync only requires one change instead of updating mocks in many different repositories.

    Testing examples

    Jest

    In the above examples I showed how People imports a component from styleguide. In order to unit test the component in people with Jest you will need to configure jest and mock the styleguide MFE. In jest configuration is done via multiple areas.

    1. Create a jest config file
    2. Setup a mocks directory at the root
    3. Add a setupFile

    Jest config

    Jest is configured with a configuration file. Below is an example configuration using some of the options. See more options on Jest's official documentation site.

    module.exports = {
    collectCoverageFrom: ['src/**/*.js'],
    modulePathIgnorePatterns: ['/cache', '/dist'],
    transform: {
    '^.+\\.(j|t)sx?$': 'babel-jest',
    },
    setupFilesAfterEnv: ['./jest.setup.js'],
    moduleNameMapper: {
    // Note this is only needed if you don't match the module name directly
    // an alternative would be to place the mock in
    // <rootDir>/__mocks__/@react-mf/styleguide.js and it would be autodetected
    '@react-mf/styleguide': '<rootDir>/__mocks__/styleguide.js',
    },
    }

    mocks directory

    Jest will detect folders named __mocks__ and if the naming convention is exact or the modules have been mapped using moduleNameMapper then Jest will use those mocks in place of an import. This structure is essential for other microfrontends where you don't have the code locally. See more information on jest's official documentation

    .
    ├── __mocks__
    │ └── styleguide.js
    ├── src
    │ ├── react-mf-people.js
    │ └── ...
    ├── node_modules
    ├── jest.setup.js
    ├── ...
    └── jest.config.js

    setup file

    Jest uses a setup file to create globals mocks that can be utilized by every test or otherwise configure the test environment. If you were mocking localStorage or SystemJS this is a good place to configure those mocks. See more use-cases for a set-up file on Jest's offical documentation

    // jest.setup.js
    // import Mocks for SystemJS mock below
    import peopleApplication from '@react-mf/people'
    // Mock SystemJS
    global.System = {
    import: jest.fn(mockImport)
    }

    function mockImport (importName) {
    // What you do in mock import will depend a lot on how you use SystemJS in the project and components you wish to test

    /* If I had already created a mock for `@react-mf/people` and I wanted to test this component:
    * https://github.com/react-microfrontends/planets/blob/main/src/planets-page/selected-planet/selected-planet.component.js#L5
    * I would want `System.import('@react-mf/people')` to resovle to my mock one way to accomplish this would be the following
    */
    if (importName === '@react-mf/people') {
    return Promise.resolve(peopleApplication)
    } else {
    console.warn('No mock module found')
    return Promise.resolve({})
    }
    }

    - - + + \ No newline at end of file diff --git a/docs/videos/index.html b/docs/videos/index.html index c48805265..fddb98641 100644 --- a/docs/videos/index.html +++ b/docs/videos/index.html @@ -14,13 +14,13 @@ - - + + - - + + \ No newline at end of file diff --git a/error/index.html b/error/index.html index 813ae3cc1..5f756d806 100644 --- a/error/index.html +++ b/error/index.html @@ -14,13 +14,13 @@ - - + + - - + + \ No newline at end of file diff --git a/help/index.html b/help/index.html index 206c66320..e9a17944e 100644 --- a/help/index.html +++ b/help/index.html @@ -14,13 +14,13 @@ - - + +

    Need help?

    If you need help with single-spa, you can try one of the mechanisms below.

    Browse Docs

    Learn more about single-spa using the official docs

    Slack

    You can join the conversation on Slack on one of our channels: #general for user help and #maintainers for contributing help.

    Github

    At our GitHub repo Browse and submit issues or pull requests for bugs you find or any new features you may want implemented. Be sure to also check out our contributing information.

    Phone call / Consulting

    Paid, hands-on consulting with a single-spa core team member is available. Our core team members have done this before and can help you set up single-spa. Services range from a one hour debugging session to an ongoing relationship while you're seeing your project to completion. Inquire at singlespa.info@gmail.com.

    Stack Overflow

    Use the single-spa tag on StackOverflow.

    Twitter

    You can follow and contact us on Twitter.

    - - + + \ No newline at end of file diff --git a/index.html b/index.html index 69190bf6e..e2babd962 100644 --- a/index.html +++ b/index.html @@ -14,13 +14,13 @@ - - + +

    single-spa

    A javascript router for front-end microservices

    Framework freedom

    Framework freedom

    Use multiple frameworks in a single-page application, allowing you to split code by functionality and have Angular, React, Vue.js, etc. apps all living in harmony.

    Lazy load applications

    Lazy load applications

    Stand up new apps next to the old one. You write the applications, single-spa makes them work together and won't load them until they're needed.

    Front-end microservices

    Front-end microservices

    Combine many small apps, empowering teams to choose their technology. Stay nimble as your team, product, and tech stack grows and changes over time.

    Who's Using This?

    This project is used by all these organizations

    - - + + \ No newline at end of file diff --git a/languages/index.html b/languages/index.html index 04ced9b2a..41b1027a6 100644 --- a/languages/index.html +++ b/languages/index.html @@ -14,13 +14,13 @@ - - + + - - + + \ No newline at end of file diff --git a/search/index.html b/search/index.html index e84bbcd4e..5e1e58606 100644 --- a/search/index.html +++ b/search/index.html @@ -14,13 +14,13 @@ - - + + - - + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index ed64f33a8..bf8cde165 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -https://single-spa.js.org/blogweekly0.5https://single-spa.js.org/blog/2016/02/26/a-case-for-soa-in-the-browserweekly0.5https://single-spa.js.org/blog/2016/12/16/a-step-by-step-guide-to-single-spaweekly0.5https://single-spa.js.org/blog/2018/06/19/single-spa-parcels-explainedweekly0.5https://single-spa.js.org/blog/2019/02/20/single-spa-inspectorweekly0.5https://single-spa.js.org/blog/2020/02/24/single-spa-5weekly0.5https://single-spa.js.org/blog/2023/08/22/single-spa-core-is-expandingweekly0.5https://single-spa.js.org/blog/2023/10/11/introducing-single-spa-new-core-team-membersweekly0.5https://single-spa.js.org/blog/2023/11/07/core-team-meeting-notesweekly0.5https://single-spa.js.org/blog/2023/11/27/single-spa-6weekly0.5https://single-spa.js.org/blog/archiveweekly0.5https://single-spa.js.org/contributors/weekly0.5https://single-spa.js.org/docs/weekly0.5https://single-spa.js.org/error/weekly0.5https://single-spa.js.org/help/weekly0.5https://single-spa.js.org/languages/weekly0.5https://single-spa.js.org/searchweekly0.5https://single-spa.js.org/sponsors/weekly0.5https://single-spa.js.org/users/weekly0.5https://single-spa.js.org/versions/weekly0.5https://single-spa.js.org/docs/4.x/apiweekly0.5https://single-spa.js.org/docs/4.x/building-applicationsweekly0.5https://single-spa.js.org/docs/4.x/code-of-conductweekly0.5https://single-spa.js.org/docs/4.x/configurationweekly0.5https://single-spa.js.org/docs/4.x/contributing-overviewweekly0.5https://single-spa.js.org/docs/4.x/ecosystemweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-angularweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-angularjsweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-backboneweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-cycleweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-emberweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-html-web-componentsweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-infernoweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-leaked-globalsweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-preactweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-reactweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-riotweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-svelteweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-vueweekly0.5https://single-spa.js.org/docs/4.x/examplesweekly0.5https://single-spa.js.org/docs/4.x/faqweekly0.5https://single-spa.js.org/docs/4.x/getting-started-overviewweekly0.5https://single-spa.js.org/docs/4.x/glossaryweekly0.5https://single-spa.js.org/docs/4.x/migrating-angularJS-tutorialweekly0.5https://single-spa.js.org/docs/4.x/migrating-existing-spasweekly0.5https://single-spa.js.org/docs/4.x/migrating-react-tutorialweekly0.5https://single-spa.js.org/docs/4.x/parcels-apiweekly0.5https://single-spa.js.org/docs/4.x/parcels-overviewweekly0.5https://single-spa.js.org/docs/4.x/separating-applicationsweekly0.5https://single-spa.js.org/docs/4.x/starting-from-scratchweekly0.5https://single-spa.js.org/docs/5.x/apiweekly0.5https://single-spa.js.org/docs/5.x/building-applicationsweekly0.5https://single-spa.js.org/docs/5.x/code-of-conductweekly0.5https://single-spa.js.org/docs/5.x/configurationweekly0.5https://single-spa.js.org/docs/5.x/contributing-overviewweekly0.5https://single-spa.js.org/docs/5.x/create-single-spaweekly0.5https://single-spa.js.org/docs/5.x/devtoolsweekly0.5https://single-spa.js.org/docs/5.x/ecosystemweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-alpinejsweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-angularweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-angularjsweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-backboneweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-cssweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-cycleweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-dojoweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-emberweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-html-web-componentsweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-infernoweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-leaked-globalsweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-preactweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-reactweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-riotweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-snowpackweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-svelteweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-viteweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-vueweekly0.5https://single-spa.js.org/docs/5.x/examplesweekly0.5https://single-spa.js.org/docs/5.x/faqweekly0.5https://single-spa.js.org/docs/5.x/getting-started-overviewweekly0.5https://single-spa.js.org/docs/5.x/glossaryweekly0.5https://single-spa.js.org/docs/5.x/layout-apiweekly0.5https://single-spa.js.org/docs/5.x/layout-definitionweekly0.5https://single-spa.js.org/docs/5.x/layout-overviewweekly0.5https://single-spa.js.org/docs/5.x/microfrontends-conceptweekly0.5https://single-spa.js.org/docs/5.x/migrating-existing-spasweekly0.5https://single-spa.js.org/docs/5.x/module-typesweekly0.5https://single-spa.js.org/docs/5.x/parcels-apiweekly0.5https://single-spa.js.org/docs/5.x/parcels-overviewweekly0.5https://single-spa.js.org/docs/5.x/recommended-setupweekly0.5https://single-spa.js.org/docs/5.x/separating-applicationsweekly0.5https://single-spa.js.org/docs/5.x/shared-webpack-configsweekly0.5https://single-spa.js.org/docs/5.x/single-spa-playgroundweekly0.5https://single-spa.js.org/docs/5.x/ssr-overviewweekly0.5https://single-spa.js.org/docs/5.x/testing/e2eweekly0.5https://single-spa.js.org/docs/5.x/testing/unitsweekly0.5https://single-spa.js.org/docs/5.x/videosweekly0.5https://single-spa.js.org/docs/next/weekly0.5https://single-spa.js.org/docs/weekly0.5https://single-spa.js.org/docs/apiweekly0.5https://single-spa.js.org/docs/building-applicationsweekly0.5https://single-spa.js.org/docs/code-of-conductweekly0.5https://single-spa.js.org/docs/configurationweekly0.5https://single-spa.js.org/docs/contributing-overviewweekly0.5https://single-spa.js.org/docs/create-single-spaweekly0.5https://single-spa.js.org/docs/devtoolsweekly0.5https://single-spa.js.org/docs/ecosystemweekly0.5https://single-spa.js.org/docs/ecosystem-alpinejsweekly0.5https://single-spa.js.org/docs/ecosystem-angularweekly0.5https://single-spa.js.org/docs/ecosystem-angularjsweekly0.5https://single-spa.js.org/docs/ecosystem-backboneweekly0.5https://single-spa.js.org/docs/ecosystem-cssweekly0.5https://single-spa.js.org/docs/ecosystem-cycleweekly0.5https://single-spa.js.org/docs/ecosystem-dojoweekly0.5https://single-spa.js.org/docs/ecosystem-emberweekly0.5https://single-spa.js.org/docs/ecosystem-html-web-componentsweekly0.5https://single-spa.js.org/docs/ecosystem-infernoweekly0.5https://single-spa.js.org/docs/ecosystem-leaked-globalsweekly0.5https://single-spa.js.org/docs/ecosystem-preactweekly0.5https://single-spa.js.org/docs/ecosystem-reactweekly0.5https://single-spa.js.org/docs/ecosystem-riotweekly0.5https://single-spa.js.org/docs/ecosystem-snowpackweekly0.5https://single-spa.js.org/docs/ecosystem-svelteweekly0.5https://single-spa.js.org/docs/ecosystem-viteweekly0.5https://single-spa.js.org/docs/ecosystem-vueweekly0.5https://single-spa.js.org/docs/examplesweekly0.5https://single-spa.js.org/docs/faqweekly0.5https://single-spa.js.org/docs/getting-started-overviewweekly0.5https://single-spa.js.org/docs/glossaryweekly0.5https://single-spa.js.org/docs/layout-apiweekly0.5https://single-spa.js.org/docs/layout-definitionweekly0.5https://single-spa.js.org/docs/layout-overviewweekly0.5https://single-spa.js.org/docs/microfrontends-conceptweekly0.5https://single-spa.js.org/docs/migrating-existing-spasweekly0.5https://single-spa.js.org/docs/module-typesweekly0.5https://single-spa.js.org/docs/parcels-apiweekly0.5https://single-spa.js.org/docs/parcels-overviewweekly0.5https://single-spa.js.org/docs/recommended-setupweekly0.5https://single-spa.js.org/docs/separating-applicationsweekly0.5https://single-spa.js.org/docs/shared-webpack-configsweekly0.5https://single-spa.js.org/docs/single-spa-playgroundweekly0.5https://single-spa.js.org/docs/ssr-overviewweekly0.5https://single-spa.js.org/docs/testing/e2eweekly0.5https://single-spa.js.org/docs/testing/unitsweekly0.5https://single-spa.js.org/docs/videosweekly0.5https://single-spa.js.org/weekly0.5 \ No newline at end of file +https://single-spa.js.org/blogweekly0.5https://single-spa.js.org/blog/2016/02/26/a-case-for-soa-in-the-browserweekly0.5https://single-spa.js.org/blog/2016/12/16/a-step-by-step-guide-to-single-spaweekly0.5https://single-spa.js.org/blog/2018/06/19/single-spa-parcels-explainedweekly0.5https://single-spa.js.org/blog/2019/02/20/single-spa-inspectorweekly0.5https://single-spa.js.org/blog/2020/02/24/single-spa-5weekly0.5https://single-spa.js.org/blog/2023/08/22/single-spa-core-is-expandingweekly0.5https://single-spa.js.org/blog/2023/10/11/introducing-single-spa-new-core-team-membersweekly0.5https://single-spa.js.org/blog/2023/11/07/core-team-meeting-notesweekly0.5https://single-spa.js.org/blog/2023/11/27/single-spa-6weekly0.5https://single-spa.js.org/blog/2023/12/12/core-team-meeting-notesweekly0.5https://single-spa.js.org/blog/archiveweekly0.5https://single-spa.js.org/contributors/weekly0.5https://single-spa.js.org/docs/weekly0.5https://single-spa.js.org/error/weekly0.5https://single-spa.js.org/help/weekly0.5https://single-spa.js.org/languages/weekly0.5https://single-spa.js.org/searchweekly0.5https://single-spa.js.org/sponsors/weekly0.5https://single-spa.js.org/users/weekly0.5https://single-spa.js.org/versions/weekly0.5https://single-spa.js.org/docs/4.x/apiweekly0.5https://single-spa.js.org/docs/4.x/building-applicationsweekly0.5https://single-spa.js.org/docs/4.x/code-of-conductweekly0.5https://single-spa.js.org/docs/4.x/configurationweekly0.5https://single-spa.js.org/docs/4.x/contributing-overviewweekly0.5https://single-spa.js.org/docs/4.x/ecosystemweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-angularweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-angularjsweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-backboneweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-cycleweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-emberweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-html-web-componentsweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-infernoweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-leaked-globalsweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-preactweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-reactweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-riotweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-svelteweekly0.5https://single-spa.js.org/docs/4.x/ecosystem-vueweekly0.5https://single-spa.js.org/docs/4.x/examplesweekly0.5https://single-spa.js.org/docs/4.x/faqweekly0.5https://single-spa.js.org/docs/4.x/getting-started-overviewweekly0.5https://single-spa.js.org/docs/4.x/glossaryweekly0.5https://single-spa.js.org/docs/4.x/migrating-angularJS-tutorialweekly0.5https://single-spa.js.org/docs/4.x/migrating-existing-spasweekly0.5https://single-spa.js.org/docs/4.x/migrating-react-tutorialweekly0.5https://single-spa.js.org/docs/4.x/parcels-apiweekly0.5https://single-spa.js.org/docs/4.x/parcels-overviewweekly0.5https://single-spa.js.org/docs/4.x/separating-applicationsweekly0.5https://single-spa.js.org/docs/4.x/starting-from-scratchweekly0.5https://single-spa.js.org/docs/5.x/apiweekly0.5https://single-spa.js.org/docs/5.x/building-applicationsweekly0.5https://single-spa.js.org/docs/5.x/code-of-conductweekly0.5https://single-spa.js.org/docs/5.x/configurationweekly0.5https://single-spa.js.org/docs/5.x/contributing-overviewweekly0.5https://single-spa.js.org/docs/5.x/create-single-spaweekly0.5https://single-spa.js.org/docs/5.x/devtoolsweekly0.5https://single-spa.js.org/docs/5.x/ecosystemweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-alpinejsweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-angularweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-angularjsweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-backboneweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-cssweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-cycleweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-dojoweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-emberweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-html-web-componentsweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-infernoweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-leaked-globalsweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-preactweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-reactweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-riotweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-snowpackweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-svelteweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-viteweekly0.5https://single-spa.js.org/docs/5.x/ecosystem-vueweekly0.5https://single-spa.js.org/docs/5.x/examplesweekly0.5https://single-spa.js.org/docs/5.x/faqweekly0.5https://single-spa.js.org/docs/5.x/getting-started-overviewweekly0.5https://single-spa.js.org/docs/5.x/glossaryweekly0.5https://single-spa.js.org/docs/5.x/layout-apiweekly0.5https://single-spa.js.org/docs/5.x/layout-definitionweekly0.5https://single-spa.js.org/docs/5.x/layout-overviewweekly0.5https://single-spa.js.org/docs/5.x/microfrontends-conceptweekly0.5https://single-spa.js.org/docs/5.x/migrating-existing-spasweekly0.5https://single-spa.js.org/docs/5.x/module-typesweekly0.5https://single-spa.js.org/docs/5.x/parcels-apiweekly0.5https://single-spa.js.org/docs/5.x/parcels-overviewweekly0.5https://single-spa.js.org/docs/5.x/recommended-setupweekly0.5https://single-spa.js.org/docs/5.x/separating-applicationsweekly0.5https://single-spa.js.org/docs/5.x/shared-webpack-configsweekly0.5https://single-spa.js.org/docs/5.x/single-spa-playgroundweekly0.5https://single-spa.js.org/docs/5.x/ssr-overviewweekly0.5https://single-spa.js.org/docs/5.x/testing/e2eweekly0.5https://single-spa.js.org/docs/5.x/testing/unitsweekly0.5https://single-spa.js.org/docs/5.x/videosweekly0.5https://single-spa.js.org/docs/next/weekly0.5https://single-spa.js.org/docs/weekly0.5https://single-spa.js.org/docs/apiweekly0.5https://single-spa.js.org/docs/building-applicationsweekly0.5https://single-spa.js.org/docs/code-of-conductweekly0.5https://single-spa.js.org/docs/configurationweekly0.5https://single-spa.js.org/docs/contributing-overviewweekly0.5https://single-spa.js.org/docs/create-single-spaweekly0.5https://single-spa.js.org/docs/devtoolsweekly0.5https://single-spa.js.org/docs/ecosystemweekly0.5https://single-spa.js.org/docs/ecosystem-alpinejsweekly0.5https://single-spa.js.org/docs/ecosystem-angularweekly0.5https://single-spa.js.org/docs/ecosystem-angularjsweekly0.5https://single-spa.js.org/docs/ecosystem-backboneweekly0.5https://single-spa.js.org/docs/ecosystem-cssweekly0.5https://single-spa.js.org/docs/ecosystem-cycleweekly0.5https://single-spa.js.org/docs/ecosystem-dojoweekly0.5https://single-spa.js.org/docs/ecosystem-emberweekly0.5https://single-spa.js.org/docs/ecosystem-html-web-componentsweekly0.5https://single-spa.js.org/docs/ecosystem-infernoweekly0.5https://single-spa.js.org/docs/ecosystem-leaked-globalsweekly0.5https://single-spa.js.org/docs/ecosystem-preactweekly0.5https://single-spa.js.org/docs/ecosystem-reactweekly0.5https://single-spa.js.org/docs/ecosystem-riotweekly0.5https://single-spa.js.org/docs/ecosystem-snowpackweekly0.5https://single-spa.js.org/docs/ecosystem-svelteweekly0.5https://single-spa.js.org/docs/ecosystem-viteweekly0.5https://single-spa.js.org/docs/ecosystem-vueweekly0.5https://single-spa.js.org/docs/examplesweekly0.5https://single-spa.js.org/docs/faqweekly0.5https://single-spa.js.org/docs/getting-started-overviewweekly0.5https://single-spa.js.org/docs/glossaryweekly0.5https://single-spa.js.org/docs/layout-apiweekly0.5https://single-spa.js.org/docs/layout-definitionweekly0.5https://single-spa.js.org/docs/layout-overviewweekly0.5https://single-spa.js.org/docs/microfrontends-conceptweekly0.5https://single-spa.js.org/docs/migrating-existing-spasweekly0.5https://single-spa.js.org/docs/module-typesweekly0.5https://single-spa.js.org/docs/parcels-apiweekly0.5https://single-spa.js.org/docs/parcels-overviewweekly0.5https://single-spa.js.org/docs/recommended-setupweekly0.5https://single-spa.js.org/docs/separating-applicationsweekly0.5https://single-spa.js.org/docs/shared-webpack-configsweekly0.5https://single-spa.js.org/docs/single-spa-playgroundweekly0.5https://single-spa.js.org/docs/ssr-overviewweekly0.5https://single-spa.js.org/docs/testing/e2eweekly0.5https://single-spa.js.org/docs/testing/unitsweekly0.5https://single-spa.js.org/docs/videosweekly0.5https://single-spa.js.org/weekly0.5 \ No newline at end of file diff --git a/sponsors/index.html b/sponsors/index.html index b4f0d7b83..44860333c 100644 --- a/sponsors/index.html +++ b/sponsors/index.html @@ -14,13 +14,13 @@ - - + +
    - - + + \ No newline at end of file diff --git a/users/index.html b/users/index.html index b6c2c50ba..025f1e78a 100644 --- a/users/index.html +++ b/users/index.html @@ -14,13 +14,13 @@ - - + + - - + + \ No newline at end of file diff --git a/versions/index.html b/versions/index.html index 3cc282653..703d81b48 100644 --- a/versions/index.html +++ b/versions/index.html @@ -14,13 +14,13 @@ - - + +

    single-spa documentation versions

    Latest version (Stable)

    Here you can find the latest documentation.

    6.xDocumentationRelease Notes

    Past Versions

    Here you can find documentation for previous versions of single-spa.

    5.xDocumentationRelease Notes
    4.xDocumentationRelease Notes
    - - + + \ No newline at end of file