速卖通素材
奋斗

如何用一套代码同时实现企业网站和微信小程序?

服务器

用一套代码同时实现企业网站和微信小程序

核心思路:跨平台框架 + 条件编译

通过条件编译(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 » 如何用一套代码同时实现企业网站和微信小程序?