双层缓存+响应式设计:博客每日一句功能完美落地教程

  Java   31分钟   114浏览   0评论

前言

你好呀,我是小邹。

在博客底部添加一个"每日一句"功能,展示中英文双语的精美句子,既能提升网站格调,又能给访客带来每日的灵感。本文将详细介绍如何实现这个功能,包括后端 API 代理、前端展示、缓存策略以及响应式设计。

一、功能需求分析

核心需求

  1. 每日更新:每天展示不同的中英文句子
  2. 稳定可靠:API 不可用时要有备用方案
  3. 性能优化:避免重复请求,使用缓存
  4. 响应式设计:适配桌面端和移动端
  5. 交互体验:长句子支持展开查看完整内容

技术选型

  • 后端: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
);

关键技术点

  1. 请求头伪装:模拟浏览器请求,避免被反爬
  2. JSON 解析:使用 Jackson 的 ObjectMapper 解析响应
  3. 异常处理:捕获各种异常,确保服务稳定性

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>

七、总结

技术亮点

  1. 双层缓存:服务端 + 客户端双重缓存,提升性能
  2. 降级策略:API 失败时自动切换到备用数据
  3. 跨域解决:后端代理模式解决前端跨域问题
  4. 交互优化:悬浮/点击展开长句子,提升用户体验
  5. 响应式设计:完美适配各种屏幕尺寸

可扩展性

  • 可接入多个 API 源,轮询使用
  • 可添加数据库持久化,积累历史句子
  • 可添加管理后台,自定义句子内容
  • 可添加分享功能,分享到社交媒体

参考链接

  • 有道词典 API:https://dict.youdao.com/infoline/style/cardList?mode=publish&client=mobile&style=daily
如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  0 条评论