前端实战:小程序搭建商品购物全流程
目录
项目概述
开发环境搭建
微信开发者工具下载与安装
项目创建流程
项目目录结构及各文件作用
商品展示页面开发
页面布局(WXML 与 WXSS)
数据获取与绑定(JavaScript)
加入购物车功能实现
购物车功能开发
购物车页面布局
购物车数据处理
全选、反选与结算功能
订单生成与支付功能开发
订单生成逻辑
支付功能接入
项目优化与完善
性能优化
安全措施
项目概述
在移动互联网蓬勃发展的当下,电商行业呈现出爆发式增长态势。消费者的购物习惯逐渐从传统线下购物向线上购物转移,这使得各类电商平台如雨后春笋般涌现。而小程序,作为一种无需下载安装、即点即用的轻量级应用,凭借其便捷性、低门槛以及与社交平台的紧密结合等优势,迅速在电商领域占据了一席之地。
搭建一个小程序商品购物系统,不仅能为商家拓展销售渠道,降低运营成本,还能为消费者提供更加便捷、高效的购物体验。通过小程序,商家可以随时随地展示商品信息,开展促销活动,与消费者进行互动;消费者则可以在微信等社交平台中轻松访问购物小程序,实现快速浏览商品、下单购买、支付结算等一系列操作,无需再在多个应用之间切换,大大节省了购物时间和精力。同时,小程序的社交分享功能还能让消费者将心仪的商品分享给亲朋好友,实现裂变式传播,为商家带来更多潜在客户。
接下来,让我们详细探讨如何开发一个功能完备的小程序商品购物系统,从项目的搭建、页面的设计、功能的实现到最终的部署上线,一步步揭开小程序开发的神秘面纱。
开发环境搭建
要开启小程序商品购物系统的开发之旅,首先得搭建好开发环境,而微信开发者工具就是我们的得力助手。下面来详细介绍其下载安装以及项目创建的流程。
微信开发者工具下载与安装
- 下载:打开浏览器,访问微信官方文档中的开发者工具下载页面(https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html )。这里提供了 Windows、Mac 和 Linux 等不同操作系统版本的下载选项,根据自己的电脑系统选择对应的安装包进行下载。
- 安装:下载完成后,找到安装包并双击运行。在安装向导中,点击 “下一步”,阅读并同意许可协议后,点击 “我接受”。接着可以自定义安装目录,选择好安装位置后点击 “安装”,等待安装过程完成,最后点击 “完成”,微信开发者工具就成功安装在你的电脑上了。
项目创建流程
- 登录:打开安装好的微信开发者工具,使用微信扫码登录。登录成功后,在欢迎界面点击 “新建项目”。
- 填写项目信息:在弹出的新建项目窗口中,依次填写项目名称,例如 “shopping - mini - program” 。然后填写 AppID,如果没有正式的 AppID,也可以勾选 “测试号” 选项,使用测试号进行开发测试。接着选择项目的存储位置,最后点击 “新建” 按钮,项目就创建成功了。
项目目录结构及各文件作用
项目创建完成后,会看到一个清晰的目录结构,主要包含以下文件和文件夹:
- pages 文件夹:存放小程序的各个页面,每个页面都对应一个独立的文件夹。例如,“pages/home” 文件夹下存放着首页的相关文件,包括 “home.js”(页面逻辑文件,用于处理页面的数据请求、事件响应等逻辑)、“home.json”(页面配置文件,可对该页面的导航栏样式、是否允许下拉刷新等进行设置,会覆盖全局配置中的相关部分)、“home.wxml”(页面结构文件,使用类似 HTML 的标签语言来构建页面的布局和元素)、“home.wxss”(页面样式文件,用于定义页面元素的样式,类似于 CSS )。
- utils 文件夹:通常用于存放一些工具函数,比如日期格式化函数、网络请求封装函数等。例如 “utils/util.js” ,在这个文件中可以定义一个格式化日期的函数,方便在各个页面中使用。
- app.js:小程序的入口文件,主要用于监听和处理小程序的生命周期函数,如小程序启动时触发的 “onLaunch” 函数、小程序显示在前台时触发的 “onShow” 函数等,同时也可以在这里声明一些全局数据,比如 “globalData” ,用于存储用户登录信息等在整个小程序中都可能用到的数据。
- app.json:小程序的全局配置文件,它决定了小程序的页面路径、窗口表现、底部 tab 栏配置等重要信息。例如,“pages” 数组中定义了小程序所有页面的路径,“window” 对象中可以设置小程序窗口的背景颜色、导航栏颜色、标题等。
- app.wxss:小程序的全局样式表文件,在这个文件中定义的样式会作用于整个小程序的所有页面,可用于设置全局的字体、背景颜色等基础样式。
- project.config.json:微信开发者工具的项目配置文件,包含项目的名称、appid、编译设置、调试设置等信息。比如可以在这里设置是否开启 ES6 语法支持、是否检查代码中的 URL 等。
- sitemap.json:小程序的站点地图文件,用于 SEO 配置,帮助微信搜索引擎更好地爬取小程序的页面,通过配置 “rules” 数组,可以指定哪些页面允许被微信索引,哪些页面不允许。
商品展示页面开发
页面布局(WXML 与 WXSS)
商品展示页面是用户接触小程序的重要界面,其布局的合理性和美观性直接影响用户体验。下面是一个简单的商品展示页面的 WXML 结构代码:
<view class="goods-list">
<block wx:for="{
{goodsList}}" wx:key="id">
<view class="goods-item">
<image class="goods-img" src="{
{item.imgUrl}}" mode="aspectFill"></image>
<view class="goods-info">
<text class="goods-name">{
{item.name}}</text>
<text class="goods-price">¥{
{item.price}}</text>
<button class="add-cart-btn" bindtap="addToCart" data-goodsId="{
{item.id}}">加入购物车</button>
</view>
</view>
</block>
</view>
在这段代码中,外层的<view class="goods - list">用于包裹整个商品列表。<block wx:for="{ {goodsList}}" wx:key="id">通过wx:for指令循环渲染商品列表,wx:key用于提高渲染性能,确保列表项的唯一性 。每个商品项由<view class="goods - item">包裹,其中<image>标签用于展示商品图片,src="{ {item.imgUrl}}"绑定图片的 URL;<view class="goods - info">中包含商品名称<text class="goods - name">{ {item.name}}</text>、商品价格<text class="goods - price">¥{ {item.price}}</text>以及加入购物车按钮<button class="add - cart-btn" bindtap="addToCart" data - goodsId="{ { item.id}}">加入购物车</button>,bindtap绑定了点击事件addToCart,data - goodsId="{ { item.id}}"用于传递商品的 ID。
对应的 WXSS 样式代码如下:
.goods-list {
padding: 10px;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.goods-item {
width: 45%;
margin-bottom: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
border-radius: 5px;
overflow: hidden;
background-color: #fff;
}
.goods-img {
width: 100%;
height: 200px;
}
.goods-info {
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.goods-name {
font-size: 16px;
color: #333;
margin-bottom: 5px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.goods-price {
font-size: 14px;
color: #f60;
margin-bottom: 10px;
}
.add-cart-btn {
width: 100%;
background-color: #f60;
color: #fff;
border: none;
border-radius: 3px;
padding: 8px 0;
font-size: 14px;
}
.add-cart-btn:active {
background-color: #ff8c00;
}
在上述 WXSS 代码中,.goods - list设置了内边距、弹性布局,使商品项能够灵活排列且在容器内均匀分布;.goods - item定义了商品项的宽度、外边距、阴影、圆角以及背景颜色,使其看起来更加美观和突出;.goods - img设置图片宽度为 100%,高度为 200px,保证图片能够完整展示;.goods - info设置了内边距和弹性布局方向,使商品信息垂直居中显示;.goods - name和.goods - price分别设置了商品名称和价格的字体大小、颜色等样式,并且对商品名称进行了溢出处理,防止名称过长影响页面布局;.add - cart-btn设置了加入购物车按钮的宽度、背景颜色、文字颜色、边框、圆角以及点击时的背景颜色变化,增强了按钮的交互性。
数据获取与绑定(JavaScript)
商品展示页面的数据通常来自后端服务器,通过接口调用获取数据后,再将数据绑定到页面上进行展示。在小程序中,使用wx.request方法来发起 HTTP 请求获取数据。假设后端提供了一个获取商品列表的接口https://api.example.com/goods,在页面的 JavaScript 文件中,可以这样编写代码获取数据:
Page({
data: {
goodsList: []
},
onLoad: function() {
this.fetchGoodsList();
},
fetchGoodsList: function() {
wx.request({
url: 'https://api.example.com/goods',
method: 'GET',
success: (res) => {
if (res.statusCode === 200) {
this.setData({
goodsList: res.data
});
} else {
console.error('请求失败,状态码:', res.statusCode);
}
},
fail: (err) => {
console.error('网络请求出错:', err);
}
});
}
});
在这段代码中,首先在data中定义了一个goodsList数组,用于存储获取到的商品数据。在onLoad生命周期函数中调用fetchGoodsList方法来获取商品列表。fetchGoodsList方法使用wx.request发起 GET 请求,url为后端接口地址。如果请求成功(statusCode为 200),将后端返回的数据res.data通过setData方法更新到页面的data中,这样页面上绑定的goodsList数据就会更新,从而实现商品列表的动态展示;如果请求失败,在控制台打印错误信息。
加入购物车功能实现
当用户点击商品展示页面的 “加入购物车” 按钮时,需要将对应的商品信息添加到购物车中。在 WXML 中,按钮已经绑定了addToCart点击事件,接下来在 JavaScript 中实现该事件处理函数:
Page({
// 其他代码...
addToCart: function(e) {
const goodsId = e.currentTarget.dataset.goodsId;
const goods = this.data.goodsList.find(item => item.id === goodsId);
if (goods) {
// 获取全局的购物车数据
const app = getApp();
let cart = app.globalData.cart;
const existingGoods = cart.find(cartItem => cartItem.id === goodsId);
if (existingGoods) {
existingGoods.count++;
} else {
goods.count = 1;
cart.push(goods);
}
app.globalData.cart = cart;
// 提示用户加入购物车成功
wx.showToast({
title: '已加入购物车',
icon:'success',
duration: 1500
});
}
}
});
在addToCart函数中,首先通过e.currentTarget.dataset.goodsId获取当前点击按钮所对应的商品 ID。然后使用find方法在goodsList中找到对应的商品信息。接着获取全局的购物车数据app.globalData.cart,检查购物车中是否已经存在该商品,如果存在,则将该商品的数量加 1;如果不存在,则为该商品添加一个数量属性count并设置为 1,然后将商品添加到购物车中。最后更新全局的购物车数据,并使用wx.showToast提示用户加入购物车成功。
购物车功能开发
购物车页面布局
购物车页面的布局需要清晰展示商品信息、数量选择、价格计算以及全选、结算等功能。以下是购物车页面的 WXML 代码:
<view class="cart-container">
<view class="cart-header">
<checkbox checked="{
{isAllChecked}}" bindchange="handleAllChecked">全选</checkbox>
<text>购物车</text>
</view>
<block wx:for="{
{cartList}}" wx:key="id">
<view class="cart-item">
<checkbox checked="{
{item.checked}}" bindchange="handleItemChecked" data-index="{
{index}}"></checkbox>
<image class="cart-item-img" src="{
{item.imgUrl}}" mode="aspectFill"></image>
<view class="cart-item-info">
<text class="cart-item-name">{
{item.name}}</text>
<text class="cart-item-price">¥{
{item.price}}</text>
<view class="quantity-control">
<button bindtap="decreaseQuantity" data-index="{
{index}}">-</button>
<text>{
{item.quantity}}</text>
<button bindtap="increaseQuantity" data-index="{
{index}}">+</button>
</view>
</view>
<button bindtap="deleteItem" data-index="{
{index}}">删除</button>
</view>
</block>
<view class="cart-footer">
<text>合计: ¥{
{totalPrice}}</text>
<button bindtap="goToCheckout" disabled="{
{!isSomeChecked}}">去结算</button>
</view>
</view>
在这段代码中,.cart - container是购物车页面的整体容器。.cart - header包含全选复选框和 “购物车” 标题。通过wx:for循环渲染购物车中的每个商品项,每个商品项包含商品的选中复选框、图片、名称、价格、数量控制按钮以及删除按钮。.cart - footer展示了商品的合计价格和去结算按钮,并且根据是否有商品被选中来控制去结算按钮的禁用状态。
对应的 WXSS 样式代码如下:
.cart-container {
padding: 10px;
}
.cart-header {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.cart-header checkbox {
margin-right: 5px;
}
.cart-item {
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
padding: 10px 0;
}
.cart-item checkbox {
margin-right: 10px;
}
.cart-item-img {
width: 80px;
height: 80px;
margin-right: 10px;
}
.cart-item-info {
flex: 1;
}
.cart-item-name {
font-size: 16px;
margin-bottom: 5px;
}
.cart-item-price {
color: #f60;
}
.quantity-control {
display: flex;
align-items: center;
margin-top: 5px;
}
.quantity-control button {
width: 30px;
height: 30px;
border: 1px solid #ccc;
background-color: #fff;
font-size: 16px;
text-align: center;
line-height: 30px;
margin: 0 5px;
}
.cart-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.cart-footer button {
background-color: #f60;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 5px;
}
.cart-footer button[disabled] {
background-color: #ccc;
}
上述 WXSS 代码定义了购物车页面各部分的样式。.cart - container设置了内边距,使内容与页面边缘有一定间距。.cart - header采用弹性布局,使全选复选框和标题水平排列,并且设置了底部外边距,使购物车标题与商品列表有明显区分。.cart - item同样使用弹性布局,展示商品的各个信息,通过设置底部边框和内边距,使每个商品项有清晰的分隔。商品图片设置了固定的宽度和高度,并预留了右边距,确保与其他信息有合适的间距。商品信息部分使用flex: 1占据剩余空间,使文字内容能够自适应排列。数量控制按钮设置了固定的大小、边框和背景颜色,以及合适的内外边距和文字样式,方便用户操作。.cart - footer采用弹性布局,使合计价格和去结算按钮分别位于两端,通过设置顶部边框和内边距,与商品列表部分区分开来。去结算按钮设置了背景颜色、文字颜色、边框和圆角,并且针对禁用状态设置了不同的背景颜色,增强用户交互体验。
购物车数据处理
购物车数据可以存储在本地存储中,也可以从后端获取。在小程序中,使用wx.setStorageSync和wx.getStorageSync方法来操作本地存储。假设购物车数据存储在本地存储的cart字段中,获取购物车数据的代码如下:
Page({
data: {
cartList: [],
isAllChecked: false,
totalPrice: 0
},
onShow: function() {
const cart = wx.getStorageSync('cart');
if (cart) {
this.setData({
cartList: cart
});
this.calculateTotalPrice();
}
},
calculateTotalPrice: function() {
const cartList = this.data.cartList;
let totalPrice = 0;
cartList.forEach(item => {
if (item.checked) {
totalPrice += item.price * item.quantity;
}
});
this.setData({
totalPrice: totalPrice
});
}
});
在上述代码中,onShow生命周期函数在页面显示时被调用,通过wx.getStorageSync('cart')获取本地存储的购物车数据。如果存在数据,则将其设置到页面的cartList数据中,并调用calculateTotalPrice方法计算购物车中已选中商品的总价。calculateTotalPrice方法遍历cartList,累加已选中商品的价格乘以数量,最后通过setData更新页面的totalPrice数据。
添加商品到购物车的代码在之前加入购物车功能实现部分已经提及,这里重点展示购物车中商品数量的增减以及删除商品的代码实现。
增加商品数量的代码:
Page({
// 其他代码...
increaseQuantity: function(e) {
const index = e.currentTarget.dataset.index;
let cartList = this.data.cartList;
cartList[index].quantity++;
this.setData({
cartList: cartList
});
wx.setStorageSync('cart', cartList);
this.calculateTotalPrice();
}
});
在increaseQuantity函数中,通过e.currentTarget.dataset.index获取当前操作商品的索引。然后从页面数据中获取cartList,将对应商品的数量加 1。接着使用setData更新页面数据,同时通过wx.setStorageSync('cart', cartList)将更新后的购物车数据保存到本地存储,最后调用calculateTotalPrice方法重新计算总价。
减少商品数量的代码:
Page({
// 其他代码...
decreaseQuantity: function(e) {
const index = e.currentTarget.dataset.index;
let cartList = this.data.cartList;
if (cartList[index].quantity > 1) {
cartList[index].quantity--;
this.setData({
cartList: cartList
});
wx.setStorageSync('cart', cartList);
this.calculateTotalPrice();
}
}
});
decreaseQuantity函数与increaseQuantity类似,首先获取当前操作商品的索引,然后判断商品数量是否大于 1,如果是则将数量减 1。接着更新页面数据和本地存储数据,并重新计算总价。
删除商品的代码:
Page({
// 其他代码...
deleteItem: function(e) {
const index = e.currentTarget.dataset.index;
let cartList = this.data.cartList;
cartList.splice(index, 1);
this.setData({
cartList: cartList
});
wx.setStorageSync('cart', cartList);
this.calculateTotalPrice();
}
});
在deleteItem函数中,通过e.currentTarget.dataset.index获取要删除商品的索引,使用splice方法从cartList中删除对应的商品。然后更新页面数据和本地存储数据,并重新计算总价。
全选、反选与结算功能
全选和反选功能可以通过一个复选框来控制购物车中所有商品的选中状态。结算功能则是在用户点击 “去结算” 按钮时,将已选中的商品信息提交到后端进行处理。
全选、反选功能的 JavaScript 代码如下:
Page({
// 其他代码...
handleAllChecked: function(e) {
const isChecked = e.detail.value;
let cartList = this.data.cartList;
cartList.forEach(item => {
item.checked = isChecked;
});
this.setData({
cartList: cartList,
isAllChecked: isChecked
});
wx.setStorageSync('cart', cartList);
this.calculateTotalPrice();
},
handleItemChecked: function(e) {
const index = e.currentTarget.dataset.index;
let cartList = this.data.cartList;
cartList[index].checked =!cartList[index].checked;
let allChecked = true;
cartList.forEach(item => {
if (!item.checked) {
allChecked = false;
return;
}
});
this.setData({
cartList: cartList,
isAllChecked: allChecked
});
wx.setStorageSync('cart', cartList);
this.calculateTotalPrice();
}
});
在handleAllChecked函数中,通过e.detail.value获取全选复选框的选中状态。然后遍历cartList,将每个商品的选中状态设置为与全选复选框一致。接着更新页面数据和本地存储数据,并重新计算总价。
handleItemChecked函数用于处理单个商品复选框的点击事件。通过e.currentTarget.dataset.index获取当前点击商品的索引,将该商品的选中状态取反。然后检查购物车中所有商品是否都被选中,如果是则将isAllChecked设置为true,否则设置为false。最后更新页面数据和本地存储数据,并重新计算总价。
结算功能的代码如下:
Page({
// 其他代码...
goToCheckout: function() {
const cartList = this.data.cartList;
const selectedGoods = cartList.filter(item => item.checked);
if (selectedGoods.length === 0) {
wx.showToast({
title: '请至少选择一件商品',
icon: 'none'
});
return;
}
// 这里可以将selectedGoods数据发送到后端进行结算处理
wx.navigateTo({
url: '/pages/checkout/checkout?goods=' + JSON.stringify(selectedGoods)
});
}
});
在goToCheckout函数中,首先从cartList中过滤出所有已选中的商品,存储在selectedGoods数组中。如果selectedGoods数组为空,则提示用户至少选择一件商品。否则,可以将selectedGoods数据发送到后端进行结算处理,这里示例通过wx.navigateTo跳转到结算页面,并将选中的商品数据以参数形式传递过去,在实际应用中,需要根据后端接口规范进行数据发送和处理。
订单生成与支付功能开发
订单生成逻辑
当用户在购物车页面点击 “去结算” 按钮后,就需要收集购物车中已选中商品的信息来生成订单。这部分主要在结算页面的 JavaScript 文件中实现。假设结算页面路径为/pages/checkout/checkout,其对应的 JavaScript 代码如下:
Page({
data: {
selectedGoods: []
},
onLoad: function(options) {
const goods = JSON.parse(options.goods);
this.setData({
selectedGoods: goods
});
},
createOrder: function() {
const selectedGoods = this.data.selectedGoods;
const orderData = {
orderItems: selectedGoods.map(item => ({
goodsId: item.id,
name: item.name,
price: item.price,
quantity: item.quantity
})),
totalPrice: selectedGoods.reduce((total, item) => total + item.price * item.quantity, 0),
// 这里可以添加更多订单相关信息,如收货地址、下单时间等
// 假设收货地址在全局变量中存储
shippingAddress: getApp().globalData.userInfo.address
};
wx.request({
url: 'https://api.example.com/createOrder',
method: 'POST',
data: orderData,
success: (res) => {
if (res.statusCode === 200) {
const orderId = res.data.orderId;
wx.navigateTo({
url: `/pages/orderDetail/orderDetail?orderId=${orderId}`
});
} else {
wx.showToast({
title: '订单生成失败',
icon: 'none'
});
}
},
fail: (err) => {
wx.showToast({
title: '网络请求失败',
icon: 'none'
});
}
});
}
});
在上述代码中,onLoad函数接收从购物车页面传递过来的已选中商品数据,并存储在selectedGoods数据中。createOrder函数构建订单数据,将商品信息整理成适合后端接收的格式,包括商品 ID、名称、价格、数量等,同时计算订单总价,并从全局变量中获取收货地址(假设用户信息已在之前登录等操作中存储在全局变量getApp().globalData.userInfo中)。然后通过wx.request向https://api.example.com/createOrder接口发起 POST 请求,将订单数据发送到后端。如果请求成功且状态码为 200,获取后端返回的订单 ID,跳转到订单详情页面;如果请求失败或状态码不为 200,提示用户订单生成失败。
支付功能接入
在微信小程序中,常用的支付方式是微信支付。接入微信支付的流程如下:
- 准备工作:在微信支付商户平台注册成为商户,获取商户号(MchID)、API 密钥等信息。同时,确保小程序已进行认证,并且在微信公众平台将小程序与商户号进行关联。
- 获取用户 openid:用户在小程序中进行支付时,需要获取用户的唯一标识 openid。可以通过微信登录接口获取用户的 code,然后将 code 发送到后端服务器,后端服务器使用小程序的 AppID、AppSecret 以及获取到的 code 向微信官方服务器发起请求,换取 openid。假设后端使用 Node.js 和 Express 框架搭建服务器,获取 openid 的代码示例如下:
const express = require('express');
const router = express.Router();
const axios = require('axios');
const { APP_ID, APP_SECRET } = require('../config'); // 假设配置文件中存储了AppID和AppSecret
router.post('/login', async (req, res) => {
try {
const { code } = req.query;
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${APP_ID}&secret=${APP_SECRET}&js_code=${code}&grant_type=authorization_code`;
const { data: { openid } } = await axios.get(url);
res.status(200).send({ openid });
} catch (err) {
res.status(500).send({ message: '获取openid失败' });
}
});
module.exports = router;
在小程序端,获取 code 并向后端请求 openid 的代码如下:
Page({
loginHandle: function() {
wx.login({
success: (res) => {
const code = res.code;
wx.request({
url: 'https://your-server-url.com/login',
method: 'POST',
data: { code },
success: (res) => {
const openid = res.data.openid;
// 可以将openid存储在本地或全局变量中
wx.setStorageSync('openid', openid);
},
fail: (err) => {
console.error('获取openid失败', err);
}
});
},
fail: (err) => {
console.error('获取code失败', err);
}
});
}
});
- 发起支付请求:在订单生成后,前端向后端发起支付请求,后端根据订单信息生成预支付订单,并向微信官方服务器请求预支付交易会话标识(prepay_id)。假设后端使用 Node.js 和 Express 框架,生成预支付订单的代码示例如下:
const express = require('express');
const router = express.Router();
const axios = require('axios');
const crypto = require('crypto');
const { APP_ID, MCH_ID, API_KEY } = require('../config'); // 假设配置文件中存储了相关信息
// 生成随机字符串
function generateNonceStr() {
return crypto.randomBytes(16).toString('hex');
}
// 生成签名
function generateSign(params) {
const keys = Object.keys(params).sort();
const string = keys.reduce((acc, key) => acc + `${key}=${params[key]}&`, '');
string += `key=${API_KEY}`;
return crypto.createHash('md5').update(string).digest('hex').toUpperCase();
}
router.post('/pay', async (req, res) => {
const { orderId, totalPrice } = req.body;
const nonceStr = generateNonceStr();
const body = '商品购买'; // 商品描述
const spbillCreateIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const notifyUrl = 'https://your-server-url.com/notify'; // 支付结果通知地址
const tradeType = 'JSAPI';
const openid = req.body.openid; // 从前端传递过来的openid
const params = {
appid: APP_ID,
mch_id: MCH_ID,
nonce_str: nonceStr,
body,
out_trade_no: orderId,
total_fee: totalPrice * 100, // 金额单位为分
spbill_create_ip: spbillCreateIp,
notify_url: notifyUrl,
trade_type: tradeType,
openid
};
const sign = generateSign(params);
params.sign = sign;
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<xml>
${Object.keys(params).map(key => `<${key}>${params[key]}</${key}>`).join('')}
</xml>`;
try {
const response = await axios.post('https://api.mch.weixin.qq.com/pay/unifiedorder', xml, {
headers: {
'Content-Type': 'application/xml'
}
});
const { prepay_id } = response.data;
const payParams = {
timeStamp: Math.floor(Date.now() / 1000).toString(),
nonceStr,
package: `prepay_id=${prepay_id}`,
signType: 'MD5'
};
payParams.paySign = generateSign(payParams);
res.status(200).send(payParams);
} catch (err) {
res.status(500).send({ message: '预支付订单生成失败' });
}
});
module.exports = router;
在小程序端,发起支付请求的代码如下:
Page({
payOrder: function() {
const orderId = '123456'; // 假设订单ID
const totalPrice = 100; // 假设订单总价
const openid = wx.getStorageSync('openid');
wx.request({
url: 'https://your-server-url.com/pay',
method: 'POST',
data: { orderId, totalPrice, openid },
success: (res) => {
const payParams = res.data;
wx.requestPayment({
...payParams,
success: (res) => {
wx.showToast({
title: '支付成功',
icon:'success'
});
// 支付成功后可以跳转到订单详情页面或其他操作
},
fail: (err) => {
wx.showToast({
title: '支付失败',
icon: 'none'
});
}
});
},
fail: (err) => {
wx.showToast({
title: '获取支付参数失败',
icon: 'none'
});
}
});
}
});
- 处理支付结果:微信支付完成后,微信服务器会向之前设置的支付结果通知地址(notifyUrl)发送支付结果通知。后端需要在该地址对应的接口中接收通知,验证通知的合法性,并处理订单状态更新等操作。假设后端使用 Node.js 和 Express 框架,处理支付结果通知的代码示例如下:
const express = require('express');
const router = express.Router();
const axios = require('axios');
const { APP_ID, MCH_ID, API_KEY } = require('../config');
const crypto = require('crypto');
// 验证签名
function verifySign(xmlData) {
const data = {};
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlData, 'text/xml');
const elements = xmlDoc.getElementsByTagName('xml')[0].childNodes;
for (let i = 0; i < elements.length; i++) {
data[elements[i].nodeName] = elements[i].textContent;
}
const keys = Object.keys(data).sort();
const string = keys.reduce((acc, key) => acc + `${key}=${data[key]}&`, '');
string += `key=${API_KEY}`;
const sign = crypto.createHash('md5').update(string).digest('hex').toUpperCase();
return sign === data.sign;
}
router.post('/notify', async (req, res) => {
const xmlData = req.body;
if (!verifySign(xmlData)) {
return res.status(400).send('签名验证失败');
}
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlData, 'text/xml');
const resultCode = xmlDoc.getElementsByTagName('result_code')[0].textContent;
const outTradeNo = xmlDoc.getElementsByTagName('out_trade_no')[0].textContent;
if (resultCode === 'SUCCESS') {
// 处理订单状态更新等操作,例如将订单状态更新为已支付
// 这里假设调用后端的更新订单状态接口
try {
await axios.post('https://your-server-url.com/updateOrderStatus', {
orderId: outTradeNo,
status: '已支付'
});
res.send('<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>');
} catch (err) {
res.status(500).send('<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理订单状态更新失败]]></return_msg></xml>');
}
} else {
res.send('<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[支付失败]]></return_msg></xml>');
}
});
module.exports = router;
通过以上步骤,就完成了微信小程序中订单生成与支付功能的开发,实现了用户在小程序中购买商品并完成支付的完整流程。
项目优化与完善
性能优化
分页加载:在商品展示页面,如果商品数据量较大,一次性加载所有数据会导致页面加载缓慢,影响用户体验。可以采用分页加载的方式,每次只加载当前页面所需的数据。例如,在商品列表请求接口时,添加page和pageSize参数,后端根据这两个参数返回对应页码和数量的数据。在小程序端,初始加载第一页数据,当用户滚动到页面底部时,触发加载下一页数据的操作。相关代码如下:
Page({
data: {
goodsList: [],
currentPage: 1,
pageSize: 10,
isLoading: false,
hasMoreData: true
},
onLoad: function() {
this.loadGoodsList();
},
onPageScroll: function(e) {
const { scrollTop, windowHeight, scrollHeight } = e.detail;
if (scrollTop + windowHeight >= scrollHeight - 20 &&!this.data.isLoading && this.data.hasMoreData) {
this.loadGoodsList();
}
},
loadGoodsList: function() {
if (this.data.isLoading) return;
this.setData({
isLoading: true
});
wx.request({
url: 'https://api.example.com/goods',
method: 'GET',
data: {
page: this.data.currentPage,
pageSize: this.data.pageSize
},
success: (res) => {
if (res.statusCode === 200) {
const newData = res.data;
const hasMoreData = newData.length >= this.data.pageSize;
this.setData({
goodsList: this.data.goodsList.concat(newData),
currentPage: this.data.currentPage + 1,
isLoading: false,
hasMoreData: hasMoreData
});
} else {
console.error('请求失败,状态码:', res.statusCode);
this.setData({
isLoading: false
});
}
},
fail: (err) => {
console.error('网络请求出错:', err);
this.setData({
isLoading: false
});
}
});
}
});
- 图片懒加载:商品展示页面通常会有大量商品图片,如果这些图片同时加载,会消耗大量的网络资源和内存,导致页面加载缓慢甚至卡顿。可以使用微信小程序提供的IntersectionObserver组件来实现图片懒加载。首先在 WXML 文件中为图片组件添加data-src属性存储图片真实 URL,初始src属性为空。然后使用IntersectionObserver监听图片是否进入可视区域,当进入可视区域时,将data-src的值赋给src属性,实现图片加载。代码如下:
<view class="goods-list">
<block wx:for="{
{goodsList}}" wx:key="id">
<view class="goods-item">
<image class="goods-img" src="{
{item.imgUrl? item.imgUrl : ''}}" data-src="{
{item.imgUrl}}" mode="aspectFill"></image>
<view class="goods-info">
<text class="goods-name">{
{item.name}}</text>
<text class="goods-price">¥{
{item.price}}</text>
<button class="add-cart-btn" bindtap="addToCart" data-goodsId="{
{item.id}}">加入购物车</button>
</view>
</view>
</block>
</view>
Page({
data: {
goodsList: []
},
onLoad: function() {
this.fetchGoodsList();
this.initImageObserver();
},
fetchGoodsList: function() {
// 数据请求代码...
},
initImageObserver: function() {
const query = wx.createIntersectionObserver(this);
query.selectAll('.goods-img').observe((res) => {
res.forEach((item) => {
if (item.intersectionRatio > 0) {
const index = item.dataset.index;
this.setData({
[`goodsList[${index}].imgUrl`]: item.target.dataset.src
});
}
});
});
}
});
安全措施
- 采用 HTTPS:小程序的所有网络请求都必须使用 HTTPS 协议,这是微信官方的强制要求。HTTPS 协议在 HTTP 的基础上加入了 SSL/TLS 加密层,能够对数据传输进行加密,确保数据在传输过程中不被窃取、篡改。在配置服务器时,需要申请 SSL 证书,并将其部署到服务器上,使服务器支持 HTTPS 访问。例如,使用阿里云的 SSL 证书服务,在申请并获取证书后,按照阿里云的文档指导将证书配置到服务器的 Web 服务器(如 Nginx 或 Apache)中。以 Nginx 为例,配置文件中添加如下代码:、
server {
listen 443 ssl;
server_name your_domain.com;
ssl_certificate /path/to/your_cert.pem;
ssl_certificate_key /path/to/your_key.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
# 其他配置...
}
}
- 防止 XSS 攻击:XSS(跨站脚本攻击)是一种常见的前端安全漏洞,攻击者通过在网页中注入恶意脚本,窃取用户信息或控制用户操作。在小程序开发中,要对用户输入的数据进行严格的过滤和转义,避免恶意脚本被执行。例如,在用户提交评论等数据时,使用工具函数对输入内容进行过滤。可以使用html-escaper库来进行转义,安装后在代码中使用:
import escaper from 'html-escaper';
// 假设inputValue是用户输入的值
const inputValue = '<script>alert("XSS攻击")</script>';
const escapedValue = escaper.escape(inputValue);
// 此时escapedValue的值为 <script>alert("XSS攻击")</script>
// 将escapedValue存储到数据库或展示到页面上,就可以防止XSS攻击
同时,在使用setData更新页面数据时,要确保数据的安全性,避免直接将未经处理的用户输入数据用于渲染页面。