大前端 - nodejs -egg实战 - web端(vue3.0)

项目介绍

请添加图片描述
npm i -g @vue/cli
vue create youtube-frontend

导入写好的前端模板页面

封装请求模块

配置接口服务地址:
.env.development

# npm run serve 加载的环境变量配置文件
VUE_APP_API_BASE_URL=http://127.0.0.1:7001/

.env.production

# npm run build 加载的这个配置文件
VUE_APP_API_BASE_URL=http://youtubeclone.lipengzhou.com/

api/request.ts

import axios from 'axios'
import { store } from '@/store'

// 创建axios实例
export const request = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL // 获取对应环境的服务地址。也就是:env.development的值
})

// 请求拦截器


// 响应拦截器

用户登录-封装请求接口

api/user.js

import { request } from '@/utils/request'

interface LoginInput {
  email: string
  password: string
}

export interface User {
  email: string
  token: string
  username: string
  channelDescription?: string
  avatar?: string
}

interface LoginPayload {
  user: User
}

export const login = (data: LoginInput) => {
  return request.post<LoginPayload>('/api/v1/users/login', data)
}

用户登录-基本流程

login/index.vue

<template>
  <div class="gspRov">
    <h2>Login to your account</h2>
    <form @submit.prevent="handleSubmit">
      <ul v-if="errors" class="errors">
        <li v-for="(error, index) in errors" :key="index">
          <!-- 显示错误消息 -->
          {{ `${error.field} ${error.message}` }}
        </li>
      </ul>
 
      <input v-model="user.email" type="email" placeholder="email" />
      <input v-model="user.password" type="password" placeholder="password" />
      <div class="action input-group">
        <span class="pointer">Signup instead</span>
        <button :disabled="isLoading">Login</button>
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import { login } from '@/api/user'
import { defineComponent, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStore } from '@/store'

const useLogin = () => {
// 在vue3.0的setup中没有this,因此this.$router.push()在3.0中不能使用。
// 需要这样写:useRouter(), 这样就可以得到路由实例了。
  const router = useRouter() // 获取router实例
  const store = useStore()
  const route = useRoute() // 获取route实例

  // reactive: 定义响应式数据
  const user = reactive({
    email: 'lpzmail@163.com',
    password: '123456'
  })

  // ref:定义errors变量而且是响应式的。ref定义的响应式数据需要在使用.value获取。
  const errors = ref([])
  const isLoading = ref(false)

  // 提交
  const handleSubmit = async () => {
    isLoading.value = true
    errors.value = []

   // try catch捕获异常
    try {
      // 发送请求
      const { data } = await login(user)
      router.push('/')
    } catch (err) {
      if (err.response.status === 422) {
        errors.value = err.response.data.detail
      }
    }
    isLoading.value = false
  }

  // 1.只有在这个地方return之后,才可以在template模版中使用。2.return中导出的数据在模版中可以直接使用。
  return {
    user,
    handleSubmit,
    errors,
    isLoading
  }
}
export default defineComponent({
  name: 'LoginIndex',
  setup () {
    return {
      ...useLogin()
    }
  }
})
</script>

配置Vuex中的State支持TS类型推断

store/index.ts

import { User } from '@/api/user'
import { InjectionKey } from 'vue'
import { createStore, Store, useStore as baseUseStore } from 'vuex'

// 声明 State 类型
export interface State {
  count: number
  user: User | null
}

// define injection key
export const key: InjectionKey<Store<State>> = Symbol()

export const store = createStore<State>({
  state: {
    count: 123,
    // 身份认证-存储用户登录信息
    user: JSON.parse(window.localStorage.getItem('user') || 'null')
  },
  // 修改state
  mutations: {
    setUser (state, user: User) {
      state.user = user
      // 身份认证-存储用户登录信息
      window.localStorage.setItem('user', JSON.stringify(state.user))
    }
  }
})

// define your own `useStore` composition function
// 不用每次使用的使用都传入key
export function useStore () {
  return baseUseStore(key)
}

main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
+ import { store, key } from './store'

createApp(App)
+  .use(store, key)
  .use(router)
  .mount('#app')

身份认证-统一添加token

