这套"HTML5测验一"不是书后习题那种填空选择,而是把日常开发里真正绕不过去的点拎出来过一遍:文档骨架、播放器兼容、Canvas绘图、综合动效。我见过太多人背了标签却写不出一个能在手机上正常跑的视频页,也见过初学者一碰到心形曲线就发怵。所以这套题没有死记硬背,只有"你平时到底有没有动手写过"。
先约定一下答题规则:每题先说场景,再给一个参考实现,最后留几个追问。建议你打开编辑器跟着敲一遍,不要只看不写。自评标准在后面,做完再看答案对不上也没关系,重要的是知道错在哪。
1. 发卷之前:这套"HTML5测验一"到底想考什么
HTML5本身是个很大的概念,从标签到API,从语义化到媒体处理,覆盖面太广。把这套题命名为"测验一",不是要考"HTML5规范里有多少个标签",而是想验证三件事:能不能写出结构干净的页面、能不能处理真实设备上的兼容问题、能不能用Canvas这类API做点看得见摸得着的东西。
1.1 为什么用"做项目"的方式出题
我见过很多简历写着"熟悉HTML5",结果让他写一个视频页面,连<source>多源都写不出来;让他做个爱心动画,只知道用GIF。原因是大部分教程都是"认识标签"级别,没有把知识点串成场景。
这套题的所有题目都有一个共同特点:它们都是你在接私活、做公司官网、写活动页时真正会碰到的需求。比如播放器兼容,你不处理就有人打开页面看不到视频;比如圣诞贺卡,你不做降级方案,低端安卓机上就是一片白屏。
所以别把它当考试,把它当一个"试试水"的项目合集。如果你能不看答案独立完成超过一半,那你的实战基础已经比大多数初学者强了。
1.2 答题约定与自评标准
建议的时间分配是:第一题15分钟,第二题20分钟,第三题40分钟,第四题30分钟。做完不算完,要把报错、异常、浏览器差异记录下来,这比代码本身值钱。
评分也不看"做没做出来",而是看三个维度:
- 结构是否语义化,有没有用对
header、main、section这类标签 - 兼容方案是否完整,不是只照顾Chrome
- 性能有没有考虑,比如粒子数量、动画帧率、循环里的开销
这三条其实也是正式项目里code review最常看的点。后面每题我都会给一个参考实现,但参考不等于唯一答案,你完全可以写出更好的方案。
2. 第一题:文档骨架与语义化结构
场景:你需要为一个本地活动页搭建最基本的HTML5骨架,要求语义清晰、移动端适配良好、SEO基础达标。请写出一个完整的页面开头部分,包含文档类型、语言声明、字符集、视口设置和基本语义化布局标签。
这道题看起来简单,但恰恰是很多人栽跟头的地方。大部分初学者会写<!DOCTYPE html>,然后就不管了,但里面藏着不少细节。
2.1 参考实现
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="本地HTML5活动页,包含视频、Canvas动画和贺卡组件"> <title>本地活动页</title> </head> <body> <header> <h1>站点标题</h1> <nav>导航链接</nav> </header> <main> <section> <h2>活动介绍</h2> <article>具体内容,独立成块</article> </section> </main> <footer> <address>联系信息</address> </footer> </body> </html>这段代码的核心不是标签数量,而是选对标签。header放页首信息,nav放导航,main是页面主体且只能有一个,section表示带标题的区块,article是能独立分发的完整内容,footer放页脚和联系信息,address专门放联系方式。
2.2 为什么语义化比标签万能更值钱
语义化标签不是给浏览器看的,主要是给三类"读者"看的:搜索引擎爬虫、读屏软件、以及三个月后回来看代码的你自己。
搜索引擎靠标签判断页面结构优先级,h1通常比div里加粗的文本权重高;读屏软件会跳着读nav、main,没有语义化,视障用户在你的页面上基本寸步难行。
实际开发里有一个细节:main标签在一个页面中只能出现一次,而section需要配标题。如果你写了一个没有标题的section,就算有语义标签,也不符合规范。HTML5的语义化不是拿标签当装饰品,而是让结构和内容对上号。
2.3 这道题最容易踩的三个坑
第一个坑:字符集声明位置太靠后。<meta charset="UTF-8">应该放在<head>的最前面、<title>之前,因为浏览器在解析HTML时如果前面字节有乱码风险,字符集声明越早越安全。
第二个坑:viewport写死缩放比例。content="width=device-width, initial-scale=1.0"是最保守的写法,不要随意加maximum-scale=1.0,那样会禁用用户缩放,对无障碍体验是减分项。
第三个坑:忘掉lang属性。很多人写<html>不带lang="zh-CN",结果浏览器翻译插件乱弹窗、读屏软件用英文音色读中文。这个属性随手写上,能省很多麻烦。
3. 第二题:浏览器对HTML5播放器的兼容支持,写一个不翻车的video
场景:产品要求页面嵌入一段mp4视频,需要在Chrome、Firefox、Safari和主流国产浏览器(同样是Chromium内核)上都能播放。请写出兼容方案,并回答:为什么同一个mp4文件有的浏览器能播、有的不能?
上一题考的是文档骨架,这一题考的是真实项目里最磨人的媒体标签兼容问题。"不同浏览器对html5播放器的支持"这个话题我至少被问过二十次,每次都是同一个原因:源文件编码格式不对。
3.1 参考实现
<video controls width="720" poster="video-cover.jpg"> <source src="meeting.mp4" type="video/mp4"> <source src="meeting.webm" type="video/webm"> <source src="meeting.ogv" type="video/ogg"> <p>你的浏览器不支持HTML5视频,请升级浏览器或下载视频观看。</p> </video>这段代码的核心是<source>多源机制。浏览器会从上到下找第一个能解的视频源,如果都不行,就显示<video>标签里的文字提示。
3.2 为什么同一个mp4有的浏览器放不了
视频能不能播,不只是看文件扩展名,还要看容器格式和编码格式。mp4只是容器,里面的视频流可能用H.264、HEVC(H.265)或AV1编码,音频可能是AAC或MP3。关键点在于,各浏览器对编码格式的支持并不一致。
| 浏览器 | MP4/H.264 | WebM/VP9 | Ogg/Theora |
|---|---|---|---|
| Chrome | 支持 | 支持 | 支持 |
| Firefox | 支持 | 支持 | 支持 |
| Safari | 支持 | macOS 11.3+ 部分支持 | 不支持 |
| 国产双核(Chromium内核) | 支持 | 支持 | 支持 |
所以常见的问题就是:源视频是H.265编码的mp4,移动端的安卓机大多是Chromium内核可以硬解,但桌面端的Safari对H.265的支持就非常看版本,播不了就黑屏。解决办法是压一版H.264 Baseline或Main编码的mp4,再配上WebM做备选,这样覆盖面最广。
3.3 播放器兼容里最容易忽视的细节
MIME type不能瞎写。type="video/mp4"对应H.264+AAC的mp4,如果源文件编码不对,光写对type也没用。另外,type属性不写也能播,浏览器会自己嗅探,但写了能帮助浏览器更快跳过不支持的源。
自动播放策略是另一个高发坑。用户一进页面就带声音自动播放,会被Chrome、Safari直接拦截。如果你确实需要在加载后播放,可以先把muted属性加上:
<video autoplay muted playsinline loop>playsinline在iOS Safari上尤其重要,不写的话视频可能自动进入全屏播放。这个属性是历史遗留的兼容性设置,但到今天依然有效。
3.4 浏览器之外的音频坑
视频之外,音频标签同样有坑。比如微信内置浏览器对Web Audio API的采样率兼容性很迷,同一个音频文件桌面端正常,安卓微信里播放会变速。这种问题调试起来非常痛苦,我的经验是:做媒体播放之前先确认目标机型,不要默认所有浏览器行为一致。
4. 第三题:Canvas爱心烟花特效——从数学公式到粒子系统的完整拆解
场景:活动页需要一个"点击后绽放爱心烟花"的交互特效。请用Canvas实现一个心形粒子动画:点击按钮后,粒子排列成爱心形状散开并回落。
这道题对没接触过Canvas的人来说会有点心理压力,但拆开看其实就三层:数学曲线生成点、粒子系统管理运动、渲染循环让画面动起来。
4.1 心形曲线从哪里来
网上流传最广的心形参数方程是:
x = 16 * sin^3(t) y = 13 * cos(t) - 5 * cos(2t) - 2 * cos(3t) - cos(4t)其中t的范围是0到2π。这个公式不是我发明的,是很多绘图工具都在用的经典曲线。把t等分成几百个点,每个点计算一个坐标,就得到了爱心的轮廓。
function generateHeartPoints(count = 120, scale = 8) { const points = []; for (let i = 0; i < count; i++) { const t = (i / count) * Math.PI * 2; const x = 16 * Math.pow(Math.sin(t), 3); const y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t); points.push({ x: x * scale, y: -y * scale }); } return points; }注意这里我把y取反了。原因是数学坐标系y轴向上,但Canvas的坐标系y轴向下,不取反爱心是倒着的。
4.2 粒子系统的最小实现
粒子系统听起来高大上,核心就是一个对象数组加上每帧更新。每个粒子记录自己的位置、速度、颜色和生命值,每帧先清屏,再更新位置,再绘制。
class Particle { constructor(x, y, vx, vy, color) { this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.color = color; this.age = 0; this.maxAge = 120; } update() { this.x += this.vx; this.y += this.vy; this.vy += 0.08; this.age++; } draw(ctx) { const alpha = Math.max(0, 1 - this.age / this.maxAge); ctx.globalAlpha = alpha; ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, 2, 0, Math.PI * 2); ctx.fill(); } }点击按钮时,把心形点集作为初始位置,每个粒子的速度朝外扩散一点,就形成了"爱心炸开"的效果。
btn.addEventListener('click', () => { const pts = generateHeartPoints(80, 8); for (const p of pts) { particles.push(new Particle( canvas.width / 2 + p.x, canvas.height / 2 + p.y, (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2, '#ff4d6d' )); } });4.3 渲染循环和性能控制
动画不能只用setInterval,一定要用requestAnimationFrame。它会在浏览器下一次重绘之前执行回调,帧率更顺滑,而且标签页切到后台会自动暂停,不会白白耗电。
function tick() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = particles.length - 1; i >= 0; i--) { particles[i].update(); if (particles[i].age > particles[i].maxAge) { particles.splice(i, 1); } } for (const p of particles) { p.draw(ctx); } requestAnimationFrame(tick); } requestAnimationFrame(tick);性能上最大的坑是globalAlpha和fillStyle的频繁切换。每frame设置一次其实还好,但如果你有几百个粒子,每帧都设置颜色和透明度就会产生明显开销。优化思路是提前按颜色分组,或者用ctx.globalCompositeOperation = 'lighter'做发光叠加效果的同时,减少阴影类的重操作。
4.4 Canvas高分屏模糊问题
这是移动端最常见的问题之一。CSS里的canvas宽高是逻辑像素,但手机屏幕是2倍甚至3倍物理像素,直接画会导致画面发虚。解决办法是让canvas的buffer尺寸和物理像素对齐,再通过scale把绘制坐标映射回来。
const dpr = window.devicePixelRatio || 1; canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr; ctx.scale(dpr, dpr);这个操作在做烟花、贺卡这类视觉效果时几乎是必须的,不然小屏手机上锯齿感非常明显。
5. 第四题:HTML5圣诞贺卡——把动效、音效、排版塞进一张卡片
场景:圣诞节快到了,你要用HTML5做一张可在微信里分享的贺卡。要求有飘雪效果、一颗可以闪动的圣诞树、点击铃铛发出声音,而且不能在低端手机上卡死。请给出你的技术组合和关键实现。
这道题是综合性最大的一题,也是热搜词里"html5圣诞贺卡"的实际场景。它不要求你用多复杂的技术,反而考验你怎么选型、怎么控制成本。
5.1 技术组合:不要什么都用Canvas
我看到很多人的第一反应是"只要动效复杂就上Canvas"。但贺卡这种页面,静态部分是绝大多数,飘雪才是动态部分,全屏Canvas反而浪费性能。
更合理的分工是:
- 圣诞树、铃铛、文字排版用HTML + CSS,动画交给CSS
animation - 飘雪效果用Canvas处理,因为雪花数量多、随机性强,CSS逐个写div到五十个以上就明显卡顿
- 音效用
AudioContext临时合成铃铛声,不需要额外音频文件
这种组合的好处是首屏HTML就能渲染出静态内容,CSS动画由GPU负责,Canvas只处理真正需要动态绘制的部分,性能压力散开了。
5.2 圣诞树和铃铛的CSS动效
圣诞树可以是一个简单的三角形叠加,用CSS渐变生成,不需要图片。铃铛可以用的小图标,加一个挂绳摇晃动画:
.bell { display: inline-block; transform-origin: top center; animation: swing 1.2s ease-in-out infinite; } @keyframes swing { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(15deg); } 75% { transform: rotate(-15deg); } }transform-origin设为顶部中心,动画才是"挂在绳子上晃"的效果,不然会变成原地转圈。这种细节是纯靠背属性背不出来的,得动手调。
5.3 飘雪效果的轻量实现
飘雪容易写,但也要控制数量。桌面端100片没问题,移动端建议30到50片就是上限,多了低端机直接发烫。
const canvas = document.getElementById('snow'); const ctx = canvas.getContext('2d'); const flakes = Array.from({ length: 40 }, () => ({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, r: Math.random() * 3 + 1, speed: Math.random() * 0.8 + 0.3, wind: Math.random() * 0.4 - 0.2 })); function drawSnow() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#ffffff'; ctx.globalAlpha = 0.8; for (const f of flakes) { ctx.beginPath(); ctx.arc(f.x, f.y, f.r, 0, Math.PI * 2); ctx.fill(); f.y += f.speed; f.x += f.wind; if (f.y > canvas.height) { f.y = -5; f.x = Math.random() * canvas.width; } } requestAnimationFrame(drawSnow); } drawSnow();这里有个小技巧:把雪花的初始y坐标随机铺满整个画布,而不是从顶部出发,不然刚进入页面时雪花会一窝蜂从顶部往下掉,观感很差。
5.4 用AudioContext合成铃铛声
有些人一听到音效就找mp3文件,其实一个简单的铃铛声不用采样,用Web Audio API自带振荡器就能做。核心思路是高频正弦波加上快速衰减的增益包络,听起来就有"叮"的质感。
function playBell() { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = 1568; gain.gain.setValueAtTime(0.5, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.8); osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.8); }注意AudioContext在部分浏览器里第一次点击时是suspended状态,需要先resume()再播放,否则不响。这也是不少人做完音效后发现没声音的原因。
5.5 微信内置浏览器的降级策略
贺卡一般是在微信里打开的,微信内置浏览器基于Chromium内核,整体兼容性不错,但有两个点要注意。第一是Canvas的devicePixelRatio可能和普通浏览器不一致,建议动态获取;第二是全屏Canvas滚动时,要监听resize和orientationchange,重新设置canvas宽高,不然横竖屏切换后画面会拉伸。
更保险的做法是:如果检测到设备很旧,直接把雪花数量减半,或者干脆用CSS动画做少许雪花。对用户来说,看到的是一张不卡顿的贺卡,而不是完美的物理模拟。
6. 收卷之后:成绩对应的能力画像与进阶方向
做到这里,这套"HTML5测验一"的核心题目就过完了。虽然名字叫测验,但它并没有一个标准分数线,我更愿意把答题情况分成三档:能独立写完前两题,说明页面结构与媒体处理的基本功是有的;第二题开始卡壳,大概率是平时只在Chrome里调试,没有养成多浏览器验证的习惯;第三题靠查资料做完,说明Canvas基础概念能理解,但缺少对数学曲线到坐标映射的练习,多画几个图形就能补上;第四题能跑起来但掉帧,那么你缺的不是HTML5知识,而是前端性能优化的sense。
我自己做这类动效页面的经验是:先让功能跑通,再做视觉优化,最后做性能优化。顺序反了会非常难受,比如你先把粒子加了发光阴影效果,结果发现掉帧严重,还得回头删代码。
下一步的练习方向可以这样走:把第三题的爱心烟花改成跟随鼠标发射的流星,把第四题的飘雪改成可以响应触摸的雪花堆叠,都是很好的练手项目。如果想让手更稳,再写一遍这些代码时,试着不参考任何资料,遇到卡住的API先查MDN,别急着看别人的示例。这样写完,你对HTML5的实际掌握就不是"知道",而是"会做"了。