图片裁剪与上传处理方案 —— 基于阿里云 OSS 处理用户资料

目录

01: 通用组件:input 构建方案分析

02: 通用组件:input 构建方案

03: 构建用户资料基础样式 

04: 用户基本资料修改方案

05: 处理不保存时的同步问题 

06: 头像修改方案流程分析

07: 通用组件:Dialog 构建方案分析

08: 通用组件:Dialog 构建方案

09: 应用 Dialog 展示头像

10: 头像裁剪构建方案 

11. 阿里云 OSS 与腾讯云 COS 对象存储方案分析 

腾讯云 COS

COS SDK(COS 的包) 

阿里云 OSS

OSS 基础概念

创建存储桶(Bucket)

使用 STS 临时访问凭证访问 OSS 

上传图片到 Bucket 的流程分析

配置 CORS 跨域处理

12. 使用临时凭证,上传裁剪图片到阿里云 OSS

13. 完成头像更新操作

14. 登录鉴权解决方案

15: 总结


 

01: 通用组件:input 构建方案分析

 

期望通用组件 input 至少满足 4 个功能:

        1. 支持单行文本输入 

        2. 支持多行文本输入

        3. 通过 v-model 实现双向数据绑定

        4. 支持最大文本输入

根据以上功能点,可判断出 input 组件要有 3 个 prop:

        1. v-model

        2. type:单行 or 多行

        3. max:支持最大字符数

02: 通用组件:input 构建方案

- src/libs
- - input
- - - index.vue
// src/libs/input/index.vue

<template>
  <div class="relative">
    <input
      v-if="type === TYPE_TEXT"
      class="border-gray-200 dark:border-zinc-600 dark:bg-zinc-800 duration-100 dark:text-zinc-400 border-[1px] outline-0 py-0.5 px-1 text-sm rounded-sm focus:border-blue-400 w-full"
      type="text"
      v-model="text"
      :maxlength="max"
    />
    <textarea
      v-if="type === TYPE_TEXTAREA"
      v-model="text"
      :maxlength="max"
      rows="5"
      class="border-gray-200 dark:border-zinc-600 dark:bg-zinc-800 duration-100 dark:text-zinc-400 border-[1px] outline-0 py-0.5 px-1 text-sm rounded-sm focus:border-blue-400 w-full"
    ></textarea>
    <span
      v-if="max"
      class="absolute right-1 bottom-0.5 text-zinc-400 text-xs"
      :class="{ 'text-red-700': currentNumber === parseInt(max) }"
      >{{ currentNumber }} / {{ max }}</span
    >
  </div>
</template>

<script>
const TYPE_TEXT = 'text'
const TYPE_TEXTAREA = 'textarea'
</script>

<script setup>
import { useVModel } from '@vueuse/core'
import { computed } from 'vue'

const props = defineProps({
  modelValue: {
    required: true,
    type: String
  },
  type: {
    type: String,
    default: TYPE_TEXT,
    validator(value) {
      const arr = [TYPE_TEXT, TYPE_TEXTAREA]
      const result = arr.includes(value)
      if (!result) {
        throw new Error(`type 的值必须在可选范围内 [${arr.join('、')}]`)
      }
      return result
    }
  },
  max: {
    type: [String, Number]
  }
})

// 事件声明
defineEmits(['update:modelValue'])

// 输入的字符
const text = useVModel(props)

// 输入的字符数
const currentNumber = computed(() => {
  return text.value?.length
})
</script>

<style lang="scss" scoped></style>

03: 构建用户资料基础样式 

tailwindcss 先构建移动端,再构建 PC 端更方便。

- src/views
- - profile
- - - index.vue
// 路由信息

{
  path: '/profile',
  name: 'profile',
  component: () => import('@/views/profile/index.vue'),
  // 标记当前的页面只有用户登录之后才可以进入
  meta: {
    user: true
  }
},
// src/views/profile/index.vue

