指尖上的记忆指尖上的记忆
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub

nuxt3下封装公共http请求:
在实际使用nuxt3请求接口的时候,我默认使用的是 useFetch 请求接口,但是有一种情况会失败,就是在created的时候调用的接口, 我后来分析了一下,useFetch其实是以恶搞client端的请求方式,可能在created的时候,页面还没有渲染完全,所以会报错,后来 找到了三种方法解决这个问题,下面一一介绍(这个问题和之前的 解决Nuxt项目中发生在服务端的请求丢失Cookie的问题 一样):

1.改用 $fetch请求接口  
使用$fetch请求接口可以解决上面的问题,但是也会产生另外一个问题,就是$fetch会在client和server端都发起请求,导致接口多次调用
  
2.使用setTimeout延迟,这个方法也可以解决问题,但是延迟多少是个问题  
setTimeout(function (){
       this.getLearnProgress()
 }.bind(this), 200)
  
3.使用 await nextTick() 可以完美的解决问题,对于需要登陆的接口,甚至不用手动在header里传递cookie信息  

实际项目中使用:

//[id].vue

<template>
  <!--  course-detail-banner-->
  <div
    class="banner-wrapper fix-p-top"
    v-if="$screen.higherThan('xl', $screen.current.value)"
  >
    <img
      class="banner-img"
      :src="resourceUrl + '/' + courseDetail.coverPicture"
      alt=""
    />
    <div class="container banner-cont">
      <CourseNav
        :navs="{ tag: courseDetail.tag }"
        v-if="courseDetail.id > 0"
      ></CourseNav>
      <div class="course-info">
        <div class="course-title">
          <p class="title white-space">{{ courseDetail.title }}</p>
        </div>
        <div class="course-statics">
          <ul class="resources clear">
            <li>{{ courseStatistics.videoCount }} Videos</li>
            <li>{{ courseStatistics.articleCount }} Article</li>
            <li>{{ courseStatistics.estimatedLearningTime }}</li>
          </ul>
          <ul class="users">
            <li>
              <img src="~/assets/img/group.png" alt="" /><span
                >{{ courseStatistics.userCount }} People joined</span
              >
            </li>
            <li v-if="courseDetail.isNeedCertification">
              <img src="~/assets/img/card_membership.png" alt="" /><span
                >With certification earned</span
              >
            </li>
          </ul>
        </div>
        <div class="join-action">
          <Button
            @click="joinTheCourse"
            rounded="none"
            customColor="#000"
            customBgColor="#01F0E0"
            :active="false"
            variant="quaternary"
            class="btn-join-style btn"
            >{{ isJoin ? 'Enter' : 'Join the course' }}
          </Button>
        </div>
      </div>
    </div>
  </div>
  <!-- 手机端页面 -->
  <div class="banner-wrapper" v-else>
    <img
      class="banner-img"
      :src="resourceUrl + '/' + courseDetail.coverPicture"
      alt=""
    />
    <div class="container banner-cont p-[20px]">
      <CourseNav
        :navs="{ tag: courseDetail.tag }"
        v-if="courseDetail.id > 0"
      ></CourseNav>
      <div class="course-info" style="padding-left: 0">
        <div class="course-title" style="font-size: 18px; margin-top: 20px">
          <p class="title white-space" style="font-size: 24px">
            {{ courseDetail.title }}
          </p>
        </div>
        <div class="course-statics">
          <ul class="resources clear">
            <li>{{ courseStatistics.videoCount }} Videos</li>
            <li>{{ courseStatistics.articleCount }} Article</li>
            <li>{{ courseStatistics.estimatedLearningTime }}</li>
          </ul>
          <ul class="users">
            <li>
              <img src="~/assets/img/group.png" alt="" /><span
                >{{ courseStatistics.userCount }} People joined</span
              >
            </li>
            <li v-if="courseDetail.isNeedCertification">
              <img src="~/assets/img/card_membership.png" alt="" /><span
                >With certification earned</span
              >
            </li>
          </ul>
        </div>
        <div class="join-action" style="width: fit-content">
          <Button
            @click="joinTheCourse"
            rounded="none"
            customColor="#000"
            customBgColor="#01F0E0"
            :active="false"
            variant="quaternary"
            class="btn-join-style btn"
            >{{ isJoin ? 'Enter' : 'Join the course' }}
          </Button>
        </div>
      </div>
    </div>
  </div>
  <!--  course-detail-content-->
  <div class="container">
    <div class="content-box">
      <div class="course-detail">
        <p class="title phone-title relin-paragraph-target">
          Information for Editors
        </p>
        <div class="phone-text" :class="{ 'more-phone-text': exp }">
          <div class="detail" v-html="courseDetail.description"></div>
        </div>
        <div
          v-if="!$screen.higherThan('xl', $screen.current.value) && !exp"
          class="text-[.875rem] text-[#337AB7] pt-[.625rem]"
          @click="readMore"
        >
          Read more
        </div>
        <div
          v-if="!$screen.higherThan('xl', $screen.current.value) && exp"
          class="text-[.875rem] text-[#337AB7] pt-[.625rem]"
          @click="fold"
        >
          Fold
        </div>
      </div>
      <div class="course-lesson course-phone-lesson">
        <p class="title relin-paragraph-targe">Course outline</p>
        <div class="lesson-box">
          <div class="lesson-info">
            <p class="num">{{ courseStatistics.lessonCount }} Chapters</p>
            <p class="times">{{ courseStatistics.estimatedLearningTime }}</p>
          </div>
          <div class="lesson-list" v-if="courseManageList.length">
            <NotificationModal ref="notify"></NotificationModal>
            <ul>
              <li
                v-for="(item, index) in courseManageList"
                :key="index"
                @click="learnTheCourse(item.id, item.type)"
              >
                <a>
                  <img
                    src="~/assets/img/play_circle.svg"
                    alt=""
                    v-if="item.type === 'video'"
                  />
                  <img
                    src="~/assets/img/article_circle.svg"
                    alt=""
                    v-else-if="item.type === 'article'"
                  />
                  <img src="~/assets/img/quiz_circle.svg" alt="" v-else />
                  <span class="title three-white-space">{{ item.title }}</span>
                </a>
                <span class="times">{{ item.length }}</span>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  </div>

  <!--  action box-->
  <div class="action-wrapper" v-if="courseDetail.id">
    <ShareAndThumb
      :url="resourceUrl + $route.fullPath"
      :title="courseDetail.title"
    ></ShareAndThumb>
  </div>

 <!--  toast -->