import axios from 'axios'
import { store } from '@/store'

// 创建axios实例
export const request = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL // 获取对应环境的服务地址。也就是:env.development的值
})

// 请求拦截器
+ request.interceptors.request.use(config => {
  // config:本次的请求对象
  // 如果有user,则是登录状态
+  const { user } = store.state
+  if (user) {
    // 身份认证-统一添加token
+    config.headers.Authorization = `Bearer ${user.token}`
  }
+  return config
+ }, err => {
+  return Promise.reject(err)
+ })

// 响应拦截器

身份认证-处理页面访问权限

router/index.ts

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import AppLayout from '@/layout/AppLayout.vue'
import { store } from '@/store'

// 路由规则表
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    component: AppLayout,
    children: [
      {
        path: '', // 默认子路由
        name: 'home',
        component: () => import(/* webpackChunkName: "home" */ '@/views/home/index.vue')
      },
      {
        path: 'profile',
        name: 'profile',
        component: () => import(/* webpackChunkName: "profile" */ '@/views/profile/index.vue'),
        // 必须是登录了,才可以登录
+        meta: { requiresAuth: true }
      },
      {
        path: 'watch/:videoId',
        name: 'watch',
        component: () => import(/* webpackChunkName: "video" */ '@/views/watch/index.vue')
      }
    ]
  },
  {
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: "login" */ '@/views/login/index.vue')
  }
]

// 创建路由实例
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

+ // 每当请求进来,就会经过这个路由拦截先处理。
+ // 请求拦截,如果没有登录,则不允许访问前端路由。
+ // meta.requiresAuth为true需要拦截是否登录状态
router.beforeEach((to, from, next) => {
  const { user } = store.state
+  if (to.matched.some(record => record.meta.requiresAuth)) {
    // this route requires auth, check if logged in
    // if not, redirect to login page.
 +   if (!user) {
 +     next({
 +       path: '/login',
 +       query: { redirect: to.fullPath }
 +     })
 +   } else {
      next()
    }
  } else {
    next() // 确保一定要调用 next()
  }
})

export default router

login/index.vue

<template>
  <div class="gspRov">
    <h2>Login to your account</h2>
    <form @submit.prevent="handleSubmit">
      <ul v-if="errors" class="errors">
        <li v-for="(error, index) in errors" :key="index">
          <!-- 显示错误消息 -->
          {{ `${error.field} ${error.message}` }}
        </li>
      </ul>
 
      <input v-model="user.email" type="email" placeholder="email" />
      <input v-model="user.password" type="password" placeholder="password" />
      <div class="action input-group">
        <span class="pointer">Signup instead</span>
        <button :disabled="isLoading">Login</button>
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import { login } from '@/api/user'
import { defineComponent, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStore } from '@/store'

const useLogin = () => {
// 在vue3.0的setup中没有this,因此this.$router.push()在3.0中不能使用。
// 需要这样写:useRouter(), 这样就可以得到路由实例了。
  const router = useRouter() // 获取router实例
  const store = useStore()
  const route = useRoute() // 获取route实例

  // reactive: 定义响应式数据
  const user = reactive({
    email: 'lpzmail@163.com',
    password: '123456'
  })

  // ref:定义errors变量而且是响应式的。ref定义的响应式数据需要在使用.value获取。
  const errors = ref([])
  const isLoading = ref(false)

  // 提交
  const handleSubmit = async () => {
    isLoading.value = true
    errors.value = []

   // try catch捕获异常
    try {
      // 发送请求
      const { data } = await login(user)    
      // 接口返回的数据data存储到store中
  +    store.commit('setUser', data.user)
  +    const redirect = (route.query.redirect || '/') as string // as string强制转换为字符串。
      router.push(redirect)
    } catch (err) {
      if (err.response.status === 422) {
        errors.value = err.response.data.detail
      }
    }
    isLoading.value = false
  }

  // 1.只有在这个地方return之后,才可以在template模版中使用。2.return中导出的数据在模版中可以直接使用。
  return {
    user,
    handleSubmit,
    errors,
    isLoading
  }
}
export default defineComponent({
  name: 'LoginIndex',
  setup () {
    return {
      ...useLogin()
    }
  }
})
</script>

