本教程将带领开发者从零搭建Uni-app项目,适配微信小程序端,集成小程序核心功能(如用户登录、本地存储、页面路由、接口请求等),最终完成项目打包、预览与发布。全程配套详细实操步骤与完整代码示例,适合前端新手或需要快速落地微信小程序项目的开发者,无需单独学习小程序原生语法,依托Uni-app跨端能力高效开发。

一、前置知识与环境准备

1.1 必备知识

  • 基础HTML/CSS/JavaScript语法

  • Vue.js基础(Uni-app基于Vue语法,推荐Vue3+TypeScript)

  • 微信小程序基本概念(AppID、小程序后台、分包加载等)

1.2 环境搭建

1.2.1 安装Uni-app开发工具(HBuilderX)

Uni-app推荐使用HBuilderX开发,内置Uni-app编译环境与小程序打包工具,操作简单:

  1. 下载HBuilderX:https://www.dcloud.io/hbuilderx.html,选择「正式版」,根据系统(Windows/Mac)选择对应版本

  2. 安装完成后打开HBuilderX,进入「工具」-「插件安装」,搜索「Uni-app」插件(默认已安装),确保插件启用

1.2.2 安装微信开发者工具

用于预览、调试Uni-app编译后的微信小程序,以及最终提交审核:

  1. 下载微信开发者工具:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html,选择对应系统版本

  2. 安装完成后打开,登录微信账号(需与小程序开发者账号绑定),开启「服务端口」:进入「设置」-「安全设置」,勾选「开启服务端口」(用于HBuilderX联动调试)

1.2.3 微信小程序账号与AppID准备

开发小程序需提前注册账号并获取AppID(测试与发布必需):

  1. 注册/登录微信小程序后台:https://mp.weixin.qq.com/(选择「小程序」类型账号)

  2. 获取AppID:登录后进入「开发」-「开发设置」,记录「AppID(小程序ID)」(后续项目配置需用到)

  3. 添加开发者:进入「成员管理」-「开发者/体验者」,添加当前开发微信账号,获得开发权限

二、创建Uni-app+微信小程序项目

2.1 初始化Uni-app项目

  1. 打开HBuilderX,点击「文件」-「新建」-「项目」

  2. 选择「Uni-app」模板,输入项目名称(如「uniapp-wechat-miniprogram」),框架选择「Vue3」,勾选「TypeScript」(可选,推荐),模板选择「默认模板」,点击「创建」

  3. 项目核心目录说明:

    • pages:存放页面文件(每个页面对应一个文件夹,包含vue、json、js等文件)

    • static:存放静态资源(图片、字体等,注意:小程序端静态资源大小有限制,建议分包或使用CDN)

    • main.ts:项目入口文件(初始化Vue实例、引入全局依赖)

    • pages.json:Uni-app核心配置文件(配置页面路由、窗口样式、tabBar等)

    • manifest.json:项目配置文件(配置小程序AppID、权限等)

    • uni_modules:存放Uni-app插件(可通过插件市场下载复用)

2.2 配置小程序基础信息(manifest.json)

打开项目根目录的「manifest.json」,配置小程序相关信息(确保能正常编译与预览):

  1. 点击「微信小程序配置」选项卡,勾选「已申请AppID」,填入第一步获取的「小程序AppID」

  2. 配置「小程序项目名称」「小程序官方主页」(可选,发布时需一致)

  3. 权限配置:根据项目需求添加权限(如获取用户信息、地理位置等),示例:勾选「获取用户信息」「获取当前位置」

2.3 本地运行小程序预览

将Uni-app项目编译为微信小程序代码,并在微信开发者工具中预览:

  1. 右键项目根目录,选择「运行」-「运行到小程序模拟器」-「微信开发者工具」

  2. HBuilderX会自动编译项目,生成小程序代码(默认路径:unpackage/dist/dev/mp-weixin),并自动打开微信开发者工具加载项目

  3. 若未自动打开,手动打开微信开发者工具,选择「导入项目」,导入上述编译后的mp-weixin文件夹,点击「预览」,即可看到Uni-app默认首页(若出现白屏,检查AppID是否配置正确、开发者账号是否有权限)

三、Uni-app适配小程序核心配置

3.1 页面路由配置(pages.json)

pages.json用于配置小程序的页面路由、窗口样式、tabBar等,示例配置(包含首页、列表页、详情页、个人中心):


{
  "pages": [
    {
      "path": "pages/index/index", // 首页路径
      "style": {
        "navigationBarTitleText": "首页", // 导航栏标题
        "navigationBarBackgroundColor": "#FFFFFF", // 导航栏背景色
        "navigationBarTextStyle": "black" // 导航栏文字颜色(black/white)
      }
    },
    {
      "path": "pages/list/list",
      "style": {
        "navigationBarTitleText": "列表页"
      }
    },
    {
      "path": "pages/detail/detail",
      "style": {
        "navigationBarTitleText": "详情页"
      }
    },
    {
      "path": "pages/mine/mine",
      "style": {
        "navigationBarTitleText": "个人中心"
      }
    }
  ],
  "globalStyle": {
    "navigationBarBackgroundColor": "#FFFFFF",
    "navigationBarTextStyle": "black",
    "backgroundColor": "#F5F5F5" // 页面背景色
  },
  "tabBar": { // 底部tabBar配置(可选,用于多tab页面切换)
    "color": "#666666", // 未选中文字颜色
    "selectedColor": "#1E88E5", // 选中文字颜色
    "backgroundColor": "#FFFFFF", // 背景色
    "borderStyle": "black", // 边框颜色(black/white)
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/tabbar/home.png", // 未选中图标
        "selectedIconPath": "static/tabbar/home-active.png" // 选中图标
      },
      {
        "pagePath": "pages/list/list",
        "text": "列表",
        "iconPath": "static/tabbar/list.png",
        "selectedIconPath": "static/tabbar/list-active.png"
      },
      {
        "pagePath": "pages/mine/mine",
        "text": "我的",
        "iconPath": "static/tabbar/mine.png",
        "selectedIconPath": "static/tabbar/mine-active.png"
      }
    ]
  }
}

3.2 小程序特有配置(page.json)

若单个页面需要特殊配置(如禁用下拉刷新、自定义导航栏),可在对应页面文件夹下创建「page.json」文件,示例(详情页禁用下拉刷新):


{
  "enablePullDownRefresh": false, // 禁用下拉刷新
  "disableScroll": false, // 允许页面滚动
  "navigationStyle": "default" // 默认导航栏(可设为custom自定义)
}

3.3 封装全局请求工具(utils/request.ts)

基于uni.request封装全局请求工具,处理请求拦截、响应拦截、错误提示,适配小程序端网络请求限制:


import { showToast } from 'uni-app';

// 基础接口地址(开发环境/生产环境可区分配置)
const BASE_URL = process.env.NODE_ENV === 'development' ? 'https://test-api.example.com' : 'https://api.example.com';

