图片裁剪与上传处理方案 —— 基于阿里云 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"><inputv-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"/><textareav-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><spanv-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><divclass="h-full bg-zinc-200 dark:bg-zinc-800 duration-400 overflow-auto xl:pt-1"><divclass="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"><spanclass="w-8 inline-block mb-2 font-bold text-sm dark:text-zinc-300 xl:block xl:mx-auto">我的头像</span><!-- 头像部分 --><divclass="relative w-[80px] h-[80px] group xl:cursor-pointer xl:left-[50%] xl:translate-x-[-50%]"@click="onAvatarClick"><imgv-lazy:src="$store.getters.userInfo.avatar"alt=""class="rounded-[50%] w-full h-full xl:inline-block"/><divclass="absolute top-0 rounded-[50%] w-full h-full bg-[rgba(0,0,0,.4)] hidden xl:group-hover:block"><m-svg-iconname="change-header-image"class="w-2 h-2 m-auto mt-2"></m-svg-icon><divclass="text-xs text-white dark:text-zinc-300 scale-90 mt-0.5">点击更换头像</div></div></div><!-- 隐藏域 --><inputv-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-inputv-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-inputv-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-inputv-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-inputv-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-inputv-model="userInfo.introduction"class="w-full"type="textarea"max="50"></m-input></div><!-- 保存修改 --><m-buttonclass="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-buttonv-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-popupv-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// 展示 DialogisDialogVisible.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 = trueawait putProfile(userInfo.value)message('success', '用户信息修改成功')// 更新 vuexstore.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 () => {// 发送修改请求 代码省略// 更新 vuexstore.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"><divv-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"><divv-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%]"><!-- 标题 --><divclass="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 = falseif (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// 展示 DialogisDialogVisible.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-iconv-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-buttonclass="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:1aspectRatio: 1,// 裁剪框不可移动cropBoxMovable: false,// 不可调整裁剪框大小cropBoxResizable: false
}// PC 端配置对象
const pcOptions = {// 裁剪框固定纵横比:1:1aspectRatio: 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', '用户头像修改成功')// 关闭 loadingloading.value = false// 关闭 dialogclose()
}/*** 关闭事件*/
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:1aspectRatio: 1,// 裁剪框不可移动cropBoxMovable: false,// 不可调整裁剪框大小cropBoxResizable: false
}// PC 端配置对象
const pcOptions = {// 裁剪框固定纵横比:1:1aspectRatio: 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', '用户头像修改成功')// 关闭 loadingloading.value = false// 关闭 dialogclose()
}/*** 关闭事件*/
const close = () => {emits(EMITS_CLOSE)
}

13. 完成头像更新操作

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

14. 登录鉴权解决方案

// src/permission.jsimport 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)。走一遍完整的流程,这样大家会对这个操作更加的熟悉。 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机组成原理·考点知识点整理

根据往年考试题&#xff0c;对考点和知识点的一个整理。 校验编码 码距 一种编码的最小码距&#xff0c;其实就是指这种编码的码距。码距有两种定义&#xff1a; 码距所描述的对象含义 2 2 2 个特定的码其二进制表示中不同位的个数一种编码这种编码中任意 2 2 2 个合法编码的…

【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; 进程程序替换 1. 前言2. exec…

【JMeter接口自动化】第8讲 Fiddler抓包Jmeter

1&#xff09;配置好Fiddler 设置Fiddler-Tools-Options-HTTPS 设置Fiddler-Tools-Options-Connections&#xff0c;设置端口为8888 2&#xff09;查看IP 在CMD中输入ipconfig 查看IP地址 3&#xff09;配置Jmeter Http请求——基本&#xff0c;设置Http请求&#xff0c;使用…

北航数据结构与程序设计第四次作业选填题复习

首先都是线性的&#xff0c;线性包括顺序和链式&#xff0c;栈和队都可以用两种方式实现。栈只能存于栈顶取于栈顶&#xff0c;队列先进先出&#xff0c;因此存取点是固定的。 函数栈帧创建原理 画图即可。 A.显然不行&#xff0c;5如果第一个出来说明5是最后一个进的&#xf…

Lambda表达式与函数式工具在Python中的应用详解

目录 一、引言 二、Lambda表达式 Lambda表达式的定义 Lambda表达式的使用场景 Lambda表达式的示例 三、函数式工具 map()函数 filter()函数 reduce()函数 itertools模块 functools模块 四、Lambda表达式与函数式工具的结合使用 五、Lambda表达式与函数式工具的注意…

【云岚家政】-day00-开发环境配置

文章目录 1 开发工具版本2 IDEA环境配置2.1 编码配置2.2 自动导包设置2.3 提示忽略大小写2.4 设置 Java 编译级别 3 Maven环境3.1 安装Maven3.2 配置仓库3.3 IDEA中配置maven 4 配置虚拟机4.1 导入虚拟机4.2 问题 5 配置数据库环境5.1 启动mysql容器5.2 使用MySQL客户端连接数据…

Java Socket 网络编程实例(阻塞IO、非阻塞IO、多路复用Selector、AIO)