<template>
  <div
    class="h-full bg-zinc-200 dark:bg-zinc-800 duration-400 overflow-auto xl:pt-1"
  >
    <div
      class="relative max-w-screen-lg mx-auto bg-white dark:bg-zinc-900 duration-400 xl:rounded-sm xl:border-zinc-200 xl:dark:border-zinc-600 xl:border-[1px] xl:px-4 xl:py-2"
    >
      <!-- 移动端 navbar -->
      <m-navbar sticky v-if="isMobileTerminal" :clickLeft="onNavbarLeftClick">
        个人资料
      </m-navbar>
      <!-- pc 端 -->
      <div v-else class="text-lg font-bold text-center mb-4 dark:text-zinc-300">
        个人资料
      </div>
      <div class="h-full w-full px-1 pb-4 text-sm mt-2 xl:w-2/3 xl:pb-0">
        <!-- 头像 -->
        <div class="py-1 xl:absolute xl:right-[16%] xl:text-center">
          <span
            class="w-8 inline-block mb-2 font-bold text-sm dark:text-zinc-300 xl:block xl:mx-auto"
          >
            我的头像
          </span>
          <!-- 头像部分 -->
          <div
            class="relative w-[80px] h-[80px] group xl:cursor-pointer xl:left-[50%] xl:translate-x-[-50%]"
            @click="onAvatarClick"
          >
            <img
              v-lazy
              :src="$store.getters.userInfo.avatar"
              alt=""
              class="rounded-[50%] w-full h-full xl:inline-block"
            />
            <div
              class="absolute top-0 rounded-[50%] w-full h-full bg-[rgba(0,0,0,.4)] hidden xl:group-hover:block"
            >
              <m-svg-icon
                name="change-header-image"
                class="w-2 h-2 m-auto mt-2"
              ></m-svg-icon>
              <div
                class="text-xs text-white dark:text-zinc-300 scale-90 mt-0.5"
              >
                点击更换头像
              </div>
            </div>
          </div>
          <!-- 隐藏域 -->
          <input
            v-show="false"
            ref="inputFileTarget"
            type="file"
            accept=".png, .jpeg, .jpg, .gif"
            @change="onSelectImgHandler"
          />
          <p class="mt-1 text-zinc-500 dark:text-zinc-400 text-xs xl:w-10">
            支持 jpg、png、jpeg 格式大小 5M 以内的图片
          </p>
        </div>
        <!-- 用户名 -->
        <div class="py-1 xl:flex xl:items-center xl:my-1">
          <span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">
            用户名
          </span>
          <m-input
            v-model="userInfo.nickname"
            class="w-full"
            type="text"
            max="20"
          ></m-input>
        </div>
        <!-- 职位 -->
        <div class="py-1 xl:flex xl:items-center xl:my-1">
          <span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">
            职位
          </span>
          <m-input
            v-model="userInfo.title"
            class="w-full"
            type="text"
          ></m-input>
        </div>
        <!-- 公司 -->
        <div class="py-1 xl:flex xl:items-center xl:my-1">
          <span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">
            公司
          </span>
          <m-input
            v-model="userInfo.company"
            class="w-full"
            type="text"
          ></m-input>
        </div>
        <!-- 个人主页 -->
        <div class="py-1 xl:flex xl:items-center xl:my-1">
          <span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">
            个人主页
          </span>
          <m-input
            v-model="userInfo.homePage"
            class="w-full"
            type="text"
          ></m-input>
        </div>
        <!-- 个人介绍 -->
        <div class="py-1 xl:flex xl:my-1">
          <span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">
            个人介绍
          </span>
          <m-input
            v-model="userInfo.introduction"
            class="w-full"
            type="textarea"
            max="50"
          ></m-input>
        </div>
        <!-- 保存修改 -->
        <m-button
          class="w-full mt-2 mb-4 dark:text-zinc-300 dark:bg-zinc-800 xl:w-[160px] xl:ml-[50%] xl:translate-x-[-50%]"
          :loading="loading"
          @click="onChangeProfile"
        >
          保存修改
        </m-button>
        <!-- 移动端退出登录 -->
        <m-button
          v-if="isMobileTerminal"
          class="w-full dark:text-zinc-300 dark:bg-zinc-800 xl:w-[160px] xl:ml-[50%] xl:translate-x-[-50%]"
          @click="onLogoutClick"
        >
          退出登录
        </m-button>
      </div>
    </div>

    <!-- PC 端 -->
    <m-dialog v-if="!isMobileTerminal" v-model="isDialogVisible">
      <change-avatar-vue
        :blob="currentBolb"
        @close="isDialogVisible = false"
      ></change-avatar-vue>
    </m-dialog>
    <!-- 移动端 -->
    <m-popup
      v-else
      :class="{ 'h-screen': isDialogVisible }"
      v-model="isDialogVisible"
    >
      <change-avatar-vue
        :blob="currentBolb"
        @close="isDialogVisible = false"
      ></change-avatar-vue>
    </m-popup>
  </div>
</template>

<script>
export default {
  name: 'profile'
}
</script>

<script setup>
import { isMobileTerminal } from '@/utils/flexible'
import { putProfile } from '@/api/sys'
import { message, confirm } from '@/libs'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { ref, watch } from 'vue'
import changeAvatarVue from './components/change-avatar.vue'

const store = useStore()
const router = useRouter()

// 隐藏域
const inputFileTarget = ref(null)
// 头像 dialog 展示
const isDialogVisible = ref(false)
// 选中的图片
const currentBolb = ref('')
/**
 * 更换头像点击事件
 */
const onAvatarClick = () => {
  inputFileTarget.value.click()
}

/**
 * 头像选择之后的回调
 */
const onSelectImgHandler = () => {
  // 获取选中的文件
  const imgFile = inputFileTarget.value.files[0]
  // 生成 blob 对象
  const blob = URL.createObjectURL(imgFile)
  // 获取选中的图片
  currentBolb.value = blob
  // 展示 Dialog
  isDialogVisible.value = true
}