// 封装请求函数
const request = <T = any>(options: {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  data?: any;
  header?: any;
}) => {
  return new Promise<T>((resolve, reject) => {
    uni.request({
      url: BASE_URL + options.url,
      method: options.method || 'GET',
      data: options.data || {},
      header: {
        'Content-Type': 'application/json',
        // 可添加全局token(如从本地存储获取)
        'Authorization': uni.getStorageSync('token') || '',
        ...options.header
      },
      success: (res) => {
        const statusCode = res.statusCode;
        // 响应状态码判断
        if (statusCode === 200) {
          const data = res.data as T;
          resolve(data);
        } else {
          showToast({
            title: res.data?.message || `请求失败(${statusCode})`,
            icon: 'none',
            duration: 2000
          });
          reject(res.data);
        }
      },
      fail: (err) => {
        showToast({
          title: '网络错误,请稍后重试',
          icon: 'none',
          duration: 2000
        });
        reject(err);
      }
    });
  });
};

// 封装GET请求
export const get = <T = any>(url: string, params?: any, header?: any) => {
  return request<T>({ url, method: 'GET', params, header });
};

// 封装POST请求
export const post = <T = any>(url: string, data?: any, header?: any) => {
  return request<T>({ url, method: 'POST', data, header });
};

export default request;

3.4 封装微信登录工具(utils/wechat.ts)

封装小程序用户登录逻辑(获取code、换取openid、存储用户信息),需后端配合实现code验证接口:


import { showToast, showLoading } from 'uni-app';
import { post } from './request';

// 微信登录响应类型
interface LoginResponse {
  code: number;
  message: string;
  data: {
    token: string;
    openid: string;
    userInfo: {
      nickname: string;
      avatarUrl: string;
    };
  };
}

/**
 * 微信小程序登录(获取code并换取用户信息)
 */
export const wechatLogin = async () => {
  try {
    showLoading({ title: '登录中...' });
    // 1. 获取微信登录code(小程序特有接口)
    const { code } = await uni.login();
    if (!code) {
      throw new Error('获取登录code失败');
    }

    // 2. 调用后端接口,用code换取openid、token、用户信息
    const res = await post<LoginResponse>('/api/wechat/login', { code });
    if (res.code === 200) {
      const { token, openid, userInfo } = res.data;
      // 3. 存储用户信息到本地(小程序本地存储)
      uni.setStorageSync('token', token);
      uni.setStorageSync('openid', openid);
      uni.setStorageSync('userInfo', userInfo);
      showToast({ title: '登录成功' });
      return userInfo;
    } else {
      throw new Error(res.message || '登录失败');
    }
  } catch (error) {
    const errMsg = (error as Error).message || '登录异常';
    showToast({ title: errMsg, icon: 'none' });
    throw error;
  } finally {
    uni.hideLoading();
  }
};

/**
 * 退出登录(清除本地存储)
 */
export const wechatLogout = () => {
  uni.removeStorageSync('token');
  uni.removeStorageSync('openid');
  uni.removeStorageSync('userInfo');
  showToast({ title: '退出登录成功' });
};

/**
 * 获取本地存储的用户信息
 */
export const getUserInfo = () => {
  return uni.getStorageSync('userInfo') || null;
};

3.5 小程序分包加载配置

微信小程序主包体积限制为2M,当项目较大(含多页面、静态资源)时,需通过分包加载拆分代码,降低主包体积。Uni-app支持自动适配小程序分包语法,核心通过pages.json配置subPackages实现。

3.5.1 分包目录结构设计

推荐将非首页、非tabBar页面拆分至分包,示例目录结构:


pages/                  // 主包(存放首页、tabBar页面)
  ├─ index/index.vue    // 首页(主包必需)
  ├─ list/list.vue      // 列表页(tabBar页面,主包)
  ├─ mine/mine.vue      // 个人中心(tabBar页面,主包)
  └─ detail/detail.vue  // 详情页(可放主包或分包)
subPackages/            // 分包根目录(自定义名称,需在pages.json配置)
  ├─ packageA/          // 分包A(例如:关于我们、帮助中心等辅助页面)
  │  └─ about/about.vue
  │  └─ help/help.vue
  └─ packageB/          // 分包B(例如:商品相关、订单相关页面)
     └─ goods/goods.vue
     └─ order/order.vue
3.5.2 pages.json分包配置

在pages.json中新增subPackages字段,配置分包路径、根目录及页面路由,示例:


{
  "pages": [
    {
      "path": "pages/index/index",
      "style": { "navigationBarTitleText": "首页" }
    },
    {
      "path": "pages/list/list",
      "style": { "navigationBarTitleText": "列表页" }
    },
    {
      "path": "pages/mine/mine",
      "style": { "navigationBarTitleText": "个人中心" }
    },
    {
      "path": "pages/detail/detail",
      "style": { "navigationBarTitleText": "详情页" }
    }
  ],
  "subPackages": [
    {
      "root": "subPackages/packageA",  // 分包A根目录
      "pages": [
        {
          "path": "about/about",       // 页面路径(相对分包根目录,完整路径:subPackages/packageA/about/about)
          "style": { "navigationBarTitleText": "关于我们" }
        },
        {
          "path": "help/help",
          "style": { "navigationBarTitleText": "帮助中心" }
        }
      ]
    },
    {
      "root": "subPackages/packageB",  // 分包B根目录
      "pages": [
        {
          "path": "goods/goods",
          "style": { "navigationBarTitleText": "商品详情" }
        },
        {
          "path": "order/order",
          "style": { "navigationBarTitleText": "我的订单" }
        }
      ]
    }
  ],
  "globalStyle": { /* 原有配置不变 */ },
  "tabBar": { /* 原有配置不变 */ }
}
3.5.3 分包跳转与注意事项
  • 跳转分包页面:与主包页面跳转语法一致,直接使用完整路径或相对路径,示例:// 方式1:使用完整路径 uni.navigateTo({ url: '/subPackages/packageA/about/about' }); // 方式2:使用相对路径(从当前页面出发) uni.navigateTo({ url: '../../subPackages/packageA/about/about' });

  • 注意事项:tabBar页面必须放在主包,不能放在分包中

  • 分包之间、分包与主包之间可自由跳转,但需使用正确的跳转API(navigateTo/redirectTo等,不支持switchTab跳分包页面)

  • 分包体积限制:单个分包体积≤2M,所有分包总和≤20M

  • 静态资源:分包内的静态资源建议放在对应分包目录下,避免占用主包体积

四、核心功能开发实战

4.1 首页开发(pages/index/index.vue)

实现功能:页面加载时自动登录、展示用户信息、配置下拉刷新、添加页面跳转按钮:


<template>
  <view class="index-container">
    <!-- 用户信息展示 -->
    <view class="user-info" v-if="userInfo">
      <image :src="userInfo.avatarUrl" class="avatar"></image>
      <view class="nickname">{{ userInfo.nickname }}</view>
    </view>
    <view class="btn-group">
      <button @click="toListPage" class="btn">进入列表页</button>
      <button @click="logout" class="btn logout-btn">退出登录</button>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'uni-app';
