【蓝桥杯Web】第十四届蓝桥杯(Web 应用开发)模拟赛 2 期 | 精品题解
🧑💼 个人简介:一个不甘平庸的平凡人🍬
🖥️ 蓝桥杯专栏:蓝桥杯题解/感悟
🖥️ TS知识总结:十万字TS知识点总结
👉 你的一键三连是我更新的最大动力❤️!
📢 欢迎私信博主加入前端交流群🌹
📑 目录
- 🔽 前言
- 1️⃣ 凭空消失的 TA
- 2️⃣ 用户名片
- 3️⃣ 芝麻开门
- 4️⃣ 宝贵的一票
- 5️⃣ 粒粒皆辛苦
- 6️⃣ 618 活动
- 7️⃣ 资讯接口
- 8️⃣ 绝美宋词
- 9️⃣ 平地起高楼
- 🔟 收快递了
- 💠 偷梁换柱(职业院校组)
- 💠 大电影(职业院校组)
- 💠 乾坤大挪移心法(职业院校组)
- 💠 不能说的秘密(职业院校组)
- 🔼 结语
🔽 前言
第十四届蓝桥杯 Web 应用开发模拟赛第二期昨天正式开始了(本来写的是今天正式开始了,结果没想到这篇文章写到了凌晨 1 点 😵💫),博主也是第一时间为大家带来了题解!这篇题解包含了大学组和职业院校组的所有内容。
因为自己在做题时忘记保存代码了,所以写这篇题解时我不得不又重新做了一遍,看在博主这么肝的份上,大佬们给个一键三连加关注吧!🤗
关于蓝桥杯更多的题解请前往专栏:蓝桥杯题解/感悟,欢迎大家的订阅!
本篇只会大概提出题目要求,关于题目的更多细节可自行去模拟赛主页查询:Web 应用开发模拟赛 2 期大学组
话不多说,开撕!
1️⃣ 凭空消失的 TA
题目说在 index.html 中未正常显示表单组件 myform,先运行看一下效果:
发现 myform组件里的立即创建和取消这两个文本被渲染了,这说明 index.html 确实是引入了 myform,但为何myform没有正常显示呢?
一开始我以为是myform组件里出了问题,可检查一遍后并没有发现问题,最后回到index.html才发现,是因为index.html中未引入element-ui的js文件,我们加一行代码引入一下就解决了:
2️⃣ 用户名片
要求是需要将这个卡片垂直居中,并且还需要将卡片中左侧文字水平居中,看了一下HTML结构,发现它们都有一个共同的类名center:
所以对center类名定义样式就行了:
/* TODO 待补充代码 */ .center { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); }
上述代码利用定位将元素垂直水平方向各偏移父元素(position: relative)的50%,这个时候元素还不是居中(因为定位偏移的中心点不在元素的中心上而是在元素的边界上):
使用transform将元素在水平和垂直的负方向移动自身的50%(transform运动的中心点在元素的中心位置):
这里深入说一下一个CSS选择器优先级的问题:
图中所示,作用于同一元素的.user-card .points(后代选择器)的样式优先生效于.center (类选择器)的样式,这就映证了网上说的后代选择器的优先级小于类选择器的说法是不够准确的。
其实,后代选择器和类选择器没有可比性,后代选择器是选择器组合方式的一种,它是一种组合,本身没有任何优先级(严格的用词叫特殊性) 可言。比如.user-card .points在计算特殊性(优先级)时,是分别计算「.user-card」和「.points」的特殊性(优先级),完全不用考虑它们之间是用后代关联的。
按照权重来说.user-card .points的样式优先生效于.center的样式,是因为.user-card .points含有两个类选择器,它的权重比.center高。
关于CSS选择器权重、优先级的问题在实际开发中是比较重要的,如果你看到这里对它们还不是很了解,建议你去网上多看看关于它们的内容。
3️⃣ 芝麻开门
这题简单的考察了Promise,最终实现以下效果:
代码:
/** * @description: 调用函数,开启弹窗,记录输入框的内容,并通过 promise 异步返回输入框中的内容 * @return {Promise} */ function mPrompt() { // 弹窗必须使用以下结构 template 保存的是弹窗的结构字符串,可以先转化为 DOM 再通过 appendChild 方式插入到 body 中 const template = ` 请输入咒语 取消 确定 `; const div = document.createElement("div"); // TODO:待补充代码 div.innerHTML=template document.body.append(div); let val = div.getElementsByTagName("input")[0]; return new Promise((resolve, reject) => { document.getElementById("cancel").onclick = function() { div.remove() reject(false) } document.getElementById("confirm").onclick = function() { div.remove() resolve(val.value) } }); }
代码很简单,按照题目要求返回一个Promise对象,并在点击事件中做出不同的处理(reject,resolve)即可。
4️⃣ 宝贵的一票
要求是实现一个动态列表的表单,可以新增选项和删除选项,最终效果:
添加的思路:
- 添加选项时先获取当前选项的个数,如果当前选项个数以及为2了,那么就需要先向前两个选项添加删除号(x)。
- 当前选项的个数小于2时,再添加一各选项,选项的总数也不会超过2,所以这时只需添加普通的选项即可。
- 当前选项的个数大于或等于2时,需要添加带有删除号(x)的选项。
删除的思路:
- 点击删除号(x)时先删除当前选项。
- 遍历余下的选项列表,更新它们的序号。
- 在遍历的时候判断余下的选项个数,若剩余的选项小于等于2了,需要删除每个选项后面的删除号(x)。
代码:
// 点击加号逻辑 $(".add").click(function () { // TODO 待补充代码 // 当前列表长度 let cl = $(".list").children().length; // 长度为2时为前两个选项加上x号 if (cl === 2) { $(".list").children().each((index,item)=>{ $(item).append(` `) }) } if (cl `); } }); // 点击 x 删除逻辑,列表小于 2 项时不显示删除图标 $(document).on("click", ".del-icon", function () { // TODO 待补充代码 // 删除这一条 $(this).parent().parent().remove() // 遍历 $(".list").children().each((index,item)=>{ // 修改剩下的列表序号 $(item).children('label').text(`选项${index + 1}`) if($(".list").children().length // 列表长度小于等于2时,请求x号 $(item).children()[2].remove() } }) }); "2017": { "wheat": 431, "soybean": 142, "potato": 232, "corn": 642 }, "2018": { "wheat": 417, "soybean": 156, "potato": 258, "corn": 643 }, "2019": { "wheat": 416, "soybean": 168, "potato": 269, "corn": 650 }, "2020": { "wheat": 436, "soybean": 174, "potato": 277, "corn": 680 }, "2021": { "wheat": 441, "soybean": 186, "potato": 289, "corn": 692 }, "2022": { "wheat": 445, "soybean": 201, "potato": 315, "corn": 706 } } wheat: ["小麦"], soybean: ["大豆"], potato: ["马铃薯"], corn: ["玉米"] }; let sourceTip = ["全部"]; // 获取数据 axios.get("./data.json").then(res={ let data = res.data.data; for (const key1 in data) { sourceTip.push(key1); for (const key2 in data[key1]) { dataObj[key2].push(data[key1][key2]); } } let newSource = []; newSource.push(sourceTip); for (const key in dataObj) { newSource.push(dataObj[key]); } option.dataset.source = newSource; myChart.setOption(option); })
代码和逻辑都比较简单,就不多说了。
6️⃣ 618 活动
就是按照官方给的最终效果图,去实现下面这个页面:
没啥技术含量,全靠堆HTML和CSS,这里就不放代码了。
但这个题是我认为是整场模拟赛里最坑人的题,特别废时间,我建议这个题要么放到最后再写(因为完成度50%以上就能得到分,其它题不行),要么完成差不多后就直接去做下面的题,别死扣细节,不然吃亏的都是你!
7️⃣ 资讯接口
题目要求使用 NodeJS 去创建一个服务器并响应一个/news接口:
- 通过在 app.js 书写代码,创建一个服务器,使服务在 8080 端口运行。
- 访问 /news 返回资讯数据,访问其他任意路径均返回字符串 404 。
代码:
// TODO: 待补充代码 const http = require("http"); // 创建http服务 const app = http.createServer(); app.on("request",(req,res)=>{ res.setHeader("Content-type", "text/html;charset=utf8"); switch (req.url) { case '/news': res.end(JSON.stringify([ { "channelId": "5572a108b3cdc86cf39001cd", "name": "国内焦点" }, { "channelId": "5572a108b3cdc86cf39001ce", "name": "国际焦点" } ])) break; default: res.end('404') break; } }) app.listen(8080);
8️⃣ 绝美宋词
相当于是使用Vue做一个搜索功能:
代码:
输入关键字,找一首词
因为需要将关键字包上一层来进行高亮,所以我使用了v-html指令来确保数据能以html的格式进行渲染,并配合replace替换关键字。
9️⃣ 平地起高楼
相当于是一道算法题,将一维数据转成树形结构,源数据:
[ { id: "51", // 区域 id name: "四川省", // 区域名字 pid: "0", // 区域的父级区域 id }, { id: "5101", name: "成都市", pid: "51", // 成都的父级是四川省,所以 pid 是 51 }, // ... ];
转换成树结构:
[ { id: "51", // 地址 id name: "四川省", // 地址名 pid: "0", // 该地址的父节点 id children: [ { id: "5101", name: "成都市", pid: "51", children: [ { id: "510101", name: "市辖区", pid: "5101", children: [], // 如果该区域节点没有子集,children 则为空数组!!! }, // ... ], }, // ... ], }, // ... ];
这题说复杂也复杂,说简单也简单,关键在于你怎么想了,想复杂的话能写几十行代码,想简单的话几行代码即可,我这里使用递归的方式进行解答。
首先需要先知道,convertToTree函数接收的参数regions代表一维数据数组,rootId代表树形结构中根节点的pid。convertToTree函数返回的是指定根节点(pid=rootId的)的树结构,所以我们只需逐渐降低rootId,递归调用convertToTree函数不断获取下一层的树形结构即可。
代码:
function convertToTree(regions, rootId = "0") { // TODO: 在这里写入具体的实现逻辑 // 将平铺的结构转化为树状结构,并将 rootId 下的所有子节点数组返回 // 如果不存在 rootId 下的子节点,则返回一个空数组 let arr = []; for (let i = 0; i
从整个过程来看,convertToTree函数执行一次就找到了一层数据,每一个数据被找到时就开始以该数据为根节点去递归调用convertToTree函数找下一层的数据。每一次调用convertToTree函数就会遍历一遍regions数组,如果最终的树形结构有三层,那么就需要遍历三遍regions数组。
如果你不想定义新的变量(如上面定义的arr)或者想炫技,你可以使用数组的reduce方法进行递归,说到这你可能会有疑问:reduce不是用来求和的吗?如果单纯的将reduce归类于求和函数,你的知识面就太过单薄了。
先来看看怎么使用reduce解答吧:
function convertToTree(regions, rootId = "0") { // TODO: 在这里写入具体的实现逻辑 // 将平铺的结构转化为树状结构,并将 rootId 下的所有子节点数组返回 // 如果不存在 rootId 下的子节点,则返回一个空数组 return regions.reduce((res,current)=>{ if (current['pid'] === rootId) { current.children = convertToTree(regions,current['id']); res.push(current); } return res; },[]) } module.exports = convertToTree; // 检测需要,请勿删除
reduce的第二个参数是一个空数组,所以:
- 第一次执行时,res=[],current=regions[0]。然后进行判断,如果current是根节点的话就以current的id作为下一层根节点pid递归调用convertToTree得到下一层的数据赋值给current.children,之后将current添加进res中,随后return出res。
- 第二次执行时,res为第一次执行返回的数组,current=regions[1]
- 第三次执行时,res为第二次执行返回的数组,current=regions[2]
- …
使用reduce跟使用for循环原理一样,只是看上去会给人一种很高级的感觉。
🔟 收快递了
这一题使用的是上一题我们转换后的树形结构:
[ { id: "51", // 地址 id name: "四川省", // 地址名 pid: "0", // 该地址的父节点 id children: [ { id: "5101", name: "成都市", pid: "51", children: [ { id: "510101", name: "市辖区", pid: "5101", children: [], }, // ... ], }, // ... ], }, // ... ];
要求是:
- 输入"市辖区"时,返回 [ “四川省”, “成都市”, “市辖区” ]。
- 输入"成都市", 则返回 [ “四川省”, “成都市” ]。
- 输入"四川省", 则返回 [ “四川省” ]。
- 如果不存在该地址,则返回一个 null。
我是思路是:
- 先递归遍历获取到指定name对象的pid。(相当于是从上向下找)
- 再根据此pid与父对象id相对应的关系递归查询父对象。
- 查询到父对象后更新pid,并保存父对象的name字段,然后开始新一轮的递归。(这时相当于是从下向上找)
function findRegion(regions, regionName) { // TODO: 在这里写入具体的实现逻辑 // 需要从树状结构的行政信息中,遍历找到目标区域的行政信息,如输入:成都市,返回 [四川省,成都市] // 如果所输入的位置信息不存在,则返回 null let arr = [],pid; // 根据name获取字对象的pid function getPid(list) { for (let i = 0; i 0) { getPid(list[i].children) } } } // 根据pid查询父对象 function addfName(list,pfid) { for (let i = 0; i 0) { addfName(list[i].children,pid) } } } getPid(regions) addfName(regions,pid) return arr.length > 0 ? arr.reverse() : null } module.exports = findRegion; // 检测需要,请勿删除
向arr数组 push name的过程是从树的底层向顶层进行的,所以最后得到的arr顺序是反的,需要reverse反向以下。
这种解法性能消耗较大,在比赛有限的时间中也想不到好的替换方法(因为博主是个算法菜鸟😬),如果大佬们有好的解法,欢迎在评论区或加入我们的交流群进行交流。
下面是职业院校组与大学组不一样的几个题:
💠 偷梁换柱(职业院校组)
考察了数据拦截,可以使用Object.defineProperty 或者 Proxy,要求:
- 如果新属性值在 0 -150 之间(包含 0 和 150),则直接更新。
- 如果新属性值小于 0,则属性值更新为 0。
- 如果新属性值大于 150,则属性值更新为 150。
使用Object.defineProperty:
// 请不要更改这个对象里面的内容 let person = { age: 0, }; // TODO:在这里写入具体的实现逻辑 // 对 person 的 age 属性更新行为进行拦截 // 如果输入的年龄在 0 - 150 之间,则认为是合法 // 否则,如果小于 0,则返回 0;如果大于 150,则返回 150 function defineReactive(obj, key, value) { Object.defineProperty(obj, key, { get() { return value; }, set(newVal) { if (newVal !== value) { newVal > 0 ? newVal > 150 ? (value = 150) : (value = newVal) : (value = 0) } } }) } defineReactive(person,'age',person.age) module.exports = person; // 检测需要,请勿删除
注意,千万不要直接这样写:
Object.defineProperty(person,'age',{ set:(newVal)=>{ newVal > 0 ? newVal > 150 ? (person.age = 150) : (person.age = newVal) : (value = 0) }, get:()=>{ return person.age } })
直接这样写会陷入死循环,因为在Setter中访问了person.age,这又会导致触发Getter并且对person.age赋值又会触发person.age,一直触发下去,完全就是一个死循环,这也就是为什么我们在上面的代码块中套了一层defineReactive函数的原因。
在defineReactive函数中的value相当于是闭包中的变量,它其实并不是真正的person.age,所以对value的一切操作都不会导致死循环。
使用Proxy:
// 请不要更改这个对象里面的内容 let person = { age: 0, }; // TODO:在这里写入具体的实现逻辑 // 对 person 的 age 属性更新行为进行拦截 // 如果输入的年龄在 0 - 150 之间,则认为是合法 // 否则,如果小于 0,则返回 0;如果大于 150,则返回 150 person = new Proxy(person,{ get: function(obj, key) { return obj[key]; }, set: function(obj, key, value) { value > 0 ? value > 150 ? (obj[key] = 150) : (obj[key] = value) : (obj[key] = 0) } }) module.exports = person; // 检测需要,请勿删除
💠 大电影(职业院校组)
要求实现一个收藏的功能:
- 点击收藏图标,收藏图标在空心(images/hollow.svg)和实心 (images/solid.svg)中进行切换。
- 点击收藏图标后,仅在收藏图标为实心图形时,成功提示框(id=toast__container,原题中说的是class=toast__container,但实际是id而不是class)元素显示,2 秒后该提示框自动隐藏或者点击提示框上面的关闭按钮(class=toast__close)该提示框隐藏。使用 display 属性设置元素的显示隐藏。
完成后,最终页面效果如下:
代码:
// TODO:待补充代码 let timer; $(".card-body-option-favorite img").each((i,t)=>{ $(t).click(function(){ if ($(this).attr('src') === './images/hollow.svg') { // 切换图片路径 this.src = "./images/solid.svg" // 显示弹窗 $('#toast__container').show() // 添加定时器,两秒后关闭弹窗 timer = setTimeout(()=>{ $('#toast__container').hide() },2000) } else { this.src = "./images/hollow.svg" } }) }) // 点击弹窗的关闭按钮 $('.toast__close').click(function () { $('#toast__container').hide() if (timer) { clearTimeout(timer) } })
💠 乾坤大挪移心法(职业院校组)
这时一道很常见的循环调用的题,要求如下:
-
mentalMethod 需要返回一个函数,可以一直进行调用,但是最后一次调用不传参。
-
函数通过以下方式执行,返回结果均为 '战胜峨眉,武当,少林'(注意逗号为英文逗号)。
mentalMethod('峨眉')('武当')('少林')(); mentalMethod('峨眉','武当')('少林')(); mentalMethod('峨眉','武当','少林')();
代码:
function mentalMethod(...args) { // TODO 待补充代码 let a ='' a += args.join(',') let fn = function (...rest) { if (rest.length > 0) { // 如果原本a有值,需要在加新值之前添加一个,分割 a += a.length > 0 ? ',' + rest.join(',') :rest.join(','); // 继续返回fn这个函数 return fn }else { // 没有参数代表是最后一次调用,这时直接返回结果 return '战胜' + a } } return fn }
这题是利用了闭包,在外界调用fn函数时能够使用函数mentalMethod内层的变量。
💠 不能说的秘密(职业院校组)
题目要求实现一个随机密码生成器,完善 generatePassword.js 中的 generatePassword 函数,实现根据规则随机生成密码的功能。密码长度已由 input 框(id=passwordLength)的属性进行了限制最小 4,最大 20。
- 生成的密码必须包含已选中的选项且只能由已选中的选项组成。
- 特殊符号如下:!@#$%^&*(){}[]=/,. 。
- 本题封装方法时只需要考虑长度符合要求( 4-20 )且有已选中条件的情况,其他情况无需处理。
最终效果:
思路:
- 首先需要理解好题目的要求,密码长度最小为4,为什么最小为4呢?因为题目还要求生成的密码必须包含已选中的选项,而题中给的选项正好有4个,这一点很重要。
- 根据用户的配置生成一个字典数组,数组中存的是密码可能所含有的所有字符。
- 根据长度进行遍历,一次次的随机向字典数组里取一个字符添加到密码字符串中。
代码:
/** * @function_name generatePassword ->生成密码的函数 * @param {*} lower 是否小写 * @param {*} upper 是否大写 * @param {*} number 是否是数字 * @param {*} symbol 是否是特殊符号 * @param {*} length 密码长度 * @return {*} string */ function generatePassword(lower, upper, number, symbol, length) { //TODO:待补充代码 // 特殊字符 let sy = '!@#$%^&*(){}[]=/,.'; // 存放字典的数组 let arr = []; // 密码结果 let str = ''; // 向str中添加字符的函数,list代表字典。 function addStrItem(list) { // 表示从list中随机选一个字符添加到str中 str += list[Math.floor(Math.random()*list.length)] } // 添加大写字母 if (upper) { // 生成全部大写字母数组 // Array(26)表示生成长度为26的空数组 // fill用来向数组中填充内容,不填充内容是无法正常使用数组遍历的方法的 let upperList = Array(26).fill('').map((item,index) => { return String.fromCharCode(index + 65) }); // 添加到字典中 arr.push(...upperList) // 此时就在所有大写字母中随机选一个添加到str中,确保了该选项对应的值在密码中存在。 addStrItem(upperList) } // 添加小写字母 if (lower) { let lowerList = Array(26).fill('').map((item,index) => { return String.fromCharCode(index + 97) }) arr.push(...lowerList) addStrItem(lowerList) } // 添加数字 if(number) { let numberList = Array(10).fill('').map((item,index)=>index) arr.push(...numberList); addStrItem(numberList) } if(symbol){ letsymbolList = sy.split('') arr.push(...letsymbolList); addStrItem(letsymbolList) } // 添加剩余长度的字符 while (str.length
静态 String.fromCharCode() 方法返回由指定的 UTF-16 代码单元序列创建的字符串。
- 小写字母的 UTF-16 代码单元序列为97-122
- 大写字母的 UTF-16 代码单元序列为65-90
🔼 结语
至此,《 第十四届蓝桥杯(Web 应用开发)模拟赛 2 期 》的题解就结束了,这期模拟赛整体上来说并不算很难,但考察的知识点还是比较多的,特别是对基础知识以及常见算法的考察(相信你在做题的过程中也能察觉到),所以博主还是建议大家在做题的过程中好好总结,好好复习,祝大家都能在正式比赛中取得满意的成绩!
记录一下考试成绩,因各种原因导致显示的解题时间有误,本人实际是做了大概三个小时左右,大学组十个题满分才 150,最后得出的 178 分应该也是受解题时间的影响。
总结来说,比较耗时的题就是第 6 题了,大家可以注意一下,在正式比赛时做好规划。
如果本篇文章对你有所帮助,还请客官一件四连!❤️
-