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:
- 按 F12 打开开发者工具
- 切换到移动设备模式(Ctrl+Shift+M)
- 选择设备类型或自定义尺寸
- 查看 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)
// #endif3. 安卓键盘遮挡输入框:
typescript
// 监听软键盘
// #ifdef H5
window.addEventListener('resize', () => {
if (document.activeElement?.tagName === 'INPUT') {
setTimeout(() => {
document.activeElement?.scrollIntoView({ behavior: 'smooth' })
}, 100)
}
})
// #endif4. 图片加载失败:
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>微信公众号适配
公众号授权登录
配置公众号:
- 登录微信公众平台
- 开发 -> 接口权限 -> 网页授权
- 填写授权回调域名
发起授权:
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 效果,可以考虑:
- 预渲染(Prerendering) - 使用 prerender-spa-plugin
- SSR - 服务端渲染(需要额外配置)
- 静态站点生成(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 错误
解决方案:
- 后端配置 CORS
- Nginx 添加 CORS 头
- 使用相同域名(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: '复制成功' }),
})
}
// #endif2. 封装平台差异
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 应用的重要发布渠道,通过合理的配置和优化,可以提供良好的用户体验。
关键要点:
- 路由模式 - 根据需求选择 history 或 hash 模式
- 跨域处理 - 开发环境代理,生产环境 CORS 或 Nginx 代理
- 浏览器兼容 - 处理 iOS/Android 差异
- 微信公众号 - 正确配置 JS-SDK 和授权流程
- SEO 优化 - 配置 Meta 信息,考虑预渲染
- 安全部署 - 使用 HTTPS,配置安全头
通过本文档的指导,可以顺利将 UniApp 项目部署到 H5 平台,并解决常见的兼容性和性能问题。
