1. 今天到底学了什么:一份前端三件套的复盘清单
今天把HTML、CSS、JS三件套从头到尾过了一遍,从上午看基础语法、下午做交互demo,到晚上搭了一个植物大战僵尸的页面框架,收获比预想中大很多。HTML这边顺便补了个冷知识:规范的后缀是.html,htm是老DOS时代流传下来的写法,浏览器照样认,但新项目建议统一写.html。CSS这边重点啃了样式引入方式、rem/px/em单位换算、图片和文字一行、涟漪光圈、字体渐变这些高频操作。JS这边花了最多时间在函数、事件对象、字符串判断、数组取值、URL校验,还有几个正经项目里经常踩的坑。
这篇小结不是把笔记搬出来,而是把所有知识点按“为什么这么做”重新捋了一遍。适合三类人:刚学完HTML基础、正在串CSS和JS的新手;入职前端不久需要补基础细节的开发者;以及想用植物大战僵尸这种经典案例练手、体验完整开发流程的学习者。不管你是哪种,建议看到代码块就动手敲一遍,别光复制,很多细节是敲过才会注意到的。
1.1 为什么HTML、CSS、JS要放在一起学
单独学HTML的时候,你会觉得这只是一堆带尖括号的标签,div、span、ul、li排布起来非常枯燥。单独学CSS,又觉得只是在调颜色、调字体。单独学JS,脑子里全是抽象的函数和对象。但把三者拼在一起,本质上就是一个"盖房子"的过程:HTML是承重结构和墙体,每个标签是一间房、一扇窗;CSS是装修,决定墙面刷什么颜色、沙发摆哪边;JS是水电和智能系统,负责开灯、开关门、响应你的操作。没有结构的房子不存在,没有装修的毛坯房能住但很简陋,没有水电的房子完全没法正常生活。
所以今天的学习顺序我没有按教科书那样"先学一个月HTML、再学一个月CSS",而是先快速过一遍HTML和CSS的基础标签,立刻进入JS逻辑,再用小项目把三者揉在一起。实际开发里写一个页面从来都是三者混着来:先在HTML里搭结构,CSS调样式,JS改DOM或者发请求。学习阶段尽早习惯这种混写,后面看别人的代码就不会发怵。
1.2 今天拆解的三个实际场景
为了让今天学的知识有个落点,我给自己定了三个小场景,正好把高频热词都串了起来。第一个是"图片和文字一行"的卡片布局,这是CSS高频题,涉及flex布局、align-items、文本垂直居中。第二个是"省市区三级联动",涉及JS的数组处理、事件监听、DOM动态渲染,也是面试高频题。第三个是"iframe里点击按钮关闭并刷新父页面",涉及JS的父页面通信和window对象。
三个场景覆盖的知识点也很清晰:
| 场景 | 涉及核心知识点 | 今天解决的主要问题 |
|---|---|---|
| 卡片布局 | flex、布局、单位、字体裁剪 | 图片文字一行、容器文本位置、rem/px/em |
| 三级联动 | 数组、事件、DOM | 数组取出、字符串判断、事件对象 |
| iframe父子页 | window、事件、资源加载 | 父页面刷新、JS引入方式与阻塞 |
2. CSS部分:样式引入、单位、布局与特效的实操细节
CSS这块今天主要啃了五个点:样式引入方式、单位换算、图片文字一行和容器内文本定位、字体渐变和涟漪光圈特效、hover鼠标移入交互。每一个单独看都不难,但组合在一起,就是页面质感的分水岭。
2.1 样式引入方式:外联、内嵌、行内,CSS文件里到底要不要写style
先解决热搜里那个高频疑问:"css文件需要写style吗"。答案很明确:不需要。外联CSS文件本身只放选择器和规则,写成.class { color: red; }这种形式,不需要也不可以用<style>包裹。<style>是HTML里的标签,用来在HTML文件内部包裹CSS代码。如果你在.css文件里写了<style>,浏览器只会把它当成无效文本,前面的样式全部不生效。
实际开发中样式引入方式有三种,今天都试了一遍。外联式最推荐,在HTML头部写<link rel="stylesheet" href="style.css">。它能利用浏览器缓存,多个页面共用一套样式,改动也只改一个文件。内嵌式是把<style>写在HTML的<head>里,适合演示、单页小工具、邮件模板这类场景,写起来快,但没法复用。行内式是直接在标签上写style="...",优先级最高,但一旦批量修改会非常痛苦,只适合微调。
这里必须提醒优先级问题:行内样式 > ID选择器 > 类选择器 > 标签选择器 > 继承样式,!important能直接压过行内样式,但滥用会让自己后期排查起来想撞墙。今天做涟漪光圈时,我就差点在多个元素上同时加!important,收手早,不然下午全耗在样式覆盖上了。
2.2 单位换算:px、em、rem、rpx到底怎么选,1.5rem等于多少
热搜里"css 1.5rem rpx"、"css rem px 桌面端"其实问的是同一类问题:这些单位到底怎么换算。今天我把四兄弟全捋了一遍。
px是最熟悉的,1px就是屏幕上一个物理像素点,桌面端UI设计稿基本都用px。em是相对单位,相对当前元素的font-size,比如父元素字号16px,子元素写margin: 2em就是32px。麻烦的是em会随嵌套层层叠加,父子都设了不同字号时很容易算乱。rem相对的是根元素<html>的font-size,不随嵌套变化,做全局缩放特别方便。rpx则是微信小程序的单位,设计稿宽度750时,1rpx等于屏幕宽度的1/750,和网页开发不是一套体系。
具体到桌面端网页,我今天的做法是:在html上设font-size: 16px,常规布局和字号用px,需要响应式缩放的模块用rem。比如1.5rem,只要根字号是16px,就是1.5 * 16 = 24px。如果想让全局字体一键缩放,直接在媒体查询里改html的font-size,所有rem单位都会跟着变。这个思路对学习阶段的同学理解"响应式"很有帮助。
2.3 图片和文字一行、容器内文本位置调整的三种方案
热搜里"图片和文字一行css"是新手问得最多的问题,今天我在卡片布局里实测了三种方案,直接说结论:首选flex。
<div class="card"> <img src="avatar.png" alt=""> <span>今日学习小结</span> </div>.card { display: flex; align-items: center; gap: 8px; }flex一两行属性就搞定,而且不依赖图片尺寸。第二种是老式的inline-block + vertical-align: middle,也能实现,但图片和文字之间有默认白边,需要额外清空格和设置font-size: 0,麻烦得很。第三种适合单行文字垂直居中,给容器设line-height等于容器高度,比如高度40px就写line-height: 40px,简洁但只对单行有效。
容器里文本位置的调整逻辑也一并说掉。水平位置用text-align: left/center/right,垂直位置单行用line-height,多行建议用flex父容器加align-items,或者给容器加padding做微调。今天踩了一个小坑:flex容器里子元素太多时,justify-content和align-items容易记混。前者控制主轴方向,后者控制交叉轴方向,先想清楚主轴是横是竖,再下手就不会错。
2.4 字体渐变、删除线、涟漪光圈扩散:三个即学即用的CSS特效
热搜里"css 字体渐变"、"css 删除线"、"css涟漪光圈扩散"都是视觉效果问题。字体渐变其实不是给文字直接上渐变,而是把渐变背景裁剪到文字上,核心代码就三行:
.gradient-text { background: linear-gradient(90deg, #f60, #06c); -webkit-background-clip: text; color: transparent; }关键点是background-clip: text配合color: transparent,文字区域显示背景渐变色,其余地方透明。注意,之后如果又给这个元素设置了背景色或color,渐变效果可能会被整个覆盖。删除线更简单,text-decoration: line-through一行搞定;如果带语义,可以用<del>标签,不过它只对文本生效,对块级容器无效。
涟漪光圈扩散今天做的是按钮hover效果,原理是给元素加伪元素,让一个圆形从中心放大并淡出。核心是一个动画循环,改变transform和opacity:
.ripple { position: relative; overflow: hidden; } .ripple::after { content: ''; position: absolute; width: 200px; height: 200px; border-radius: 50%; background: rgba(255, 255, 255, 0.5); transform: scale(0); opacity: 1; transition: transform .6s, opacity .6s; } .ripple:hover::after { transform: scale(2); opacity: 0; }做这类特效最容易忽略的是overflow: hidden,不加的话圆圈会溢出容器边缘。想实现"连续波纹",把transition换成animation,再用animation-iteration-count: infinite循环即可。
2.5 CSS鼠标移入事件:hover的玩法与局限
热搜"css 鼠标移入事件"其实就是:hover。纯CSS层面,鼠标移入移出都可以通过hover改变样式,再配合transition做平滑过渡。今天做卡片时加了一个悬浮抬升效果:
.card { transition: transform .3s, box-shadow .3s; } .card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); }这类交互写起来很顺手,但一定要知道hover的局限。它只能响应"移入"和"移出"两种状态,没法判断停留了多久,也没法做节流,更没法绑定复杂逻辑。今天做三级联动的时候,鼠标移到一个省上要联动显示对应市,靠CSS完全不行,必须交给JS的mouseenter/mouseleave事件。选择方案的标准很简单:只是视觉效果用CSS,涉及数据判断和复杂逻辑就必须上JS。
3. JS部分:函数、事件、字符串与数组的高频操作
CSS可以让页面好看,但要让页面"动起来",核心还是JS。今天下午大部分时间都泡在JS里,从函数是不是对象,到event对象里的target和currentTarget,再到字符串判断、数组取值、URL校验、脚本加载,一个个过。
3.1 函数是对象吗?从函数声明到箭头函数的this指向
热搜里"js中函数是对象吗"是个很经典的面试题。答案是:函数也是对象,它是Function构造函数的实例,可以被赋值给变量、作为参数传递、作为返回值返回,这就是所谓的"一等公民"。
写函数有三种常见方式:函数声明function foo() {},函数表达式const foo = function() {},箭头函数const foo = () => {}。三者之间最大的区别之一是this指向。普通函数的this在调用时确定,谁调用了它this就是谁;箭头函数的this在定义时确定,继承外层作用域。今天我踩了一个很直观的坑:给按钮绑点击事件时,用普通函数写回调,里面的this指向按钮;想改成箭头函数拿外层上下文,结果this完全变了。理解了这一点,比背十遍八股都有用。
const btn = document.getElementById('btn'); btn.addEventListener('click', function() { console.log(this); // 按钮元素 }); btn.addEventListener('click', () => { console.log(this); // 外层this(比如window) });另外看到热搜里有"js void",这里顺带补一嘴:void 0其实就是undefined的另一种写法,老代码里常见的javascript:void(0)就是让链接不产生跳转,遇到这类写法不会懵。
3.2 事件中的event:target和currentTarget别再混淆
浏览器在事件触发时会生成一个event对象,参数、属性一大堆,今天只挑最常用的说。最容易混淆的是event.target和event.currentTarget。target是真正触发事件的元素,currentTarget是当前事件绑定的元素。举个例子,事件委托里给一个ul绑定点击,点li时target是那个li,currentTarget是ul。想判断点中的具体是哪个子元素,只能拿target。
基于这个特性可以做事件委托:把事件统一绑到父容器上,通过target判断子元素。这样动态新增的节点也能被响应,不用反复绑定。还有一个高频问题是preventDefault和stopPropagation的区别。前者阻止默认行为,比如a标签跳转;后者阻止事件继续冒泡到父元素。今天做植物大战僵尸的时候,如果忘记stopPropagation,点击一个植物卡片会同时触发背景的种植逻辑,位置就全乱了。
3.3 字符串包含判断和忽略大小写:includes、indexOf、正则怎么选
热搜里"js判断字符串是否包含"和"js 忽略大小写"可以放一起讲。判断包含的核心API有includes和indexOf。includes直接给布尔值,语义化更强;indexOf返回位置,-1表示不存在,老代码里更常见。search和match则要配合正则用。
忽略大小写最常见的做法是统一转小写再比较:
const str = 'HelloWorld'; if (str.toLowerCase().includes('hello')) { console.log('包含,忽略大小写'); }或者用正则加i修饰符:/hello/i.test(str)。两种写法在实际开发里都够用。今天写三级联动筛选城市时,我用includes做模糊匹配,搭配toLowerCase处理用户输入的大小写,效果很稳。需要注意的坑是includes不会做类型转换,数字判断时先转成字符串再比。
3.4 数组取出与三级联动:从JSON字符串解析到动态渲染
热搜里有条看着很奇怪的:"["1","2"]" js怎么取出数组。其实这串东西如果直接从接口返回,前端拿到的多半是字符串,得先用JSON.parse解析成真正的数组,才能用[0]、map、forEach这些操作。
const raw = '["1","2"]'; const arr = JSON.parse(raw); console.log(arr[0]); // "1"如果后端返回的是复杂嵌套数据,parse之后要注意数组里可能还有对象,操作时用解构赋值会清晰很多。今天做的三级联动就是一个典型的"数据驱动渲染"结构:先定义省份、城市、区县三层数据,监听省下拉框的变化,动态渲染对应的市选项;市变化后再渲染区县。核心代码简化后是这个样子:
citySelect.addEventListener('change', () => { const cities = data.provinces.find(p => p.name === provinceSelect.value).cities; citySelect.innerHTML = cities.map(c => `<option>${c.name}</option>`).join(''); districtSelect.innerHTML = '<option>请选择</option>'; });这类联动逻辑本身不难,难点在于数据层级设计和事件解绑。如果反复触发change,很容易出现多个监听器叠加,导致选项重复。简单粗暴的办法是每次更新前先innerHTML = ''清空,比一个个移除监听器省心。
3.5 URL校验、生成Word文档、el-input-number取值:三个实际开发场景
热搜里"js验证url有效性"、"js生成word文档有哪些js库"、"js 获取值 el-input-number"都属于实际项目场景题。
校验URL我推荐用new URL()配合try/catch,比纯正则可靠得多:
function isValidUrl(str) { try { new URL(str); return true; } catch { return false; } }这个方案能自动处理协议缺失、非法字符等奇葩输入,因为浏览器自己就有一套完整URL解析逻辑。要注意的是new URL对相对路径会报错,想支持相对路径得先补一个基地址。
生成Word文档的JS库,目前社区里比较常用的是docx库,它能直接用JS构建段落、表格、标题并导出为.docx文件。另一个思路是html-docx-js,把HTML片段转成Word可识别的格式。如果只是临时导出,还可以把HTML内容转成Blob,设置type: application/msword,用a标签触发下载,虽然生成的不是标准docx,应急完全够用。
el-input-number是Element Plus的数字输入框组件。取值方式是在组件上绑v-model,在@change事件里拿到当前值。今天有个坑:这个组件不同使用方式可能返回字符串或数字,初始值为空时返回undefined或null,所以取值时最好统一做一次Number()转换和判空,避免直接拿去参与计算。
3.6 JS引入方式、加载性能与第三方源风险
热搜里"js引入"看着简单,实际大有讲究。常规方式是在HTML里写<script src="app.js"></script>,但这样会阻塞页面渲染,脚本下载和执行期间用户看到的是白屏。优化手段主要有两个:defer让脚本在HTML解析完后再执行,多个defer脚本按顺序执行;async是下载完立刻执行,不保证顺序,适合独立脚本。
<script src="app.js" defer></script> <script src="analytics.js" async></script>模块化引入则是import/export,现代浏览器原生支持<script type="module">,适合组件化和多文件工程。今天看到热搜里有一个带第三方站点链接形式的jquery引用,这里多说一句:引用任何第三方JS都要搞清楚来源和可信度。把别人服务器上的脚本直接引到自己页面,等于把一部分执行权交给对方,一旦对方改脚本或下线,你的页面就会莫名出问题。前端引入外部库的正确做法是下载到本地、锁版本,而不是裸引用一个不熟悉的URL。
3.7 进阶话题避坑:JS八股、宏任务微任务、iframe父子页操作
"js八股"泛指面试高频题,今天主要复习了闭包、原型链、作用域这几块。闭包的本质是函数能记住自己定义时的作用域,即使外层函数已经返回,内层函数依然能访问外层变量。面试代码题考得非常多,实际开发里的防抖节流、私有变量也都基于它。原型链则是对象通过__proto__找到构造函数原型对象的链,最终指向Object.prototype。
宏任务和微任务今天也专门跑了几个例子。简单记:同步代码先执行,微任务(Promise.then、MutationObserver)会赶在宏任务(setTimeout、setInterval、I/O)之前清空。所以setTimeout(() => console.log('a'), 0)和Promise.resolve().then(() => console.log('b'))同时出现时,先打印b后打印a。这个话题看起来偏理论,但排查异步执行顺序全靠它。
iframe父子页操作在"iframe+关闭+jquery+并刷新+父页面+js"这条热搜里体现得很典型。子页面里拿父页面对象是window.parent,刷新父页面可以直接window.parent.location.reload(),想关闭当前iframe容器则取决于父页面的具体结构,常见做法是父页面提供一个全局函数,子页面调用它。需要注意跨域限制:同源下这些操作畅通无阻,跨域时很多属性会被拦截,这时候就得靠postMessage通信。
还有热搜里"js 判断最后一个div元素",这个在CSS里用:last-child或:last-of-type,在JS里先document.querySelectorAll('div')拿NodeList,再用divs[divs.length - 1]取最后一个。NodeList不是数组,但可以用Array.from()转成数组再调pop(),两种方法都行。
最后补一句关于"js反爬实战""js逆向"的边界提醒:学习JS的调试方法、理解运行机制是正经技能,但用在别人网站上的破解、绕过风控就超出边界了,既不道德也违反用户协议。想练手就对自己写的页面调试,用正规的学习项目验证技术,这个原则希望大家记着。
4. 实战项目:用HTML+CSS+JS复刻植物大战僵尸的经典关卡
白天拆了一堆知识点,晚上把它们焊接到一个完整项目里。我选的练手项目是植物大战僵尸的页面版,热搜里出现"植物大战僵尸 html 完整代码"说明想玩这个的人不在少数。我不会贴几百行完整代码,而是把实现思路和关键代码讲透,照着思路写,效果会比直接复制好得多。
4.1 为什么选它做练手项目
植物大战僵尸页面版是一个非常典型的小游戏,它几乎覆盖了今天学到的所有内容:HTML负责布局草地和卡片,CSS负责样式、布局、特效,JS负责种植逻辑、僵尸移动、碰撞检测和游戏循环。难度比纯静态页面高一截,但拆开看每一块都不复杂,非常适合在学习阶段建立"完整项目"的感觉。
很多教程会让你直接下载现成代码,我建议不要。手动实现一遍,你会经历结构设计、样式调整、逻辑调试的完整闭环,这个过程带来的收获远大于"能跑"这个结果。
4.2 问题拆解与整体布局
游戏核心可以拆成三块:草皮网格(5行9列)、植物卡片区、僵尸出现与移动。用HTML把网格铺出来,每格是一个div,按顺序排列。CSS负责让每个格子大小一致,比如宽80px、高90px,整个战场画布宽1440px、高810px时正好放9列,视觉效果接近经典版本。僵尸从右侧出生,向左移动,遇到植物后停下攻击,植物被吃光后继续前进。
这种拆解方式一句话总结:先把"有什么"列清楚,再想"怎么摆",最后想"怎么动"。一切页面项目都适合这个套路。
4.3 核心逻辑实现:植物种植与碰撞检测
JS部分我用一个plants数组保存所有植物,每个元素包含row、col、hp、type。点击格子时,把植物对象push进数组,同时在对应格子渲染一个向日葵或豌豆射手。僵尸另起一个数组,每个僵尸有x、row、hp、speed,用requestAnimationFrame驱动整体移动和碰撞检测。
碰撞检测的思路很简单:遍历僵尸,和当前行所有植物做矩形相交判断。如果相交,僵尸停止移动并扣植物血,血量归零移除植物;如果没有相交且僵尸x小于0,游戏失败。这个逻辑比想象中容易写:
function update() { zombies.forEach(z => { if (!hitPlant(z)) z.x -= z.speed; }); requestAnimationFrame(update); }requestAnimationFrame是浏览器自带的帧循环,比setInterval更平滑,刷新率自动匹配屏幕。今天跑起来后帧率很稳,基本可以保持在60fps。
4.4 调试踩坑记录
第一个坑是点击穿透。点击植物卡片时,事件冒泡到了草皮格子上,同时触发了种植,导致误操作。解决办法是给卡片点击事件加stopPropagation(),并在格子点击逻辑里判断当前格是否已有植物。
第二个坑是僵尸移动速度。一开始用z.x -= 1,在1440px宽的画布上走完要太久;调成1.5后节奏舒服多了。调试这类参数时,我的方法是在页面顶部加一个实时状态条,直接显示僵尸数量、植物数量、帧率,边看边调。
第三个坑是内存消耗。如果每一帧都重新创建DOM节点,页面会越来越卡。今天尝试了"对象池"思路:僵尸被消灭后不立刻销毁DOM,而是挪出屏幕外复用,后续新僵尸直接复用节点。这个小优化值得所有做页面游戏的人学习。
5. 今日踩坑记录与常见问题速查表
最后把今天的坑和热搜里的高频问题整理成一个速查表,以后忘了直接翻。
5.1 常见问题速查表
| 问题 | 原因 | 解决办法 |
|---|---|---|
| CSS文件里写了style样式不生效 | .css文件不需要style包裹 | 外联文件直接写选择器;只有HTML内嵌才用style |
| 1.5rem到底是多少px | rem相对根字号 | 根字号16px时1.5rem=24px |
| 图片和文字对不齐 | 默认基线对齐 | 父容器flex + align-items: center |
| 字体渐变不显示 | 缺少background-clip和透明色 | 加 -webkit-background-clip: text; color: transparent |
| 涟漪溢出容器 | 伪元素超出边缘 | 父容器加overflow: hidden |
| event.target拿不到预期元素 | target和currentTarget混淆 | target是触发元素,currentTarget是绑定元素 |
| 字符串包含判断不忽略大小写 | 大小写敏感 | toLowerCase()后includes,或正则/i |
| 三级联动选项重复 | 监听器重复绑定 | 每次更新前先清空innerHTML或解绑事件 |
| el-input-number拿到的不是数字 | 返回字符串或undefined | 用Number()统一转换并判空 |
| iframe无法刷新父页面 | 跨域限制 | 同源用window.parent;跨域用postMessage |
| 动态新增节点点不动 | 事件没有委托 | 把事件绑定到父容器,用target判断 |
5.2 独家避坑技巧与学习建议
根据今天的实操,几条建议值得单独提出来。第一,CSS优先级是新人最常翻车的点,遇到"改了样式没反应",先检查是不是被行内样式或更高优先级覆盖,而不是盲目加!important。第二,flex布局的justify-content和align-items一定要分清主轴和交叉轴,画个箭头比死记快。第三,事件委托能解决大部分动态列表的绑定问题,遇到"新添加的元素没反应"先想委托。
第四,调试前端页面不要只用console.log,多用console.table看数组、用debugger打断点、用Elements面板实时改样式,效率会高很多。第五,写植物大战僵尸时我感受最深的一点:先写能跑的最小版本,再逐步加功能。第一版只有一个向日葵和一个僵尸,跑通之后,再加射击、加不同僵尸,每一步都有反馈,心态也稳。
关于热搜里出现的"洛雪音乐导入源.js"这类第三方脚本,也顺便表个态:这类脚本本质是收集网上各种音乐接口,版权和稳定性都存在隐患,只建议在个人学习环境里研究代码写法,千万不要往正式项目里放。js逆向相关的内容同理,学技术原理没问题,但别用在未经授权的系统中。
提示:网上流传的各种"一键导入源"工具,本质都是在收集第三方接口,来源不明、稳定性无保证。学习JS可以研究它的实现思路,但涉及版权和合规风险的部分,心里要有根弦。
5.3 今天学习节奏的个人体会
最后讲点今天最真实的感受。今天唯一的遗憾是没有一开始就写代码,上午花了太多时间做笔记,下午发现很多"我以为会了"的知识点,实际一写就露馅。比如图片文字一行,看教程觉得简单,自己写时忘了处理对齐,折腾了好久才想起来用flex。这恰恰印证了那句话:看会不算会,能写出来才算。
如果让我给今天的学习节奏打个分,我会打8分。少掉的2分,是因为中间被热搜里的各种名词带偏了几次,比如js散度、mc路这些,后来才想明白有些根本不是前端主线的概念,不用在这个阶段深究。学习前端最怕的不是学不会,而是被信息洪流带着到处跑。今天能坚持把植物大战僵尸的框架做出来,赢在"每一段知识都有落点"。建议你下次学习时,也给自己找一个能把这些知识点串起来的项目,哪怕只是一个卡片、一个表单,也比漫无目的地刷教程强得多。