学着写一个简版的微前端框架(2)

学着写一个简版的微前端框架(1)中已经实现了

  • 开发一个微前端框架,具备监听路由变化的功能
  • 之后根据路由加载相应子应用的资源,渲染该资源,应用该资源
  • 路由切换后清除上一个子应用资源

并且留下了后期计划

  • 子应用的生命周期函数
  • window隔离
  • 元素作用域隔离
  • 样式隔离
  • 添加子应用之间通信

本文就来实现后期计划部分。

实现子应用的生命周期函数

子应用的生命周期函数实际就是在注册子应用时注册的方法。可以直接在第一版的基础上,在注册方法registerApp参数里面添加方法,添加生命周期方法bootstrapmountunmount

js 复制代码
microFramework.registerApp('app1', {
  activeRule: '/vue',
  pageEntry: 'http://localhost:8001',
  mountPoint: 'app1',
  bootstrap(){
    console.log('app1挂载前')
  },
  mount() {
    console.log('app1已挂载')         
  },
  unmount() {
    console.log('app1已卸载')         
  },
});

microFramework.registerApp('app2', {
  activeRule: '/react',
  pageEntry: 'http://localhost:8002',
  mountPoint: 'app2',
  bootstrap(){
    console.log('app2挂载前')
  },
  mount() {
    console.log('app2已挂载')         
  },
  unmount() {
    console.log('app2已卸载')         
  },
});

同时在子应用对应阶段执行。

实现子应用的window隔离

这里使用Proxy创建了一个虚拟的window代理对象proxyWindow,然后在执行子应用的代码时,将window对象替换为proxyWindow

js 复制代码
export function executeScripts(scripts, currentApp) {
    const proxyWindow = new ProxyWindowSandBox(currentApp).proxyWindow

    try {
        scripts.forEach(code => {
            // 将子应用的 js 代码全局 window 环境指向代理环境 proxyWindow
            const warpCode = `
                ;(function(proxyWindow){
                    with (proxyWindow) {
                        (function(window){${code}\n}).call(proxyWindow, proxyWindow)
                    }
                })(this);
            `

            new Function(warpCode).call(proxyWindow)
        })
    } catch (error) {
        throw error
    }
}

此处原理就是利用call改变对象this的指向。

window隔离除了改变原有子应用的this指向,还需要为代理对象proxyWindow添加常用的事件方法,这是因为window本身就是具有这些方法的。所谓的代理对象proxyWindow实际就是window的一个深拷贝。而这里不做直接深拷贝的操作是因为还需要添加一些其他逻辑在隔离类里面。

创建一个隔离类或者叫沙箱类。用于创建代理对象proxyWindow,以及添加一些其它逻辑。

js 复制代码
/**
 * js 沙箱,用于隔离子应用 window 作用域
 */
export default class ProxyWindowSandBox {

    constructor(currentApp) {
      
    }
}

添加劫持方法,给代理对象添加事件方法。在此之前先保存window本身的原生事件

js 复制代码
export const originalPushState = window.history.pushState
export const originalReplaceState = window.history.replaceState
export const originalDocument = document
export const originalWindow = window

export const originalWindowAddEventListener = window.addEventListener
export const originalWindowRemoveEventListener = window.removeEventListener
export const originalDocumentAddEventListener = document.addEventListener
export const originalDocumentRemoveEventListener = document.removeEventListener
export const originalEval = window.eval
export const originalDefineProperty = Object.defineProperty

export const originalAppendChild = Element.prototype.appendChild
export const originalInsertBefore = Element.prototype.insertBefore
export const originalCreateElement = Document.prototype.createElement
export const originalQuerySelector = Document.prototype.querySelector
export const originalQuerySelectorAll = Document.prototype.querySelectorAll
export const originalGetElementById = Document.prototype.getElementById
export const originalGetElementsByClassName = Document.prototype.getElementsByClassName
export const originalGetElementsByTagName = Document.prototype.getElementsByTagName
export const originalGetElementsByName = Document.prototype.getElementsByName

具体的劫持方法