/**
 * 监听 dialog 关闭
 */
watch(isDialogVisible, (val) => {
  if (!val) {
    // 防止 change 不重复触发
    inputFileTarget.value.value = null
  }
})

/**
 * 数据本地的双向同步,增加一个单层深拷贝
 */
const userInfo = ref({ ...store.getters.userInfo })
// const changeStoreUserInfo = (key, value) => {
//   store.commit('user/setUserInfo', {
//     ...store.getters.userInfo,
//     [key]: value
//   })
// }

/**
 * 修改个人信息
 */
const loading = ref(false)
const onChangeProfile = async () => {
  loading.value = true
  await putProfile(userInfo.value)
  message('success', '用户信息修改成功')
  // 更新 vuex
  store.commit('user/setUserInfo', userInfo.value)
  loading.value = false
}

/**
 * 移动端后退处理
 */
const onNavbarLeftClick = () => {
  // 配置跳转方式
  store.commit('app/changeRouterType', 'back')
  router.back()
}

/**
 * 移动端:退出登录
 */
const onLogoutClick = () => {
  confirm('确定要退出登录吗?').then(() => {
    store.dispatch('user/logout')
  })
}
</script>

<style lang="scss" scoped></style>

04: 用户基本资料修改方案

// 第一种方式:直接修改 vuex state 数据,违背 vue 理念。

<m-input v-model="$store.getters.userInfo.nickname" />


// 第二种方式:优化第一种方式。

<m-input :modelValue="$store.getters.userInfo.nickname"
    @update:modelValue="changeStoreUserInfo('nickname', $event)"
/>

const changeStoreUserInfo = (key, value) => {
   store.commit('user/setUserInfo', {
     ...store.getters.userInfo,
     [key]: value
   })
}


// 第三种方式:解决用户信息未提交,但提前缓存问题。推荐。

<m-input v-model="userInfo.nickname" />

/**
 * 数据本地的双向同步,增加一个单层深拷贝
 */
const userInfo = ref({ ...store.getters.userInfo })

/**
 * 修改个人信息
 */
const onChangeProfile = async () => {
  // 发送修改请求 代码省略
  // 更新 vuex
  store.commit('user/setUserInfo', userInfo.value)
}

05: 处理不保存时的同步问题 

讲解一下 v-model 拆解的问题。

给不理解的同学讲解一下:为什么不能用 v-model 直接绑定 vuex 中的数据?以及绑定之后会产生什么样的问题?

思路及代码在上一小节之中。

06: 头像修改方案流程分析

接下来我们需要处理头像修改的业务逻辑。

对于该功能而言,分为 PC 端和 移动端 两种情况,我们需要分别进行处理:

1. PC 端:

        1. 点击更换头像。

        2. 选择对应文件。

        3. 通过 Dialog 展示图片剪裁。

        4. 剪裁后图片上传。

        5. 功能完成。

2. 移动端:

        1. 点击更换头像。

        2. 选择对应文件。

        3. 通过 popup 展示图片剪裁。

        4. 剪裁后图片上传。

        5. 功能完成。

由此可以发现,两者之间需要通过不同的组件进行裁剪展示。

因此我们后续的开发流程为:

        1. 通用组件:Dialog。

        2.  处理图片剪裁。

        3. 处理剪裁后上传。

07: 通用组件:Dialog 构建方案分析

首先我们来处理 Dialog 通用组件。

对于 Dialog 通用组件而言,我们可以参考 confirm 组件的构建过程。

它们两个构建方案非常相似,唯二不同的地方是:

        1. Dialog 无需通过方法调用的形式展示。

        2. Dialog 的内容区可以渲染任意的内容。

排除这两点之后,其余与 confirm 完全相同。

08: 通用组件:Dialog 构建方案

- src/libs
- - dialog
- - - index.vue
<template>
  <div>
    <!-- 蒙版 -->
    <transition name="fade">
      <div
        v-if="isVisable"
        @click="close"
        class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"
      ></div>
    </transition>
    <!-- 内容 -->
    <transition name="up">
      <div
        v-if="isVisable"
        class="max-w-[80%] max-h-[80%] overflow-auto fixed top-[10%] left-[50%] translate-x-[-50%] z-50 px-2 py-1.5 rounded-sm border dark:border-zinc-600 cursor-pointer bg-white dark:bg-zinc-800 xl:min-w-[35%]"
      >
        <!-- 标题 -->
        <div
          class="text-lg font-bold text-zinc-900 dark:text-zinc-200 mb-2"
          v-if="title"
        >
          {{ title }}
        </div>
        <!-- 内容 -->
        <div class="text-base text-zinc-900 dark:text-zinc-200 mb-2">
          <slot />
        </div>
        <!-- 按钮 -->
        <div class="flex justify-end" v-if="cancelHandler || confirmHandler">
          <m-button type="info" class="mr-2" @click="onCancelClick">{{
            cancelText
          }}</m-button>
          <m-button type="primary" @click="onConfirmClick">{{
            confirmText
          }}</m-button>
        </div>
      </div>
    </transition>
  </div>
