本文参考microApp的框架的设计,记录一下如何通过web components实现一个微前端框架
主要内容如下:
- 实现微应用的思路
- 微应用的渲染过程
- 微应用的卸载
本文参考:
实现微应用的思路
刚开始接触接触到微前端的时候,行业内已经出现了乾坤
,看了看乾坤,文档并不是很友好,然后想一想是否有其他的方法可以实现,想到微应用肯定是两个应用,当时iframe
便出现在脑海中,但是由于iframe天然的局限性,并不适合复杂应用。于是继续找方法,方法总比困难多
。百度出来了,microApp
,简单使用之后,便觉得 microApp
一定是微前端行业的主流,于是便开始了学习之路,
什么是web components?
具体不做赘述了,接下来主要看microApp是如何实现微应用的渲染的
微应用的渲染过程
一切的起源都要从 microApp.start() 来说,代码如下:
import {defineElement} from "./element";
const SimpleMicroApp = {
start() {
defineElement()
}
}
export default SimpleMicroApp;
start函数里面调用了 定义微应用
的方法,是通过web components实现的,
import CreateApp, {appInstanceMap} from "./app";
class MyElement extends HtmlElement {
// 需要监听的属性,这两个属性变化之后会触发 attributeChangedCallback 函数
static get observedAttributes() {
return ["name", "url"]
}
// 这里就是写核心逻辑的钩子函数,
connectedCallback() {
// 创建微应用实例
const app = new CreateApp({
name: this.name,
url: this.url,
container: this
})
// 缓存起来该应用留着有用
appInstanceMap.set(this.name, app)
}
// ....省略下面的代码
}
// 挂载
export function defineElement() {
if (!window.customElements.get('micro-app')) {
customElements.define('micro-app', MyElement);
}
}
start方法里面调用defineElement
函数,defineElement
函数创建一个custom element,在custom element里面,调用CreateApp
创建微应用实例
CreateApp 是什么?
export default class CreateApp {
constructor({name, url, container}) {
this.name = name;
this.url = url;
this.container = container;
this.status = 'loading';
loadHtml(this)
}
// 组件状态,created/loading/mount/unmount
status = 'created'
source = {
links: new Map(),// link元素对应的静态资源
scripts: new Map(), // script元素对应的静态资源
}
// 资源加载完成时
onLoad(htmlDom) {
// 如果是第二次的话,则表示 css 和 script 脚本都加载完毕了
this.loadCount = this.loadCount ? this.loadCount + 1 : 1;
if (this.loadCount === 2 && this.status !== 'unmount') {
// 记录dom结构用户后续操作
this.source.html = htmlDom;
this.mount()
}
}
// 资源加载完成后进行渲染
mount() {
}
}
这里只贴出了现在需要关注的代码,CreateApp函数在 构造函数constructor
里面进行常规赋值,和loadHtml(this)
这个函数是一个工具函数,用来获取app 的 html entry的相关资源,核心代码如下:
export default function loadHtml(app) {
fetchSource(app.url)
.then(html => {
html = html
.replace(/<head[^>]*>[\s\S]*?<\/head>/i, match => {
return match.replace(/<head/i, '<micro-app-head')
.replace(/<\/head>/i, '</micro-app-head>')
})
.replace(/<body[^>]*>[\s\S]*?<\/body>/i, match => {
return match
.replace(/<body/i, '<micro-app-body')
.replace(/<\/body>/i, '</micro-app-body>')
})
// 将 htm字符串转换为Dom结构
const htmlDom = document.createElement('div')
htmlDom.innerHTML = html;
// 进一步提取和处理 js,css等静态资源
extractSourceDom(htmlDom, app)
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)
}
})
}
这里面仍然调用了 四个其他的工具函数 fetchSource
,extractSourceDom
,fetchScriptsFromHtml
,fetchLinksFromHtml
简单解释这四个函数的意思:
- fetchSource 可以理解成fetch,这也就是说子应用必须是允许跨域的
- extractSourceDom 根据获取下来的html字符串进行资源整合,然后把资源缓存到 app.source里面
- fetchScriptsFromHtml 通过 上一步的操作,在 app.source.scripts中可以获取脚本资源
- fetchLinksFromHtml 作用同上
这么几波小操作下来,constructor里面的已经整合好了资源,然后 app.onLoad函数是在最后两个函数中触发的,
// 资源加载完成时
onLoad(htmlDom) {
// 如果是第二次的话,则表示 css 和 script 脚本都加载完毕了
this.loadCount = this.loadCount ? this.loadCount + 1 : 1;
if (this.loadCount === 2 && this.status !== 'unmount') {
// 记录dom结构用户后续操作
this.source.html = htmlDom;
this.mount()
}
}
这样的话就可以确定资源都获取完成的时机了,然后通过mount函数添加到 custom element中
// 资源加载完成后进行渲染
mount() {
// 资源加载完成后进行渲染
const cloneHtml = this.source.html.cloneNode(true)
// 创建fragment作为外壳节点
const fragment = document.createDocumentFragment()
Array.from(cloneHtml.childNodes).forEach(node => {
fragment.appendChild(node)
})
// this.container 指的就是 创建的 customElement;
this.container.appendChild(fragment);
this.source.scripts.forEach(info => {
(0, eval)(info.code)
})
this.status = 'mounted'
}
卸载
因为 custom element 在销毁的时候会触发一个生命周期函数 disconnectedCallback
在这个函数里面调用 app.unmount函数进行app的卸载就可以了
disconnectedCallback() {
const app = appInstanceMap.get(this.name);
app.unmount(this.hasAttribute('destory'))
console.log('micro-app is disconnected')
}
// 卸载应用
unmount(destory) {
this.status = 'unmount';
this.container = null;
if(destory) {
appInstanceMap.delete(this.name)// 清除app缓存
}
}