js 复制代码
   /* eslint-disable class-methods-use-this */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getEventTypes, isFunction } from './util'
import {
    originalWindowAddEventListener,
    originalWindowRemoveEventListener,
    originalDocument,
    originalEval,
    originalWindow,
    originalDefineProperty,
} from './originalEnv'

/**
 * js 沙箱,用于隔离子应用 window 作用域
 */
export default class ProxyWindowSandBox {

    constructor(currentApp) {

        // 子应用 window 的代理对象
        this.proxyWindow = {}
        // 子应用 window 对象
        this.microAppWindow = {}

        // 子应用向 window 注入的 key
        this.injectKeySet = new Set()
        // 子应用 setTimeout 集合,退出子应用时清除
        this.timeoutSet = new Set()
        // 子应用 setInterval 集合,退出子应用时清除
        this.intervalSet = new Set()

        // 子应用绑定到 window 上的事件,退出子应用时清除
        this.windowEventMap = new Map()
        // 子应用 window onxxx 事件集合,退出子应用时清除
        this.onWindowEventMap = new Map()

        this.appName = currentApp
        this.hijackProperties()
    }

    /**
     * 劫持 window 属性
     */
    hijackProperties() {
        const {
            microAppWindow,
            intervalSet,
            timeoutSet,
            windowEventMap,
            onWindowEventMap,
        } = this

        microAppWindow.setInterval = function setInterval(callback, timeout) {
            const timer = originalWindow.setInterval(callback, timeout)
            this.intervalSet.add(timer)
            return timer
        }

        microAppWindow.clearInterval = function clearInterval(timer) {
            if (timer === undefined) return
            originalWindow.clearInterval(timer)
            intervalSet.delete(timer)
        }

        microAppWindow.setTimeout = function setTimeout(callback, timeout) {
            const timer = originalWindow.setTimeout(callback, timeout)
            timeoutSet.add(timer)
            return timer
        }

        microAppWindow.clearTimeout = function clearTimeout(timer) {
            if (timer === undefined) return
            originalWindow.clearTimeout(timer)
            timeoutSet.delete(timer)
        }

        microAppWindow.addEventListener = function addEventListener(
            type,
            listener,
            options,
        ) {
            if (!windowEventMap.get(type)) {
                windowEventMap.set(type, [])
            }

            windowEventMap.get(type)?.push({ listener, options })
            return originalWindowAddEventListener.call(originalWindow, type, listener, options)
        }

        microAppWindow.removeEventListener = function removeEventListener(
            type,
            listener,
            options,
        ) {
            const arr = windowEventMap.get(type) || []
            for (let i = 0, len = arr.length; i < len; i++) {
                if (arr[i].listener === listener) {
                    arr.splice(i, 1)
                    break
                }
            }

            return originalWindowRemoveEventListener.call(originalWindow, type, listener, options)
        }

        microAppWindow.eval = originalEval
        microAppWindow.document = originalDocument
        microAppWindow.originalWindow = originalWindow
        microAppWindow.window = microAppWindow
        microAppWindow.parent = microAppWindow

        // 劫持 window.onxxx 事件
        getEventTypes().forEach(eventType => {
            originalDefineProperty(microAppWindow, `on${eventType}`, {
                configurable: true,
                enumerable: true,
                get() {
                    return onWindowEventMap.get(eventType)
                },
                set(val) {
                    onWindowEventMap.set(eventType, val)
                    originalWindowAddEventListener.call(originalWindow, eventType, val)
                },
            })
        })
    }
}

// 构造函数、类、或使用 call() bind() apply() 绑定了作用域的函数都需要绑定到原始 window 上
// call() bind() apply() 绑定的函数,函数的name输出:"bound myFunction"
export function needToBindOriginalWindow(fn) {
    if (
        fn.toString().startsWith('class')
        || isBoundFunction(fn)
        || (/^[A-Z][\w_]+$/.test(fn.name) && fn.prototype?.constructor === fn)
    ) {
        return false
    }

    return true
}

