The observer mode realizes picture preloading and opens the event listening interface

Posted by Wizard4U on Thu, 02 Dec 2021 16:57:42 +0100

reference resources

demand

Realize the picture preloading function. When each picture is loaded successfully and failed, the loadProgress and loadError functions need to be called respectively; After the image is loaded, you need to call the loadComplete function.

Optional:

  • The above three interface functions can be switched at any time and support multiple preloads.
  • The above three interface functions are expanded to "events", and any number of event listeners can be added.

Technology stack: vue3. At first, vue3 was selected because it was expected that Vue could easily insert the preloaded Image object into the DOM. I explored it for a long time, and finally announced that my expectation failed. This problem will be discussed below.

We implement a loader and expect it to work like this:

      let loader = new Loader()
      loader.addEvent(Loader.LOAD_PROGRESS, loadProgress1)
      loader.addEvent(Loader.LOAD_PROGRESS, loadProgress2)
      loader.addEvent(Loader.LOAD_COMPLETE, loadComplete)
      loader.addEvent(Loader.LOAD_ERROR, loadError)
      loader
        .load(['imgs/1.png', 'imgs/2.png'])
        .then(() => {
          // 2nd preload
          loader.setEvent(Loader.LOAD_COMPLETE, loadComplete2)
          return loader.load(['imgs/4.png', 'imgs/3.png'])
        })
        .then(() => {
          // 3rd preload
          return loader.load(['imgs/4.png', 'imgs/5.png'])
        })
        .then(() => {
          // 4th preload
          return loader.load(['imgs/1.png'])
        })

The listener function is about this long

      let loadComplete = resp => {
        Vue.nextTick(() => {
          console.log(resp.msg, `Number of pictures loaded successfully: ${resp.sucCount},Number of failures: ${resp.failCount}`)
          this.imgData = resp.data
          this.drawCanvas()
        })
      }

All listener functions have only one parameter: resp. This parameter contains all relevant data. Format of resp:

{URL: url, progress: Proportion of successfully loaded pictures to the total number of pictures}//Single picture loaded successfully
{errURL: url, msg: 'Loading failed!'}//Single picture loading failed
{
  data: [{
    img: Image Example 1, url: picture url1, succeed: Load successfully 1
  },{
    img: Image Example 2, url: picture url2, succeed: Load successfully 2
  }],
  msg: 'Loading complete!',
  sucCount: Number of pictures loaded successfully,
  failCount: Number of pictures failed to load
}//All pictures loaded

We write two JS files. img_loader.js is the implementation of Loader, and observer mode realizes picture preloading. JS is the use of Loader.

Implementation of Loader

First, implement an EventListener, which is the standard observer mode. The Loader uses an EventListener object to trigger events. My view is different from the reference link. The reference link directly implements the observer mode in the Loader, and I think it is better to separate it.

The overall framework of the load function:

imgs.forEach((url) => {
    let im = new Image()
    im.onload = () => {...}
    im.onerror = () => {...}
    im.src = url
})

Because it is an asynchronous operation, we need to encapsulate it with Promise, so the load function should return a Promise object.

At first, I wrote very ugly (the annotated code). Later, I found that I could write code with normal painting style with Promise.all.

  load(imgs) {
    let sucCount = 0
    return Promise.all(imgs.map(url => {
      return new Promise((resolve, reject) => {
        let img = new Image()
        img.onload = () => resolve({img, URL: url, progress: (++sucCount) / imgs.length})
        img.onerror = () => reject({img, errURL: url, msg: 'Loading failed!'})
        img.src = url
      }).then(res => {
        this.e.trigger(Loader.LOAD_PROGRESS, null, res)
        return {img: res.img, url, succeed: true}
      }, err => {
        this.e.trigger(Loader.LOAD_ERROR, null, err)
        return {img: err.img, url, succeed: false}
      })
    })).then(imgData => {
      this.e.trigger(Loader.LOAD_COMPLETE, null, {
        data: imgData,
        msg: 'Load complete qwq!',
        sucCount,
        failCount: imgs.length - sucCount
      })
    })
  }