</template>

<script setup>
import { useVModel } from '@vueuse/core'

const props = defineProps({
  // 控制开关
  modelValue: {
    type: Boolean,
    required: true
  },
  // 标题
  title: {
    type: String
  },
  // 取消按钮文本
  cancelText: {
    type: String,
    default: '取消'
  },
  // 确定按钮文本
  confirmText: {
    type: String,
    default: '确定'
  },
  // 取消按钮点击事件
  cancelHandler: {
    type: Function
  },
  // 确定按钮点击事件
  confirmHandler: {
    type: Function
  },
  // 关闭的回调
  close: {
    type: Function
  }
})

defineEmits(['update:modelValue'])

// 控制显示处理
const isVisable = useVModel(props)

/**
 * 取消按钮点击事件
 */
const onCancelClick = () => {
  if (props.cancelHandler) {
    props.cancelHandler()
  }
  close()
}

/**
 * 确定按钮点击事件
 */
const onConfirmClick = () => {
  if (props.confirmHandler) {
    props.confirmHandler()
  }
  close()
}

const close = () => {
  isVisable.value = false
  if (props.close) {
    props.close()
  }
}
</script>

<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
  transition: all 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.up-enter-active,
.up-leave-active {
  transition: all 0.3s;
}

.up-enter-from,
.up-leave-to {
  opacity: 0;
  transform: translate3d(-50%, 100px, 0);
}
</style>

09: 应用 Dialog 展示头像

// src/views/profile/index.vue

<template>
    <img :src="currentBolb" />
</template>

<script setup>

// 选中的图片
const currentBolb = ref('')

/**
 * 头像选择之后的回调
 */
const onSelectImgHandler = () => {
  // 获取选中的文件
  const imgFile = inputFileTarget.value.files[0]
  // 生成 blob 对象
  const blob = URL.createObjectURL(imgFile)
  // 获取选中的图片
  currentBolb.value = blob
  // 展示 Dialog
  isDialogVisible.value = true
}

/**
 * 当 input file 两次选择文件,是同一个的时候,change 的回调不会被再次触发。
 * 想要解决这个问题,只需要在每次选择的图片不再被使用之后,清空掉 input file 的 value。
 * 监听 dialog 关闭
 */
watch(isDialogVisible, (val) => {
  if (!val) {
    // 防止 change 不重复触发
    inputFileTarget.value.value = null
  }
})
</script>
- src/views/profile
- - components
- - - change-avatar.vue
// src/views/profile/components/change-avatar.vue

<template>
  <div class="overflow-auto relative flex flex-col items-center">
    <m-svg-icon
      v-if="isMobileTerminal"
      name="close"
      class="w-3 h-3 p-0.5 m-1 ml-auto"
      fillClass="fill-zinc-900 dark:fill-zinc-200 "
      @click="close"
    ></m-svg-icon>

    <img class="" ref="imageTarget" :src="blob" />

    <m-button
      class="mt-4 w-[80%] xl:w-1/2"
      :loading="loading"
      @click="onConfirmClick"
    >
      确定
    </m-button>
  </div>
</template>

<script>
const EMITS_CLOSE = 'close'

// 移动端配置对象
const mobileOptions = {
  // 将裁剪框限制在画布的大小
  viewMode: 1,
  // 移动画布,裁剪框不动
  dragMode: 'move',
  // 裁剪框固定纵横比:1:1
  aspectRatio: 1,
  // 裁剪框不可移动
  cropBoxMovable: false,
  // 不可调整裁剪框大小
  cropBoxResizable: false
}

// PC 端配置对象
const pcOptions = {
  // 裁剪框固定纵横比:1:1
  aspectRatio: 1
}
</script>

<script setup>
import { isMobileTerminal } from '@/utils/flexible'
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import { ref, onMounted } from 'vue'
import { getOSSClient } from '@/utils/sts'
import { message } from '@/libs'
import { useStore } from 'vuex'
import { putProfile } from '@/api/sys'

defineProps({
  blob: {
    type: String,
    required: true
  }
})

const emits = defineEmits([EMITS_CLOSE])

/**
 * 图片裁剪处理
 */
const imageTarget = ref(null)
let cropper = null
onMounted(() => {
  /**
   * 接收两个参数:
   * 1. 需要裁剪的图片 DOM
   * 2. options 配置对象
   */
  cropper = new Cropper(
    imageTarget.value,
    isMobileTerminal.value ? mobileOptions : pcOptions
  )
})

/**
 * 确定按钮点击事件
 */
const loading = ref(false)
const onConfirmClick = () => {
  loading.value = true
  // 获取裁剪后的图片
  cropper.getCroppedCanvas().toBlob((blob) => {
    // 裁剪后的 blob 地址
    // console.log(URL.createObjectURL(blob))
    putObjectToOSS(blob)
  })
}