export function isBoundFunction(fn) {
    return fn?.name?.startsWith('bound ')
}

为沙箱类添加创建代理对象方法

js 复制代码
 /**
     * 创建 window 代理对象
     */
    createProxyWindow(appName) {
        const descriptorMap = new Map ()
        return new Proxy(this.microAppWindow, {
            get(target, key) {
                if (Reflect.has(target, key)) {
                    return Reflect.get(target, key)
                }
                const result =  originalWindow[key]
                // window 原生方法的 this 指向必须绑在 window 上运行,否则会报错 "TypeError: Illegal invocation"
                // e.g: const obj = {}; obj.alert = alert;  obj.alert();
                return (isFunction(result) && needToBindOriginalWindow(result)) ? result.bind(window) : result
            },

            set: (target, key, value) => {

                this.injectKeySet.add(key)
                return Reflect.set(target, key, value)
            },

            has(target, key) {
                return key in target || key in originalWindow
            },

            // Object.keys(window)
            // Object.getOwnPropertyNames(window)
            // Object.getOwnPropertySymbols(window)
            // Reflect.ownKeys(window)
            ownKeys(target) {
                const result = Reflect.ownKeys(target).concat(Reflect.ownKeys(originalWindow))
                return Array.from(new Set(result))
            },

            deleteProperty: (target, key) => {
                this.injectKeySet.delete(key)
                return Reflect.deleteProperty(target, key)
            },

            // Object.getOwnPropertyDescriptor(window, key)
            // Reflect.getOwnPropertyDescriptor(window, key)
            getOwnPropertyDescriptor(target, key) {
                // 为什么不使用 Reflect.getOwnPropertyDescriptor() 
                // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor
                if (Reflect.has(target, key)) {
                    // 这里的作用是保证在获取(Object.getOwnPropertyDescriptor)和设置(Object.defineProperty)一个 key 的 descriptor 时,都操作的是同一个对象
                    // 即都操作 proxyWindow 或 originalWindow,否则会报错 
                    descriptorMap.set(key, 'target')
                    return Object.getOwnPropertyDescriptor(target, key)
                }

                if (Reflect.has(originalWindow, key)) {
                    descriptorMap.set(key, 'originalWindow')
                    return Object.getOwnPropertyDescriptor(originalWindow, key)
                }
            },

            // Object.defineProperty(window, key, Descriptor)
            defineProperty: (target, key, value) => {

                if (descriptorMap.get(key) === 'target') {
                    return Reflect.defineProperty(target, key, value)
                }

                return Reflect.defineProperty(originalWindow, key, value)
            },

            // 返回真正的 window 原型
            getPrototypeOf() {
                return Reflect.getPrototypeOf(originalWindow)
            },
        })
    }

上面使用Proxy创建代理对象proxyWindow。主要是里面的get和set方法,最终获取的是this.microAppWindow上的属性,以及在this.microAppWindow上进行设置。可以理解所谓隔离就是找一个对象临时替换原有的window。

有了上面的事件添加和代理创建就可以替换原有代码的window了,也就是开头的代码中的proxyWindow

js 复制代码
   const proxyWindow = new ProxyWindowSandBox(currentApp).proxyWindow

    try {
        scripts.forEach(code => {
            // 将子应用的 js 代码全局 window 环境指向代理环境 proxyWindow
            const warpCode = `
                ;(function(proxyWindow){
                    with (proxyWindow) {
                        (function(window){${code}\n}).call(proxyWindow, proxyWindow)
                    }
                })(this);
            `

            new Function(warpCode).call(proxyWindow)
        })
    } catch (error) {
        throw error
    }

需要强调一点的是,第一版微前端框架加载带有属性srcscript使用的方法是创建一个新的script标签在注入到主应用body中的办法,但这个方法有个问题,就是浏览器加载完代码后会自动执行,这样就没法替换代码中window了。

所以需要改为先异步获取script代码,在将其window替换的方式。

同时为了保证代码的顺序执行,需要按照顺序获取到子应用中所有的带有src标签中的script代码,也就是顺序异步获取。

