1. 写在DAY7开头:为什么这一天值得单独写一篇
先交代一下背景。我在给自己的前端进阶计划做固定打卡,DAY1到DAY6基本都在啃零散的知识点:HTML语义化、CSS盒模型、JavaScript闭包、事件循环、Promise、Fetch API,每天一个主题,学完就过。到了DAY7,我本来想照旧推进新内容,但回头看了一遍前六天的笔记,发现一个很尴尬的问题——知识点记得挺清楚,但让我拿这些知识去"做点东西",大脑居然是一片空白。
这种感觉就像背了一整本游泳手册,下水还是会呛水。DAY7我决定换个策略:不学新东西,把前六天学的东西全部串起来,做一个综合应用。目标很朴素——用原生HTML、CSS、JavaScript完成一个带天气数据展示的前端小应用。不依赖框架,不用构建工具,把最底层的东西先走一遍。这篇文章就是DAY7的完整复盘:学什么、怎么做、遇到哪些坑、最终代码长什么样。
如果你也正处于"基础学了不少但不会用"的阶段,这篇应该能对得上你的胃口。我会把整个思考过程和踩坑记录都写出来,包括为什么这样设计、每段代码解决什么问题、哪些写法是经验之谈而不是标准答案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. DAY7的复盘:前六天到底攒下了哪些家底
2.1 HTML部分:不是写过标签就叫会了
DAY1我学的是HTML语义化,当时觉得特别简单,不就是div、span、h1到h6、p、a这些嘛。但等我真的要做一个完整页面的时候才发现,语义化的意义不是"让代码好看",而是让页面结构本身就有逻辑。
比如今天我做的天气卡片页面,头部区域用header表示,主要内容用main,底部放版权信息用footer。中间的数据展示区,我用了section加上aria-label来描述它的用途。这些标签浏览器默认样式几乎没差别,但对屏幕阅读器、搜索引擎、以及未来接手你代码的同事来说,差别是巨大的。
另一个容易被忽略的是meta标签。我今天在页面里写了viewport设置:
html复制<meta name="viewport" content="width=device-width, initial-scale=1.0">
没有这行,手机浏览器就会用默认的980像素宽度渲染页面,然后整体缩小显示。加上这行,页面才能自适应屏幕宽度。这个知识点DAY1学过,但如果今天不看手机端效果,我根本不会意识到它的重要性。
2.2 CSS部分:布局方案选择是真正的分水岭
DAY2和DAY3我分别学了Flexbox和Grid。学的时候觉得这两个东西挺像的,都能做布局,但在实际应用里它们的定位完全不同。
我做天气卡片应用的时候,整体页面结构是一个纵向单列布局,导航栏在顶部,内容区在中间,底部是版权。这种从上到下的排列,用普通的块级元素就够了,不需要Flex。但内容区里有多张天气卡片横着排列,每张卡片内部又有图标、温度、描述信息需要垂直居中——这里就是Flexbox的主场。
代码很直观:
css复制.cards-container {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
}
.weather-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 180px;
padding: 20px;
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
Grid我这次没用到,因为页面布局的"二维特性"不强——没有那种同时需要控制行和列的复杂区域划分。但我知道,下次做仪表盘或者后台管理界面的时候,Grid会是比Flex更合适的工具。选型逻辑很简单:一维排列用Flex,二维网格用Grid,两者不冲突。
2.3 JavaScript部分:DAY7应用里真正跑起来的知识
前六天JavaScript的内容占了三天,分别是变量与数据类型、函数与作用域、异步编程。今天全部派上了用场。
异步编程是我今天最深的体会。天气数据需要通过Fetch API从远程接口获取,这个过程是异步的。如果不理解promise和async/await,拿到数据之前页面就会渲染空内容,或者更糟——直接报错。
我这个应用的数据获取逻辑是这个样子:
javascript复制async function fetchWeatherData() {
try {
const response = await fetch('https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074¤t_weather=true');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('获取天气数据失败:', error);
throw error;
}
}
这个接口是Open-Meteo,免费、无需API Key,很适合前端练手。注意我用了try...catch,如果不做错误处理,网络一断页面就白屏了。数据获取成功后,渲染工作是DOM操作完成的:
javascript复制function renderWeatherCard(weatherData) {
const card = document.createElement('div');
card.className = 'weather-card';
const temperature = document.createElement('p');
temperature.className = 'temperature';
temperature.textContent = `${weatherData.current_weather.temperature}°C`;
const description = document.createElement('p');
description.className = 'description';
description.textContent = weatherData.current_weather.weathercode;
card.appendChild(temperature);
card.appendChild(description);
document.querySelector('.cards-container').appendChild(card);
}
这里有一个经常被忽略的细节:我用的textContent而不是innerHTML。因为数据来自外部接口,如果返回的数据里含有恶意内容,innerHTML会把它当作HTML渲染,造成XSS攻击。虽然天气数据不会有这种情况,但从第一天开始养成安全习惯,比之后踩坑了再改要好得多。
3. 应用设计的完整拆解:在动手前先解决四个问题
3.1 这个应用到底要解决什么问题
学习阶段的练手项目,最忌讳的就是"为了做而做"。我见过很多人学完HTML就照着教程敲一个静态页面,敲完就删了,因为那个页面跟自己没有任何关系。所以我今天给自己的要求是:做一个我真正愿意用的东西。
我选了"天气查看工具"这个方向——需求真实存在,数据获取逻辑有挑战性,UI设计有发挥空间,而且API是免费的。每个学习阶段的项目都应该满足这三个条件:真实需求、适度难度、自驱动力。
3.2 数据从哪来
选开源API的时候,我对比过三个方案:
| 方案 | 是否需要Key | 是否支持CORS | 适合场景 |
|---|---|---|---|
| Open-Meteo | 否 | 是 | 学习练手、个人项目 |
| 和风天气 | 是 | 是 | 需要更多天气字段的正式项目 |
| OpenWeatherMap | 是 | 是 | 老牌选择,文档完善但免费额度有限 |
我选了Open-Meteo,理由很简单:学习阶段应该把注意力放在前端逻辑上,而不是去处理API Key的申请和配额。但如果你做的是正式项目,我还是推荐用有正式服务保障的天气服务商,域名限制、单日配额、数据准确性这些对生产环境来说都是硬指标。
3.3 布局怎么设计
结构上是"头部 + 内容区 + 底部"三段式,这是Web页面最常见的骨架。内容区这次只展示一张卡片,但我在设计的时候就做了卡片容器,为的是之后可以扩展成多城市天气对比。前端开发里有一个经验:你没法预知未来需求,但可以在不增加太多复杂度的情况下给未来留个口子。一个flex-wrap属性就能实现这个效果,何乐而不为。
卡片本身的信息层级是:温度是大字,天气描述是中等字号,城市名是弱化的小号文字。视觉上从上到下,信息量从强到弱,用户扫一眼就能抓住重点。这个排列逻辑跟CSS代码里的flex-direction: column是对应的。
3.4 代码怎么组织
我有意识地把代码分成了三个文件:index.html负责结构,style.css负责样式,app.js负责逻辑。听起来像废话,但很多初学者图省事,把三样全塞进一个HTML文件里。等到要调试一个CSS问题,要在几百行代码里翻找;要加一个功能,得小心不要碰到其它部分。
分离的好处不只是"整洁",它让每一层都能独立修改而不影响其它层。改样式不用碰JavaScript,改数据结构不影响HTML结构。组件化、模块化这种听起来高级的概念,本质就是从这个最朴素的原则发展出来的。
4. 真正的挑战:从设计到落地之间的那些坑
4.1 接口数据长什么样,你不能靠猜
我第一天调接口的时候,想当然地认为返回数据里应该有一个"温度"字段,字段名就叫temperature。结果打开控制台一看,实际字段名是current_weather.temperature,温度字段还嵌套在一个对象里面。
这个问题的根源是:我用"我以为"代替了"实际查证"。正确的做法是先把接口文档看清楚,或者直接把API返回的JSON打印到控制台,确认数据结构后再写渲染逻辑。
今天实际返回的数据结构是:
json复制{
"latitude": 39.9042,
"longitude": 116.4074,
"current_weather": {
"temperature": 23.5,
"windspeed": 11.2,
"weathercode": 1
}
}
weathercode是一个数字编码,1代表基本晴朗,2代表局部多云。要显示成人类能读懂的文案,需要一个映射表:
javascript复制const weatherCodeMap = {
0: '晴朗',
1: '基本晴朗',
2: '局部多云',
3: '阴天',
45: '雾',
51: '小毛毛雨',
61: '小雨',
63: '中雨',
80: '阵雨'
};
如果不看文档,就算代码写对了,最终页面上显示的也只是一个个数字,毫无用户体验可言。这就是"数据可视化"的第一步——把原始数据翻译成用户可以理解的信息。
4.2 CORS跨域问题:前端开发永远绕不开的坎
我在调接口的过程中遇到过一个问题:控制台报错Access to fetch at 'https://api.open-meteo.com/...' from origin 'null' has been blocked by CORS policy。
当时我的页面是直接用浏览器的file://协议打开的,也就是双击HTML文件那种方式。浏览器在这个模式下,Origin会被识别为null,很多API的CORS策略会拒绝这种请求。
这个问题有两种解决办法:一是把文件部署到本地服务器上,比如用Python的python -m http.server 8000起一个静态服务,然后通过http://localhost:8000访问页面;二是直接用支持CORS的在线API服务商。Open-Meteo本身支持CORS,但我用file协议打开时仍然会遇到问题。用http服务打开后,请求就正常了。
这个坑我栽了十分钟才爬出来,但爬完之后我对CORS机制的理解比看十篇文章都深。浏览器为什么会阻止跨域?因为这是浏览器的一种安全机制,防止一个网页通过脚本去请求另一个域名下的敏感数据。作为前端开发者,你需要知道的是:页面在开发时最好通过本地服务器访问,而不仅仅是双击文件打开。
4.3 异步渲染的顺序问题:为什么我拿到的是undefined
核心逻辑跑通之后,我在开发另一个功能时遇到了一个经典的异步问题:在fetchWeatherData()执行完成之后,我立刻去读取某个变量的值,结果拿到的是undefined。
当时的代码逻辑结构:
javascript复制let weatherData = null;
async function init() {
weatherData = await fetchWeatherData();
// 这里可以正常读取 weatherData
}
init();
// 但在这里读取 weatherData
// 拿到的是 null,因为 init() 还在执行过程中
这个问题的本质是:init()函数是异步的,它调用fetchWeatherData()之后不会等结果,而是直接往下执行。虽然init内部用了await,但外部其他代码不会等init执行完。JavaScript的事件循环机制决定了这一点。
解决方案是:所有依赖异步数据的代码,都应该放在await之后的代码路径里,或者放在.then()回调里。不能在外面同步读取。这个概念在书上看十遍都不如实际踩一遍来得深刻。
4.4 DOMCLContentLoaded:为什么脚本要放在body底部
我一开始把<script>标签放在HTML的<head>里,结果浏览器报错:Cannot read properties of null (reading 'appendChild')。原因很简单:浏览器从上往下解析HTML,执行到<head>里的脚本时,body都还没开始解析,document.querySelector('.cards-container')自然就是null。
解决方案有两种比较好用的:一是把<script>放在</body>之前,让DOM先解析完;二是在脚本里监听DOMContentLoaded事件:
javascript复制document.addEventListener('DOMContentLoaded', function() {
init();
});
两种方案都可以,我平时习惯用第二种,因为脚本放在哪都行,不会受位置影响。但如果你用模块化的ES Module写法,浏览器默认会延迟执行,defer效果已经内置了。
5. 踩坑记录后的进阶:关于性能、缓存与移动端适配
5.1 图片和资源加载:为什么页面首屏有点慢
我用API返回的数据做渲染,页面很小,但实际测下来首屏渲染速度还是不够理想。研究了一下发现,问题不在代码逻辑,而在于网络请求本身——每次刷新页面,浏览器都要重新请求API数据,这个请求耗时在100-300毫秒之间,取决于网络状况。
一个很常见的优化手段是引入浏览器缓存。我用localStorage简单处理了一下:请求成功后把数据和当前时间存起来,下次打开页面时先检查缓存,如果在5分钟内直接复用缓存数据,否则才重新请求。代码思路:
javascript复制const CACHE_KEY = 'weather-cache';
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟
async function fetchWithCache() {
const cache = localStorage.getItem(CACHE_KEY);
if (cache) {
const { timestamp, data } = JSON.parse(cache);
if (Date.now() - timestamp < CACHE_DURATION) {
return data;
}
}
const freshData = await fetchWeatherData();
localStorage.setItem(CACHE_KEY, JSON.stringify({
timestamp: Date.now(),
data: freshData
}));
return freshData;
}
这个实现虽然粗糙,但它让重复访问的速度提升非常明显。如果你想让缓存策略更完善,可以考虑使用Service Worker或者HTTP缓存头,但对于学习阶段的项目,localStorage方案足够。
5.2 移动端适配:为什么我的卡片在手机上挤成了一团
我是在桌面浏览器上写的样式,写完切到手机模拟器一看——卡片宽度还是180px,而手机屏幕只有375px宽,卡片之间没有空隙,整体挤成一团。
解决方式是用媒体查询,在窄屏下调整卡片宽度:
css复制@media (max-width: 480px) {
.weather-card {
width: 100%;
padding: 16px;
}
}
这个媒体查询的意思是:当屏幕宽度小于等于480px时,应用这些覆盖样式。在移动端,卡片占满整行,阅读体验会好很多。响应式设计的核心思路就是:不同屏幕尺寸,提供不同的布局表现。
另外一个被我忽略的细节是<meta name="viewport">标签。没有它,手机浏览器会默认以980px宽度渲染网页然后缩放。这个问题在DAY1的笔记里出现过,但直到今天真的在手机上打开页面才发现它有多重要。
5.3 代码里那些"未来会很麻烦"的味道
写代码的时候,我注意到自己身上有一个习惯问题:变量命名随意,比如data、res、a这种。当时觉得无所谓,反正是自己看的代码。但后面写renderWeatherCard(weatherData)这行时,我就发现一个问题——我根本记不住data到底是从哪里来的、里面有什么字段。
后来我花了一点时间统一命名:获取数据的函数叫fetchWeatherData,存储返回值的变量叫weatherData,渲染函数叫renderWeatherCard。命名一清晰,整个代码逻辑自己就能讲明白了。这个体会很朴素,但它可能是今天所有收获里对后续开发影响最大的一个:读得懂的代码,才能改得动。
6. 面试视角倒推:DAY7这个应用到底能证明什么
6.1 面试题考的是场景,不是八股文
我自己刷过不少前端面试题,发现一个规律:靠背答案(也就是大家常说的八股文)能解决的概念题,面试官早就不太问了。现在更常见的是给你一个场景,问你方案。
比如有一种常见问法:如果页面上要展示多个城市天气,你会怎么设计数据结构和渲染逻辑?这个问题的背后考察的是:
- 你知不知道JSON数组怎么表达同构数据
- 你清不清楚
Array.prototype.map这个常用方法 - 你有没有封装组件/函数来避免重复代码的意识
你今天如果只是背了"map方法是用来遍历数组的",遇到这个场景题大概率会卡住。但如果你真的写过类似的渲染逻辑,你会下意识地说:数据结构用数组,每条记录是一个city对象,用map遍历生成卡片HTML,再批量插入DOM。
6.2 手写一个promise到底在考什么
我在DAY5学异步编程的时候,面试资料里反复出现"手写Promise"这道题。当时我的理解是:面试官要考察我有没有深入理解Promise的实现原理。后来我实际用promise写完整个数据请求流程,我有了另一个角度的理解:这道题真正要考察的,其实是"你有没有从实现者角度去思考问题的习惯"。
用Promise的API完成异步请求和调度是使用者的视角;手写Promise需要你理解resolve、reject、then这些内部机制是怎么协调的,这是实现者的视角。前端开发走到一定阶段,从使用者变成实现者是一次非常重要的跨越。DAY7这个项目让我第一次尝到"使用者"的甜头,也让我对实现者视角有了更多好奇。
6.3 从DAY7这个项目延伸出去的高频追问
如果我把这个天气应用写在简历里,面试官会追问哪些问题?我自己列了一下,全是基于这个项目可以展开的:
- 接口挂了怎么办?——这就要聊错误处理、用户提示、降级方案
- 数据更新频率怎么设计?——这是缓存策略、轮询/WebSocket/SSE的讨论
- 卡片变多了如何保证性能?——DOM批量操作、文档碎片、虚拟列表
- 如何添加城市搜索功能?——这就要聊防抖、事件委托
- 怎么保证你切换城市之后页面还能正确渲染?——这是状态管理的话题
一个简单的天气卡片应用,能延展出来的问题基本可以覆盖初级到中高级前端的面试范围。所以如果你也在做类似的练手项目,别做完就扔,试着站在面试官角度给自己出出题,这个过程本身就是深度学习。
7. 十天前的我 vs DAY7的我:知识落地的转变
十天前,我把"前端"理解为一堆标签、属性和语法的集合。HTML是结构,CSS是样式,JavaScript是行为,三者背熟了自然会做页面。DAY7之后我发现这个理解其实只对了一小半。
真正让页面"活"起来的,不是某个具体API的记忆,而是把三样东西组织成一个系统的能力。比如,HTML里每一类标签都对应一种信息类型的组织方式;CSS布局方案的选择直接决定界面的可维护性;JavaScript异步逻辑决定了用户什么时候能看到什么内容。这三层交织在一起,任何一个环节出问题,页面都不会正常工作。
DAY7最让我有成就感的一个瞬间不是代码跑通了,而是我发现自己开始能"预测"问题了。写渲染代码的时候,我会想到如果接口返回的字段名变了会发生什么;写CSS的时候,我会想到手机屏幕下卡片会不会变形;写异步逻辑的时候,我会想到如果请求失败用户看到什么。这种"多想一步"的直觉,是前六天纯学理论时完全不具备的。
如果你也正在走前端学习这条路,或者正处于某个知识体系的入门期,我的建议是:学完一个模块不要急着推下一个模块,强迫自己做一个用得上前面所有知识的小东西。也许很粗糙,也许很简单,但"从0到1完成一个能用的东西"这个体验本身,会教你比任何教程都多的东西。
8. 再往前一步:DAY7之后我打算这么做
今天的项目虽然是练手,但我给它规划了三个明确的后续方向,这里一并分享出来。
第一是交互增强。目前页面只是"展示"天气数据,下一步我想加一个城市选择下拉框,用户切换城市后重新拉取对应数据,卡片内容动态更新。这就会用到事件监听、DOM更新策略、状态管理这些更进阶的内容。
第二是代码工程化。当前逻辑写在一个app.js文件里,往后继续扩展会越来越难维护。我计划把数据请求、数据格式化、DOM渲染拆成独立模块,用ES Module的方式组织代码。这个阶段不引入打包工具,先用浏览器原生模块能力把小项目的结构捋顺。
第三是测试意识的引入。哪怕只是简单到"接口返回空数据时页面有没有兜底提示",也可以写成自动化测试。前端走到后面,测试能力是区分"能做页面"和"能做靠谱产品"的重要分界线。
当然,这三个规划里我最有信心的还是第一个——先把交互做出来,效果立竿见影。后面两个在动手过程中会陆续分享。
另外,关于最近总被问到的"AI编程工具这么强,前端工程师是不是没出路了"这个问题,DAY7也有了一点新的体会。今天我在写代码的时候也尝试让AI辅助,比如让它帮我补全CSS样式,或者生成一段遍历数据的代码。但我发现自己必须非常清楚"我要什么",才能判断AI给的结果对不对。前端工程师的核心竞争力,从来不是你能不能写出某几行代码,而是你具不具备把模糊需求拆解成清晰技术方案的能力。这个能力AI暂时学不会,但对于一个新人来说,它只能靠一天天真实写代码积累出来。共勉。