import { wechatLogin, wechatLogout, getUserInfo } from '@/utils/wechat';

const router = useRouter();
const userInfo = ref<any>(null);

// 页面加载时初始化
onMounted(() => {
  initPage();
  // 配置下拉刷新
  uni.startPullDownRefresh();
});

// 初始化页面(获取用户信息,未登录则自动登录)
const initPage = async () => {
  const localUserInfo = getUserInfo();
  if (localUserInfo) {
    userInfo.value = localUserInfo;
  } else {
    // 未登录,自动触发微信登录
    const info = await wechatLogin();
    userInfo.value = info;
  }
  // 停止下拉刷新
  uni.stopPullDownRefresh();
};

// 跳转列表页
const toListPage = () => {
  router.push('/pages/list/list');
};

// 退出登录
const logout = () => {
  wechatLogout();
  userInfo.value = null;
};

</script>

<style scoped>
.index-container {
  padding: 20rpx;
  box-sizing: border-box;
  min-height: 100vh;
  background-color: #F5F5F5;
}
.user-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 40rpx;
  padding: 30rpx;
  background-color: #FFFFFF;
  border-radius: 20rpx;
}
.avatar {
  width: 160rpx;
  height: 160rpx;
  border-radius: 50%;
  margin-bottom: 20rpx;
}
.nickname {
  font-size: 32rpx;
  font-weight: bold;
  color: #333333;
}
.btn-group {
  display: flex;
  flex-direction: column;
  gap: 30rpx;
}
.btn {
  width: 100%;
  height: 80rpx;
  line-height: 80rpx;
  font-size: 32rpx;
  background-color: #1E88E5;
  color: #FFFFFF;
  border-radius: 40rpx;
}
.logout-btn {
  background-color: #FF4D4F;
}
</style>

4.2 列表页开发(pages/list/list.vue)

实现功能:请求接口获取列表数据、下拉刷新、上拉加载更多、点击跳转详情页:


<template>
  <view class="list-container">
    <!-- 列表项 -->
    <view class="list-item" v-for="(item, index) in listData" :key="index" @click="toDetailPage(item.id)">
      <view class="item-title">{{ item.title }}</view>
      <view class="item-desc">{{ item.desc }}</view>
      <view class="item-time">{{ item.createTime }}</view>
    </view>
    <!-- 加载提示 -->
    <view class="load-more" v-if="loading">加载中...</view>
    <view class="no-more" v-if="noMore">没有更多数据了</view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'uni-app';
import { get } from '@/utils/request';

const router = useRouter();
// 列表数据
const listData = ref<Array<{
  id: number;
  title: string;
  desc: string;
  createTime: string;
}>>([]);
// 分页参数
const page = ref(1);
const pageSize = ref(10);
// 加载状态
const loading = ref(false);
const noMore = ref(false);

// 页面加载时获取列表数据
onMounted(() => {
  getListData();
  // 监听下拉刷新
  uni.onPullDownRefresh(() => {
    refreshList();
  });
  // 监听上拉加载更多
  uni.onReachBottom(() => {
    loadMoreData();
  });
});

// 获取列表数据
const getListData = async () => {
  try {
    loading.value = true;
    const res = await get<{
      code: number;
      data: {
        list: typeof listData.value;
        total: number;
      };
    }>('/api/list', { page: page.value, pageSize: pageSize.value });
    if (res.code === 200) {
      const { list, total } = res.data;
      if (page.value === 1) {
        // 第一页,覆盖数据
        listData.value = list;
      } else {
        // 非第一页,追加数据
        listData.value = [...listData.value, ...list];
      }
      // 判断是否还有更多数据
      noMore.value = listData.value.length >= total;
    }
  } catch (error) {
    console.error('获取列表数据失败:', error);
  } finally {
    loading.value = false;
    uni.stopPullDownRefresh();
  }
};

// 下拉刷新
const refreshList = () => {
  page.value = 1;
  noMore.value = false;
  getListData();
};

// 上拉加载更多
const loadMoreData = () => {
  if (!loading.value && !noMore.value) {
    page.value++;
    getListData();
  }
};

// 跳转详情页(携带参数)
const toDetailPage = (id: number) => {
  router.push({
    path: '/pages/detail/detail',
    query: { id } // 携带列表项ID
  });
};
</script>

<style scoped>
.list-container {
  padding: 20rpx;
  box-sizing: border-box;
  background-color: #F5F5F5;
  min-height: 100vh;
}
.list-item {
  padding: 20rpx;
  margin-bottom: 20rpx;
  background-color: #FFFFFF;
  border-radius: 20rpx;
}
.item-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #333333;
  margin-bottom: 10rpx;
}
.item-desc {
  font-size: 28rpx;
  color: #666666;
  margin-bottom: 10rpx;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.item-time {
  font-size: 24rpx;
  color: #999999;
}
.load-more, .no-more {
  text-align: center;
  padding: 20rpx;
  font-size: 28rpx;
  color: #999999;
}
</style>

4.3 详情页开发(pages/detail/detail.vue)

实现功能:接收列表页参数、请求详情数据、展示数据、返回上一页:


<template>
  <view class="detail-container" v-if="detailData">
    <view class="detail-title">{{ detailData.title }}</view>
    <view class="detail-time">{{ detailData.createTime }}</view>
    <view class="detail-content">{{ detailData.content }}</view>
    <button @click="goBack" class="back-btn">返回上一页</button>
  </view>
  <view class="loading" v-else>加载中...</view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute } from 'uni-app';
import { get } from '@/utils/request';

const route = useRoute();
// 详情数据
const detailData = ref<{
  id: number;
  title: string;
  createTime: string;
  content: string;
}>(null);

// 页面加载时获取详情数据
onMounted(() => {
  const id = route.query.id as string;
  if (id) {
    getDetailData(Number(id));
  } else {
    uni.showToast({ title: '参数错误', icon: 'none' });
    uni.navigateBack();
  }
});

// 获取详情数据
const getDetailData = async (id: number) => {
  try {
    const res = await get<{
      code: number;
      data: typeof detailData.value;
    }>(`/api/detail/${id}`);
    if (res.code === 200) {
      detailData.value = res.data;
    }
  } catch (error) {
    console.error('获取详情数据失败:', error);
    uni.showToast({ title: '获取详情失败', icon: 'none' });
  }
};

// 返回上一页
const goBack = () => {
  uni.navigateBack({ delta: 1 });
};
</script>

<style scoped>
.detail-container {
  padding: 20rpx;
  box-sizing: border-box;
  background-color: #FFFFFF;
  min-height: 100vh;
}
.detail-title {
  font-size: 36rpx;
  font-weight: bold;
  color: #333333;
  margin-bottom: 20rpx;
  text-align: center;
}
.detail-time {
  font-size: 24rpx;
  color: #999999;
  text-align: center;
  margin-bottom: 30rpx;
}
.detail-content {
  font-size: 28rpx;
  color: #666666;
  line-height: 48rpx;
}
.back-btn {
  width: 100%;
  height: 80rpx;
  line-height: 80rpx;
  font-size: 32rpx;
  background-color: #1E88E5;
  color: #FFFFFF;
  border-radius: 40rpx;
  margin-top: 40rpx;
}
.loading {
  text-align: center;
  padding: 50rpx;
  font-size: 32rpx;
  color: #666666;
}
</style>