这里没有考虑script带有defer或者async属性情况。

写到这里我想要是子应用有很多远程获取js代码的script标签是不是会影响体验呢?我想是一定的。所以我认为或许这种微前端并不是最好的方案,当然这是后话。

实现子应用的元素作用域隔离

所谓子应用的元素作用域隔离就是比如querySelectorgetElementByIdcreateElement等的隔离,这是因为这些方法都挂载在Document上,而且Document可以直接使用的,而Document方法是可以在全局搜索的,所以需要隔离。

隔离的办法就是给这些方法添加一个挂载点,只能搜索挂载点下属的dom节点。采用重写这些方法实现元素作用域隔离,当使用查询方法方法时,其搜索范围为挂载下的dom节点。

同样需要注意对于insertBeforeappendChild如果添加的是js则需要修改js中的window,如果添加的是css则需要对css隔离,这就是下一节的内容。

具体元素元素作用域隔离方法

js 复制代码
import { isUniqueElement } from './dom'
import { executeScripts, fetchScriptAndExecute, fetchStyleAndReplaceStyleContent } from './appendSonAppResource'
import { 
    originalAppendChild,
    originalCreateElement,
    originalDocument,
    originalGetElementById, 
    originalGetElementsByClassName, 
    originalGetElementsByName, 
    originalGetElementsByTagName, 
    originalInsertBefore, 
    originalQuerySelector,
    originalQuerySelectorAll, 
} from './originalEnv'

export function patchDocument(appName) {
    const container = document.getElementById(appName)
    Element.prototype.appendChild = function appendChild(node) {
        return patchAddChild(this, node, null, 'append')
    }
    
    Element.prototype.insertBefore = function insertBefore(newNode, referenceNode) {
        return patchAddChild(this, newNode, referenceNode, 'insert')
    }

    Document.prototype.createElement = function createElement(
        tagName,
        options,
    ) {
        const element = originalCreateElement.call(this, tagName, options)
        appName && element.setAttribute('single-spa-name', appName)
        return element
    }

    // 将所有查询 dom 的范围限制在子应用挂载的 dom 容器上
    Document.prototype.querySelector = function querySelector(selector) {
        if (!selector || isUniqueElement(selector)) {
            return originalQuerySelector.call(this, selector)
        }
        if(container){
            return container.querySelector(selector)
        }
        return originalQuerySelector.call(this, selector)
    }

    Document.prototype.querySelectorAll = function querySelectorAll(selector) {
        if (!selector || isUniqueElement(selector)) {
            return originalQuerySelectorAll.call(this, selector)
        }

        if(container){
            return container.querySelectorAll(selector)
        }
        return originalQuerySelectorAll.call(this, selector)
    }

    Document.prototype.getElementById = function getElementById(id) {
        return getElementHelper(this, originalGetElementById, 'querySelector', id, `#${id}`, container)
    }

    Document.prototype.getElementsByClassName = function getElementsByClassName(className) {
        return getElementHelper(this, originalGetElementsByClassName, 'getElementsByClassName', className, className, container)
    }

    Document.prototype.getElementsByName = function getElementsByName(elementName) {
        return getElementHelper(this, originalGetElementsByName, 'querySelectorAll', elementName, `[name=${elementName}]`, container)
    }

    Document.prototype.getElementsByTagName = function getElementsByTagName(tagName) {
        return getElementHelper(this, originalGetElementsByTagName, 'getElementsByTagName', tagName, tagName, container)
    }
}

function getElementHelper(
    parent, 
    originFunc, 
    funcName,
    originSelector, 
    newSelector,
    container
) {
    if (!originSelector) {
        return originFunc.call(parent, originSelector)
    }
    if(container){
        return container[funcName](newSelector)
    }
    return document[funcName](newSelector)
}


