做微信小程序开发这些年,被问得最多的分享功能其实是两件事:一个是怎么把页面发给微信好友,另一个是怎么分享到朋友圈。很多人以为在页面上放个分享按钮就完事了,但实际上每次分享的入口、参数、用户落地体验、甚至微信的审核规则都不一样,坑踩多了才能摸清楚。
这篇文章我会从分享能力的整体逻辑讲起,把onShareAppMessage和onShareTimeline这两个核心 API 的用法、参数、兼容条件都拆开讲,再把分享图片、动态生成海报、以及线上最常遇到的“菜单不见了”“参数丢了”“图片黑了”这些实际问题一起梳理一遍。内容适合正在做微信小程序、想在电商或内容场景里做分享回流的新手,也适合已经接了分享但总觉得体验不顺畅的开发者参考。
1. 分享能力的三层拆解:好友、群聊、朋友圈到底有什么不同
1.1 微信官方语境里的“转发”和“分享”其实是两套东西
微信小程序里,给好友、群聊发卡片,官方术语叫“转发”,对应的是onShareAppMessage;而分享到朋友圈,对应的是onShareTimeline。这两个虽然都叫“分享”,但从入口到打开后的用户体感完全是两套逻辑。
好友转发有个很直观的入口,就是页面右上角的胶囊按钮“···”。只要页面定义了onShareAppMessage,用户点开右上角菜单就能看到“转发给朋友”这一项。没有定义的话,这个菜单项压根不出现。这也是很多初学者第一次踩坑的地方:代码明明写了分享按钮,右上角菜单里却什么都没有,其实问题就是没在 Page 里声明onShareAppMessage。
朋友圈分享的入口也藏在右上角菜单里,但触发条件更严格:需要基础库版本在 2.11.3 以上,页面里还要定义onShareTimeline,并且要在代码里调用wx.showShareMenu把菜单项显式打开。三者缺一,朋友圈那个入口就不会出现。
1.2 三种去向的体验差异
我做了个表,方便你直观看到“发给好友”“发到群聊”“发到朋友圈”这三类分享行为落地后的差异。
| 分享去向 | 用户点击后看到什么 | 开发者能带什么参数 | 特殊机制 |
|---|---|---|---|
| 发给好友 | 聊天窗口里的卡片,标题 + 图片 + 小程序名称 | title、path、imageUrl | 无 |
| 发到群聊 | 聊天窗口里的卡片,和好友卡片类似 | title、path、imageUrl | 可开启 withShareTicket 获取群标识 |
| 发到朋友圈 | 朋友圈的一条图文动态,点图片后进入单页模式 | title、query、imageUrl | 无 path 字段,不能指定任意页面打开 |
发到朋友圈和另外两种最大的区别,是它没有path字段。onShareTimeline只能返回标题、query 和图片,微信会把当前页面的路径作为基础路径,把你传的 query 拼上去,然后以“单页模式”打开。很多人不理解这一点,硬想在分享到朋友圈时指定另一个页面路径,结果发现没这个参数,这就是设计使然,不是 bug。
1.3 常见认知误区:开通分享不等于完成分享
一个最容易忽略的事实是:分享能力是页面级的,不是全局级的。
你在 app.json 里找不到“统一开启分享”的开关。每个页面想支持转发,就必须在自己的 Page 配置里写onShareAppMessage;想支持朋友圈,就必须写onShareTimeline。有些项目有几十个页面,结果只在首页配了分享,用户在其他页面点右上角菜单就看不到转发项,然后过来质问“为什么分享没了”,这种排查特别浪费时间。
所以在项目初期做页面模板时,最好把分享相关的方法直接写进公共 mixin 或基类里,新页面默认就带分享能力。我见过不少项目是后期一个个页面补的,补到后面总有遗漏。
2. 先实现“发给朋友”:onShareAppMessage 从入门到能上生产
2.1 页面级配置与返回值设计
先把最基础的代码写出来。在 Page 里定义一个onShareAppMessage:
Page({ onShareAppMessage() { return { title: '这个商品真的不错,进来看看', path: '/pages/goods/detail?id=1024&from=share_button', imageUrl: '/assets/share-card.png' }; } });这段代码返回三个字段:
title:分享卡片的标题,一般控制在 20 个字以内,长了会被截断。path:别人点开卡片后进入的小程序页面路径,必须以/开头,可以带 query 参数。imageUrl:分享卡片的配图,建议用 5:4 比例的图片,不传的话微信会截取当前页面作为分享图。
这里有个容易忽视的点:path是给“接收端”用的,而不是给“分享者”用的。你要想清楚,别人点开卡片后落在哪个页面、看到什么内容,然后把对应路径写在path里。很多电商场景会让用户分享一个商品页,但分享出去的path写成了首页路径,结果点进来还得再找商品,转化率掉得厉害。
2.2 自定义分享按钮:open-type="share" 的用法
右上角菜单是系统入口,但产品上通常需要在页面里有个显眼的“分享给好友”按钮。微信小程序提供了button组件的open-type="share":
<button open-type="share" bindshare="handleShareSuccess">分享给好友</button>当用户点击这个按钮时,微信会自动调起分享面板,面板里的内容仍然来自页面里的onShareAppMessage返回值。换句话说,自定义按钮和右上角菜单走的是同一个数据源,你不需要为按钮单独写一套逻辑。
bindshare是用户完成分享后的回调。注意,这个事件在用户“分享成功”时触发,但微信并没有提供 100% 可靠的“对方是否点开”的回调,所以别用它来判断转化,只能用来做“分享动作已完成”的埋点。
2.3 参数透传与前端路由:path 里到底该放什么
分享回流的本质是参数传递。你在分享时把参数拼在path里,用户点开卡片后,小程序通过onLoad(options)接收参数:
Page({ onLoad(options) { const { id = '', from = '' } = options; this.setData({ goodsId: id, shareSource: from }); } });这段逻辑看起来简单,但有几个细节值得注意:
第一,分享出去的path必须以/开头,否则微信会报错或者打开空白页。第二,query 参数里有中文或特殊字符时,一定要先encodeURIComponent,否则参数会被截断或者解析错误。第三,不要把完整分享链接写到path里,那是 H5 的思维,小程序里path只接受页面路径和参数,不接受https://开头的地址。
关于参数约定,我强烈建议项目一开始就统一好from之类的来源字段取值。比如from=share_button表示页面内按钮分享,from=menu表示右上角菜单分享。后面统计数据就能按来源拆分,知道哪个入口带来的用户质量更高。
2.4 异步准备分享内容:从同步 return 到 Promise 的演进
onShareAppMessage最早要求同步返回分享参数,因为微信需要立即拿到数据生成分享卡片。但实际业务里经常遇到这种情况:用户点击分享时,我们得先请求接口拿到最新的优惠券信息,再生成分享文案。
如果只在onShareAppMessage里同步返回一个写死的标题,就会导致分享出去的内容不是最新的。微信从基础库 2.12.0 开始支持onShareAppMessage返回 Promise:
async onShareAppMessage() { const coupon = await getLatestCoupon(); return { title: `领取 ${coupon.amount} 元优惠券`, path: `/pages/goods/detail?id=${coupon.goodsId}&from=share_button`, imageUrl: coupon.cardImage }; }用 Promise 的写法可以等数据回来再生成分享内容,体验会好很多。但要注意基础库版本兼容,如果你的小程序最低支持版本低于 2.12.0,就得用折中方案:在页面加载时提前请求好分享数据,存到data里,用户点击分享时直接读缓存。
3. 再说“分享到朋友圈”:onShareTimeline 的适配与单页模式
3.1 基础库版本与菜单开关:一个都不能少
分享到朋友圈是后面才开放的能力,所以版本限制更明显。要在页面里开启朋友圈分享,三个条件缺一不可:
- 基础库版本不低于 2.11.3;
- 页面里定义了
onShareTimeline; - 显式调用过
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })。
示例代码如下:
Page({ onLoad() { wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] }); }, onShareTimeline() { return { title: '这个商品真的不错,进来看看', query: 'id=1024&from=timeline', imageUrl: '/assets/share-card.png' }; } });很多人会在app.json里找配置项,但那个地方管不了朋友圈分享,必须在页面里处理。
另外还要留意一个点:wx.showShareMenu的调用时机。它需要在页面加载后、用户点开右上角菜单之前执行,所以放在onLoad里是安全的。如果你在某个页面里不希望出现朋友圈入口,可以用wx.hideShareMenu({ menus: ['shareTimeline'] })把它藏起来。
3.2 onShareTimeline 的返回字段与限制
onShareTimeline的返回值字段比onShareAppMessage少一个path,只有title、query、imageUrl。这是很多刚接触的人最容易困惑的地方。
我直接说结论:分享到朋友圈时,微信固定用当前页面路径作为打开路径,你没法指定其他页面。query字段会拼在路径后面,接收端依然通过onLoad(options)拿到。
query的写法有一些注意事项。首先,不要带?前缀,直接写id=1024&from=timeline这种格式。其次,query 有长度限制,千万别塞一长串 JSON 进去,一是容易截断,二是朋友圈场景用户根本没有耐心等待一个臃肿的页面加载。经验是只放定位页面必需的关键参数,比如商品 id、内容 id,其余追踪参数尽量精简。
标题方面,朋友圈分享的标题显示在图片下方,建议做成一句话钩子,配合图片形成“图 + 文”的吸引力。纯标题党容易引起反感,标题里最好像商品标题一样带上核心利益点。
3.3 分享到朋友圈后的用户体感:单页模式的边界
用户从朋友圈点开分享卡片后,进入的是微信小程序的“单页模式”,不是完整的小程序。这个模式下,页面顶部没有胶囊按钮,底部没有 TabBar,用户在页面里看到的只有当前这一页。
单页模式对用户体验的影响体现在几个方面:
- 页面左上角通常只有一个关闭按钮,用户想回朋友圈就点关闭;
- 一些依赖完整宿主环境的能力会受限,比如自定义导航栏、跳转到另一个 Tab 页、支付、登录等操作,在单页模式下会遇到阻碍或表现异常;
- 页面内如果有引导用户“点击右上角···分享”的提示,在单页模式下会失效,因为右上角菜单被隐藏了。
这些不是 bug,是微信故意设计的轻量化展示逻辑。理解这一点后,你在设计分享落地页时就要特别注意:不要把关键转化动作只放在“跳转到首页”或“打开另一个页面”上,尽量在当前页面内提供完整的信息和操作入口。
3.4 跨端框架(uni-app / Taro)里怎么封装
如果你用 uni-app 或 Taro 开发微信小程序,分享逻辑和原生写法很接近,但要注意框架层的兼容判断。
uni-app 里,在页面中定义onShareAppMessage和onShareTimeline的方式与原生 Page 写法基本一致,但朋友圈分享能力需要在 manifest 里确认基础库最低版本设置,否则低版本用户看不到入口。Taro 里也有对应的useShareAppMessage和useShareTimelinehooks,用法和原生对齐。
跨端框架最容易出的问题,是开发者只写了onShareAppMessage,忘了在兼容代码里判断基础库版本,导致部分低版本设备上wx.showShareMenu直接报错。稳妥做法是先做版本判断,再调用菜单控制接口,避免一进入页面就白屏。
4. 分享场景的图片实战:截图默认图、自定义海报和动态生成
4.1 默认截图为什么总是曝光不足
如果不传imageUrl,微信会自动截取当前页面可见区域作为分享卡片图。听起来很方便,但实际效果通常很糟糕。
页面里如果有 loading 状态、弹窗、视频播放器、未加载完成的图片,截图的时间点稍微不对,分享出去的卡片可能就是一张灰蒙蒙或者半截内容的图。在电商场景里,这种截图往往会截到价格区域或者评论区,观感很差。
所以我的建议是:凡是核心分享场景,一定要手动传imageUrl。一张设计好的分享图,比任何代码优化都能提升点击率。
4.2 用 Canvas 2D 生成一张可分享的海报
静态分享图有个问题:每个商品的图片、价格、二维码都不一样,没法提前做素材。这就要求在用户点击分享时,动态生成一张海报。
微信小程序里生成海报的常规做法是用 Canvas 2D。大致步骤是:
- 在 WXML 里放一个隐藏或屏幕外的 canvas 节点;
- 通过
wx.createSelectorQuery拿到 canvas 节点上下文; - 在 canvas 上绘制背景图、商品图、价格文案、小程序码;
- 用
wx.canvasToTempFilePath导出为临时图片文件; - 把临时图片路径作为
imageUrl传给分享接口。
核心代码大概是这个形态:
const query = wx.createSelectorQuery(); query.select('#shareCanvas') .fields({ node: true, size: true }) .exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = wx.getSystemInfoSync().pixelRatio; canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); // 绘制背景、商品图、文字... wx.canvasToTempFilePath({ canvas, success(res) { // res.tempFilePath 就是生成的图片临时路径 } }); });有几个细节直接影响成图质量。第一,canvas 的尺寸要按设备像素比放大,否则导出的图片在手机上看起来发虚。第二,绘制商品图片前要先wx.getImageInfo或者用canvas.createImage()加载图片,加载完成后再画,否则画出来是空白。第三,绘制中文文案时要注意字体大小和换行,Canvas 不会自动换行,需要自己按照字符宽度截断。
小程序码这里多说一句。真正的商品详情页分享图里,二维码一般是带参数的专属小程序码,通常由后端调用微信接口生成,返回给前端后再画到 canvas 上。前端直接画用户头像、昵称等内容时,注意隐私合规,不要过度采集。
4.3 保存图片与临时文件生命周期管理
wx.canvasToTempFilePath生成的图片路径是临时路径,这个临时文件在小程序运行期间有效,但跨会话复用会存在失效风险。如果用户分享后过几天再打开商品详情页,之前缓存的分享图路径可能已经不能用了。
所以有三类处理方式:
- 用完即弃:分享动作发生时即时生成、即时使用,不缓存。
- 保持会话内可用:把临时路径放到全局变量或 storage 里,只在当前小程序生命周期内复用。
- 长期复用:把生成好的海报文件上传到自己的服务器或云存储,返回永久 URL,后端在下一次请求时直接使用。
如果你发现某张分享图总是裂开,优先检查是不是引用了已经过期的临时路径。另外,如果要把图片保存到用户相册,需要调用wx.saveImageToPhotosAlbum,这个接口需要用户授权scope.writePhotosAlbum,小程序里要先引导用户授权再调用,不能悄悄保存。
4.4 不同分享入口的图片适配
同一个页面同时支持好友转发和朋友圈分享时,最好不要让两个入口用同一张图。
好友转发的卡片图是横版比例,偏宽;朋友圈分享的封面图在动态流里展示时更接近方形,微信会对图片做裁剪。如果你直接用一张横向大图去发朋友圈,图片边缘可能被裁掉,关键信息会丢失。
我的做法是为每个入口分别准备imageUrl。onShareAppMessage用 5:4 的横图,onShareTimeline用接近 1:1 的封面图。这样虽然多花一点素材成本,但点击率和整体观感会好很多。
5. 线上最容易踩的五个坑:菜单消失、参数丢失、图片过期、双端差异
5.1 排查链路:右上角菜单为什么没有“分享到朋友圈”
这个问题在线上出现频率非常高,现象是右上角菜单里只有“转发”,没有“分享到朋友圈”。我建议按下面的顺序排查。
| 排查项 | 处理方式 |
|---|---|
| 基础库版本 | 在开发者工具里确认当前基础库版本是否 ≥ 2.11.3,低版本不支持朋友圈分享 |
| 页面是否定义 onShareTimeline | 全项目搜索该页面的 Page 配置里有没有这个函数 |
| 是否调用了 showShareMenu | 检查 onLoad 里有没有显示shareTimeline菜单项 |
| 是否误调用 hideShareMenu | 搜一下项目里有没有无条件隐藏朋友圈菜单的代码 |
| 用户微信版本 | 让用户把微信升级到较新版本,老版本客户端可能不显示入口 |
这里插一句,基础库版本在开发者工具的“详情 - 本地设置”里可以调试,但线上用户的实际基础库分布需要看后台统计。如果你的小程序还在支持很老的基础库,朋友圈分享功能要有降级方案,至少不能让页面报错。
5.2 排查链路:分享卡片打开了但参数丢了
参数丢失是我见过最多的线上问题之一。有次一个客户反馈,从分享卡片打开的商品页总是定位到默认商品,后来发现是path里的商品 id 被中文参数干扰,解析出来是空字符串。
解决这个问题要抓住几个关键点:
path必须以/开头,且不能是网络地址;- query 参数统一用
encodeURIComponent编码,接收端记得decodeURIComponent; - 参数名不要用微信保留字段,比如
scene在扫码场景里有特殊含义,分享场景里最好避开; - 如果你改了页面路径,旧分享卡片里的路径可能已经失效,用户从聊天记录点进来会提示页面不存在。
另外还有一个隐蔽场景:用户从朋友圈单页模式进入时,页面可能没有走正常的onLoad,此时要确认参数是从onLoad(options)里取,还是从onShow里取。单页模式下onLoad依然会执行,但如果有页面栈复用的情况,参数可能不会重新触发onLoad,需要同时在onShow里做一次兜底处理。
5.3 排查链路:图片不显示或者模糊
分享图片显示异常通常分两类:一类是直接不显示,另一类是显示但模糊。
先看不显示。如果你在imageUrl里传的是网络图片,而且这个域名的下载没有配置到小程序后台的合法域名里,微信在生成分享卡片时可能拉取不到图片。官方文档允许网络图片路径,但线上实际体验中,分享卡片渲染图片的时机不可控,网络图片加载失败的概率比本地图片高。我的习惯是:分享图片先通过wx.downloadFile下载到本地,拿到临时路径后再传给imageUrl。这样虽然多点一步,但稳定很多。
再看模糊。分享图模糊通常有两个原因:一是素材本身分辨率低,比如用了一张 200x200 的缩略图去当分享封面;二是 canvas 导出图片时没有按设备像素比放大。解决问题的方式很简单,设计稿用 2 倍图,canvas 导出时destWidth和destHeight设置为显示尺寸的 2 到 3 倍。
5.4 iOS 与安卓在分享场景里的差异
双端差异主要在图片缓存和页面表现上。
iOS 对分享卡片图片的缓存策略比较激进。你更新了分享图,但老用户分享出去的卡片可能还是旧图,这个不是代码能立刻解决的,只能靠图片 URL 变化来规避。比如上传到服务器时,每次重新生成的海报文件名带时间戳,能尽量让微信重新拉取。
安卓端相对好一些,但在单页模式下,不同机型的导航栏返回行为有差异,有的机型显示“关闭”文字按钮,有的显示 X 图标。这些差异不会影响功能,但测试时要覆盖主流机型。
还遇到过一个很具体的问题:部分安卓机型在 canvas 绘制时,如果 canvas 节点在页面里是display: none,绘制结果会是空白。解决办法是把 canvas 放到屏幕外而不是用 display 隐藏,比如position: fixed; left: 9999px; top: 0;。
6. 分享回流的数据复盘:如何判断一次分享到底带来了什么
6.1 分享追踪的埋点设计
分享功能做完还不算完,如果不做数据回收,你根本不知道分享按钮放在哪里转化率最高,也不知道哪个渠道的用户质量最好。
我的埋点方案比较简单:在所有分享入口的path或query里统一带from字段,然后统计每个from值对应的打开次数和最终转化次数。
// 页面内按钮分享 path: `/pages/goods/detail?id=1024&from=share_button` // 右上角菜单分享 path: `/pages/goods/detail?id=1024&from=menu` // 朋友圈分享 query: 'id=1024&from=timeline'接收端在onLoad里把这个from上报到数据平台,同时记录当前页面的goodsId。这样就能看到同一个商品,通过“页面按钮”和“右上角菜单”带来的访问量差异。
这里有个细节:用户点开分享卡片进入页面后,如果又点击右上角菜单再转发一次,那么这个新分享出去链接的from字段应该重新标记,否则会出现来源归因混乱。处理方式是在页面onShow里检测当前是否已经有分享来源参数,如果用户是从分享链接进来的,就不要再把旧的from透传下去。
6.2 同一个页面如何给不同渠道不同文案
同一个页面在做分享时,不一定只能有一种标题。比如一个抽奖活动页,分享给好友时可以说“邀请好友一起抽奖”,分享到朋友圈时可以说“今天运气不错,抽到了 XX 奖品”。两种场景下的用户心智完全不同。
实现方式也不复杂,在data里维护一套分享文案模板,根据当前页面的业务状态动态决定title和imageUrl:
Page({ data: { shareTitle: '', shareImage: '/assets/default-share.png' }, onShareAppMessage() { return { title: this.data.shareTitle, path: `/pages/activity/index?from=share_button`, imageUrl: this.data.shareImage }; }, onShareTimeline() { return { title: this.data.shareTitle, query: 'from=timeline', imageUrl: this.data.shareImage }; } });这里有个容易忽视的点:如果onShareAppMessage里通过 Promise 去请求新数据,而页面已经切到后台,回来后 Promise 的结果可能无法正确注入分享卡片。所以我更推荐在页面核心数据准备好的时候就同步算好分享文案,分享方法只是读数据、返回数据,不承担网络请求任务。
6.3 分享一下,体验和转化的平衡点
分享按钮不是越多越好,过度设计会损害产品体验。
以电商小程序为例,商品详情页的分享按钮通常放在底部操作栏,旁边是购物车和立即购买。这里分享的转化率高,是因为用户正好在做决策。但如果是在工具型小程序里,比如一个计算器页面,用户没有分享动机,放一个巨大的分享按钮反而显得突兀。
好的做法是找到用户“完成某件事后最有表达欲”的节点,顺势引导分享。比如抽奖结果页、成绩单页、优惠券领取成功页,这些场景用户天然愿意分享,这时候放分享按钮的转化率远高于主页面。
还要理解微信对诱导分享的态度。用红包、实物奖励强制要求用户分享给多个好友才能领取,很容易触碰平台规则,严重的会被限制分享能力。合规的做法是“分享后可获得额外抽奖机会”这类让用户自主选择的机制,同时要保证不分享也能享受基本功能。
最后说一个我自己长期用的习惯:每次分享功能上线后,我会先用两台手机、不同微信版本、不同网络环境把分享链路完整走一遍,重点检查右上角菜单、参数接收、图片渲染三个环节。这个动作虽然简单,但能挡掉大量线上事故。分享不是一个点,而是一条链路,每一环都稳了,用户才会愿意点那一下。