4.4 个人中心开发(pages/mine/mine.vue)

实现功能:展示本地存储的用户信息、退出登录、跳转关于页面:


<template>
  <view class="mine-container">
    <!-- 用户信息卡片 -->
    <view class="user-card">
      <image :src="userInfo?.avatarUrl || '/static/default-avatar.png'" class="avatar"></image>
      <view class="user-name">{{ userInfo?.nickname || '未登录' }}</view>
    </view>
    <!-- 功能列表 -->
    <view class="func-list">
      <view class="func-item" @click="goToAbout">
        <view class="item-text">关于我们</view>
        <image src="/static/arrow-right.png" class="arrow-icon"></image>
      </view>
      <view class="func-item" @click="logout" v-if="userInfo">
        <view class="item-text" style="color: #FF4D4F;">退出登录</view>
      </view>
      <view class="func-item" @click="login" v-else>
        <view class="item-text" style="color: #1E88E5;">立即登录</view>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'uni-app';
import { wechatLogin, wechatLogout, getUserInfo } from '@/utils/wechat';

const router = useRouter();
const userInfo = ref<any>(null);

// 页面加载时获取用户信息
onMounted(() => {
  getUserInfoFromLocal();
});

// 从本地存储获取用户信息
const getUserInfoFromLocal = () => {
  const info = getUserInfo();
  userInfo.value = info;
};

// 登录
const login = async () => {
  const info = await wechatLogin();
  userInfo.value = info;
};

// 退出登录
const logout = () => {
  wechatLogout();
  userInfo.value = null;
};

// 跳转关于我们页面
const goToAbout = () => {
  router.push('/pages/about/about');
};
</script>

<style scoped>
.mine-container {
  padding: 0;
  box-sizing: border-box;
  background-color: #F5F5F5;
  min-height: 100vh;
}
.user-card {
  background-color: #1E88E5;
  padding: 40rpx 20rpx;
  text-align: center;
  color: #FFFFFF;
}
.avatar {
  width: 160rpx;
  height: 160rpx;
  border-radius: 50%;
  border: 4rpx solid #FFFFFF;
  margin-bottom: 20rpx;
}
.user-name {
  font-size: 32rpx;
  font-weight: bold;
}
.func-list {
  margin-top: 20rpx;
  background-color: #FFFFFF;
}
.func-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 20rpx;
  height: 88rpx;
  border-bottom: 1rpx solid #F5F5F5;
}
.item-text {
  font-size: 32rpx;
  color: #333333;
}
.arrow-icon {
  width: 24rpx;
  height: 40rpx;
}
</style>

五、高级功能集成:小程序支付

小程序支付需微信商户号与小程序账号绑定,核心流程:前端发起支付请求→后端生成预支付订单→前端调用微信支付API→支付结果回调处理。以下是Uni-app适配小程序支付的完整实现。

5.1 前置准备

  • 注册微信商户号:https://pay.weixin.qq.com/,完成实名认证

  • 绑定小程序:在商户号后台「产品中心」-「AppID授权管理」,绑定当前小程序AppID

  • 获取商户密钥(key):在商户号后台「账户中心」-「API安全」,设置API密钥(后续后端接口需使用)

  • 后端准备:开发支付相关接口(创建预支付订单、查询支付结果、接收支付回调通知)

5.2 封装支付工具(utils/pay.ts)

封装小程序支付核心逻辑,包括调用后端预支付接口、唤起微信支付面板、处理支付结果:


import { showToast, showLoading, requestPayment } from 'uni-app';
import { post } from './request';

// 预支付订单响应类型(后端返回)
interface PrepayResponse {
  code: number;
  message: string;
  data: {
    timeStamp: string;    // 时间戳(微信支付要求为字符串)
    nonceStr: string;     // 随机字符串
    package: string;      // 预支付订单号(格式:prepay_id=xxx)
    signType: 'MD5' | 'HMAC-SHA256'; // 签名类型
    paySign: string;      // 支付签名
  };
}

/**
 * 小程序支付核心函数
 * @param orderId 订单ID(前端传入,用于后端查询订单信息)
 * @returns 支付结果(成功/失败)
 */
export const wechatPay = async (orderId: number) => {
  try {
    showLoading({ title: '发起支付中...' });
    // 1. 调用后端接口,获取预支付订单信息(核心参数由后端生成)
    const res = await post<PrepayResponse>('/api/pay/createOrder', { orderId });
    if (res.code !== 200) {
      throw new Error(res.message || '获取预支付订单失败');
    }
    const { timeStamp, nonceStr, package: prepayPackage, signType, paySign } = res.data;

    // 2. 调用微信支付API(Uni-app封装的requestPayment,自动适配小程序)
    const payResult = await requestPayment({
      provider: 'weixin',  // 支付提供商(小程序固定为weixin)
      timeStamp,
      nonceStr,
      package: prepayPackage,
      signType,
      paySign
    });

    // 3. 支付成功处理(实际业务中需再次调用后端接口验证支付结果,避免前端伪造)
    showToast({ title: '支付成功' });
    // 验证支付结果(可选,增强安全性)
    const verifyRes = await post<{ code: number; message: string }>('/api/pay/verify', { orderId });
    if (verifyRes.code === 200) {
      return true; // 支付成功且验证通过
    } else {
      throw new Error('支付结果验证失败,请稍后查询订单状态');
    }
  } catch (error) {
    const errMsg = (error as Error).message || '支付失败';
    // 区分用户主动取消和其他错误
    if (errMsg.includes('cancel') || errMsg.includes('取消')) {
      showToast({ title: '已取消支付', icon: 'none' });
    } else {
      showToast({ title: errMsg, icon: 'none' });
    }
    throw error;
  } finally {
    uni.hideLoading();
  }
};

5.3 支付页面开发(示例:订单确认页)

在订单确认页添加支付按钮,调用上述支付工具,示例代码(pages/order/confirm.vue):


<template>
  <view class="order-confirm-container">
    <!-- 订单信息展示 -->
    <view class="order-info">
      <view class="order-item">订单ID:{{ orderId }}</view>
      <view class="order-item">订单金额:¥{{ amount.toFixed(2) }}</view>
    </view>
    <!-- 支付按钮 -->
    <button @click="handlePay" class="pay-btn">立即支付</button>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'uni-app';
import { wechatPay } from '@/utils/pay';
import { get } from '@/utils/request';

const route = useRoute();
const router = useRouter();
const orderId = ref<number>(0);
const amount = ref<number>(0);

// 页面加载时获取订单信息
onMounted(() => {
  const id = route.query.orderId as string;
  if (id) {
    orderId.value = Number(id);
    getOrderInfo();
  } else {
    uni.showToast({ title: '订单参数错误', icon: 'none' });
    uni.navigateBack();
  }
});

