3 Composition with Ajax and server-side routing

This chapter covers:

  • Integrating fragments into a page via Ajax
  • Applying project-wide namespaces to avoid style or script collisions
  • Utilizing the Nginx web server to serve all applications from one domain
  • Implementing request routing to forward incoming requests to the right server

We covered a lot of ground in the previous chapter. The applications for two teams are ready to go. You learned how to integrate user interfaces via links and iframes. These are all valid integration methods, and they provide strong isolation. But they come with trade-offs in the areas of usability, performance, layout flexibility, accessibility, and search engine compatibility. In this chapter, we’ll look at fragment integration via Ajax to address these issues. We’ll also configure a shared web server to expose all applications through a single domain.

3.1 Composition via Ajax

Our customers love the new product page. Presenting all recommendations directly on that page has measurable positive effects. On average, people spend more time on the site than before.

But Waldemar, responsible for marketing, noticed that the site does not rank very well in most search engines. He suspects that the suboptimal ranking has something to do with the use of iframes. He talks to the development teams to discuss options to improve the ranking.

The developers are aware that the iframe integration has issues, especially when it comes to semantic structure. Since good search-engine ranking is essential for getting the word out and reaching new customers, they decide to address this issue in the upcoming iteration.

The plan is to ditch the document-in-document approach of the iframe and choose a deeper integration using Ajax. With this model, Team Inspire will deliver the recommendations as a fragment--a snippet of HTML. This snippet is loaded by Team Decide and integrated into the product page’s DOM. Figure 3.1 illustrates this. They’ll also have to find a good way to ship the styling that’s necessary for the fragment.

Figure 3.1 Integrating the recommendations into the product page’s DOM via Ajax

We have to complete two tasks to make the Ajax integration work:

  1. Team Inspire exposes the recommendations as a fragment.

  2. Team Decide loads the fragment and inserts it into their DOM.

Before getting to work, Team Inspire and Team Decide must talk about the URL for the fragment. They choose to create a new endpoint for the fragments markup and expose it under http://localhost:3002/fragment/recommendations/<sku>. The existing standalone recommendation page stays the same. Now both teams can go ahead and implement in parallel.

3.1.1 How to do it

Creating the fragment endpoint is straightforward for Team Inspire. All data and styles are already there from the iframe integration. Figure 3.2 shows the updated folder structure.

Figure 3.2 Folder structure of the Ajax example code 03_ajax.

Team Inspire adds an HTML file for each fragment, which is a stripped-down version of the recommendation page. They also introduce dedicated fragment styles (fragment.css). Team Decide introduces a page.js, which will trigger the Ajax call.

Markup

The fragment markup looks like this.

Listing 3.1 team-inspire/fragment/recommendations/porsche.html

<link href="http://localhost:3002/static/fragment.css" rel="stylesheet" /> 
<h2>Recommendations</h2>
<div class="recommendations">
  ...
</div>

Reference to the recommendation style

