微前端 前端
理解single-spa对vue主子项目的影响

功能实现

要实现一个:【用户点击浏览器返回则把页面当前tag标签清除】的功能

可以通过popState事件实现:

window.addEventListener('popstate', ()=>{
    // n = this.tags.find(i => i.path === lastRouter.path)
    this.tags.split(n,1)
}, false)

在vue-router系统里,这个n可以从watch:$router回调获得:

 watch: {
    '$route': function (newVal, oldVal) {
      lastRouter = oldVal
    }
}

用户还希望删掉的同时不保留表单,所以还需要对keep-alive的缓存删除

// 找keep-alive下的cache,删除
const $v = this.$children.filter(i => !!i.$vnode.data.routerView)
            .find(i => i.$vnode.data.key === t.path)
const $vnode = $v.$vnode
const cache = $vnode.parent.componentInstance.cache
const cacheKey = $vnode.key
delete cache[cacheKey] // 删除了keep-alive的引用,
$v.$destroy() // 删除this.$children的引用

完整commit: gitee

qiankun接入,拆分主子应用

接下来按照qiankun官网  接入子项目hrm系统,其中unmount钩子也有vue的$destroy调用

完整commit: gitee

借用博主的加载流程:乾坤的微应用注册流程 ,这里只把qiankun理解为黑盒子:


接入后的问题

此时主应用可以打开子应用了,但至少多出了两个问题:

问题1. 在打开任意路由时,上一个tag总是丢失

问题2. 在跳出子应用时,子应用的keep-alive缓存丢失了

这里需要先了解qiankun的执行加载和卸载的流程和时机

最后发现问题转变成:

1. 不能使用无法控制加卸载时机的registerMicroApps/start api

2. 需要使用手动控制加卸载的api

3. 如何在主应用通知子应用卸载



调试源码,理解流程

先来看vue的$destory做了什么,以理解为何除了keepalive的引用,还需要做$destory

Vue.prototype.$destroy = function () {
    const vm: Component = this
    const parent = vm.$parent
    if (parent) {
      remove(parent.$children, vm)
      // 关键代码,如果不执行$destroy,parent会一直保持当前组件的引用
    }
}
// vue@2.7.16 简化代码

如果注释掉:

可以看到如果一直保持引用,浏览器就无法对其做垃圾回收,多个页面如此,就会造成内存增长

这里顺便提一下, keep-alive也有destory,不过是在超出使用活度或者卸载的时候:

methods: {
    cacheVNode() {
        if (this.max && keys.length > parseInt(this.max)) { // 超出常用的长度时
            keys[0].componentInstance.$destroy() 
        }
    }
},
destroyed() { // 卸载时
    for (const key in this.cache) {
        this.cache[key].componentInstance.$destroy()
    }
}
// vue@2.7.16 简化代码



下面用简化代码来表明初始化流程

1. spa拦截popstate

// single-spa@5.9.5 简化代码
if (isInBrowser) {
    window.addEventListener("popstate", urlReroute); // 命中子应用路由触发的方法:urlReroute
    var originalAddEventListener = window.addEventListener; // 闭包记住源生引用
    window.addEventListener = function (eventName, fn) {
        if ('popstate' === eventName) {
            capturedEventListeners[eventName].push(fn); // 遇到绑定就推进数组如:vue-router注册popstate,[handleRoutingEvent]被spa送进执行数组
            return;
        }
        return originalAddEventListener.apply(this, arguments); // 其他事件正常绑定
    }
}
// 1.1 点击浏览器回退时,触发的urlReroute
function urlReroute() {
    // ...
    Promise.resolve().then(function () {
        capturedEventListeners['popstate'].forEach(function (listener) {
            listener.apply(this); // 执行数组内的函数如下面的:[handleRoutingEvent]
        });
    });
}

2. vuerouter注册popstate时,被spa送进执行数组(本文开头的window.addEventListener('popstate')也被spa送进执行数组)

// vue-router@3.6.5 简化代码
HTML5History.prototype.setupListeners = function setupListeners () {
    var handleRoutingEvent = function () {
        // ...
        this.beforeHooks.forEach(i=>i.call(this)) // 也即执行beforeEach钩子,直到钩子里遇到迭代器的next()才往下走
        resolveAsyncComponents() // 获取异步组件
        updateRoute()  // 更改当前url和router的引用
    };
    window.addEventListener('popstate', handleRoutingEvent);
};

3. 更改router后,vue的侦听出发setter,执行watch:$router

