2021 / 08 / 19

用ts开发一个vue的组件-阿里云oss图片组件

本文字数: 7966阅读时间: 19分钟

设计

因为oss支持拓展参数用来转码,所以这个组件围绕着阿里云oss的静态资源转码参数和浏览器对wbep的兼容性来做,主要的流程如下:

加载流程图[ 加载流程图 ]

开工

在上次的项目中,我们设定好所有的组件放在/src/components的文件夹里,所以直接在目录下新建v-oss-img文件夹,新建好index.vueindex.ts供组件的导入。

判断是否来源于阿里云OSS

首先,我们最先需要判断图片来源是不是阿里云的oss,很简单,一句正则就可以搞定: /aliyun/.test(this.src)

判断图片是不是webp格式

同样也是一句正则: /^(\s|\S)+(webp|WEBP)+$/.test(this.src)

判断浏览器是否支持webp

这里是通过canvas来做判断的(目前找到最小的最快的检测代码quq):

const testWebP = (): boolean => {  
  const canvas: HTMLCanvasElement | undefined = typeof document === 'object' ?  
  document.createElement('canvas') : undefined;  
  if (canvas) {  
  canvas.width = canvas.height = 1;  
  return canvas.toDataURL ? canvas.toDataURL('image/webp').indexOf('image/webp') === 5 : false;  
 } else {  
  return false  
  }  
}
// 注入Vue的原型链
vue.prototype.$supportWebp = testWebP()

配置图片的最终加载路径

因为同样规格的图片在加载移动端,pc端或者不同的界面和场景的时候,需要的规格可能跟实际的有出入,例如一张4K的图片放在用户的头像的位置,但是实际加载的时候,几MB的流量为了加载用户的头像明显是不划算且不明智的,所以给图片转webp的时候同时加上了长宽的配置.

get currentSrc () {  
  let resizeText = `${this.currentWidth ? `,w_${this.currentWidth}` : ''}${this.currentHeight ? `,h_${this.currentHeight}` : ''}`  
  http://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/resize,l_100  
  if (this.isOss) {  
  if (this.$supportWebp) {  
  if (this.isWebp) {  
  return `${this.src}?x-oss-process=image/resize${resizeText}`  
  }  
  if (/x-oss-process/.exec(this.src)) { // oss已经使用了参数的情况 追加选项即可  
  return this.src + ',image/format,webp' + resizeText  
  }  
  return this.src + '?x-oss-process=image/format,webp,resize' + resizeText  
  } else {  
  return this.src + '?x-oss-process=image/interlace,1/format,jpg,resize' + resizeText  
  }  
 } else {  
  return this.src  
  }  
}

值得一提的是,为了兼容性考虑,因为有人喜欢给长宽输入100或者100px这些单位,有可能是数字,有可能是字符串,那我们需要对图片的真实长宽:

get currentWidth (): string {  
  if (this.width) {  
  if (typeof this.width === 'string') {  
  const matchesArr = this.width.match(/\d*/)  
  if (matchesArr && matchesArr[0]) {  
  return matchesArr[0]  
 } else {  
  return ''  
  }  
 } else if (typeof this.width === 'number') {  
  return String(this.width)  
 } else {  
  return ''  
  }  
 } else {  
  return ''  
  }  
}  
get currentHeight (): string {  
  if (this.height) {  
  if (typeof this.height === 'string') {  
  const matchesArr = this.height.match(/\d*/)  
  if (matchesArr && matchesArr[0]) {  
  return matchesArr[0]  
 } else {  
  return ''  
  }  
 } else if (typeof this.height === 'number') {  
  return String(this.height)  
 } else {  
  return ''  
  }  
 } else {  
  return ''  
  }  
}

根据刚刚的流程,我们写出以下的代码

/components/v-oss-img/index.vue

<template>  
 <img ref="img" :src="currentSrc" @error="loadFallback" :alt="errorText || alt" @click="$emit('click')" :width="currentWidth" :height="currentHeight">  
