Skip to content

H5 适配

介绍

H5 是 UniApp 支持的重要目标平台之一,可以让应用直接在浏览器中运行。与小程序和 App 相比,H5 具有无需安装、即点即用的特点,但也存在一些浏览器兼容性和功能限制需要处理。

本文档详细介绍如何将 UniApp 项目适配到 H5 平台,包括环境配置、路由模式、跨域处理、浏览器兼容性、SEO 优化、微信公众号适配等内容。

主要内容:

  • 项目配置 - manifest.json 和 vite.config.ts 配置
  • 路由模式 - history 和 hash 模式选择
  • 跨域处理 - 开发环境代理和生产环境 CORS
  • 浏览器兼容性 - 移动端浏览器差异处理
  • 微信公众号 - 公众号授权登录、分享、支付
  • SEO 优化 - 搜索引擎优化策略
  • 部署方案 - Nginx 配置和 CDN 部署

环境配置

manifest.json 配置

manifest.config.ts 中配置 H5 相关参数:

typescript
// manifest.config.ts
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'

export default defineManifestConfig({
  // H5 配置
  h5: {
    // 路由配置
    router: {
      mode: 'history', // 路由模式: history | hash
      base: '/', // 应用基础路径
    },
    // 页面标题
    title: 'RuoYi-Plus-UniApp',
    // HTML 模板
    template: 'index.html',
    // 开发服务器配置
    devServer: {
      https: false, // 是否启用 HTTPS
      port: 3000, // 开发服务器端口
      proxy: {
        '/api': {
          target: 'http://localhost:8080',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
      },
    },
    // 打包优化
    optimization: {
      treeShaking: {
        enable: true,
      },
    },
  },
})

关键配置说明:

配置项说明推荐值
mode路由模式'history'(需要服务器配置)
base基础路径'/' 或子路径
https是否使用 HTTPS生产环境 true
proxy开发环境代理配置后端 API 地址

vite.config.ts 配置

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'

export default defineConfig({
  plugins: [uni()],

  // 开发服务器配置
  server: {
    host: '0.0.0.0',
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
      '/ws': {
        target: 'ws://localhost:8080',
        ws: true,
      },
    },
  },

  // 构建配置
  build: {
    target: 'es2015',
    minify: 'esbuild',
    cssCodeSplit: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          utils: ['lodash-es', 'dayjs'],
        },
      },
    },
  },
})

环境变量配置

bash
# .env.development
VITE_APP_BASE_API=/api
VITE_APP_PUBLIC_PATH=/

# .env.production
VITE_APP_BASE_API=https://api.example.com
VITE_APP_PUBLIC_PATH=/

开发环境搭建

启动开发服务

bash
# 启动 H5 开发模式
pnpm dev:h5

# 或使用简写
npm run dev:h5

访问应用

bash
# 本地访问
http://localhost:3000

# 局域网访问(手机调试)
http://192.168.x.x:3000

调试工具

Chrome DevTools:

  1. 按 F12 打开开发者工具
  2. 切换到移动设备模式(Ctrl+Shift+M)
  3. 选择设备类型或自定义尺寸
  4. 查看 Console、Network、Elements 等面板

vConsole 调试:

typescript
// 在 main.ts 中添加
// #ifdef H5
if (import.meta.env.DEV) {
  import('vconsole').then((VConsole) => {
    new VConsole.default()
  })
}
// #endif

路由配置

history 模式

优势:

  • URL 更简洁美观
  • 有利于 SEO

配置:

typescript
// manifest.config.ts
h5: {
  router: {
    mode: 'history',
    base: '/',
  },
}

Nginx 配置(必须):

nginx
server {
  listen 80;
  server_name example.com;
  root /var/www/html;

  location / {
    try_files $uri $uri/ /index.html;
  }

  location /api {
    proxy_pass http://backend-server;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

hash 模式

优势:

  • 无需服务器特殊配置
  • 兼容性更好

配置:

typescript
// manifest.config.ts
h5: {
  router: {
    mode: 'hash',
    base: '/',
  },
}

跨域处理

开发环境代理

typescript
// vite.config.ts
server: {
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, ''),
    },
  },
}

生产环境 CORS

后端配置(Spring Boot):

java
@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return new CorsFilter(source);
    }
}

Nginx 配置:

nginx
location /api {
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
        return 204;
    }

    proxy_pass http://backend-server;
}

浏览器兼容性

