代理模式:在vue中惰性加载echarts等第三方库

1.基础知识点

(1)代理模式

先从一个简单的例子入手代理模式。如代码所示

// 注意:如果在node环境下运行程序请安装node-fetch
// const fetch=require('node-fetch') 
// 根据keyword请求数据
function request(keyword=''){    
    return new Promise((resolve,reject)=>{
        fetch(`http://example.com/data/${keyword}`)
        .then(response=> {
            resolve(response.json())
        })
    })
}
// 主体程序
async function main(){
    let res
    try{
        res=await request('a')
    }catch(e){

    }
    ..........
}

通常遇到异步请求的场景中,我们会把异步请求逻辑写在一个独立的函数里面,如上述代码中的request函数,然后主函数main通过调用request函数获取数据。

但如果该接口的开销比较大、调用比较频繁多且查询的条件部分重复,我们可以在main和request中间加一层缓存代理,在下一次调用时如果传入的参数跟之前一致,则可直接返回之前查询的结果。代码如下所示:

// 根据keyword请求数据
function request(keyword=''){    
    return new Promise((resolve,reject)=>{
        fetch(`http://example.com/data/${keyword}`)
        .then(response=> {
            resolve(response.json())
        })
    })
}

// 缓存代理
const requestProxy=(function(){
    let cache={}
    return async function(keyword){
        if(cache[keyword])
            return cache[keyword]
        else{
            cache[keyword]=await request(keyword)
            return cache[keyword]
        }    
    }
})()

// 主体程序
async function main(){
    let res
    try{
        // 第一次:通过异步获取结果
        res=await requestProxy('a')
        // 第二次:通过缓存获取结果
        res=await requestProxy('a')    }catch(e){

    }
    ..........
}

除此以外,代理模式的运用场景也很多,例如在img节点加载好图片之前先加载菊花图,简单来说,先加载菊花图,菊花图加载完成后出发onload回调函数,把img的src设置回原本要加载的图片。

用《JavaScript设计模式与开发实践》中的一句话来总结:代理模式是为一个对象提供一个代用品或占位符,一边控制对它的访问。

(2)ES6 Proxy

MDN Proxy会更直接,这里就直接跳过说明了。

2.代理模式在vue项目中的实践

(1)需求分析

SPA应用一个显著的缺点就是首页加载时间长,然后其中之一的解决方法就是通过分割出比较大的第三方独立库减少入口文件的体积大小。

在维护公司saas项目时,我发现公司基本把echarts,video.js等较大的第三方库都打包在app.js中,按道理可以通过import动态加载把他们都分离出来。但此后在每个要引用到echarts等的文件中动态引入比较麻烦。

为了不修改已有的稳定运行的模块的同时实现减少入口文件的需求,自己通过代理模式实现惰性加载

(2)功能实现

以echarts为例实现惰性加载,先直接贴上代码:

//echarts.js
let handler
let echartFactory
const echart = {
    init: (...args) => {
        let container = {
            // initFlag标志着echart是否已初始化,false代表未初始化,true代表已初始化
            initFlag: false,
            // 记录在未初始化完之前,主函数中要执行的方法都会存放在fns里面
            fns: [],
            // 待初始化的实例,在加载后所有方法都会在在echart中实现
            echart:undefined
        }
        echartFactory.create(args)
            .then(echart => {
                container.echart = echart
                container.fns.forEach(({ fn, args }) => {
                    if(typeof(echart[fn])==='function')
                        echart[fn].apply(echart, args)
                })
                container.initFlag = true
            })
        return new Proxy(container, handler)
    }
}

handler = {
    get: function (target, key) {
        if (!target.initFlag) {
            // container中的echart未加载时,收集要执行的程序的名称和参数以对象的形式存放在fns中
            return (...args) => {
                target.fns.push({
                    fn: key,
                    args
                })
            }
        } else {
            // 当echarts初始化完成后,直接返回方法
            if (key in target.echart) {
                return target.echart[key]
            } else {
                return undefined
            }
        }
    }
}

echartFactory = {
    // 存放从外部加载的echarts模块
    _echart: undefined,
    create: async function (args) {
        if (!this._echart) {
            this._echart = await import(/* webpackPrefetch: true */ 'echarts')
        }
        return this._echart.init.apply(undefined, args)
    }
}

export default echart

从模块导出的echart是一个仅有init方法的对象。当执行init方法时,每次会创建一个名为container的对象,然后通过echartFactory中的create方法初始化container中的echart变量,此过程会在Promise中进行,即在微任务队列中进行。init方法最后返回一个以container为被代理对象、handler为处理器的Proxy实例,此时container中的echart还未被初始化。

echartFactory对象的create方法在执行中会先检查自身的一个私有变量_echart,当其为undefined则开始把echarts第三方库加载进来。当加载完成后通过apply执行init方法且返回结果。

在微任务队列中把返回的结果复制到container对象中的echart,从而完成初始化。完成初始化后,会把fns中的函数和形参通过apply依次执行。执行完后把initFlag赋值为true,代表整个初始化过程已完成。

之后在app.js入口文件中直接引入即可。

// app.js入口文件

// 引用上述echart.js
import echarts from './echarts.js'
Vue.prototype.$echarts = echarts

(3)实现效果

image.png
从bundleAnalyzer生成的分割图看出echarts的确从app.js中分离出来了。

// 当使用echarts时,不需要改变原文件的逻辑
...
<script>
export default {
    ...
    methods:{
        initEcharts(){
            this.echarts = this.$echarts.init(document.getElementById('echarts'))
            this.echarts.clear()
            let option={....}
            this.echarts.setOption(option)
        }
    }
    ...
}
</script>

当在例如以上vue文件中使用echarts时,可达到下面的gif效果。

从gif中可以看出切换到使用echarts的路由后才开始加载带有echarts的28.js包。

同样的原理也可以用来实现video.js的惰性加载,这里就直接贴代码不解释了。

import 'video.js/dist/video-js.css'
let handler
let videoFactory

const video = function (...args) {
  let container = {
    initFlag: false,
    fns: [],
    video: undefined
  }
  videoFactory.create(args)
    .then(video => {
      container.video = video
      container.fns.forEach(({ fn, args }) => {
        video[fn].apply(video, args)
      })
      container.initFlag = true
    })
  return new Proxy(container, handler)
}

videoFactory = {
  _video: undefined,
  create: async function (args) {
    if (!this._video) {
      this._video = await import(/* webpackPrefetch: true */ 'video.js')
      this._video = this._video.default
    }
    return this._video.apply(undefined, args)
  }
}

handler = {
  get: function (target, key) {
    if (!target.initFlag) {
      return (...args) => {
        target.fns.push({
          fn: key,
          args
        })
      }
    } else {
      if (key in target.video) {
        return target.video[key]
      } else {
        return undefined
      }
    }
  }
}

export default video

通过上面的分离,把入口文件从9m多缩小成4.5m左右。

未分离前加载时间
image.png
分割后的加载时间:
image.png
多次对比可得出,在生产环境下,分割后的入口文件加载时间比分割前的少1~1.5s。

3.拓展

通过代理模式实现限制请求的并发数量

阅读 109

推荐阅读