</template>  
<script lang="ts">  
  import {Vue, Prop, Component} from 'vue-property-decorator'  
  @Component({})  
  export default class VOssImg extends Vue {  
  @Prop({  
  type: String,  
  default: ''  
  }) src: string  
  @Prop(String) alt!: string  
  @Prop([String, Number]) width: string | number  
  @Prop([String, Number]) height: string | number  
  isOss: boolean = false  
  isWebp: boolean = false  
  errorText: string = ''  
  errorTimes: number = 0  
  $supportWebp: boolean = Vue.prototype.$supportWebp  
  created() {  
  if (this.src) {  
  this.isOss = /aliyun/.test(this.src)  
  this.isWebp = /^(\s|\S)+(webp|WEBP)+$/.test(this.src)  
 } }  loadFallback(e: EventTarget) {  
  const img: HTMLImageElement = (this.$refs as any).img  
  if (this.errorTimes >= 5) { // 5 times errorFallback  
  const isChinese = new Date().toLocaleString().match(/\p{Unified_Ideograph}/u)  
  this.errorText = isChinese ? '图片加载失败' : 'image loaded error, please check image url'  
  return  
  }  
  if (this.$supportWebp && !this.isWebp) { // fallback image  
  img.src = this.src  
  } else { // image loaded error  
  if (img.src !== `${img.baseURI}error.png`) {  
  img.src = 'error.png'  
  }  
 }  this.errorTimes ++  
 }  get currentSrc () {  
  let resizeText = `${this.currentWidth ? `,w_${this.currentWidth}` : ''}${this.currentHeight ? `,h_${this.currentHeight}` : ''}`  
  http://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/resize,l_100  
  if (this.isOss) {  
  if (this.$supportWebp) {  
  if (this.isWebp) {  
  return `${this.src}?x-oss-process=image/resize${resizeText}`  
  }  
  if (/x-oss-process/.exec(this.src)) { // oss已经使用了参数的情况 追加选项即可  
  return this.src + ',image/format,webp' + resizeText  
  }  
  return this.src + '?x-oss-process=image/format,webp,resize' + resizeText  
  } else {  
  return this.src + '?x-oss-process=image/interlace,1/format,jpg,resize' + resizeText  
  }  
 } else {  
  return this.src  
  }  
 }  get currentWidth (): string {  
  if (this.width) {  
  if (typeof this.width === 'string') {  
  const matchesArr = this.width.match(/\d*/)  
  if (matchesArr && matchesArr[0]) {  
  return matchesArr[0]  
 } else {  
  return ''  
  }  
 } else if (typeof this.width === 'number') {  
  return String(this.width)  
 } else {  
  return ''  
  }  
 } else {  
  return ''  
  }  
 }  get currentHeight (): string {  
  if (this.height) {  
  if (typeof this.height === 'string') {  
  const matchesArr = this.height.match(/\d*/)  
  if (matchesArr && matchesArr[0]) {  
  return matchesArr[0]  
 } else {  
  return ''  
  }  
 } else if (typeof this.height === 'number') {  
  return String(this.height)  
 } else {  
  return ''  
  }  
 } else {  
  return ''  
  }  
 } }</script>  
<style lang="less">  
</style>

封装组件

组件的内容写好了,那么我们怎么在实际的项目中使用这个组件呢,回到刚刚创建的index.ts,这就是我们组件的入口文件,他管理组件的注入和配置。vue的组件注入会有一个install的方法来供给Vue调用,同时也可以配置组件的options。所以我们这里要暴露一个install的方法给Vue挂载组件。

import VOssImg from './index.vue'  
import Vue from 'vue'  
const testWebP = (): boolean => {  
  const canvas: HTMLCanvasElement | undefined = typeof document === 'object' ?  
  document.createElement('canvas') : undefined;  
  if (canvas) {  
  canvas.width = canvas.height = 1;  
  return canvas.toDataURL ? canvas.toDataURL('image/webp').indexOf('image/webp') === 5 : false;  
 } else {  
  return false  
  }  
}  
export const install = (vue: typeof Vue, options: {} = {}) => {  
  vue.prototype.$supportWebp = testWebP()  
  vue.component('v-oss-img', VOssImg)  
}  
export default {  
  install  
}

然后我们在/project的实验项目中,挂载我们写好的组件.

/project/main.ts

import VueUtilsComponents from '../../src'
Vue.use(VueUtilsComponents)
//or
// import {VOssImg} from '../../src'
// Vue.use(VOssImg)

/project/src/App.vue

<v-oss-img src="https://xxxxx.oss-cn-shenzhen.aliyuncs.com/newboss/2019-03-28/00c69974-ecb2-400e-806b-4d177c844b24.jpg"></v-oss-img>

这里我本来用的是一张3.9MB的jpg,然后转化为webp后大小是360kb,足足小了10倍的大小。

原图大小

原图[ 原图 ]

原图

原图[ 原图 ]

webp 大小

原图[ 原图 ]

webp

原图[ 原图 ]

可以在Network看到两者的区别一个是卡在Content Download(原图),一个是卡在Waiting(TTFB)(webp),content download字面意思就是下载图片,而TTFB的意思是:

TTFB 是 Time to First Byte 的缩写,指的是浏览器开始收到服务器响应数据的时间(后台处理时间+重定向时间),是反映服务端响应速度的重要指标。就像你问朋友了一个问题,你的朋友思考了一会儿才给你答案,你朋友思考的时间就相当于 TTFB。你朋友思考的时间越短,就说明你朋友越聪明或者对你的问题越熟悉。对服务器来说,TTFB 时间越短,就说明服务器响应越快。

所以这里的TTFB实际上应该就是阿里云服务器处理我们的参数的时间,也就是转码成webp和压缩改变长宽的图片的时间。所以无脑转webp也不是最优方案,但是在网络条件不好的情况下或者节省流量的情况下,也是一种不错的选择

总结

虽然webp的格式能够大大压缩图片的体积和网络加载速度,但是TTFB的时间也会随着图片的分辨率和大小而加长,所以甚至存在图片加载速度可能比原来的更慢,例如加载本来就很小的icon,本来就几kb加载速度10几ms, 但是转码一下,可能TTFB的时间都需要100多ms,所以需要根据实际的业务场景来决定要不要使用webp的格式,我觉得最优的方案是图片经过媒体处理,根据大小决定是不是要直接在阿里云转码多一份webp格式的原图供前端直接加载,这样TTFB的时间和Content Download的时间都是最大的优化,但是也增加了维护成本,毕竟同一份图片存了2份,但是考虑webp的大小和加载速度和节省的流量,没准也是一条路子,如果有更好的建议和解决方案可以在评论区留言哦^ ^

更多阅读