UniApp项目中的多服务环境配置与跨域代理实现

在前后端分离的开发模式下,前端应用经常需要与多个后端服务进行交互。本文将详细介绍如何在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,
},

这段配置的核心逻辑是:

  1. 仅在H5环境且启用代理时生效
  2. 解析VITE_SERVER_BASEURLS中的服务配置
  3. 为每个服务创建对应的代理规则
  4. 代理规则使用服务名作为路径前缀,如/service1/service2
  5. 请求会被重写,去掉服务名前缀后转发到目标服务器

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项目中实现了多服务环境的支持:

  1. 灵活的服务配置:通过环境变量配置多个后台服务地址
  2. 统一的请求处理:使用拦截器统一处理不同服务的请求
  3. 平台适配:针对H5和小程序等不同平台提供不同的处理逻辑
  4. 开发便利性:在H5开发环境中通过代理解决跨域问题

这种架构设计使得前端应用能够灵活地与多个后端服务进行交互,同时保持代码的清晰和可维护性。对于需要对接多个服务的复杂应用,这种方案提供了一个可靠的解决方案。

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax