ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

原生小程序商城开发实战:从setData到webview与支付集成

原生小程序商城开发实战:从setData到webview与支付集成 简介这是一套基于 JavaScript 开发的原生微信小程序源码及配套说明面向需要搭建或二次开发电商类小程序的开发者。项目以芸众商城为核心场景覆盖商品展示、购物车、订单管理、支付、用户系统等常见模块凭借全开源特性可依业务需求深度定制。代码使用微信官方原生开发框架借助组件与 API 完成页面构建及服务器交互并通过 Ajax 异步通信、虚拟 DOM 更新等机制优化浏览和操作体验包内还整合了 webview 内嵌 H5 的扩展思路便于加载外部页面或第三方服务。资源压缩包约 479KB虽体积不大但内容聚焦适合已有 JavaScript 基础、希望研究原生小程序商城架构或借鉴开源实现的初中级开发者。目前已有 242 人学习对快速理解商城小程序的工程组织与关键交互设计具有参考价值。1. 为什么芸众wxapp要用原生小程序而不是H5壳这套芸众wxapp源码让我最意外的地方是它没有走大多数商城项目的捷径——套一个H5壳再包装成小程序而是用微信原生小程序框架重写了整个前端。做过电商项目的开发都清楚H5壳开发快但页面打开慢、长列表滚动卡购物车和结算这种高频操作体验尤其明显。芸众把原生开发放在前面同时保留全开源代码给二次开发留出了充足空间。这套代码适合两类人一类是接商城定制开发的自由职业者需要一套能直接改的模板进场另一类是业务稳定后想把前端彻底握在自己手里的技术团队。拿到手先别急着跑第一件事是把 biggest2t3 这条分支定位清楚它决定了当前代码处于哪个迭代阶段。2. 拆解芸众原生小程序的工程骨架与模块边界很多同学拿到一份全开源小程序代码第一反应是打开 app.json 看注册了多少个页面这个方法没问题但还不够。我更建议先看一眼 pages 目录下每个文件夹里的文件构成。一个典型的页面文件夹里会有四个文件.js 处理逻辑、.wxml 描述结构、.wxss 控制样式、.json 配置当前页面的窗口表现和组件引用。芸众wxapp遵循的就是这个原生约定。2.1 从目录树看页面与组件的边界把整个工程的目录摊开看边界会比想象中清晰yunzhong-wxapp/ ├── app.js # 全局逻辑登录态初始化、全局数据 ├── app.json # 页面路由、tabBar、分包配置 ├── app.wxss # 全局样式变量 ├── pages/ │ ├── index/ # 商城首页 │ ├── goods/ # 商品详情 / 商品列表 │ ├── cart/ # 购物车 │ ├── order/ # 订单列表 / 订单详情 │ ├── user/ # 用户中心 │ └── webview/ # wxapp-webview-h5-yunzhong 所在页面 ├── components/ # 可复用业务组件 │ ├── goods-card/ # 商品卡片 │ └── stepper/ # 数量选择器 ├── utils/ │ ├── request.js # wx.request 统一封装 │ └── util.js # 价格、时间等格式化工具 └── static/ # 静态资源这套结构的边界很清楚pages 里放的是有独立路由的页面components 里放的是被多个页面复用的视图单元。二次开发时最容易犯的错误是把所有东西都塞进页面文件里导致一个商品卡片在首页和搜索结果页各写一份。在芸众这种商城项目里商品卡片往往要同时出现在首页推荐、分类列表、搜索结果、订单快照等多个位置把它抽成组件是必须的。判断边界就一条标准同一块界面超过两个页面在用就移到 components 里同时把商品ID作为组件属性传进去。2.2 页面模块与后端接口的数据约定前面目录里出现了 utils/request.js这是整个前端能跑起来的关键。芸众商城采用典型的前后端分离架构前端通过 HTTP 接口获取商品、用户、订单数据。原生的 wx.request 是微信提供的基础网络请求 API直接在业务页面里写会让登录校验、错误提示、token 刷新逻辑散落各处所以芸众类项目里几乎都会做一层封装。我一般会这样组织请求模块// utils/request.js const BASE_URL https://your-api-host.com/api; function request(path, { method GET, data {}, needAuth true } {}) { return new Promise((resolve, reject) { const header { Content-Type: application/json }; const token wx.getStorageSync(token); if (needAuth token) { header[Authorization] Bearer token; // 芸众后端常用 JWT 鉴权 } wx.request({ url: BASE_URL path, method, data, header, success(res) { if (res.data.code 0) { resolve(res.data.data); } else if (res.data.code 401) { // token 过期走静默登录刷新流程 handleTokenExpired(); reject(res.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail(err) { reject(err); } }); }); } module.exports { request };这里封装的核心逻辑是三点把业务状态码和 HTTP 状态分离code 为 0 才算成功每个请求自动带上存储在本地的 token避免每个页面重复取401 统一拦截而不是让用户在某个页面里看到登录过期弹窗后茫然无措。参数方面method 默认是 GET需要提交数据的场景显式传 POSTneedAuth 字段用于登录页这类不需要鉴权的接口。实际对接时要把 BASE_URL 换成你部署的芸众商城后端域名token 的存储 key 也要和后端下发的字段保持一致常见的有 token、access_token 两种我在对接的时候遇到过因为这儿不一致导致所有请求都 401 的尴尬。下表是芸众商城几个核心模块最常打交道的接口约定接口路径以你手上这份源码实际的 api.js 配置为准但字段风格基本一致模块典型接口请求方法关键参数商品列表/goods/listGETpage, pageSize, categoryId商品详情/goods/detailGETid加入购物车/cart/addPOSTgoodsId, num, spec订单创建/order/createPOSTgoodsList, addressId, remark支付参数/order/pay-paramsPOSTorderId, payType用户信息/user/infoGET无token 决定身份这张表解决一个问题当你看到页面报错时能快速区分是前端字段拼错了还是后端根本没接这个接口。调试建议是先用微信开发者工具里的 Network 面板观察请求路径和响应结构对照这个表里的规格去查。2.3 biggest2t3 分支上的几个关键变更点biggest2t3 看起来像是一个分支或者 tag 的命名在版本管理里这类标识通常用来追踪某个迭代阶段。对芸众这类持续迭代的商城系统git 提交记录里往往写明了每次变更伴随的业务调整。比如商品表加了限购字段、订单里多了分销佣金计算、支付回调新增了签名校验这些都会体现在代码中。常见做法是先在仓库里执行 git branch -a 和 git tag 看全部分支与标签再 git diff 对比相邻两个节点之间的差异。从商城系统的演进规律看biggest2t3 这类版本多半聚焦在三个位置商品接口增加新的过滤条件、分销相关数据的展示逻辑、以及小程序端的分享裂变参数。改这些位置有个通用技巧不要直接在大段代码里找差异先全局搜商品详情页里发给后端接口的 data 对象把所有新增字段列出来和上一版本对比就能看出这个版本加了什么能力。当你不确定字段含义时去翻后端接口的入参校验那里面通常会写明每个字段的可选值和用途比注释更可靠。碰到命名像 biggest2t3 这种含义不明的节点建议先 checkout 到该节点确认能正常编译再往里加业务代码避免在错误的基线上一路改下去。3. JavaScript 在芸众商城里的关键实现数据流、状态与支付3.1 setData 与虚拟 DOM 的取舍小程序的数据驱动机制先把一个容易混淆的概念掰开微信小程序并没有像完整的虚拟 DOM 那样的 diff 层它的更新模型是逻辑层与渲染层分离逻辑层通过 this.setData 把数据序列化后传到渲染层由渲染层以数据驱动的方式更新界面。前端圈子里常讲的虚拟 DOM 优化在这里并不完全适用芸众wxapp 里真正影响性能的是 setData 的调用频率和单次传输的数据量。我第一次接这类项目时习惯性地把整个购物车对象直接 setData 上去结果在低端安卓机上明显感觉到滚动掉帧后来把大对象拆成具体字段更新页面就顺畅了。// 页面加载后拉取商品列表并更新 async loadGoods(page) { const goodsList await request(/goods/list, { data: { page, pageSize: 10 } }); // 用计算属性名精确指定要更新的数组下标不会触发整列表渲染 this.setData({ [goodsList[${page - 1}]]: goodsList.items, hasMore: goodsList.items.length 10 }); }这段代码里用了 ES6 的计算属性名语法本质上是告诉渲染层只需更新这一段商品数据。如果后端返回的不是分页结构而是完整列表可以先合并对象再整体 setDataconst merged { ...this.data.goodsList, ...newItems };展开运算符合并对象这种写法在 JavaScript 日常开发里最常用语义也比 Object.assign 更直观。参数 page 代表当前加载的分页序号pageSize 是每页条数这两个值要和后端约定的分页规则保持一致不然会出现页码对不上、列表重复加载的问题。社区里常见的优化思路还包括只 setData 当前滚动区域内可见的字段把图片地址等不常变的属性留在初次渲染时一次性设置。虚拟 DOM 那种先算差异再更新的思路在小程序里被简化成了你告诉我哪里变了我就更新哪里所以开发者自己要承担这个差异计算的责任这正是原生小程序和 H5 商城在写法上最大的区别。3.2 购物车与订单状态管理状态机与 filter 实战商城里的购物车和订单本质上是状态机。从状态机视角来看更容易设计数据结构购物车项有选中/未选中订单状态有等待付款、待发货、待收货、已完成、已取消等。芸众商城前端里订单列表往往会用一个字段表示状态值再用一个映射表把状态值翻译成人话。这类映射如果用 if-else 堆后续新增一个状态就要改三四处的展示和操作逻辑我把常见做法整理成下面的表状态值页面展示可执行操作0待付款去支付 / 取消订单1待发货提醒发货2待收货确认收货3已完成再次购买 / 评价4已取消删除记录状态机的价值在于它把当前状态能做什么固化下来避免用户从待付款直接点到评价按钮。前端实现时用数组 filter 来按状态筛选订单是最高效的 JavaScript 方案这也是 javascript filter 函数在社区里被高频提及的原因// 按状态过滤订单返回当前状态可见的订单数组 function filterOrdersByStatus(orders, status) { return orders.filter(order order.status status); } // 页面里获取待付款订单的数量 const pendingPaymentCount filterOrdersByStatus(this.data.orders, 0).length;filter 是数组原生的高阶方法接收一个返回布尔值的回调函数只有回调返回 true 的元素才会被收进新数组原数组不受影响。用在这里的好处是语义清晰、无需维护额外状态而且可以和其他数组方法链式调用比如先 filter 再 map把过滤后的订单数据提取成渲染需要的字段。真正复杂的是购物车的全选和反选逻辑那需要维护一个 checkedMap 对象把商品 ID 映射为布尔值每次勾选只更新局部数据。避免把勾选状态和商品数据混在同一个数组对象里否则 setData 时要整个重建数组性能又回到上一节说的问题里去了。3.3 支付串联从下单到 wx.requestPayment 的完整链路支付是商城类小程序里坑最多的环节。正确的调用链是先在后端创建订单再请求后端返回支付参数然后前端用 wx.requestPayment 唤起微信支付面板最后在成功回调里跳转订单详情页。很多人第一次写会漏掉第二步直接在前端用商品金额拼一个支付参数出来这在真实环境里必挂因为微信支付的签名必须由后端用商户密钥生成前端不持有密钥也没有权限。落地的代码大概是这样的// 订单支付先向后端换取支付参数再唤起微信支付 async payOrder(orderId) { const payParams await request(/order/pay-params, { method: POST, data: { orderId, payType: wechat } }); wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, // 注意是小写 package是 prepay_idxxx 格式 signType: payParams.signType || RSA, paySign: payParams.paySign, success() { wx.navigateTo({ url: /pages/order/detail?id orderId }); }, fail(res) { // errMsg 里包含 cancel 表示用户主动取消不弹错误提示 if (res.errMsg res.errMsg.indexOf(cancel) -1) { wx.showToast({ title: 支付发起失败, icon: none }); } } }); }这段代码里最容易栽跟头的是参数名。接口文档里返回的时间戳往往是 timeStamp但有些后端返回 timestamp混着用会出现支付签名校验失败package 字段在微信支付参数里是保留字后端取值传过来时如果是包装过的对象需要先取出来再塞进 wx.requestPayment。用户取消支付和支付失败是两种完全不同的事件我用 errMsg 里是否包含 cancel 来区分这样不会在用户取消时弹出刺眼的报错。这些字段的对应关系建议做成一张表留在项目文档里微信小程序字段后端常见字段备注timeStamptimeStamp / timestamp统一转换为字符串nonceStrnonce_str / nonceStr随机字符串长度有限制packageprepay_id值为 prepay_idxxxx 格式paySignpaySign / sign后端签名后的结果signTypesignTypeMD5 或 RSA与后端算法一致提示调试支付没有捷径必须用真机预览开发者工具里的模拟支付和真机行为有差异。如果报支付验证签名失败先把后端返回的五个参数原样打出来和生产支付商户号上配置的证书、密钥逐项核对绝大多数问题出在密钥配置上而不是前端代码。4. wxapp-webview-h5-yunzhong内嵌 H5 的正确打开方式4.1 什么场景该上 webview什么场景不该上这里我直接说结论芸众商城里的视频播放页、会员协议、帮助文档、活动落地页这类内容型页面放进 webview 里很合适因为这些内容更新频繁让运营直接改 H5 页面比发版小程序快得多。但商品列表、购物车、结算这些核心交易链路我不建议往里塞。原因很实际webview 页面在小程序里是一个黑盒无法直接调用小程序的登录态、无法精准控制分享行为用户从 H5 里点支付会跳出小程序原生支付环境体验是断的。做一个直接对比能看得比较清楚对比维度原生页面webview 内嵌 H5更新频率需要发布新版本后端发版即时生效支付体验原生拉起微信支付受限于 H5 内部实现登录态原生 token 直接可用需要通过 URL 或 JSBridge 传入页面性能接近原生依赖浏览器内核审核风险正常审核加载外部网页需配置业务域名在项目结构里看到 wxapp-webview-h5-yunzhong 这个文件名基本可以确定这是芸众把某些页面放进 webview 容器的实现最常见的就是协议页、分销说明页和营销活动页。接到这类需求时先问一句这个页面是内容展示还是业务操作内容展示优先考虑 webview业务操作要评估能不能用原生页面重写。4.2 web-view 组件的接入与 URL 参数拼接微信为原生小程序提供了内置的 web-view 组件芸众的 webview 页面在 wxml 里的写法很直接!-- pages/webview/index.wxml -- web-view src{{h5Url}} bindmessageonH5Message/web-viewsrc 是唯一决定页面内容的属性它必须是一个以 https:// 开头的合法地址而且在微信公众平台后台必须配置为该小程序的业务域名否则真机上会白屏并提示该域名不合法。我一般会在进入 webview 页面前拼好完整链接把小程序侧的身份信息通过 URL 参数带给 H5// pages/webview/index.js Page({ data: { h5Url: }, onLoad(options) { const token wx.getStorageSync(token); const baseH5Url decodeURIComponent(options.url || ); const targetUrl baseH5Url (baseH5Url.indexOf(?) -1 ? : ?) token encodeURIComponent(token) fromwxappt Date.now(); this.setData({ h5Url: targetUrl }); } });参数拼接这里我用了 encodeURIComponent因为 token 里可能带有特殊符号不编码会截断 URL 或导致参数解析错位。fromwxapp 是给 H5 端识别来源用的让 H5 知道当前是跑在小程序里可以启用微信内快捷登录之类的差异化逻辑tDate.now() 是为了防止页面被浏览器缓存每次进入都拿最新的内容。onLoad 阶段 setData 一个带 token 的 URL 发生在页面渲染之前所以用户端不会看到先加载旧地址再跳转的闪烁。要注意 web-view 组件覆盖层级高几乎不会受到普通页面样式影响但页面内导航栏需要在小程序页面 json 里配置用 navigationStyle 自定义导航时要给 webview 页面单独留出返回按钮否则用户一旦进入 H5 内部就退不回来。4.3 原生层与 H5 页面的双向通信记住一个常识小程序 webview 与 H5 的通信是单向且受控的。H5 页面可以通过 wx.miniProgram.postMessage 向小程序发送消息但小程序并不能随时收到只有在特定时机——比如页面分享、组件销毁、特定生命周期回调——消息才会被 bindmessage 捕获。不少人在这个地方调试了半天发现 postMessage 发了但 onH5Message 不触发原因就是时机不对。// H5 页面里向小程序发消息 const wx window.wx; // 由微信注入的 JSBridge const payload { type: order:complete, orderId: 2024001 }; wx.miniProgram.postMessage({ data: payload }); // H5 里跳转小程序原生页面 wx.miniProgram.navigateTo({ url: /pages/order/detail?id2024001 });对应的小程序侧监听 bindmessage 拿到数据后通常不会直接修改当前页面状态而是把数据暂存起来供用户返回原生页面时使用。实际项目里还有一条反向通路小程序要通知 H5 执行动作最安稳的方式仍然是重新设置 web-view 的 src让 H5 重新加载并解析新的参数。这种方式简单、绕开了 JSBridge 的时机限制代价是 H5 会整体刷新一次影响连续操作的体验。做一个通信方式对比表会更明确方向手段到达时机小程序到 H5修改 web-view src 参数页面重新加载时H5 到小程序wx.miniProgram.postMessage分享、销毁时机由 bindmessage 触发H5 到小程序wx.miniProgram.navigateTo立即跳转H5 到小程序wx.miniProgram.switchTab立即切换 tab设计通信方案时最重要的原则postMessage 只能用来传递结果和事件不能作为核心业务驱动的唯一手段。如果 H5 里完成了一个关键操作需要让小程序页面刷新建议在 postMessage 里带上完整的业务标识小程序收到后重新请求接口而不是信任 H5 传来的显示数据。5. 从 biggest2t3 起步的二次开发先动这三个地方5.1 把商品数据源改造成可动态配置对于把同一份源码交付给多个客户使用的团队来说最头疼的是每次上线都要改 BASE_URL 和商品接口路径。我在接芸众这类项目时会把接口地址和站点标识提到一个独立配置文件里// config/index.js module.exports { apiBaseUrl: https://your-api-host.com/api, siteId: your-site-id, enableShare: true, payType: wechat };页面里通过 require 引入这份配置所有请求从 config.apiBaseUrl 取前缀。这样换客户时只需要改这个文件不用全局搜索替换每个页面里的 URL。配合微信开发者工具的编译模式功能可以把不同客户的站点配置做成多个编译模式调试时一键切换而不是频繁改代码。别小看这一步我见过不少项目因为直接改源码里的商户号而导致误提交把 A 客户的支付配置发到了线上。5.2 独立分包与分包预下载的配置商城项目最容易被微信的 2MB 主包大小限制卡住。芸众这类功能齐全的商城商品详情、订单、用户中心等页面打包后很容易超限。用独立分包可以把重量级的页面拆分出来用户从首页进入时再按需加载// app.json 片段 { pages: [pages/index/index, pages/goods/detail], subpackages: [ { root: pages/order, pages: [list/index, detail/index] } ], preloadRule: { pages/index/index: { network: wifi, packages: [pages/order] } } }分包的收益有两个一是主包体积降下来更容易过审核和提升启动速度二是 preloadRule 让用户在 WiFi 环境下提前把订单包下载好进入订单页时几乎无感加载。配置时注意 subpackages 里的 root 字段不能写在 pages 里再次注册preloadRule 的 key 要写触发预加载的页面路径network 选项还可以配成 all但 WiFi 条件下比较稳妥不会让用户在弱网情况下被迫消耗流量预下载。5.3 真机调试最容易翻车的三个位置最后把我在这类项目上踩过的真机问题集中说一下。第一个是本地存储的登录态。开发工具里 wx.getStorageSync 偶尔能读到开发时残留的旧 token真机上用户数据是隔离的表现是请求 401。处理办法是登录逻辑里加一个版本号每次发布前把 storage 里版本号不匹配的数据清掉。第二个是 webview 白屏。直接把 H5 地址放到开发者工具里可能正常真机却白屏最常见原因是业务域名没配置或证书过期先在后台域名配置里核对再看 H5 服务器返回的证书是否有效。第三个是支付回调丢失。用户付完款后没有跳回小程序经常是因为支付参数里的 package 值没有按照前面提到的 prepay_id 格式组装后端返回的是原始 prepayid 字符串时要补上前缀。真机调试的顺序也有讲究别一上来就复现路径。先在开发者工具的 Network 面板跑通接口链路确认数据正常再用预览二维码在真机上走一遍非支付流程最后才用体验版测试支付和 webview。这样每一步的问题范围都是被切开的出状况时直接看对应的 network 请求和 storage 值比盯着报错弹窗猜要高效得多。时间紧的时候可以先跳过 5.1 和 5.2 直接做业务但登录态版本号一定不能省省下的那点功夫会在 401 调试里加倍还回来。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表