// 获取订单信息
const getOrderInfo = async () => {
  try {
    const res = await get<{
      code: number;
      data: { orderId: number; amount: number };
    }>(`/api/order/info/${orderId.value}`);
    if (res.code === 200) {
      amount.value = res.data.amount;
    }
  } catch (error) {
    console.error('获取订单信息失败:', error);
  }
};

// 处理支付
const handlePay = async () => {
  try {
    const paySuccess = await wechatPay(orderId.value);
    if (paySuccess) {
      // 支付成功后跳转至订单详情页
      router.push(`/pages/order/detail?orderId=${orderId.value}`);
    }
  } catch (error) {
    console.error('支付失败:', error);
  }
};
</script>

<style scoped>
.order-confirm-container {
  padding: 20rpx;
  box-sizing: border-box;
  min-height: 100vh;
  background-color: #F5F5F5;
}
.order-info {
  background-color: #FFFFFF;
  padding: 20rpx;
  border-radius: 20rpx;
  margin-bottom: 40rpx;
}
.order-item {
  font-size: 28rpx;
  color: #333333;
  margin-bottom: 15rpx;
}
.pay-btn {
  width: 100%;
  height: 80rpx;
  line-height: 80rpx;
  font-size: 32rpx;
  background-color: #FF4D4F;
  color: #FFFFFF;
  border-radius: 40rpx;
}
</style>

5.4 支付结果回调处理

  • 前端回调:上述wechatPay函数中,支付成功后会触发resolve,可在此处跳转页面、更新订单状态

  • 后端回调:微信支付成功后,会向商户号后台配置的「回调通知地址」发送POST请求,后端需处理该回调(验证签名、更新订单状态、记录支付日志),前端可通过轮询或WebSocket获取最终订单状态(避免前端回调不可靠问题)

5.5 注意事项

  • 支付接口域名需配置:在小程序后台「开发」-「开发设置」-「服务器域名」,添加支付相关接口域名(需备案,HTTPS)

  • 签名验证:前端无需处理签名生成,所有签名(paySign)由后端基于商户密钥生成,确保安全性

  • 测试环境:可使用微信支付沙箱环境(商户号后台申请)进行测试,避免真实支付

  • 错误处理:需兼容用户主动取消支付、网络异常、订单过期等多种异常场景


<template>
  <view class="detail-container" v-if="detailData">
    <!-- 自定义分享按钮(需设置open-type="share") -->
    <button open-type="share" class="share-btn">分享给好友</button>
    <!-- 页面原有内容 -->
    <view class="detail-title">{{ detailData.title }}</view>
    <!-- 省略其他内容... -->
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute } from 'uni-app';
import { get } from '@/utils/request';
import { getShareConfig } from '@/utils/share';

const route = useRoute();
const detailData = ref<{
  id: number;
  title: string;
  content: string;
  coverImage: string;
}>(null);

// 页面加载时获取详情数据
onMounted(() => {
  const id = route.query.id as string;
  if (id) {
    getDetailData(Number(id));
  }
});

const getDetailData = async (id: number) => {
  // 省略原有逻辑...
};

// 1. 配置右上角菜单分享(好友)
const onShareAppMessage = () => {
  if (detailData.value) {
    // 动态拼接分享路径(携带详情页id,确保跳转后能正确加载数据)
    const sharePath = `/pages/detail/detail?id=${detailData.value.id}`;
    return getShareConfig(
      detailData.value.title, // 用文章标题作为分享标题
      sharePath,
      detailData.value.coverImage // 用文章封面作为分享图片
    );
  }
  // 数据未加载完成时的默认配置
  return getShareConfig();
};

// 2. 配置右上角菜单分享(朋友圈)- 可选
const onShareTimeline = () => {
  if (detailData.value) {
    return {
      title: detailData.value.title,
      query: `id=${detailData.value.id}`, // 分享参数
      imageUrl: detailData.value.coverImage
    };
  }
  return {
    title: '默认分享标题',
    query: '',
    imageUrl: '/static/share-default.png'
  };
};

// 暴露生命周期函数(Vue3 setup语法必需)
defineExpose({
  onShareAppMessage,
  onShareTimeline
});
</script>

<style scoped>
/* 新增分享按钮样式 */
.share-btn {
  width: 100%;
  height: 80rpx;
  line-height: 80rpx;
  font-size: 32rpx;
  background-color: #1E88E5;
  color: #FFFFFF;
  border-radius: 40rpx;
  margin: 20rpx 0;
}
/* 省略其他样式... */
</style>

六、常用扩展功能集成

6.1 微信地图定位功能

实现获取用户当前地理位置、解析地理位置信息(如省市区),需用到微信小程序getLocation接口和腾讯地图SDK(可选,用于逆地址解析)。

