简介:一份面向微信小程序初学者的LOL战绩查询案例源码,源自社区分享计划,围绕英雄联盟战绩查询这一实际场景,完整演示了从页面搭建、网络请求、数据处理到用户授权的开发链路。压缩包共包含158个文件,约5.48MB,其中94张png图片和4张gif动图覆盖头像、对局记录、段位展示等界面素材与效果预览;18个js文件处理查询逻辑及接口调用,15个wxml构建页面结构,14个wxss定义视觉样式,11个json负责小程序配置,整体文件分类清晰。已有170人学习下载,适合初学者对照研读。通过该源码可重点掌握wx.request()向后端查询游戏数据、数据绑定渲染页面信息、wx.authorize()完成用户授权,同时理解全局逻辑与页面脚本的分工配合。资源附带完整的演示动图和界面截图,便于开发者在实践中逐段调试,快速上手微信小程序与游戏数据类应用开发。
1. 微信小程序开发里的LOL战绩查询,难点从来不在小程序端
做过这个案例的人都有个共同感受:小程序页面、路由和组件半天就能写完,真正卡住你两三天的是那套Riot API的鉴权、数据聚合和延迟处理。LOL战绩查询的本质是三层调用:小程序端负责展示,中间服务层负责把召唤师名换成PUUID再去拉对局列表,最后按gameId批量抓取每局详细数据。这三层只要一层超时或字段对不上,战绩页面就白屏。
这个案例适合两类人:一是想练微信小程序网络层封装和列表渲染的初中级前端,二是在研究第三方API鉴权、缓存设计和数据归一化的后端开发。源码本身并不复杂,但把“召唤师信息、对局历史、单局详情”串起来的链路,比看上去要长得多。下面按我通常会落地的方案,把这个案例的完整实现路径拆开讲,从API选型、服务层设计到小程序端页面的可复现写法一次说清。
2. 战绩数据的来源与鉴权方案:Riot API的调用结构和数据层级
2.1 为什么不能直接从小程序调Riot API
最直观的做法是小程序里直接请求Riot的接口,省掉中间层。但这个方案在实际开发中撑不过一天。Riot的开发者密钥分两种:开发密钥(24小时过期)和个人应用密钥(长期有效但有速率限制)。开发密钥的过期机制决定了它没法写死在前端,而个人应用密钥的请求头里要带X-Riot-Token,一旦小程序代码被反编译,密钥就直接泄露。
另一个限制是域名白名单。微信小程序的生产环境强制要求所有请求走HTTPS,且域名必须在小程序后台配置到request合法域名里。Riot的接口域名按区域分散在na1.api.riotgames.com、kr.api.riotgames.com、euw1.api.riotgames.com等十几个区域节点上,如果每次都动态切换域名,白名单配置会变成一个维护灾难。所以这个案例的标准做法是:服务端代理转发,用你自己的域名做唯一出口。
2.2 数据链路的三个层级
Riot API按数据粒度分成三层,这个分层直接决定数据库表和缓存key的设计:
| 层级 | 接口 | 返回内容 | 调用频率 |
|---|---|---|---|
| 账户层 | /riot/account/v1/accounts/by-riot-id/{gameName}/{tagLine} | PUUID、gameName、tagLine | 按召唤师维度,低频 |
| 对局列表层 | /lol/match/v5/matches/by-puuid/{puuid}/ids | 对局ID列表(默认20个,最多100个) | 每次查询触发,中频 |
| 对局详情层 | /lol/match/v5/matches/{matchId} | 完整对局数据,含参与者和时间线 | 每局一次,高频 |
这里最容易被忽略的坑是:对局列表接口只返回一串gameId数组,不含胜负、击杀、补刀这些实际展示数据。你要先把gameId循环去请求详情接口,才能拼出“最近20场的战绩列表”。这就意味着一次页面刷新背后可能触发21个上游请求(1个列表+20个详情),如果中间没有缓存和并发控制,Riot的速率限制会在第10局左右开始报429。
# 伪代码示例:对局列表到详情数据的聚合逻辑 import requests import concurrent.futures def fetch_recent_matches(puuid, region, api_key): # 1. 拉取最近20局的对局ID列表 list_url = f"https://{region}.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?count=20" match_ids = requests.get(list_url, headers={"X-Riot-Token": api_key}).json() # 2. 并发拉取每局详情,限制最大并发数为4,避免触发限流 with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: futures = { executor.submit(fetch_match_detail, mid, region, api_key): mid for mid in match_ids } matches = [] for future in concurrent.futures.as_completed(futures): matches.append(future.result()) return matches def fetch_match_detail(match_id, region, api_key): detail_url = f"https://{region}.api.riotgames.com/lol/match/v5/matches/{match_id}" resp = requests.get(detail_url, headers={"X-Riot-Token": api_key}) return resp.json()这段伪代码里最值得关注的参数是max_workers=4和count=20。并发数设太高会立刻撞上Riot的速率限制,设太低则页面响应会慢到用户直接退出小程序。20这个数字是默认值,实际案例里大多会开放一个“更多战绩”按钮来增量加载,而不是一次拉100局。另外注意region参数——对局类接口属于match-v5,它请求的域名不是召唤师所在区服的域名,而是根据对局实际发生的区域路由,常见做法是在后端维护一个区域到域名的映射表。
2.3 小程序端与服务端的鉴权配合
在真实案例源码里,小程序端不会直接触碰Riot的token,而是用微信登录体系换一个自己的会话身份。推荐的流程是:小程序端调用wx.login()拿到code,发送到后端,后端用code换openid,再用openid去关联一个绑定好的PUUID。之后前端每次请求都带上这个会话标识,后端查自己的用户表得到PUUID,再决定是走缓存还是回源Riot。
这样做一个显著的好处是:战绩查询可以做成“按用户维度缓存”。同一个召唤师在小程序里被反复查询,Riot侧的调用次数能被压缩到一个很小的量级,生产环境下不容易触发限流。如果是用uniapp那套Taro或uni-app做一个微信小程序版本,设计思路也一样,只是wx.login和wx.request要换成uni.login和uni.request的写法。
3. 搭建可复现的服务层:从空目录到能跑通的最小API
3.1 项目目录结构与选型
这个案例的服务端我一般会选择Python的FastAPI来做,原因有两个:async支持对并发拉取Riot接口很友好;自动生成的Swagger文档在小程序联调期能省不少沟通成本。当然用Node.js的Express也没问题,结构上是一样的,只是并发写法不同。
lol-match-service/ ├── app/ │ ├── main.py # FastAPI入口与路由注册 │ ├── riot_client.py # Riot API封装,统一处理鉴权和重试 │ ├── cache.py # 缓存层,用Redis做对局数据的短时存储 │ ├── models.py # Pydantic模型,定义响应结构 │ └── routers/ │ ├── summoner.py # 召唤师查询与绑定 │ └── matches.py # 战绩列表与详情 ├── requirements.txt └── .env # 存放密钥与配置这个结构没有过度拆分,每一层的职责在小项目里刚好合适。riot_client.py负责对外部API的一切交互,上层路由不直接拼URL;cache.py独立出来是因为战绩数据有很强的时效性——一个用户打开战绩页后,10分钟之内再次查看,没有理由重新打Riot去拉20局详情,这既是体验优化也是限流保护。
3.2 核心的Riot客户端封装
# riot_client.py import httpx import asyncio from typing import Optional class RiotClient: def __init__(self, api_key: str, default_region: str = "asia"): self.api_key = api_key self.region = default_region self.headers = { "X-Riot-Token": api_key, "User-Agent": "LOL-Match-MiniApp/1.0" } # 记录每个区域域名下的请求时间,用于本地限流 self._request_timestamps = [] async def _get(self, url: str, retry: int = 3) -> Optional[dict]: # 带429重试的GET请求,退回指数退避策略 async with httpx.AsyncClient(timeout=15) as client: for attempt in range(retry): resp = await client.get(url, headers=self.headers) if resp.status_code == 200: return resp.json() elif resp.status_code == 429: # Retry-After头是Riot标准限流响应 wait_time = int(resp.headers.get("Retry-After", 2 ** attempt)) await asyncio.sleep(wait_time) elif resp.status_code == 404: return None # 召唤师不存在或对局ID无效 return None async def get_puuid_by_name(self, game_name: str, tag_line: str) -> Optional[str]: url = f"https://asia.api.riotgames.com/riot/account/v1/accounts/by-riot-id/{game_name}/{tag_line}" data = await self._get(url) return data.get("puuid") if data else None async def get_match_ids(self, puuid: str, count: int = 20) -> list[str]: url = f"https://asia.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?count={count}" data = await self._get(url) return data or []几个关键参数要单说。timeout=15不是拍脑袋定的——Riot match-v5接口的响应时间通常在200ms到3秒之间,但如果遇上大乱斗模式的对局,详情数据量大,极端情况会到8秒以上;15秒是在“不能太慢让用户干等”和“不能太短误杀正常请求”之间取的值。retry=3配合Retry-After头,比固定等待更尊重上游限流策略。404返回None而不是抛异常,是为了区分“数据不存在”和“网络错误”,这两个状态在API响应里要给小程序端不同的提示文案。
3.3 对局详情的批量拉取与数据归一化
拿到match_ids列表后,真正的工程量在数据归一化。Riot返回的match详情里,participants数组里的位置不是固定的——第一个人不一定是蓝方上单,而是按系统排序的。要拿到“我这个召唤师在这局里杀了几个、助攻几个”,必须先通过PUUID找到他在participants数组里的索引位置,再去读对应的stats子对象。
# 从对局详情中提取单个召唤师的战绩摘要 def extract_summoner_match_summary(match_data: dict, puuid: str) -> dict: # match_data["metadata"]["participants"] 按顺序与 participants 数组下标一致 participants = match_data["metadata"]["participants"] if puuid not in participants: return None idx = participants.index(puuid) participant_info = match_data["info"]["participants"][idx] stats = participant_info.get("stats", {}) # 击杀/死亡/助攻来自challenges子对象,纯stats里这些字段名要特殊处理 kills = stats.get("kills", 0) deaths = stats.get("deaths", 0) assists = stats.get("assists", 0) return { "win": stats.get("win", False), "kills": kills, "deaths": deaths, "assists": assists, "champion": participant_info.get("championName", "Unknown"), "gameMode": match_data["info"].get("gameMode", ""), "gameDuration": match_data["info"].get("gameDuration", 0) }participants.index(puuid)这个操作是整个战绩查询的枢纽,几乎所有字段的提取都依赖这个索引对应关系。源码包里如果看到pUUID和participants混用的地方,大概率是这里没理清楚。另外Riot的championName字段返回的是英雄英文名(如"Ahri"、"LeeSin"),小程序端要显示中文名和英雄头像,需要额外加一层本地映射,从Data Dragon的CDN拉取champion.json来做ID到资源路径的转换。
3.4 缓存层的必要性验证
用Redis做对局缓存的策略很简单:以match_id为key,TTL设为10分钟,存完整JSON。但要注意一个边界——同一个召唤师在不同时间点查询同一局,对局数据里他自己的stats是固定的,但对局中的其他9个人如果有后续的反馈数据变化,旧缓存会拉出新数据。所以按match_id缓存只适合“当前用户视角”的展示,不适合做全量数据仓库。
# cache.py 缓存读取逻辑 import redis import json r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True) def get_cached_match(match_id: str): data = r.get(f"match:{match_id}") return json.loads(data) if data else None def set_cached_match(match_id: str, match_data: dict, ttl: int = 600): r.setex(f"match:{match_id}", ttl, json.dumps(match_data))TTL设600秒对应一个实际的交互场景:用户在小程序里看战绩,返回列表页,再点进同一局,10分钟内都不该再次请求Riot。超过10分钟则重新拉取,这个时间窗口在生产环境里已经足够温和覆盖大部分重复查询。
4. 小程序端战绩页面的实现与加载策略
4.1 页面结构与数据流
小程序端按“搜索页 → 战绩列表页 → 对局详情页”三个页面来做,这个划分和API的层级天然对应。搜索页输入召唤师名和Tag(国服玩家通常只记得游戏ID),请求后端拿到PUUID;战绩列表页拿到match_ids后循环请求详情;对局详情页则展示单局内的完整出装、技能加点和对位数据。
// matches.js - 战绩列表页的核心逻辑 Page({ data: { matches: [], loading: false, hasMore: true, puuid: "" }, loadMatches(reset = false) { if (this.data.loading) return; this.setData({ loading: true }); const start = reset ? 0 : this.data.matches.length; wx.request({ url: "https://your-domain.com/api/matches", data: { puuid: this.data.puuid, start: start, count: 10 // 缓存key按start+count设计,分页拉取 }, success: (res) => { const newMatches = res.data.matches; this.setData({ matches: reset ? newMatches : this.data.matches.concat(newMatches), loading: false, hasMore: newMatches.length === 10 }); }, fail: () => { wx.showToast({ title: "战绩加载失败", icon: "none" }); this.setData({ loading: false }); } }); }, onPullDownRefresh() { this.loadMatches(true); wx.stopPullDownRefresh(); }, onReachBottom() { if (this.data.hasMore) { this.loadMatches(false); } } })这里的分页参数是start而不是page,因为后端查询的是连续数组切片,用start语义更准确。hasMore的判断依据是“本次返回数量是否等于请求数量”——如果返回少于10条,说明没有更多对局了,这个判断在对局总数恰为10的倍数时会多一次无效请求,但代价可接受,不值得为边界情况增加复杂度。
4.2 列表渲染的性能边界
战绩列表里的每一项要做的事包括:英雄头像、击杀/死亡/助攻比、胜负色块、对局时长、装备图标。这些字段全部来自match_detail的数据归一化结果,如果在小程序端做二次处理,会拖慢滚动渲染。所以在后端聚合时就应该把字段裁剪成小程序端直接能用的结构,比如直接返回{ "win": true, "kills": 12, "deaths": 3, "assists": 7, "champion": "阿狸", "championIcon": "https://cdn.example.com/ahri.png" }。
<!-- matches.wxml 列表项的核心渲染 --> <view class="match-item {{item.win ? 'win-bg' : 'lose-bg'}}"> <image src="{{item.championIcon}}" class="champion-icon" /> <view class="kda-container"> <text class="kda-text">{{item.kills}} / {{item.deaths}} / {{item.assists}}</text> <text class="game-mode">{{item.gameMode}}</text> </view> <view class="duration">{{item.gameDurationText}}</view> </view>WXML里直接绑定item.win做类名切换,比通过wx:if渲染两套DOM性能好很多。gameDuration需要在后端转成“32分05秒”这样的可读格式,而不是让前端去算。图片用懒加载策略——champion-icon类名的图片可以做占位,但小程序原生的image组件本身就带lazy-load属性,列表项不多时开不开差别不大。关键的性能隐患在onReachBottom触发的分页加载里,如果你的count设置过大(比如一次20局),后端聚合响应时间会显著上升,列表会卡在loading态。这个案例里10是一个比较均衡的数值——一屏能显示完,下拉加载下一批的响应时间也不会太久。
4.3 启动加载页与首屏优化
相关热词里“修改刚进入的加载页面”其实就是小程序的启动loading页配置。在小程序里首屏优化常见做法是调整app.json里的window配置,把backgroundColor设成接近主色调的值,配合navigationBarBackgroundColor,让冷启动白屏不那么突兀:
// app.json 中与首屏加载相关的配置 { "window": { "navigationBarBackgroundColor": "#0a0e14", "navigationBarTitleText": "战绩查询", "navigationBarTextStyle": "white", "backgroundColor": "#0a0e14", "backgroundTextStyle": "light" } }首屏的性能瓶颈不在配置项,而在数据请求时机。搜索页是首页时,用户必须自己输入召唤师名才能触发查询;但如果这个案例里首页直接是“最近查看的召唤师”列表,就要在onLoad里提前请求后端获取历史记录。这里容易踩的坑是:onLoad里发起的请求如果较慢,用户切到别的tab再切回来,请求回调会丢。合理的做法是在onShow里做数据刷新判断,把网络请求的生命周期和页面可见性绑定。
5. 常见报错与限流避让:从unknown player到429的排查路径
5.1 召唤师解析失败:unknown player现象的根因
如果你真在开发这个案例,一定会遇到“LOL unknown player”或者查询结果莫名为空的情况。这个问题国服尤其明显:国服的召唤师数据经过腾讯的脱敏和渠道迁移,很多老账号的gameName和tagLine在Riot国际服API下查不到对应PUUID,返回的就是unknown player。相比之下,leagueakari这类在线战绩查询工具做得更完整,就是因为它们对归并过的PUUID做了持久化索引,查询过的召唤师信息会长期驻留数据库,而不是每次都回源Riot。
如果你按国际服API做这个案例,遇到unknown player时的排查顺序是:先确认gameName和tagLine是否区分大小写——Riot的by-riot-id接口对大小写敏感,但部分玩家ID里的特殊字符(空格、下划线、中文)在URL编码后会和预期不一致;再确认调用的是.api.riotgames.com账号接口而不是区服接口;最后确认PUUID是否因为账号迁移而失效。生产环境里我一般会在后端把PUUID的查询结果缓存到数据库里,即使Riot侧24小时后查不到了,小程序端依然能展示历史战绩。
5.2 限流、超时与重试参数的最终建议
排错时最常见的状态码是429和504。429说明你的请求太快,Riot在保护自己的服务;504则是Riot那边自己卡了。这两种情况对应完全不同的应对策略:
| 错误码 | 含义 | 处理方式 |
|---|---|---|
| 429 | 请求过于频繁,触发限流 | 读取Retry-After头做指数退避,或降低并发数 |
| 504 | 上游网关超时 | 直接降级返回缓存数据,没有缓存就提示用户稍后重试 |
| 403 | API Key无效或过期 | 检查开发密钥是否到了24小时期限 |
| 404 | 召唤师不存在或对局已删除 | 前端提示“查无此人”或“对局不存在” |
重试参数方面,最终版我固定在max_workers=4、timeout=15、retry=3这三个数值上。retry=3在504场景下意味着最坏等待时间可能达到45秒(15秒超时×3次重试),用户早走了,所以504要单独处理成“直接返回失败”而不是重试。429场景则相反,重试成功率很高,值得多等几秒。实现上的做法是:在_get方法里判断status_code,429走重试逻辑,504直接返回None并记录日志。
5.3 用抓包工具验证联调数据
联调阶段最好用的调试手段是抓包看真实请求响应。相关热词里“微信小程序抓包”指向的其实就是Charles或whistle抓HTTPS包。开发版小程序在开发者工具里打开“不校验合法域名”后,所有请求路径可以直接在Network面板看到。注意一个细节:小程序开发者工具里的wx.request和手机真机上跑的表现有时不一致,尤其在证书和域名白名单上,所以上线前必须用真机预览模式做一遍抓包确认。
// 在开发者工具控制台手动执行,验证后端接口连通性 wx.request({ url: "https://your-domain.com/api/summoner/query?name=暗夜猎手&tag=CN1", success: (res) => { console.log("PUUID:", res.data.puuid); console.log("对局数:", res.data.totalMatches); } })这一步能快速定位是前端数据链路断了,还是后端回源Riot时出了问题。如果工具里能看到返回但真机上白屏,优先检查小程序后台的request合法域名有没有加HTTPS证书链完整的域名。调试则建议把开发域名和大版本发布域名分开配置,避免在真机环境里频繁切换测试地址。
本文还有配套的精品资源,点击获取