目錄
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)。走一遍完整的流程,這樣大家會對這個操作更加的熟悉。?