跳到主要内容

· 阅读需 5 分钟

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 分钟

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 分钟

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 分钟

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 分钟

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.