用一套代码同时实现企业网站和微信小程序
核心思路:跨平台框架 + 条件编译
通过条件编译(Conditional Compilation)和统一业务逻辑层,实现"一次编写,多端运行"。
一、主流技术选型对比
| 方案 | 前端框架 | 小程序支持 | Web支持 | 成熟度 |
|---|---|---|---|---|
| Uni-app | Vue2/3 | ✅ 原生渲染 | ✅ H5/App | ⭐⭐⭐⭐⭐ |
| Taro | React/Vue | ✅ 多端 | ✅ H5/RN | ⭐⭐⭐⭐ |
| Remax | React | ✅ 微信专属 | ❌ | ⭐⭐⭐ |
| Flutter | Dart | ⚠️ 实验性 | ✅ Web/Mobile | ⭐⭐⭐ |
推荐 Uni-app(国内生态最完善,Vue 开发者上手最快)
二、项目架构设计
project/
├── src/
│ ├── pages/ # 页面(共享)
│ │ ├── index.vue # 首页
│ │ └── about.vue # 关于我们
│ ├── components/ # 通用组件
│ │ ├── Header.vue # 头部导航
│ │ └── ProductCard.vue # 商品卡片
│ ├── api/ # API 接口层(完全共享)
│ │ └── request.js
│ ├── utils/ # 工具函数(完全共享)
│ │ ├── format.js
│ │ └── auth.js
│ ├── store/ # 状态管理(完全共享)
│ │ └── index.js
│ ├── static/ # 静态资源
│ └── App.vue
├── uni_modules/ # Uni-app 插件
├── manifest.json # 应用配置
├── pages.json # 路由配置
├── package.json
└── vite.config.js # 构建配置
三、核心实现代码
1. 条件编译语法(关键!)
<template>
<!-- #ifdef MP-WEIXIN -->
<view class="container">
<button @tap="onLogin">微信登录</button>
</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<div class="container">
<button @click="onLogin">账号登录</button>
</div>
<!-- #endif -->
</template>
<script>
export default {
methods: {
onLogin() {
// #ifdef MP-WEIXIN
wx.login({
success: (res) => {
this.$api.loginWithWechat(res.code)
}
})
// #endif
// #ifdef H5
// H5 使用表单登录或其他认证方式
this.$api.loginWithForm(this.username, this.password)
// #endif
}
}
}
</script>
2. 统一 API 请求层(完全共享)
// src/api/request.js
import { PlatformType } from '@/utils/platform'
const BASE_URL = process.env.NODE_ENV === 'production'
? 'https://api.yourcompany.com'
: 'http://localhost:3000'
/**
* 统一的请求方法,自动适配不同平台
*/
function request(url, method = 'GET', data = {}) {
const header = {
'Content-Type': 'application/json'
}
// 获取 token(不同平台存储方式不同)
const token = getToken()
if (token) {
header['Authorization'] = `Bearer ${token}`
}
return new Promise((resolve, reject) => {
// #ifdef H5
fetch(`${BASE_URL}${url}`, {
method,
headers: header,
body: method !== 'GET' ? JSON.stringify(data) : undefined
})
.then(res => res.json())
.then(resolve)
.catch(reject)
// #endif
// #ifdef MP-WEIXIN
wx.request({
url: `${BASE_URL}${url}`,
method,
header,
data,
success: (res) => resolve(res.data),
fail: (err) => reject(err)
})
// #endif
})
}
// #ifdef H5
function getToken() {
return localStorage.getItem('token')
}
function setToken(token) {
localStorage.setItem('token', token)
}
// #endif
// #ifdef MP-WEIXIN
function getToken() {
return wx.getStorageSync('token') || ''
}
function setToken(token) {
wx.setStorageSync('token', token)
}
// #endif
export default {
get(url, params) {
return request(`${url}?${new URLSearchParams(params).toString()}`, 'GET')
},
post(url, data) {
return request(url, 'POST', data)
},
put(url, data) {
return request(url, 'PUT', data)
},
delete(url) {
return request(url, 'DELETE')
}
}
3. 业务逻辑层(完全共享,零条件编译)
// src/api/product.js
import request from './request'
/**
* 获取产品列表 —— 纯业务逻辑,无任何平台相关代码
*/
export function getProductList(page = 1, pageSize = 10) {
return request.get('/products', { page, pageSize })
}
/**
* 获取产品详情
*/
export function getProductDetail(id) {
return request.get(`/products/${id}`)
}
/**
* 提交订单
*/
export function createOrder(orderData) {
return request.post('/orders', orderData)
}
4. 页面示例:首页
<!-- src/pages/index.vue -->
<template>
<view class="page">
<!-- 顶部 Banner -->
<swiper class="banner" autoplay circular indicator-dots>
<swiper-item v-for="(item, index) in banners" :key="index">
<image :src="item.url" mode="aspectFill" />
</swiper-item>
</swiper>
<!-- 产品列表 -->
<view class="product-list">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
/>
</view>
<!-- #ifdef MP-WEIXIN -->
<view class="bottom-bar">
<navigator url="/pages/contact/contact" hover-class="none">联系我们</navigator>
</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<a href="/contact.html" class="bottom-bar">联系我们</a>
<!-- #endif -->
</view>
</template>
<script>
import { getProductList } from '@/api/product'
export default {
data() {
return {
banners: [
{ url: '/static/banner1.jpg' },
{ url: '/static/banner2.jpg' }
],
products: []
}
},
onLoad() {
this.loadProducts()
},
methods: {
async loadProducts() {
try {
const res = await getProductList(1, 10)
this.products = res.list
} catch (e) {
console.error('加载失败', e)
}
}
}
}
</script>
<style scoped>
/* 样式在两个平台基本一致 */
.page { padding: 20rpx; }
.banner { height: 400rpx; border-radius: 16rpx; overflow: hidden; }
.product-list { display: flex; flex-wrap: wrap; justify-content: space-between; }
</style>
5. 自定义组件封装(屏蔽平台差异)
<!-- src/components/ProductCard.vue -->
<template>
<!-- #ifdef MP-WEIXIN -->
<view class="card" @tap="goToDetail">
<image :src="product.image" mode="aspectFill" class="card-image" />
<view class="card-info">
<text class="title">{{ product.name }}</text>
<text class="price">¥{{ product.price }}</text>
</view>
</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<div class="card" @click="goToDetail">
<img :src="product.image" mode="aspectFill" class="card-image" />
<div class="card-info">
<span class="title">{{ product.name }}</span>
<span class="price">¥{{ product.price }}</span>
</div>
</div>
<!-- #endif -->
</template>
<script>
export default {
props: {
product: Object
},
methods: {
goToDetail() {
// #ifdef MP-WEIXIN
wx.navigateTo({
url: `/pages/product/detail?id=${this.product.id}`
})
// #endif
// #ifdef H5
window.location.href = `/product/${this.product.id}`
// #endif
}
}
}
</script>
<style scoped>
/* 两套平台共用同一套 CSS 类名 */
.card { width: 48%; margin-bottom: 20rpx; border-radius: 12rpx; overflow: hidden; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1); }
.card-image { width: 100%; height: 300rpx; }
.card-info { padding: 16rpx; }
.title { font-size: 28rpx; color: #333; display: block; margin-bottom: 8rpx; }
.price { font-size: 32rpx; color: #e74c3c; font-weight: bold; }
</style>
6. 平台检测工具
// src/utils/platform.js
export const isWeixinMiniProgram = typeof wx !== 'undefined' && wx.getSystemInfoSync
export const isH5 = typeof window !== 'undefined' && !isWeixinMiniProgram
export const platform = (() => {
if (typeof wx !== 'undefined') return 'mp-weixin'
if (typeof window !== 'undefined') return 'h5'
return 'unknown'
})()
四、构建与部署
Uni-app 构建命令
# 安装依赖
npm install
# 开发模式 - 微信小程序
npx uni-cli serve --platform mp-weixin
# 开发模式 - H5
npx uni-cli serve --platform h5
# 生产构建 - 微信小程序
npx uni-cli build --platform mp-weixin
# 生产构建 - H5
npx uni-cli build --platform h5
Vite 配置示例
// vite.config.js
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
export default defineConfig({
plugins: [uni()],
resolve: {
alias: {
'@': '/src'
}
},
build: {
rollupOptions: {
output: {
// H5 输出到 dist/build/h5
assetFileNames: 'assets/[name]-[hash][extname]'
}
}
}
})
五、需要注意的平台差异处理
常见差异及解决方案
| 差异点 | 小程序 | H5 | 解决方案 |
|---|---|---|---|
| 路由跳转 | wx.navigateTo |
window.location / Vue Router |
条件编译封装 |
| 数据存储 | wx.setStorageSync |
localStorage |
条件编译封装 |
| 图片标签 | <image> |
<img> |
条件编译或自定义组件 |
| 事件绑定 | @tap |
@click |
条件编译或自定义指令 |
| 分享功能 | onShareAppMessage |
Web Share API |
条件编译 |
| 支付 | wx.requestPayment |
支付宝/微信支付JSAPI | 后端统一下单,前端调起 |
| 位置权限 | wx.getLocation |
Geolocation API | 条件编译 |
封装平台无关的 API
// src/utils/router.js
export function navigateTo(url) {
// #ifdef MP-WEIXIN
wx.navigateTo({ url })
// #endif
// #ifdef H5
window.location.href = url
// #endif
}
export function redirectTo(url) {
// #ifdef MP-WEIXIN
wx.redirectTo({ url })
// #endif
// #ifdef H5
window.location.href = url
// #endif
}
// src/utils/storage.js
export function setStorage(key, value) {
// #ifdef MP-WEIXIN
wx.setStorageSync(key, value)
// #endif
// #ifdef H5
localStorage.setItem(key, JSON.stringify(value))
// #endif
}
export function getStorage(key) {
// #ifdef MP-WEIXIN
return wx.getStorageSync(key)
// #endif
// #ifdef H5
return JSON.parse(localStorage.getItem(key))
// #endif
}
六、最佳实践总结
✅ 做得好的地方 ❌ 避免的做法
─────────────────────────────────────────────────
• 业务逻辑完全共享 • 到处写条件编译
• 仅 UI 和平台 API 差异化 • 为每个平台维护独立代码库
• 封装平台无关的工具函数 • 直接调用平台特有 API
• 使用条件编译标记 • 运行时判断平台类型
• 后端提供统一 RESTful API • 前后端强耦合
• 组件粒度细,复用率高 • 页面级复制粘贴
代码占比估算
共享代码(业务逻辑/API/工具/状态管理): ~70%
平台特定代码(UI微调/平台API调用) : ~30%
七、替代方案:Taro(React 技术栈)
如果团队熟悉 React,可以用 Taro:
// src/pages/index.jsx
import Taro from '@tarojs/taro'
import { View, Image, Swiper, SwiperItem } from '@tarojs/components'
export default function Index() {
const [products, setProducts] = useState([])
useEffect(() => {
fetchProducts().then(setProducts)
}, [])
const goToDetail = (id) => {
Taro.navigateTo({ url: `/pages/product/detail?id=${id}` })
}
return (
<View className='page'>
<Swiper>
{banners.map((b, i) => (
<SwiperItem key={i}>
<Image src={b.url} mode='aspectFill' />
</SwiperItem>
))}
</Swiper>
{products.map(p => (
<ProductCard key={p.id} product={p} onClick={() => goToDetail(p.id)} />
))}
</View>
)
}
总结
核心原则:业务逻辑 100% 共享,UI 层最小化条件编译。通过 Uni-app/Taro 等跨平台框架,配合良好的分层架构,可以实现真正意义上的一套代码覆盖企业官网和微信小程序,开发效率提升 2~3 倍。
轻量云Cloud