前言
你好呀,我是小邹。
在博客底部添加一个"每日一句"功能,展示中英文双语的精美句子,既能提升网站格调,又能给访客带来每日的灵感。本文将详细介绍如何实现这个功能,包括后端 API 代理、前端展示、缓存策略以及响应式设计。
一、功能需求分析
核心需求
- 每日更新:每天展示不同的中英文句子
- 稳定可靠:API 不可用时要有备用方案
- 性能优化:避免重复请求,使用缓存
- 响应式设计:适配桌面端和移动端
- 交互体验:长句子支持展开查看完整内容
技术选型
- 后端:Spring Boot + RestTemplate
- 前端:原生 JavaScript + CSS3
- 数据源:有道词典每日一句 API
- 缓存:ConcurrentHashMap(服务端)+ localStorage(客户端)
二、后端实现
2.1 接口设计
@GetMapping(value = "/api/daily-quote", produces = "application/json;charset=UTF-8")
@ResponseBody
public Map<String, String> getDailyQuote() {
// 实现逻辑...
}
设计要点:
- 使用
/api/daily-quote路径,统一 API 接口规范 - 返回 JSON 格式,包含英文原文(en)、中文翻译(cn)、成功标识(success)
2.2 缓存策略
// 使用 ConcurrentHashMap 作为内存缓存
private static final ConcurrentHashMap<String, Map<String, String>> quoteCache =
new ConcurrentHashMap<>();
// 缓存 key 为日期字符串,value 为句子数据
String today = java.time.LocalDate.now().toString();
if (quoteCache.containsKey(today)) {
return quoteCache.get(today);
}
为什么使用 ConcurrentHashMap?
- 线程安全:支持多并发访问
- 内存存储:访问速度快
- 自动清理:每天新 key 会覆盖旧数据
2.3 API 调用与解析
String apiUrl = "https://dict.youdao.com/infoline/style/cardList?mode=publish&client=mobile&style=daily";
HttpHeaders headers = new HttpHeaders();
headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
headers.add("Accept", "application/json, text/plain, */*");
headers.add("Host", "dict.youdao.com");
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
ResponseEntity<String> responseEntity = restTemplate.exchange(
apiUrl,
HttpMethod.GET,
requestEntity,
String.class
);
关键技术点:
- 请求头伪装:模拟浏览器请求,避免被反爬
- JSON 解析:使用 Jackson 的
ObjectMapper解析响应 - 异常处理:捕获各种异常,确保服务稳定性
2.4 数据解析
JsonNode rootNode = objectMapper.readTree(responseStr);
if (rootNode.isArray() && rootNode.size() > 0) {
JsonNode firstItem = rootNode.get(0);
String en = firstItem.get("title").asText().trim();
String cn = firstItem.get("summary").asText().trim();
result.put("en", en);
result.put("cn", cn);
result.put("success", "true");
quoteCache.put(today, result);
}
有道词典 API 返回的是 JSON 数组,我们取第一个元素,其中:
title:英文句子summary:中文翻译
2.5 降级策略
} catch (Exception e) {
log.error("获取每日一句失败", e);
}
// 所有方案都失败时返回备用数据
result.put("en", "The best time to plant a tree was 10 years ago. The second best time is now.");
result.put("cn", "种一棵树最好的时间是十年前,其次是现在。");
result.put("success", "false");
为什么需要降级?
- 第三方 API 可能不稳定
- 网络波动导致请求失败
- 保证用户体验,始终有内容展示
三、前端实现
3.1 HTML 结构
<div class="daily-quote" id="daily-quote">
<div class="daily-quote-content">
<div class="daily-quote-en" id="quote-en">Loading...</div>
<div class="daily-quote-cn" id="quote-cn">正在获取每日一句...</div>
</div>
</div>
3.2 CSS 样式
基础样式
.daily-quote {
display: flex;
flex-direction: column;
justify-content: center;
padding: 16px 20px;
height: 80px;
width: 360px;
min-width: 360px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-radius: 16px;
border: 1px solid #bae6fd;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.06);
position: relative;
overflow: hidden;
}
/* 引号装饰 */
.daily-quote::before {
content: '"';
position: absolute;
top: 8px;
left: 12px;
font-size: 3rem;
color: rgba(14, 165, 233, 0.15);
font-family: Georgia, serif;
line-height: 1;
pointer-events: none;
}
文字截断与展开
.daily-quote-en {
font-size: 1.05rem;
color: #0369a1;
font-weight: 500;
line-height: 1.4;
font-style: italic;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
position: relative;
transition: all 0.3s ease;
}
/* 悬浮/展开状态 */
.daily-quote-en:hover,
.daily-quote-en.expanded {
white-space: normal;
overflow: visible;
text-overflow: clip;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
padding: 4px 8px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.15);
z-index: 10;
}
关键技术点:
white-space: nowrap+text-overflow: ellipsis实现省略号:hover和.expanded类控制展开状态z-index确保展开内容在最上层
3.3 JavaScript 交互
数据获取
(function fetchDailyQuote() {
var quoteEn = document.getElementById('quote-en');
var quoteCn = document.getElementById('quote-cn');
// 检查元素是否存在,不存在则重试
if (!quoteEn || !quoteCn) {
setTimeout(fetchDailyQuote, 500);
return;
}
// 调用后端接口
fetch('/api/daily-quote')
.then(function(response) {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(function(data) {
if (data && data.en && data.cn) {
quoteEn.textContent = data.en;
quoteCn.textContent = data.cn;
// 缓存到 localStorage
var today = new Date().toDateString();
localStorage.setItem('dailyQuote', JSON.stringify({
en: data.en,
cn: data.cn
}));
localStorage.setItem('dailyQuoteDate', today);
}
})
.catch(function(error) {
console.error('Error:', error);
// 使用备用数据
quoteEn.textContent = "The best time to plant a tree was 20 years ago...";
quoteCn.textContent = "种一棵树最好的时间是十年前,其次是现在。";
});
})();
点击展开功能
// 移动端点击展开/收起
function toggleExpand(element) {
element.classList.toggle('expanded');
}
quoteEn.addEventListener('click', function(e) {
e.stopPropagation();
toggleExpand(quoteEn);
});
quoteCn.addEventListener('click', function(e) {
e.stopPropagation();
toggleExpand(quoteCn);
});
// 点击其他地方收起
document.addEventListener('click', function() {
quoteEn.classList.remove('expanded');
quoteCn.classList.remove('expanded');
});
四、双层缓存架构
4.1 缓存层次
┌─────────────────────────────────────────┐
│ 客户端 localStorage │
│ - 当天数据缓存 │
│ - 减少重复请求 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 服务端 ConcurrentHashMap │
│ - 当天数据缓存 │
│ - 减少第三方 API 调用 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 第三方 API │
│ - 有道词典每日一句 │
└─────────────────────────────────────────┘
4.2 缓存策略对比
| 缓存层 | 存储位置 | 有效期 | 作用 |
|---|---|---|---|
| localStorage | 浏览器 | 1天 | 减少页面刷新后的请求 |
| ConcurrentHashMap | JVM内存 | 1天 | 减少第三方API调用 |
五、响应式设计
5.1 断点设置
/* 桌面端 */
.daily-quote-en { font-size: 1.05rem; }
.daily-quote-cn { font-size: 1rem; }
/* 平板端 (max-width: 768px) */
@media (max-width: 768px) {
.daily-quote-en { font-size: 1rem; }
.daily-quote-cn { font-size: 0.95rem; }
}
/* 手机端 (max-width: 480px) */
@media (max-width: 480px) {
.daily-quote-en { font-size: 0.95rem; }
.daily-quote-cn { font-size: 0.9rem; }
}
5.2 布局适配
.daily-quote {
width: 360px;
min-width: 360px;
}
@media (max-width: 768px) {
.daily-quote {
width: 100%;
max-width: 340px;
}
}
@media (max-width: 480px) {
.daily-quote {
width: 100%;
max-width: 100%;
}
}
六、完整代码
6.1 后端完整代码
@RestController
public class DailyQuoteController {
private static final ConcurrentHashMap<String, Map<String, String>> quoteCache =
new ConcurrentHashMap<>();
@Autowired
private RestTemplate restTemplate;
@Autowired
private ObjectMapper objectMapper;
@GetMapping(value = "/api/daily-quote", produces = "application/json;charset=UTF-8")
@ResponseBody
public Map<String, String> getDailyQuote() {
String today = java.time.LocalDate.now().toString();
// 1. 检查缓存
if (quoteCache.containsKey(today)) {
return quoteCache.get(today);
}
Map<String, String> result = new HashMap<>();
String apiUrl = "https://dict.youdao.com/infoline/style/cardList?mode=publish&client=mobile&style=daily";
try {
HttpHeaders headers = new HttpHeaders();
headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
headers.add("Accept", "application/json, text/plain, */*");
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
apiUrl, HttpMethod.GET, requestEntity, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
JsonNode rootNode = objectMapper.readTree(response.getBody());
if (rootNode.isArray() && rootNode.size() > 0) {
JsonNode item = rootNode.get(0);
result.put("en", item.get("title").asText().trim());
result.put("cn", item.get("summary").asText().trim());
result.put("success", "true");
quoteCache.put(today, result);
return result;
}
}
} catch (Exception e) {
log.error("获取每日一句失败", e);
}
// 备用数据
result.put("en", "The best time to plant a tree was 10 years ago. The second best time is now.");
result.put("cn", "种一棵树最好的时间是十年前,其次是现在。");
result.put("success", "false");
return result;
}
}
6.2 前端完整代码
<div class="daily-quote" id="daily-quote">
<div class="daily-quote-content">
<div class="daily-quote-en" id="quote-en">Loading...</div>
<div class="daily-quote-cn" id="quote-cn">正在获取每日一句...</div>
</div>
</div>
<script>
(function fetchDailyQuote() {
var quoteEn = document.getElementById('quote-en');
var quoteCn = document.getElementById('quote-cn');
if (!quoteEn || !quoteCn) {
setTimeout(fetchDailyQuote, 500);
return;
}
// 点击展开功能
function toggleExpand(el) { el.classList.toggle('expanded'); }
quoteEn.addEventListener('click', function(e) { e.stopPropagation(); toggleExpand(quoteEn); });
quoteCn.addEventListener('click', function(e) { e.stopPropagation(); toggleExpand(quoteCn); });
document.addEventListener('click', function() {
quoteEn.classList.remove('expanded');
quoteCn.classList.remove('expanded');
});
// 获取数据
fetch('/api/daily-quote')
.then(r => r.json())
.then(data => {
if (data.en && data.cn) {
quoteEn.textContent = data.en;
quoteCn.textContent = data.cn;
localStorage.setItem('dailyQuote', JSON.stringify(data));
localStorage.setItem('dailyQuoteDate', new Date().toDateString());
}
})
.catch(() => {
quoteEn.textContent = "The best time to plant a tree was 20 years ago...";
quoteCn.textContent = "种一棵树最好的时间是十年前,其次是现在。";
});
})();
</script>
七、总结
技术亮点
- 双层缓存:服务端 + 客户端双重缓存,提升性能
- 降级策略:API 失败时自动切换到备用数据
- 跨域解决:后端代理模式解决前端跨域问题
- 交互优化:悬浮/点击展开长句子,提升用户体验
- 响应式设计:完美适配各种屏幕尺寸
可扩展性
- 可接入多个 API 源,轮询使用
- 可添加数据库持久化,积累历史句子
- 可添加管理后台,自定义句子内容
- 可添加分享功能,分享到社交媒体
参考链接:
- 有道词典 API:
https://dict.youdao.com/infoline/style/cardList?mode=publish&client=mobile&style=daily

评论区 0