Preface
Since the micro-front-end framework micro-app open sourced, many small partners are very interested and ask me how to achieve it, but this is not a few words to explain. In order to clarify the principle, I will implement a simple micro front-end framework from scratch. Its core functions include: rendering, JS sandbox, style isolation, and data communication. Because there is too much content, it will be divided into four articles to explain according to the function. This is the first article in a series: rendering.
Through these articles, you can understand the specific principles and implementation methods of the micro front-end framework, which will be of great help when you use the micro-front-end framework or write a set of the micro-front-end framework yourself. If this article is helpful to you, please like and leave a comment.
related suggestion
Micro-app source code address: https://github.com/micro-zoe/micro-app
Overall structure
Like micro-app, our simple micro front-end framework design idea is as simple as using iframes, but can avoid the problems of iframes. Its use is as follows:
The final effect is similar. The entire micro front-end application is encapsulated in a custom label micro-app. The rendered effect is as follows:
So our overall architecture idea is: CustomElement + HTMLEntry .
HTMLEntry uses html file as the entry address for rendering, and http://localhost:3000/
in the above figure is an html address.
concept drawing:
Pre-work
Before the official start, we need to set up a development environment and create a code warehouse simple-micro-app
.
Directory structure
The code repository is mainly divided into the src main directory and the examples case directory. Vue2 is the base application, and react17 is the sub-application. Both projects are created using official scaffolding, and the build tool uses rollup.
The two application pages are as follows:
base application - vue2
sub-application - react17
In the vue2 project, configure resolve.alias
and 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'))
},
Configure static resources in react17's webpack-dev-server to support cross-domain access.
// config/webpackDevServer.config.js
...
headers: {
'Access-Control-Allow-Origin': '*',
},
Officially begin
In order to speak more clearly, we will not directly post the completed code, but start from scratch and implement the whole process step by step, so that it can be clearer and easier to understand.
Create a container
The rendering of the micro front end loads the static resources such as js and css of the sub-application into the base application for execution, so the base application and the sub-application are essentially the same page. This is different from an iframe, which creates a new window. Since the entire window information is initialized every time it is loaded, the performance of the iframe is not high.
Just as each front-end frame must specify a root element when rendering, a root element must also be specified as a container when the micro front-end is rendered. This root element can be a div or other elements.
Here we use custom elements created through customElements, because it not only provides an element container, but also comes with life cycle functions. We can perform operations such as loading and rendering in these hook functions to simplify the steps.
// /src/element.js
// 自定义元素
class MyElement extends HTMLElement {
// 声明需要监听的属性名,只有这些属性变化时才会触发attributeChangedCallback
static get observedAttributes () {
return ['name', 'url']
}
constructor() {
super();
}
connectedCallback() {
// 元素被插入到DOM时执行,此时去加载子应用的静态资源并渲染
console.log('micro-app is connected')
}
disconnectedCallback () {
// 元素从DOM中删除时执行,此时进行一些卸载操作
console.log('micro-app has disconnected')
}
attributeChangedCallback (attr, oldVal, newVal) {
// 元素属性发生变化时执行,可以获取name、url等属性的值
console.log(`attribute ${attrName}: ${newVal}`)
}
}
/**
* 注册元素
* 注册后,就可以像普通元素一样使用micro-app,当micro-app元素被插入或删除DOM时即可触发相应的生命周期函数。
*/
window.customElements.define('micro-app', MyElement)
micro-app
element may have repeated definitions, so we add a layer of judgment and put it into the function.
// /src/element.js
export function defineElement () {
// 如果已经定义过,则忽略
if (!window.customElements.get('micro-app')) {
window.customElements.define('micro-app', MyElement)
}
}
In /src/index.js
define default object SimpleMicroApp
, is introduced and executed defineElement
function.
// /src/index.js
import { defineElement } from './element'
const SimpleMicroApp = {
start () {
defineElement()
}
}
export default SimpleMicroApp
Introduce simple-micro-app
Introduce simple-micro-app into the 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()
Then you can use the micro-app tag anywhere in the vue2 project.
<!-- page1.vue -->
<template>
<div>
<micro-app name='app' url='http://localhost:3001/'></micro-app>
</div>
</template>
After inserting the micro-app label, you can see the hook information printed on the console.
Above we have completed the initialization of the container elements, and all the elements of the sub-application will be put into this container. Next, we need to complete the static resource loading and rendering of the sub-application.
Create a micro application instance
Obviously, the initialization operation should be executed connectedCallback
We declare a class, each instance of which corresponds to a micro application, which is used to control the resource loading, rendering, and unloading of the micro application.
// /src/app.js
// 创建微应用
export default class CreateApp {
constructor () {}
status = 'created' // 组件状态,包括 created/loading/mount/unmount
// 存放应用的静态资源
source = {
links: new Map(), // link元素对应的静态资源
scripts: new Map(), // script元素对应的静态资源
}
// 资源加载完时执行
onLoad () {}
/**
* 资源加载完成后进行渲染
*/
mount () {}
/**
* 卸载应用
* 执行关闭沙箱,清空缓存等操作
*/
unmount () {}
}
We initialize the instance in the connectedCallback
function, pass in the name, url and the element itself as parameters, CreateApp
, and request html according to the url address.
// /src/element.js
import CreateApp, { appInstanceMap } from './app'
...
connectedCallback () {
// 创建微应用实例
const app = new CreateApp({
name: this.name,
url: this.url,
container: this,
})
// 记入缓存,用于后续功能
appInstanceMap.set(this.name, app)
}
attributeChangedCallback (attrName, oldVal, newVal) {
// 分别记录name及url的值
if (attrName === 'name' && !this.name && newVal) {
this.name = newVal
} else if (attrName === 'url' && !this.url && newVal) {
this.url = newVal
}
}
...
When the instance is initialized, static resources are requested based on the parameters passed in.
// /src/app.js
import loadHtml from './source'
// 创建微应用
export default class CreateApp {
constructor ({ name, url, container }) {
this.name = name // 应用名称
this.url = url // url地址
this.container = container // micro-app元素
this.status = 'loading'
loadHtml(this)
}
...
}
Request html
We use fetch to request static resources. The advantage is that the browser comes with and supports promises, but this also requires that the static resources of the sub-application support cross-domain access.
// 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('加载html出错', e)
})
}
Because fetch is required to request js, css, etc., we extract it as a public method.
// /src/utils.js
/**
* 获取静态资源
* @param {string} url 静态资源地址
*/
export function fetchSource (url) {
return fetch(url).then((res) => {
return res.text()
})
}
Re-use the encapsulated method and process the obtained html.
// src/source.js
import { fetchSource } from './utils'
export default function loadHtml (app) {
fetchSource(app.url).then((html) => {
html = html
.replace(/<head[^>]*>[\s\S]*?<\/head>/i, (match) => {
// 将head标签替换为micro-app-head,因为web页面只允许有一个head标签
return match
.replace(/<head/i, '<micro-app-head')
.replace(/<\/head>/i, '</micro-app-head>')
})
.replace(/<body[^>]*>[\s\S]*?<\/body>/i, (match) => {
// 将body标签替换为micro-app-body,防止与基座应用的body标签重复导致的问题。
return match
.replace(/<body/i, '<micro-app-body')
.replace(/<\/body>/i, '</micro-app-body>')
})
// 将html字符串转化为DOM结构
const htmlDom = document.createElement('div')
htmlDom.innerHTML = html
console.log('html:', htmlDom)
// 进一步提取和处理js、css等静态资源
extractSourceDom(htmlDom, app)
}).catch((e) => {
console.error('加载html出错', e)
})
}
After the html is formatted, we can get a DOM structure. As you can see from the figure below, this DOM structure contains tags such as link, style, script, and so on. Next, you need to further process the DOM.
Extract static resource addresses such as js and css
We recursively process each DOM node in the extractSourceDom
method, query all the link, style, and script tags, extract the static resource address and format the tags.
// src/source.js
/**
* 递归处理每一个子元素
* @param parent 父元素
* @param app 应用实例
*/
function extractSourceDom(parent, app) {
const children = Array.from(parent.children)
// 递归每一个子元素
children.length && children.forEach((child) => {
extractSourceDom(child, app)
})
for (const dom of children) {
if (dom instanceof HTMLLinkElement) {
// 提取css地址
const href = dom.getAttribute('href')
if (dom.getAttribute('rel') === 'stylesheet' && href) {
// 计入source缓存中
app.source.links.set(href, {
code: '', // 代码内容
})
}
// 删除原有元素
parent.removeChild(dom)
} else if (dom instanceof HTMLScriptElement) {
// 并提取js地址
const src = dom.getAttribute('src')
if (src) { // 远程script
app.source.scripts.set(src, {
code: '', // 代码内容
isExternal: true, // 是否远程script
})
} else if (dom.textContent) { // 内联script
const nonceStr = Math.random().toString(36).substr(2, 15)
app.source.scripts.set(nonceStr, {
code: dom.textContent, // 代码内容
isExternal: false, // 是否远程script
})
}
parent.removeChild(dom)
} else if (dom instanceof HTMLStyleElement) {
// 进行样式隔离
}
}
}
Request static resources
The addresses of static resources such as css and js in html have been obtained above, and the next step is to request these addresses to get the content of the resources.
Then improve loadHtml
, add the method of requesting resources extractSourceDom
// src/source.js
...
export default function loadHtml (app) {
...
// 进一步提取和处理js、css等静态资源
extractSourceDom(htmlDom, app)
// 获取micro-app-head元素
const microAppHead = htmlDom.querySelector('micro-app-head')
// 如果有远程css资源,则通过fetch请求
if (app.source.links.size) {
fetchLinksFromHtml(app, microAppHead, htmlDom)
} else {
app.onLoad(htmlDom)
}
// 如果有远程js资源,则通过fetch请求
if (app.source.scripts.size) {
fetchScriptsFromHtml(app, htmlDom)
} else {
app.onLoad(htmlDom)
}
}
fetchLinksFromHtml
and fetchScriptsFromHtml
request css and js resources respectively. The processing methods after requesting the resources are different. The css resources will be converted into style tags and inserted into the DOM, but js will not be executed immediately. We will execute js in the mount method of the application.
The specific implementation of the two methods is as follows:
// src/source.js
/**
* 获取link远程资源
* @param app 应用实例
* @param microAppHead micro-app-head
* @param htmlDom html DOM结构
*/
export function fetchLinksFromHtml (app, microAppHead, htmlDom) {
const linkEntries = Array.from(app.source.links.entries())
// 通过fetch请求所有css资源
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]
// 拿到css资源后放入style元素并插入到micro-app-head中
const link2Style = document.createElement('style')
link2Style.textContent = code
microAppHead.appendChild(link2Style)
// 将代码放入缓存,再次渲染时可以从缓存中获取
linkEntries[i][1].code = code
}
// 处理完成后执行onLoad方法
app.onLoad(htmlDom)
}).catch((e) => {
console.error('加载css出错', e)
})
}
/**
* 获取js远程资源
* @param app 应用实例
* @param htmlDom html DOM结构
*/
export function fetchScriptsFromHtml (app, htmlDom) {
const scriptEntries = Array.from(app.source.scripts.entries())
// 通过fetch请求所有js资源
const fetchScriptPromise = []
for (const [url, info] of scriptEntries) {
// 如果是内联script,则不需要请求资源
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]
// 将代码放入缓存,再次渲染时可以从缓存中获取
scriptEntries[i][1].code = code
}
// 处理完成后执行onLoad方法
app.onLoad(htmlDom)
}).catch((e) => {
console.error('加载js出错', e)
})
}
It can be seen above, after the completion of loading css and js are performed onLoad
method, so onLoad
method is executed twice, then we will improve onLoad
method and render micro applications.
Rendering
Because onLoad
been executed twice, we mark it. When it is executed for the second time, it means that all resources are loaded, and then the rendering operation is performed.
// /src/app.js
// 创建微应用
export default class CreateApp {
...
// 资源加载完时执行
onLoad (htmlDom) {
this.loadCount = this.loadCount ? this.loadCount + 1 : 1
// 第二次执行且组件未卸载时执行渲染
if (this.loadCount === 2 && this.status !== 'unmount') {
// 记录DOM结构用于后续操作
this.source.html = htmlDom
// 执行mount方法
this.mount()
}
}
...
}
In the mount
method, insert the DOM structure into the document, and then execute the js file to perform the rendering operation. At this time, the micro-application can complete the basic rendering.
// /src/app.js
// 创建微应用
export default class CreateApp {
...
/**
* 资源加载完成后进行渲染
*/
mount () {
// 克隆DOM节点
const cloneHtml = this.source.html.cloneNode(true)
// 创建一个fragment节点作为模版,这样不会产生冗余的元素
const fragment = document.createDocumentFragment()
Array.from(cloneHtml.childNodes).forEach((node) => {
fragment.appendChild(node)
})
// 将格式化后的DOM结构插入到容器中
this.container.appendChild(fragment)
// 执行js
this.source.scripts.forEach((info) => {
(0, eval)(info.code)
})
// 标记应用为已渲染
this.status = 'mounted'
}
...
}
The above steps have completed the basic rendering operation of the micro front end, let's take a look at the effect.
start using
We embed the micro front end under the base application:
<!-- vue2/src/pages/page1.vue -->
<template>
<div>
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld :msg="'基座应用vue@' + version" />
<!-- 👇嵌入微前端 -->
<micro-app name='app' url='http://localhost:3001/'></micro-app>
</div>
</template>
The final result is as follows:
It can be seen that react17 has been embedded and running normally.
page2
to the sub-application react17 to verify whether the multi-page application can run normally.
page2
is also very simple, just a title:
Add a button on the page, click to jump to page2.
Click the button, the result is as follows:
Render normally! 🎉🎉
A simple micro front-end framework is completed, of course, it is very basic at this time, there is no JS sandbox and style isolation.
We will share a separate article about JS sandbox and style isolation, but at this time we still have one thing to do-uninstall the application.
Uninstall
When the micro-app element is deleted, the life cycle function disconnectedCallback
will be automatically executed, and we perform uninstall related operations here.
// /src/element.js
class MyElement extends HTMLElement {
...
disconnectedCallback () {
// 获取应用实例
const app = appInstanceMap.get(this.name)
// 如果有属性destory,则完全卸载应用包括缓存的文件
app.unmount(this.hasAttribute('destory'))
}
}
Next, complete the application of the unmount
method:
// /src/app.js
export default class CreateApp {
...
/**
* 卸载应用
* @param destory 是否完全销毁,删除缓存资源
*/
unmount (destory) {
// 更新状态
this.status = 'unmount'
// 清空容器
this.container = null
// destory为true,则删除应用
if (destory) {
appInstanceMap.delete(this.name)
}
}
}
When destroy is true, delete the instance of the application. At this time, all static resources lose their references and are automatically recycled by the browser.
Add a button to the base application vue2 to switch the display/hide state of the sub-applications, and verify whether multiple rendering and uninstallation work normally.
The effect is as follows:
Once it is running normally! 🎉
Concluding remarks
At this point, the article on the micro front end rendering is over. We have completed the rendering and uninstallation functions of the micro front end. Of course, its function is very simple, just narrating the basic realization ideas of the micro front end. Next, we will complete the JS sandbox, style isolation, data communication and other functions. If you can read it patiently, it will be very helpful for you to understand the micro front end.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。