Nuxt3 服务端接口与客户端接口请求最佳实践
现在2023了,但Nuxt3的文章真的太少了,网上也找不到项目中接入接口的方案
今天分享一下我刚做的Nuxt3的完整项目中,服务端接口的处理
开源项目:vuejs-msite: vue3 + nuxt3 + vant (gitee.com) - https://gitee.com/ChuPiJiang/vuejs-msite
Nuxt 3 Minimal Starter
演示地址: sfc.jsvue.cn
vue3代码库演示: demo
项目目录介绍
|- api #
|-- request.js
|- components
|- composables
|-- index.js
|- pages
|- server # 服务接口默认自动会加载
|-- api
|--- user
|---- updateUser.js
|--- setAvatar.js
|-- middleware
|--- auth.js
|- utils
|- nuxt.config.js
服务端接口
nuxt3 已经与nuxt2完全不同,在项目中处理跨域问题都是在server 中express 层去再次发起请求,并不会产生跨域提示,express
并不严谨因为nuxt使用 nitro 启的服务,当然如果你是禁用服务端渲染的(ssr: false)还是会产生前端提示跨域
跨域处理
如果需要使用 代理 处理请求,官方文档或是网上都说配置里 devserver、 webpack都配置不会生效了,要使用 nitro
里的 devProxy (楼主使用的Nuxt v3.0.0)。
{
devProxy: {
'/proxy/test': 'http://localhost:3001',
'/proxy/example': {
target: 'https://example.com', changeOrigin: true
}
}
}
目录server 中的 api
, middleware
两部分分别代表接口服务与中间件。 Nuxt3 使用四种api请求接口,其实没什么用,就简单说一种以前的asyncData
。现在并不区分提前请求,如果你打开ssr会等数据请求回来
api 中的路径就代表接口路径,比如 useFetch('/api/user/getUserInfo').then()
。您需在api下建立 user 文件夹,并在下面建立getUserInfo.js 文件用来接受请求来的参数,
// /api/user/getUserInfo.js
import { readRawBody, getQuery, getMethod } from 'h3'
export default defineEventHandler(async (event) => {
const method = getMethod(event).toUpperCase()
let body
if (method !== 'GET') body = await readRawBody(event)
const res = await $fetch('/user-center/user/getUserInfo', {
method,
baseURL: event.context.baseUrl,
headers: event.context.headers,
params: getQuery(event),
body
})
console.log(res, 'res')
return res || { userInfo: {} }
})
在请求进入这个文件前,请求会进入到 server里的所有中间件中,比如我在里面建立了 auth.js
// /api/middleware/auth.js
import getFingerPrint from '@/utils/finger'
import { getHeaders } from 'h3'
export default defineEventHandler(async (event) => {
const reqHeaders = getHeaders(event)
const ssrHeader = new Headers()
const { app } = useRuntimeConfig()
ssrHeader.set('cookie', reqHeaders.cookie)
ssrHeader.set('x-xsrf-token', app['XSRF_HEADER'])
ssrHeader.set('app-id', app['APP_ID'])
ssrHeader.set('client-id', await getFingerPrint())
event.context.headers = ssrHeader
event.context.baseUrl = app['BASE_URL']
})
携带Cookie
保让getHeaders里面能接收到cookie,如果没有开启ssr,以上是可以接到浏览器带的cookie的,当然不能是localhost域名,但是如果开启了ssr 那就得在 pages里的文件里加下:
const headers = useRequestHeaders(['cookie'])
useFetch('/user-center/user/getUserInfo', { headers })
服务端才能收到,不然你刷新浏览器会丢失用户登录信息
当然你如果只是用来做前后端分离普通的Vue项目, 前端直接请求服务端接口也是可以的,但是这就没有了ssr 的意义,那为什么要选这个骨头,nuxt3太多坑。
具体封装请看项目 /api/request
import { showFailToast, showSuccessToast } from 'vant'
import { timeout, isString } from '@/utils'
import { goLogin } from '@/utils/app'
import getFingerPrint from '@/utils/finger'
let clientId
const getClientId = async () => {
if (clientId) return clientId
clientId = await getFingerPrint()
return clientId
}
const fetch = async (url, options) => {
if (!isString(url)) {
console.warn('url is must be string!')
return
}
const { $config } = useNuxtApp()
const reqUrl = url.indexOf('http') > -1 ? url : $config.app['BASE_URL'] + url
if (!options.headers) {
options.credentials = 'include' // 跨域携带cookie
options.headers = {
'X-XSRF-TOKEN': $config.app['XSRF_HEADER'],
'APP-ID': $config.app['APP_ID'],
'CLIENT-ID': await getClientId()
}
}
return new Promise((resolve, reject) => {
useFetch(reqUrl, { ...options }).then(({ data, error}) => {
if (error.value) {
showFailToast('网络错误')
reject(error.value)
return
}
const res = data.value
if (res && res.code === 0 && res.notify) showSuccessToast(res.notify)
else if (res && (res.msg || res.errMsg || res.message)) {
showFailToast(res.msg || res.errMsg || res.message)
if (res.code === 403110) { // 会话过期跳登录
timeout(1200).then(() => {
goLogin()
})
}
}
resolve(data)
}).catch(err => {
reject(err)
})
})
}
export default new class Http {
get(url, params) {
return fetch(url, { method: 'GET', params })
}
post(url, body) {
return fetch(url, { method: 'POST', body })
}
reqData(url, body) {
return fetch(url, { method: 'POST', body, headers: {} })
}
}
在根目录增加 composables
里增加index.js ,这个文件里的接口会自动引入到pages里的文件里,就像components里的组件一样,你可以直接使用,不用import 引入
// composables/index.js
import Http from '[@/api/request]()'
export const getUserInfo = (data) => {
return Http.post('/user-center/user/getUserInfo', data)
}
使用方式,请求后端接口(注意会产生跨域问题)pages业务中代码如下:
await getUserInfo({key: value})
文件上传
了解Nuxt /server API,但似乎不知道如何将包含表单数据(即文件)的POST请求发送到Nuxt服务器,以便转发到外部服务
在pages页面文件中,使用第三方UI库选好文件后提交到下面方法
async function afterRead(file) {
const formData = new FormData();
const { file: fileInfo, content } = file
formData.append('file', fileInfo)
await $fetch("/api/send", {
method: "POST",
body: formData
});
}
在 server/api 中的接口接口代码如下
export default defineEventHandler(async (event) => {
const { method } = event.node.req;
// I THINK THE ISSUE IS HERE
const body =
method !== "GET" && method !== "HEAD"
? await readMultipartFormData(event)
: undefined;
const response = await $fetch.raw(https://*******, {
method,
baseURL: *********,
headers: {
},
body: body
});
return response._data;
}
Nuxt有效地创建了一个直通API,这样外部端点就不会暴露给最终用户。只是不知道如何以正确的格式访问formData以在服务器端通过。我不认为我应该使用readMultipartFormData()
,因为它似乎以某种方式解析数据,而我只想直接将formData传递给外部API。
我试过使用readMultipartFormData()
和readBody()
,但似乎都不起作用。
readMultipartFormData 取出的是一个数据,但它不是真正的formData结构数据,网上找不到几人回答,最终看到一个人是这样做的,new 成blob 再插入到新的formData中,因为数组对象中有data
api 中 不再使用 form-data 这个包,因为在vscode中提示用下面这个 formdata-node
import { FormData } from 'formdata-node'
const file = body[0]
formData.set('file', new Blob([file.data], { type: file.type }))
await $fetch('https://xxxxxx.com/', {
method: 'POST',
body: formData
})
文件上传在 $fetch 中已经不需要加入 content-type: multipart/form-data
,你只需把 formdata 传到 body上即可,如上面代码那样。
亲测试这种方法,后端可以接收到文件,但有一个不好的地方,blob不能定义文件的原始名,fname 会识取不到,如果后端用到会有显示 blob 为 fname。
以上是我这几天所填Nuxt3的坑,当然还有别的我再更新,当然也希望同伴碰到其他问题,可以在下面留言。
转载自:https://juejin.cn/post/7199250953333522491