乐趣区

关于webpack:代理模式在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)实现成果


从 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 左右。

未分离前加载工夫

宰割后的加载工夫:

屡次比照可得出,在生产环境下,宰割后的入口文件加载工夫比宰割前的少 1~1.5s。

3. 拓展

通过代理模式实现限度申请的并发数量

退出移动版