UniApp 小程序转发朋友圈、分享好友配置全解
本文面向使用 UniApp 开发微信小程序的开发者,涵盖分享好友、分享朋友圈、海报生成、常见问题避坑等完整方案。所有代码均为 UniApp 原生写法,可直接复制使用。
一、分享好友 vs 分享朋友圈的区别
微信小程序的分享能力分为两大类:分享给好友 和 分享到朋友圈。两者在 API、 UI 样式、功能限制上都有明显差异。
onShareAppMessage | onShareTimeline | |
path 传递 | query 传递(长度限制 20 字符) | |
UniApp 中如何开启分享菜单
在页面或 pages.json 中配置 enableShareMenu:
// 页面 onLoad 中调用onLoad() {// 开启分享菜单,显示"分享到朋友圈"入口uni.showShareMenu({withShareTicket: true,menus: ['shareAppMessage', 'shareTimeline']})}
或在 pages.json 页面配置中全局开启:
{"pages": [{"path": "pages/index/index","style": {"enableShareMenu": true}}],"globalStyle": {"enableShareMenu": true}}
⚠️ 重要:不调用
uni.showShareMenu,朋友圈分享入口不会显示。
二、onShareAppMessage 分享好友
基础用法
// pages/index/index.vueexport default {data() {return {productId: '',productName: ''}},onLoad(options) {this.productId = options.id || ''this.productName = options.name || '默认商品'},// #ifdef MP-WEIXINonShareAppMessage(res) {return {title: `${this.productName} - 限时特惠中!`, // 分享标题path: `/pages/index/index?id=${this.productId}`, // 分享后打开的页面路径imageUrl: 'https://xxx.com/static/share-card.png' // 分享图片}},// #endif}
动态标题:根据页面数据生成
// 商品详情页示例onShareAppMessage(res) {const good = this.currentGoodreturn {title: good.goods_name || '发现一个好物',path: `/pages/goods/detail?id=${good.id}&from=share`,imageUrl: good.goods_image || '/static/default-share.png'}}
图片尺寸要求
比例:推荐 5:4(横向),也支持正方形
最小尺寸:200px × 200px
格式:PNG、JPG 均可
文件大小:建议不超过 500KB(过大会影响加载速度)
通过分享传参
// 发送方onShareAppMessage() {return {title: '邀请好友赢好礼',path: '/pages/invite/index?scene=abc123&from=share',imageUrl: 'https://xxx.com/invite.png'}}// 接收方 onLoad 中获取onLoad(query) {console.log('分享参数:', query)// { scene: 'abc123', from: 'share' }}
💡 传参技巧:参数过长时,建议用 Base64 编码或让后端生成短链接映射。
三、onShareTimeline 分享朋友圈
基础用法
// pages/index/index.vueexport default {// #ifdef MP-WEIXINonShareTimeline() {return {title: '朋友圈分享标题 - 点进来看看', // 朋友圈卡片标题query: 'id=123&from=timeline', // 页面参数imageUrl: 'https://xxx.com/poster-square.png' // 海报图片}},// #endif}
朋友圈分享的限制
| 只能分享当前页面 | |
query 字段,长度限制 20 个字符 | |
| 只支持单张图片 | |
动态生成朋友圈内容
onShareTimeline() {const good = this.currentGood// 压缩参数,避免超长const query = `id=${good.id}&from=tl`return {title: good.goods_name,query: query,imageUrl: good.share_poster || '/static/default-poster.png'}}
完整配置:同时支持好友和朋友圈
export default {data() {return {shareId: '',shareTitle: '默认分享标题'}},onLoad(options) {this.shareId = options.id || '0'this.shareTitle = options.title || '默认分享标题'// 开启分享菜单uni.showShareMenu({withShareTicket: true,menus: ['shareAppMessage', 'shareTimeline']})},// #ifdef MP-WEIXINonShareAppMessage(res) {return {title: this.shareTitle,path: `/pages/index/index?id=${this.shareId}&from=share`,imageUrl: '/static/share-friend.png'}},onShareTimeline() {return {title: this.shareTitle + ' - 限时优惠',query: `id=${this.shareId}`,imageUrl: '/static/share-timeline.png'}},// #endif}
四、UniApp 特有的分享能力
uni.share - App 端分享到多平台
App 端支持分享到微信、QQ、微博等平台:
// #ifdef APP-PLUSuni.share({provider: 'weixin',type: 0, // 0: 文字 1: 图片 5: 小程序title: '分享标题',summary: '分享摘要',href: 'https://xxx.com/page',imageUrl: '/static/share.png',success: (res) => {console.log('分享成功', res)uni.showToast({ title: '分享成功' })},fail: (err) => {console.error('分享失败', err)}})// #endif
uni.share - 分享小程序卡片(App 端)
// #ifdef APP-PLUSuni.share({provider: 'weixin',type: 5, // 5: 小程序title: '小程序标题',scene: 'WXSceneSession', // WXSceneSession=好友 WXSceneTimeline=朋友圈miniProgram: {id: 'gh_xxxxxxx', // 小程序原始 idpath: '/pages/index/index',type: 0, // 0-正式版 1-开发版 2-体验版webUrl: 'https://xxx.com/h5-fallback' // 非微信环境打开的 H5 页面},success: (res) => {console.log('分享成功')},fail: (err) => {console.error('分享失败', err)}})// #endif
uni.shareWithSystem - 系统分享面板
调起系统原生分享菜单(所有平台通用):
// #ifdef APP-PLUSuni.shareWithSystem({title: '分享标题',summary: '分享内容摘要',href: 'https://xxx.com/page',imageUrl: '/static/share.png',success: (res) => {console.log('分享成功')},fail: (err) => {console.error('分享失败', err)}})// #endif
button 触发分享
使用 button 的 open-type="share" 触发分享:
<template><button open-type="share">分享给好友</button></template>
// #ifdef MP-WEIXINonShareAppMessage(res) {// 区分不同 button 触发的分享if (res.from === 'button') {console.log('来自button点击分享', res.target)}return {title: '分享标题',path: '/pages/index/index',imageUrl: '/static/share.png'}}// #endif
五、分享到朋友圈(H5 + 小程序双端)
H5 端分享说明:UniApp H5 端调微信分享必须使用微信官方 JSSDK(
wx.API),这是微信平台限制,非代码问题。小程序端请继续使用下方 UniApp 原生写法。
H5 端分享(微信 JSSDK)
<!-- public/index.html 引入 JSSDK --><script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
// #ifdef H5import wx from 'weixin-js-sdk'export default {onLoad() {this.initWxShare()},methods: {async initWxShare() {// 1. 从后端获取签名const signPackage = await this.$api.getWxSignPackage()// 2. 配置 JSSDKwx.config({debug: false,appId: signPackage.appId,timestamp: signPackage.timestamp,nonceStr: signPackage.nonceStr,signature: signPackage.signature,jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData']})wx.ready(() => {// 分享给好友wx.updateAppMessageShareData({title: '分享标题',desc: '分享描述',link: window.location.href,imgUrl: 'https://xxx.com/share.png'})// 分享到朋友圈wx.updateTimelineShareData({title: '朋友圈标题',link: window.location.href,imgUrl: 'https://xxx.com/share.png'})})}}}// #endif
⚠️ H5 注意:必须在微信公众平台配置 JS 安全域名,且调用 JSSDK 的页面 URL 必须与入口 URL 一致。
六、UniApp 端海报生成方案
方案概述
朋友圈分享不支持小程序码,但可以通过生成带小程序码的海报实现裂变。用户保存海报到相册后扫码进入小程序。
整体流程
用户点击"生成海报" → 调用云函数获取小程序码 → Canvas 绘制海报 → 用户保存到相册步骤 1:获取小程序码(UniCloud 云函数)
//云函数 /uni-cloud-jql/functions/getQRCode/index.js'use strict';const crypto = require('crypto')exports.main = async (event, context) => {const {scene, // 参数,最大32个可见字符page, // 页面路径,非主包需配置 appidwidth, // 二维码宽度auto_color, // 自动配置线条颜色env_version // 小程序版本 trial-体验版 develop-开发版 release-正式版} = eventconst accessToken = await getAccessToken()const apiUrl = `https://api.weixin.qq.com/wxa/getUnlimited?access_token=${accessToken}`const res = await uniCloud.httpclient.request(apiUrl, {method: 'POST',data: {scene: scene || 'from=poster',page: page || 'pages/index/index',width: width || 280,auto_color: auto_color || false,env_version: env_version || 'release',is_hyaline: true // 透明背景},dataType: 'json'})// 返回 Bufferreturn res.data.buffer || res.data}// 获取 access_token(需缓存)async function getAccessToken() {const db = uniCloud.database()const conf = db.collection('mp-config')// 从数据库读取缓存的 tokenconst { data } = await conf.where({ key: 'access_token' }).get()const record = data[0]if (record && record.expire_time > Date.now()) {return record.value}// 重新获取const res = await uniCloud.httpclient.request(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${SECRET}`)const token = JSON.parse(res.data).access_token// 存入数据库缓存(提前5分钟过期)if (record) {await conf.doc(record._id).update({value: token,expire_time: Date.now() + 7100 * 1000})} else {await conf.add({key: 'access_token',value: token,expire_time: Date.now() + 7100 * 1000})}return token}
步骤 2:前端调用获取小程序码
methods: {async generatePoster() {uni.showLoading({ title: '生成中...' })try {// 调用云函数获取小程序码const res = await uniCloud.callFunction({name: 'getQRCode',data: {scene: `id=${this.goodsId}&uid=${this.userId}`,page: 'pages/goods/detail',width: 280}})// 将返回的 Buffer 转成本地临时路径const filePath = await this.bufferToTempFile(res.result)// 绘制海报await this.drawPoster(filePath)} catch (e) {uni.showModal({ title: '生成失败', content: e.message })} finally {uni.hideLoading()}},// Buffer 转临时文件bufferToTempFile(buffer) {return new Promise((resolve, reject) => {const fs = uni.getFileSystemManager()const fileName = `${Date.now()}_qrcode.png`const filePath = `${uni.env.USER_DATA_PATH}/${fileName}`fs.writeFile({filePath,data: buffer,encoding: 'base64',success: () => resolve(filePath),fail: reject})})},// 绘制海报async drawPoster(qrcodePath) {const ctx = uni.createCanvasContext('posterCanvas', this)const dpr = uni.getSystemInfoSync().pixelRatioconst canvasWidth = 600const canvasHeight = 800// 设置 canvas 尺寸(真实像素)this.canvasWidth = canvasWidththis.canvasHeight = canvasHeightthis.canvasDpr = dpr// 绘制背景ctx.fillStyle = '#FFFFFF'ctx.fillRect(0, 0, canvasWidth, canvasHeight)// 绘制商品图(假设已下载到本地)ctx.drawImage(this.localImagePath, 50, 50, 500, 400)// 绘制商品名称ctx.setFontSize(36)ctx.setFillStyle('#333333')ctx.fillText(this.goodsName, 50, 500)// 绘制价格ctx.setFontSize(48)ctx.setFillStyle('#FF5000')ctx.fillText(`¥${this.goodsPrice}`, 50, 570)// 绘制小程序码ctx.drawImage(qrcodePath, 200, 600, 200, 200)// 绘制引导文案ctx.setFontSize(28)ctx.setFillStyle('#666666')ctx.fillText('长按识别小程序码', 175, 850)ctx.draw(false, () => {// 导出图片uni.canvasToTempFilePath({canvasId: 'posterCanvas',x: 0,y: 0,width: canvasWidth,height: canvasHeight,destWidth: canvasWidth * 3,destHeight: canvasHeight * 3,success: (res) => {this.posterPath = res.tempFilePath},fail: (err) => {console.error('导出失败', err)}})})}}
<!-- 海报画布(隐藏) --><canvascanvas-id="posterCanvas"id="posterCanvas"style="position:fixed;left:-9999px;width:600px;height:900px;"></canvas><!-- 显示海报 --><view v-if="posterPath"><image :src="posterPath" mode="aspectFit" /><button @click="savePoster">保存到相册</button></view>
步骤 3:保存到相册(权限处理)
methods: {async savePoster() {// 1. 检查权限const setting = await uni.getSetting()if (!setting.authSetting['scope.writePhotosAlbum']) {// 2. 请求权限const res = await uni.authorize({ scope: 'scope.writePhotosAlbum' })if (res.errMsg.includes('auth deny')) {// 3. 用户拒绝过,引导去设置页const modalRes = await uni.showModal({title: '提示',content: '需要您授权保存图片到相册',confirmText: '去授权'})if (modalRes.confirm) {await uni.openSetting()}return}}// 4. 保存图片await uni.saveImageToPhotosAlbum({filePath: this.posterPath,success: () => {uni.showToast({ title: '已保存到相册' })},fail: (err) => {uni.showToast({ title: '保存失败', icon: 'none' })}})}}
完整海报组件封装
// components/poster-generator/poster-generator.vue<template><view class="poster-wrapper"><canvascanvas-id="posterCanvas":style="{ width: canvasStyle.width, height: canvasStyle.height }"class="poster-canvas"></canvas><view v-if="posterPath" class="poster-preview"><image :src="posterPath" mode="widthFix" class="poster-image" /><button type="primary" @click="saveToAlbum" class="save-btn">保存到相册</button></view></view></template><script>export default {props: {// 商品数据goods: {type: Object,required: true},// 用户 IDuserId: {type: String,default: ''}},data() {return {posterPath: '',localImagePath: '',canvasStyle: {width: '600rpx',height: '900rpx'}}},methods: {async generate() {uni.showLoading({ title: '生成海报中...' })try {// 1. 下载商品主图到本地await this.downloadGoodsImage()// 2. 获取小程序码const qrcodeRes = await this.getWxacode()const qrcodePath = await this.bufferToTempFile(qrcodeRes.result)// 3. 绘制海报await this.drawPoster(qrcodePath)} catch (e) {console.error('生成海报失败', e)uni.showModal({ title: '提示', content: '海报生成失败,请重试' })} finally {uni.hideLoading()}},async downloadGoodsImage() {return new Promise((resolve, reject) => {uni.downloadFile({url: this.goods.image,success: (res) => {this.localImagePath = res.tempFilePathresolve(res)},fail: reject})})},async getWxacode() {return uniCloud.callFunction({name: 'getQRCode',data: {scene: `id=${this.goods.id}&uid=${this.userId}&from=poster`,page: 'pages/goods/detail',width: 280}})},bufferToTempFile(buffer) {return new Promise((resolve, reject) => {const fs = uni.getFileSystemManager()const fileName = `qr_${Date.now()}.png`const filePath = `${uni.env.USER_DATA_PATH}/${fileName}`fs.writeFile({filePath,data: buffer,encoding: 'base64',success: () => resolve(filePath),fail: reject})})},drawPoster(qrcodePath) {return new Promise((resolve) => {const ctx = uni.createCanvasContext('posterCanvas', this)// 清空画布ctx.fillStyle = '#FFFFFF'ctx.fillRect(0, 0, 600, 900)// 绘制商品图ctx.drawImage(this.localImagePath, 0, 0, 600, 450)// 绘制标题ctx.setFontSize(32)ctx.setFillStyle('#333333')ctx.fillText(this.goods.name, 30, 510, 540)// 绘制价格ctx.setFontSize(48)ctx.setFillStyle('#FF4A00')ctx.fillText(`¥${this.goods.price}`, 30, 600)// 绘制小程序码ctx.drawImage(qrcodePath, 200, 650, 200, 200)// 绘制引导文案ctx.setFontSize(24)ctx.setFillStyle('#999999')ctx.textAlign = 'center'ctx.fillText('长按识别 立即购买', 300, 900)ctx.draw(false, () => {uni.canvasToTempFilePath({canvasId: 'posterCanvas',success: (res) => {this.posterPath = res.tempFilePathresolve()}})})})},async saveToAlbum() {try {await uni.saveImageToPhotosAlbum({ filePath: this.posterPath })uni.showToast({ title: '已保存到相册', icon: 'success' })this.$emit('saved')} catch (e) {if (e.errMsg.includes('auth deny')) {const res = await uni.showModal({title: '提示',content: '需要授权保存图片',confirmText: '去授权'})if (res.confirm) {uni.openSetting()}}}}}}</script><style scoped>.poster-wrapper {padding: 20rpx;}.poster-canvas {position: fixed;left: -9999px;}.poster-image {width: 100%;border-radius: 16rpx;}.save-btn {margin-top: 30rpx;}</style>
七、分享成功后通知后端
分享埋点方案
// #ifdef MP-WEIXINonShareAppMessage(res) {// 分享前上报this.reportShare('friend', {page: '/pages/index/index',goodsId: this.goodsId,shareType: res.from // 'button' | 'menu'})return {title: this.shareTitle,path: `/pages/index/index?id=${this.goodsId}`,imageUrl: this.shareImage}},onShareTimeline() {// 分享到朋友圈上报this.reportShare('timeline', {page: '/pages/index/index',goodsId: this.goodsId})return {title: this.shareTitle,query: `id=${this.goodsId}`}},methods: {async reportShare(type, data) {try {await uniCloud.callFunction({name: 'reportShare',data: {type,...data,uid: uni.getStorageSync('userId'),shareTime: Date.now()}})} catch (e) {console.error('埋点上报失败', e)}}}// #endif
云函数记录分享
// /uni-cloud-jql/functions/reportShare/index.js'use strict';exports.main = async (event, context) => {const db = uniCloud.database()const collection = db.collection('share-log')await collection.add({uid: event.uid,type: event.type, // 'friend' | 'timeline'page: event.page,goodsId: event.goodsId,shareTime: event.shareTime,createTime: Date.now()})return { success: true }}
防刷机制
// 云函数防刷exports.main = async (event, context) => {const db = uniCloud.database()const collection = db.collection('share-log')const uid = event.uidconst now = Date.now()const oneHourAgo = now - 3600 * 1000// 1. 检查1小时内分享次数const count = await collection.where({uid,shareTime: db.command.gte(oneHourAgo)}).count()if (count > 50) {return { success: false, message: '分享过于频繁' }}// 2. 记录分享await collection.add({...})return { success: true }}
八、常见问题与避坑(UniApp 场景)
| 分享图片不显示 | ||
| 朋友圈分享入口不显示 | uni.showShareMenu | onLoad 中调用并指定 menus: ['shareAppMessage', 'shareTimeline'] |
| 小程序码生成失败 | ||
| App 端分享失败 | ||
| H5 端无法分享 | ||
| 分享带参数失效 | encodeURIComponent 编码,或后端生成短链接映射 | |
| canvas 海报绘制模糊 | destWidth/destHeight 放大 2-3 倍导出 | |
| canvas 导出失败 | uni.env.USER_DATA_PATH 路径,或在 uniCloud 云函数中处理 | |
| onShareTimeline 分享当前页不对 | ||
| button open-type=“share” 无效 |
避坑重点
图片域名白名单:所有
imageUrl必须是已在微信公众平台配置过的合法域名onShareTimeline 只能分享当前页:无法通过
path指定页面朋友圈 query 长度限制 20 字符:超长参数用后端映射或 encode
canvas 导出在开发工具正常、真机失败:检查高清导出参数和路径
App 端分享小程序需要开放平台:需将移动应用与小程序关联
九、快速复制模板
完整分享配置模板(双端合一)
// pages/goods/detail/detail.vueexport default {data() {return {goodsId: '',goodsName: '',goodsPrice: 0,shareImage: ''}},onLoad(options) {this.goodsId = options.id || ''this.loadGoodsDetail()// 开启分享菜单uni.showShareMenu({withShareTicket: true,menus: ['shareAppMessage', 'shareTimeline']})},// #ifdef MP-WEIXIN// 分享给好友onShareAppMessage(res) {// 区分触发来源const trigger = res.from || 'menu'console.log('分享来源:', trigger)return {title: `${this.goodsName} - ¥${this.goodsPrice}限时抢`,path: `/pages/goods/detail/detail?id=${this.goodsId}&from=share`,imageUrl: this.shareImage || '/static/default-share.png'}},// 分享到朋友圈onShareTimeline() {return {title: `${this.goodsName} - 限时优惠中`,query: `id=${this.goodsId}`,imageUrl: this.shareImage || '/static/default-poster.png'}},// #endif// #ifdef APP-PLUSonShareAppMessage(res) {return {title: `${this.goodsName} - ¥${this.goodsPrice}`,content: '发现一个超值的商品,点击查看',imageUrl: this.shareImage,href: `https://your-domain.com/pages/goods/detail?id=${this.goodsId}`}}// #endif}
App 端多平台分享模板
// components/share-button/share-button.vue<template><button @click="handleShare">分享</button></template><script>export default {methods: {handleShare() {// #ifdef APP-PLUSuni.showActionSheet({itemList: ['微信好友', '朋友圈', 'QQ', '微博', '系统分享'],success: (res) => {const actions = ['weixin', 'weixin', 'qq', 'sinaweibo', '']const type = actions[res.tapIndex]if (res.tapIndex === 4) {// 系统分享this.shareWithSystem()} else {this.shareTo(type)}}})// #endif// #ifdef MP-WEIXIN// 小程序端直接使用原生分享// #endif},shareTo(provider) {// #ifdef APP-PLUSuni.share({provider,type: 0,title: '分享标题',summary: '分享内容',imageUrl: '/static/share.png',href: 'https://your-domain.com',success: (res) => uni.showToast({ title: '分享成功' }),fail: (err) => uni.showToast({ title: '分享失败', icon: 'none' })})// #endif},shareWithSystem() {// #ifdef APP-PLUSuni.shareWithSystem({title: '分享标题',summary: '分享内容',href: 'https://your-domain.com',imageUrl: '/static/share.png',success: (res) => console.log('分享成功'),fail: (err) => console.error('分享失败', err)})// #endif}}}</script>
权限处理完整流程
// components/poster-save/poster-save.vueexport default {methods: {async checkAndRequestAuth(scope, title, content) {return new Promise(async (resolve, reject) => {// 获取当前权限状态const setting = await uni.getSetting()if (setting.authSetting[scope] === true) {// 已有权限resolve(true)} else if (setting.authSetting[scope] === undefined) {// 从未请求过权限try {await uni.authorize({ scope })resolve(true)} catch (e) {// 用户拒绝,尝试引导this.showAuthDialog(title, content)reject(e)}} else {// 之前被拒绝this.showAuthDialog(title, content)reject(new Error('auth denied'))}})},showAuthDialog(title, content) {uni.showModal({title,content,confirmText: '去授权',success: async (res) => {if (res.confirm) {// 打开设置页await uni.openSetting()// 用户可能在设置页关闭了权限,返回后需重新检查const setting = await uni.getSetting()if (!setting.authSetting['scope.writePhotosAlbum']) {uni.showToast({ title: '请开启相册权限', icon: 'none' })}}}})}}}
总结
onShareAppMessage | ||
onShareTimeline | ||
uni.shareuni.shareWithSystem | ||
uni.createCanvasContext | ||
📌 项目推荐:将分享能力封装为独立组件,统一管理分享配置、权限处理和海报生成,便于多项目复用。
夜雨聆风