/**
 * 进行 OSS 上传
 */
let ossClient = null
let store = useStore()
const putObjectToOSS = async (file) => {
  if (!ossClient) {
    ossClient = await getOSSClient()
  }
  try {
    // 因为当前凭证只具备 images 文件夹下的访问权限,所以图片需要上传到 images/xxx.xx 。否则你将得到一个 《AccessDeniedError: You have no right to access this object because of bucket acl.》 的错误
    const fileTypeArr = file.type.split('/')
    const fileName = `${store.getters.userInfo.username}/${Date.now()}.${
      fileTypeArr[fileTypeArr.length - 1]
    }`
    // 文件存放路径,文件
    const res = await ossClient.put(`images/${fileName}`, file)
    // 通知服务器
    onChangeProfile(res.url)
  } catch (e) {
    message('error', e)
  }
}

/**
 * 上传新头像到服务器
 */
const onChangeProfile = async (avatar) => {
  // 更新本地数据
  store.commit('user/setUserInfo', {
    ...store.getters.userInfo,
    avatar
  })
  // 更新服务器数据
  await putProfile(store.getters.userInfo)
  // 通知用户
  message('success', '用户头像修改成功')
  // 关闭 loading
  loading.value = false
  // 关闭 dialog
  close()
}

/**
 * 关闭事件
 */
const close = () => {
  emits(EMITS_CLOSE)
}
</script>

<style lang="scss" scoped></style>

10: 头像裁剪构建方案 

        接下来我们需要在 src/views/profile/components/change-avatar.vue 中处理对应的图片裁剪功能。 

        想要处理图片裁剪,我们需要使用到 cropperjs,它是一个 Javascript 的库,同时支持 PC 端 和 移动端。

        目前 cropperjs 的最新发布版本为 1.6.2,V2 级别的版本还是 RC 阶段,所以我们还是使用它的 V1 版本。

1. 安装 cropperjs

npm install cropperjs@1.5.12 --save

2. 在 src/views/profile/components/change-acatar.vue 中进行导入

import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'

3. 使用 new Cropper 进行初始化,区分 PC端 和 移动端:所有配置项

// 移动端配置对象
const mobileOptions = {
  // 将裁剪框限制在画布的大小
  viewMode: 1,
  // 移动画布,裁剪框不动
  dragMode: 'move',
  // 裁剪框固定纵横比:1:1
  aspectRatio: 1,
  // 裁剪框不可移动
  cropBoxMovable: false,
  // 不可调整裁剪框大小
  cropBoxResizable: false
}

// PC 端配置对象
const pcOptions = {
  // 裁剪框固定纵横比:1:1
  aspectRatio: 1
}

4. 

/**
 * 图片裁剪处理
 */
const imageTarget = ref(null)
let cropper = null
onMounted(() => {
  /**
   * 接收两个参数:
   * 1. 需要裁剪的图片 DOM
   * 2. options 配置对象
   */
  cropper = new Cropper(
    imageTarget.value,
    isMobileTerminal.value ? mobileOptions : pcOptions
  )
})

5.

/**
 * 确定按钮点击事件
 */
const loading = ref(false)
const onConfirmClick = () => {
  loading.value = true
  // 获取裁剪后的图片
  cropper.getCroppedCanvas().toBlob((blob) => {
    // 裁剪后的 blob 地址
    // console.log(URL.createObjectURL(blob))
    putObjectToOSS(blob)
  })
}

11. 阿里云 OSS 与腾讯云 COS 对象存储方案分析 

当图片裁剪处理完成之后,接下来我们就可以处理裁剪之后的图片上传了。

        通常情况下,在企业开发中,图片或文件的管理都会通过 对象存储 的方案进行。目前国内常见的对象存储云方案主要有两个平台: 

        1. 阿里云 OSS

        2. 腾讯云 COS

腾讯云 COS

腾讯云 COS 目前提供了 实名认证赠送 6个月 对象存储的服务,点击跳转

 

        这个服务对大家而言是非常好的一个练习服务,大家可以直接使用该服务来实现 裁剪图片上传到腾讯云。

以下为腾讯云 COS 使用流程:

        1. 注册 腾讯云 账号,并完成 实名认证。 

        2. 选择免费产品。

        3. 选择腾讯云 COS。

        4. 点击立即开通。

        5. 创建存储桶。

        6. 点击创建。

        7. 选择 公有读、私有写

        8. 一路下一步,创建即可。

此时你可以得到对应的存储桶,所有的文件都会被上传到该存储桶之中。

接下来就可以按照以下步骤进行图片上传:

COS SDK(COS 的包) 

对应文档地址 

1. 下载依赖包:

npm i cos-js-sdk-v5 --save

2. 构建 cos 实例:初始化 cos 对象参数 

名称描述
SecretId开发者拥有的项目身份识别 ID,用以身份认证,可在 API 密钥管理 页面获取
SecretKey开发者拥有的项目身份密钥,可在 API 密钥管理 页面获取
import COS from 'cos-js-sdk-v5'

const cos = new COS({
    SecretId: '',
    SecretKey: ''
})

3. 执行上传操作

cos.putObject({
    Bucket: '',  // 填入您自己的存储痛,必须字段
    Region: '',  // 存储桶所在地域,例如 ap-beijing,必须字段
    Key: params.file.name,  // 存储在桶里的对象键(例如 1.jpg a/b/test.txt),必须字段
    StorageClass: 'STANDARD',
    Body: ,  // 上传文件对象
    onProgress: function (progressData) {
        console.log(JSON.stringify(progressData)
    }
}, function (err, data) {
    // 上传成功返回的数据,data.location 为图片的地址
    console.log(err || data)
})

4. 图片上传成功,在存储桶中即可查看上传的图片。 

阿里云 OSS

目前国内企业,使用最多的云服务为 阿里云,所以说咱们文章将会以 阿里云 为例进行开发。

阿里云的 OSS 服务,有新人三个月的免费试用。

我们将使用 OSS 进行图片的上传。

阿里云 OSS 使用流程:

        1. 注册登录 阿里云服务。

        2. 找到 阿里云 OSS 对象存储服务。

        3. 点击 立即开通。

        4. 选择 立即开通。

        5. 勾选服务,点击立即开通。

        6. 提示开通成功之后,可以进行两个操作。

                1. 点击 管理控制台 进入 OSS 控制台。

                2. 点击 对象存储新手入门 查看文档。

OSS 基础概念

OSS 中包含了很多的基础概念,可以点击 这里 直接进行访问。

需要大家了解的基础概念有:

        1. Bucket:存储空间。

        2. Object:存储文件。

创建存储桶(Bucket)

控制台 左侧点击 Bucket 列表,然后点击 创建 Bucket(读写权限为 私有

使用 STS 临时访问凭证访问 OSS 

在 Bucket 构建完成之后,接下来我们就需要去处理 访问凭证。 

注意:构建访问凭证,会涉及到服务端的配置。在实际开发中,不需要 前端工程师来处理访问凭证相关的内容。

具体的构建流程可以点击 这里 进行查看。

名词解释:

        RAM(Resource Access Management)

        STS(Security Token Service)

        ARN是指云服务所定义的资源(Aliyun Resource Name)

上传图片到 Bucket 的流程分析

1. 想要上传文件到 Bucket,我们需要使用 ali-sdk ali-oss。

2. 利用 ali-oss 生成 OSS 对象。

3. 在生成 OSS 对象时,需要传递 文件上传凭证

        1. accessKeyId。

        2. accessKeySecret。

        3. stsToken。

4. 需要通过接口 /user/sts 获取 文件上传凭证

整体的文件上传流程为:

        1. 通过接口 /user/sts 获取 文件上传凭证

        2. 通过 npm i ali-oss 安装依赖包。

        3. 使用凭证中的数据构建 OSS 对象。点击这里查看文档。

配置 CORS 跨域处理

当我们尝试使用 put 方法上传文件时,会得到一个跨域的错误。

想要处理这个问题,则需要 配置跨域规则。点击这里查看配置方案。

12. 使用临时凭证,上传裁剪图片到阿里云 OSS

本小节我们将讲解如何将裁剪后的图片上传到阿里云 OSS。

具体步骤如下:

        1. 安装 ali-oss 依赖。

        2. 通过接口获取临时访问凭证,生成 OSS 实例。

        3. 利用 ossClient.put 方法,完成对应上传。

接下来我们一步一步去做:

1. 安装 ali-oss 依赖。

npm i --save ali-oss@6.17.0

2. 创建 src/utils/sts.js 模块,用来生成 OSS 实例。

import OSS from 'ali-oss'
import { REGION, BUCKET } from '@/constants'
import { getSts } from '@/api/sys'

export const getOSSClient = async () => {
  const res = await getSts()
  return new OSS({
    // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
    region: REGION,
    // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
    accessKeyId: res.Credentials.AccessKeyId,
    accessKeySecret: res.Credentials.AccessKeySecret,
    // 从STS服务获取的安全令牌(SecurityToken)。
    stsToken: res.Credentials.SecurityToken,
    // 填写Bucket名称。
    bucket: BUCKET,
    // 刷新 token,在 token 过期后自动调用(但是并不生效,可能会在后续的版本中修复)
    refreshSTSToken: async () => {
      // 向您搭建的STS服务获取临时访问凭证。
      const res = await getSts()
      return {
        accessKeyId: res.Credentials.AccessKeyId,
        accessKeySecret: res.Credentials.AccessKeySecret,
        stsToken: res.Credentials.SecurityToken
      }
    },
    // 刷新临时访问凭证的时间间隔,单位为毫秒。
    refreshSTSTokenInterval: 5 * 1000
  })
}

3. 在 constants 中定义 REGION,BUCKET

// STS 上传数据
export const REGION = 'oss-cn-beijing'
export const BUCKET = 'imooc-front'

4. 

/**
 * 进行 OSS 上传
 */
let ossClient = null
let store = useStore()
const putObjectToOSS = async (file) => {
  if (!ossClient) {
    ossClient = await getOSSClient()
  }
  try {
    // 因为当前凭证只具备 images 文件夹下的访问权限,所以图片需要上传到 images/xxx.xx 。否则你将得到一个 《AccessDeniedError: You have no right to access this object because of bucket acl.》 的错误
    const fileTypeArr = file.type.split('/')
    const fileName = `${store.getters.userInfo.username}/${Date.now()}.${
      fileTypeArr[fileTypeArr.length - 1]
    }`
    // 文件存放路径,文件
    const res = await ossClient.put(`images/${fileName}`, file)
    // 通知服务器
    onChangeProfile(res.url)
  } catch (e) {
    message('error', e)
  }
}

/**
 * 上传新头像到服务器
 */
const onChangeProfile = async (avatar) => {
  // 更新本地数据
  store.commit('user/setUserInfo', {
    ...store.getters.userInfo,
    avatar
  })
  // 更新服务器数据
  await putProfile(store.getters.userInfo)
  // 通知用户
  message('success', '用户头像修改成功')
  // 关闭 loading
  loading.value = false
  // 关闭 dialog
  close()
}

/**
 * 关闭事件
 */
const close = () => {
  emits(EMITS_CLOSE)
}

13. 完成头像更新操作

        为了代码连贯性,故代码写在上一小节中。

14. 登录鉴权解决方案

// src/permission.js

import router from '@/router'
import store from '@/store'
import { message } from '@/libs'

/**
 * 处理需登录页面的访问权限
 */
router.beforeEach((to, from) => {
  // 无需登录的页面访问
  if (!to.meta.user) {
    return
  }
  // 已登录,可进入
  if (store.getters.token) {
    return true
  }

  // 未登录,警告然后返回首页
  message('warn', '登录失效,请重新登录!')
  return '/'
})

15: 总结

在本篇文章中,我们接触到了两个新的通用组件:

        1. input

        2. dialog

        除此之外,我们还接触到了头像裁剪、OSS、COS 的概念。这些概念可能对有些同学而言会比较新奇。大家也可以自己申请一个对应的 OSS 或者 COS 的账号(推荐 COS)。走一遍完整的流程,这样大家会对这个操作更加的熟悉。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/681867.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Transformer学习(4)

上篇文章完成了Transformer剩下组件的编写&#xff0c;因此本文就可以开始训练。 本文主要介绍训练时要做的一些事情&#xff0c;包括定义损失函数、学习率调整、优化器等。 下篇文章会探讨如何在多GPU上进行并行训练&#xff0c;加速训练过程。 数据集简介 从网上找到一份中…

双向链表的讲解与实现

双向链表的讲解与实现 一、双向链表的结构二、顺序表和双向链表的优缺点分析三、双向链表的实现(使用VS2022)1.初始化、销毁、打印、判空2.尾插尾删、头插头删3.查找、指定插入、指定删除 四、代码优化五、完整 List.c 源代码 一、双向链表的结构 带头”跟前面我们说的“头节点…

家政预约小程序13我的订单

目录 1 我的订单页面布局2 全部订单页面3 完善订单状态4 查询订单信息总结 现在我们已经完成了家政预约小程序主体功能的开发&#xff0c;包含服务的查看&#xff0c;在线预约已经登录等功能。预约之后就需要家政公司的客服进行派单&#xff0c;由服务人员进行上门服务。在小程…

gitlabcicd-k8s部署gitlab

一.安装准备环境 存储使用nfs挂载持久化 k8s环境 helm安装 建议helm 3 二.部署gitlab-deploy.yaml nfs的ip是192.168.110.190 挂载目录是/data/data 注意所需要的目录需要创建&#xff1a;/data/data/gitlab/config &#xff0c;/data/data/gitlab/logs &#xff0c;/dat…

LeetCode刷题之HOT100之合并区间

雨下了一整天&#xff0c;中午早早就回去吃饭拿快递了&#xff0c;今天拿了很多快递。我的书回来啦哈哈&#xff0c;还有好多零食&#xff0c;爽歪歪啊&#xff0c;放在下面了&#xff0c;然后准备开始做题啦&#xff01; 图一&#xff1a;左一是xh送我的&#xff0c;非常精彩…

JAVA小案例-99乘法表

JAVA小案例-99乘法表 今天学的一个小案例&#xff0c;主要是为了复习一下嵌套循环。 代码如下&#xff1a; public class Cheng {public static void main(String[] args) {int i;int j;for (i 1; i < 10; i) { //i<10和<9一个意思for (j 1; j < i 1; j) {…

CDH服务红,查看日志发现host有问题

看host后&#xff0c;发现里面节点ip都是127.0.0.1然后全部改成对应的ip&#xff0c; 1.在/etc/hosts里面全部加上了 ip以及对应的角色名称 2然后注释了127.0.0.1 hostname 3.然后重启所有的机器agent和server&#xff0c;在重新登录&#xff0c;点击重新部署。 重启agent sy…

STM32作业实现(八)触摸按键TPAD

目录 STM32作业设计 STM32作业实现(一)串口通信 STM32作业实现(二)串口控制led STM32作业实现(三)串口控制有源蜂鸣器 STM32作业实现(四)光敏传感器 STM32作业实现(五)温湿度传感器dht11 STM32作业实现(六)闪存保存数据 STM32作业实现(七)OLED显示数据 STM32作业实现(八)触摸按…

echarts柱状图的背景动态效果

这里的动态效果实现原理&#xff0c;就是相当于柱状图多了一组同系列数据&#xff0c;其值与数组展示数据一致&#xff08;类似下图&#xff09; 即&#xff0c;柱形图的每一个柱体都有它对应的另外一个柱体 其中白色柱体要与展示柱体重合&#xff0c;效果类似与给柱体做背景…

Linux---网络相关配置

文章目录 目录 文章目录 前言 一.查看网络信息 二.修改网络配置信息 总结 前言 一台主机需要配置必要的网络信息&#xff0c;才可以连接到互联网&#xff0c;需要的配置网络信息包括IP&#xff0c;子网掩码&#xff0c;网关和DNS。 一.查看网络信息 查看IP信息可以通过ifcon…

用单链表实现集合

一、实验题目 &#xff08;1&#xff09;实验题目 用单链表实现集合 &#xff08;2&#xff09;问题描述 用有序单链表实现集合的判等、交、并和差等基本运算。 二、实验内容 &#xff08;1&#xff09;采用有序单链表存储集合&#xff1b; &#xff08;2&#xff09;实现交…

IntelliJ IDEA智能编程插件AI Assistant

IntelliJ IDEA集成开发工具最新版本提供了人工智能AI编程助手的插件&#xff0c;AI Assistant使用手册的文档地址是AI Assistant | IntelliJ IDEA Documentation AI Assistant提供以下的编程能力以及工具特性&#xff1a; 与AI Assistant聊天&#xff0c;提问与项目相关或者与…

DNF手游辅助职业推荐:魔道学者云手机辅助玩法攻略!

在DNF手游中&#xff0c;魔道学者是一个独特且强力的辅助职业&#xff0c;深受玩家喜爱。她不仅能提供强大的辅助效果&#xff0c;还拥有丰富的技能机制。本文将简要介绍魔道学者的辅助玩法&#xff0c;推荐适合的装备和技能搭配&#xff0c;帮助玩家更好地掌握这一职业。 魔道…

LeetCode25_K个一组翻转链表

. - 力扣&#xff08;LeetCode&#xff09; 一、题目描述 二、过程模拟 1. 第一步 2. 第二步&#xff1a;子链表分组 3. 第三步&#xff1a;断开前后两组 4. 第四步&#xff1a;翻转start到end的部分 5. 第五步&#xff1a;连接翻转好的前半部分和未翻转的后半部分&#xff…

IEAD常用快捷键

如题 网页图片不清晰&#xff0c;可下载后查看

mac Network: use --host to expose

本地启动无法访问&#xff0c;这个不是权限问题是mac 主机端口安全策略&#xff0c;现在我们只需要开启端口自动检测就可以 npm run dev --host 网络&#xff1a;未暴露 方案一 1、执行 npm run dev -- --host 方案二 1、请在 vite.config.js server: {host: true } 1…

【SpringBoot】SpringBoot同时可以处理多少请求

目录 问题Web三大容器三者区别TomcatJetty小结 最大连接数和最大等待数同时处理请求数拓展&#xff1a;设置Web容器设置容器为Jetty设置容器为Undertow 问题 之前看到过一个面试题&#xff1a;SpringBoot同时可以处理多少请求&#xff1f; 准确的来说&#xff0c;Spring Boot…

Android Studio的Gradle面板里不显示task,build ,assemble 无法出aar包

按照以下方式把对应开关打开就可以正常进行build/assemble进行aar的生成了

景源畅信数字:抖音直播人气品类有哪些?

随着短视频平台的兴起&#xff0c;抖音成为了人们日常生活中不可或缺的娱乐方式之一。而抖音直播作为平台的重要组成部分&#xff0c;吸引了大量的观众和主播参与。那么&#xff0c;在抖音直播中&#xff0c;哪些品类能够吸引更多的人气&#xff0c;成为观众们关注的焦点呢?接…

运维工具 - SFTP 和 FTP 的区别?

SFTP 和 FTP 的区别有三点 连接方式 SFTP 是在客户端和服务器之间通过 SSH 协议建立的安全连接来传输文件&#xff0c;而 FTP 则是 TCP 端口 21 上的控制连接建立连接。 安全性 SFTP 使用加密传输认证信息来传输数据&#xff0c;因此 SFTP 相对于 FTP 更安全的。 效率 SF…