preface
Since the micro front-end framework micro-App became open source, many friends are very interested and ask me how to achieve it, but it is not a few words can be understood. To illustrate how this works, I’m going to implement a simple micro front-end framework from scratch. Its core features include rendering, JS sandbox, style isolation, and data communication. This is the first in a series of four articles: Rendering.
Through these articles, you will learn how micro front end frameworks work and how they are implemented, which will be of great help if you use them later or write your own. If this post helped you, feel free to like it and leave a comment.
Related to recommend
Micro-app source address: github.com/micro-zoe/m…
The overall architecture
Like micro-app, our simple micro front end framework is designed to be as simple as using iframe, while avoiding the problems existing in iframe. Its usage is as follows:
The end result is a bit similar, with the entire micro front application encapsulated in the custom TAB Micro-app. The rendered effect is as follows:
So our overall architecture idea is: CustomElement + HTMLEntry.
HTMLEntry is rendered with an HTML file as the entry address. http://localhost:3000/ in the image above is an HTML address.
Concept map:
Pre – work
Before we can get started, we need to set up a development environment and create a code repository, simple-micro-app.
The directory structure
The code repository is divided into the SRC main directory and examples directory, with vuE2 for the base application and React17 for the sub-application. Both projects are built using official scaffolding and the build tool is rollup.
The two application pages are shown below:
Base application — VUE2
Subapplication — REact17
In the VUE2 project, configure resolve.alias to point simple-micro-app to index.js in the SRC directory.
// vue.config.js.chainWebpack: config= > {
config.resolve.alias
.set("simple-micro-app", path.join(__dirname, '.. /.. /src/index.js'))},Copy the code
Configure static resources in React17’s Webpack-dev-server to support cross-domain access.
// config/webpackDevServer.config.js.headers: {
'Access-Control-Allow-Origin': The '*',},Copy the code
The official start of the
To make this clear, instead of Posting the finished code, we’ll start from scratch and implement the process step by step so it’s clearer and easier to understand.
Create a container
The rendering of the micro front end is to load the static resources such as JS and CSS of the child application into the base application for execution, so the base application and the child application are essentially the same page. This is different from the iframe, which creates a new window. Since the entire window information is initialized every time the iframe is loaded, the performance of the iframe is low.
Just as each front-end frame must specify a root element at render time, micro front-end renders need to specify a root element as a container, which can be a div or other element.
Here we use customElements created by customElements, because it not only provides an element container, but also comes with its own lifecycle functions. We can do things like load rendering in these hook functions to simplify the steps.
// /src/element.js
// Customize the element
class MyElement extends HTMLElement {
// Declare the names of the attributes that you want to listen to, and only when those attributes change will the attributeChangedCallback be triggered
static get observedAttributes () {
return ['name'.'url']}constructor() {
super(a); }connectedCallback() {
// When the element is inserted into the DOM, the static resources of the child application are loaded and rendered
console.log('micro-app is connected')
}
disconnectedCallback () {
// When an element is removed from the DOM, some unload operations are performed
console.log('micro-app has disconnected')
}
attributeChangedCallback (attr, oldVal, newVal) {
// When an element attribute changes, you can get the value of the attribute name, URL, and so on
console.log(`attribute ${attrName}: ${newVal}`)}}When a micro-app element is inserted or removed from the DOM, the corresponding lifecycle function is triggered. * /
window.customElements.define('micro-app', MyElement)
Copy the code
Micro-app elements can be duplicated, so we add a layer of judgment and put it in the function.
// /src/element.js
export function defineElement () {
// If it is already defined, ignore it
if (!window.customElements.get('micro-app')) {
window.customElements.define('micro-app', MyElement)
}
}
Copy the code
Define the default object SimpleMicroApp in/SRC /index.js and introduce and execute the defineElement function.
// /src/index.js
import { defineElement } from './element'
const SimpleMicroApp = {
start () {
defineElement()
}
}
export default SimpleMicroApp
Copy the code
Introducing simple – micro – app
Introduce simple-micro-app in main.js of the VUE2 project and execute the start function for initialization.
// vue2/src/main.js
import SimpleMicroApp from 'simple-micro-app'
SimpleMicroApp.start()
Copy the code
The Micro-App tag can then be used anywhere in the VUE2 project.
<! -- page1.vue -->
<template>
<div>
<micro-app name='app' url='http://localhost:3001/'></micro-app>
</div>
</template>
Copy the code
After inserting the Micro-app label, you can see the hook information printed by the console.
This completes the initialization of the container element, and all the elements of the child application will be placed into the container. Next we need to complete the static resource loading and rendering of the child application.
Create a microapplication instance
Obviously, the initialized operation is executed in a connectedCallback. We declare a class, each instance of which corresponds to a microapplication, to control the microapplication resource loading, rendering, unloading, and so on.
// /src/app.js
// Create a micro application
export default class CreateApp {
constructor () {}
status = 'created' / / component state, including created/loading/mount/unmount to
// Store the static resources of the application
source = {
links: new Map(), // The static resource corresponding to the link element
scripts: new Map(), // Static resources corresponding to the script element
}
// Execute when the resource has been loaded
onLoad () {}
/** render resources after loading */
mount () {}
/** * Uninstall the application * perform operations such as closing the sandbox and clearing the cache */
unmount () {}
}
Copy the code
We initialize the instance in the connectedCallback function, passing in the name, URL, and element itself as parameters, recording these values in CreateApp’s constructor, and requesting the HTML based on the URL address.
// /src/element.js
import CreateApp, { appInstanceMap } from './app'. connectedCallback () {// Create a microapplication instance
const app = new CreateApp({
name: this.name,
url: this.url,
container: this,})// Write to the cache for subsequent functionality
appInstanceMap.set(this.name, app)
}
attributeChangedCallback (attrName, oldVal, newVal) {
// Record the value of name and URL respectively
if (attrName === 'name'&&!this.name && newVal) {
this.name = newVal
} else if (attrName === 'url'&&!this.url && newVal) {
this.url = newVal
}
}
...
Copy the code
Static resources are requested based on the parameters passed in when the instance is initialized.
// /src/app.js
import loadHtml from './source'
// Create a micro application
export default class CreateApp {
constructor ({ name, url, container }) {
this.name = name // Application name
this.url = url / / url
this.container = container / / micro - app elements
this.status = 'loading'
loadHtml(this)}... }Copy the code
Request the HTML
We use FETCH to request static resources. The advantage is that the browser supports promises, but this also requires cross-domain access to the child application’s static resources.
// src/source.js
export default function loadHtml (app) {
fetch(app.url).then((res) = > {
return res.text()
}).then((html) = > {
console.log('html:', html)
}).catch((e) = > {
console.error('Error loading HTML', e)
})
}
Copy the code
Since the request JS, CSS, and so on are required to use fetch, we extract it as a public method.
// /src/utils.js
/** * Get static resources *@param {string} Url Static resource address */
export function fetchSource (url) {
return fetch(url).then((res) = > {
return res.text()
})
}
Copy the code
Reuse the wrapped method and process the retrieved HTML.
// src/source.js
import { fetchSource } from './utils'
export default function loadHtml (app) {
fetchSource(app.url).then((html) = > {
html = html
.replace(/
]*>[\s\S]*? <\/head>/i
[^>.(match) = > {
// Replace the head tag with micro-app-head, because web pages are only allowed to have one head tag
return match
.replace(/<head/i.'<micro-app-head')
.replace(/<\/head>/i.'</micro-app-head>')
})
.replace(/
]*>[\s\S]*? <\/body>/i
[^>.(match) = > {
// Replace the body tag with micro-app-body to prevent problems caused by duplication of the body tag applied to the dock.
return match
.replace(/<body/i.'<micro-app-body')
.replace(/<\/body>/i.'</micro-app-body>')})// Convert the HTML string to a DOM structure
const htmlDom = document.createElement('div')
htmlDom.innerHTML = html
console.log('html:', htmlDom)
// Further extract and process js, CSS and other static resources
extractSourceDom(htmlDom, app)
}).catch((e) = > {
console.error('Error loading HTML', e)
})
}
Copy the code
After the HTML is formatted, we have a DOM structure. As you can see from the image below, the DOM structure contains the link, style, script, and so on. The DOM needs to be processed further.
Extract static resource addresses such as JS and CSS
We recursively process each DOM node in the extractSourceDom method, query all link, style, script tags, extract the static resource address and format the tags.
// src/source.js
/** * recursively process each child element *@param Parent Parent element *@param App Application instance */
function extractSourceDom(parent, app) {
const children = Array.from(parent.children)
// Recurse each child element
children.length && children.forEach((child) = > {
extractSourceDom(child, app)
})
for (const dom of children) {
if (dom instanceof HTMLLinkElement) {
// Extract the CSS address
const href = dom.getAttribute('href')
if (dom.getAttribute('rel') = = ='stylesheet' && href) {
// Count to the source cache
app.source.links.set(href, {
code: ' '.// Code content})}// Delete the original element
parent.removeChild(dom)
} else if (dom instanceof HTMLScriptElement) {
// And extract the JS address
const src = dom.getAttribute('src')
if (src) { / / remote script
app.source.scripts.set(src, {
code: ' '.// Code content
isExternal: true.// Whether to remote script})}else if (dom.textContent) { / / an inline script
const nonceStr = Math.random().toString(36).substr(2.15)
app.source.scripts.set(nonceStr, {
code: dom.textContent, // Code content
isExternal: false.// Whether to remote script
})
}
parent.removeChild(dom)
} else if (dom instanceof HTMLStyleElement) {
// Make style isolation}}}Copy the code
Requesting static resources
The above has got HTML CSS, JS and other static resources in the address, the next is to request these addresses, get the content of the resource.
Next, we refine loadHtml by adding a method to request resources under extractSourceDom.
// src/source.js.export default function loadHtml (app) {...// Further extract and process js, CSS and other static resources
extractSourceDom(htmlDom, app)
// Get the micro-app-head element
const microAppHead = htmlDom.querySelector('micro-app-head')
// If there is a remote CSS resource, request it via fetch
if (app.source.links.size) {
fetchLinksFromHtml(app, microAppHead, htmlDom)
} else {
app.onLoad(htmlDom)
}
// If there is a remote JS resource, request it via fetch
if (app.source.scripts.size) {
fetchScriptsFromHtml(app, htmlDom)
} else {
app.onLoad(htmlDom)
}
}
Copy the code
FetchLinksFromHtml and fetchScriptsFromHtml request CSS and JS resources, respectively. After requesting resources, the processing mode is different. CSS resources will be converted into style tags and inserted into the DOM, while JS will not execute immediately.
The concrete implementation of the two methods is as follows:
// src/source.js
/** * Get link remote resource *@param App Application instance *@param microAppHead micro-app-head
* @param HtmlDom HTML DOM structure */
export function fetchLinksFromHtml (app, microAppHead, htmlDom) {
const linkEntries = Array.from(app.source.links.entries())
// Request all CSS resources via fetch
const fetchLinkPromise = []
for (const [url] of linkEntries) {
fetchLinkPromise.push(fetchSource(url))
}
Promise.all(fetchLinkPromise).then((res) = > {
for (let i = 0; i < res.length; i++) {
const code = res[i]
// Get the CSS resource and insert the style element into the micro-app-head
const link2Style = document.createElement('style')
link2Style.textContent = code
microAppHead.appendChild(link2Style)
// Put the code in the cache and get it from the cache when you render it again
linkEntries[i][1].code = code
}
// Execute the onLoad method after the processing is complete
app.onLoad(htmlDom)
}).catch((e) = > {
console.error('Error loading CSS', e)
})
}
/** * Get js remote resource *@param App Application instance *@param HtmlDom HTML DOM structure */
export function fetchScriptsFromHtml (app, htmlDom) {
const scriptEntries = Array.from(app.source.scripts.entries())
// Request all JS resources via fetch
const fetchScriptPromise = []
for (const [url, info] of scriptEntries) {
// If it is an inline script, there is no need to request resources
fetchScriptPromise.push(info.code ? Promise.resolve(info.code) : fetchSource(url))
}
Promise.all(fetchScriptPromise).then((res) = > {
for (let i = 0; i < res.length; i++) {
const code = res[i]
// Put the code in the cache and get it from the cache when you render it again
scriptEntries[i][1].code = code
}
// Execute the onLoad method after the processing is complete
app.onLoad(htmlDom)
}).catch((e) = > {
console.error('Error loading JS', e)
})
}
Copy the code
As you can see above, both the CSS and JS execute onLoad methods after loading, so the onLoad method is executed twice. Next we need to refine the onLoad method and render the micro application.
Apply colours to a drawing
Since onLoad is executed twice, we mark it, and when the second execution is done, all resources are loaded, and then render.
// /src/app.js
// Create a micro application
export default class CreateApp {...// Execute when the resource has been loaded
onLoad (htmlDom) {
this.loadCount = this.loadCount ? this.loadCount + 1 : 1
// Perform the render on the second execution and the component is not unloaded
if (this.loadCount === 2 && this.status ! = ='unmount') {
// Record the DOM structure for subsequent operations
this.source.html = htmlDom
// Execute the mount method
this.mount()
}
}
...
}
Copy the code
The micro application completes basic rendering by inserting the DOM structure into the document in the mount method and then executing the JS file for rendering.
// /src/app.js
// Create a micro application
export default class CreateApp {.../** render resources after loading */
mount () {
// Clone the DOM node
const cloneHtml = this.source.html.cloneNode(true)
// Create a fragment node as a template so that no redundant elements are generated
const fragment = document.createDocumentFragment()
Array.from(cloneHtml.childNodes).forEach((node) = > {
fragment.appendChild(node)
})
// Insert the formatted DOM structure into the container
this.container.appendChild(fragment)
/ / js
this.source.scripts.forEach((info) = >{(0.eval)(info.code)
})
// The tag is applied as rendered
this.status = 'mounted'}... }Copy the code
The above steps complete the basic rendering of the micro front end, let’s take a look at the effect.
Begin to use
We embedded the micro front under the base application:
<! -- vue2/src/pages/page1.vue -->
<template>
<div>
<img alt="Vue logo" src=".. /assets/logo.png">
<HelloWorld :msg="' Base application vue@' + version" />
<! -- 👇 embedded micro front end -->
<micro-app name='app' url='http://localhost:3001/'></micro-app>
</div>
</template>
Copy the code
The final results are as follows:
React17 is already embedded and running.
Let’s add a lazy page page2 to the child application react17 to verify that the multi-page application works properly.
page2
Is very simple, just a paragraph of the title:
Add a button to the page and click to jump to Page2.
Click the button to get the following effect:
Render normally! 🎉 🎉
A simple micro front-end framework is complete, but at this point it is very basic, without JS sandbox and style isolation.
We’ll do a separate article on JS sandbox and style isolation, but there’s one more thing we need to do in the meantime — uninstall the application.
uninstall
The lifecycle function disconnectedCallback is automatically executed when the micro-app element is deleted, where we perform the unload related operations.
// /src/element.js
class MyElement extends HTMLElement {... disconnectedCallback () {// Obtain the application instance
const app = appInstanceMap.get(this.name)
// If there is a deStory property, the application is completely uninstalled, including cached files
app.unmount(this.hasAttribute('destory'))}}Copy the code
Next, refine the unmount method of the application:
// /src/app.js
export default class CreateApp {.../** * Uninstall application *@param Destory Whether to completely destroy and delete cache resources */
unmount (destory) {
// Update the status
this.status = 'unmount'
// Empty the container
this.container = null
// If deStory is true, the application is deleted
if (destory) {
appInstanceMap.delete(this.name)
}
}
}
Copy the code
When deStory is true, the instance of the application is deleted. At this point, all static resources lose reference and are automatically reclaimed by the browser.
Add a button in the base app VUE2 to toggle the show/hide state of the child app and verify that multiple render and uninstall are working properly.
The effect is as follows:
A and normal operation! 🎉
conclusion
To this micro front-end rendering article on the end of the article, we have completed the micro front-end rendering and unloading function, of course, its function is very simple, just describe the micro front-end basic implementation ideas. Next we will complete THE JS sandbox, style isolation, data communication and other features, if you can bear to read it, it will help you understand the micro front end.
Code address:
Github.com/bailicangdu…