问题背景
在Nuxt3应用中,通常在开发模式下,vue组件与后台交互是经过nuxt的server层的,而实际生产环境是经过nginx转发的。但开发模式下,经nuxt的server层转发请求有多种实现方式,也会有形形色色的问题,如转发cookie,转发websocket请求等等。
本文梳理了一下目前常见的代理实现方式。在Nuxt 2中可以直接采用 @nuxtjs/proxy包。
了解代理的实现,能帮助我们进一步掌握Nuxt3中间件概念和一些API。
nuxt3的middleware使用
middleware可以拦截所有请求,并修改请求。
nuxt3项目下有两个middleware的预置目录,一个是/middleware,一个是/server/middleware。一个是拦截前端的请求,一个是拦截后端的请求。注意:/server目录的下的代码都是运行在服务端的,也就是Nodejs进程内。
在/server/middleware的代码中,是可以自动导入h3的函数的。
采用h3的sendProxy()
sendProxy是h3的内置方法。
import { sendProxy } from "h3";
const config = useRuntimeConfig();
export default defineEventHandler(async (event) => {
const target = new URL(
event.req.url.replace(/^\/proxy-api/, ""),
config.proxyApiUrl
);
return await sendProxy(event, target.toString());
});
上述代码放到server/routes/proxy-api/[…].ts下即可。
采用h3的内置API实现转发
import {defineEventHandler, getCookie, getHeaders, getMethod, getQuery, readBody} from "h3";
const config = useRuntimeConfig()
const baseURL = config.public.apiBaseUrl
export default defineEventHandler(async (event) => {
const method = getMethod(event)
const params = getQuery(event)
const headers = getHeaders(event)
const authorization = headers.Authorization || getCookie(event, 'auth._token.local')
const url = event.req.url as string
const body = method === "GET" ? undefined : await readBody(event)
return await $fetch(url, {
headers: {
"Content-Type": headers["content-type"] as string,
Authorization: authorization as string,
},
baseURL,
method,
params,
body,
})
})
采用h3的proxyRequest()方法
/server/middleware/proxy.ts
export default defineEventHandler((event) => {
// proxy only "/api" requests
if (!event.node.req.url?.startsWith('/api/')) return
const { apiBaseUrl } = useRuntimeConfig()
const target = new URL(event.node.req.url, apiBaseUrl)
if (should_be_proxied)
return proxyRequest(event, target.toString(), {
headers: {
host: target.host // if you need to bypass host security
}
})
})
采用http-proxy模块
在server/middleware下创建proxy.ts文件:
import httpProxy from 'http-proxy';
const proxy = httpProxy.createProxyServer({
target: 'http://localhost:7500/', // change to your backend api url
changeOrigin: true,
});
export default defineEventHandler((event) => {
return new Promise((resolve) => {
const options = {};
const origEnd = event.res.end;
event.res.end = function() {
resolve(null);
return origEnd.call(event.res);
}
const prefix = '/api'
if (req.url.startsWith(prefix)) {
proxy.web(event.req, event.res, options); // proxy.web() works asynchronously
} else {
next()
}
});
});
采用Nitro引擎的devProxy选型
在nuxt.config.ts中配置nitro.devProxy选择,此方法我没有验证过。
// config:
export default defineNuxtConfig({
nitro: {
devProxy: {
'/api/': {
target: process.env.API_TARGET,
changeOrigin: true
}
}
}
})
// ======================
// later, on server side:
// ======================
const r = await (await fetch('/api/test')).json();
// => 500 Invalid URL
采用nuxt-proxy模块
老外基于http-proxy-middleware和h3写的一个nuxt3代理中间件。
地址:https://github.com/wobsoriano/nuxt-proxy
export default defineNuxtConfig({
modules: ['nuxt-proxy'],
// See options here https://github.com/chimurai/http-proxy-middleware#options
proxy: {
options: {
target: 'https://jsonplaceholder.typicode.com',
changeOrigin: true,
pathRewrite: {
'^/api/todos': '/todos',
'^/api/users': '/users'
},
pathFilter: [
'/api/todos',
'/api/users'
]
}
},
// OR
// runtimeConfig: {
// proxy: {...}
// }
})
// GET /api/todos -> https://jsonplaceholder.typicode.com/todos [304]
// GET /api/users -> https://jsonplaceholder.typicode.com/users [304]
基于http-proxy-middleware和http模块的实现
也是写一个nuxt3的中间件来实现。
server/middleware/api-proxy.ts
import type { IncomingMessage, ServerResponse } from 'http'
import { createProxyMiddleware } from 'http-proxy-middleware'
import config from '#config'
// Temporary dev proxy until @nuxtjs/proxy module is available.
const apiProxyMiddleware = createProxyMiddleware('/api-proxy/**', {
target: config.API_URL as string,
changeOrigin: true,
pathRewrite: { '^/api-proxy/': '/' },
logLevel: 'debug',
})
export default async (req: IncomingMessage, res: ServerResponse) => {
// Workaround for h3 not awaiting next.
await new Promise<void>((resolve, reject) => {
const next = (err?: unknown) => {
if (err) {
reject(err)
} else {
resolve()
}
}
// @ts-expect-error -- incompatible types express.Request and http.IncomingMessage. This still works though.
apiProxyMiddleware(req, res, next)
})
}
然后,你可以写一个useFetch的wrapper API, 例如叫callApi(),写法如下:
import { murmurHashV3 } from 'murmurhash-es'
type RequestOptions = {
method?: string
body?: Record<string, unknown>
pick?: string[]
params?: Record<string, unknown>
}
function getBaseURL() {
const config = useRuntimeConfig() as { API_URL: string }
return process.server ? config.API_URL : '/api-proxy'
}
export const useApi = async <Result = unknown>(
endpoint: string,
opts?: RequestOptions
) => {
const baseURL = getBaseURL()
const headers = useRequestHeaders(['cookie'])
return useFetch<string, Result>(endpoint, {
method: opts?.method,
body: opts?.body,
baseURL,
headers,
params: opts?.params,
// The default key implementation includes the baseURL in the hasing process.
// As this is different for server and client, the default implementation leads to different
// keys, resulting in hydration errors.
key: '$api-' + murmurHashV3(JSON.stringify({ endpoint, opts })).toString(),
})
}
参考链接
- http-proxy-middleware
- node的http-proxy模块
- proxy-module
- 三个参考模块:‘@nuxtjs-alt/auth’, ‘@nuxtjs-alt/http’, ‘@nuxtjs-alt/proxy’
- nitro源码
- Nuxt代理方式讨论