身份认证-处理头部内容展示状态

可以直接在模版中这样使用store:$store.state.user

app/loayout/AppHeader.vue

<template>
  <div class="sc-AxhUy JixXX">
    <div class="logo flex-row">
      <span><a href="/">YouTube Clone</a></span>
    </div>
    <div class="sc-AxjAm iVYHdX">
      <input class="search" type="text" placeholder="Search" value="" />
    </div>
    <ul>
 +     <template v-if="$store.state.user">
        <li>
          <div>
            <label for="video-upload" @click="isUploadShow = true">
              <svg
                viewBox="0 0 24 24"
                preserveAspectRatio="xMidYMid meet"
                height="27"
                width="27"
                fill="#FFF"
                focusable="false"
              >
                <g>
                  <path
                    d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4zM14 13h-3v3H9v-3H6v-2h3V8h2v3h3v2z"
                  ></path>
                </g>
              </svg>
            </label>
          </div>
        </li>
        <li>
          <svg
            viewBox="0 0 24 24"
            preserveAspectRatio="xMidYMid meet"
            focusable="false"
            height="27"
            fill="#FFF"
            width="27"
          >
            <g>
              <path
                d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"
              ></path>
            </g>
          </svg>
        </li>
        <li>
          <a href="/feed/my_videos">
            <img
              class="sc-AxhCb eSwYtm pointer"
              src="https://res.cloudinary.com/douy56nkf/image/upload/v1594060920/defaults/txxeacnh3vanuhsemfc8.png"
              alt="user-avatar"
          /></a>
        </li>
      </template>

+      <template v-else>
        <li>
          <a href="">
            <span>SIGN IN</span>
          </a>
        </li>
        <li>
          <a href="">
            <span>SIGN UP</span>
          </a>
        </li>
      </template>
    </ul>
  </div>

  <upload-video v-if="isUploadShow" @close="isUploadShow = false"/>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import UploadVideo from '@/components/UploadVideo/index.vue'

export default defineComponent({
  name: 'AppHeader',
  components: {
    UploadVideo
  },
  setup () {
   // 初始化变量:isUploadShow, ref设置响应式数据
    const isUploadShow = ref(false)
    return {
      isUploadShow
    }
  }
})
</script>

创建视频-预览视频

component/UploadViode/index.vue

<template>
  <div class="sc-AxiKw dZbDOR">
    <div class="modal-content">
      <form @submit.prevent="handleSubmit">
        <div class="modal-header">
          <div class="modal-header-left">
            <svg
              viewBox="0 0 24 24"
              preserveAspectRatio="xMidYMid meet"
              focusable="false"
              @click="handleClose"
            >
              <g>
                <path
                  d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
                ></path>
              </g>
            </svg>
            <h3>Upload Video</h3>
          </div>
          <div style="display: block">
            <button class="sc-AxirZ erzyjX">Save</button>
          </div>
        </div>
        <div class="tab video-form">
          <input ref="file" required type="file" @change="handleFileChange" />
          // 预览视频
          <video ref="videoEl" controls></video>
          <input v-model="video.title" required type="text" placeholder="Enter the title" />
          <textarea
            v-model="video.description"
            required
            placeholder="Tell viewers about your video"
          ></textarea>
        </div>
      </form>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
import { createUploadVideo, refreshUploadVideo } from '@/api/vod'
import { createVideo } from '@/api/video'
import router from '@/router'
import { useRouter } from 'vue-router'

export default defineComponent({
  name: 'UploadVideo',
  setup (props, context) {
    const router = useRouter()
    // 上传的video
+    const file = ref(null)
    
    // 预览的video
+    const videoEl = ref(null)
    // 关闭弹出框
    const handleClose = () => {
      // 对外发布一个自定义事件
      context.emit('close')
    }
    // 上传文件
+    const handleFileChange = () => {
+      const fileObj = (file.value as any).files[0];
      // (videoEl.value as any): videoEl.value转换为any类型
+     (videoEl.value as any).src = URL.createObjectURL(fileObj)
    }
    return {
      handleClose,
      file,
+     handleFileChange,
    }
  }
})
</script>

