
简介一份面向微信小程序初学者的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 fhttps://{region}.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?count20 match_ids requests.get(list_url, headers{X-Riot-Token: api_key}).json() # 2. 并发拉取每局详情限制最大并发数为4避免触发限流 with concurrent.futures.ThreadPoolExecutor(max_workers4) 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 fhttps://{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_workers4和count20。并发数设太高会立刻撞上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. 搭建可复现的服务层从空目录到能跑通的最小API3.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的一切交互上层路由不直接拼URLcache.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(timeout15) as client: for attempt in range(retry): resp await client.get(url, headersself.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 fhttps://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 fhttps://asia.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?count{count} data await self._get(url) return data or []几个关键参数要单说。timeout15不是拍脑袋定的——Riot match-v5接口的响应时间通常在200ms到3秒之间但如果遇上大乱斗模式的对局详情数据量大极端情况会到8秒以上15秒是在“不能太慢让用户干等”和“不能太短误杀正常请求”之间取的值。retry3配合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为keyTTL设为10分钟存完整JSON。但要注意一个边界——同一个召唤师在不同时间点查询同一局对局数据里他自己的stats是固定的但对局中的其他9个人如果有后续的反馈数据变化旧缓存会拉出新数据。所以按match_id缓存只适合“当前用户视角”的展示不适合做全量数据仓库。# cache.py 缓存读取逻辑 import redis import json r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue) def get_cached_match(match_id: str): data r.get(fmatch:{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(fmatch:{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按startcount设计分页拉取 }, 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 classmatch-item {{item.win ? win-bg : lose-bg}} image src{{item.championIcon}} classchampion-icon / view classkda-container text classkda-text{{item.kills}} / {{item.deaths}} / {{item.assists}}/text text classgame-mode{{item.gameMode}}/text /view view classduration{{item.gameDurationText}}/view /viewWXML里直接绑定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上游网关超时直接降级返回缓存数据没有缓存就提示用户稍后重试403API Key无效或过期检查开发密钥是否到了24小时期限404召唤师不存在或对局已删除前端提示“查无此人”或“对局不存在”重试参数方面最终版我固定在max_workers4、timeout15、retry3这三个数值上。retry3在504场景下意味着最坏等待时间可能达到45秒15秒超时×3次重试用户早走了所以504要单独处理成“直接返回失败”而不是重试。429场景则相反重试成功率很高值得多等几秒。实现上的做法是在_get方法里判断status_code429走重试逻辑504直接返回None并记录日志。5.3 用抓包工具验证联调数据联调阶段最好用的调试手段是抓包看真实请求响应。相关热词里“微信小程序抓包”指向的其实就是Charles或whistle抓HTTPS包。开发版小程序在开发者工具里打开“不校验合法域名”后所有请求路径可以直接在Network面板看到。注意一个细节小程序开发者工具里的wx.request和手机真机上跑的表现有时不一致尤其在证书和域名白名单上所以上线前必须用真机预览模式做一遍抓包确认。// 在开发者工具控制台手动执行验证后端接口连通性 wx.request({ url: https://your-domain.com/api/summoner/query?name暗夜猎手tagCN1, success: (res) { console.log(PUUID:, res.data.puuid); console.log(对局数:, res.data.totalMatches); } })这一步能快速定位是前端数据链路断了还是后端回源Riot时出了问题。如果工具里能看到返回但真机上白屏优先检查小程序后台的request合法域名有没有加HTTPS证书链完整的域名。调试则建议把开发域名和大版本发布域名分开配置避免在真机环境里频繁切换测试地址。本文还有配套的精品资源点击获取