在前后端分离的开发模式下,前端应用经常需要与多个后端服务进行交互。本文将详细介绍如何在UniApp项目中配置和管理多个后台服务地址,以及如何处理跨域请求问题,特别是在H5环境下的代理配置。
1. 环境变量配置
在UniApp项目中,我们可以通过环境变量文件来管理不同环境下的配置信息。以下是开发环境的配置示例:
properties
# env/.env.development
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = true
# 后端服务上传地址
VITE_UPLOAD_BASEURL = 'https://example-upload.com/upload'
# 多个后台服务地址
VITE_SERVER_BASEURLS='[{"service1":"https://example-api.com/service1"},{"service2":"https://example-api.com/service2"},{"service3":"https://example-api.com/service3"}]'
在基础环境配置文件中,我们还定义了代理相关的设置:
properties
# env/.env
# h5是否需要配置代理
VITE_APP_PROXY=true
VITE_APP_PROXY_PREFIX = '/api'
1.1 多服务地址配置说明
VITE_SERVER_BASEURLS
变量使用JSON格式存储多个后台服务地址,每个服务以键值对形式定义,键为服务名称,值为对应的API基础URL。这种设计允许我们在一个应用中同时连接多个不同的后端服务。
2. TypeScript类型定义
为了在TypeScript环境中获得良好的类型支持,我们需要为环境变量定义接口:
typescript
// src/env.d.ts
interface ImportMetaEnv {
/** 网站标题,应用名称 */
readonly VITE_APP_TITLE: string
/** 服务端口号 */
readonly VITE_SERVER_PORT: string
/** 后台接口地址 */
readonly VITE_SERVER_BASEURL: string
/** H5是否需要代理 */
readonly VITE_APP_PROXY: 'true' | 'false'
/** H5是否需要代理,需要的话有个前缀 */
readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
/** 上传图片地址 */
readonly VITE_UPLOAD_BASEURL: string
/** 是否清除console */
readonly VITE_DELETE_CONSOLE: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
3. Vite配置中的代理设置
在Vite配置文件中,我们需要设置代理规则,特别是在H5环境下:
typescript
// vite.config.ts
server: {
host: '0.0.0.0',
hmr: true,
port: Number.parseInt(VITE_APP_PORT, 10),
// 仅 H5 端且 VITE_APP_PROXY 为 true 时生效
proxy: UNI_PLATFORM === 'h5' && JSON.parse(VITE_APP_PROXY)
? (() => {
const proxyConfig = {}
// 处理多个后端服务地址
if (VITE_SERVER_BASEURLS) {
try {
const services = JSON.parse(VITE_SERVER_BASEURLS)
services.forEach((service) => {
const [serviceName, url] = Object.entries(service)[0]
proxyConfig[`^/${serviceName}`] = {
target: url,
changeOrigin: true,
rewrite: path => path.replace(new RegExp(`^/${serviceName}`), ''),
}
})
// 打印代理规则
console.log('====== 代理规则配置 ======')
Object.entries(proxyConfig).forEach(([path, config]) => {
console.log(`路径模式: ${path}`)
console.log(`目标地址: ${config.target}`)
console.log(`重写规则: ${path} -> ${config.target}`)
console.log('------------------------')
})
console.log('=========================')
}
catch (error) {
console.error('解析 VITE_SERVER_BASEURLS 失败:', error)
}
}
return Object.keys(proxyConfig).length > 0 ? proxyConfig : undefined
})()
: undefined,
},
这段配置的核心逻辑是:
- 仅在H5环境且启用代理时生效
- 解析
VITE_SERVER_BASEURLS
中的服务配置 - 为每个服务创建对应的代理规则
- 代理规则使用服务名作为路径前缀,如
/service1
、/service2
等 - 请求会被重写,去掉服务名前缀后转发到目标服务器
4. 请求拦截器实现
为了统一处理请求,我们实现了请求拦截器:
typescript
// src/interceptors/request.ts
// 请求基准地址
const baseUrls = (() => {
try {
return JSON.parse(import.meta.env.VITE_SERVER_BASEURLS || '[]').reduce((acc, item) => {
const [key, value] = Object.entries(item)[0]
return { ...acc, [key]: value }
}, {})
}
catch (e) {
console.error('解析 VITE_SERVER_BASEURLS 失败:', e)
return {}
}
})()
function getServiceBaseUrl(serviceName = 'service1') {
// 小程序端环境区分
if (isMp) {
const { miniProgram } = uni.getAccountInfoSync()
const envVersion = miniProgram.envVersion
// 根据环境返回不同的URL
if (serviceName === 'service1') {
switch (envVersion) {
case 'develop':
return 'https://example-dev.com'
case 'trial':
return 'https://example-test.com'
case 'release':
return 'https://example-prod.com'
default:
return 'https://example-dev.com'
}
}
}
return baseUrls[serviceName] || baseUrls.service1
}
拦截器的核心功能是处理请求URL:
typescript
// 拦截器配置
const httpInterceptor = {
// 拦截前触发
invoke(options: CustomRequestOptions) {
// ... 其他处理逻辑 ...
// 非 http 开头需拼接地址
if (!options.url.startsWith('http')) {
// #ifdef H5
if (JSON.parse(import.meta.env.VITE_APP_PROXY)) {
// 检查是否指定了服务名
if (options.serviceName) {
// 在 H5 环境中启用了代理时,将服务名作为请求路径前缀
options.url = `/${options.serviceName}${options.url}`
}
else {
// 如果没有指定服务名,使用默认服务名'service1'
options.url = `/service1${options.url}`
}
}
else {
// H5 但没有启用代理时,使用完整 URL
const serviceBaseUrl = getServiceBaseUrl(options.serviceName)
options.url = serviceBaseUrl + options.url
}
// #endif
// 非H5正常拼接
// #ifndef H5
const serviceBaseUrl = getServiceBaseUrl(options.serviceName)
options.url = serviceBaseUrl + options.url
// #endif
}
// ... 添加token等其他处理 ...
},
}
5. HTTP请求工具封装
基于拦截器,我们可以封装HTTP请求工具,支持指定服务名:
typescript
// src/utils/http.ts
function get<T>(url: string, query?: Record<string, any>, options?: Partial<ExtendedRequestOptions> & { serviceName?: string }) {
return request<T>({
method: 'GET',
url,
query,
...options,
})
}
function post<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, options?: Partial<ExtendedRequestOptions> & { serviceName?: string }) {
return request<T>({
method: 'POST',
url,
data,
query,
...options,
})
}
6. 使用示例
6.1 基本使用
typescript
// 默认使用service1服务
http.get('/api/user/info')
// 指定使用service2服务
http.get('/api/articles', null, { serviceName: 'service2' })
// 指定使用service3服务
http.post('/api/comments', { content: '评论内容' }, null, { serviceName: 'service3' })
6.2 在组件中使用
vue
<script setup lang="ts">
import { http } from '@/utils'
import { ref } from 'vue'
const articles = ref([])
// 从service2服务获取文章列表
async function fetchArticles() {
try {
const res = await http.get('/api/articles', null, { serviceName: 'service2' })
articles.value = res.data
}
catch (error) {
console.error('获取文章失败', error)
}
}
// 向service3服务提交表单
async function submitForm(data) {
try {
await http.post('/api/submit', data, null, { serviceName: 'service3' })
uni.showToast({ title: '提交成功' })
}
catch (error) {
uni.showToast({ title: '提交失败', icon: 'none' })
}
}
onMounted(() => {
fetchArticles()
})
</script>
7. 总结
通过以上配置和实现,我们成功地在UniApp项目中实现了多服务环境的支持:
- 灵活的服务配置:通过环境变量配置多个后台服务地址
- 统一的请求处理:使用拦截器统一处理不同服务的请求
- 平台适配:针对H5和小程序等不同平台提供不同的处理逻辑
- 开发便利性:在H5开发环境中通过代理解决跨域问题
这种架构设计使得前端应用能够灵活地与多个后端服务进行交互,同时保持代码的清晰和可维护性。对于需要对接多个服务的复杂应用,这种方案提供了一个可靠的解决方案。