fileUploaded,uploadedError
9-2上传文件的两种方式
action="/api/upload"
input type="file"name=""
const handleFileChange=(e:Event)=>{
const target=e.targte ass HTMLInputElement
const files=target.fiiles
if(files){
const uploadFile=files[0]
const formData=new FormData()
forData.append(uploadedFile.name,uploadedFile);
axios.port('/upload',formData,{headers:{'Content-Type':'multipart/form-dat'}
9-3Uploader组件
《button class="btb bt-primary"
<template>
<div class="file-upload">
<div class="file-upload-container" @click.prevent="triggerUpload" v-bind="$attrs">
<slot v-if="fileStatus === 'loading'" name="loading">
<button class="btn btn-primary" disabled>正在上传...</button>
</slot>
<slot v-else-if="fileStatus === 'success'" name="uploaded" :uploadedData="uploadedData">
<button class="btn btn-primary">上传成功</button>
</slot>
<slot v-else name="default">
<button class="btn btn-primary">点击上传</button>
</slot>
</div>
<input
type="file"
class="file-input d-none"
ref="fileInput"
@change="handleFileChange"
>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, PropType, watch } from 'vue'
import axios from 'axios'
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
type CheckFunction = (file: File) => boolean;
export default defineComponent({
props: {
action: {
type: String,
required: true
},
beforeUpload: {
type: Function as PropType<CheckFunction>
},
uploaded: {
type: Object
}
},
inheritAttrs: false,
emits: ['file-uploaded', 'file-uploaded-error'],
setup(props, context) {
const fileInput = ref<null | HTMLInputElement>(null)
console.log(props.uploaded)
const fileStatus = ref<UploadStatus>(props.uploaded ? 'success' : 'ready')
const uploadedData = ref(props.uploaded)
watch(() => props.uploaded, (newValue) => {
if (newValue) {
fileStatus.value = 'success'
uploadedData.value = newValue
}
})
const triggerUpload = () => {
if (fileInput.value) {
fileInput.value.click()
}
}
const handleFileChange = (e: Event) => {
const currentTarget = e.target as HTMLInputElement
if (currentTarget.files) {
const files = Array.from(currentTarget.files)
if (props.beforeUpload) {
const result = props.beforeUpload(files[0])
if (!result) {
return
}
}
fileStatus.value = 'loading'
const formData = new FormData()
formData.append('file', files[0])
axios.post(props.action, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(resp => {
fileStatus.value = 'success'
uploadedData.value = resp.data
context.emit('file-uploaded', resp.data)
}).catch((error) => {
fileStatus.value = 'error'
context.emit('file-uploaded-error', { error })
}).finally(() => {
if (fileInput.value) {
fileInput.value.value = ''
}
})
}
}
return {
fileInput,
triggerUpload,
fileStatus,
uploadedData,
handleFileChange
}
}
})
</script>
type CheckFunction=(fikle:file)=>boolean;
context.emit('file-uploaded',resp.data);
context.emit
9-5-自定义模板
scope slots
import { createRouter, createWebHistory } from 'vue-router'
import axios from 'axios'
import Home from './views/Home.vue'
import Login from './views/Login.vue'
import Signup from './views/Signup.vue'
import ColumnDetail from './views/ColumnDetail.vue'
import CreatePost from './views/CreatePost.vue'
import PostDetail from './views/PostDetail.vue'
import store from './store'
const routerHistory = createWebHistory()
const router = createRouter({
history: routerHistory,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/login',
name: 'login',
component: Login,
meta: { redirectAlreadyLogin: true }
},
{
path: '/signup',
name: 'signup',
component: Signup,
meta: { redirectAlreadyLogin: true }
},
{
path: '/create',
name: 'create',
component: CreatePost,
meta: { requiredLogin: true }
},
{
path: '/column/:id',
name: 'column',
component: ColumnDetail
},
{
path: '/posts/:id',
name: 'post',
component: PostDetail
}
]
})
router.beforeEach((to, from, next) => {
const { user, token } = store.state
const { requiredLogin, redirectAlreadyLogin } = to.meta
if (!user.isLogin) {
if (token) {
axios.defaults.headers.common.Authorization = `Bearer ${token}`
store.dispatch('fetchCurrentUser').then(() => {
if (redirectAlreadyLogin) {
next('/')
} else {
next()
}
}).catch(e => {
console.error(e)
store.commit('logout')
next('login')
})
} else {
if (requiredLogin) {
next('login')
} else {
next()
}
}
} else {
if (redirectAlreadyLogin) {
next('/')
} else {
next()
}
}
})
export default router
9-8创建文章最后流程
import { ColumnProps, ImageProps, UserProps } from './store'
export function generateFitUrl(data: ImageProps, width: number, height: number, format = ['m_pad']) {
if (data && data.url) {
const formatStr = format.reduce((prev, current) => {
return current + ',' + prev
}, '')
data.fitUrl = data.url + `?x-oss-process=image/resize,${formatStr}h_${height},w_${width}`
}
}
export function addColumnAvatar(data: ColumnProps | UserProps, width: number, height: number) {
if (data.avatar) {
generateFitUrl(data.avatar, width, height)
} else {
const parseCol = data as ColumnProps
data.avatar = {
fitUrl: require(parseCol.title ? '@/assets/column.jpg' : '@/assets/avatar.jpg')
}
}
}
interface CheckCondition {
format?: string[];
size?: number;
}
type ErrorType = 'size' | 'format' | null
export function beforeUploadCheck(file: File, condition: CheckCondition) {
const { format, size } = condition
const isValidFormat = format ? format.includes(file.type) : true
const isValidSize = size ? (file.size / 1024 / 1024 < size) : true
let error: ErrorType = null
if (!isValidFormat) {
error = 'format'
}
if (!isValidSize) {
error = 'size'
}
return {
passed: isValidFormat && isValidSize,
error
}
}
interface TestProps {
_id: string;
name: string;
}
const testData: TestProps[] = [{ _id: '1', name: 'a' }, { _id: '2', name: 'b' }]
export const arrToObj = <T extends { _id?: string }>(arr: Array<T>) => {
return arr.reduce((prev, current) => {
if (current._id) {
prev[current._id] = current
}
return prev
}, {} as { [key: string]: T })
}
const result = arrToObj(testData)
console.log(result)
export const objToArr = <T>(obj: {[key: string]: T}) => {
return Object.keys(obj).map(key => obj[key])
}
const testData2: {[key: string]: TestProps} = {
1: { _id: '1', name: 'a' },
2: { _id: '2', name: 'b' }
}
const result2 = objToArr(testData2)
console.log(result2)
function beforeUploadCheck(file:Filemcondition:)
interface ChekcCondition{
fomat?/:string[];
size?:number;}
export function beforeUploadCheck(file:File,confition:CheckCondition){
const{format,szi}=condition
const isValidFormat=format?format.include(file.type):true
ctreaePost({commit},payload){
store.ispatch('createPost',newPost).tehn(()=>{
createMessage('
9-9作业完成详情页
<template>
<div class="post-detail-page">
<modal title="删除文章" :visible="modalIsVisible"
@modal-on-close="modalIsVisible = false"
@modal-on-confirm="hideAndDelete"
>
<p>确定要删除这篇文章吗?</p>
</modal>
<article class="w-75 mx-auto mb-5 pb-3" v-if="currentPost">
<img :src="currentImageUrl" alt="currentPost.title" class="rounded-lg img-fluid my-4" v-if="currentImageUrl">
<h2 class="mb-4">{{currentPost.title}}</h2>
<div class="user-profile-component border-top border-bottom py-3 mb-5 align-items-center row g-0">
<div class="col">
<user-profile :user="currentPost.author" v-if="typeof currentPost.author === 'object'"></user-profile>
</div>
<span class="text-muted col text-right font-italic">发表于:{{currentPost.createdAt}}</span>
</div>
<div v-html="currentHTML"></div>
<div v-if="showEditArea" class="btn-group mt-5">
<router-link
type="button"
class="btn btn-success"
:to="{name: 'create', query: { id: currentPost._id}}"
>
编辑
</router-link>
<button type="button" class="btn btn-danger" @click.prevent="modalIsVisible = true">删除</button>
</div>
</article>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, computed, ref } from 'vue'
import { marked } from 'marked'
import { useStore } from 'vuex'
import { useRoute, useRouter } from 'vue-router'
import { GlobalDataProps, PostProps, ImageProps, UserProps, ResponseType } from '../store'
import UserProfile from '../components/UserProfile.vue'
import Modal from '../components/Modal.vue'
import createMessage from '../components/createMessage'
export default defineComponent({
name: 'post-detail',
components: {
UserProfile,
Modal
},
setup() {
const store = useStore<GlobalDataProps>()
const route = useRoute()
const router = useRouter()
const modalIsVisible = ref(false)
const currentId = route.params.id
onMounted(() => {
store.dispatch('fetchPost', currentId)
})
const currentPost = computed<PostProps>(() => store.getters.getCurrentPost(currentId))
const currentHTML = computed(() => {
if (currentPost.value && currentPost.value.content) {
const { isHTML, content } = currentPost.value
return isHTML ? content : marked.parse(content)
} else {
return ''
}
})
const showEditArea = computed(() => {
const { isLogin, _id } = store.state.user
if (currentPost.value && currentPost.value.author && isLogin) {
const postAuthor = currentPost.value.author as UserProps
return postAuthor._id === _id
} else {
return false
}
})
const currentImageUrl = computed(() => {
if (currentPost.value && currentPost.value.image) {
const { image } = currentPost.value
return (image as ImageProps).url + '?x-oss-process=image/resize,w_850'
} else {
return null
}
})
const hideAndDelete = () => {
modalIsVisible.value = false
store.dispatch('deletePost', currentId).then((rawData: ResponseType<PostProps>) => {
createMessage('删除成功,2秒后跳转到专栏首页', 'success', 2000)
setTimeout(() => {
router.push({ name: 'column', params: { id: rawData.data.column } })
}, 2000)
})
}
return {
currentPost,
currentImageUrl,
currentHTML,
showEditArea,
modalIsVisible,
hideAndDelete
}
}
})
</script>
第9章 道高一尺 - 上传组件
#9-1 上传组件需求分析
流程图
组件设计
<uploader
action="https://upload-me"
beforeUpload=""
@uploading=""
@fileUploaded=""
@uploadedError=""
>
<Button />
<template #uploaded>
</template>
<template #loading>
</template>
</>
#9-2 上传文件的两种实现方式
- 一种是使用 传统的 form submission,表单提交的方式
- 第二种是使用 Javascript 发送异步请求的方式
传统表单模式
<form method="post" encType="multipart/form-data" action="https://jsonplaceholder.typicode.com/posts">
<input type="file" />
<button type="submit">Submit</button>
</form>
关于 POST encType 的描述:developer.mozilla.org/zh-CN/docs/…
- application/x-www-form-urlencoded: 数据被编码成以 '&' 分隔的键-值对, 同时以 '=' 分隔键和值. 非字母或数字的字符会被 percent-encoding: 这也就是为什么这种类型不支持二进制数据(应使用 multipart/form-data 代替).
- multipart/form-data
- text/plain
使用异步请求上传文件
// 示例代码
const handleFileChange = (e: Event) => {
// 在这个 input 组件中,我们可以拿到它选择的 file 对象
const target = e.target as HTMLInputElement
const files = target.files
// 注意这个是一个 files 列表,也就是 fileList 对象,它是一个 array-like 的 object,但是不是一个数组,它支持选择多个文件,所以它可能有多个
if (files) {
// 我们拿到它的第一项,就是我们选择的文件
const uploadedFile = files[0]
// 然后让我们来模拟表单的数据我们可以使用 FormData 对象,这是另一种针对 XHR2 设计的新数据类型。使用 FormData 能够很方便地实时以 JavaScript 创建 HTML <form>。
// 文档 https://developer.mozilla.org/zh-CN/docs/Web/API/FormData
const formData = new FormData()
// 并通过调用 append 方法为其附加了 <input> 值。
// 就这样我们通过 FormData 对象添加了 input 的值
formData.append(uploadedFile.name, uploadedFile)
// 现在有了表单数据,我们可以发送 post 请求了,注意 axios post 的第二个参数,即支持普通的 object,也支持 formData。在这里我们需要添加一个额外的 header,就是Content-type,这个对应的表单的 encType,为了传文件,我们修改成 mutilpart
axios.post("/upload", formData , {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(resp => {
console.log(resp)
})
}
}
#9-3 至 9-5 Uploader 组件编码
代码提交地址 9-3:git.imooc.com/coding-449/…
9-4:git.imooc.com/coding-449/…
9-5:git.imooc.com/coding-449/…
vue 关于 scoped slot 的描述:vuejs.org/guide/compo…
#9-6 改进路由验证系统
流程图
代码提交:git.imooc.com/coding-449/…
#9-7 创建文章页面实现 Uploader 自定义样式
Object-fit 来限制图片的展示:developer.mozilla.org/zh-CN/docs/…
代码提交:git.imooc.com/coding-449/…
#9-9 作业 完成文章详情页
使用 Markdown-it 来完成从 markdown 到 HTML 转换:github.com/markdown-it…
代码提交详情:git.imooc.com/coding-449/…