const head = originalDocument.head
const tags = ['STYLE', 'LINK', 'SCRIPT']
function patchAddChild(parent, child, referenceNode, type) {
    const tagName = child.tagName
    if (!tags.includes(tagName)) {
        return addChild(parent, child, referenceNode, type)
    }
    
    const appName = child.getAttribute('single-spa-name')
    if (!appName) return addChild(parent, child, referenceNode, type)

    // 所有的 style 都放到 head 下
    if (tagName === 'STYLE') {
        return addChild(head, child, referenceNode, type)
    }

    if (tagName === 'SCRIPT') {
        const src = child.src
        if (
            src
        ) {
            fetchScriptAndExecute(src, appName)
            return null
        }

        executeScripts([child.textContent], appName)
        return null
    }

    if ( 
        child.rel === 'stylesheet' 
        && child.href
    ) {
        const href = child.href

        const style = document.createElement('style')
        style.setAttribute('type', 'text/css')

        fetchStyleAndReplaceStyleContent(style, href, appName)

        return addChild(head, style, referenceNode, type)
    }

    return addChild(parent, child, referenceNode, type)
}

function addChild(parent, child, referenceNode, type) {
    if (type === 'append') {
        return originalAppendChild.call(parent, child)
    }

    return originalInsertBefore.call(parent, child, referenceNode)
}

实现子应用的样式隔离

子应用样式隔离分为两块内容,一块是子应用自带的style标签和link标签的,一块是子应用js代码产生的样式。

两块样式都需要隔离,隔离的办法是重写样式,使用cssRule.cssText获取到样式,之后改写。

具体改写的方法,addCSSScope为添加样式隔离方法

js 复制代码
import { nextTick } from './util'

/**
 * 给每一条 css 选择符添加对应的子应用作用域
 * 1. a {} -> a[single-spa-name=${appName}] {}
 * 2. a b c {} -> a[single-spa-name=${appName}] b c {}
 * 3. a, b {} -> a[single-spa-name=${appName}], b[single-spa-name=${appName}] {}
 * 4. body {} -> #${子应用挂载容器的 id}[single-spa-name=${appName}] {}
 * 5. @media @supports 特殊处理,其他规则直接返回 cssText
 */
export default function addCSSScope(style, appName) {
    // 等 style 标签挂载到页面上,给子应用的 style 内容添加作用域
    nextTick(() => {
        console.log(style, 'style')

        // 禁止 style 生效
        style.disabled = true
        if (style.sheet?.cssRules) {
            style.textContent = handleCSSRules(style.sheet.cssRules, appName)
        }
        
        // 使 style 生效
        style.disabled = false
    })
}

function handleCSSRules(cssRules, appName) {
    let result = ''
    Array.from(cssRules).forEach(cssRule => {
        result += handleCSSRuleHelper(cssRule, appName)
    })

    return result
}

function handleCSSRuleHelper(cssRule, appName) {
    let result = ''
    const cssText = cssRule.cssText
    const selectorText = cssRule.selectorText
    if (selectorText) {
        result += modifyCSSText(cssRule, appName)
    } else if (cssText.startsWith('@media')) {
        result += `
            @media ${(cssRule).conditionText} { 
                ${handleCSSRules((cssRule).cssRules, appName)} 
            }
        `
    } else if (cssText.startsWith('@supports')) {
        result += `
            @supports ${(cssRule).conditionText} { 
                ${handleCSSRules((cssRule).cssRules, appName)} 
            }
        `
    } else {
        result += cssText
    }

    return result
}

/**
 * 用新的 css 选择符替换原有的选择符
 */
function modifyCSSText(cssRule, appName) {
    const selectorText = (cssRule).selectorText
    return cssRule.cssText.replace(
        selectorText, 
        getNewSelectorText(selectorText, appName),
    )
}

let count = 0
const re = /^(\s|,)?(body|html)\b/g
function getNewSelectorText(selectorText, appName) {
    const arr = selectorText.split(',').map(text => {
        const items = text.trim().split(' ')
        items[0] = `${items[0]}[single-spa-name=${appName}]`
        return items.join(' ')
    })

    // 如果子应用挂载的容器没有 id,则随机生成一个 id
    let id = 0
    if (!id) {
        id = 'single-spa-id-' + count++
    }

    // 将 body html 标签替换为子应用挂载容器的 id
    return arr.join(',').replace(re, `#${id}`)
}

