OSS 图片链接处理工具和组件

1,039 阅读6分钟

背景

目前有部分前端项目附件会保存在阿里云上管理,无论是前端直接调用ali-oss上传,还是通过调用后端接口上传,最终返回的是一个oss 图片链接,基于不同的需求场景我们需要对返回的图片链接进行相应地处理,虽然ali-oss提供了一套较为完善的开发文档,但是实际使用起来还是会有点不太方便。

同学们对以下图片进行缩放处理的操作有点眼熟吗?

p3-juejin.byteimg.com/tos-cn-i-k3…

缩放图

上述图片的处理是直接在原图片链接添加不同的参数来完成,x-oss-process=image 指定对图片类型的处理,resize指定当前为缩放操作,w_200 则缩放图片宽度为200px。

使用过oss 图片链接的同学会发现,这个图片处理方式只是冰山一角,例如还有对图片质量的控制,模糊度处理等,下面列出几种处理图片的方式

竟然我们已经早到了规律之后,我们有没有更好的方式根据指定的配置方式对链接进行处理呢?其实我们可以使用对象的方式描述我们的处理方式,可以链式调用以多种不同方式处理链接。

代码实现

以下代码使用typescript实现,有兴趣查看完整代码的可以移步到完整源码

声明接口和类

根据ali-oss官网文档的API说明,我们先定义好各种处理方式的参数接口,以下以定义缩放的接口为例,其他的方式以此类推。

export type MethodsType =
  | 'resize'
  | 'blur'
  | 'circle'
  | 'crop'
  | 'indexcrop'
  | 'rotate'
  | 'bright'
  | 'contrast'
  | 'sharpen'
  | 'format'
  | 'watermark'
  | 'interlace'
  | 'quality'
  | 'roundedCorners'
  | 'autoOrient'

type OssResizeMode = 'lfit' | 'mfit' | 'fill' | 'pad' | 'fixed'

export interface OssResize {
  /**
   * 指定缩放的模式
   */
  m?: OssResizeMode
  /**
   * 指定目标缩放图的宽度
   * - [1,4096]
   */
  w?: number
  /**
   * 指定目标缩放图的高度。
   * - [1,4096]
   */
  h?: number
  /**
   * 指定目标缩放图的最长边。
   * - [1,4096]
   */
  l?: number
  /**
   * 指定目标缩放图的最长边。
   * - [1,4096]
   */
  s?: number
  /**
   * 指定当目标缩放图大于原图时是否进行缩放。
   * - 1(默认值):表示不按指定参数进行缩放,直接返回原图。
   * - 0:按指定参数进行缩放。
   */
  limit?: 0 | 1
  /**
   * 当缩放模式选择为pad(缩放填充)时,可以设置填充的颜色
   * - RGB颜色值,例如:000000表示黑色,FFFFFF表示白色。
   * - 默认值:FFFFFF(白色)
   */
  color?: string
}

另外对处理函数的接口我们也需要声明一下。

export interface ApiHandler<T> {
  (params: T): OssImage
}

接下来我们看下处理图片类的定义

class OssImage {
  private methods = [
    'resize',
    ...
  ]
  ...
  resize!: ApiHandler<OssResize>
  ...
}

划重点了,我们要遍历methods数组的字符串来定义不同的图片处理方法

...
/**
 * 处理的方法参数对象
 */
private methodKeys: Record<string, any> = {}

constructor(private originUrl: string) {
  this.originUrl = originUrl

  this.methods.forEach((method: MethodsType) => {
    OssImage.prototype[method] = function(
     key: any,
     value?: any,
    ) {
      if (typeof key === 'object') {
        this.methodKeys[method] = key
      } else {
        if (!this.methodKeys[method]) this.methodKeys[method] = {}
        this.methodKeys[method][key] = value
      }
      return this
    }
  })
}
...

构造函数中的originUrl参数为原始oss 图片链接,我们遍历methods数组,将方法设置到类的原型对象prototype上,如果我们只是单纯的改变图片的一个维度的参数是,可以传入两个参数,第一个参数为k,第二个参数为v;如果是多个维度是,则只需要传入第一个参数,而且为k-v键值对对象;还有一种情况就是第一个参数是一个v的,例如rotate类型等;最终需要返回当前实例this,便于使用链式方式对图片以多个不同类型进行处理,例如:

// 链式调用,缩放图片宽度为100px,高度为100px,输出质量为50,旋转180度
const ossImage = new OssImage(url).resize({w: 100, h: 100}).quality({ q: 50 }).rotate(180)

接着只需要把各种类型的处理方式拼接起来,返回处理好的图片链接,该功能就完成了

...
 /**
  * 获取处理后的链接
  */