<!--  <ToastMessage ref="toastMessage"></ToastMessage>-->
  <ToastMessage v-model:modelValue="isSHow" :toastObj="toastObj"></ToastMessage>
</template>

<script setup lang="ts">
import {onMounted, ref} from 'vue'
import {useAwesomeScreen} from "../../composables/use-awesome-screen";
import {
  definePageMeta,
  useRuntimeConfig,
  useRoute,
  createFetchOptions,
  useRouter,
} from "../../.nuxt/imports";

definePageMeta({
  middleware: ['param-number', 'course-auth']
})

const $screen = useAwesomeScreen()

const exp = ref<boolean>(false)
const readMore = () => {
  exp.value = true
}
const fold = () => {
  exp.value = false
}

const isSHow = ref<boolean>(false)
const toastObj = ref<MessageObj>({variant:'', message:''})
//逻辑
import _ from 'lodash'
import utils from '../../utils/utils'
import { MButton as Button } from '@mdpi-ui/design-system'
import {CourseDetail} from "../../composables/CourseDetail";
import {CourseStatistics} from "../../composables/CourseStatistics";
import {ApiResponse} from "../../composables/ApiResponse";
import {FetchOptions} from "../../composables/FetchOptions";
import {JoinCourse, UserCourse} from "../../composables/UserCourse";
import {MessageObj} from "../../composables/MessageObj";
import {RequestBody} from "../../composables/RequestBody";
import {CourseManage, isCourseManage} from "../../composables/CourseManage";
import httpRequests from "../../composables/useHttp"


