preface
After talking about ModuleFederation provided by Webpack5 and discussing its possibilities in depth, some friends asked me if this is the micro front end, which looks very complicated. Emm, it’s true that it’s easy to implement micro fronts with ModuleFederation, but micro fronts can be implemented without tools, and today we’ll leave Webpack behind and talk about micro fronts.
What is a micro front end?
A micro front end is a way for multiple teams to work together to build modern Web applications by independently publishing functionality. Each team can use their preferred technology stack, develop independently, deploy independently, and do incremental upgrades.
By means of the description here we can see, the front end is more of a preference for the concept of software development and management, under this strategy, we adjust our organization structure, divided into multiple small team, limit the development task of each team in a small scope, independent of each other noninterference, thus can parallel development, at the same time for multiple business extension, This improves iteration speed. In addition, the change of the development form is invisible to users, and a complete app will finally reach the hands of users. This involves putting together the work of various teams, which can be done using hyperlinks, IFrame, SSR, and other techniques.
Some people may wonder, in our previous development, we would break the module into more smaller tasks to complete, what is different? It splits teams to develop individual apps, and then puts them back together again. What’s the good of all this effort? The special point of micro front end is independence and control. People who have learned about micro-services may be more able to understand this, and other people may not be so anxious. Here, we still feel the practice of this idea through a practical example.
Practice for two times?
Let’s say we need to build a blog app. It starts with a list of articles, details of articles, and recommended articles. After discussion, the members unanimously decided to quickly iterate A version first, adopt the micro front end mode, and develop three functions in parallel. Accordingly, three teams were divided. Team A realized the list of articles, team B realized the details of articles, and team C realized the recommendation of articles. (The way we divide the team here is actually a bit unreasonable. The list of articles and the details of articles belong to the same business category, but in order to keep things as simple as possible and clarify the concept, we divide them this way.)
The A team first implemented their feature by providing an app to display A list of posts, and then deployed it on their own server, allowing users to browse the existing blog list directly using the app:
Class ArticleListPage extends HTMLElement {connectedCallback() {console.log("ArticleListPage page connectedCallback"); this.attachShadow({ mode: "open" }); This. ShadowRoot. InnerHTML = ` < h1 > hello ~ < / h1 > < strong > this is the article list: < / strong > < ul > < li > < a href = "/ article/webpack" > webpack principle analysis < / a > < / li > < li > < a href = "/ article/mf" > micro front-end from entry to give up < / a > < / li > < li > < a Href ="/article/react"> </a></li> </ul> '; } } window.customElements.define("article-list-page", ArticleListPage);Copy the code
The app here can be a complex SPA or a few simple pages. Instead of using a framework, we used the Web Component to keep the example as simple as possible. Web Component is a new standard proposed in recent two years, which can realize componentize development. Besides, React, Vue and other frameworks also provide solutions compatible with Web Component. We can use Web Component to write some glue code. In addition, the Web Component also provides some lifecycle functions (such as the connectedCallback here) for you to learn more about. We also didn’t involve the back-end call here, so we wrote some data. In addition, we also used Shadow Dom, mainly for isolation and to avoid problems such as style conflicts when different micro front-end apps are integrated together.
A list of articles is not enough, but at about the same time team B deployed their article details app:
< span style =" box-sizing: border-box; color: # 33px; color: # 33px "> < span style =" box-sizing: border-box; color: # 33px "> <h1>${article.name}</h1> <span>${article.content} </span> `; }; class WebpackArticle extends HTMLElement { connectedCallback() { this.innerHTML = template({name: "webpack", content: "webpack"}); } } window.customElements.define( "webpack-article", WebpackArticle ); class ReactArticle extends HTMLElement { connectedCallback() { this.innerHTML = template( {name: "react", content: "react"}); } } window.customElements.define( "react-article", ReactArticle ); class MfArticle extends HTMLElement { connectedCallback() { this.innerHTML = template({name: "micro-frontend", content: "micro-frontend"}); } } window.customElements.define( "mf-article", MfArticle );Copy the code
The normal situation here is to implement a common component, and then after the component is mounted to the background request data, we also write some data here. Now the two teams’ apps can jump to each other using the agreed rules for defining urls:
Don’t forget there’s a more Articles recommendation button at the top of the post, which takes you to the recommendations page:
The class RecommendationArticlePage extends HTMLElement {connectedCallback () {the console. The log (" RecommendationArticlePage page connectedCallback"); this.attachShadow({ mode: "open" }); This. ShadowRoot. InnerHTML = ` < h1 > good readers ~ < / h1 > < a href = "/" > home page < / a > < span > this is a recommended list of articles: < / span > < ul > < li > < a href = "/ article/webpack" > webpack principle analysis < / a > < / li > < li > < a href = "/ article/mf" > micro front-end from entry to give up < / a > < / li > < li > < a Href ="/article/react"> </a></li> </ul> '; } } window.customElements.define("recommendation-article-page", RecommendationArticlePage);Copy the code
Now the teams have implemented their businesses and deployed them on their own servers. Since they can choose their preferred technology stack (for example, recommendation systems may involve machine learning and their back end may go straight to python), they can also choose their preferred cloud platform for deployment and even have each app have its own domain name. As they are deployed independently, even if the recommendation system crashes, other apps will not be affected.
In this process, the advantage of the micro front end development is shown. If the team now finds that a publishing function is needed for the open test of the system, based on the current development model, just one more team can be responsible for that function without affecting the progress of the other teams. And because each team is focused on a small goal, the cost of refactoring and even rewriting is greatly reduced if the technology stack needs to be switched later, and new people join the team more quickly. This also answers the question we had above about micro front end development.
Everything is ready, we need to put these apps together to give the user a silky experience. In fact, to some extent, these apps have been associated with each other. Whenever the user wants to go from one business scenario to another, he clicks the hyperlink in the page to open the next page, which has completed the business scenario switch. Users may feel strange when they see that different pages in the address bar use different domain names. We can use Nginx to set up a proxy, configure access through a unified domain name, and then forward the request to an app responsible for this business according to different routes. It should be noted that this approach is somewhat contrary to the concept of decentralization of the micro front end. There may be a situation where every app at the bottom is still running but the agent is down. In this case, users cannot use the app. And in this age of spas, it’s a bit out of place to have so many Windows open.
It is also possible to implement similar routing between multiple apps. The most straightforward idea is to listen for URL changes to dynamically display different content.
Well, one window per page is a bit bad
Here, we use the History library to implement the function of listening to URLS. Our familiar React Router is also based on this library to implement the function of listening to urls.
Create a new HTML and use it as a shell for the other micro front-end apps. Put a div in it first, making a hole in the area where we want to show the integrated micro front-end app. This page will load the other apps’ code from their servers at runtime.
<div id="app-content"></div>
Copy the code
Then create a history object that listens for route changes:
window.appHistory = HistoryLibrary.createBrowserHistory();
Copy the code
Next, configure the mapping between the route and the corresponding component:
const routes = {
"/article/webpack": "webpack-article",
"/article/mf": "mf-article",
"/article/react": "react-article",
"/recommendation": "recommendation-article-page",
"/": "article-list-page",
};
Copy the code
So when switching routes, we can determine what micro front-end app to display according to the route! Now that the mapping relationship is configured, we need to set the callback when the route changes and replace each app:
const findComponentByPath = (pathname) => { return routes[pathname]; } const updateContent = ({location}) => { const next = findComponentByPath(location.pathname); const current = appContent.firstChild; if (current.nodeName.toLowerCase() ! == next) { const newComponent = document.createElement(next); appContent.replaceChild(newComponent, current); } } window.appHistory.listen(updateContent);Copy the code
Finally, don’t forget to intercept global click events on hyperlinks to avoid opening new pages:
Document. The addEventListener (" click ", e = > {the console. The log (" app - shell intercepted hyperlink click event "); if (e.target.nodeName === "A") { const href = e.target.getAttribute("href"); window.appHistory.push(href); e.preventDefault(); }});Copy the code
Intercepted the click on the links after the event, according to the content of the URL for the current page do replace, the whole experience is much better now, we realize the state of flow in the same window, but the current practice of the coupling is very high, do not conform to the concept of micro front-end decentralized, “shell” need to know that each application contains each page of the corresponding address, Every time we publish a page, we have to tell the shell to configure its route, which causes the shell to republish, creating dependencies and making it impossible to publish and deploy independently.
Spa-like routing mode
To repeat, each of our micro front-end apps can have its own route (for example, the micro front-end app implemented by React will have its React Router to process the jump of specific pages). For an app that has adopted SPA technology, we do not need to process the route at all. What we really need is a spa-like transition from one team’s business scenario to another. With this logic simplified, our shell now just has to figure out which URL prefix to forward to which app. If the micro front-end app is already on the page, it doesn’t move. Its routing system will handle everything. If the corresponding micro front-end app is not on the page, then we replace it on the page.
Now our “shell” task is reduced, unless we now add a new development team to develop other apps, otherwise there is no need to modify the “shell” and redeploy, and in each app, it does not need to notify others how many new pages, in their own business scope can be freely deployed.
window.appHistory = HistoryLibrary.createBrowserHistory(); const appContent = document.querySelector("#app-content"); const routes = { "/article/": "article-page", "/recommendation": "recommendation-article-page", "/": "article-list-page", }; const findComponentByPath = (pathname) => { const prefix = Object.keys(routes).find(key => pathname.startsWith(key) ); return routes[prefix]; } const updateContent = ({location}) => { const next = findComponentByPath(location.pathname); const current = appContent.firstChild; if (current.nodeName.toLowerCase() ! == next) {console.log(" Micro front page used inside app-shell changed "); Console. log(" Top level routing changed "); const newComponent = document.createElement(next); appContent.replaceChild(newComponent, current); } else {console.log(" the micro front page used inside the app-shell remains unchanged "); } } window.appHistory.listen(updateContent); updateContent({location: window.location}); Document. The addEventListener (" click ", e = > {the console. The log (" app - shell intercepted hyperlink click event "); if (e.target.nodeName === "A") { const href = e.target.getAttribute("href"); window.appHistory.push(href); e.preventDefault(); }});Copy the code
Now our “shell” only determines which micro front-end app to cut by the prefix when the URL changes. The specific page to be displayed will be handled by the corresponding micro front-end app. Since we are not using a framework here, we can do some listening inside the Web Component in order to simulate the behavior of routes within the SPA.
const articles = { webpack: {name: "webpack", content: "webpack"}, mf: {name: "micro-frontend", content: "micro-frontend"}, react: {name: "react", content: "react"} }; Class ArticlePage extends HTMLElement {connectedCallback() {console.log("ArticlePage page connectedCallback"); this.render(window.location); this.unlisten = window.appHistory.listen(({location}) => { this.render(location) } ) } render(location) { Console. log(" Route changed (level-2 route/article)"); const match = location.pathname.match("/article/(.*)"); const article = match && articles[match[1]]; if (article) { this.attachShadow({mode: "open"}); This. ShadowRoot. InnerHTML = ` < a href = "/" > home page < / a > - < a href = "/ recommendation" > more articles recommended < / a > < h1 > ${article. Name} < / h1 > <span>${article.content} </span> `; } } disconnectedCallback() { this.unlisten(); } } window.customElements.define("article-page", ArticlePage);Copy the code
This replaces the three separate article components with a more generic article page component that displays the correct content based on routing.
Note that our implementation does not limit which framework each integrated app must use, as long as it provides a well-implemented Web Component. This is our protocol. This is not too difficult for modern front-end frameworks
This is how most of the micro front-end frameworks are implemented.
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
start();
Copy the code
Or the registration code of single-SPA:
singleSpa.registerApplication(
'appName',
() => System.import('appName'),
location => location.pathname.startsWith('appName'),
);
Copy the code
We can find many commonalities:
- We’re through
Web Component
Life cycle as a protocol to do some processing, and these frameworks also provide a mechanism to properly handle load behavior (e.gsingle-spa
Provides a number of columnssingle-spa-*
Toolkit to handle some load timing). - These frameworks also require a tag ID (usually div) to attach the integrated micro front-end app to the page.
- Also specify which routes the registered micro front-end app maps to.
These frameworks are built on top of this with more optimizations, enabling on-demand loading and so on. Once you understand the underlying fundamentals, you’ll be able to use any micro front end framework you want.
Write in the last
Here, we must have found that there is no fancy and novel black technology, which is based on the existing technology to change the way of thinking to complete this set of development mode. That’s all for today, I hope this article can let you have a further understanding of the micro front end, welcome to communicate ~ together
The complete code