After loading successfully, go to the fully qualified branch, and if it fails, go to the rejected branch, so as to ensure that each element of the Promise array is a fully qualified Promise object. Do you think it is much simpler than the code of the reference link~

Insert Image object into DOM

I've been looking for it for a long time. vue doesn't seem to have a way to bind HTMLElement and HTML. So I finally directly operate DOM

All kinds of inferior materials on the Internet (don't ask, ask is csdn none) 🐎) Only one method is shown: use canvas to transfer the picture to Base64. This method needs to overcome the cross domain problem. It's too troublesome. Forget it

Output (partial)

Picture loading failed: imgs/4.png,Message: loading failed!
Currently loaded successfully: imgs/3.png
 Current loading progress: 50%
Load complete qwq! Number of successfully loaded pictures: 1, number of failed pictures: 1

Effect: draw a preloaded picture into the canvas; click the button to insert the preloaded HTMLElement into the DOM.

code

HTML
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <title>Image preloading in observer mode</title>
  <!--<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">-->
  <!--<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">-->
  <link rel="stylesheet" type="text/css" href = "./Image preloading in observer mode.css" />
  <!--<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>-->
  <script src="https://unpkg.com/vue@3.0.5/dist/vue.global.js"></script>
  <!--<script src="https://unpkg.com/element-ui/lib/index.js"></script>-->
</head>
<body>
  <div id="app">
    <canvas id="canvas" ref="canvas" width="400" height="400"></canvas>
    <div class="right">
      <div><button @click="showImgs">Click to show the picture</button></div>
      <div class="container" ref="imgContainer"></div>
    </div>
  </div>
  <script src="./img_loader.js"></script>
  <script src="./Image preloading in observer mode.js"></script>
</body>
</html>
CSS
body{
  margin: 0;
  background-color: wheat;
}

div{
  box-sizing: border-box;
}

#app{
  display: flex;
  align-items: flex-start;
}

#canvas{
  border: 1px solid blue;
}

.container{
  border: 1px solid red;
  display: grid;
  grid-template-columns: repeat(2,1fr);
}
img_loader.js
"use strict";

class EventListener {
  constructor() {
    this.listener = {}
  }

  addEvent(type, callback) {
    if (!this.listener[type]) {
      this.listener[type] = []
    }
    this.listener[type].push(callback)
  }

  removeEvent(type, callback) {
    if (!this.listener[type]) {
      this.listener[type] = []
    }
    let idx = this.listener[type].indexOf(callback)
    if (~idx) this.listener[type].splice(idx, 1)
  }

  clearEvents(type) {
    this.listener[type] = []
  }

  trigger(type, context, ...args) {
    for (let cb of this.listener[type]) cb.apply(context, args)
  }
}

class Loader {
  static LOAD_PROGRESS = Symbol()
  static LOAD_COMPLETE = Symbol()
  static LOAD_ERROR = Symbol()

  constructor() {
    this.e = new EventListener()
  }

  addEvent(type, callback) {
    this.e.addEvent(type, callback)
  }

  setEvent(type, callback) {
    this.e.clearEvents(type)
    this.e.addEvent(type, callback)
  }

  // load(imgs) {
  //   return new Promise(resolve => {
  //     let singleLoadedHandle = (succeed, i) => {
  //       if (succeed) this.sucCount++
  //       this.loadStates[i] = succeed
  //       if ((++finished) >= imgs.length) resolve()
  //     }
  //     this.sucCount = 0
  //     let finished = 0
  //     this.loadStates = []
  //     this.imgData = imgs.map((url, i) => {
  //       let img = new Image()
  //       img.onload = () => {
  //         singleLoadedHandle(true, i)
  //         this.e.trigger(Loader.LOAD_PROGRESS, null, {
  //           URL: url, progress: this.sucCount / imgs.length
  //         })
  //       }
  //       img.onerror = () => {
  //         singleLoadedHandle(false, i)
  //         this.e.trigger(Loader.LOAD_ERROR, null, {errURL: url, msg: 'load failed!'})
  //       }
  //       img.src = url
  //       return img
  //     })
  //   }).then(() => {
  //     this.e.trigger(Loader.LOAD_COMPLETE, null, {
  //       data: this.imgData.map((img, i) => ({
  //         img, url: imgs[i], succeed: this.loadStates[i]
  //       })),
  //       msg: 'loading completed!',
  //       sucCount: this.sucCount,
  //       failCount: imgs.length - this.sucCount
  //     })
  //   })
  // }