6.1.1 前置准备
  • 在小程序后台「开发」-「开发设置」-「接口设置」中,开启「获取当前地理位置」接口

  • 若需解析地址,注册腾讯地图开发者账号,申请WebService API密钥(https://lbs.qq.com/)

  • 在manifest.json中添加定位权限:微信小程序配置→权限配置→勾选「获取当前位置」

6.1.2 封装定位工具(utils/location.ts)

import { showToast, showLoading, getLocation } from 'uni-app';
import { get } from './request';

// 腾讯地图SDK基础地址(用于逆地址解析)
const TENCENT_MAP_API = 'https://apis.map.qq.com/ws/geocoder/v1/';
// 替换为自己的腾讯地图API密钥
const TENCENT_MAP_KEY = 'YOUR_TENCENT_MAP_KEY';

/**
 * 获取用户当前地理位置(经纬度+地址信息)
 * @param type 定位类型:wgs84(GPS坐标)、gcj02(国测局坐标,小程序默认)
 * @returns 地理位置信息
 */
export const getUserLocation = async (type: 'wgs84' | 'gcj02' = 'gcj02') => {
  try {
    showLoading({ title: '获取位置中...' });
    // 1. 获取经纬度
    const { latitude, longitude } = await getLocation({ type });
    if (!latitude || !longitude) {
      throw new Error('获取经纬度失败');
    }

    // 2. 调用腾讯地图API解析地址(可选)
    const res = await get<{
      status: number;
      result: {
        address: string; // 详细地址
        address_component: {
          province: string; // 省
          city: string; // 市
          district: string; // 区
          street: string; // 街道
        };
      };
    }>(TENCENT_MAP_API, {
      location: `${latitude},${longitude}`,
      key: TENCENT_MAP_KEY,
      get_poi: 0 // 不获取周边POI
    });

    if (res.status !== 0) {
      throw new Error('地址解析失败');
    }

    const { address, address_component } = res.result;
    return {
      latitude,
      longitude,
      address,
      province: address_component.province,
      city: address_component.city,
      district: address_component.district
    };
  } catch (error) {
    const errMsg = (error as Error).message || '获取位置失败';
    showToast({ title: errMsg, icon: 'none' });
    throw error;
  } finally {
    uni.hideLoading();
  }
};
6.1.3 页面中使用定位功能

<template>
  <view class="location-container">
    <button @click="getLocation" class="location-btn">获取当前位置</button>
    <view class="location-info" v-if="locationData">
      <view>当前地址:{{ locationData.address }}</view>
      <view>经纬度:{{ locationData.latitude }}, {{ locationData.longitude }}</view>
      <view>省份:{{ locationData.province }}</view>
      <view>城市:{{ locationData.city }}</view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { getUserLocation } from '@/utils/location';

const locationData = ref<{
  latitude: number;
  longitude: number;
  address: string;
  province: string;
  city: string;
  district: string;
}>(null);

const getLocation = async () => {
  try {
    const data = await getUserLocation();
    locationData.value = data;
  } catch (error) {
    console.error('获取位置失败:', error);
  }
};
</script>

6.2 本地存储进阶(异步存储+数据加密)

基础本地存储(uni.setStorageSync)为同步操作,大量数据存储会阻塞主线程;敏感数据(如用户token、手机号)需加密存储。以下封装异步加密存储工具。

6.2.1 封装加密存储工具(utils/storage.ts)

import { showToast } from 'uni-app';
// 简单加密工具(实际项目建议使用crypto-js等专业加密库)
const encrypt = (data: string, key: string = 'YOUR_STORAGE_KEY') => {
  let result = '';
  for (let i = 0; i < data.length; i++) {
    result += String.fromCharCode(data.charCodeAt(i) + key.charCodeAt(i % key.length));
  }
  return result;
};

const decrypt = (data: string, key: string = 'YOUR_STORAGE_KEY') => {
  let result = '';
  for (let i = 0; i < data.length; i++) {
    result += String.fromCharCode(data.charCodeAt(i) - key.charCodeAt(i % key.length));
  }
  return result;
};

/**
 * 异步加密存储
 * @param key 存储键名
 * @param data 存储数据(支持任意类型)
 */
export const setStorage = async (key: string, data: any) => {
  try {
    const stringData = JSON.stringify(data);
    const encryptedData = encrypt(stringData);
    await uni.setStorage({ key, data: encryptedData });
    return true;
  } catch (error) {
    showToast({ title: '存储失败', icon: 'none' });
    console.error('存储失败:', error);
    return false;
  }
};

/**
 * 异步解密获取存储
 * @param key 存储键名
 * @returns 解密后的原始数据
 */
export const getStorage = async (key: string) => {
  try {
    const { data } = await uni.getStorage({ key });
    if (!data) return null;
    const decryptedData = decrypt(data);
    return JSON.parse(decryptedData);
  } catch (error) {
    showToast({ title: '获取存储失败', icon: 'none' });
    console.error('获取存储失败:', error);
    return null;
  }
};

/**
 * 异步删除存储
 * @param key 存储键名
 */
export const removeStorage = async (key: string) => {
  try {
    await uni.removeStorage({ key });
    return true;
  } catch (error) {
    showToast({ title: '删除存储失败', icon: 'none' });
    console.error('删除存储失败:', error);
    return false;
  }
};
6.2.2 工具使用示例

// 存储用户信息
await setStorage('userInfo', { nickname: '张三', avatarUrl: 'xxx' });

// 获取用户信息
const userInfo = await getStorage('userInfo');

// 删除用户信息
await removeStorage('userInfo');

6.3 小程序消息订阅功能

实现用户订阅消息后,后端可通过接口推送模板消息(如订单通知、活动提醒),需先在小程序后台申请消息模板。

6.3.1 前置准备
  • 登录微信小程序后台,进入「功能」-「订阅消息」,申请所需消息模板(如“订单支付成功通知”),记录模板ID

  • 在manifest.json中添加订阅消息权限:微信小程序配置→权限配置→勾选「订阅消息」

6.3.2 封装订阅工具(utils/subscribe.ts)

import { showToast, requestSubscribeMessage } from 'uni-app';

/**
 * 发起消息订阅
 * @param tmplIds 消息模板ID数组(最多3个)
 * @returns 订阅结果
 */
export const subscribeMessage = async (tmplIds: string[]) => {
  try {
    if (tmplIds.length === 0) {
      throw new Error('请传入消息模板ID');
    }
    // 调用微信订阅消息接口
    const res = await requestSubscribeMessage({ tmplIds });
    // 处理订阅结果(res为{模板ID: 'accept'|'reject'|'ban'})
    const result: { accept: string[]; reject: string[] } = { accept: [], reject: [] };
    for (const [tmplId, status] of Object.entries(res)) {
      if (status === 'accept') {
        result.accept.push(tmplId);
      } else {
        result.reject.push(tmplId);
      }
    }
    if (result.accept.length > 0) {
      showToast({ title: `成功订阅${result.accept.length}类消息` });
    }
    if (result.reject.length > 0) {
      showToast({ title: `拒绝订阅${result.reject.length}类消息`, icon: 'none' });
    }
    return result;
  } catch (error) {
    const errMsg = (error as Error).message || '订阅消息失败';
    showToast({ title: errMsg, icon: 'none' });
    throw error;
  }
};
6.3.3 页面中发起订阅

<template>
  <view class="subscribe-container">
    <button @click="handleSubscribe" class="subscribe-btn">订阅订单通知</button>
  </view>
</template>

<script setup lang="ts">
import { subscribeMessage } from '@/utils/subscribe';

// 替换为自己申请的消息模板ID
const ORDER_NOTICE_TMPL_ID = 'YOUR_ORDER_NOTICE_TMPL_ID';

const handleSubscribe = async () => {
  try {
    const result = await subscribeMessage([ORDER_NOTICE_TMPL_ID]);
    console.log('订阅结果:', result);
  } catch (error) {
    console.error('订阅失败:', error);
  }
};
</script>
6.3.4 注意事项
  • 订阅消息需用户主动触发(如点击按钮),不能自动弹出订阅弹窗

  • 同一模板ID,用户拒绝后需引导用户去小程序设置页重新开启订阅(通过uni.openSetting)

  • 订阅消息有有效期,单次订阅仅支持推送一次,长期订阅需申请长期模板(仅特定类目支持)

6.4 小程序分享功能

小程序分享分为「页面内按钮分享」和「右上角菜单分享」,Uni-app通过onShareAppMessage和onShareTimeline生命周期函数适配,可实现自定义分享标题、图片、路径等功能。

6.4.1 封装分享工具(utils/share.ts)

封装通用分享配置,统一管理分享内容,支持动态修改参数:


import { showToast } from 'uni-app';

/**
 * 生成通用分享配置
 * @param title 分享标题
 * @param path 分享路径(需携带必要参数,如页面id)
 * @param imageUrl 分享图片(建议尺寸2.35:1,不小于300*157)
 * @returns 分享配置对象
 */
export const getShareConfig = (
  title: string = '默认分享标题',
  path: string = '/pages/index/index',
  imageUrl: string = '/static/share-default.png'
) => {
  return {
    title,
    path,
    imageUrl,
    // 分享成功回调
    success: () => {
      showToast({ title: '分享成功' });
    },
    // 分享失败回调
    fail: (err: any) => {
      console.error('分享失败:', err);
      showToast({ title: '分享失败', icon: 'none' });
    }
  };
};
6.4.2 页面内实现分享功能

在需要分享的页面(如详情页)中,通过生命周期函数配置分享,同时可添加自定义分享按钮:


<template>
  <view class="detail-container" v-if="detailData">
    <!-- 自定义分享按钮(需设置open-type="share") -->
    <button open-type="share" class="share-btn">分享给好友</button>
    <!-- 页面原有内容 -->
    <view class="detail-title">{{ detailData.title }}</view>
    <view class="detail-time">{{ detailData.createTime }}</view>
    <view class="detail-content">{{ detailData.content }}</view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute } from 'uni-app';