创建视频-视频上传-初始化上传SDK + 视频上传完成 + 保存成功

public/index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <link rel="stylesheet" href="/main.css">
+    <script src="/aliyun-upload-sdk-1.5.0/lib/es6-promise.min.js"></script>
+    <script src="/aliyun-upload-sdk-1.5.0/lib/aliyun-oss-sdk-5.3.1.min.js"></script>
+    <script src="/aliyun-upload-sdk-1.5.0/aliyun-upload-sdk-1.5.0.min.js"></script>
+    <link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.9.3/skins/default/aliplayer-min.css" />
+    <script src="https://g.alicdn.com/de/prismplayer/2.9.3/aliplayer-h5-min.js"></script>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

component/UploadViode/index.vue

<template>
  <div class="sc-AxiKw dZbDOR">
    <div class="modal-content">
      <form @submit.prevent="handleSubmit">
        <div class="modal-header">
          <div class="modal-header-left">
            <svg
              viewBox="0 0 24 24"
              preserveAspectRatio="xMidYMid meet"
              focusable="false"
              @click="handleClose"
            >
              <g>
                <path
                  d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
                ></path>
              </g>
            </svg>
            <h3>Upload Video</h3>
          </div>
          <div style="display: block">
            <button class="sc-AxirZ erzyjX">Save</button>
          </div>
        </div>
        <div class="tab video-form">
          <input ref="file" required type="file" @change="handleFileChange" />
          <video ref="videoEl" controls></video>
          <input v-model="video.title" required type="text" placeholder="Enter the title" />
          <textarea
            v-model="video.description"
            required
            placeholder="Tell viewers about your video"
          ></textarea>
        </div>
      </form>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
import { createUploadVideo, refreshUploadVideo } from '@/api/vod'
import { createVideo } from '@/api/video'
import router from '@/router'
import { useRouter } from 'vue-router'

export default defineComponent({
  name: 'UploadVideo',
  setup (props, context) {
    const router = useRouter()
    // 上传的video
    const file = ref(null)
    
    // 预览的video
    const videoEl = ref(null)
    
    const video = reactive({
      title: '',
      description: '',
      vodVideoId: ''
    })
    // 关闭弹出框
    const handleClose = () => {
      // 对外发布一个自定义事件
      context.emit('close')
    }

    // 上传文件
    const handleFileChange = () => {
      const fileObj = (file.value as any).files[0];
      // (videoEl.value as any): videoEl.value转换为any类型
      (videoEl.value as any).src = URL.createObjectURL(fileObj)
    }

    // 创建上传的实例
    const createUploader = () => {
      const uploader = new window.AliyunUpload.Vod({
        // 阿里账号ID,必须有值
        userId: '122',
        // 分片大小默认1 MB,不能小于100 KB
        partSize: 1048576,
        // 并行上传分片个数,默认5
        parallel: 5,
        // 网络原因失败时,重新上传次数,默认为3
        retryCount: 3,
        // 网络原因失败时,重新上传间隔时间,默认为2秒
        retryDuration: 2,
        // 是否上报上传日志到视频点播,默认为true
        enableUploadProgress: true,
        // 开始上传
        onUploadstarted: async function (uploadInfo: any) {
          console.log('onUploadstarted', uploadInfo)
          // 上传方式1,需要根据uploadInfo.videoId是否有值,调用视频点播的不同接口获取uploadauth和uploadAddress,如果videoId有值,调用刷新视频上传凭证接口,否则调用创建视频上传凭证接口
          if (uploadInfo.videoId) {
            // 如果uploadInfo.videoId存在,调用刷新视频上传凭证接口
            const { data } = await refreshUploadVideo(uploadInfo.videoId)
            uploader.setUploadAuthAndAddress(uploadInfo, data.UploadAuth, data.UploadAddress, data.VideoId)
          } else {
            // 如果uploadInfo.videoId不存在,调用获取视频上传地址和凭证接口
            // 从视频点播服务获取的uploadAuth、uploadAddress和videoId,设置到SDK里
            //  uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress,videoId);
            const { data } = await createUploadVideo({
              Title: uploadInfo.file.name,
              FileName: uploadInfo.file.name
            })
            uploader.setUploadAuthAndAddress(uploadInfo, data.UploadAuth, data.UploadAddress, data.VideoId)
          }
        },
        // 文件上传成功
        onUploadSucceed: async function (uploadInfo: any) {
          console.log('onUploadSucceed', uploadInfo)
          video.vodVideoId = uploadInfo.videoId
          // 提交给后台保存数据
          const { data } = await createVideo(video)
          console.log('保存成功', data)
          router.push({
            name: 'watch',
            params: {
              videoId: data.video._id
            }
          })
          context.emit('close')
        },
        // 文件上传失败
        onUploadFailed: function (uploadInfo: any, code: any, message: any) {
          console.log('onUploadFailed', uploadInfo, code, message)
        },
        // 文件上传进度,单位:字节
        onUploadProgress: function (uploadInfo: any, totalSize: any, loadedPercent: any) {
          console.log('onUploadProgress', `${Math.ceil(loadedPercent * 100)}%`)
        },
        // 上传凭证超时
        onUploadTokenExpired: async function (uploadInfo: any) {
          // console.log('onUploadTokenExpired', uploadInfo)
          // 实现时,根据uploadInfo.videoId调用刷新视频上传凭证接口重新获取UploadAuth
          // 从点播服务刷新的uploadAuth,设置到SDK里
          const { data } = await refreshUploadVideo(uploadInfo.videoId)

          uploader.resumeUploadWithAuth(data.UploadAuth)
        },
        // 全部文件上传结束
        onUploadEnd: function (uploadInfo: any) {
          console.log('onUploadEnd', uploadInfo)
        }
      })
      return uploader
    }

    // 提交
    const handleSubmit = async () => {
      // 获取 uploader 上传实例
      const uploader = createUploader()
      // 添加上传文件
      const paramData = JSON.stringify({ Vod: {} })
      uploader.addFile((file.value as any).files[0], null, null, null, paramData)
      // 开始上传
      uploader.startUpload()
      // 上传完成 -> 创建视频
    }
    return {
      handleClose,
      file,
      videoEl,
      handleFileChange,
      handleSubmit,
      video
    }
  }
})
</script>

api/video.ts

import { request } from '@/utils/request'

interface CreateVideoInput {
  title: string
  description: string
  vodVideoId: string
}

interface VideoAuth {
  _id: string
  username: string
  avatar: string
  isSubscribed: boolean
  subscribersCount: number
}

export interface Video {
  _id: string
  title: string
  description: string
  vodVideoId: string
  commentsCount: number
  createdAt: string
  dislikesCount: number
  likesCount: number
  isLiked: boolean
  isDisliked: boolean
  viewsCount: number
  user: VideoAuth
}

interface VideoPayload {
  video: Video
}

export const createVideo = (data: CreateVideoInput) => {
  return request.post<VideoPayload>('/api/v1/videos', data)
}

export const getVideo = (videoId: string) => {
  return request.get<VideoPayload>(`/api/v1/videos/${videoId}`)
}

api/vod.ts

import { request } from '@/utils/request'

interface CreateUploadVideoParams {
  Title: string
  FileName: string
}

interface CreateUploadVideoPayload {
  RequestId: string
  VideoId: string
  UploadAddress: string
  UploadAuth: string
}

export const createUploadVideo = (params: CreateUploadVideoParams) => {
  return request.get<CreateUploadVideoPayload>('/api/v1/vod/CreateUploadVideo', {
    params
  })
}

export const refreshUploadVideo = (videoId: string) => {
  return request.get<CreateUploadVideoPayload>('/api/v1/vod/RefreshUploadVideo', {
    params: {
      videoId
    }
  })
}

interface VideoMeta {
  CoverURL: string
  Duration: number
  Status: string
  Title: string
  VideoId: string
}

export interface VideoPlayAuthPayload {
  RequestId: string
  PlayAuth: string
  VideoMeta: VideoMeta
}

export const getVideoPlayAuth = (vodVideoId: string) => {
  return request.get<VideoPlayAuthPayload>('/api/v1/vod/GetVideoPlayAuth', {
    params: {
      VideoId: vodVideoId
    }
  })
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值