1. Basic knowledge
(1) Agent mode
Start with a simple example of the proxy pattern. This is shown in the code
/ / note: Node-fetch =require('node-fetch') // const fetch=require('node-fetch') // function request(keyword= "){return new Promise((resolve,reject)=>{ fetch(`http://example.com/data/${keyword}`) .then(response=> { resolve(response.json()) })})} / / main program async function main () {let res try {res = await the request (' a ')} the catch (e) {}... }Copy the code
In the case of asynchronous requests, we usually write the asynchronous request logic in a separate function, such as the request function in the above code, and then the main function gets the data by calling the Request function.
However, if the cost of the interface is high, the call is frequent, and the query conditions are repeated, we can add a layer of caching proxy between main and Request. If the parameters passed in the next call are the same as before, the query results can be directly returned. The code looks like this:
Function request(keyword=''){return new Promise((resolve,reject)=>{ Fetch (' http://example.com/data/${keyword} ').then(response=> {resolve(response.json()})})} // Cache proxy const requestProxy=(function(){ let cache={} return async function(keyword){ if(cache[keyword]) return cache[keyword] else{ Cache [keyword]=await request(keyword) return cache[keyword]}}})() Get result asynchronously res=await requestProxy('a') // second time: get result by cache res=await requestProxy('a')}catch(e){}.......... }Copy the code
In addition, the proxy mode can be used in many scenarios, such as loading the chrysanthemum image before the img node loads the image. In simple terms, the chrysanthemum image is loaded first. After the chrysanthemum image is loaded, the onload callback function is used to set the img SRC back to the original image.
To summarize, from JavaScript Design Patterns and Development Practices, the proxy pattern provides a proxy or placeholder for an object while controlling access to it.
(2) ES6 Proxy
It’s more straightforward to look at the MDN Proxy, so I’ll skip the description here.
2. Practice of proxy mode in VUE project
(1) Demand analysis
One of the obvious disadvantages of SPA apps is that the home page takes a long time to load, and one of the solutions is to reduce the size of the entry file by splitting it up into a larger independent third-party library.
When maintaining the company’s saas project, I found that the company basically packaged echarts,video.js and other large third-party libraries in app.js, which could be separated by dynamic import loading. But then it’s a hassle to introduce it dynamically in every file you want to reference to echarts, etc.
In order not to modify the existing stable running module and realize the need to reduce the entry file, we implement lazy loading by proxy mode.
(2) Function realization
Using echarts as an example to implement lazy loading, first directly paste code:
//echarts.js let handler let echartFactory const echart = { init: (... Args) => {let container = {// initFlag indicates whether echart is initialized. False indicates that echart is not initialized, and true indicates that initFlag is initialized: False, // The method to be executed in the main function will be stored in FNS until the initialization is complete: [], // The instance to be initialized, Echart :undefined} echartFactory.create(args). Then (echart => {container 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 (! InitFlag) {// When echart in container is not loaded, the name and parameters of the program to be executed are collected and stored in the FNS as objects. Return (... args) => { target.fns.push({ fn: Key, args})}} else {// When echarts is initialized, If (key in target.echart) {return target.echart[key]} else {return undefined}}}} echartFactory = {// _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 echartCopy the code
The echart exported from the module is an object with only an init method. When the init method is executed, an object named Container is created one at a time, and the Echart variable in the Container is initialized using the Create method in the echartFactory. This happens in a Promise, in the microtask queue. The init method finally returns a Proxy instance with container as the proxied object and handler as the handler, while echart in the Container has not been initialized.
The create method of the echartFactory object checks for a private variable of its own, _echart, and starts loading echarts third-party libraries when it is undefined. When the load is complete, init is executed through Apply and the result is returned.
Initialization is accomplished by copying the returned result into echart in the Container object in the microtask queue. After initialization, the functions and parameters in FNS are executed by Apply. After the execution, set initFlag to true to indicate that the initialization process is complete.
Then import it directly in the app.js entry file.
// app.js import echarts from './echarts.js' vue.prototype.$echarts = echartsCopy the code
(3) To achieve the effect
The split graph generated by bundleAnalyzer shows that Echarts is indeed separated from app.js.
// When using echarts, there is no need to change the logic of the original file... <script> export default { ... methods:{ initEcharts(){ this.echarts = this.$echarts.init(document.getElementById('echarts')) this.echarts.clear() let option={.... } this.echarts.setOption(option) } } ... } </script>Copy the code
When using Echarts in a vue file such as the one above, the following GIF effect can be achieved.
As you can see from the GIF, the load of the 28.js package with Echarts started after switching to the route using Echarts.
The same principle can be used to implement lazy loading of video.js, which is not explained here.
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 videoCopy the code
Through the above separation, the entry file was reduced from 9m to about 4.5m.
Loading time before separation
Loading time after segmentation:
Multiple comparisons show that in the production environment, the loading time of entry files after segmentation is 1~1.5s less than that before segmentation.
3. Develop
The proxy mode implementation limits the number of concurrent requests