改写子应用自带的style标签内样式

js 复制代码
export function appendStyle(docTag, currentApp) {
    const fragmentForStyle = document.createDocumentFragment();
    const styles = Array.from(docTag.getElementsByTagName('style'));
    for (let i = 0, style; style = styles[i++];) {
        let newStyle = document.createElement('style');
        newStyle.textContent = style.textContent;
        newStyle.dataset.app = currentApp;
        addCSSScope(newStyle, currentApp)
        fragmentForStyle.appendChild(newStyle);
    }
    document.head.appendChild(fragmentForStyle)
}

改写自带的link标签内样式,为了成功改写link标签的样式,也需要异步获取后再修改,先是异步获取样式

js 复制代码
export function appendLink(docTag, currentApp) {
    const links = Array.from(docTag.getElementsByTagName('link'));
    let promiseArr = links.filter((link) => {
        return link.rel === 'stylesheet'
    }).map((link) => {
        return loadScriptAndStyle(link.href)
    })
    promiseArr.length > 0 && Promise.all(promiseArr)
        .then(data => {
            executeStyle(data, currentApp, docTag)
        })
}

在执行方法里面隔离css

js 复制代码
export function executeStyle(styles, currentApp, docTag) {
    const fragmentForScript = document.createDocumentFragment();
    styles.forEach(item => {
       // 隔离样式
        addCSSScope(item, currentApp)
        fragmentForScript.appendChild(item)
    })
    docTag.appendChild(fragmentForLInk);
}

对于子应用js生成的style样式,通过覆写拦截document.head.appendchildDocument.prototype.appendchild方法实现

js 复制代码
...
const head = originalDocument.head
const tags = ['STYLE', 'LINK', 'SCRIPT']
function patchAddChild(parent, child, referenceNode, type) {
    const tagName = child.tagName
    if (!tags.includes(tagName)) {
        return addChild(parent, child, referenceNode, type)
    }
    
    const appName = child.getAttribute('single-spa-name')
    if (!appName) return addChild(parent, child, referenceNode, type)

    // 所有的 style 都放到 head 下
    if (tagName === 'STYLE') {
        // 隔离样式
        addCSSScope(child, appName)
        child.dataset.app = appName;
        return addChild(head, child, referenceNode, type)
    }

    if (tagName === 'SCRIPT') {
        const src = child.src
        if (
            src
        ) {
            fetchScriptAndExecute(src, appName)
            return null
        }

        executeScripts([child.textContent], appName)
        return null
    }

    if ( 
        child.rel === 'stylesheet' 
        && child.href
    ) {
        const href = child.href

        const style = document.createElement('style')
        style.setAttribute('type', 'text/css')

        fetchStyleAndReplaceStyleContent(style, href, appName)

        return addChild(head, style, referenceNode, type)
    }

    return addChild(parent, child, referenceNode, type)
}

function addChild(parent, child, referenceNode, type) {
    if (type === 'append') {
        return originalAppendChild.call(parent, child)
    }

    return originalInsertBefore.call(parent, child, referenceNode)
}

实现子应用之间通信

子应用之间的通信实际是一个发布订阅模式。主应用订阅子应用的事件,之后子应用触发。发布订阅模式都挂载在window上。

为了避免发布订阅被window代理对象proxyWindow覆盖,订阅需要发生在生成代理对象之前。这样子应用即便发布事件,主应用也会接收到。

发布订阅模式

js 复制代码
import { isFunction } from './util'

export default class EventBus {
    constructor(){
      this.events = {}
    }
    on(event, callback) {
        if (!isFunction(callback)) {
            throw Error(`The second param ${typeof callback} is not a function`)
        }
        if(!this.events[event]){
            this.events[event] = []
        }
        this.events[event].push(callback)
    }

    off(event, callback) {

        if (!this.events[event]) return

        if (callback) {
            const cbs = this.events[event]
            let l = cbs.length
            while (l--) {
                if (callback == cbs[l]) {
                    cbs.splice(l, 1)
                }
            }
        } else {
            this.events[event] = []
        }
    }

