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开发环境中通过代理解决跨域问题

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

相关推荐
AI大模型-小华22 分钟前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子3 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅3 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni3 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学4 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
赵庆明老师5 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
wc885 小时前
微软EDGE浏览器功能学习
前端·学习·edge
Csvn5 小时前
🎯 原生 `<dialog>` 元素:终于可以扔掉一半的自定义弹窗组件了?
前端
Csvn5 小时前
🎨 CSS @layer:用「层叠优先级」终结样式打架的世纪难题
前端