// vue@2.7.16 简化代码
function defineReactive(obj, key) {
    var dep = new Dep();
    Object.defineProperty(obj, key, {
        // ...
        set: function reactiveSetter(newVal) {
            // 更改了router的引用,触发vue的setter,通知变更到watch函数: dep.notify 触发 sub.update() => 触发watch.update()
            dep.notify({
                type: "set",
                target: obj,
                key: key,
                newValue: newVal,
                oldValue: value
            });
        }
    });
    return dep;
}
Watcher.prototype.update = function () {
    // ...
    Promise.resolve().then((watch)=>{
        watch.call(this) // 也即执行fn如:watch:{ '$route': fn }
    })
};

4. 当用户点击about页面(也即调用pushState)时,single-spa强制触发popstate提前存储的两个函数,并注入标识位singleSpa(目的是为了解决https://github.com/single-spa/single-spa/pull/291)

window.history.pushState = ()=>{
    // ...
    const evt = new PopStateEvent("popstate", { state: window.history.state })
    evt.singleSpa = true;
    window.dispatchEvent(evt);
}
// single-spa@5.9.5 简化代码

到这里可以解决问题1了:

window.addEventListener('popstate', (e)=>{
    // 解决问题1. 在打开任意路由时,上一个tag总是丢失
    if (!e.singleSpa){
        this.tags.split(n,1)
    }
}, false)
// https://gitee.com/wxwxnzm/qiankun-vue2-demo/commit/4e22b43b74cfeeb3339ca3018af635871121efc8

但是这样写还有一个诡异的问题:watch:$router发生在了this.tags.split(n,1)之后

调试过后发现,问题的根本在于single-spa拦截的popstate函数改变了程序的执行顺序

先来看下事件循环的demo:

    window.addEventListener('popstate', ()=>{
        console.log('1. ')
        Promise.resolve().then(()=>{
            console.log('3. ')
        })
    })
    window.addEventListener('popstate', ()=>{
        console.log('2. ')
    })
    // 猜猜浏览器返回时打印了什么?
    // 案例1答案是1,3,2
    const hasPromise =  ()=>{
        console.log('gussAgain 1. ')
        Promise.resolve().then(()=>{
            console.log('gussAgain 3. ')
        })
    }
    const noPromise =  ()=>{
        console.log('gussAgain 2. ')

    }
    // 猜猜浏览器打印了什么?
    // 案例2答案是1,2,3
    hasPromise();noPromise()

没错,正如这个例子一样,在single-spa拦截前,对应着案例1: 1是vue-router,3是微任务watch:$router,2是删除tag;拦截后,2发生在了3之前

这使我对事件循环有个新的认知:

在同一个事件回调里(也就是主线程),微任务是要被排到【微任务】队列里的,直到主线程清空执行栈,才会取队列的微任务的回调到主线程里执行

但是在两个事件里,虽然前事件的promise也是要排到【微任务】队列里,但却排在了后事件的任务前面

得出结论: 只在同一个浏览器事件执行中,主线程->微任务,在多个事件中,事件A的主线程+微任务都是优先于事件B的主线程+微任务

所以想保证这个功能的顺序,得想法子让后面的事件也进入队列:

window.addEventListener('popstate', (e)=>{
    if (!e.singleSpa) {
        Promise.resolve().then(()=>{ // 解决问题1
            this.tags.split(n,1)
        })
    }
}, false)


一图胜千言

问题2:子应用里用同样的方法来清空keep-alive,这里主应用简单的用dispatch派发事件来解决父子通讯问题:

// 主应用发出
if (hrmRoutes.includes(t.path)) {
        // 通知子应用删除缓存
        const delEvent = new CustomEvent('delete-sub-keep-alive-from-main')
        delEvent.data = { path: t.path }
        window.dispatchEvent(delEvent)
}

// 子应用接收
let removeKeepAlive = ({ data: { path } }) => {
      // ...    效仿主应用
      delete cache[cacheKey]
      $v.$destroy()
}
window.addEventListener('delete-sub-keep-alive-from-main', removeKeepAlive)
this.$on('hook:destroy', () => {
      window.removeEventListener('delete-sub-keep-alive-from-main', removeKeepAlive)
})

完整commit: gitee

4. 主线程空闲后,r:执行微任务beforeEach

5. 检测出是子应用,qiankun去拉取html,js,css,并挂载到dom

删除子应用tag时:

1. vue执行微任务watch:router

2. dispatch:event-curPath到子应用

3. 子应用执行监听event,通过keep-alive找到cache,删除key,执行$destory方法

总结

完整demo地址: https://gitee.com/wxwxnzm/qiankun-vue2-demo

通过一个微前端实现问题来学习single-spa,qiankun,vue2(keep-alive,destroy),vue-route3 等部分源码

日期:2024-01-06 19:20 | 阅读:118 | 评论:0