const courseDetail = ref<CourseDetail>({
  id:0,
  title: '',
  description: '',
  coverPicture: '',
  isNeedCertification: '',
  tag: ''
})
const courseStatistics = ref<CourseStatistics>({
  estimatedLearningTime: '',
  lessonCount: 0,
  videoCount: 0,
  articleCount: 0,
  userCount: 0
})
const courseManageList = ref([])
const resourceUrl = ref("")
const isJoin = ref<boolean>(false)
const isNotification = ref<boolean>(false)
const config = useRuntimeConfig()
const route = useRoute()
const router = useRouter()
const notify = ref(null);

const requestBody1: RequestBody = {
  id: route.params.id,
  type: 'course'
}

const requestBody3: RequestBody = {
  id: route.params.id,
  type: 'course',
  estimatedLearningTime: 1,
  lessonCount: 1,
  videoCount: 1,
  articleCount: 1,
  userCount: 1
}

const requestBody4: RequestBody = {
  id: route.params.id,
  type: 'course'
}

const requestBody5: RequestBody = {
  id: route.params.id,
  type: 'course'
}

const options1: FetchOptions = createFetchOptions(true, requestBody1);
const options2: FetchOptions = createFetchOptions(false);
const options3: FetchOptions = createFetchOptions(false, requestBody3);
const options4: FetchOptions = createFetchOptions(false, requestBody4);
const options5: FetchOptions = createFetchOptions(true, requestBody5);

onMounted(() => {
  resourceUrl.value = config.public.envData.VITE_APP_URL

  isJoinTheCourse()
  getCourseDetail()
  getCourseStatistics()
  getCourseManage()
})

async function isJoinTheCourse() {
  if (utils.isLogin()) {
    // const res: ApiResponse<UserCourse> = await ($fetch as (url: string, options?: FetchOptions) => Promise<ApiResponse<UserCourse>>)( '/api/user/course', options1);
    // if (res.code == 0 && res.data.id){
    //   isJoin.value = true
    // }

    // 自定义请求方法
    // httpRequests.useHttpPost('/api/user/course', {
    //   id: route.params.id,
    //   type: 'course'
    // }).then((res) => {
    //   if (!res.status){
    //     isSHow.value = true
    //     toastObj.value = {variant: 'accent', message: res.msg}
    //   }else {
    //     isJoin.value = true
    //   }
    // }).catch()

    const res = await httpRequests.useHttpPost('/api/user/course', {
      id: route.params.id,
      type: 'course'
    })

    if (!res.status){
      isSHow.value = true
      toastObj.value = {variant: 'accent', message: res.msg}
    }else {
      isJoin.value = true
    }
  }
}

async function getCourseDetail(){
  const res: ApiResponse<CourseDetail> = await ($fetch as (url: string, options?: FetchOptions) => Promise<ApiResponse<CourseDetail>>)( '/api/course/'+route.params.id, options2);
  try {
    if (res.code){
      isSHow.value = true
      toastObj.value = {variant: 'accent', message: res.msg}
    }else {
      courseDetail.value.id = res.data.id
      courseDetail.value.title = res.data.title
      courseDetail.value.description = res.data.description
      courseDetail.value.coverPicture = res.data.coverPicture
      courseDetail.value.isNeedCertification = res.data.isNeedCertification
      courseDetail.value.tag = res.data.tag
    }
  }catch (error){
    isSHow.value = true
    toastObj.value = {variant: 'error', message: 'require api failed'}
  }
}

async function getCourseStatistics(){
  const res: ApiResponse<CourseStatistics> = await ($fetch as (url: string, options?: FetchOptions) => Promise<ApiResponse<CourseStatistics>>)( '/api/course/statistics', options3);
  try {
    if (res.code){
      isSHow.value = true
      toastObj.value = {variant: 'accent', message: res.msg}
    }else {
      courseStatistics.value.estimatedLearningTime = res.data.estimatedLearningTime
      courseStatistics.value.lessonCount = res.data.lessonCount
      courseStatistics.value.videoCount = res.data.videoCount
      courseStatistics.value.articleCount = res.data.articleCount
      courseStatistics.value.userCount = res.data.userCount
    }
  }catch (error){
    isSHow.value = true
    toastObj.value = {variant: 'error', message: 'require api failed'}
  }
}

