我最早学前端的时候,一直以为 HTML、CSS、JS 三件套是“画网页”的,直到有个同学往我的输入框里粘了一小段 <script> 标签,页面瞬间弹窗、DOM 乱掉,我才意识到:只要页面里有用户输入的内容,前端就不只是“画”这么简单。这个标题里的 XSS,不是蹭安全热度的点缀,而是新手学完基础后最容易踩到、也最容易忽视的一道坎。
这篇笔记不是从零开始的语法词典,而是把 HTML 搭结构、CSS 做样式、JS 管交互这条主线走通之后,再顺手带大家认识 XSS 到底是怎么利用这三件套“钻空子”的。适合两类人看:一类是刚学完三件套基础语法、想找个完整学习路径把知识点串起来的新手;另一类是已经能写简单页面、但没认真想过“用户输入的数据为什么不能直接插入页面”的初级前端。作为一篇个人实战学习笔记,我会直接把学习过程中验证过值得记的东西、走过的弯路、还有练习 XSS 时该用的本地环境和思路分享出来,希望对你们有帮助。
1. 这套学习笔记到底在解决什么问题
先搞清楚 HTML、CSS、JS 三者的分工,很多学习笔记其实没把这层讲透。HTML 是页面内容的骨架,它负责“这里有一个标题、那里有一个输入框”;CSS 是皮肤,负责颜色、间距、布局;JS 是肌肉和大脑,负责监听用户操作、改变页面状态、和服务器交换数据。这三者不是一个先学完再学另一个的关系,而是从一开始就要在一个页面里混合着用。国内很多新手把 HTML 和 CSS 当成“静态页面”学完,再开始碰 JS,结果遇到 DOM 操作就懵,就是因为没有建立起“HTML 结构可以被 JS 动态改”的意识。
我建议在学习笔记里把三者的关系始终拉在一起:先写 HTML,再用 CSS 调整布局,最后用 JS 控制结构变化和交互行为。XSS 之所以放在这组话题里,是因为它本质上就是在“结构可以被动态改变”这个环节出的问题——当一段用户输入被当作 HTML 结构直接插入 DOM,页面就会失去对“内容”和“代码”的边界控制。
这套学习内容把 XSS 定位成“前端基础的一部分”,而不是“安全专家的领域”,这也是我在实际学习中体会最深的一点。如今前后端分离的架构越来越多,页面里基于 URL 参数、本地存储、接口返回值动态渲染 DOM 的场景极其普遍,前端代码本身就已经是 XSS 攻击发生和防御的第一道关口。如果只看 HTML/CSS/JS 不管 XSS,学到后面几乎必然要回头补课。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. HTML 与 CSS 打基础阶段的重点拆解
2.1 标准文档骨架:每个标签到底在干什么
很多人第一次搜 HTML 相关代码时,会看到一大段 < !doctype html> ... <meta charset ...> 这样的模板,然后直接复制粘贴,根本不管每一行是什么。直接背这套骨架本身没有意义,但理解它非常重要。
html复制<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>页面标题</title>
</head>
<body>
<!-- 页面可见内容 -->
</body>
</html>
<!doctype html> 的作用是告诉浏览器“请用现代标准模式解析”,没有它,老浏览器可能进入怪癖模式,盒模型、布局都会出问题;lang="zh-cn" 影响屏幕阅读器和翻译插件的语言判断;meta charset="utf-8" 防止中文乱码;meta name="viewport" 是移动端页面适配的起点。我建议动手一个字一个字敲一遍这套骨架,而不是依赖编辑器的自动补全,因为工作里你会频繁看到别人写出的不规范骨架,只有基础自己熟,出了问题才能一眼定位。
HTML 里另一个核心概念是“块级元素 vs 行内元素”。简单说,块级元素(div、p、h1、ul 等)默认独占一行,宽度撑满父容器;行内元素(span、a、strong 等)只占内容需要的宽度,多个行内元素可在同一行排列。这个差异直接决定了你用 CSS 写布局时选择什么方式,很多新手把 <a> 写进 <div> 后设宽高没反应,就是把行内元素属性记混了。
2.2 语义化标签与初学者最容易忽略的盒子模型
学习 HTML 时,很多讲义会提“语义化”,但没有强调语义化对 CSS 和 JS 选节点的后续影响。用 <header> <nav> <main> <section> <footer> 这些标签,结构清晰是一回事,更重要的是让 CSS 的标签选择器、JS 的 querySelector 有更明确的锚点。当然,语义化标签要配合标题层级使用,不要为了语义化把每个 div 都改成 section。
CSS 部分,盒子模型是关键。标准盒模型中,width 指 content 的宽度,padding 和 border 是在这个宽度基础上向外扩展的;而 box-sizing: border-box 会让 width 包含 padding 和 border。我学的时候一度不理解为什么移动端适配经常加这么一句:
css复制* {
box-sizing: border-box;
}
原因其实很简单:加了这行以后,你设置一个元素宽度为 50%,再给它加上固定 padding 或 border,它的总宽度不会超过父容器的 50%,布局不会无缘无故溢出。没有这行,50% + padding 就会超宽挤爆。这个理论好像很基础,但实际项目里大量“布局乱了”的报错都是从这里来的。
2.3 flex 布局中“子元素宽度自适应”的几种套路
标题相关热搜里有个很高频的问题:flex 布局子元素宽度自适应。我看到很多新手一上来把 flex: 1 当成万能写法,实际上它只是简写,完整展开是 flex-grow、flex-shrink、flex-basis 三个属性。
核心逻辑记住一句话:flex-grow 控制“剩余空间怎么分配”,flex-shrink 控制“空间不够时谁先缩”,flex-basis 控制“在分配剩余空间之前的主轴基础尺寸”。想让多个子元素在一行内按比例填满宽度,最常用的是:
css复制.item {
flex: 1; /* 等价于 flex: 1 1 0% */
}
但这里有个经典大坑:如果子元素里有一长串英文或一张大图片,即使设置了 flex: 1,内容也可能把元素撑开,导致宽度自适应失败。原因是 min-width 的默认值是 auto,而 flex 项目在主轴方向上的最小尺寸默认由内容决定。解决办法是给子元素补一个 min-width: 0(水平方向布局)或 overflow: hidden,让内容可以溢出或截断,flex 的宽度分配才能真正生效。
另外一个实用的套路是“等宽列表 + 最后一个元素不换行”这种场景:父容器用 flex-wrap: wrap,子元素用 flex: 0 0 calc(25% - 间距) 控制每行显示几个。这里 flex: 0 0 表示不放大、不缩小,配合 calc 算好间距,自适应成几列很容易。
2.4 兄弟选择器、旋转与字体处理的常见记忆点
选择器部分最容易让人卡住的是“上一个兄弟元素怎么选”。CSS 只有相邻兄弟选择器 + 和通用兄弟选择器 ~,这两个都是从当前元素“往后找”的,也就是说 A 元素在前、B 元素在后,你能通过 A + B 或 A ~ B 选到后面的 B,却没法用纯 CSS 从 B 反向选到前面的 A。这是很多新手以为 CSS 能力不足的瞬间,实际不用太纠结:可以调整 HTML 顺序再用 order 属性控制视觉顺序,或者改用父容器 flex-direction: column-reverse 倒排子元素顺序。我早期调一个手风琴菜单时在这里卡了大半天,后来发现用 JS 给前面的兄弟加类名才是正经解法。
旋转在 CSS 里其实不难,但要注意坐标系和旋转中心两个点。transform: rotate(45deg) 默认绕元素正中心旋转,transform-origin: left top 可以改成绕左上角旋转。热搜里“css 旋转代码”通常搜到的就是这两行的排列组合。
字体属性的记忆点则是 font-family 的顺序策略:先写期望字体,再写系统字体兜底,最后加通用字体族。例如 font-family: "PingFang SC", "Microsoft YaHei", sans-serif;,这样可以避免某个字体不存在时使用浏览器默认的奇怪字体。还要注意自定义字体通过 @font-face 引入,存在跨域和字体文件体积的问题,新手阶段优先使用系统字体栈是稳妥的选择。
2.5 关于原子化 CSS 和素材网站的个人看法
搜索词里还有“原子性 css”和“免费的 css 素材网站”,这里聊一下。原子化 CSS 是指 .text-red、.m-4、.flex 这样每个类只干一件事的思路,Tailwind CSS 是其中最成功的工具之一。刚开始学 CSS 时我不建议急着上原子化框架,因为原子化要求你先理解传统 CSS 的“选择器 + 声明块”,否则你只是在抄类名,并不知所以然;但学了一段时间后,原子化的思路确实能帮你控制样式污染,尤其是改别人代码的时候,工具类不会像后代选择器那样越界影响不相关的元素。
至于免费 CSS 素材网站,学习阶段可以用网上的按钮样式、卡片样式做参考,但不要直接整段抄进项目而不知道里面的 display: flex、transition、border-radius 是怎么回事。把素材当 API 参考,把自己写的 demo 当实验田,是我比较推荐的做法。
3. JavaScript 入门必须搞清楚的基础点
3.1 script 引入:位置、defer 与 async 的区别
JS 入门第一个真正影响实践的细节,不是变量类型,而是 <script> 标签放哪里。如果你把 <script> 放在 <head> 里,浏览器解析到它时会停下来先下载并执行 JS,此时 <body> 里的 DOM 还没解析出来,你用 document.getElementById 拿元素很可能拿到 null。早期教材让你把 script 放在 body 末尾,就是为了规避这个问题。现代做法是只在 head 里加 defer:
html复制<script src="app.js" defer></script>
defer 告诉浏览器:这个脚本先下载,但等 HTML 解析完成后再执行。“先下载、后执行”这个顺序是 defer 和 async 的本质差异。async 也是异步下载,但下载完立刻执行,所以执行时 DOM 不一定解析完,而且多个 async 脚本之间的执行顺序不保证。如果你的脚本不依赖 DOM、不依赖其他脚本,可以用 async 加快加载;如果是业务主逻辑,应该优先用 defer。我实际调试时碰到过一个“偶发拿不到元素”的问题,就是因为插件脚本用了 async,它在某些弱网环境下抢在 DOM 解析前执行了。
3.2 字符串与 DOM 操作:includes 和其他常用方法
“js判断字符串是否包含”也是新手高频搜索点。JavaScript 里最直接的是 includes:
javascript复制const text = 'Hello, XSS';
console.log(text.includes('XSS')); // true
注意 includes 区分大小写,如果需要忽略大小写,可以先 .toLowerCase() 再判断。替代方案 indexOf(...) !== -1 在老代码里很常见,二者选一个记就行,但要留意 includes 在部分旧环境(如很老的内置 WebView)里支持度不够。字符串处理还常用 startsWith、endsWith、replace、split、trim 等,配合起来处理用户输入会顺手很多。
DOM 操作里最基础的一条线是“查询元素—修改属性和文本—添加/删除节点”。查询方式 getElementById、querySelector 和 querySelectorAll 足够覆盖大部分需求;修改结构时有 innerHTML、textContent、createElement + appendChild 三条路径可选。其中 textContent 是纯文本赋值,任何标签字符都会被当成文本显示,而 innerHTML 会把字符串当 HTML 解析——这个差别就是后面 XSS 章节的伏笔。
3.3 宏任务与微任务:为什么要知道事件循环
搜索词里有“js宏”,其实是在说宏任务(macrotask)与微任务(microtask)。这也是我在学习笔记里特别加进来的一块:因为它是很多“代码执行顺序奇怪”问题的根源。我可以先给一个结论:每次事件循环都会先执行一个宏任务,宏任务执行完后会清空当前所有的微任务,然后才进入下一次渲染或取下一个宏任务。
javascript复制console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出顺序:1, 4, 3, 2
setTimeout 的回调是宏任务,Promise.then 的回调是微任务。整段同步代码执行完后,微任务队列先被清空,所以 3 比 2 先输出。新手不必把事件循环背得滚瓜烂熟,但需要理解:setTimeout 的 0 毫秒不代表“立刻执行”,它只是“尽快进入宏任务队列”。写交互代码的时候,如果发现 setTimeout 里的代码在期望时机后执行,优先怀疑是不是有某个微任务或前面耗时的同步任务拖慢了循环。
3.4 console 调试与 JSON.stringify 的一个常见坑
前端调试从 console.log 开始,这是所有人都绕不过的。但我在学习过程中有一个重要的心得:不要只打印变量本身,而要多打印“带标签的数据”和它的 JSON 形态。比如 console.log('用户点击数据', data),字段名和数据一起输出,排查效率立刻提升。浏览器 console 里的对象默认是“活引用”,你展开它时看到的可能是最新的属性值,而不是打印那一刻的值,所以排查循环引用或动态变化的对象时,直接看 JSON.parse(JSON.stringify(data)) 的快照,比展开对象更直观。
JSON.stringify 还有一个新手很少注意的表现:当数据里含 undefined、函数、Symbol 时,这些键会被直接跳过;如果是数组中的 undefined,会被转换成 null;遇到循环引用会直接抛错。所以用 JSON.stringify 做“深拷贝”并不是万能的,含函数和循环引用的对象用它就会出问题。我在做学习项目时习惯用 structuredClone 实现更完整的深拷贝,但需要确认浏览器支持情况。
4. 把三件套串起来的第一个小项目
4.1 选题与功能规划
不用一上来就做很复杂的管理后台,最适合串三件套知识的入门项目,是“留言墙”或者说“待办展示板”。因为它包含输入框、按钮、列表渲染,天然会用到 HTML 表单结构、CSS 布局与美化、JS 事件监听与 DOM 操作。而它带“用户输入可变内容”这个特性,又正好为后面引入 XSS 做一个再自然不过的铺垫。
功能可以先定义成两大块:
- 输入区:文本框加“添加”按钮。
- 展示区:以卡片列表展示输入内容,支持点击删除。
4.2 实现时怎么安排代码结构
代码可以分成三个文件:index.html 负责结构和语义,style.css 负责样式,app.js 是交互逻辑。HTML 里先写一个
