项目运行致浏览器奔溃问题总结

297 阅读3分钟

场景简要说明
公司某个核心业务系统存在高强度编辑修改的需求,对于首屏速度、编辑保存速度要求较高,年初进行了一个版本的重构,由打开多个外部标签改为项目内部实现多页签,上线后发现严重的浏览器页面崩溃问题,遂进入研究修复bug

问题原因

为了实现内部多页签功能并且最小化开发时间 使用了vueRouter的 router.getMatchedComponents 方法创建了一个新的路由push方法代码如下,通过$push方法理论上可以任意创建n个新的页面(理论上哈哈)
使用以下代码打开新的页面在构建完毕之后会占用20m的内存空间并且不可回收,因为关闭页签这边会走清理逻辑将已知产生的数据做清除但是依旧被保留在内存之中,期间我同事尝试的方法记录如下,根据推断vue-router或vue自身内部保持了这些组件和实例化之后对象的引用,导致浏览器无法回收销毁的页面

  1. 谷歌浏览器启动时添加命令,主动调用gc方法清理内存
  2. 控制台工具 Performance 检查内存秀漏电
  3. 认为是路由版本问题
const $push = (info)=>{
    const {name} = info
    const {
        resolved:{
            path:p
        },
        route:{
            meta
        }
    } = router.resolve({
        // 通过resolve对应name的路由数据
        name,
    })
    // 生成一个随机code
    const randomcode = Math.random().toString(36).substring(2)
    // 合并随机code 创建出新的name 防止冲突
    const newName = [name,randomcode].join('/')
    // 补充地址创建新的路径防止冲突
    const newPath = [path,randomcode].join('/')
    // 通过老的路径去路由表里面匹配路由,这里返回的是匹配路径 所以直接调用pop方法把最后一个匹配到的组件取出来(本身这个api是提供给服务端渲染使用的,但是前端浏览器环境也是支持渲染的)
    const component = router.getMatchedComponents(p).pop()
    // 补充到路由表
    router.addRoutes([
        {
            path:'/',
            component: () => import( /* webpackChunkName: "base" */ '@/BaseLayout'),
            children:[
                {
                    path:newPath,
                    name:newName,
                    meta:{...meta},
                    component:{
                    // 解构用来隔断引用关系
                        ...component,
                        name:newName
                    }
                }
            ]
        }
    ])
    return router.push({
        ...info,
        name:newName,
    })
}
router.$push = $push

最终解决方法

根据以上结论既然无法控制框架内部的实现那么就在外部加一层解决逻辑,为了不影响业务代码做出了如下方案,在页面离开或者切换的时候保存当前页面的数据到一个内置的对象中通过viewcode做区分,切页签也就是切数据源,关页签就是删除数据源,以此实现了多页签同时能够很好的控制内存回收,一定程度的解决数据没有正常回收导致内存溢出浏览器奔溃的问题,唯一有一点无法解决的当用户同时打开200个以上tab标签就会极大概率出现奔溃的问题数据一只堆砌并没有进行回收,目前在使用中做了限制只要用户同时保持到指定数量的页签就会拒绝打开新的页签

首先修改路由改为动态路由需要多页面的统一改为 viewCode

    {
        path:'/core/processEditor/:viewCode',
        name:'processEditor',
        component:()=> import('@/views/corePage/process'),
        meta:{
            title:'工序编辑'
        }
    },

然后对应页面接入mixin


import {
    readLink
} from '@/utils'
// 页面数据缓存
const map = {};

export default {
    beforeRouteUpdate(to, from, next) {
        next()
        // 路由切换触发页签加载
        this.multiTabLoad(to, from)
    },
    deactivated() {
        // 路由页面被卸载缓存数据
        this.multiTabCache(this.viewCode)
    },
    activated() {
        // 页面被激活触发页签加载
        this.multiTabLoad()
    },
    methods: {
        multiTabLoad(to, from) {
            // 这里处理同页面切换过来的逻辑 只要是支持多页签的页面就先缓存数据
            const fromViewCode = readLink(from, 'params.viewCode')
            if (fromViewCode) {
                this.multiTabCache(fromViewCode)
            }
            //  这里读取最新的路由数据 如果在缓存里面就从缓存里面取数据遍历赋值给当前页面
            const viewCode = readLink(this.$route, 'params.viewCode')
            if (map[viewCode]) {
                const cache = map[viewCode]
                Object.keys(cache).forEach(k => {
                    this[k] = cache[k]
                })
            } else {
            // 如果没有被缓存就认为他是最新的进入初始化逻辑 
                this.__isDEL = false
                this.viewCode = viewCode
                this.init()
            }
        },
        multiTabCache(code) {
            // 如果这个页面数据被标记删除就不走缓存的逻辑
            if (this.__isDEL) return
            // 通过viewcode直接拷贝当前页面的所以数据
            map[code] = {
                ...this.$data
            }
        },
        multiCloseCache(viewCode) {
        // 进入清理逻辑 先打标识数据要被删除不要再走缓存的逻辑
            this.__isDEL = true
            // 提取缓存对象中的数据
            const data = map[viewCode]
            // 这里先判断一下意外情况
            if (!data) return
            // 首先删除缓存中的记录
            delete map[viewCode]
            // 然后遍历对象的key逐一删除加快回收的时机
            Object.keys(data).forEach(k => {
                delete data[k]
            })
        }
    }
}