vite介绍
vite为什么快
webpack 在开发时构建时,默认会去 抓取并构建你的整个应用,然后才能提供服务 ,这就导致你的项目中,存在的任何一个错误(哪怕这个错误是在用户从来都没有进入过的页面中出现的),它依然会影响到你的整个项目构建。
也正是因为这个原因,当你的项目越大时,构建的时间就会越长 ,你的项目启动速度也就会越慢。
vite 不会在一开始就构建你的整个项目,而是会将应用中的模块区分为 依赖 和 源码(项目代码) 两部分,对于 源码 部分,它会根据 路由来拆分 代码模块,只会去构建一开始就必须要构建的内容。
同时 vite 以 原生 ESM 的方式为浏览器提供源码,让浏览器接管了 打包 的部分工作。
vite存在的问题
vite 以 原生 ESM 的方式为浏览器提供源码,让浏览器接管了打包的部分工作,所以它无法打包commonjs模块。
解决方案:依赖于构建
vite 提供了 依赖预构建 的功能,会先将 CommonJS 或 UMD 发布的依赖项转换为 ESM 之后,再重新进行编译。这也可以理解为 速度对业务的一个妥协。
tailwindcss介绍与为什么使用tailwindcss
什么是tailwindcss
tailwindcss预设了大量的css 类名,每一个类名都代表了一个 css 属性 ,这个样式在 tailwindcss 中被称作为 原子化 CSS,我们只需书写 HTML 代码,无需书写 CSS,就可以构建网站。
tailwindcss原理
Tailwind CSS 的工作原理是扫描所有 HTML 文件、JavaScript 组件以及任何 模板中的 CSS 类(class)名,然后生成相应的样式代码并写入 到一个静态 CSS 文件中。
为什么使用tailwindcss
为什么在这个项目中使用它,而不是在vue模板中写css,或者使用类似element的ui框架?
- 这个项目是一个前台系统,前台系统讲究 高定制化、高个性化、高交互性, 类似element的ui框架风格统一,无法满足我们的需求。
- 不使用框架,意味着我们需要写大量的css。而tailwindcss一个类名就代表一个样式,相对来说,可以让我们少写很多css,并且没有取大量类名的心智负担。
总结:在 高定制化、高个性化、高交互性 的前台应用中, 原子化 CSS 是最合适的一种形式。而这种形式的具体体现就是 tailwindcss
项目架构和路由分析
项目架构
├── src
│ ├── App.vue // 项目根组件,一级路由出口
│ ├── api // 接口请求
│ ├── assets // 静态资源
│ │ ├── icons // svg icon 图标
│ │ ├── images // image 图标。比如:xxx.png
│ │ └── logo.png // logo
│ ├── components // 通用的业务组件。比如:一个组件在多个页面中使用到
│ ├── constants // 常量
│ ├── directives // 自定义指令
│ ├── libs // 通用组件,可用于构建中台物料库或通用组件库
│ ├── main.js // 入口文件
│ ├── permission.js // 页面权限控制中心
│ ├── router // 路由
│ │ ├── index.js // 路由处理中心
│ │ └── modules // 路由模块
│ │ ├── mobile-routes.js // 移动端路由
│ │ └── pc-routes.js // PC 端路由
│ ├── store // 全局状态
│ │ ├── getters.js // 全局状态访问处理
│ │ ├── index.js // 全局状态中心
│ │ └── modules // 状态子模块
│ ├── styles // 全局样式
│ │ └── index.scss // 全局通用的样式处理
│ ├── utils // 工具模块
│ ├── vendor // 外部供应资源。比如:人类行为认证
│ └── views // 页面组件。与 components 的区别在于:此处组件对应路由表,以页面的形式展示
│ └── layout // 用于 PC 端,分割一级路由和二级路由
│ ├── components // 该页面组件下的业务组件
│ └── index.vue // layout 组件
├── tailwind.config.js // wailwind css 配置文件,与 src 平级
└── vite.config.js // vite 配置文件,与 src 平级
此项目为什么要生成移动端和pc端两份路由表
因为移动端和pc端的页面交互不同,在此项目移动端中,没有嵌套路由概念,只需要一个出口,而pc端有嵌套路由,需要两个出口。所以根据当前设备不同,构建不同的路由表。
如何判断当前设备是移动端还是pc端
一般可以根据navigator.userAgent字段判断
export const isMobileTerminal = computed(() => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
})
vite几个常用配置
1. 如何在vite中定义@软链接
vite 提供了 resolve.alias 功能,表示:通过别名在指向一个具体的路径
import { join } from 'path'
export default defineConfig({
plugins: [vue()],
// 软链接
resolve: {
alias: {
'@': join(__dirname, '/src')
}
}
})
2. 如何定义配置代理服务
vite有一个和webpack差不多的功能,配置方式也一样
// 代理
server: {
proxy: {
// 代理所有 /api 的请求,该求情将被代理到 target 中
'/api': {
// 代理请求之后的请求地址
target: 'https://api.imooc-front.lgdsunday.club/',
// 跨域
changeOrigin: true
}
}
}
3. vite配置全局变量
只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理
# base api, 只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理
VITE_BASE_API = '/api'
使用的时候,用import.meta.env.VITE_XX
如何动态根据屏幕大小动态设置rem
- 在文档解析完以后
DOMContentLoaded,获取屏幕大小window.innerWidth。 - 把屏幕大小除以一个固定值,这里我们取10。
- 设置的html元素上
export const useREM = () => {
// 定义最大的 fontSize
const MAX_FONT_SIZE = 40
// 监听 html 文档被解析完成的事件
document.addEventListener('DOMContentLoaded', () => {
// 获取 html 标签
const html = document.querySelector('html')
// 获取根元素 fontSize 标准,屏幕宽度 / 10。(以 Iphone 为例 Iphone 6 屏幕宽度为 375,则标准 fontSize 为 37.5)
let fontSize = window.innerWidth / 10
// 获取到的 fontSize 不允许超过我们定义的最大值
fontSize = fontSize > MAX_FONT_SIZE ? MAX_FONT_SIZE : fontSize
// 定义根元素(html)fontSize 的大小 (rem)
html.style.fontSize = fontSize + 'px'
})
}
移动端 slider 处理
如上图,对于图中高亮滑块的处理,当点击切换标签的时候,有三个重点功能需要做:
- 如何算出高亮区的宽度:只需要能拿到当前被点击的元素,通过
getBoundingClientRect函数可以拿到。 - 如何知道定位到哪里:就是要拿到滑块的left偏移量,可以通过获得ul滚动的像素 + 当前元素距离屏幕的偏移量left。
- 滑动动画效果:通过css3属性
animation-duration做到
具体做法
- 给所有滑块添加点击事件,添加字段
currentCategoryIndex记录当前是第几个标签被点击了 - 监听
currentCategoryIndex字段,当currentCategoryIndex变化的时候,通过ref的方式获得当前被点击的元素。 - 通过
getBoundingClientRect函数,得出当前被点击元素的width,即滑块的width,并且得到距离屏幕的位移left,再加上外层ul的滚动距离,即为滑块的偏移量
核心代码
// 滑块初始化宽度和位移量
const sliderStyle = ref({
transform: 'translateX(0px)',
width: '60px'
})
// 选中的 item 下标
const currentCategoryIndex = ref(0)
// 获取填充的所有 item 元素
let itemRefs = []
const setItemRef = (el) => {
if (el) {
itemRefs.push(el)
}
}
onBeforeUpdate(() => {
itemRefs = []
})
// 获取 ul 元素,以计算偏移位置
const ulTarget = ref(null)
const { x: ulScrollLeft } = useScroll(ulTarget)
watch(currentCategoryIndex, (val) => {
// 获取选中元素的 left、width
const { left, width } = itemRefs[val].getBoundingClientRect()
// 为 sliderStyle 设置属性
sliderStyle.value = {
// ul 横向滚动位置 + 当前元素的 left 偏移量
transform: `translateX(${ulScrollLeft.value + left - 10 + 'px'})`,
width: width + 'px'
}
})
// item 点击事件
const onItemClick = (index) => {
currentCategoryIndex.value = index
}
如何处理导航标签刷新闪动的问题
原因
每一次刷新页面,一开始都是没有数据的,需要从后台获取数据,而数据还没有返回来的时间内,就是空白,所以会闪动
解决方案
先在代码中写死一段假数据,作为后台还没返回数据的填充。然后等后台返回了真实数据后,替换掉这些假数据
import { ALL_CATEGORY_ITEM, CATEGORY_NOMAR_DATA } from '@/constants'
export default {
state: () => ({
// 初始的数据
categorys: CATEGORY_NOMAR_DATA
}),
mutations: {
/**
* 为 categorys 赋值
*/
setCategorys(state, categorys) {
// 接口返回后替换掉初始化数据
state.categorys = [ALL_CATEGORY_ITEM, ...categorys]
}
},
tailwindcss如何进行主题替换
主题替换的本质是css样式的改变,如何设计一套方案,去管理css,让css在变更主题的时候改变。
tailwindcss的主题替换功能使用方法
- 给根节点
html指定一个主题样式类名,如:dark - 给所有需要根据主题改变样式的元素,都根据主题写多个主题样式,如默认白色写
light:bg-white,深色dark:bg-black
实现思路
- 监听主题的切换行为
- 根据行为保存当前展示的主题到
vuex中 - 根据
vuex中保存的当前主题,展示header -> theme的显示图标 - 根据
vuex中保存的当前主题,修改html的class
核心代码
监听主题变量改变后,实时改变替换掉根元素html的主题class
import store from '@/store'
import { watch } from 'vue'
import { THEME_LIGHT, THEME_DARK } from '@/constants'
/**
* 初始化主题
*/
export default () => {
watch(
() => store.getters.themeType,
(val) => {
// html 的 class
let themeClassName = ''
switch (val) {
case THEME_LIGHT:
themeClassName = 'light'
break
case THEME_DARK:
themeClassName = 'dark'
break
}
// 修改 html 的 class
document.querySelector('html').className = themeClassName
},
{
// 初始执行一次
immediate: true
}
)
}
如何根据系统(浏览器)主题改变页面主题
前置知识
Window.matchMedia()
该方法返回一个新的 MediaQueryList 对象,表示指定的媒体查询 (en-US)字符串解析后的结果。返回的 MediaQueryList 可被用于判定 Document 是否匹配媒体查询,或者监控一个 document 来判定它匹配了或者停止匹配了此媒体查询。
prefers-color-scheme
CSS 媒体特性用于检测用户是否有将系统的主题色设置为亮色或者暗色。
实现思路 通过Window.matchMedia()这个方法可以监听到当前系统是深色主题还是浅色主题,在用户选择跟随系统系统后,监听并获取系统当前的主题,然后改变样式。
/**
* 监听系统主题变更
*/
let matchMedia
const watchSystemThemeChange = () => {
// 仅需初始化一次即可
if (matchMedia) return
matchMedia = window.matchMedia('(prefers-color-scheme: dark)')
// 监听主题变更
matchMedia.onchange = function () {
changeTheme(THEME_SYSTEM)
}
}
/**
* 变更主题
* @param {*} theme 主题的标记常量
*/
const changeTheme = (theme) => {
// html 的 class
let themeClassName = ''
switch (theme) {
case THEME_LIGHT:
themeClassName = 'light'
break
case THEME_DARK:
themeClassName = 'dark'
break
case THEME_SYSTEM:
watchSystemThemeChange()
themeClassName = matchMedia.matches ? 'dark' : 'light'
break
}
// 修改 html 的 class
document.querySelector('html').className = themeClassName
}
/**
* 初始化主题
*/
export default () => {
watch(() => store.getters.themeType, changeTheme, {
// 初始执行一次
immediate: true
})
}
- 每看完一章,就写笔记作总结,不要等到最后来做
- 在看视频过程中,想到什么问题就记下
- 要写代码实现项目
第三方用户反馈平台介绍
为什么要用第三方用户反馈平台,而不是公司自己开发?
因为这个功能非常雷同,不需要定制化,而且自己公司开发起来成本较高。所以可以直接用第三方的平台来处理。
用第三方平台的用户反馈平台有什么缺陷?
数据是存储在第三方的,不是在自己公司的,相对来说不那么安全。
所以一般中小型公司都是用第三方用户反馈平台。
当前国内可用的平台有:兔小巢
使用过程
- 登录该平台,创建项目,获得兔小巢反馈地址。
- 在项目中跳转链接中加入改地址即可
// 兔小巢反馈地址
export const FEEDBACK_URL = 'https://support.qq.com/product/xxxx(产品id)'
/**
* 反馈处理
*/
const onToFeedback = () => {
window.open(FEEDBACK_URL, '_blank')
}
第三方分享功能介绍
对于网站而言,不支持微信聊天分享,微信朋友圈分享。 微信只支持app,企业号,公众号,服务号的分享。
微信分享的本质 把一段文字或者图片,发送到聊天或者朋友圈的过程。
而我们做的分享功能,只不过节约了复制文字图片步骤,跳转到微信app界面步骤的过程。
微博分享实现
微博分享的本质是,把一段预先设定好的文字或者图片,通过window.open打开新窗口传参的方式,复制到微博发送动态的页面。
在实现微博分享前,先要去微博开发平台做相关的注册和认证,这里不做介绍。
具体代码实现
- 在index.html导入微博sdk
<!-- 微博分享 -->
<script src="https://tjs.sjs.sinajs.cn/open/api/js/wb.js" type="text/javascript" charset="utf-8"></script>
- 创建分享模块
import { WEI_BO_APP_KEY, WEI_BO_UID } from '@/constants'
/**
* 微博分享
* @param {*} imgUrl 分享的图片 URL
* @param {*} path 网页链接
*/
export const weiboShare = (imgUrl, path) => {
window.open(
`https://service.weibo.com/share/share.php?appkey=${WEI_BO_APP_KEY}&ralateUid=${WEI_BO_UID}&pic=${imgUrl}&title=这张图不错哦,给大家分享一下 ${path}`,
'_blank'
)
}
- 点击分享按钮事件,加入对应参数
/**
* 分享按钮点击处理
*/
const onShareClick = () => {
weiboShare(
props.data.photo,
`https://xxx/${props.data.id}`
)
}
vite中指定自动注册功能实现
思路: 先通过vite的s.html#glob-import这个同步导入函数,将所有指定文件导入,然后for循环注册。
/**
* 全局指令注册
*/
export default {
async install(app) {
// https://cn.vitejs.dev/guide/features.html#glob-import
// import.meta.globEager 为同步导入
const directives = import.meta.globEager('./modules/*.js')
for (const [key, value] of Object.entries(directives)) {
// 拼接组件注册的 name
const arr = key.split('/')
const directiveName = arr[arr.length - 1].replace('.js', '')
// 完成注册
app.directive(directiveName, value.default)
}
}
}
高亮词语如何做
这种高亮其中某几个字符,应该如何做?通常可以用正则匹配,然后通过v-html插入
代码示范:
<div
v-html="highlightText(text)"
></div>
/**
* 处理关键字高亮
*/
const highlightText = (text) => {
// 生成高亮标签
const highlightStr = `<span class="text-zinc-900 dark:text-zinc-200">${props.searchText}</span>`
// 构建正则表达式,从《显示文本中》找出与《用户输入文本相同的内容》,使用《高亮标签》进行替换
const reg = new RegExp(props.searchText, 'gi')
// 替换
return text.replace(reg, highlightStr)
}
基于vee-validata的表单校验
vue项目如果没有引入任何UI框架,或者UI框架没有表单校验功能,那么如何快速实现表单校验业务,这里推荐vee-validata插件,专门为vue项目做表单校验的插件。
实现表单校验要用到三个组件:
Form:表单Field:输入框ErrorMessage:错误提示
基础使用方式如下
<template>
<div id="app">
<Form @submit="onSubmit">
<Field name="email" type="email" :rules="validateEmail" />
<ErrorMessage name="email" />
<button>Sign up</button>
</Form>
</div>
</template>
<script>
import { Form, Field, ErrorMessage } from 'vee-validate';
export default {
components: {
Form,
Field,
ErrorMessage,
},
methods: {
onSubmit(values) {
console.log(values, null, 2);
},
validateEmail(value) {
// if the field is empty
if (!value) {
return 'This field is required';
}
// if the field is not a valid email
const regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
if (!regex.test(value)) {
return 'This field must be a valid email';
}
// All is good
return true;
},
},
};
</script>
人类行为验证如何实现
许多网站,在登录的时候,都有这样的校验,这种校验叫做人类行为验证,判断当前操作页面的是人还是机器人。
为什么要进行人类行为验证
主要是为了防止网站被机器恶意操作。例如,我们写了一篇博客,如何防止被机器刷浏览量,刷评论,则就要进行人类行为验证,保证每一个账号都是真正的人登录的。
判断原理
参考网易易盾的回答
dun.163.com/news/p/d1ab…
人机验证通过对用户的行为数据、设备特征与网络数据构建多维度数据分析, 采用完整的可信前端安全方案保证数据采集的真实性、有效性。比如以下方面(但不仅仅限于):
(1)浏览器特征检查:所有浏览器都有差异,可以通过各种前端相关手段检查浏览器环境的真实性。
(2)鼠标事件(click、move、hover、leave……)
(3)页面窗口(size、scroll、坐标……)
(4)cookie,等等。
开源插件SliderCaptcha实现人机验证
对于开源免费的人机验证插件,可以选择SliderCaptcha插件。
这个插件前端做的事情是把鼠标的行为轨迹记录下来,然后发送给服务端,由服务端判断是否是人在操作鼠标还是机器在操作鼠标。
代码示范
<div id="captcha"></div>
<div id="captcha"></div>
<script>
sliderCaptcha({
id: 'captcha'
});
sliderCaptcha({
id: 'captcha',
width: 280,
height: 150,
sliderL: 42,
sliderR: 9,
offset: 5,
loadingText: '正在加载中...',
failedText: '再试一次',
barText: '向右滑动填充拼图',
repeatIcon: 'fa fa-redo',
setSrc: function () {
},
onSuccess: function () {
},
onFail: function () {
},
onRefresh: function () {
}
});
</script>
高阶路由过渡处理方案
vue-router 提供的 过渡动效,是从一个路由页面跳转到另一个路由页面,是整个页面的过渡动画,但如果我们想要只有页面内的部分内容在切换页面的时候有过渡动画,则vue-router提供的过渡动效做不到。所以我们要自己实现。
需求
图一
图二
如果我们希望点击图一中的图片时候,有一个从当前点击放大的效果,进入到图二,应该如何做呢。此时这种效果无法用vue-router的过渡动效来做到。
实现思路
由于不能用vue-router的过渡动效来实现,而我们又没有办法给vue-router自定义特别的过渡动效,所以这个跳转页面功能我们不能用vue-router来实现。 我们需要解决两个问题
- 这个动效本身如何实现
- 如何做到点击图片改变url,但不跳转路由,而是以我们自己手动用代码去展示一个组件。
这个动效本身如何实现
动效本身可以用GSAP库实现
如何改变url却不vue-router监听到不跳转路由
用History.pushState() 方法。
代码实现
动画起点的位置在哪里,可以通过计算每一边距离页面上下左右的距离算出来。动画的终点位置则是页面的每个边。
/**
* pins 跳转处理,记录图片的中心点(X|Y位置 + 宽|高的一半)
*/
const {
x: imgContainerX,
y: imgContainerY,
width: imgContainerWidth,
height: imgContainerHeight
} = useElementBounding(imgTarget)
const imgContainerCenter = computed(() => {
return {
translateX: parseInt(imgContainerX.value + imgContainerWidth.value / 2),
translateY: parseInt(imgContainerY.value + imgContainerHeight.value / 2)
}
})
动画用Asap动画库实现,只要设定好初始位置和最终位置就行。
<!-- 大图详情处理 -->
<transition
:css="false"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<pins-vue v-if="isVisiblePins" :id="currentPins.id" />
</transition>
import gsap from 'gsap'
const beforeEnter = (el) => {
gsap.set(el, {
scaleX: 0,
scaleY: 0,
transformOrigin: '0 0',
translateX: currentPins.value.localtion?.translateX,
translateY: currentPins.value.localtion?.translateY,
opacity: 0
})
}
const enter = (el, done) => {
gsap.to(el, {
duration: 0.3,
scaleX: 1,
scaleY: 1,
opacity: 1,
translateX: 0,
translateY: 0,
onComplete: done
})
}
const leave = (el, done) => {
gsap.to(el, {
duration: 0.3,
scaleX: 0,
scaleY: 0,
x: currentPins.value.localtion?.translateX,
y: currentPins.value.localtion?.translateY,
opacity: 0
})
}
在点击的时候触发跳转路由和组件显示
/**
* 进入 pins
*/
const onToPins = (item) => {
history.pushState(null, null, `/#/pins/${item.id}`)
isVisiblePins.value = true
currentPins.value = item
}
const isVisiblePins = ref(false)
const currentPins = ref({})
图片裁剪cropperjs的使用
cropperjs 是一个 JavaScript 的库,同时支持 PC 端 和 移动端
基本使用方法
- 需要一个img标签,给img标签指定id,方便js找到
- new一个cropper对象,并且传入对应配置
<!-- Wrap the image or canvas element with a block element (container) -->
<div>
<img id="image" src="picture.jpg">
</div>
<style>
/* Make sure the size of the image fits perfectly into the container */
img {
display: block;
/* This rule is very important, please don't ignore this */
max-width: 100%;
}
</style>
// import 'cropperjs/dist/cropper.css';
import Cropper from 'cropperjs';
const image = document.getElementById('image');
const cropper = new Cropper(image, {
aspectRatio: 16 / 9,
crop(event) {
console.log(event.detail.x);
console.log(event.detail.y);
console.log(event.detail.width);
console.log(event.detail.height);
console.log(event.detail.rotate);
console.log(event.detail.scaleX);
console.log(event.detail.scaleY);
},
});
注意:裁剪完后,拿到的是一个bolb对象,,我们可以通过URL.createObjectURL方法把bolb对象转化成可以在浏览器上显示的blob地址
// 获取裁剪后的图片
cropper.getCroppedCanvas().toBlob((blob) => {
// 裁剪后的 blob 地址
console.log(URL.createObjectURL(blob))
})
上传图片到阿里云对象存储需要怎样配置
一共有三个步骤
- 安装
ali-oss依赖 - 通过接口获取临时访问凭证,生成
OSS实例 - 利用
ossClient.put方法,完成对应上传
需要填写的配置如下:
region:yourRegion填写Bucket所在地域。accessKeyId:访问密钥idaccessKeySecret:访问密钥stsToken:安全令牌bucket:桶名称refreshSTSToken:刷新 token的回调refreshSTSTokenInterval: 刷新token 的间隔时间
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
})
}
利用 ossClient.put 方法,完成对应上传
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)
// TODO:图片上传成功
} catch (e) {
message('error', e)
}
}
上传的图片是一个blob对象,图片上传成功后会返回url