get url() {
  const url = new URL(this.originUrl)
  const formatParams = Object.keys(this.methodKeys).reduce((acc, method) => {
    const str = joinParams(this.methodKeys[method], '_', ',')
    if (!str) return acc
    return (acc += `/${decamelize(method)},${str}`)
  }, '')
  url.searchParams.set('x-oss-process', `image${formatParams}`)
  return url.href
}
...

这里使用getter方式,直接访问实例的url就能获取链接。下面对joinParamsdecamelize两个方法说明一下。

joinParams方法的作用的是拼接每个处理方式的配置信息,例如joinParams({w:100, h: 100}, '_', ','),返回的就是'w_100,h_100',具体实现如下:

/**
 * 用joinParams拼接obj里面的每对k-v,并用splitFlag分割
 */
function joinParams(obj: Record<string, any>, joinFlag: string, splitFlag: string): string {
  return Object.keys(obj)
    .reduce((acc, cur) => {
      if (obj[cur]) return (acc += `${splitFlag}${cur}${joinFlag}${obj[cur]}`)
      return (acc += `${splitFlag}${cur}`)
    }, '')
    .slice(1)
}

decamelize方法则是将处理类型驼峰转中横线链接,例如decamelize('autoOrient'),返回的就是'auto-orient',具体实现如下:

/**
 * 驼峰转中横线
 */
export function decamelize(str: string): string {
  return str.replace(/\B([A-Z])/g, '-$1').toLowerCase()
}

完成上述功能后,我们就可以拿到处理好的图片链接了

const ossImage = new OssImage(url)
// 获取处理好的图片链接
const src = ossImage.resize({w: 100, h: 100}).quality({ q: 50 }).url

继续优化下,如果我们每次都需要使用new 的方式对图片处理是不是很麻烦,我们可不可以不使用new 方式而是直接调用ossImage方法进行处理呢?答案是可以的,只需要用一个函数处理返回就好。

export function ossImage(url: string): OssImage {
  return new OssImage(url)
}

接下来就可以使用以下方式调用了。

// 获取处理好的图片链接
const src = ossImage(url).resize({w: 100, h: 100}).quality({ q: 50 }).url

继续思考下,如果是在Vue项目中,可以不可以使用<oss-image src="..." :resize="..."></oss-image>方式进行调用呢?答案是可以的,只需要基于工具集成到Vue组件就OK了。

封装成Vue组件

为了方便说明,假设基于ElementUI开发,我们使用使用ElImage组件进行二次封装,这里直接用img标签也可以实现,只是需要额外编写下样式和交互效果。具体实现如下:

<template>
  <el-image :src="url" v-bind="$attrs" v-on="$listeners"></el-image>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
import ossImage from 'path/to/oss-image'
import { OssResize, ... } from 'path/to/oss-image/interface'

const METHOD_LIST = ['quality', 'resize', ...]

@Component({
  name: 'OssImage',
})
export default class OssImage extends Vue {
  @Prop({ type: String, required: true }) src!: string

  @Prop() resize!: OssResize
  // 其他配置项以此类推...
  ...

  get url() {
    let target = ossImage(this.src)
    METHOD_LIST.forEach((key: MethodsType) => {
      this[key] && (target = (target[key] as any)(this[key]))
    })
    return target.url
  }
}
</script>

是不是很简单,就是一堆props和一个computed就完成了,接下来我们就可以愉快的使用这个组件了。

<template>
  <div>
    <oss-image :src="url" :resize="resize"></oss-image>
    <oss-image :src="url" :resize="resize" :quality="quality"></oss-image>
    <oss-image :src="url" :resize="resize" :blur="blur"></oss-image>
  </div>
</template>

<script lang="ts">
export default {
  data() {
    return {
      url:'https://xiaoedu-ams.oss-cn-shenzhen.aliyuncs.com/test/20201029/18/1603966544271Fiine.jpeg',
      resize: { w: 160, h: 160 },
      quality: { q: 10},
      blur: { r: 25, s: 10 },
    }
  },
}
</script>

效果如下:

使用过el-image组件的同学应该都知道,其实它是支持预览功能的,而且它提供了preview-src-list属性来表示要预览的图片列表,会根据当前url链接判断当前预览的是第几张图片,如果我们处理过url,那么无论我们点击的是第二张图片,那我们预览时也会跳到第一张,所以我们重写了它的imageIndex的计算属性。

import Element from 'element-ui'

/**
 * 兼容oss-image 阿里云图片 预览展示
 */
Element.Image.computed.imageIndex = function() {
  let previewIndex = 0
  const srcIndex = this.previewSrcList.findIndex((url: string) =>
    this.src.includes(url),
  )
  if (srcIndex >= 0) {
    previewIndex = srcIndex
  }
  return previewIndex
}

最后

感谢大家的细心阅读,现在是不是跃跃欲试地想运用到自己的项目中了,如果这篇文档对同学们有所帮忙和启发,欢迎点赞收藏。