async function getCourseManage(){
  const res: ApiResponse<number[]> = await ($fetch as (url: string, options?: FetchOptions) => Promise<ApiResponse<number[]>>)( '/api/course/manage', options4);
  try {
    if (res.code){
      isSHow.value = true
      toastObj.value = {variant: 'accent', message: res.msg}
    }else {
      if (res.data.length) {
        _.forEach(res.data, (item) => {
          let obj = {} as CourseManage
          if (isCourseManage(item)){
            obj.id = item.id
            obj.type = item.type
            obj.title = item.title
            obj.length = item.length
          }

          courseManageList.value.push(obj)
        })
      }
    }
  }catch (error){
    isSHow.value = true
    toastObj.value = {variant: 'error', message: 'require api failed'}
  }
}

function learnTheCourse(item_id, type){
  if (utils.isLogin()){
    if (isJoin){
      if (type === 'video') {
        router.push({
          name: 'course-video',
          query: { item_id: item_id }
        })
      } else if (type === 'article') {
        router.push({
          name: 'course-article',
          query: { item_id: item_id }
        })
      } else {
        router.push({
          name: 'course-quiz',
          query: { item_id: item_id }
        })
      }
    }else {
      notify.value.isShow = true;
      notify.value.id = route.params.id;
    }
  }else {
    isSHow.value = true
    toastObj.value = {variant: 'accent', message: 'Please login'}
  }
}

async function joinTheCourse(){
  if (utils.isLogin()) {
    const res: ApiResponse<JoinCourse> = await ($fetch as (url: string, options?: FetchOptions) => Promise<ApiResponse<JoinCourse>>)( '/api/join/course', options5);
    try {
      if (res.code){
        isSHow.value = true
        toastObj.value = {variant: 'accent', message: res.msg}
      }else {
        let type = res.data.type
        if (type === 'video'){
          await router.push({
            name: 'course-video',
            query: {item_id: res.data.itemId}
          })
        }else if (type === 'article'){
          await router.push({
            name: 'course-article',
            query: {item_id: res.data.itemId}
          })
        }else {
          await router.push({
            name: 'course-quiz',
            query: {item_id: res.data.itemId}
          })
        }
      }
    }catch (error){
      isSHow.value = true
      toastObj.value = {variant: 'error', message: 'require api failed'}
    }
  } else {
    isSHow.value = true
    toastObj.value = {variant: 'error', message: 'require api failed'}
  }
}
</script>