import { get } from '@/utils/request';
import { getShareConfig } from '@/utils/share';

const route = useRoute();
const detailData = ref<{
  id: number;
  title: string;
  createTime: string;
  content: string;
  coverImage: string;
}>(null);

// 页面加载时获取详情数据
onMounted(() => {
  const id = route.query.id as string;
  if (id) {
    getDetailData(Number(id));
  }
});

const getDetailData = async (id: number) => {
  // 省略原有逻辑...
};

// 1. 配置右上角菜单分享(好友)
const onShareAppMessage = () => {
  if (detailData.value) {
    const sharePath = `/pages/detail/detail?id=${detailData.value.id}`;
    return getShareConfig(
      detailData.value.title,
      sharePath,
      detailData.value.coverImage
    );
  }
  return getShareConfig();
};

// 2. 配置右上角菜单分享(朋友圈)- 可选
const onShareTimeline = () => {
  if (detailData.value) {
    return {
      title: detailData.value.title,
      query: `id=${detailData.value.id}`,
      imageUrl: detailData.value.coverImage
    };
  }
  return {
    title: '默认分享标题',
    query: '',
    imageUrl: '/static/share-default.png'
  };
};

// 暴露生命周期函数(Vue3 setup语法必需)
defineExpose({
  onShareAppMessage,
  onShareTimeline
});
</script>

<style scoped>
.share-btn {
  width: 100%;
  height: 80rpx;
  line-height: 80rpx;
  font-size: 32rpx;
  background-color: #1E88E5;
  color: #FFFFFF;
  border-radius: 40rpx;
  margin-bottom: 30rpx;
}
/* 省略其他样式... */
</style>
6.4.3 注意事项
  • 分享路径必须是pages.json中已配置的页面,且不能携带超过1024字节的参数

  • 分享图片建议使用网络图片或本地静态图片,尺寸推荐500*400px,避免使用页面截图

  • onShareTimeline仅支持微信小程序基础库2.11.3及以上版本,需在小程序后台配置最低基础库版本

  • 页面内自定义分享按钮必须设置open-type=“share”,否则无法触发分享逻辑

6.5 小程序客服功能集成

小程序客服功能支持用户与商家实时沟通,核心通过button组件的open-type="contact"实现,可配置自动回复、快捷回复等功能,无需前端复杂开发。

6.5.1 前置准备
  • 登录微信小程序后台,进入「功能」-「客服」,开启客服功能

  • 配置客服人员:在「客服」-「客服人员管理」中,添加客服微信号(需绑定小程序主体)

  • 可选配置:设置自动回复、快捷回复、菜单导航等(在客服后台配置)

6.5.2 页面集成客服入口

在需要添加客服的页面(如个人中心、订单页),使用button组件设置open-type=“contact”,示例代码:


<template>
  <view class="mine-container">
    <!-- 原有个人中心内容 -->
    <view class="func-list">
      <view class="func-item" @click="goToAbout">
        <view class="item-text">关于我们</view>
        <image src="/static/arrow-right.png" class="arrow-icon"></image>
      </view>
      <!-- 客服入口按钮 -->
      <button open-type="contact" class="service-btn">
        <image src="/static/service-icon.png" class="service-icon"></image>
        <view class="service-text">联系客服</view>
      </button>
    </view>
  </view>
</template>

<script setup lang="ts">
// 省略原有逻辑...
</script>

<style scoped>
.service-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 88rpx;
  background-color: #FFFFFF;
  border: none;
  margin-top: 20rpx;
}
.service-icon {
  width: 40rpx;
  height: 40rpx;
  margin-right: 15rpx;
}
.service-text {
  font-size: 32rpx;
  color: #333333;
}
/* 省略其他样式... */
</style>
6.5.3 客服消息推送配置(可选)

若需实现客服消息实时推送至客服人员微信,需在小程序后台「客服」-「消息推送」中配置:

  • 消息推送模式:选择「推送到微信客服系统」(默认),客服人员可通过微信客服助手小程序接收消息

  • 自定义推送:若需推送到自有服务器,可配置「开发者模式」,设置消息接收地址、加密方式等(需后端配合开发)

6.5.4 注意事项
  • 客服按钮样式可自定义,但需保留清晰的客服标识,避免用户误解

  • 客服功能仅在真机上可正常使用,模拟器中可能无法触发

  • 需告知用户客服在线时间,提升用户体验

  • 敏感内容过滤:微信会自动过滤客服消息中的敏感词汇,避免违规

6.6 小程序广告植入功能集成

小程序支持接入微信广告平台(流量主)的多种广告形式,如Banner广告、插屏广告、激励视频广告等,可实现流量变现。以下以常用的Banner广告和插屏广告为例,介绍集成流程。

6.6.1 前置准备
  • 小程序需满足流量主申请条件:累计独立访客(UV)≥1000,且无违规记录

  • 登录微信小程序后台,进入「流量主」,申请开通流量主功能,审核通过后创建广告位,获取广告位ID

  • 广告位类型选择:根据页面场景选择(Banner广告适合嵌入页面底部/顶部,插屏广告适合页面切换时展示)

6.6.2 Banner广告集成

Banner广告是固定在页面顶部或底部的横幅广告,集成步骤如下:


<template>
  <view class="list-container">
    <!-- 列表内容 -->
    <view class="list-item" v-for="(item, index) in listData" :key="index">
      <view class="item-title">{{ item.title }}</view>
      <view class="item-desc">{{ item.desc }}</view>
    </view>
    <!-- Banner广告容器 -->
    <view class="banner-ad-container" v-if="adShow"></view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { get } from '@/utils/request';

const listData = ref<Array<any>>([]);
const adShow = ref(false);
let bannerAd: WechatMiniprogram.BannerAd | null = null;

// 页面加载时创建Banner广告
onMounted(() => {
  getListData();
  createBannerAd();
});

// 页面卸载时销毁广告,避免内存泄漏
onUnmounted(() => {
  if (bannerAd) {
    bannerAd.destroy();
    bannerAd = null;
  }
});

// 获取列表数据(原有逻辑)
const getListData = async () => {
  // 省略原有逻辑...
};

