如何用一套代码同时实现企业网站和微信小程序
核心思路:跨端统一开发框架
通过组件化架构 + 条件编译/平台适配,实现"一次编写,多端运行"。以下是几种主流方案:
一、主流技术方案对比
| 方案 | 技术栈 | 适用场景 | 学习成本 |
|---|---|---|---|
| Uni-app | Vue.js | 最成熟,生态丰富 | ⭐⭐ |
| Taro | React/Vue | 阿里系,支持多端 | ⭐⭐⭐ |
| Remax | React | 纯原生小程序体验 | ⭐⭐⭐ |
| Flutter Web + Mini Program | Dart | 高性能UI | ⭐⭐⭐⭐ |
推荐 Uni-app(Vue生态,国内最广泛使用)
二、以 Uni-app 为例的完整实现
1. 项目结构
project/
├── pages/
│ ├── index/ # 首页
│ │ ├── index.vue
│ │ └── index.json
│ ├── about/ # 关于我们
│ │ └── about.vue
│ └── contact/ # 联系我们
│ └── contact.vue
├── components/ # 公共组件
│ ├── Header.vue
│ ├── Footer.vue
│ └── ProductCard.vue
├── static/ # 静态资源
├── utils/ # 工具函数
│ ├── api.js # API封装
│ └── format.js
├── store/ # 状态管理
├── App.vue
├── main.js
├── manifest.json # 应用配置
├── pages.json # 页面路由配置
└── package.json
2. 关键配置
pages.json — 统一路由
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
}
},
{
"path": "pages/about/about",
"style": {
"navigationBarTitleText": "关于我们"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "企业官网",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
},
"condition": { // 小程序专属启动模式
"current": 0,
"list": [
{
"name": "首页",
"path": "pages/index/index",
"query": ""
}
]
}
}
manifest.json — 多端差异化配置
{
"name": "我的企业",
"appid": "",
"description": "企业官网+小程序",
"versionName": "1.0.0",
"versionCode": "100",
"h5": {
"title": "企业官网",
"router": {
"mode": "hash",
"base": "./"
},
"devServer": {
"port": 8080
},
"sdkConfigs": {
"maps": {}
}
},
"mp-weixin": {
"appid": "wxxxxxxxxxxxxxx",
"setting": {
"urlCheck": false
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "需要获取您的位置信息"
}
}
},
"mp-alipay": {
"usingComponents": true
}
}
3. 核心代码示例
条件编译 — 处理平台差异
<template>
<view class="container">
<!-- #ifdef MP-WEIXIN -->
<button @tap="shareToFriend">分享给好友</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<a href="/about" class="link">了解更多</a>
<!-- #endif -->
<!-- 通用部分 -->
<product-card v-for="item in products" :key="item.id" :data="item" />
</view>
</template>
<script>
export default {
data() {
return {
products: []
};
},
methods: {
shareToFriend() {
// #ifdef MP-WEIXIN
wx.showShareMenu({ withShareTicket: true });
// #endif
// #ifdef H5
alert('H5端请使用浏览器分享功能');
// #endif
}
}
};
</script>
页面逻辑 — 统一数据流
<!-- pages/index/index.vue -->
<template>
<view class="page-index">
<header-component />
<!-- Banner -->
<swiper class="banner" :autoplay="true" :interval="3000" circular>
<swiper-item v-for="(img, idx) in banners" :key="idx">
<image :src="img" mode="aspectFill" class="banner-img" />
</swiper-item>
</swiper>
<!-- 产品服务 -->
<section class="services">
<view class="section-title">我们的服务</view>
<scroll-view scroll-x class="service-scroll">
<view class="service-item" v-for="s in services" :key="s.id">
<image :src="s.icon" class="service-icon" />
<text>{{ s.name }}</text>
</view>
</scroll-view>
</section>
<!-- 新闻动态 -->
<section class="news">
<view class="section-title">最新动态</view>
<news-list :list="newsList" />
</section>
<footer-component />
</view>
</template>
<script>
import headerComponent from '@/components/Header.vue';
import footerComponent from '@/components/Footer.vue';
import newsList from '@/components/NewsList.vue';
export default {
components: {
headerComponent,
footerComponent,
newsList
},
data() {
return {
banners: [],
services: [],
newsList: []
};
},
onLoad() {
this.loadData();
},
onPullDownRefresh() {
this.loadData().then(() => {
uni.stopPullDownRefresh();
});
},
methods: {
async loadData() {
try {
const [bannersRes, servicesRes, newsRes] = await Promise.all([
uni.$http.get('/api/banners'),
uni.$http.get('/api/services'),
uni.$http.get('/api/news?limit=5')
]);
this.banners = bannersRes.data;
this.services = servicesRes.data;
this.newsList = newsRes.data;
} catch (e) {
console.error('加载失败:', e);
}
}
}
};
</script>
<style scoped>
.banner {
height: 400rpx;
}
.banner-img {
width: 100%;
height: 100%;
}
.section-title {
font-size: 36rpx;
font-weight: bold;
margin: 30rpx 0;
}
.service-scroll {
white-space: nowrap;
}
.service-item {
display: inline-block;
text-align: center;
margin-right: 40rpx;
}
.service-icon {
width: 100rpx;
height: 100rpx;
}
</style>
公共组件 — 高度复用
<!-- components/ProductCard.vue -->
<template>
<view class="product-card" @tap="handleClick">
<image :src="data.image" mode="aspectFill" class="card-image" />
<view class="card-content">
<text class="card-title">{{ data.title }}</text>
<text class="card-desc">{{ data.description }}</text>
<view class="card-footer">
<text class="card-price">¥{{ data.price }}</text>
<!-- #ifdef MP-WEIXIN -->
<button size="mini" type="primary" @tap.stop="addToCart">加入购物车</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<button size="mini" type="primary" @tap.stop="addToCart">加入购物车</button>
<!-- #endif -->
</view>
</view>
</view>
</template>
<script>
export default {
props: {
data: {
type: Object,
required: true
}
},
methods: {
handleClick() {
uni.navigateTo({
url: `/pages/product/detail?id=${this.data.id}`
});
},
addToCart() {
// 购物车逻辑
uni.showToast({ title: '已添加', icon: 'success' });
}
}
};
</script>
API 统一封装
// utils/api.js
const BASE_URL = process.env.NODE_ENV === 'development'
? 'http://localhost:3000/api'
: 'https://api.yourcompany.com';
export function request(url, method = 'GET', data = {}) {
return new Promise((resolve, reject) => {
uni.request({
url: `${BASE_URL}${url}`,
method,
data,
header: {
'Content-Type': 'application/json',
// #ifdef MP-WEIXIN
'Authorization': `Bearer ${uni.getStorageSync('token')}`
// #endif
// #ifdef H5
'Authorization': `Bearer ${localStorage.getItem('token')}`
// #endif
},
success(res) {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(res);
}
},
fail(err) {
reject(err);
}
});
});
}
export const http = {
get: (url, data) => request(url, 'GET', data),
post: (url, data) => request(url, 'POST', data),
put: (url, data) => request(url, 'PUT', data),
delete: (url) => request(url, 'DELETE')
};
// 挂载到全局
uni.$http = http;
4. 登录鉴权 — 多端统一
// utils/auth.js
export function getUserInfo() {
// #ifdef MP-WEIXIN
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (loginRes) => {
uni.getUserProfile({
desc: '用于完善用户资料',
success: (userRes) => {
const token = loginRes.code; // 后端换取openId
uni.setStorageSync('token', token);
uni.setStorageSync('userInfo', userRes.userInfo);
resolve(userRes.userInfo);
},
fail: reject
});
},
fail: reject
});
});
// #endif
// #ifdef H5
return Promise.resolve(localStorage.getItem('userInfo'));
// #endif
}
5. 构建与发布
# 安装依赖
npm install
# 开发 - 微信开发者工具预览
npm run dev:mp-weixin
# 开发 - H5 网页预览
npm run dev:h5
# 构建 - 微信小程序
npm run build:mp-weixin
# 构建 - H5 网站
npm run build:h5
# 构建 - 支付宝小程序
npm run build:mp-alipay
三、关键差异处理策略
1. 导航差异
// 统一导航方法
export function navigateTo(url) {
// #ifdef MP-WEIXIN
uni.navigateTo({ url });
// #endif
// #ifdef H5
window.location.href = url;
// #endif
}
2. 支付差异
export function pay(orderId) {
// #ifdef MP-WEIXIN
uni.requestPayment({
timeStamp: '',
nonceStr: '',
package: '',
signType: 'MD5',
paySign: '',
success() {
console.log('支付成功');
},
fail(err) {
console.error('支付失败', err);
}
});
// #endif
// #ifdef H5
// 跳转至H5支付页面或调用微信支付JSAPI
window.location.href = `/pay/h5?orderId=${orderId}`;
// #endif
}
3. 地图定位差异
export function getLocation() {
// #ifdef MP-WEIXIN
return new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
success: resolve,
fail: reject
});
});
// #endif
// #ifdef H5
return new Promise((resolve, reject) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(resolve, reject);
} else {
reject(new Error('浏览器不支持定位'));
}
});
// #endif
}
四、最佳实践总结
┌─────────────────────────────────────────────┐
│ 一套代码 · 多端运行 │
├─────────────────────────────────────────────┤
│ │
│ 📦 共享层 │
│ ├─ 业务逻辑 (utils/store) │
│ ├─ 公共组件 (components/) │
│ └─ API 接口 (api.js) │
│ │
│ 🔀 条件编译 │
│ ├─ #ifdef MP-WEIXIN / #endif │
│ ├─ #ifdef H5 / #endif │
│ └─ 平台差异化处理 │
│ │
│ 🎨 样式适配 │
│ ├─ rpx 单位 (自动换算) │
│ ├─ CSS 变量 (主题切换) │
│ └─ 媒体查询 (响应式) │
│ │
│ 🚀 构建输出 │
│ ├─ dist/mp-weixin → 微信小程序 │
│ ├─ dist/h5 → H5 网站 │
│ └─ dist/mp-alipay→ 支付宝小程序 │
│ │
└─────────────────────────────────────────────┘
✅ 优势
- 代码复用率 70%~90%,维护成本大幅降低
- 统一的技术栈,团队只需掌握一门语言
- 快速迭代,新功能同时上线所有平台
⚠️ 注意事项
- 避免过度条件编译,尽量抽象共性
- 性能优化需针对各平台单独调优
- 第三方 SDK 可能不兼容所有平台
- SEO 仅 H5 有效,小程序需额外做搜索优化
这套方案已在大量企业级项目中验证,是性价比最高的跨端解决方案。
云知识CLOUD