1、上传组件需求分析
我们还需要新建和展示文章,新建文章自然是发送post请求,同时在post中自带对应的数据,展示文章就是根据id取出已有的数据并且展示出来。
这里有一个难点就是上传组件,上传文件是App应用中最基本的需求,在Ajax出现之前,我们一般都使用一个简单的input框来实现这个上传的流程,把它的type设置成file,把from提交以后都交给后端处理。
自从有了ajax出现了以后,JS有了异步发送请求的能力,我们可以更方便的在页面不跳转的情况下完成文件的上传,同时还能获得很好的可视化效果,比如说传的百分比、图片的预览、最终的状态等等
整个流程,一开始我们应该有能力让用户去检查这个图片的格式或者大小的,我们会提供一个属性叫beforeUpload,它是一个function,让用户去检查这个文件的一些具体的需求;然后通过以后,这个时候这个组件就会向外反射一个uploading这样一个事件;然后上传成功以后就会触发filrUploaded这个事件,或者出现错误就会触发uploadedError这个事件。这些事件对于用户来说非常重要,他们很需要知道这些事情是什么样的时间发生的,然后可以做取得额外的工作,比如说beforeUploaded我们进行上传前的检查如文件类型/大小等等,或者在最终上传失败的时候我们可以弹出一些额外信息等等,这些都是事件给我们带来的魔力,除了这些事件以外,自定义也是我们的一大看点,我们的用户应该可以使用template来自定义一些显示的内容,比如说我们一开始上传区域长成什么样可以是一大片也可以是一个button等,上传中显示什么图标什么文字,上传完毕以后要显示什么样的数据,这些我们应该是可以完全自定义的
通过流程这么分析,我们大致得出组件大致应该有些什么,如下
它应该有一个action,代表着我们要把这个请求发送到后端哪里去处理;然后应该有一个function即beforeUploaded去完成一些上传前的校验;之后就是3个要触发的事件,uploading(点击上传按钮后)、fileUploaded(上传成功后)、uploadedError(上传失败后);除了这些我们还要支持自定义模板,默认就是一开始它长什么样,我们这里是一个button,然后我们可以添加这个template,这个template分为uploaded的即上传前的模板和loading的即上传中的模板
发送异步请求是上传组件的一个核心内容,那么接下来我们就谈谈使用axios来发送异步请求怎样完成文件的上传这个流程
2、上传文件的两种实现方式
接下来我们来了解上传文件的原理
我们先从form提交的时候谈起,既然要上传文件,当然就要选择文件,那么就可以选用如下
<input type="file">的标签,这时候它就会渲染出一个可以选择文件的对话框;那么当文件选择完毕以后,我们有两种方式上传,一个是使用传统的form submit即表单提交的方式;第二种是使用JavaScript发送异步请求的方式,这是我们着重要实现和理解的方法;
(1)如下我们了解一下第一种使用传统的form submit即表单提交的方式的流程
如下例子,我们选择文件然后点击submit的时候它就会运行form的默认行为,带input当中的数据,然后直接发送到一个特定的HTTP request请求到axios 的url,我们这里axios是/api/upload即会把input中的数据post到后端API为upload的地方中去,然后server端接到这个请求以后会做对应的处理,并且返回结果,这是一种典型的request到server的形式。这里要特别注意,我们这个表单一般于发送普通消息的表单,不同的地方就在于我们发送了文件,文件和普通的字符串不同,文件属于一种二进制的格式,所以我们需要设置这个enctype="multipart/form-data",注意你要上传文件或者你表单发送的有二进制的文件,那么就最好设置这种enctype="multipart/form-data"格式,因为表单的默认格式不支持二进制数据,所以需要设置成支持二进制数据的
我们选择了文件后,点击submit可以看到后端拿到这个内容以后就会做出对应的响应,它会返回如下图片地址
我们的接口文档中也提供了对应的接口
(2)使用JavaScript做file类型的表单上传功能
了解了这些之后,我们就可以使用JavaScript来发送异步上传文件的post请求了,提交form其实也就是发送http请求,那么使用JavaScript发送异步当然有更好的体验了,其实它的过程也是万变不离其宗的,也是用它来模拟表单的发送http请求
我们来新建文章页面来尝试一下,如下
然后我们尝试一下上传功能,如下我们点击上传某图片后,上传成功后它就打印出上传成功的信息
从上面可以看到,使用JavaScript发送异步请求的方式和form提交的过程是非常相似的,只不过是用javascript模拟原生form的提交方式
3、Uploader组件 第一部分 -- 上传组件流程
就和上面一样,完成那个流程即可
(1)首先创建一个按钮,使得点击这个按钮就执行file类型的input的点击事件即弹出选择文件的弹窗
如下,创建一个上传组件叫Uploader.vue
复习一下:还记得怎么在setup中拿到一个dom节点吗,通过ref咋拿来着,如下首先在标签里通过加上ref="fileInput",然后const fileInput = ref<null | HTMLInputElement>(null),然后直接通过fileInput.value即可拿到这个fileInputDOM节点了
form那个方式是有一个input框,然后右边有一个submit按钮,我们想有一个按钮,然后点击这个按钮就触发file类型的input框,即想只展现出一个按钮,这个按钮的点击事件中就触发这个file input即form上传功能。
怎么做,我们就如下设置一个button,也设置一个file类型的input,但是让这个input隐藏即可,然后button的点击事件中去获取这个input的DOM节点,拿到input节点后接着去触发input的点击事件,即form中的那个submit的点击事件(这个点击事件就会去让你挑选文件这样子)。这样就实现了页面中展示一个点击上传的按钮,然后你点击这个按钮,就会自动去触发类型为file的input的点击事件,从而就会展示出让你选择文件的弹窗
接下来我们来添加属性
我们要接收一个action,这个action是接收发送请求的地址,而且是必选项是必须填的
(2)接下来就是上面说的经典的上传过程
这里有个问题就是我们要根据不同的上传阶段,展示不同的元素,所以我们需要有一个字段来指示一下状态如下,然后我们就开始做状态变化后触发的函数即handleFileChange函数
在这个函数中,我们做的就是如下获取input这个DOM节点,这是为了我们在选择文件的时候去获取这个input的files属性,因为我们选择文件后这个input会带上files属性,这个属性中就是我们选择的那些文件们。我们获取到我们选择的文件后,首先去修改上传的状态,如下
这个e.target即事件对象就是这个input
我们选择文件后,这个input就会加上这个files属性,这个属性即currenTarget.files打印出来可以看到就是我们选择的那些文件,是一个列表list
然后继续,获取到我们选择的文件后,我们把这个files变成一个Array,因为这个files是一个list并不是一个Array,我们用Array.form()来转换成Array,是为了后面取它第一个。
然后创建一个form表单数据的属性,怎么创建就通过new FormData() 从而创建一个Form表单数据属性;然后把files的第一个数据给新建的这个FormData;FormData表单中有数据了,那么就可以进行post请求了,这个post请求中要传三个参数,一个是url,一个是传的数据,一个是headers(要写成如下这个才可以接收二进制文件);最终处理请求的结果,通过.then() 接请求成功的结果然后进行如下处理,通过.catch() 接请求失败的结果然后进行如下处理,最后那个finally是无论成功还是失败都进行的函数,因为我们选择了文件了嘛,所以input中的value被这个文件占了,你得先清空,下次再选择时才不会出错
(3)整个上传流程总结:
我们的template中有button和file类型的input(给隐藏input),然后我们是通过点击这个button去触发该input的click点击事件,这个点击事件就会弹出让你选择文件的弹窗。
然后此时input在变化即change,说明此时在上传文件,那么我们就给个change事件,这个事件是上传的重点
在这个change事件中,我们要做的流程就是:获取上传的文件-->建表单数据-->发起请求,把选择的文件传给对应API
首先,获取这个input这个dom节点(因为选择文件后input会增加一个files属性,这个属性中就是我们选择上传的文件列表),获取这个input就是为了获取我们选择上传的文件
然后,拿到要上传的文件后,新建一个表单数据属性,并且给这个属性files的值(因为post请求中要提交你选择的文件嘛)
①把这个files列表变成Array即数组(这是为了获取列表的第一个文件,我们只能选择一个文件)
②新建一个form表单数据属性,通过new FormData()
③把files中第一个文件数据给新建的这个FormData
最后,发起post请求,处理请求可能的请求成功、请求失败的处理
至此文件已经可以成功上传
过程中有一个小错误:
这里有一个要注意的点,就是发现在setup中说取不到props,要注意,setup(){}的括号中要写上props才能取到props的值,有时候不小心漏了,setup函数,它接收两个参数,一个是props,一个是context
4、Uploader组件 第二部分 -- 不同阶段自定义操作
上面我们完成了上传组件的最基本的流程,现在我们要将这里面的功能慢慢丰富进去
第一大功能就是在不同阶段暴露出一系列的事件,对于用户想在不同阶段进行自定义操作的话就很有用;第二个是自定义模板,用户可以根据需求渲染自己想要的页面
(1)在上传组件前,我们想自定义一个函数在上传之前去检查,比如上传前检查图片是不是JGP格式,是JPG格式才能上传
在上传前我们想要有一些检查的流程即beforeUpload函数,这个检查是怎么检查我们想要自定义式,怎么才能自定义呢,就你使用的地方定这个检查逻辑,在这个上传组件中只调用这个函数即可。
这个函数中应该接收用户选择的文件,然后经过一些列检查,最终返回布尔值,布尔值代表着检查的结果
如下在Uploader组件中
如下在Home.vue中,我们创建自定义检查的这个beforeUpload函数,并且传给uploader组件
即可如下,点击上传比如png的图片,就会出现如下提示
(2)上传成功后和上传失败后,我们想在这两个时候做一些自定义的事情,则如下
如下,定义emits,emits中有file-uploaded表示上传成功时调的函数、file-uploaded-error表示上传失败时调的函数,然后在上传的post请求成功时通过context.emit()去调用对应的函数
从接口文档中,我们可以看到返回的数据都是code、msg、data这样的,所以我们到store中定义一个通用的格式,这样就能享受到TypeScript类型的帮助,如下
然后我们在Home组件中定义这个上传成功的函数以及上传失败的函数,如下
5、Uploader 组件第三部分 -- 自定义模板
(1)使用slot来做自定义模板
我们在Dropdown组件中我们已经见识过自定义模板,它是使用这个slot来完成这个对应功能的,让我们来复习一下,我们使用name slot 来完成不同自定义模板的需求,我们在组件中添加slot,并且加上对应的name属性,然后在使用的时候我们可以使用template标签配合v-slot:name属性来使用,这里面就是你自定义的HTML内容,v-slot也可以简写成一个#
现在要展示3个阶段的界面,所以自然要展示三个slot来放置对应的自定义模板,所以如下我们来修改一下Uploader
如下,把正在上传、上传成功、点击上传都做成一个slot插槽,然后默认没什么的时候它们就是一个个按钮
然后如果这里面插入了东西,比如插入一段<h2>,那么就会替换这个插槽,但是这个h2标题还是有这个button这个按钮功能,如下
插槽名为default时即展示h2的点击上传文本
插槽名为loading时,则展示这个旋转图标
上传完毕后,我们需要在父组件Home.vue中拿到子组件Uploader.vue的一些数据,比如我们上传完毕后想展示出这个图片啥的,为了让组件在slot中访问子组件的某些特性,vue提供我们Scoped Slots就是为了让我们解决这个问题的
(2)使用scoped slot 来解决子组件传值给父组件的问题
看文档,如下,子组件slot标签中可以通过v-bind将一个属性绑定到这个slot上面去,然后我们就可以给父组件中对应标签中加上v-slot="xxx",然后通过xxx.这个属性即可拿到这个属性
如下,在Uploader组件中,我们新建一个ref属性记录请求成功时返回的数据,然后我们在slot中通过v-bind把这个数据传出去,如下
然后到Home组件中使用,如下用v-slot接传过来的值,把传过来的值取名叫dataProps,然后在img中使用如下
如下,上传成功后,图片就展示出来了,这样就实现了在slot中父组件使用子组件的数据的问题
6、改进路由验证系统
接下来我们将上传组件添加到新建文章页面中,但是现在路由跳转还有一些问题,我们先处理这个。
我们刚开始的路由验证流程是如下这样
但是我们发现了有一些问题,就是我们在下拉菜单组件点击新建文章按钮,可以正常跳转到新建文章页面,但是如果此时我们在新建文章页面刷新就会发现会跳转到登录页面,但是我们明明已经登录了呀
这是因为路由中我们还是使用了store.state.user.isLogin来判断的,而点击刷新后store中的数据是都重置回初始值的。所以你登录后isLogin确实从false置为true了,但是你刷新后isLogin就又是为false了,那么就符合这个if判断了,所以就重定向到了登录页面去给你了
但是其实你此时是已经登录状态,而你用store.state.user.isLogin判断就给你错判成了未登录状态,所以首页登录过以后再点击新建文章通过这个链接进行跳转的话就不会有这个问题,这是我们要解决的问题,这里涉及稍微复杂的流程
所以我们要改进一下路由验证系统,让它更完善一些
我们做个流程图,其实我们多的逻辑就是在发送请求这一步,其他的没有什么区别,只不过现在的逻辑比以前复杂了一些
如下,一开始我们判断这个user.isLogin,
如果已登录是true,则判断redirectAlreadyLogin这个参数存不存在,存在则跳转到首页,不存在则继续进行;
如果未登录是flase,那么我们就去判断是否有token,
如果token不存在,则去判断它访问的这个路由是不是需要登录才能访问的,如果不是则继续进行即让他去这个路由的页面,如果是需要登录才能访问的则跳转到登录页面让他先登录;
(前面的和我们之前的一样,我们新添加的逻辑就在这里)如果token存在,也就是说user的isLogin为false但是有token的情况,那么有可能是像上面这种刚刚刷新了isLogin为false但是其实是已登录的状态,也可能是有token但是是之前登录的现在确实还没有登录,有token就说明这个用户刚才或者之前登录过,所以就直接重新发送一次异步fetchCurrentUser请求去登录(这也是常见的,短时间内会自动登录的嘛);然后就如果登录失败就弹出提示回到登录页面让重新登录,如果登录成功则判断是不是要到登录或注册页面,如果是则重定向回首页,如果不是比如是要到新建文章页面那么直接next()即去新建文章页面反正此时都登录了