条件编译

vue
<template>
  <!-- 仅在 H5 中显示 -->
  <!-- #ifdef H5 -->
  <div class="h5-only">H5 专属内容</div>
  <!-- #endif -->

  <!-- 非 H5 平台显示 -->
  <!-- #ifndef H5 -->
  <view class="not-h5">其他平台内容</view>
  <!-- #endif -->
</template>

<script setup lang="ts">
// #ifdef H5
import { useRouter } from 'vue-router'
const router = useRouter()
// #endif
</script>

平台判断

typescript
// utils/platform.ts
export const isH5 = (() => {
  // #ifdef H5
  return true
  // #endif
  // #ifndef H5
  return false
  // #endif
})()

// 判断是否在微信浏览器中
export const isWechatBrowser = () => {
  // #ifdef H5
  return /MicroMessenger/i.test(navigator.userAgent)
  // #endif
  return false
}

// 判断是否在支付宝浏览器中
export const isAlipayBrowser = () => {
  // #ifdef H5
  return /AlipayClient/i.test(navigator.userAgent)
  // #endif
  return false
}

常见兼容问题

1. iOS 滚动穿透:

vue
<template>
  <view class="popup" @touchmove.stop.prevent>
    <view class="content" @touchmove.stop>
      <!-- 弹窗内容 -->
    </view>
  </view>
</template>

2. iOS 300ms 延迟:

typescript
// 使用 fastclick 或 touch-action
// #ifdef H5
import FastClick from 'fastclick'
FastClick.attach(document.body)
// #endif

3. 安卓键盘遮挡输入框:

typescript
// 监听软键盘
// #ifdef H5
window.addEventListener('resize', () => {
  if (document.activeElement?.tagName === 'INPUT') {
    setTimeout(() => {
      document.activeElement?.scrollIntoView({ behavior: 'smooth' })
    }, 100)
  }
})
// #endif

4. 图片加载失败:

vue
<template>
  <image
    :src="imageSrc"
    @error="handleImageError"
    mode="aspectFill"
  />
</template>

<script setup lang="ts">
const imageSrc = ref(props.src)

const handleImageError = () => {
  // #ifdef H5
  imageSrc.value = '/static/images/default.png'
  // #endif
}
</script>

微信公众号适配

公众号授权登录

配置公众号:

  1. 登录微信公众平台
  2. 开发 -> 接口权限 -> 网页授权
  3. 填写授权回调域名

发起授权:

