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

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

相关推荐
不和乔治玩的佩奇1 分钟前
【 设计模式】常见前端设计模式
前端
bloxed7 分钟前
vue+vite 减缓首屏加载压力和性能优化
前端·vue.js·性能优化
打野赵怀真20 分钟前
React Hooks 的优势和使用场景
前端·javascript
HaushoLin24 分钟前
ERR_PNPM_DLX_NO_BIN No binaries found in tailwindcss
前端·vue.js·css3·html5
Lafar24 分钟前
Widget 树和 Element 树和RenderObject树是一一 对应的吗
前端
小桥风满袖26 分钟前
炸裂,前端神级动效库合集
前端·css
匆叔26 分钟前
Tauri 桌面端开发
前端·vue.js
1_2_3_27 分钟前
react-antd-column-resize(让你的table列可以拖拽列宽)
前端
Lafar27 分钟前
Flutter和iOS混合开发
前端·面试