<style lang="postcss" scoped>
.banner-wrapper {
  width: 100%;
  height: 40.6875rem;
  position: relative;
  z-index: 10;

  .banner-img {
    display: block;
    width: 100%;
    height: 45.0625rem;
    position: absolute;
    top: -4.375rem;
  }

  .banner-cont {
    position: relative;
    z-index: 61;

    .course-info {
      padding-left: 2.1875rem;
    }

    .course-title {
      margin-top: 7.3125rem;

      .title {
        font-weight: 300;
        font-size: 5.875rem;
        @media screen and (max-width: 992px) {
          font-size: 4.875rem;
        }
        @media screen and (max-width: 768px) {
          font-size: 3.875rem;
        }
        line-height: 6.4625rem;
        height: 6.4375rem;
        color: #fff;
      }
    }
    .course-statics {
      margin-top: 2.65625rem;
      .resources {
        li {
          float: left;
          color: #fff;
          font-size: 0.875rem;
          line-height: 1.4rem;
          position: relative;
          padding-right: 0.5625rem;
          margin-right: 0.5625rem;
          font-weight: 400;
          &:after {
            content: '';
            position: absolute;
            width: 0.0625rem;
            height: 1.25rem;
            right: 0;
            background: url('/assets/img/split_line.png') no-repeat;
          }
          &:last-child:after {
            content: '';
            width: 0;
          }
        }
      }
      .users {
        li {
          margin-top: 1rem;
          display: flex;
          align-items: flex-start;
          color: #fff;
          img {
            display: block;
            width: 1.25rem;
            height: 1.25rem;
          }
          span {
            font-weight: normal;
            font-size: 0.875rem;
            line-height: 1.375rem;
            margin-left: 0.375rem;
          }
        }
      }
    }
    .join-action {
      margin-top: 1.90625rem;
      width: 10.875rem;
      height: 2.75rem;
      a {
        display: block;
        padding: 0.625rem 1.875rem;
        font-size: 1rem;
        text-align: center;
        color: #000;
        line-height: 1.5rem;
        background-color: #01f0e0;
      }
    }
  }
}
.content-box {
  .course-detail {
    .title {
      font-weight: 500;
      font-size: 1.75rem;
      line-height: 2.4375rem;
    }
    .detail {
      margin-top: 1.875rem;
      font-weight: normal;
      font-size: 1rem;
      line-height: 170%;
      &:deep(ul) {
        list-style: disc outside;
        margin-left: 1rem;
      }
      &:deep(ol) {
        list-style: decimal outside;
        margin-left: 1.125rem;
      }
    }
  }
  .course-lesson {
    .title {
      font-weight: 400;
      font-size: 1.75rem;
      line-height: 2.4375rem;
    }
    .lesson-box {
      margin-top: 1.875rem;
      width: 28.125rem;
      @media screen and (max-width: 48rem) {
        width: 100%;
      }
      .lesson-info {
        display: flex;
        justify-content: space-between;
        align-items: center;
        width: 100%;
        height: 2.875rem;
        background-color: #000;
        color: #fff;
        .num {
          width: 10.125rem;
          font-size: 1rem;
          line-height: 1.5rem;
          margin-left: 1.25rem;
        }
        .times {
          width: 5.0625rem;
          height: 1.625rem;
          line-height: 1.625rem;
          font-size: 0.875rem;
        }
      }
      .lesson-list {
        border: 1px solid #000;
        position: relative;
        ul {
          li {
            display: flex;
            align-items: center;
            justify-content: space-between;
            height: 5rem;
            padding: 0.53125rem 1.25rem;
            box-sizing: border-box;
            border-bottom: 1px solid #757575;
            cursor: pointer;
            a {
              display: flex;
              color: #000;
              align-items: center;
              img {
                display: block;
                width: 1.5rem;
                height: 1.5rem;
                margin-right: 0.75rem;
              }
              @media (min-width: 48rem) {
                .title {
                  max-width: 17.1875rem;
                  font-size: 0.875rem;
                  line-height: 150%;
                }
              }
              /* @media (max-width: 768px) {
                .title {
                  max-width: 14rem;
                }
              } */
            }
            .times {
              margin-left: auto;
              font-size: 0.875rem;
              line-height: 1.3125rem;
            }
          }
        }
      }
    }
  }
}
@media (min-width: 48rem) {
  .content-box {
    display: flex;
    justify-content: space-between;
    margin-top: 6.25rem;
    color: #000;
    margin-bottom: 9.125rem;
    .course-detail {
      width: 50.625rem;
    }
  }
}
@media (max-width: 64rem) {
  .banner-wrapper {
    height: 100%;
    .banner-img {
      display: block;
      width: 100%;
      height: auto;
      position: absolute;
      top: -4.375rem;
    }
  }
  
  .content-box {
    padding-left: 1.25rem;
    padding-right: 1.25rem;
    margin-top: 6.25rem;
    margin-bottom: 9.125rem;
    .course-phone-lesson {
      padding-top: 1.25rem;
    }
  }
  .times {
    width: auto !important;
    padding-right: 0.625rem;
  }
  .lesson-list {
    display: block;
    ul {
      li {
        display: block !important;
        height: 100% !important;
        a {
          .title {
            width: 100%;
            font-size: 0.875rem !important;
            line-height: 150%;
          }
        }
        .times {
          padding-left: 2.2rem;
        }
      }
    }
  }

  .relin-paragraph-targe {
    font-size: 2rem !important;
    font-weight: 600 !important;
  }
  .phone-title {
    font-size: 2rem !important;
    font-weight: 600 !important;
  }
  .phone-text {
    display: -webkit-box;
    -webkit-line-clamp: 9;
    -webkit-box-orient: vertical;
    overflow: hidden;
    &.more-phone-text {
      -webkit-line-clamp: 999;
    }
  }
}