文章目录 1. 概述2. TCP 阻塞式IO 网络编程实例2.1 TCP网络编程服务端2.2 ByteBufferUtil2.3 客户端代码2.4 运行截图 3. TCP 非阻塞式IO 网络编程实例3.1 服务端3.2 客户端3.3 运行截图 4. 多路复用4.1 服务器端4.2 客户端4.3 运行截图 5. AIO5.1 AIO 服务端5.2 客户端5.3 运行…

C++笔试强训day39

目录 1.神奇的字母&#xff08;二&#xff09; 2.字符编码 3.最少的完全平方数 1.神奇的字母&#xff08;二&#xff09; 链接https://ac.nowcoder.com/acm/problem/205832 看输出描述即可知输出次数最多的那个字母即可。 哈希表直接秒了&#xff1a; #include <iostre…

一维时间序列突变检测方法(小波等,MATLAB R2021B)

信号的突变点检测问题是指在生产实践中&#xff0c;反映各种系统工作状态的信号&#xff0c;可能因为受到不同类型的噪声或外界干扰而发生了信号突变&#xff0c;导致严重失真的信号出现&#xff0c;因此必须探测突变出现的起点和终点。研究目的在于设计出检测方案&#xff0c;…

【python】python租房数据分析可视化(源码+数据+报告)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

OpenCV学习(4.3) 图像阈值

1.目的 在本教程中&#xff1a; 你会学到简单阈值法&#xff0c;自适应阈值法&#xff0c;以及 Otsu 阈值法(俗称大津法)等。你会学到如下函数&#xff1a;**cv.threshold&#xff0c;cv.adaptiveThreshold** 等。 2.简单阈值法 此方法是直截了当的。如果像素值大于阈值&am…

word2016版本中同时显示多个页面

为了方便查看word内容&#xff0c;我们会将多个页面同时显示。 对于2016版&#xff0c;操作方法如下&#xff1a; 视图 ---》多页

Jan任意文件读取/下载和上传漏洞

自从ChatGPT横空出世以来&#xff0c;我一直想找一个可以自己训练的AI大模型&#xff0c;然而在使用Jan的过程中&#xff0c;数据包中传递的参数引起了我的兴趣&#xff0c;简单尝试后发现了任意文件读取和任意文件上传漏洞。 简介 Jan是ChatGPT的开源替代品&#xff0c;它在…

vuInhub靶场实战系列--bulldog-1

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶场信息1.2 靶场配置 二、信息收集2.1 主机发现2.1.1 netdiscover2.1.2 nmap主机扫描2.1.3 arp-scan主机扫描 2.2 端口扫描…

友思特案例 | 自动快速定位:使用波长选择器测量滤光片的关键光学性能指标

导读 光学滤光片检测的手动调节校准的传统方法存在诸多不确定误差和高昂的成本消耗。友思特全自动可调谐光源检测解决方案&#xff0c;可全自动调节波长带宽&#xff0c;快速收集光谱数据&#xff0c;缩短检测时间、降低质检成本&#xff0c;实现极高的准确率和快速检测效率。…

RA8D1-Vision Board上OSPI-Flash实践

Vision-Board 开发板是 RT-Thread 推出基于瑞萨 Cortex-M85 架构 RA8D1 芯片,拥有Helium和TrustZone技术的加持,性能非常强大。 内核:480 MHz Arm Cortex-M85,包含Helium和TrustZone技术 存储:集成2MB/1MB闪存和1MB SRAM(包括TCM,512KB ECC保护) 外设:兼容xSPI的四线O…

如何在强数据一致性要求下设计数据库的高可用架构

在高可用的三大架构设计(基于数据层的高可用、基于业务层的高可用,以及融合的高可用架构设计)中。仅仅解决了业务连续性的问题:也就是当服务器因为各种原因,发生宕机,导致MySQL 数据库不可用之后,快速恢复业务。但对有状态的数据库服务来说,在一些核心业务系统中,比如…

运营商卷大模型,云厂商霸主地位不保?

文&#xff5c;艺 思 编&#xff5c;王一粟 经过了2023年的小试牛刀&#xff0c;2024年&#xff0c;三大运营商带着大模型一路狂飙。 刚刚过去的5月&#xff0c;中国电信、中国移动、中国联通三大运营商集体完成了新一轮的大模型进化&#xff0c;特别是围绕大模型的研发与…

04--Tomcat

前言&#xff1a;本章整理tomcat的知识点&#xff0c;tomcat知识点相较nginx比较少&#xff0c;但是也是运维必会的软件&#xff0c;这里结合实际项目整理一下。 1、tomcat简介 Tomcat 服务器是一个免费的开放源代码的Web 应用服务器&#xff0c;属于轻量级应用服务器&#x…

强烈安利10款手机App!

AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频https://aitools.jurilu.com/ 1.听书神器——昊昊听书 昊昊听书app是一款专门为用户提供有声读物的应用程序。它不仅提供了各种类型的有声书籍&#xff0c;还有各种知名的电…