likes
comments
collection
share

Nuxt3 服务端接口与客户端接口请求最佳实践

作者站长头像
站长
· 阅读数 16

现在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的坑,当然还有别的我再更新,当然也希望同伴碰到其他问题,可以在下面留言。