这次我们来看一道蓝桥杯 Web 应用开发方向的模拟题解析:用 ES6 生成器异步产生随机数。题目只有十几个字,但实际覆盖的知识点并不少,包括 Generator 状态机、Promise 异步流程控制、随机数 API 选型,以及一个简单的 Web 接口封装。把这条链路完整跑通之后,你既能在裁判系统里写出符合要求的答案,也可以顺手做一个带 HTTP 接口的随机数批量生成服务。
先给结论:整篇文章不需要引入任何第三方框架,原生 JavaScript 就能完成,浏览器控制台和 Node.js 环境都可以直接验证。如果你正在备赛蓝桥杯,或者想搞懂 ES6 Generator 到底怎么配合异步流程,这篇文章可以收藏备用。下面会按照“题目设计 → 前置知识 → 环境准备 → 基础实现 → 异步实现 → 接口与批量任务 → 测试验证 → 排错 → 最佳实践”的顺序展开,所有代码都可以单独复制运行。
1. 核心能力速览
在开始写代码之前,先用一张表把这道模拟题的能力边界说清楚。这样你判断“这个技术点是否适合我现在用”会更直接。
| 能力项 | 说明 |
|---|---|
| 题目类型 | 蓝桥杯 Web 应用开发方向模拟题解析,ES6 异步编程与随机数应用 |
| 核心知识点 | ES6 Generator、Promise、异步流程控制、随机数生成、HTTP 接口封装 |
| 运行环境 | 现代浏览器控制台,或 Node.js 16+,无需第三方依赖 |
| 启动方式 | 浏览器打开 HTML文件 /node 文件名.js/node server.js |
| 主要功能 | 生成指定范围随机数、模拟异步生成延迟、批量生成、HTTP 接口返回 |
| 是否支持 API | 支持,提供 Node.js 原生http模块接口示例 |
| 是否支持批量任务 | 支持,通过循环迭代生成器获取多个随机数 |
| 随机数方案 | Math.random()入门演示;加密安全场景建议使用 Web Crypto API 或 Node.jscrypto.randomInt |
| 适合场景 | 蓝桥杯备赛、ES6 异步学习、随机数工具接口原型 |
需要特别说明的是,这道题本质是“用 Generator 管理异步流程”,而不是“随机数本身只能异步生成”。真实开发中Math.random()是同步的,真正有意义的是后面这套“Promise + Generator 手动迭代”的流程控制方式。把这一点理解清楚,题目才算真正吃透。
2. 题目设计与考察点分析
这一类蓝桥杯模拟题通常不会只考一个语法点,而是会把多个基础能力串起来。参考实际备赛时的常见考法,可以设计出如下题目描述:
设计一个 Web 应用,要求:
- 使用 ES6 Generator 函数封装随机数生成逻辑;
- 每次获取随机数时模拟异步等待,等待结束后返回一个
[min, max]范围内的整数;- 在浏览器页面点击按钮后,能连续获取多个随机数并展示到页面上;
- 随机数来源的 API 要可切换,至少能区分普通随机数和加密安全随机数;
- 如果通过 Node.js 运行,需要实现一个 HTTP 接口,通过
GET /api/random返回单个随机数,通过GET /api/random/batch?count=5返回批量随机数。
输入输出示例:
输入:范围 1-100,数量 5 输出:[23, 87, 56, 9, 42]表面看,输出结果只是几个随机数,但评分时可以拆出的考察点非常多:
- Generator 的基本用法:是否理解
function*、yield、next()的配合方式; - 惰性求值:生成器函数体不会在调用时立即执行,而是每次
next()才推进一次; - 异步流程控制:能否把 Generator 产生的 Promise 正确消费掉;
- 随机数 API 选择:是否知道
Math.random()的局限,以及加密安全随机数的使用场景; - 接口封装与批量任务:能否把底层逻辑封装成可访问的服务,并控制批量请求的边界。
使用边界上,这道题适合以下读者:正在备赛蓝桥杯 Web 应用开发方向的学生、想补 ES6 Generator 异步知识的开发者、需要快速搭一个随机数生成接口的前端工程师。不适合完全不熟悉 JavaScript 基础语法的人直接上手,建议至少先掌握函数、Promise、数组方法这三个前置能力。
3. 前置知识:ES6 Generator 与异步流程控制
3.1 Generator 是什么
Generator 是 ES6 提供的一种可以暂停和恢复执行的函数。定义时在function后面加一个星号:
function* randomGenerator(min, max) { yield Math.random(); yield Math.random(); } const gen = randomGenerator(1, 10); console.log(gen); // 生成器对象,函数体还没有执行调用生成器函数时,函数体不会立即运行,而是返回一个生成器对象。只有调用next()之后,代码才会执行到第一个yield,然后暂停在那里:
const result = gen.next(); console.log(result); // { value: 随机数, done: false }next()返回值是一个对象,包含value和done两个字段。value是yield后面表达式的值,done表示生成器是否已经结束。这个“暂停 + 恢复”能力,正是异步流程控制的基础。
3.2 为什么 Generator 可以控制异步
异步代码的根本问题是“不知道什么时候结束”。Promise 解决了回调地狱,但 Promise 链仍然需要手动管理。而 Generator 可以做到:把异步任务包装成 Promise,再用yield把它抛出;外部通过next()拿到这个 Promise,等它 resolve 之后再调用一次next(value),把结果传回生成器内部。
看起来比较绕,但本质就是一个手动版的async/await。实际上,async/await就是 Generator 和 Promise 的语法糖。理解这一点,你对 async/await 的掌握会更扎实。
3.3 随机数 API 怎么选
这道题涉及随机数,最容易丢分的地方就是 API 选型。简单对比一下:
| 方案 | 是否加密安全 | 使用场景 | 注意事项 |
|---|---|---|---|
Math.random() | 否 | 页面展示、洗牌演示、游戏随机 | 实现简单,但不能用于密码、令牌、抽奖等需要不可预测性的场景 |
crypto.getRandomValues() | 是 | 安全令牌、抽奖、密钥相关 | 需要安全上下文,file://协议下不一定可用 |
Node.jscrypto.randomInt() | 是 | Node.js 服务端随机数 | 使用方便,直接返回整数 |
题目要求是模拟练习,用Math.random()入门没问题,但代码注释里最好写清楚:如果需要安全随机数,应该换成加密安全 API。这也是竞赛评分中“代码健壮性”和“工程意识”的一部分。
4. 环境准备与前置条件
这道题虽然简单,但环境还是需要简单确认一下。推荐使用 Node.js 环境,因为后面要验证 HTTP 接口。如果你电脑上已经有 Node.js,直接进入下一步;如果没有,也可以先用浏览器 F12 控制台跑完同步和异步生成器部分。
4.1 检查 Node.js 和 npm
打开终端,执行:
node -v npm -v如果输出类似v18.x.x、9.x.x,说明环境可用。如果没有输出,需要先安装 Node.js LTS 版本。
4.2 初始化项目目录
建议单独建一个目录,避免和已有文件混在一起:
mkdir es6-random-generator cd es6-random-generator npm init -ynpm init -y会生成一个最简package.json。本示例不依赖第三方模块,所以不需要安装任何依赖。
如果你不想用 Node.js,可以直接在浏览器里新建一个 HTML 文件,在<script>标签中写代码,用浏览器打开即可。后面接口部分会用到 Node.js,但核心的 Generator 异步逻辑浏览器也能跑。
4.3 准备编辑器
推荐 VS Code,装不装插件都行。只要你写的.js文件和.html文件编码是 UTF-8,中文输出就不会乱码。
5. 基础实现:用同步生成器产生随机数
先从最简单的版本开始。创建一个random-sync.js,内容如下:
// random-sync.js function* randomGenerator(min = 0, max = 100) { while (true) { yield Math.floor(Math.random() * (max - min + 1)) + min; } } const gen = randomGenerator(1, 100); console.log(gen.next().value); console.log(gen.next().value); console.log(gen.next().value);在终端运行:
node random-sync.js预期输出是三行1到100之间的整数,例如:
42 7 86这里有几个关键点:
while (true)让生成器进入无限循环,每次next()都会停在yield处,调用方按需取值;Math.floor(Math.random() * (max - min + 1)) + min是生成区间内整数的标准写法,注意max - min + 1不能少;- 生成器是惰性的,不会一次把所有随机数算出来,内存占用很低。
如果你把gen.next().value改成gen.next(),会发现输出变成了{ value: 42, done: false }这样的对象。这是初学 Generator 最容易搞混的地方:next()返回的是对象,真正的值在.value上。
6. 异步实现:用 ES6 生成器异步产生随机数
6.1 用 Promise 包装随机数
题目要求“异步产生”,这里可以用setTimeout模拟一次耗时操作。比如模拟从服务端获取随机数,每次生成前等待 100 到 300 毫秒。创建random-async.js:
// random-async.js function createAsyncRandom(min = 0, max = 100) { return new Promise((resolve) => { const random = Math.floor(Math.random() * (max - min + 1)) + min; const delay = Math.floor(Math.random() * 200) + 100; setTimeout(() => { resolve(random); }, delay); }); } function* asyncRandomGenerator(min = 0, max = 100) { while (true) { yield createAsyncRandom(min, max); } }注意,这里生成器yield出去的不是随机数,而是一个 Promise。调用方拿到 Promise 之后,需要等它 resolve。
6.2 手动迭代生成器
最简单的消费方式就是手动调用next():
const gen = asyncRandomGenerator(1, 100); gen.next().value.then((value) => { console.log('第一次异步随机数:', value); return gen.next().value; }).then((value) => { console.log('第二次异步随机数:', value); });每次gen.next()都会产生一个新的 Promise,然后用.then()处理结果。这样做的问题是:如果次数多了,代码会变得很啰嗦,和回调地狱没有本质区别。
6.3 用递归驱动批量任务
更常见的做法是写一个调度函数,从生成器里持续取 Promise,resolve 后再继续取下一个。下面的代码会批量生成total个随机数:
function collectRandom(generator, total) { const result = []; function step(index) { if (index >= total) { return Promise.resolve(result); } const next = generator.next(); if (next.done) { return Promise.resolve(result); } return Promise.resolve(next.value).then((value) => { result.push(value); return step(index + 1); }); } return step(0); } const batchGen = asyncRandomGenerator(1, 100); collectRandom(batchGen, 5).then((numbers) => { console.log('批量结果:', numbers); });运行:
node random-async.js预期输出类似:
批量结果: [ 23, 87, 56, 9, 42 ]这段代码的思路是:自己实现一个微型调度器,把 Generator 产生的 Promise 一个个消费掉。如果你理解了这个流程,再去看 async/await,就是换了一套更顺手的语法。
6.4 async/await 版本对比
同样功能,用 async/await 写会直观很多:
async function batchCollect(generator, total) { const result = []; for (let i = 0; i < total; i++) { const random = await generator.next().value; result.push(random); } return result; } const gen2 = asyncRandomGenerator(1, 100); batchCollect(gen2, 5).then((numbers) => { console.log('async/await 版本:', numbers); });这里要特别强调:await generator.next().value是可行的,因为generator.next().value返回的是 Promise,而await就是在等 Promise resolve。这就是 Generator 和 Promise 天然衔接的证明。
7. 封装为 Web 接口与批量任务
题目说“Web 应用开发实战”,所以只写控制台脚本还远远不够。这里用 Node.js 原生模块写一个随机数接口服务,不需要 Express 等框架,竞赛环境里更稳。
7.1 HTTP 服务代码
创建server.js:
// server.js const http = require('http'); const { randomInt } = require('crypto'); const host = '127.0.0.1'; const port = 3000; function* asyncRandomGenerator(min = 0, max = 100) { while (true) { const random = randomInt(min, max + 1); yield new Promise((resolve) => { setTimeout(() => resolve(random), 50); }); } } function sendJSON(res, statusCode, data) { res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(data)); } const server = http.createServer((req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); if (url.pathname === '/api/random') { const gen = asyncRandomGenerator(1, 100); gen.next().value.then((value) => { sendJSON(res, 200, { code: 0, data: value }); }); return; } if (url.pathname === '/api/random/batch') { const count = Number(url.searchParams.get('count') || 5); const safeCount = Math.min(Math.max(count, 1), 1000); const gen = asyncRandomGenerator(1, 100); const result = []; function run(i) { if (i >= safeCount) { sendJSON(res, 200, { code: 0, data: result, total: result.length }); return; } gen.next().value.then((value) => { result.push(value); run(i + 1); }); } run(0); return; } sendJSON(res, 404, { code: 404, message: 'not found' }); }); server.listen(port, host, () => { console.log(`随机数接口已启动:http://${host}:${port}`); });启动服务:
node server.js如果终端输出:
随机数接口已启动:http://127.0.0.1:3000说明服务已经跑起来了。这里用crypto.randomInt替代Math.random(),是因为接口服务的随机数可能被用于抽奖或令牌场景,加密安全的随机数来源更合适。
7.2 curl 调用接口
新开一个终端,直接请求:
curl "http://127.0.0.1:3000/api/random"返回示例:
{"code":0,"data":73}批量接口:
curl "http://127.0.0.1:3000/api/random/batch?count=5"返回示例:
{"code":0,"data":[23,87,56,9,42],"total":5}注意,count参数即使传一个很大的数字,接口也会限制到 1000,这是为了防止批量任务把服务拖垮。
7.3 Python 调用接口
很多备赛学生会用 Python 写自动化测试,这里给一个简单的请求示例:
import requests url = "http://127.0.0.1:3000/api/random/batch" params = {"count": 5} response = requests.get(url, params=params, timeout=5) print(response.status_code) print(response.json())如果requests库没有安装,先执行:
pip install requests7.4 批量任务设计建议
接口服务暴露出来之后,批量任务需要考虑几个点:
- 限制数量:不要让调用方一次请求 10 万个随机数,
count要设上限; - 逐步生成:生成器一次只产出一个 Promise,用递归或循环逐步消费,不会把所有任务一次性压进内存;
- 失败重试:这里模拟的是本地
setTimeout,不容易失败;如果是真实远程接口,建议在消费 Promise 时加上重试逻辑; - 日志记录:每次批量请求记录时间戳、数量、结果条数,方便排查问题。
8. 功能测试与效果验证
一个功能能不能过评审,最终要看测试结果。建议你按下面的表格逐项验证:
| 测试项 | 输入 | 预期结果 | 判断标准 |
|---|---|---|---|
| 同步生成器 | 执行random-sync.js | 输出三行整数 | 数字都在 1 到 100 之间 |
| 异步生成器 | 执行random-async.js | 输出数组包含 5 个整数 | 数组长度等于 5,元素类型为 number |
| 范围边界 | 生成器传min=1, max=1 | 始终输出 1 | 不会出现其他数字 |
| 单个接口 | GET /api/random | JSON 返回code=0 | data是整数 |
| 批量接口 | GET /api/random/batch?count=5 | JSON 返回数组 | 数组长度等于 5 |
| 参数上限 | GET /api/random/batch?count=10000 | 实际生成 1000 个 | 接口不被拖垮 |
| 404 分支 | 请求不存在的路径 | JSON 返回code=404 | 状态码为 404 |
运行测试时建议开启 Node.js 的进程观察,在另一个终端执行:
node --trace-warnings server.js这样可以更早发现未处理的 Promise 警告。对于大批量生成,可以通过任务管理器或系统监视器观察 Node.js 进程的内存占用。由于这套实现是逐个消费 Promise,内存增幅通常远低于一次性Array.from({ length: n }, ...)的做法。
9. 常见问题与排查方法
备赛时最容易踩的坑如下,直接对照排查。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 调用生成器函数后没有任何输出 | 没有调用next(),函数体不会执行 | 检查是否写了gen.next() | 调用迭代器,取.value |
next()返回对象而不是数字 | 直接把next()当成随机数 | 打印返回值看结构 | 使用gen.next().value |
控制台输出[object Promise] | 生成器 yield 出去的是 Promise,没消费 | 检查是否用了.then或await | gen.next().value.then(...) |
浏览器中crypto.getRandomValues报错 | file://协议下安全 API 不可用 | 换成localhost或 HTTPS 环境 | 用Math.random()演示,或启动本地服务器 |
| 接口 404 | 请求路径或查询参数写错 | 打印url.pathname和searchParams | 对齐/api/random和/api/random/batch |
| 端口被占用 | 3000 端口被其他进程占用 | 终端报EADDRINUSE | 修改port为 3001 等 |
| 批量任务结果顺序不对 | 使用了Promise.all并发获取 | 观察生成顺序 | 使用递归或for await逐个消费 |
| 随机数质量不满足要求 | 安全场景使用Math.random() | 评估是否需要不可预测性 | 切换 Web Crypto API 或crypto.randomInt |
其中“生成器不执行”是最多人踩的坑,本质原因是 Generator 的惰性求值。记住一句话:调用生成器函数只是创建对象,next()才是真正的执行动作。
10. 高分答案的组织与最佳实践
如果你要把这道题的答案写到竞赛系统里,建议不要只堆代码,而是按模块拆分,并加一些关键注释。
- 先写最小可运行示例:先跑通同步生成器,再改成异步,最后加接口。不要一开始就全写在一起。
- 生成器只负责状态流转:把“随机数怎么产生”和“是否异步等待”拆开,随机数 API 可以通过参数传入,方便测试时替换。
- 批量任务要有边界:接口参数限制、最大数量、失败重试,这些工程细节是加分项。
- 善用 Node.js 内置模块:不要为了一个随机数接口硬装 Express,原生
http模块足够,而且裁判环境更可控。 - 注释里写清楚安全边界:说明
Math.random()不适合密码学场景,建议换成加密安全随机数来源。这体现的不只是代码能力,更是工程意识。
还有一个很重要的合规提醒:如果题目后续扩展成真实业务场景,涉及抽奖、权限令牌、用户数据,必须使用加密安全的随机数来源,并且确认你的使用场景符合平台规则和法律法规。本文所有代码仅用于学习和技术验证,不要直接用于高风险生产环境而不做安全评估。
11. 总结与下一步
这道模拟题真正值得掌握的三个点:第一,Generator 的暂停和恢复机制是异步流程控制的核心;第二,async/await本质上就是 Generator 加 Promise 的语法糖;第三,随机数 API 的选型要跟着场景走,不是所有随机数都只能用Math.random()。
最容易踩的坑是next()返回的是一个对象,而不是直接的值。后续如果你想继续扩展,可以尝试把上面的生成器换成真实接口请求,比如从远程服务获取随机数,或者把批量生成改为流式输出,每生成一个推给前端一次。这样你对 Generator 的异步控制能力会有更深的理解。