从前有匹马叫代码
心若没有栖息的地方,到哪里都是流浪

本文参考microApp的框架的设计,记录一下如何通过web components实现一个微前端框架
主要内容如下:

  1. 实现微应用的思路
  2. 微应用的渲染过程
  3. 微应用的卸载

本文参考:

实现微应用的思路

刚开始接触接触到微前端的时候,行业内已经出现了乾坤,看了看乾坤,文档并不是很友好,然后想一想是否有其他的方法可以实现,想到微应用肯定是两个应用,当时iframe便出现在脑海中,但是由于iframe天然的局限性,并不适合复杂应用。于是继续找方法,方法总比困难多。百度出来了,microApp,简单使用之后,便觉得 microApp
一定是微前端行业的主流,于是便开始了学习之路,

什么是web components?

学习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缓存
        }
    }
posted on 2022-07-08 14:39  从前有匹马叫代码  阅读(70)  评论(0编辑  收藏  举报