微信小程序LOL战绩查询实战:云函数转发、缓存与Canvas截图全解析
简介这是一份微信小程序源码项目主题为LOL英雄联盟战绩查询适合正在学习小程序开发的初学者或希望快速搭建查询类应用的开发者参考。项目包含完整的前端页面与逻辑代码演示了如何通过微信小程序实现游戏战绩的检索与展示。压缩包内共有158个文件以94个png截图和4个gif演示动画为主另有17个js逻辑文件、15个wxml页面结构、14个wxss样式及11个json配置包体仅4.1MB结构紧凑。当前已有290人浏览学习。源码中可以看到启动页、查询页、详情页等模块划分页面截图和动态效果图便于对照理解界面布局与交互流程js与json文件则展示了数据配置和接口调用的常见写法适合作为课设或练手项目直接修改复用。1. 用微信小程序做LOL战绩查询先想清楚数据从哪来很多玩家会搜微信小程序源码含截图LOL战绩查询以为难点在小程序前端真正动起手来才会发现卡住你的第一件事永远是数据源。Riot/腾讯的官方接口有Key、有频率限制第三方数据平台要付费或加签名自己抓网页又有风控问题。这篇不聊某个现成源码包的下载而是讲清楚我怎么把一个可运行的战绩查询小程序从零搭出来数据层怎么转发、源码目录怎么组织、战绩卡片如何用Canvas画出来并保存成截图。适合刚接这类需求的前端也适合想弄明白完整链路的后端同学。2. LOL战绩查询小程序的数据层设计从API接入到本地缓存2.1 先定数据源官方API、第三方服务还是自建爬虫做战绩查询第一个要定的不是界面是数据从哪来。官方API需要申请Key有严格的Rate Limit而且国内访问连续性不太好第三方付费API接入简单但对小程序端来说有白名单和跨域限制自己写爬虫去解析官网页面稳定性和合规性都不可控不推荐在生产环境用。我的建议是个人项目或毕业设计用第三方免费接口配合Mock数据正式项目买数据服务商的套餐他们会给你一个类似https://api.demo-lol-data.com/v1/player?name{name}region{region}的HTTP接口我们只需要拼参数拿JSON。这里有个常见误区。有人想着小程序直接wx.request请求对方接口结果发现微信要求所有请求域名必须HTTPS且加入后台白名单数据服务商不会轻易给你开白名单。所以最常见的做法是小程序只请求自己的后端或云函数由云函数转发到外部API。这样既绕过跨域也能把你的API密钥藏在服务端。2.1.1 用云函数转发请求把密钥藏起来如果你的项目刚好用了微信云开发最省事的方式是用云函数转发。云函数运行在服务端天然没有跨域问题第三方接口的Key放在process.env里小程序端拿不到。下面是一个用Node.js写的云函数示例接收召唤师名称先查基本信息再查最近20场对局// 云函数: summoner const cloud require(wx-server-sdk) const axios require(axios) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const BASE_URL process.env.LOL_DATA_BASE_URL const API_KEY process.env.LOL_DATA_API_KEY exports.main async (event, context) { const { name, region cn } event if (!name) return { code: 400, msg: name is required } const headers { Content-Type: application/json, Authorization: Bearer ${API_KEY} } // 1. 查召唤师基本信息 const res await axios.get(${BASE_URL}/summoner, { params: { name, region }, headers }) const summoner res.data.data // 2. 查最近战绩 const matches await axios.get(${BASE_URL}/matches, { params: { puuid: summoner.puuid, count: 20 }, headers }) return { code: 0, data: { summoner: { id: summoner.id, name: summoner.name, level: summoner.level, iconUrl: summoner.iconUrl }, matches: matches.data.data } } }这段代码里BASE_URL和API_KEY都从云函数的环境变量读取不写死在源码里。先拿到puuid再用它去拉对局列表因为大多数第三方接口把个人基础信息和对局列表拆成了两个端点。需要留意axios要先在cloudfunctions/summoner目录下执行npm install axios否则上传云函数后运行会报找不到模块。如果你不想引入第三方依赖也可以用Node内置的https模块但处理响应的代码会啰嗦一些。调用云函数时如果玩家昵称不存在第三方接口可能返回类似unknown player的英文提示。不要直接把这个字符串弹给用户我一般会统一转换成code: 5001小程序端根据这个code提示没找到这个召唤师检查一下有没有输错区服。2.2 本地缓存把重复查询的响应存到Storage数据接口有频率限制尤其战绩接口一天可能只有几百次配额小程序端必须做缓存。我的习惯是缓存键用lol:summoner:{region}:{name}缓存值是一个对象包含数据和拉取时间。取数据时如果缓存存在且未过期直接返回否则调云函数。function getSummonerWithCache(name, region) { const key lol:summoner:${region}:${name} const cached wx.getStorageSync(key) if (cached Date.now() - cached.updatedAt 5 * 60 * 1000) { return Promise.resolve(cached.data) } return wx.cloud.callFunction({ name: summoner, data: { name, region } }).then(res { wx.setStorageSync(key, { data: res.result.data, updatedAt: Date.now() }) return res.result.data }) }缓存时间设5分钟是因为战绩数据变化不会特别快5分钟延迟对用户基本无感但对接口配额帮助很大。如果做的是实时段位或比赛直播里的比分查询缓存时间要缩短到30秒左右。另外wx.setStorageSync是同步写入数据量大时可能阻塞渲染所以只存摘要信息不要把完整的对局时间线塞进去。数据源优点缺点适合场景官方API数据权威、字段全申请周期长、调用限制、国内访问不稳有企业资质的商业项目第三方付费API接入简单、稳定按量计费、需要后端转发大部分生产环境自建爬虫免费、灵活风控风险、维护成本高学习研究勿用于生产3. 搭建LOL战绩查询小程序的源码结构路由、页面与WXML布局3.1 小程序目录和app.json配置一个最小可跑的项目源码目录大致长这样├── cloudfunctions/ │ └── summoner/ │ ├── index.js │ └── package.json ├── miniprogram/ │ ├── pages/ │ │ ├── index/ │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ └── detail/ │ │ ├── detail.js │ │ ├── detail.json │ │ ├── detail.wxml │ │ └── detail.wxss │ ├── app.js │ ├── app.json │ ├── app.wxss │ └── project.config.jsonapp.json里要注册页面、配窗口样式和tabBar。搜索页作为首页战绩详情页作为二级页面不需要tab。如果你打算做战绩和我的两个tab也可以加。下面是一个基础配置注意window里的navigationBarTitleText会直接显示在小程序顶部也就是用户看到的导航栏标题。{ pages: [ pages/index/index, pages/detail/detail ], window: { navigationBarBackgroundColor: #1c1c2e, navigationBarTextStyle: white, navigationBarTitleText: LOL战绩查询, backgroundColor: #ffffff }, style: v2, sitemapLocation: sitemap.json }很多新手会纠结顶部导航栏高度。不同机型上刘海屏状态栏高度不一致如果要把navigationStyle设为custom做自定义导航栏就得动态获取wx.getWindowInfo()的statusBarHeight和菜单按钮位置。常见做法是在app.js的onLaunch里把值存到全局变量再在页面样式中用padding-top撑开。如果不想折腾保留默认导航栏是最省力的做法。页面文件职责关键接口pages/index输入召唤师名发起查询跳转详情wx.cloud.callFunction,wx.navigateTopages/detail展示战绩列表生成截图wx.createSelectorQuery,wx.canvasToTempFilePathcloudfunctions/summoner转发第三方API返回标准化数据axios.get, 环境变量读取3.2 搜索页输入召唤师名防抖触发查询搜索页的逻辑很简单输入昵称点击搜索按钮调用云函数拿到基本信息后跳转到详情页。但这里最值得注意的不是跳转而是输入框事件。很多人会在bindinput里直接发起请求用户每敲一个字母就触发一次既浪费请求又会让界面卡顿。正确做法是加一个防抖函数比如300ms内没有继续输入才真正请求。// pages/index/index.js Page({ data: { name: , loading: false }, onInput(e) { const value e.detail.value this.setData({ name: value }) if (this._timer) clearTimeout(this._timer) this._timer setTimeout(() { this.search(value) }, 300) }, search(name) { if (!name) return this.setData({ loading: true }) wx.cloud.callFunction({ name: summoner, data: { name, region: cn } }).then(res { this.setData({ loading: false }) if (res.result.code 0) { wx.navigateTo({ url: /pages/detail/detail?summonerId${res.result.data.summoner.id} }) } else { wx.showToast({ title: res.result.msg, icon: none }) } }).catch(() { this.setData({ loading: false }) wx.showToast({ title: 查询失败, icon: none }) }) } })这里的_timer要挂在this上而不是放在data里。因为data里任何变化都会触发视图更新而this上的普通属性不会。跳转时通过URL参数传summonerId不要只传名字。同一个昵称在不同区服可能重名只传名字的话详情页拿到的可能是别人的战绩。3.3 战绩列表渲染wx:for 和 block 的配合详情页拿到云函数返回的matches后先setData存进去再用wx:for渲染。推荐用wx:for加wx:key避免列表重排时的性能问题。!-- pages/detail/detail.wxml -- view classmatch-list block wx:for{{matches}} wx:keymatchId view classmatch-card view classmatch-info text classchampion{{item.championName}}/text text classresult {{item.win ? win : lose}}{{item.win ? 胜利 : 失败}}/text /view view classkda {{item.kills}} / {{item.deaths}} / {{item.assists}} /view /view /block /viewwx:key一定要用对局的唯一ID比如matchId不要用索引index。因为战绩列表可能有多场相同英雄、相同KDA的对局用索引时微信在增删列表节点后无法精确复用容易出现渲染错位。block本身不会渲染成真实节点只是用来包住多个同级的view。提示有些接口返回的KDA字段不是item.kills而是item.stats.kills。建议在detail.js的onLoad里先console.log(res.result.data)在开发者工具Console看输出比对着文档猜字段层级靠谱得多。4. 把对战信息画成图片Canvas绘制与截图保存4.1 为什么用Canvas生成截图而不是直接截图页面标题里的含截图通常指源码包里附带运行截图方便演示效果。但为了提升分享体验很多开发者会把对局信息用Canvas画成一张分享图用户长按保存或转发给朋友。直接用wx.pageScrollTo截图只能截当前屏幕可视区域而且受CSS和机型影响大Canvas方案可以自定义背景、文字、图标画出来更稳定。4.1.1 Canvas 2D初始化与坐标计算小程序里推荐用canvas组件并指定type2d。通过wx.createSelectorQuery()拿到canvas节点再调用节点的getContext(2d)。需要注意canvas 的宽度和高度不能直接用CSS的px因为canvas内部有物理像素需要根据设备像素比缩放否则在Retina屏上画出来会模糊。// pages/detail/detail.js let canvasNode null Page({ onReady() { const query wx.createSelectorQuery() query.select(#matchImage) .fields({ node: true, size: true }) .exec(res { const canvas res[0].node const ctx canvas.getContext(2d) const dpr wx.getWindowInfo().pixelRatio canvas.width res[0].width * dpr canvas.height res[0].height * dpr ctx.scale(dpr, dpr) canvasNode canvas this.drawMatchCard(ctx, res[0].width, res[0].height) }) }, drawMatchCard(ctx, width, height) { ctx.fillStyle #1c1c2e ctx.fillRect(0, 0, width, height) ctx.font bold 16px sans-serif ctx.fillStyle #ffffff ctx.fillText(LOL战绩查询, 16, 32) ctx.font 14px sans-serif ctx.fillStyle #c8aa6e ctx.fillText(KDA: 12 / 3 / 9, 16, 64) } })fillText的第二个和第三个参数是文本左下角的x、y坐标不是文字中心点。第一次画容易整体偏到左下角多调几次就能找到手感。canvas.width res[0].width * dpr这里res[0].width是CSS尺寸dpr是物理像素比两行之后必须调用ctx.scale(dpr, dpr)否则后续坐标会错位。4.2 把Canvas导出为图片canvasToTempFilePath与保存相册画完图后用wx.canvasToTempFilePath导出临时图片文件再调用wx.saveImageToPhotosAlbum保存到相册。导出接口需要传入canvas节点并且必须在绘制完成后调用否则会导出空白图。saveToAlbum() { if (!canvasNode) return wx.canvasToTempFilePath({ canvas: canvasNode, success: (res) { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () wx.showToast({ title: 已保存到相册, icon: success }), fail: () wx.showToast({ title: 保存失败, icon: none }) }) }, fail: () wx.showToast({ title: 生成图片失败, icon: none }) }) }注意canvas参数在不同基础库版本上写法不同。老版本用canvasId新版本要求传入node对象。如果你的项目需要兼容旧版可以在onReady里同时保存canvasNode和旧版本的canvasId调用时判断基础库版本再选择参数。wx.saveImageToPhotosAlbum调用前需要用户授权scope.writePhotosAlbum未授权会直接fail建议在按钮点击前先wx.getSetting检查授权状态没授权时引导用户去wx.openSetting打开。参数类型必填说明canvasCanvas是Canvas节点基础库2.9.0fileTypeString否导出图片类型默认pngqualityNumber否图片质量jpg时生效0~1destWidthNumber否输出图片宽度默认canvas宽度destHeightNumber否输出图片高度默认canvas高度4.3 处理玩家头像和异步图片绘制Canvas上画头像时不能直接ctx.drawImage(url, x, y, w, h)。需要先用wx.getImageInfo获取到本地路径再在回调里drawImage。如果头像URL是HTTPS且域名已配置wx.getImageInfo可以直接用否则先下载到本地临时文件。wx.getImageInfo({ src: summoner.iconUrl, success(info) { ctx.save() ctx.beginPath() ctx.arc(40, 40, 20, 0, Math.PI * 2) ctx.closePath() ctx.clip() ctx.drawImage(info.path, 20, 20, 40, 40) ctx.restore() } })arc画了一个圆形裁剪区域clip之后drawImage只显示在圆形内部。常见的坑是ctx.save()和ctx.restore()不成对导致后续绘制的文字也被裁剪。如果你发现文字或另一半头像消失了先检查是不是忘了restore。5. LOL战绩查询源码调试与性能优化加载页、缓存更新与长按拖拽的实战细节5.1 修改刚进入的加载页面避免白屏用户从搜索页跳详情页时需要等云函数返回。如果这段等待时间页面是空白的体验很差。常见做法是在详情页onLoad里展示一个骨架屏或者先渲染缓存过的上一局数据。view wx:if{{loading}} classskeleton view classskeleton-line/view view classskeleton-line short/view /view view wx:else classcontent !-- 正常内容 -- /view骨架屏用纯CSS动画模拟加载状态不需要图片。关键点是wx:if和wx:else要相邻中间不能插别的节点否则隐藏时内容会残留。5.2 用乐观更新减少接口调用如果用户从详情页返回再进入不希望每次都重新请求。可以在onShow里检查缓存时间只对超过5分钟的数据重新请求。同时在setData时先渲染旧数据再在后台悄悄拉新数据拉到了再覆盖一次用户永远先看到旧数据再看到新数据。onShow() { const cached wx.getStorageSync(lol:match: this.summonerId) if (cached) this.setData({ matches: cached.data }) this.refresh() }refresh里调用云函数后更新缓存和界面。这样在弱网环境也能秒开因为读的是本地缓存网络失败时用户仍然有上一次的对局可看。5.3 长按拖拽滚动与图片长按识别战绩列表里经常需要长按复制玩家名或战绩ID但长按在scroll-view里会和滚动冲突。解决办法是在scroll-view上设置enhanced属性长按手势就能被列表项正常识别。scroll-view scroll-y enhanced show-scrollbarfalse bindscrollonScroll !-- 列表 -- /scroll-viewenhanced是基础库2.10.0之后支持的属性开启后滚动性能更好对长按手势的识别也更准确。注意show-scrollbarfalse中间没有空格写错会被当成字符串true。5.4 判断截图时机的技巧Canvas生成截图最常见的真机问题是画了一部分就导出图片上只有半个背景。原因是dx.drawImage或ctx.fillText还没执行完就调用了导出接口。对于type2d的Canvas所有绘制操作是同步的但在部分Android机型上wx.getImageInfo回调完成后立即导出会有概率失败。我一般会加一个小延迟比如setTimeout100ms或者在最后一个绘制回调里调用导出。不要依赖setTimeout做所有事只在头像加载这个环节加。真机上如果导出空白优先检查ctx.draw是否被旧版Canvas API调用新版type2d不需要ctx.draw()旧版需要。这个坑比预期更常见代码从网上复制时尤其容易混用两代API。本文还有配套的精品资源点击获取