// 创建Banner广告
const createBannerAd = () => {
  // 替换为自己的Banner广告位ID
  const adUnitId = 'adunit-xxxxxxxxxxxxxxxx';
  // 创建广告实例
  bannerAd = uni.createBannerAd({
    adUnitId,
    adIntervals: 30, // 广告自动刷新间隔(30-120秒)
    style: {
      width: uni.getSystemInfoSync().windowWidth, // 广告宽度(建议与屏幕宽度一致)
      height: 100 // 广告高度(根据广告位要求设置)
    }
  });

  // 监听广告加载成功
  bannerAd.onLoad(() => {
    console.log('Banner广告加载成功');
    adShow.value = true;
  });

  // 监听广告加载失败
  bannerAd.onError((err) => {
    console.error('Banner广告加载失败:', err);
    adShow.value = false;
  });

  // 监听广告被点击
  bannerAd.onClose(() => {
    console.log('Banner广告被关闭');
  });
};
</script>

<style scoped>
.banner-ad-container {
  width: 100%;
  height: 100rpx;
  margin-top: 20rpx;
}
/* 省略其他样式... */
</style>
6.6.3 插屏广告集成

插屏广告在页面切换或特定操作后弹出,覆盖部分页面内容,集成步骤如下:


<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { useRouter } from 'uni-app';

const router = useRouter();
let interstitialAd: WechatMiniprogram.InterstitialAd | null = null;

// 页面加载时创建插屏广告
onMounted(() => {
  createInterstitialAd();
});

// 页面卸载时销毁广告
onUnmounted(() => {
  if (interstitialAd) {
    interstitialAd.destroy();
    interstitialAd = null;
  }
});

// 创建插屏广告
const createInterstitialAd = () => {
  // 替换为自己的插屏广告位ID
  const adUnitId = 'adunit-yyyyyyyyyyyyyyyy';
  // 判断微信基础库是否支持插屏广告
  if (uni.createInterstitialAd) {
    interstitialAd = uni.createInterstitialAd({ adUnitId });

    // 监听广告加载成功
    interstitialAd.onLoad(() => {
      console.log('插屏广告加载成功');
    });

    // 监听广告加载失败
    interstitialAd.onError((err) => {
      console.error('插屏广告加载失败:', err);
    });

    // 监听广告被关闭
    interstitialAd.onClose((res) => {
      console.log('插屏广告被关闭', res);
      // 广告关闭后可执行后续操作(如跳转页面)
      if (res && res.isEnded) {
        // 广告播放完成后关闭
        router.push('/pages/detail/detail');
      } else {
        // 广告未播放完成被关闭
        uni.showToast({ title: '广告未完成播放', icon: 'none' });
      }
    });
  }
};

// 触发插屏广告显示(如点击按钮后)
const showInterstitialAd = () => {
  if (interstitialAd) {
    interstitialAd.show().catch((err) => {
      console.error('插屏广告显示失败:', err);
      // 显示失败时重新加载广告
      interstitialAd.load().then(() => {
        interstitialAd.show();
      });
    });
  }
};
</script>
6.6.4 注意事项
  • 广告位ID需正确配置,测试环境可使用微信提供的测试广告位ID

  • 广告加载和显示需处理异常情况(如网络错误、广告加载失败),避免影响用户体验

  • 广告展示频率需合理,避免过度推送导致用户反感(遵循微信流量主规范)

  • 禁止对广告进行遮挡、篡改,否则可能被取消流量主资格

  • 激励视频广告(用户观看后获得奖励)需额外配置奖励发放逻辑,可参考微信官方文档

七、项目打包与发布

7.1 打包小程序代码

将Uni-app项目编译为微信小程序生产环境代码:

  1. 打开HBuilderX,右键项目根目录,选择「发行」-「小程序-微信」

  2. 在弹出的打包配置窗口中,填写「小程序名称」「小程序AppID」(需与manifest.json一致),勾选「压缩代码」「运行时压缩」(生产环境推荐),点击「发行」

  3. 等待打包完成,HBuilderX会生成生产环境的小程序代码(默认路径:unpackage/dist/build/mp-weixin)

7.2 小程序预览与调试

在微信开发者工具中预览打包后的代码,确保功能正常:

  1. 打开微信开发者工具,点击「导入项目」,选择打包后的mp-weixin文件夹,点击「导入」

  2. 点击「预览」按钮,用微信扫码查看小程序效果,测试所有功能(登录、跳转、接口请求等)

  3. 若发现问题,在微信开发者工具中查看控制台日志(Console),定位问题后回到HBuilderX修改代码,重新打包预览

7.3 小程序提交审核与发布

功能测试无误后,提交审核并发布:

  1. 在微信开发者工具中,点击「上传」按钮,填写「版本号」(如1.0.0)、「项目备注」(如首次提交),点击「上传」

  2. 上传成功后,登录微信小程序后台:https://mp.weixin.qq.com/,进入「版本管理」-「开发版本」,找到刚上传的版本,点击「提交审核」

  3. 填写审核信息:选择「服务类目」(需与小程序功能一致)、填写「小程序介绍」「测试账号(可选)」,点击「提交」

  4. 等待微信审核(通常1-3个工作日),审核通过后,进入「版本管理」-「审核版本」,点击「发布」,小程序即可上线

八、常见问题解决

8.1 小程序登录失败(code无效)

  • 检查小程序AppID是否正确(开发环境与生产环境AppID需区分)

  • 确保后端接口正确处理code,code有效期为5分钟,需及时换取openid

  • 检查开发者账号是否已添加到小程序后台的「成员管理」中

8.2 接口请求失败(跨域/403)

  • 小程序端需在后台配置「合法域名」:进入小程序后台「开发」-「开发设置」-「服务器域名」,添加接口域名(需备案)

  • 开发环境可勾选微信开发者工具「设置」-「项目设置」-「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」(仅开发用)

  • 检查接口请求头是否携带必要参数(如token),后端是否正确验证

8.3 打包后小程序体积过大(超过2M)

  • 使用Uni-app分包加载:在pages.json中配置subPackages,将非首页的页面拆分到分包中(小程序主包体积限制2M,分包总和限制20M)

  • 压缩静态资源:图片用tinypng压缩,优先使用WebP格式,大图片使用CDN加载

  • 删除无用代码与依赖:清理未使用的组件、插件,减少项目体积

8.4 页面跳转失败(路由错误)

  • 检查pages.json中是否配置了目标页面的路由路径,路径是否正确(区分相对路径与绝对路径)

  • 确保跳转时携带的参数格式正确(如id为数字类型,避免字符串格式错误)

  • tabBar页面只能用uni.switchTab跳转,不能用uni.navigateTo跳转

九、总结

本教程从零完成了Uni-app+微信小程序项目的搭建与落地,核心流程为:环境准备→项目创建→核心配置(路由、请求、登录)→功能开发→打包发布。依托Uni-app的跨端能力,开发者无需学习小程序原生语法,即可快速开发小程序,同时代码可复用至H5、App等其他端。

如需进一步扩展功能,可参考微信小程序官方文档(https://developers.weixin.qq.com/miniprogram/dev/framework/)和Uni-app官方文档(https://uniapp.dcloud.net.cn/),学习更多高级功能(如小程序支付、推送、地图等)。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