简介:这是一套面向前端开发者与微信小程序初学者的海鲜配送类实战项目源码,聚焦于移动端便捷服务场景,帮助学习者掌握小程序核心开发流程与电商功能实现。资源共145个文件,包含24个JavaScript业务逻辑脚本(如cart.js、orderDetail.js、addressAdd.js等)、14个WXML页面结构文件、15个WXSS样式表、16个JSON配置文件及74个PNG资源图,辅以WXS工具脚本与Markdown说明文档,整体压缩包仅734KB,轻量易部署。已有311人学习下载,适合用于课程设计、毕业项目或快速搭建本地生鲜服务原型。读者可直接运行调试完整购物流程——从商品浏览、购物车管理、地址添加到订单提交与支付对接,代码结构清晰,模块职责分明,尤其在表单验证(WxValidate.js)、富文本解析(htmlparser.js、showdown.js)及区域选择(area.js)等细节处具备较强参考价值。
1. 为什么用 JavaScript 写海鲜配送小程序,不是“套模板”而是真要解决冷鲜链路的实时性问题
你打开一个卖活虾、冰鲜带鱼的小程序,下单后页面卡顿两秒才显示“已接单”,配送员位置30秒刷新一次,图片加载失败时只显示灰色方块——这不是UI丑,是JavaScript层面对冷链时效、库存波动、多端状态同步的响应能力没立住。基于JavaScript的海鲜配送微信小程序,核心不在“能跑起来”,而在用原生小程序框架(非uni-app或Taro)直接调度wx.request、wx.uploadFile、wx.getLocation等API,把“活物在途”这个高敏感场景拆解成可预测、可干预、可回溯的原子操作:比如用Date.now()打时间戳校验订单创建与冷链设备上报温湿度的毫秒级偏差;用Web Worker预处理大量渔港经纬度坐标,避免UI线程阻塞导致地图拖拽卡顿;用WeakMap缓存用户最近三次搜索的海鲜品类关键词,让“皮皮虾”“椒盐皮皮虾”“冷冻皮皮虾”自动联想而非全量匹配。这类项目面向的是中小型水产批发商、社区团购团长、自营冷链车队,他们没预算养全栈团队,但对“下单即锁库存”“配送中温度异常自动告警”“分拣员扫码即更新状态”有硬性要求。本文不讲WXML语法糖,只聚焦JavaScript如何成为连接微信生态与真实渔业供应链的执行引擎。
2. 用原生小程序框架在本地跑通海鲜配送最小闭环:从商品列表到订单提交的5个关键JS文件
微信小程序的JavaScript执行环境是独立于浏览器的,它不支持document.querySelector,但提供wx.createSelectorQuery替代方案;不能用fetch,必须用wx.request封装HTTP请求;localStorage被wx.setStorage取代。这意味着海鲜配送小程序的JS逻辑必须围绕微信运行时重写,而非简单移植H5代码。下面以最简可行路径展开:一个包含商品浏览、购物车、地址管理、订单提交四步的闭环,所有交互由JavaScript驱动,不依赖任何第三方UI库。
2.1 商品列表页(pages/index/index.js):用Promise.all并发拉取渔港数据与实时库存
海鲜配送的核心矛盾是“货在哪儿”和“货还有多少”必须强一致。我们不采用轮询,而是在页面onLoad时并发请求两个接口:/api/ports获取合作渔港列表(含经纬度),/api/inventory?port_id=123获取该渔港当前可售品类。关键点在于用Promise.all保证两者同时返回,避免因库存接口慢导致渔港列表渲染后长时间空白:
// pages/index/index.js Page({ data: { ports: [], inventory: {}, loading: true }, onLoad() { this.fetchPortAndInventory(); }, fetchPortAndInventory() { const portPromise = wx.cloud.callFunction({ name: 'getPorts' }); const inventoryPromise = wx.cloud.callFunction({ name: 'getInventory', data: { portId: '123' } }); Promise.all([portPromise, inventoryPromise]) .then(([portRes, invRes]) => { this.setData({ ports: portRes.result.data || [], inventory: invRes.result.data || {}, loading: false }); }) .catch(err => { wx.showToast({ title: '数据加载失败', icon: 'none' }); console.error('并发请求失败:', err); }); } });提示:此处用云函数而非wx.request直连后端,是因为渔港数据需校验管理员权限,云函数可内置wx.context.OPENID做身份透传,避免前端暴露鉴权逻辑。若用自建服务器,需在wx.request的header中手动注入token。
2.2 购物车页(pages/cart/cart.js):用Map对象管理动态SKU,解决活鲜规格组合爆炸问题
海鲜商品存在多维规格:基围虾分“40-50头/斤”“50-60头/斤”,三文鱼分“挪威原切”“智利急冻”,同一商品不同规格对应不同库存、不同价格、不同冷链要求。用传统数组存储购物车极易因规格ID拼接错误导致重复添加。我们改用ES6 Map,以规格组合字符串为key,确保唯一性:
// pages/cart/cart.js Page({ data: { cartItems: new Map() // key: 'shrimp_40-50_20240501', value: { count: 2, price: 89.9 } }, addToCart(e) { const { skuId, spec, price } = e.currentTarget.dataset; const key = `${skuId}_${spec}_${new Date().toISOString().split('T')[0]}`; // 加日期防跨日库存混淆 const current = this.data.cartItems.get(key) || { count: 0, price }; current.count += 1; this.data.cartItems.set(key, current); this.updateCartDisplay(); // 触发视图更新 }, updateCartDisplay() { // 将Map转为Array供WXML遍历,同时计算总价 const itemsArray = Array.from(this.data.cartItems.entries()).map(([k, v]) => ({ key: k, ...v })); const total = itemsArray.reduce((sum, item) => sum + item.count * item.price, 0); this.setData({ cartItemsArray: itemsArray, cartTotal: total.toFixed(2) }); } });注意:Map对象无法直接通过this.setData更新,必须先转为普通数组。此处
key拼接日期是为了应对海鲜按日配货的业务规则——今日下单的“40-50头基围虾”与明日下单的同规格商品,库存池完全隔离。
2.3 地址管理页(pages/address/address.js):用wx.chooseLocation精准获取冷链配送起点坐标
海鲜配送对地址精度要求极高:渔港码头、冷库出入口、社区自提点需精确到10米内。wx.chooseLocation返回的经纬度比手动输入更可靠,且自动关联腾讯地图POI。关键是在选择后立即调用逆地理编码,将坐标转为结构化地址,避免用户填写“XX码头3号冷库”这类模糊描述:
// pages/address/address.js Page({ data: { address: '', latitude: 0, longitude: 0 }, selectLocation() { wx.chooseLocation({ success: (res) => { this.setData({ latitude: res.latitude, longitude: res.longitude, address: res.address // 先用原始地址占位 }); // 立即逆编码校验精度 this.reverseGeocode(res.latitude, res.longitude); } }); }, reverseGeocode(lat, lng) { wx.request({ url: 'https://apis.map.qq.com/ws/geocoder/v1/', data: { location: `${lat},${lng}`, key: 'YOUR_TENCENT_MAP_KEY', get_poi: 1 }, success: (res) => { if (res.data.status === 0 && res.data.result.pois?.length > 0) { // 取最匹配的POI名称,如“舟山国际水产城-冷链分拣中心” this.setData({ address: res.data.result.pois[0].title }); } } }); } });提示:腾讯地图逆编码返回的POI可能包含多个结果,此处取第一个作为默认地址,但实际项目中应提供下拉列表让用户二次确认,避免将“沈家门渔港”误选为“沈家门客运站”。
2.4 订单提交页(pages/order/submit.js):用wx.getNetworkType动态降级支付策略
活鲜订单对网络稳定性极度敏感:用户在渔港信号弱区点击“立即支付”,若直接调起wx.requestPayment可能因超时失败。我们先检测网络类型,对2G/3G/无网络场景强制走“货到付款”,并弹窗提示“当前网络不稳定,已为您切换为到付模式”:
// pages/order/submit.js Page({ data: { paymentMethod: 'wechat' // 默认微信支付 }, onSubmit() { wx.getNetworkType({ success: (res) => { const network = res.networkType; if (['2g', '3g', 'none'].includes(network)) { this.setData({ paymentMethod: 'cod' }); wx.showModal({ title: '网络提示', content: '检测到当前网络信号较弱,已自动切换为货到付款,签收时支付即可', showCancel: false }); } this.placeOrder(); } }); }, placeOrder() { const orderData = { items: this.getCartItems(), address: this.getAddress(), payment_method: this.data.paymentMethod }; wx.cloud.callFunction({ name: 'createOrder', data: orderData, success: (res) => { if (this.data.paymentMethod === 'wechat') { this.handleWechatPayment(res.result.orderId); } else { wx.redirectTo({ url: '/pages/order/success?orderId=' + res.result.orderId }); } } }); } });注意:此处
wx.getNetworkType返回的是粗粒度网络类型,不能替代信号强度检测。生产环境建议结合wx.onNetworkStatusChange监听实时变化,在用户支付过程中网络中断时主动降级。
2.5 全局工具类(utils/orderUtils.js):用正则+Date精确解析海鲜保质期字段
海鲜商品详情页常含“保质期:72小时”“最佳赏味期:2024-05-01 18:00前”等非结构化文本。前端需将其转为可计算的时间戳,用于倒计时组件和库存预警。我们编写专用解析函数,覆盖三种常见格式:
// utils/orderUtils.js function parseShelfLife(text) { if (!text) return null; // 匹配 "72小时" -> 转为毫秒 const hourMatch = text.match(/(\d+)小时/); if (hourMatch) { return Date.now() + parseInt(hourMatch[1]) * 60 * 60 * 1000; } // 匹配 "2024-05-01 18:00" -> 转为时间戳 const datetimeMatch = text.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/); if (datetimeMatch) { return new Date(datetimeMatch[1]).getTime(); } // 匹配 "今日18:00前" -> 计算今日指定时间戳 const todayMatch = text.match(/今日(\d{2}:\d{2})前/); if (todayMatch) { const [h, m] = todayMatch[1].split(':'); const today = new Date(); today.setHours(parseInt(h), parseInt(m), 0, 0); return today.getTime(); } return null; } // 导出供页面调用 module.exports = { parseShelfLife };提示:此函数需配合WXML中的
<view wx:if="{{shelfTime}}">剩余{{timeLeft}}小时</view>使用,其中timeLeft由页面定时器每秒计算Math.floor((shelfTime - Date.now()) / 3600000)得出。注意避免在onShow中重复启动定时器导致内存泄漏。
3. 海鲜配送小程序的3个必调JavaScript参数:wx.setStorageSync容量、wx.uploadFile超时、WebSocket心跳间隔
原生小程序JavaScript运行受微信客户端严格管控,参数配置不当会导致冷鲜订单状态丢失、图片上传失败、配送轨迹断连。以下三个参数必须根据海鲜业务特性重设,而非使用默认值。
3.1 wx.setStorageSync最大存储量:从10MB调整为2MB,规避缓存污染风险
微信规定单次wx.setStorageSync最多写入10MB数据,但海鲜配送小程序需缓存渔港定位、用户历史订单、冷链设备日志等高频读取数据。若不限制单条缓存大小,易因某张高清渔港全景图(8MB)挤占全部空间,导致后续库存变更无法写入。正确做法是主动截断:
// utils/storage.js function safeSetStorageSync(key, data) { try { const strData = JSON.stringify(data); // 强制限制单条缓存不超过2MB(2*1024*1024字节) if (strData.length > 2 * 1024 * 1024) { console.warn(`缓存${key}过大,已截断至2MB`); // 截取前2MB字符,保留JSON结构完整性(需确保不截断在字符串中间) const truncated = strData.substring(0, 2 * 1024 * 1024); wx.setStorageSync(key, JSON.parse(truncated)); } else { wx.setStorageSync(key, data); } } catch (e) { console.error('缓存写入失败:', e); } }注意:JSON.parse截断后的字符串可能非法,生产环境应使用JSON流式解析库或改用分片存储。此处简化处理,重点在于建立“主动限容”意识。
3.2 wx.uploadFile超时时间:从60秒延长至180秒,适配冷链图片大文件上传
活鲜商品需上传带时间水印的捕捞现场图、检疫证明扫描件,单文件常达5-10MB。默认60秒超时必然失败。必须在调用时显式设置timeout:
// pages/goods/upload.js uploadProof() { wx.chooseMedia({ sourceType: ['album', 'camera'], mediaType: ['image'], maxDuration: 30, camera: 'back', success: (res) => { const tempFile = res.tempFiles[0]; wx.uploadFile({ url: 'https://your-api.com/upload', filePath: tempFile.tempFilePath, name: 'file', timeout: 180000, // 关键:180秒超时 formData: { type: 'inspection_cert', timestamp: Date.now() }, success: (uploadRes) => { const data = JSON.parse(uploadRes.data); this.setData({ proofUrl: data.url }); }, fail: (err) => { wx.showToast({ title: '上传失败,请检查网络', icon: 'none' }); } }); } }); }提示:timeout单位为毫秒,180000=180秒。若后端Nginx配置了client_max_body_size 10M,需同步调整,否则超时前会收到413错误。
3.3 WebSocket心跳间隔:从30秒缩短至10秒,保障配送员位置实时同步
海鲜配送依赖WebSocket维持配送员与调度中心长连接。默认30秒心跳在移动网络切换基站时易断连,导致位置轨迹丢失。我们将心跳间隔压缩至10秒,并增加重连机制:
// utils/websocket.js class SeafoodSocket { constructor() { this.socketTask = null; this.heartbeatTimer = null; this.reconnectCount = 0; } connect() { this.socketTask = wx.connectSocket({ url: 'wss://your-ws.com', success: () => { this.startHeartbeat(); this.reconnectCount = 0; // 连接成功重置重连计数 } }); this.socketTask.onOpen(() => { console.log('WebSocket连接成功'); }); this.socketTask.onError((err) => { console.error('WebSocket错误:', err); this.reconnect(); }); this.socketTask.onClose(() => { console.log('WebSocket关闭'); this.reconnect(); }); } startHeartbeat() { clearInterval(this.heartbeatTimer); this.heartbeatTimer = setInterval(() => { if (this.socketTask && this.socketTask.readyState === 'OPEN') { this.socketTask.send({ data: JSON.stringify({ type: 'heartbeat', ts: Date.now() }) }); } }, 10000); // 关键:10秒发送一次心跳 } reconnect() { this.reconnectCount++; if (this.reconnectCount <= 5) { setTimeout(() => { this.connect(); }, Math.min(1000 * this.reconnectCount, 10000)); // 指数退避 } } } module.exports = new SeafoodSocket();注意:微信小程序WebSocket不支持ping/pong帧,必须用业务消息模拟心跳。此处
type: 'heartbeat'消息由后端忽略处理,仅用于维持连接活跃。
4. 验证海鲜配送小程序JavaScript健壮性的3种真机测试法:冷链设备模拟、弱网注入、多进程抢占
写完代码只是开始,海鲜配送场景的特殊性决定了必须用真实硬件环境验证JavaScript行为。以下方法绕过开发者工具模拟,直击生产环境痛点。
4.1 用树莓派+温湿度传感器模拟冷链设备上报,验证时间戳校验逻辑
在真实渔港冷库中,设备每5分钟上报一次温湿度。我们用树莓派运行Python脚本,通过USB串口向手机发送模拟数据,测试小程序是否能正确解析并触发告警:
# raspberry_pi_sensor.py import serial import time import json ser = serial.Serial('/dev/ttyUSB0', 9600) while True: # 模拟冷链设备上报:{"temp":2.3,"humidity":85,"ts":1714567890123} data = { "temp": round(2.0 + (time.time() % 10) * 0.1, 1), # 温度在2.0~2.9间波动 "humidity": 85, "ts": int(time.time() * 1000) } ser.write((json.dumps(data) + '\n').encode()) time.sleep(300) # 5分钟间隔在小程序中,我们用wx.onBluetoothDeviceFound监听蓝牙设备(实际部署用NB-IoT模块,此处蓝牙简化),当收到数据时执行校验:
// pages/device/monitor.js Page({ data: { temp: 0, alertActive: false }, onReady() { // 假设已配对树莓派蓝牙设备 wx.onBluetoothDeviceFound((res) => { if (res.devices.some(d => d.name === 'ColdChain-Sensor')) { this.readSensorData(); } }); }, readSensorData() { // 伪代码:从蓝牙特征值读取JSON字符串 const rawData = '{"temp":2.3,"humidity":85,"ts":1714567890123}'; const sensorData = JSON.parse(rawData); // 关键校验:时间戳与本地时间偏差超过30秒则丢弃(防设备时钟漂移) const timeDiff = Math.abs(Date.now() - sensorData.ts); if (timeDiff > 30000) { console.warn('设备时间偏差过大,丢弃数据'); return; } this.setData({ temp: sensorData.temp }); // 温度超2.5℃触发告警 if (sensorData.temp > 2.5 && !this.data.alertActive) { this.triggerAlert(); } } });提示:此测试验证了JavaScript层面对设备时钟漂移的容错能力。真实项目中,设备应定期与NTP服务器同步,但前端必须有兜底逻辑。
4.2 用Charles Proxy注入弱网延迟,测试订单提交的降级流程
在Charles中设置“Throttle Settings”,将网络条件设为“Edge 2G”(上行115kbps,下行230kbps,延迟1000ms),然后在小程序中反复提交订单,观察是否触发2G网络下的货到付款降级:
| 网络类型 | 检测方式 | 降级动作 | 验证点 |
|---|---|---|---|
| 2G/3G | wx.getNetworkType返回2g或3g | 自动切换payment_method为cod | WXML中wx:if="{{paymentMethod==='cod'}}"区块显示“货到付款”文案 |
| 无网络 | wx.getNetworkType返回none | 禁用提交按钮,显示“请连接网络” | 按钮disabled属性为true,toast提示出现 |
关键代码验证点:
// pages/order/submit.js 中的onSubmit方法 onSubmit() { wx.getNetworkType({ success: (res) => { console.log('当前网络:', res.networkType); // 在Charles中查看此日志 if (['2g', '3g', 'none'].includes(res.networkType)) { this.setData({ paymentMethod: 'cod' }); // 此处必须有视觉反馈,如按钮文字变为“货到付款” this.setData({ submitButtonText: '货到付款' }); } this.placeOrder(); } }); }注意:Charles需安装根证书并配置手机代理,iOS需在“设置-通用-关于本机-证书信任设置”中启用。Android需在WiFi设置中手动配置代理。
4.3 多微信小程序进程抢占测试:验证wx.getStorageSync的并发安全
海鲜配送员可能同时打开“配送任务”“冷链监控”“渔港报单”三个小程序,它们共享同一storage空间。我们用两个小程序实例并发写入同一key,验证数据不被覆盖:
小程序A(配送任务):
// pages/delivery/index.js setDeliveryStatus() { const status = { orderId: 'ORD123', status: 'on_way', ts: Date.now() }; wx.setStorageSync('delivery_status', status); console.log('A写入:', status.ts); }小程序B(冷链监控):
// pages/coldchain/index.js updateTemp() { const tempData = { temp: 2.1, ts: Date.now() }; wx.setStorageSync('delivery_status', tempData); // 错误:覆盖了A的状态! console.log('B写入:', tempData.ts); }正确解法:用wx.getStorageSync读-改-写,加时间戳锁:
// utils/concurrentStorage.js function safeUpdateStorage(key, updater) { const now = Date.now(); const lockKey = `${key}_lock`; // 尝试加锁(写入带时间戳的锁) wx.setStorageSync(lockKey, { locked: true, ts: now }); // 读取原值 const oldValue = wx.getStorageSync(key); const newValue = updater(oldValue); // 写入新值 wx.setStorageSync(key, newValue); // 解锁 wx.removeStorageSync(lockKey); } // 使用示例 safeUpdateStorage('delivery_status', (old) => ({ ...old, last_update: Date.now(), temp: 2.1 }));提示:微信storage本身无事务,此方案用锁key模拟乐观锁。生产环境建议用云开发数据库的
db.collection().doc().update(),其原子性由服务端保证。
5. 一个具体技巧:用Canvas动态生成带时间水印的海鲜订单截图,规避截图被篡改风险
海鲜配送中,用户常需截图订单给家人确认,但普通WXML截图可能被PS修改金额或时间。我们用Canvas在前端动态绘制不可篡改的水印:将订单创建时间、用户OpenID哈希、订单ID三者拼接后SHA256,取前8位作为水印文本,斜向铺满整个订单页:
// pages/order/detail.js drawWatermark() { const query = wx.createSelectorQuery(); query.select('#orderCanvas').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); // 生成水印文本:订单ID + OpenID + 时间戳 的SHA256前8位 const watermarkText = this.generateWatermarkHash(); ctx.font = '12px sans-serif'; ctx.fillStyle = 'rgba(0,0,0,0.05)'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; // 斜向铺满画布 for (let x = -50; x < res[0].width + 100; x += 150) { for (let y = -50; y < res[0].height + 100; y += 150) { ctx.save(); ctx.translate(x + 50, y + 50); ctx.rotate(-Math.PI / 6); ctx.fillText(watermarkText, 0, 0); ctx.restore(); } } }); }, generateWatermarkHash() { // 使用小程序内置crypto API(需基础库2.25.0+) const crypto = wx.getRealtimeLogManager ? wx.getRealtimeLogManager().crypto : null; if (crypto) { const data = `${this.data.order.id}${wx.getStorageSync('openId')}${this.data.order.create_time}`; return crypto.createHash('sha256').update(data).digest('hex').substring(0, 8); } return 'NOHASH'; // 降级处理 }注意:此Canvas必须放在订单页WXML的顶层,z-index最高,且背景设为透明。截图时调用
wx.canvasToTempFilePath导出,用户看到的每张截图都含唯一哈希,无法伪造。若基础库版本低,可用纯JS实现SHA256(如crypto-js精简版),但需注意性能损耗。
本文还有配套的精品资源,点击获取