Note that the fragment references its own CSS file from the markup. The URL has to be absolute (http://localhost:3002/...) because Team Decide will insert this markup into its DOM, which they serve from port 3001.

Shipping a link tag together with the actual content is not always optimal. If a page included multiple recommendation strips, it would end up with multiple redundant link tags. In chapter 10 we’ll explore some more advanced techniques for referencing associated CSS and JavaScript resources.

Ajax request

The fragment is ready to use. Let’s switch hats and slip into Team Decide’s shoes.

Loading a piece of HTML via Ajax and appending it to the DOM is not that complicated. Let’s introduce our first client-side JavaScript.

Listing 3.2 team-decide/static/page.js

const element = document.querySelector(".decide_recos");   
const url = element.getAttribute("data-fragment");         
 
window
  .fetch(url)                                              
  .then(res => res.text())
  .then(html => {
    element.innerHTML = html;                              
  });

Finding the element to insert the fragment in

Retrieving the fragment URL from an attribute

Fetching the fragment HTML via the native window.fetch API

Inserting the loaded markup to the product page’s DOM

Now we have to include this script into our page and add the data-fragment attribute to our .decide_recos element. The product page markup now looks like this.

Listing 3.3 team-decide/view.js

...
  <aside
    class="decide_recos"
    data-fragment="http://localhost:3002/fragment/recommendations/porsche" 
  >
    <a href="http://localhost:3002/recommendations/porsche">               
     Show Recommendations                                                  
    </a>                                                                   
  </aside>
  <script src="/static/page.js" async></script>                            
</body>
...

Team Inspire’s recommendation fragment URL

Link to the recommendation page. In case the Ajax call failed or hasn’t finished yet, the customer can use this link as a fallback: Progressive Enhancement.

Referencing the JavaScript file, which will make the Ajax request

Let’s try the example by running the following code:

npm run 03_ajax

The result looks the same as with the iframe. But now the product page and recommendation strip live in the same document.

3.1.2 Namespacing styles and scripts

Running inside the same document introduces some challenges. Now both teams have to build their applications in a way that doesn’t conflict with the others. When two teams style the same CSS class or try to write to the same global JavaScript variable, weird side effects can happen that are hard to debug.

Isolating styles

Let’s look at CSS first. Sadly, browsers don’t offer much help here. The deprecated Scoped CSS specification would have been an excellent fit for our use case. It allowed you to mark a style or link tag with the attribute scoped. The effect was that these styles would only be active in the DOM subtree they’re defined in. Styles from higher up in the tree would still propagate down, but styles from within a scoped block would never leak out. This specification did not last long, and browsers which already supported it pulled their implementation. 1 Some frameworks like Vue.js still use the scoped syntax to achieve this isolation. But they use automatic selector prefixing under the hood to make this work in the browser.

NOTE In modern browsers 2 it’s possible to get strong style scoping today via JavaScript and the ShadowDOM API, which is part of the Web Components specification. We’ll talk about this in chapter 5.

Since CSS rules are global by nature, the most practical solution is to namespace all CSS selectors. Many CSS methodologies like BEM 3 use strict naming rules to avoid unwanted style leaking between components. But two different teams might come up with the same component name independently, like the headline component in our example. That’s why it’s a good idea to introduce an extra team-level prefix. Table 3.1 shows what this namespacing might look like.

Table 3.1 Namespacing all CSS selectors with a team prefix

Team name

Team prefix

Example selectors

Decide

decide

.decide_headline .decide_recos

Inspire

inspire

.inspire_headline .inspire_recommendation__item

Checkout

checkout

.checkout_minicart .checkout_minicart--empty

NOTE To keep the CSS and HTML size small, we like to use two-letter prefixes like de, in, and ch. But for easier understanding, I opted for using longer and more descriptive prefixes in this book.

When every team follows these naming conventions and only uses class-name-based selectors, the issue of overwriting styles should be solved. Prefixing does not have to be done manually. Some tools can help here. CSS Modules, PostCSS, or SASS are a good start. You can configure most CSS-in-JS solutions to add a prefix to each class name. It does not matter which tool a team chooses, as long as all selectors are prefixed.

Isolating JavaScript

The fragment, in our example, does not come with any client-side JavaScript. But you also need inter-team conventions to avoid collisions in the browser. Luckily JavaScript makes it easy to write your code in a non-global way.

A popular way is to wrap your script in an IIFE (immediately invoked function expression). 4 This way, the declared variables and functions of your application are not added to the global window object. Instead, we limit the scope to the anonymous function. Most build tools already do this automatically. For the static/page.js of Team Decide it would look like this.

Listing 3.4 team-decide/static/page.js

(function () {             
  const element = ...;     
  ...
})();                      

Immediately invoked function expression

Variable is not added to the global scope

But sometimes you need a global variable. A typical example is when you want to ship structured data in the form of a JavaScript object alongside your server-generated markup. This object must be accessible by the client-side JavaScript. A good alternative is to write your data to your markup in a declarative way.

Instead of writing this

<script>
const MY_STATE = {name: "Porsche"};
</script>

you could express it declaratively and avoid creating a global variable:

<script data-inspire-state type="application/json">
{"name":"Porsche"}
</script>

Accessing the data can be done by looking up the script tag in your part of the DOM tree and parsing it:

(function () {
  const stateContainer = fragment.querySelector("[data-inspire-state]");
  const MY_STATE = JSON.parse(stateContainer.innerHTML);
})();

But there are a few places where it’s not possible to create real scopes, and you have to fall back to namespaces and conventions. Cookies, storage, events, or unavoidable global variables should be namespaced. You can use the same prefixing rules we’ve introduced for CSS class names for this. Table 3.2 shows a few examples.

Table 3.2 Some JavaScript functionalities also need namespacing.

Function

Example

Cookies

document.cookie = "decide_optout=true";

Local storage

localStorage["decide:last_seen"] = "a,b";

Session storage

sessionStorage["inspire:last_seen"] = "c,d";

Custom events

new CustomEvent("checkout:item_added"); window.addEventListener("checkout:item_added", ...);

Unavoidable globals

window.checkout.myGlobal = "needed this!"

Meta tags

<meta name="inspire:feature_a" content="off" />

Namespacing helps with more than just avoiding conflicts. Another valuable factor in day-to-day work is that they also indicate ownership. When an enormous cookie value leads to an error, you just have to look at the cookie name to know which team can fix that.

The described methods for avoiding code interference are not only helpful for the Ajax integration--they also apply for nearly all other integration techniques. I highly recommend setting up global namespacing rules like this when you’re setting up a micro frontends project. It will save everyone a lot of time and headaches.

3.1.3 Declarative loading with h-include

Let’s look at a way to make composition via Ajax even easier. In our example, Team Decide loads the fragment’s content imperatively by looking up a DOM element, running fetch (), and inserting the resulting HTML into the DOM.

The JavaScript library h-include provides a declarative approach for fragment loading. 5 Including a fragment feels like including an iframe in the markup. You don’t have to care about finding the DOM element and making the actual HTTP request. The library introduces a new HTML element called h-include, which handles everything for you. The code for the recommendations would look like this.

Listing 3.5 team-decide/product/porsche.html

...
<aside class="decide_recos">
  <h-include
    src="http://localhost:3002/fragment/recommendations/porsche">      
  </h-include>
</aside>
...

h-include fetches the HTML from the src and inserts it into the element itself

The library also comes with extra features like defining timeouts, reducing reflows by bundling the insertion of multiple fragments together, and lazy loading.

3.1.4 The benefits

Ajax integration is a technique that is easy to implement and understand. Compared to the iframe approach, it has a lot of advantages.

Natural document flow

In contrast to iframe, we integrate all content into one DOM. Fragments are now part of the page’s document flow. Being part of this flow means that a fragment takes precisely the space it needs. The team that includes the fragment does not have to know the height of the fragment in advance. Whether Team Inspire displays one or three recommendation images, the product page adapts in height automatically.

Search engines and accessibility

Even though integration happens in the browser and the fragment is not present in the page’s initial markup yet, this model works well for search engines. Their bots execute JavaScript and index the assembled page. 6 Assistive technologies like screen readers also support this. It’s essential, though, that the combined markup semantically makes sense as a whole. So make sure that your content hierarchy is marked up correctly.

Progressive enhancement

An Ajax-based solution typically plays well with the principles of progressive enhancement. 7 Delivering server-rendered content as a fragment or as a standalone page doesn’t introduce a lot of extra code.

You can provide a reliable fallback in case JavaScript failed or hasn’t executed yet. On our product page, users with broken JavaScript will see the Show Recommendations link, which will bring them to the standalone recommendations page. Architecting for failure is a valuable technique that will increase the robustness of your application. I recommend checking out Jeremy Keith’s publications 8 for more details on progressive enhancement.

Flexible error handling

You also get a lot more options for dealing with errors. When the fetch () call fails or takes too long, you can decide what you want to do--show the progressive enhancement fallback, remove the fragment from the layout altogether, or display a static alternative content you’ve prepared for this case.

3.1.5 The drawbacks

The Ajax model also has some drawbacks. The most obvious one is already present in its name: it’s asynchronous.

Asynchronous loading

You might have noticed that the site jumps or wiggles a bit when it’s loading. The asynchronous loading via JavaScript causes this delay. We could implement the fragment loading so that it blocks the complete page rendering and only shows the page when the fragments are successfully loaded. But this would make the overall experience worse.

Loading content asynchronously always comes with the trade-off that the content pops in with a delay. For fragments that are further down the page and outside the viewport, this is not an issue. But for content inside of the viewport, this flickering is not nice. In the next chapter, you’ll learn how to solve this with server-side composition.

Missing isolation

The Ajax model does not come with any isolation. To avoid conflicts, teams have to agree on inter-team conventions for namespacing. Conventions are fine when everyone plays by the book. But you have no technical guarantees. When something slips through, it can affect all teams.

Server request required

Updating or refreshing an Ajax fragment is as easy as loading it initially. But when you implement a solution that relies purely on Ajax, this means that every user interaction triggers a new call to the server to generate the updated markup. A server roundtrip is acceptable for many applications, but sometimes you need to respond to user input quicker. Especially when network conditions are not optimal, the server roundtrip can get quite noticeable.

No lifecycle for scripts

Typically a fragment also needs client-side JavaScript. If you want to make something like a tooltip work, an event handler needs to be attached to the markup that triggers it. When the outer page updates a fragment by replacing it with new markup fetched from the server, this event handler needs to be removed first and re-added to the new markup.

The team that owns the fragment must know when their code should run. There are multiple ways to implement this. MutationObserver, 9 annotation via data-* attributes, custom elements, or custom events can help here. But you have to implement these mechanisms manually. In chapter 5 we’ll explore how Web Components can help here.

3.1.6 When does an Ajax integration make sense?

Integration via Ajax is straightforward. It’s robust and easy to implement. It also introduces little performance overhead, especially compared to the iframe solution, where every fragment creates a new browsing context.

If you are generating your markup on the server-side, this solution makes sense. It also plays well together with the server-side includes concept we’ll learn in the next chapter.

For fragments that contain a lot of interactivity and have local state, it might become tricky. Loading and reloading the markup from the server on every interaction might feel sluggish due to network latency. The use of Web Components and client-side rendering we’ll discuss later in the book can be an alternative.

3.1.7 Summary

Let’s revisit the three integration techniques we’ve touched on so far. Figure 3.3 shows how the links, iframe, and Ajax approach compare to each other from a developer’s and user’s perspective.

Figure 3.3 Comparison of different integration techniques. Compared to the iframes or links approach, it’s possible to build more performant and usable solutions with Ajax. But you lose technical isolation and need to rely on inter-team conventions like using CSS prefixes.

I decided to compare them along four properties:

  • Technical complexity describes how easy or complicated it is to set up and work in a model like this.

  • Technical isolation indicates how much native isolation you get out of the box.

  • Interactivity says how well this method is suited for building applications that feel snappy and respond to user input quickly.

  • First load time describes the performance characteristics. How fast does the user get to the content they want to see?

Note that this comparison should only give you an impression of how these techniques relate to each other in the defined categories. It’s by no means representative, and you can always find counterexamples.

Next, we’ll look at how to integrate our sample applications further. The goal is to make the applications of all teams available under one single domain.

3.2 Server-side routing via Nginx

The switch from iframe to Ajax had measurable positive effects. Search engine ranking improved, and we received emails from visually impaired users who wrote in to say that our site is much more screen-reader-friendly now. But we also got some negative feedback. Some customers complained that the URLs for the shop are quite long and hard to remember. Team Decide picked Heroku as a hosting platform and published their site at https://team-decide-tractors.herokuapp.com/. Team Inspire chose Google Firebase for hosting. They’ve released their application at https://tractor-inspirations .firebaseapp.com/. This distributed setup worked flawlessly, but switching domains on every click is not optimal.

Ferdinand, the CEO of Tractor Models, Inc., took this request seriously. He decided that all of the company’s web properties should be accessible from one domain. After lengthy negotiations, he was able to acquire the domain the-tractor.store.

The next task for the teams is to make their applications accessible through https://the-tractor.store. Before going to work, they need to make a plan. A shared web server is needed. It will be the central point where all requests to https://the -tractor.store will arrive initially. Figure 3.4 illustrates this concept.

Figure 3.4 The shared web server is inserted between the browser and the team applications. It acts as a proxy and forwards the requests to the responsible teams.

The server routes all requests to the responsible application. It does not contain any business logic besides this. This routing web server is often called a frontend proxy. Each team should receive its own path prefix. The frontend proxy should route all requests starting with /decide/ to Team Decide’s server. They also require additional routing rules. The frontend proxy passes all requests starting with /product/ to Team Decide; the ones with /recommendations/ go to Team Inspire.

In our development environment, we again use different port numbers instead of configuring actual domain names. The frontend proxy we will set up listens on port 3000. Table 3.2 shows the routing rules our frontend proxy should implement.

Table 3.3 Frontend proxy routes incoming requests to the teams applications

Rule #

Path prefix

Team

Application

per team prefixes (default)

#1

/decide/

Decide

localhost:3001

#2

/inspire/

Inspire

localhost:3002

per page prefixes (additional)

#3

/product/

Decide

localhost:3001

#4

/recommendations/

Inspire

localhost:3002

Figure 3.5 illustrates how an incoming network request is processed. Let’s follow the numbered steps:

  1. The customer opens the URL /product/porsche. The request reaches the frontend proxy.

  2. The frontend proxy matches the path /product/porsche against its routing table. Rule #3 /product/ is a match.

  3. The frontend proxy passes the request to Team Decide’s application.

  4. The application generates a response and gives it back to the frontend proxy.

  5. The frontend proxy passes the answer to the client.

Figure 3.5 Flow of a request. The frontend proxy decides which application should handle an incoming request. It decides based on the URL path and the configured routing rules.

Let’s have a look at how to build a frontend proxy like this.

3.2.1 How to do it

The teams picked Nginx for this task. It’s a popular, easy to use, and pretty fast web server. Don’t worry if you haven’t worked with Nginx before. We’ll explain the fundamental concepts necessary to make our routing work.

Installing Nginx locally

If you want to run the example code locally, you need Nginx on your machine. For Windows users, it should work out of the box because I’ve included the Nginx binaries in the sample code directory. If you’re running macOS, the easiest option is to install Nginx via the Homebrew package manager. 10 Most Linux distributions offer an official Nginx package. On Debian- or Ubuntu-based systems you can install it via sudo apt-get install nginx. There’s no need for extra configuration. The example code only needs the Nginx binary to be present on your system. The npm script will automatically start and stop the Nginx together with the team’s applications.

Starting the applications

Start all three services by running this:

npm run 04_routing

The familiar Porsche Diesel Master should appear in your browser. Check your terminal to find a logging output that looks like this:

[decide ] :3001/product/porsche
[nginx ] :3000/product/porsche 200
[decide ] :3001/decide/static/page.css
[decide ] :3001/decide/static/page.js
[nginx ] :3000/decide/static/page.css 200
[nginx ] :3000/decide/static/page.js 200
[inspire] :3002/inspire/fragment/recommendations/porsche
[nginx ] :3000/inspire/fragment/recommendations/porsche 200
[inspire] :3002/inspire/static/fragment.css
[nginx ] :3000/inspire/static/fragment.css 200

In this log message, we see two entries for each request--one from the team ([decide] or [inspire]) and one from the frontend proxy [nginx]. You can see that all requests pass through the Nginx. The services create the log entry when they’ve produced a response. That explains why we always see the team application first and then the message from Nginx.

NOTE On Windows, the nginx log messages don’t appear because nginx.exe doesn’t offer an easy way to log to stdout. If you’re running Windows, you have to believe it’s working as described (or reconfigure the access_log in the nginx.conf to write them to a local file of your choice).

Let’s look into the frontend proxy configuration. You’ll need to understand two Nginx concepts for this:

  • Forwarding a request to another server (proxy_pass/upstream)

  • Differentiating incoming requests (location)

Nginx’s upstream concept allows you to create a list of servers that Nginx can forward requests to. The upstream configuration for Team Decide looks like this:

upstream team_decide {
  server localhost:3001;
}

You can differentiate incoming requests using location blocks. A location block has a matching rule that gets compared against every incoming request. Here’s a location block that matches all requests starting with /product/:

location /product/ {
  proxy_pass  http://team_decide;
}

See the proxy_pass directive in the location block? It advises Nginx to forward all matched requests to the team_decide upstream. You can consult the Nginx documentation 11 for a more in-depth explanation, but for now we have everything we need to understand our ./webserver/nginx.config configuration file.

Listing 3.6 webserver/nginx.conf

upstream team_decide {                  
  server localhost:3001;                
}                                       
upstream team_inspire {
  server localhost:3002;
}
http {
  ...
  server {
    listen 3000;
    ...
    location /product/ {                
      proxy_pass  http://team_decide;   
    }                                   
    location /decide/ {
      proxy_pass  http://team_decide;
    }
    location /recommendations {
      proxy_pass  http://team_inspire;
    }
    location /inspire/ {
      proxy_pass  http://team_inspire;
    }
}

Registers Team Decide’s application as an upstream called “team_decide”

Handles all request starting with /product/ and forwards them to the team_decide upstream

NOTE In our example, we use a local setup. The upstream points to localhost:3001. But you can put in any address you want here. Team Decide’s upstream might be team-decide-tractors.herokuapp.com. Keep in mind that the web server introduces an extra network hop. To reduce latency, you might want your web and application servers to be located in the same data center.

3.2.2 Namespacing resources

Now that both applications run under the same domain, their URL structure mustn’t overlap. For our example, the routes for their pages (/product/ and /recommendations) stay the same. All other assets and resources are moved into a decide/ or inspire/ folder.

We need to adjust the internal references to the CSS and JS files. But the URL patterns both teams agreed upon (the contract between the teams) also need to be updated. With the central frontend proxy in place, a team does not have to know the domain of the other team’s application anymore. It’s sufficient to use the path of the resource. Now Nginx’s upstream configuration encapsulates the domain information. Since all requests should go through the frontend proxy, we can remove the domain from the pattern:

  • product page

    old: http://localhost:3001/product/<sku>

    new: /product/<sku>

  • recommendation page

    old: http://localhost:3002/recommendations/<sku>

    new: /recommendations/<sku>

  • recommendation fragment

    old: http://localhost:3002/fragment/recommendations/<sku>

    new: /inspire/fragment/recommendations/<sku>

NOTE Notice that the path of the recommendation fragment URL received a team prefix (/inspire).

Introducing URL namespaces is a crucial step when working with multiple teams on the same site. It makes the route configuration in the web server easy to understand. Everything that starts with /<teamname>/ goes to upstream <teamname>. Team prefixes help with debugging because they make attribution easier. Looking at the path of a CSS file that’s causing an issue reveals which team owns it.

3.2.3 Route configuration methods

When your project grows, the number of entries in the routing configuration also grows. It can get complicated quickly. There are different ways to deal with this complexity. We can identify two different kinds of routes in our example application:

  1. Page-specific routes (like /product/)

  2. Team-specific routes (like /decide/)

Strategy 1: Team routes only

The easiest way to simplify your routes is to apply a team prefix to every URL. This way, your central routes configuration only changes when you introduce a new team to the project. The configuration looks like this:

/decide/   -> Team Decide
/inspire/  -> Team Inspire
/checkout/ -> Team Checkout

The prefixing is not an issue for internal URLs--ones the customer does not see like APIs, assets, or fragments. But for URLs that show up in the browser address bar, search results, or printed marketing material, this may be an issue. You are exposing your internal team structure through the URLs. You also introduce words (like decide, inspire) which a search engine bot would read and add to their index.

Choosing shorter one- or two-letter-prefixes can moderate this effect. This way your URLs might look like this:

/d/product/porsche  -> Team Decide
/i/recommendations  -> Team Inspire
/c/payment          -> Team Checkout

Strategy 2: Dynamic route configuration

If prefixing everything is not an option, putting the information about which team owns which page into your frontend proxy’s routing table is unavoidable:

/product/*        -> Team Decide
/wishlist         -> Team Decide
/recommendations  -> Team Inspire
/summer-trends    -> Team Inspire
/cart             -> Team Checkout
/payment          -> Team Checkout
/confirmation     -> Team Checkout

When you start small, this is usually not a big issue, but the list can quickly grow. And when your routes are not only prefix-based but include regular expressions, it can get hard to maintain.

Since routing is a central piece in a micro frontend architecture, it’s wise to invest in quality assurance and testing. You don’t want a new route entry to bring down other pieces of software.

There are multiple technical solutions for handling your routing. Nginx is only one option. Zalando open-sourced its routing solution called Skipper. 12 They’ve built it to handle more than 800,000 route definitions.

3.2.4 Infrastructure ownership

The key factors when setting up a micro-frontends-style architecture are team autonomy and end-to-end responsibility. Consider these aspects of every decision you make. Teams should have all the power and tools they need to accomplish their job as well as possible. In a micro frontends architecture, we accept redundancy in favor of decoupling.

Introducing a central web server does not fit this model. To serve everything from the same domain, it’s technically necessary to have one single service that acts as a common endpoint, but it also introduces a single point of failure. When the web server is down, the customer sees nothing, even if the applications behind it are still running. Therefore, you should keep central components like this to a minimum. Only introduce them when there is no reasonable alternative.

Clear ownership is vital to ensure that these central components run stably and get the attention they need. In classical software projects, a dedicated platform team would run it. The goal of this team would be to provide and maintain these shared services. But in practice, these horizontal teams create a lot of friction.

Distributing infrastructure responsibility across the product teams can help to keep the focus on customer value (see figure 3.6). In our example, Team Decide could take responsibility for running and maintaining Nginx. They, and the other teams, have a natural interest in this service being well maintained and running stably. The feature teams have no motivation to make a shared service fancier than it needs to be. In our projects we’ve had good experiences with this approach. It helped to maintain customer focus, even when we were working deeper in the stack. In chapters 12 and 13, we’ll go deeper into the centralized vs. decentralized discussion.

Figure 3.6 Avoid introducing pure infrastructure teams. Distributing responsibility for shared services to the product teams can be a good alternative model.

3.2.5 When does it make sense?

Delivering the contents of multiple teams through a single domain is pretty standard. Customers expect that the domain in their browser address bar does not change on every click.

It also has technical benefits:

  • Avoids browser security issues (CORS)

  • Enables sharing data like login-state through cookies

  • Better performance (only one DNS lookup, SSL handshake, ...)

If you are building a customer-facing site that should be indexed by search engines, you definitely want to implement a shared web server. For an internal application, it might also be ok to skip the extra infrastructure and just go with a subdomain-per-team approach.

Now we’ve discussed routing on the server side. Nginx is only one way to do it; other tools like Traefik 13 or Varnish 14 offer similar functionality. In chapter 7 you’ll learn how to move these routing rules to the browser. Client-side routing enables us to build a unified single-page app. But before we get there, we’ll stay on the server and look at more sophisticated composition techniques.

Summary

  • You can integrate the contents of multiple pages into a single document by loading them via Ajax.

  • Compared to the iframe approach, a deeper Ajax integration is better for accessibility, search engine compatibility, and performance.

  • Since the Ajax integration puts fragments into the same document, it’s possible to have style collisions.

  • You can avoid CSS collisions by introducing team namespaces for CSS classes.

  • You can route the content of multiple applications through one frontend proxy, which serves all content through a unified domain.

  • Using team prefixes in the URL path is an excellent way to make debugging and routing easier.

  • Every piece of software should have clear ownership. When possible, avoid creating horizontal teams like a platform team.


1.See Arly BcBlain, “Saving the Day with Scoped CSS,” CSS-Tricks, https://css-tricks.com/saving-the-day-with-scoped-css/.

2.See “Can I Use Shadow DOM?” https://caniuse.com/#feat=shadowdomv1.

3.BEM, http://getbem.com/naming/.

4.See http://mng.bz/Edoj.

5.See https://github.com/gustafnk/h-include.

6.For details on the Googlebot’s JavaScript support, see https://developers.google.com/search/docs/guides/rendering.

7.See https://en.wikipedia.org/wiki/Progressive_enhancement.

8.See https://resilientwebdesign.com/.

9.See Louis Lazaris, “Getting To Know The MutationObserver API,” Smashing Magazine, http://mng.bz/Mdao.

10.Get the Homebrew package manager at https://brew.sh and install Nginx by running brew install nginx.

11.See https://nginx.org/en/docs/beginners_guide.html#proxy.

12.See https://opensource.zalando.com/skipper/.

13.See https://docs.traefik.io.

14.See https://varnish-cache.org.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.19.27.178