typescript
// utils/wechatAuth.ts
export const getWechatAuthUrl = (redirectUri: string, state: string) => {
  const appId = import.meta.env.VITE_WECHAT_APPID
  const encodedUri = encodeURIComponent(redirectUri)

  return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${encodedUri}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`
}

// 跳转授权
export const redirectToWechatAuth = () => {
  const currentUrl = window.location.href
  const authUrl = getWechatAuthUrl(currentUrl, 'login')
  window.location.href = authUrl
}

处理回调:

typescript
// 在 App.vue 或路由守卫中处理
// #ifdef H5
if (isWechatBrowser()) {
  const urlParams = new URLSearchParams(window.location.search)
  const code = urlParams.get('code')
  const state = urlParams.get('state')

  if (code && state === 'login') {
    // 调用后端接口换取 token
    const [err, data] = await loginWithWechat({ code })
    if (!err) {
      userStore.token = data.token
      // 清除 URL 中的授权参数
      const newUrl = window.location.pathname + window.location.hash
      window.history.replaceState({}, document.title, newUrl)
    }
  }
}
// #endif

微信 JS-SDK

引入 JS-SDK:

html
<!-- index.html -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>

初始化配置:

typescript
// utils/wechatSdk.ts
interface WxConfig {
  appId: string
  timestamp: number
  nonceStr: string
  signature: string
}

export const initWechatSdk = async () => {
  // 获取签名配置
  const [err, config] = await getWechatConfig({
    url: window.location.href.split('#')[0],
  })

  if (err) {
    console.error('获取微信配置失败:', err)
    return
  }

  // 配置 wx.config
  wx.config({
    debug: import.meta.env.DEV,
    appId: config.appId,
    timestamp: config.timestamp,
    nonceStr: config.nonceStr,
    signature: config.signature,
    jsApiList: [
      'updateAppMessageShareData',
      'updateTimelineShareData',
      'chooseImage',
      'previewImage',
      'getLocation',
      'scanQRCode',
    ],
  })

  return new Promise((resolve, reject) => {
    wx.ready(() => {
      console.log('微信 JS-SDK 初始化成功')
      resolve(true)
    })

    wx.error((res) => {
      console.error('微信 JS-SDK 初始化失败:', res)
      reject(res)
    })
  })
}

微信分享

typescript
// 设置分享内容
export const setWechatShare = (options: {
  title: string
  desc: string
  link: string
  imgUrl: string
}) => {
  // 分享给朋友
  wx.updateAppMessageShareData({
    title: options.title,
    desc: options.desc,
    link: options.link,
    imgUrl: options.imgUrl,
    success: () => {
      console.log('设置分享成功')
    },
  })

  // 分享到朋友圈
  wx.updateTimelineShareData({
    title: options.title,
    link: options.link,
    imgUrl: options.imgUrl,
    success: () => {
      console.log('设置分享成功')
    },
  })
}

微信支付

typescript
// H5 微信支付
export const wechatH5Pay = (payParams: {
  appId: string
  timeStamp: string
  nonceStr: string
  package: string
  signType: string
  paySign: string
}) => {
  return new Promise((resolve, reject) => {
    if (typeof WeixinJSBridge === 'undefined') {
      reject(new Error('WeixinJSBridge 不存在'))
      return
    }

    WeixinJSBridge.invoke('getBrandWCPayRequest', payParams, (res) => {
      if (res.err_msg === 'get_brand_wcpay_request:ok') {
        resolve(res)
      } else {
        reject(new Error(res.err_msg))
      }
    })
  })
}

SEO 优化

页面 Meta 配置

typescript
// pages.config.ts
export default {
  pages: [
    {
      path: 'pages/index/index',
      style: {
        navigationBarTitleText: '首页',
        // H5 SEO 配置
        h5: {
          title: '首页 - RuoYi-Plus-UniApp',
          meta: [
            {
              name: 'description',
              content: '这是首页的描述信息',
            },
            {
              name: 'keywords',
              content: '关键词1,关键词2,关键词3',
            },
          ],
        },
      },
    },
  ],
}

动态设置 Meta

typescript
// 动态设置页面标题和描述
export const setPageMeta = (options: {
  title: string
  description?: string
  keywords?: string
}) => {
  // #ifdef H5
  // 设置标题
  document.title = options.title

  // 设置 description
  if (options.description) {
    let descMeta = document.querySelector('meta[name="description"]')
    if (!descMeta) {
      descMeta = document.createElement('meta')
      descMeta.setAttribute('name', 'description')
      document.head.appendChild(descMeta)
    }
    descMeta.setAttribute('content', options.description)
  }

  // 设置 keywords
  if (options.keywords) {
    let keywordsMeta = document.querySelector('meta[name="keywords"]')
    if (!keywordsMeta) {
      keywordsMeta = document.createElement('meta')
      keywordsMeta.setAttribute('name', 'keywords')
      document.head.appendChild(keywordsMeta)
    }
    keywordsMeta.setAttribute('content', options.keywords)
  }
  // #endif
}

SSR 预渲染

如需更好的 SEO 效果,可以考虑:

  1. 预渲染(Prerendering) - 使用 prerender-spa-plugin
  2. SSR - 服务端渲染(需要额外配置)
  3. 静态站点生成(SSG) - 适合内容型网站

部署配置

构建命令

bash
# 构建 H5 生产版本
pnpm build:h5

# 构建产物在 dist/build/h5 目录

Nginx 配置

nginx
server {
    listen 80;
    server_name example.com;

    # 开启 gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_min_length 1k;
    gzip_comp_level 6;

    # 静态资源目录
    root /var/www/html;
    index index.html;

    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
    }

    location ~* \.(css|js)$ {
        expires 7d;
        add_header Cache-Control "public, max-age=604800";
    }

    # history 模式支持
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API 代理
    location /api/ {
        proxy_pass http://backend-server/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # WebSocket 代理
    location /ws/ {
        proxy_pass http://backend-server/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }

    # 安全头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
}

HTTPS 配置

nginx
server {
    listen 443 ssl http2;
    server_name example.com;

    # SSL 证书配置
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # SSL 优化
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # 现代协议和加密套件
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;

    # 其他配置同上
    root /var/www/html;
    ...
}

# HTTP 重定向到 HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

CDN 部署

typescript
// vite.config.ts
export default defineConfig({
  base: import.meta.env.VITE_APP_PUBLIC_PATH || '/',

  build: {
    // CDN 资源路径
    assetsDir: 'static',
    rollupOptions: {
      output: {
        // 分离 chunk
        chunkFileNames: 'static/js/[name]-[hash].js',
        entryFileNames: 'static/js/[name]-[hash].js',
        assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
      },
    },
  },
})

常见问题

1. 页面刷新 404

问题: history 模式下刷新页面显示 404

解决方案:

nginx
location / {
    try_files $uri $uri/ /index.html;
}

2. 跨域请求失败

问题: 生产环境接口请求报 CORS 错误

解决方案:

  1. 后端配置 CORS
  2. Nginx 添加 CORS 头
  3. 使用相同域名(Nginx 代理)

3. 微信分享失败

问题: 微信内分享不显示自定义内容

解决方案:

typescript
// 每次路由变化都要重新配置
router.afterEach(async (to) => {
  if (isWechatBrowser()) {
    await initWechatSdk()
    setWechatShare({
      title: to.meta.title || '默认标题',
      desc: to.meta.description || '默认描述',
      link: window.location.href,
      imgUrl: 'https://example.com/share.png',
    })
  }
})

4. iOS 橡皮筋效果

问题: iOS 页面过度滚动出现橡皮筋效果

解决方案:

scss
// 全局样式
page {
  height: 100%;
  overflow: hidden;
}

.container {
  height: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

5. 软键盘遮挡

问题: 输入框被软键盘遮挡

解决方案:

typescript
// #ifdef H5
const handleFocus = (event: Event) => {
  setTimeout(() => {
    (event.target as HTMLElement).scrollIntoView({
      behavior: 'smooth',
      block: 'center',
    })
  }, 300)
}
// #endif

最佳实践

1. 使用条件编译

typescript
// ✅ 推荐:H5 特有功能使用条件编译
// #ifdef H5
const handleCopy = async (text: string) => {
  await navigator.clipboard.writeText(text)
  uni.showToast({ title: '复制成功' })
}
// #endif

// #ifndef H5
const handleCopy = (text: string) => {
  uni.setClipboardData({
    data: text,
    success: () => uni.showToast({ title: '复制成功' }),
  })
}
// #endif

2. 封装平台差异

typescript
// utils/clipboard.ts
export const copyText = async (text: string) => {
  // #ifdef H5
  if (navigator.clipboard) {
    await navigator.clipboard.writeText(text)
  } else {
    // 降级方案
    const textarea = document.createElement('textarea')
    textarea.value = text
    document.body.appendChild(textarea)
    textarea.select()
    document.execCommand('copy')
    document.body.removeChild(textarea)
  }
  // #endif

  // #ifndef H5
  await uni.setClipboardData({ data: text })
  // #endif

  uni.showToast({ title: '复制成功' })
}

3. 优化构建体积

typescript
// vite.config.ts
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        // 分离第三方库
        'vue-vendor': ['vue', 'vue-router', 'pinia'],
        'utils-vendor': ['lodash-es', 'dayjs'],
      },
    },
  },
  // 移除 console
  terserOptions: {
    compress: {
      drop_console: true,
      drop_debugger: true,
    },
  },
}

4. 性能优化

typescript
// 路由懒加载
const routes = [
  {
    path: '/user',
    component: () => import('@/pages/user/index.vue'),
  },
]

// 图片懒加载
<image lazy-load src="xxx" />

// 列表虚拟滚动
<scroll-view enable-virtual-list />

5. 安全加固

typescript
// 防止 XSS
const sanitize = (html: string) => {
  return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
}

// 敏感信息处理
const maskPhone = (phone: string) => {
  return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}

总结

H5 平台是 UniApp 应用的重要发布渠道,通过合理的配置和优化,可以提供良好的用户体验。

关键要点:

  1. 路由模式 - 根据需求选择 history 或 hash 模式
  2. 跨域处理 - 开发环境代理,生产环境 CORS 或 Nginx 代理
  3. 浏览器兼容 - 处理 iOS/Android 差异
  4. 微信公众号 - 正确配置 JS-SDK 和授权流程
  5. SEO 优化 - 配置 Meta 信息,考虑预渲染
  6. 安全部署 - 使用 HTTPS,配置安全头

通过本文档的指导,可以顺利将 UniApp 项目部署到 H5 平台,并解决常见的兼容性和性能问题。