.action-wrapper {
  position: absolute;
  top: 53.875rem;
  right: 0;
}
</style>
//封装http请求,useHttp.ts文件

import {nextTick, ref} from 'vue'
import {useCookie, useFetch} from '../.nuxt/imports'

interface HttpRequestOption {
    headers?: Record<string, string>,
    method?: 'GET' | 'POST',
    body?: {},
    lazy?: boolean
}

interface HttpResponse {
    status: boolean;
    statusCode: number;
    msg: string;
    code?: number;
    data?: any;
}

function useGetFetchOptions(options: HttpRequestOption) {
    // options.key = 'my-key'
    options.headers = options.headers ? options.headers : {}
    options.headers['X-Requested-With'] = 'XMLHttpRequest'
    options.headers['cookie'] = useCookie("DUOXIAOZHANSSESSID").value || ""

    return options
}

export async function useHttpRequest(url: string, options: HttpRequestOption): Promise<HttpResponse> {
    // 有了 await nextTick(),当用户登录以后, 后面的 options = useGetFetchOptions(options) 也可以不要,就是说不用cookie信息也可以请求成功,可以f12看到接口调用
    await nextTick();
    options = useGetFetchOptions(options);

    // 这里使用 useFetch 请求
    const res = await useFetch(url, {
        ...options,
        //transform是一个 callback 用于处理 useFetch 的响应数据,这里其实没有做任何处理
        transform: (res: any) => res
    });

    // 客户端错误处理
    //@ts-ignore
    if (process.client && res.error?.value) {
        const errorData = res.error.value.data;
        const statusCode = res.error.value.statusCode;

        if (statusCode === 401) {
            location.href = '/login';
        }

        const msg = errorData?.data;
        if (!options.lazy) {
            // 如果需要返回错误信息和statusCode,可以在此处进行处理
            return {
                status: false,
                statusCode: statusCode,
                msg: msg || '服务端错误',
            };
        }
    }

    // 如果没有错误,返回数据
    if (res.data.value.code) {
        return {
            status: false,
            statusCode: 200,
            msg: res.data.value.msg || '请求成功',
            code: res.data.value.code
        };
    }

    return {
        status: true,
        statusCode: 200,
        msg: res.data.value.msg || '请求成功',
        code: res.data.value.code,
        data: res.data.value.data
    }
}

// GET请求
function useHttpGet(url, params = {}, options: HttpRequestOption = {}): Promise<HttpResponse> {
    options.method = "GET"

    return useHttpRequest(url, options)
}

// POST请求
function useHttpPost(url, params: {}, options: HttpRequestOption = {}): Promise<HttpResponse> {
    options.method = "POST"
    options.body = params

    return useHttpRequest(url, options)
}

function myAsyncFunction() {
    return new Promise((resolve, reject) => {
        // Simulate an asynchronous operation
        setTimeout(() => {
            resolve("Hello, World!");
        }, 1000);
    });
}

// Usage without await
const promise = myAsyncFunction();

// async function myAsyncFunction() {
//     return new Promise((resolve, reject) => {
//         // Simulate an asynchronous operation
//         setTimeout(() => {
//             resolve("Hello, World!");
//         }, 1000);
//     });
// }

// Usage with await
// const promise = await myAsyncFunction();

async function testPromise() {
    promise.then(result => {
        console.log("result is:", result); // "Hello, World!"
    }).catch(error => {
        console.error("error is:", error);
    });

    // try {
    //     const promise = await myAsyncFunction();
    //     console.log("promise is:", promise);
    // }catch (error){
    //     console.error("error is:", error);
    // }

    //上面两种情况,下面的 console 的执行顺序是不一样的
    console.log(1111111112222222222)
}

//重要结论
// it's important to note that an async function can implicitly return a resolved Promise even if the return type is not explicitly Promise<T>.

// export {
//     useHttpGet,
//     useHttpPost
// }

const httpRequests = {
    useHttpGet,
    useHttpPost,
    testPromise
}
//
export default httpRequests;