    emit(event, ...args) {
        this.events[event].forEach((callback) => {
            /**
             * 如果是点击其他子应用或父应用触发全局数据变更,则当前打开的子应用获取到的 app 为 null
             * 所以需要改成用 activeRule 来判断当前子应用是否运行
             */
            callback.call(this, ...args)
        })
    }

    // once(event, callback) {
    //     // eslint-disable-next-line @typescript-eslint/no-this-alias
    //     const self = this

    //     function wrap(...args) {
    //         callback.call(self, ...args)
    //         self.off(event, wrap)
    //     }

    //     this.on(event, wrap)
    // }

    clearEventsByAppName() {
        this.events = {}
    }
}

在微前端创建时,就在window上生成发布订阅对象spaGlobalState

js 复制代码
...
import EventBus from './utils/EventBus'
import {originalWindow} from './utils/originalEnv'

export default class MicroFrontendFramework {
  constructor() {
    this.apps = {};
    this.currentApp = null;
  }
  static start() {
    // 在window上挂的发布订阅事件
    originalWindow.spaGlobalState = new EventBus()

    const instance = new MicroFrontendFramework();
    overwriteApiAndSubscribeEvent(instance.switchApp.bind(instance))
    return instance;
  }
  ...
}

在主应用main.js订阅一个vue事件

js 复制代码
window.spaGlobalState.on('vue', () => alert('父应用监听到 vue 子应用发送了一个全局事件: vue'))

在vue子应用发布或者触发

js 复制代码
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <button @click="sendMainMessage">发消息给主应用</button>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  methods: {
    sendMainMessage(){
      window.spaGlobalState.emit('vue')
    }
  }
}
</script>

代码地址

github:github.com/zhensg123/r...

可优化方案以及后期计划

整体代码可进一步优化,尤其代码结构以及整体思路。

优化整体思路:可以将子应用单独抽出来作为一个类比如叫SonApp,其具备属性样式、script、生命周期函数等属性。然后根据路径匹配规则加载指定的子应用。例如

js 复制代码
export class SonApp {
   constructor(name, mountPoint, activeRule, pageEntry, styles, scripts, pageBody, sandbox, bootstrap, mount, unmount){
      this.name = name // 名称
      this.mountPoint = mountPoint // 挂载点
      this.activeRule = activeRule // 激活规则
      this.pageEntry = pageEntry // 子应用入口
      this.styles = styles // 子应用样式集
      this.scripts = scripts // 子应用scripts
      this.pageBody = pageBody // 子应用入口页面的 html 内容(body 部分)
      this.sandbox = sandbox // 子应用沙箱)
      this.bootstrap = bootstrap // 生命周期函数挂载前
      this.mount = mount // 生命周期函数挂载
      this.unmount = unmount // 生命周期函数卸载
   }
}

然后微前端框架实际上就是根据路径加载不同子应用,同时子应用自己管理自己的资源:样式和js注入和隔离,子应用自己管理资源的注入和卸载。这也是后期优化计划之一。

后期添加缓存功能,对某些过程做缓存,比如缓存代理对象不至于每次切换子应用都重新生成window代理对象。

总结

上文中的window隔离方案是微前端框架qiankun的隔离方案。实际上隔离方案也可以是iframe这种,比如微前端框架wujie就采用的iframe。

有些文章就指出,包括我的体会当项目复杂到一定程度,性能最好的方案可能是iframe,至于iframe的缺点则是使用iframe后要解决的问题。

本文的一个收获是开发某些情况下是在构建一种结构关系,只要触发条件不变,只要结构关系不变,可以有不止一种编码形式。

本文完。

参考文章

手把手教你写一个简易的微前端框架

相关推荐
ekskef_sef12 分钟前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine64137 分钟前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻1 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云1 小时前
npm淘宝镜像
前端·npm·node.js
dz88i81 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr1 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook
程序员_三木1 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
顾平安2 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网2 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工2 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染