  load(imgs) {
    let sucCount = 0
    return Promise.all(imgs.map(url => {
      return new Promise((resolve, reject) => {
        let img = new Image()
        img.onload = () => resolve({img, URL: url, progress: (++sucCount) / imgs.length})
        img.onerror = () => reject({img, errURL: url, msg: 'Loading failed!'})
        img.src = url
      }).then(res => {
        this.e.trigger(Loader.LOAD_PROGRESS, null, res)
        return {img: res.img, url, succeed: true}
      }, err => {
        this.e.trigger(Loader.LOAD_ERROR, null, err)
        return {img: err.img, url, succeed: false}
      })
    })).then(imgData => {
      this.e.trigger(Loader.LOAD_COMPLETE, null, {
        data: imgData,
        msg: 'Load complete qwq!',
        sucCount,
        failCount: imgs.length - sucCount
      })
    })
  }
}
Image preloading in observer mode. js
"use strict";

function main() {
  let app = {
    data() {
      return {
        imgData: null,
        inserted: false
      }
    },
    methods: {
      drawCanvas() {
        let canvas = this.$refs.canvas
        let ctx = canvas.getContext('2d')
        let img = this.imgData[1].img
        let x = (canvas.width - img.width) / 2, y = (canvas.height - img.height) / 2
        ctx.drawImage(img, x, y)
      },
      // Show preloaded images at the right time
      showImgs() {
        if (this.inserted) return
        this.inserted = true
        let container = this.$refs.imgContainer
        let elements = this.imgData.map(dat => {
          if (dat.succeed) {
            dat.img.title = `${dat.url}`
            return dat.img
          }
          let p = document.createElement('p')
          p.innerText = `${dat.url}Loading failed QAQ`
          return p
        })
        elements.forEach(ele => container.appendChild(ele))
      }
    },
    created() {
      let loadProgress1 = resp => {
        console.log(`Currently loaded successfully: ${resp.URL}`)
      }
      let loadProgress2 = resp => {
        // It can be modified to code related to progress bar update
        let prog = Math.round(resp.progress * 100)
        console.log(`Current loading progress: ${prog}%`)
      }
      let loadComplete = resp => {
        Vue.nextTick(() => {
          console.log(resp.msg, `Number of pictures loaded successfully: ${resp.sucCount},Number of failures: ${resp.failCount}`)
          this.imgData = resp.data
          this.drawCanvas()
        })
      }
      let loadError = resp => {
        // It can be modified to code related to user prompt
        console.log(`Picture loading failed: ${resp.errURL},Message: ${resp.msg}`)
      }
      let loadComplete2 = resp => {
        Vue.nextTick(() => {
          console.log(resp.msg, `Number of pictures loaded successfully: ${resp.sucCount},Number of failures: ${resp.failCount}`)
          this.imgData = this.imgData.concat(resp.data)
        })
      }
      let loader = new Loader()
      loader.addEvent(Loader.LOAD_PROGRESS, loadProgress1)
      loader.addEvent(Loader.LOAD_PROGRESS, loadProgress2)
      loader.addEvent(Loader.LOAD_COMPLETE, loadComplete)
      loader.addEvent(Loader.LOAD_ERROR, loadError)
      loader
        .load(['imgs/1.png', 'imgs/2.png'])
        .then(() => {
          // 2nd preload
          loader.setEvent(Loader.LOAD_COMPLETE, loadComplete2)
          return loader.load(['imgs/4.png', 'imgs/3.png'])
        })
        .then(() => {
          // 3rd preload
          return loader.load(['imgs/4.png', 'imgs/5.png'])
        })
        .then(() => {
          // 4th preload
          return loader.load(['imgs/1.png'])
        })
    }
  }
  Vue.createApp(app).mount('#app')
}

main()

Topics: Vue Design Pattern