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;
