weather-app开发手记 02 JSON基础 | API 调用 400 错误修复 | JWT 认证问题

1 JSON基础

对象、数组、值

JSON 解析实例

1. 什么是这里的 "JSON 数据"?

从服务器收到的字符串格式数据:

bash 复制代码
{"name":"runoob", "alexa":"10000", "site":"www.runoob.com"}

它是键值对结构的文本,是前后端传递数据的常用格式。

2. 为什么要用JSON.parse()

JavaScript 不能直接用这个字符串里的内容(比如拿name的值),所以需要用 JSON.parse()把它转成 JavaScript 对象

javascript 复制代码
var obj = JSON.parse('{"name":"runoob", "alexa":"10000", "site":"www.runoob.com"}');

执行后,obj就变成了一个 JS 对象,结构是:

javascript 复制代码
{
  name: "runoob",
  alexa: "10000",
  site: "www.runoob.com"
}

3. 注意点:JSON 格式必须 "标准"

如果 JSON 字符串写错(比如键没加双引号、逗号漏了),JSON.parse()会报错。比如下面的写法是错的(name 没加双引号):

bash 复制代码
{name:"runoob"} // 错误,JSON的键必须用双引号

4. 解析后怎么用?

转成 JS 对象后,就可以通过对象。属性名获取对应的值,比如在网页中显示:

html 复制代码
<!-- 页面上的容器 -->
<p id="demo"></p>

<script>
var obj = JSON.parse('{"name":"runoob", "alexa":"10000", "site":"www.runoob.com"}');
// 把name和site的值放到p标签里
document.getElementById("demo").innerHTML = obj.name + " " + obj.site;
</script>

最终页面会显示:runoob www.runoob.com

2 API 调用 400 错误修复

问题深度剖析

  • 错误本质:第三方 API(和风天气)的接口契约明确要求查询参数为「城市 ID」,而非中文城市名。中文城市名存在两大核心问题:
  • ① 多义性(如 "西安" 可能指西安市或下属的西安区,不同行政区域对应不同天气数据);
  • ② 编码差异(中文参数在 URL 传输时需经过 UTF-8 编码,若前端编码不规范或后端解码逻辑不一致,会导致服务器无法识别参数),最终触发 HTTP 400 Bad Request(请求参数格式非法)。
  • 错误重现 :当直接调用 https://devapi.qweather.com/v7/weather/now?location=北京&key=你的API密钥 时,服务器因无法解析location=北京这个非标准参数,直接返回 400 错误,响应体通常包含 "参数错误:location 格式非法" 的提示。

解决方案的详细实现与逻辑

(1)创建城市 ID 映射表(constants.ts

  • 设计思路:城市 ID 是和风天气 API 定义的唯一标识(由数字组成,无歧义),从官方城市列表中获取准确 ID 后,通过映射表将用户易懂的城市名与机器可识别的城市 ID 绑定,既保证参数规范性,又不影响用户操作体验。
  • 完整代码示例
TypeScript 复制代码
// constants.ts
/**
 * 城市ID映射表(数据来源:和风天气官方城市列表API)
 * 文档参考:https://dev.qweather.com/docs/api/geo/city-lookup/
 * as const 关键字:将对象变为只读常量,TypeScript会精准推导每个键值对的类型(而非笼统的string)
 */
export const CITY_ID_MAP = {
  北京: '101010100', // 北京市(直辖市,一级行政单位)
  上海: '101020100', // 上海市(直辖市)
  广州: '101280101', // 广州市(广东省省会)
  深圳: '101280601', // 深圳市(广东省副省级市)
  杭州: '101210101', // 杭州市(浙江省省会)
  南京: '101190101', // 南京市(江苏省省会)
  成都: '101270101', // 成都市(四川省省会)
  重庆: '101040100'  // 重庆市(直辖市)
} as const;

/**
 * 支持的城市列表(从映射表中推导,避免手动重复维护)
 * 类型:("北京" | "上海" | ... | "重庆")[],具备严格的类型约束
 */
export const SUPPORTED_CITIES = Object.keys(CITY_ID_MAP) as Array<keyof typeof CITY_ID_MAP>;

(2)更新 API 调用逻辑(App.tsx

  • 核心修改:将原来直接传递城市名的逻辑,改为先通过映射表获取城市 ID,再传递 ID 参数。
  • 完整代码示例
TypeScript 复制代码
// App.tsx
import { useState } from 'react';
import { getCurrentWeather } from './api/weather';
import { CITY_ID_MAP, SUPPORTED_CITIES } from './constants';

const App = () => {
  const [selectedCity, setSelectedCity] = useState<keyof typeof CITY_ID_MAP>('北京');
  const [weatherData, setWeatherData] = useState(null);
  const [error, setError] = useState('');

  // 查询天气的核心函数
  const fetchWeather = async () => {
    try {
      setError('');
      // 1. 通过城市名获取对应的城市ID(从映射表中读取,确保参数合法)
      const cityId = CITY_ID_MAP[selectedCity];
      // 2. 调用API时,将location参数设为城市ID(符合和风天气API规范)
      const data = await getCurrentWeather(cityId);
      setWeatherData(data);
    } catch (err: any) {
      setError(err.message || '查询天气失败,请稍后重试');
    }
  };

  return (
    <div className="app">
      <h1>天气查询</h1>
      {/* 城市选择器:仅展示支持的城市,避免用户选择无效城市 */}
      <select
        value={selectedCity}
        onChange={(e) => setSelectedCity(e.target.value as keyof typeof CITY_ID_MAP)}
      >
        {SUPPORTED_CITIES.map((city) => (
          <option key={city} value={city}>
            {city}
          </option>
        ))}
      </select>
      <button onClick={fetchWeather}>查询天气</button>
      {/* 错误提示与天气展示 */}
      {error && <div className="error">{error}</div>}
      {weatherData && (
        <div className="weather-card">
          <h3>{selectedCity} 实时天气</h3>
          <p>温度:{weatherData.now.temp}℃</p>
          <p>天气状况:{weatherData.now.text}</p>
          <p>更新时间:{weatherData.now.obsTime}</p>
        </div>
      )}
    </div>
  );
};

export default App;

(3)添加城市有效性验证(防御性编程)

  • 设计思路:即使前端 UI 仅提供支持的城市选择,仍需防止通过手动修改 DOM、接口直接调用等方式传入无效城市名,因此在工具函数中添加验证逻辑。
  • 工具函数示例
TypeScript 复制代码
// utils/cityUtils.ts
import { CITY_ID_MAP, SUPPORTED_CITIES } from '../constants';

/**
 * 验证城市是否支持,并返回对应的城市ID
 * @param cityName - 用户输入的城市名
 * @returns 合法的城市ID
 * @throws 不支持的城市会抛出错误
 */
export const validateAndGetCityId = (cityName: string) => {
  // 严格校验:城市名必须在支持的列表中
  if (!SUPPORTED_CITIES.includes(cityName as keyof typeof CITY_ID_MAP)) {
    throw new Error(`暂不支持查询"${cityName}"的天气,请选择以下支持的城市:${SUPPORTED_CITIES.join('、')}`);
  }
  // 类型断言:确保返回的城市ID是映射表中定义的准确值
  return CITY_ID_MAP[cityName as keyof typeof CITY_ID_MAP];
};

// 在API调用中使用
import { validateAndGetCityId } from '../utils/cityUtils';
export const getCurrentWeather = async (cityName: string) => {
  const cityId = validateAndGetCityId(cityName);
  const response = await axios.get('/v7/weather/now', {
    params: { location: cityId, key: import.meta.env.VITE_QWEATHER_KEY }
  });
  return response.data;
};

相关官方文档

3 JWT 认证问题(前端身份认证核心)

问题深度剖析

  • 错误本质 :JWT(JSON Web Token)生成时必须使用有效的「私钥(Secret/Key)」进行签名,若私钥配置缺失、为空或格式错误,会导致jsonwebtoken库抛出 "invalid signature"(无效签名)错误,无法生成合法令牌;进而导致 API 请求的Authorization头无效,服务器返回 401 Unauthorized(未授权)。
  • 常见场景 :开发环境中可能因.env文件配置遗漏(如VITE_JWT_SECRET未定义),或私钥字符串包含特殊字符未转义,导致认证失败。

解决方案的详细实现与逻辑

(1)添加备用私钥与环境变量兼容(jwtUtils.ts

  • 设计思路:优先从环境变量读取私钥(符合生产环境配置规范),若环境变量未配置,则使用备用私钥(保证开发环境功能可用),同时设置令牌过期时间(避免永久有效导致安全风险)。
  • 完整代码示例
TypeScript 复制代码
// utils/jwtUtils.ts
import jwt from 'jsonwebtoken';

/**
 * JWT配置:优先读取环境变量, fallback 到备用私钥
 * 生产环境必须在.env文件中配置 VITE_JWT_SECRET(长度建议≥16位,包含大小写、数字、特殊字符)
 */
const JWT_SECRET = import.meta.env.VITE_JWT_SECRET || 'weather-app-backup-secret-2024_@#$';
const JWT_EXPIRES_IN = '1h'; // 令牌有效期1小时(生产环境可根据需求调整)

/**
 * 生成JWT令牌
 * @param payload - 存储在令牌中的非敏感数据(如用户ID、角色)
 * @returns 签名后的JWT令牌字符串
 */
export const generateJwtToken = (payload: object) => {
  try {
    // 签名并设置过期时间:jwt.sign(数据, 私钥, 配置选项)
    return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
  } catch (error: any) {
    console.error('JWT令牌生成失败:', error.message);
    throw new Error('认证令牌生成失败,请检查配置');
  }
};

/**
 * 验证JWT令牌有效性(前端可选,主要用于后端验证;前端可用于解析令牌数据)
 * @param token - 待验证的JWT令牌
 * @returns 解析后的 payload 数据(若令牌有效)
 */
export const verifyJwtToken = (token: string) => {
  try {
    return jwt.verify(token, JWT_SECRET);
  } catch (error: any) {
    if (error.name === 'TokenExpiredError') {
      throw new Error('认证令牌已过期,请重新获取');
    }
    throw new Error('无效的认证令牌');
  }
};

(2)在 Axios 请求拦截器中添加认证头

  • 核心逻辑 :每次 API 请求前,自动生成 JWT 令牌并添加到Authorization头,符合 Bearer Token 认证规范。
  • 代码示例
TypeScript 复制代码
// api/request.ts(Axios实例配置)
import axios from 'axios';
import { generateJwtToken } from '../utils/jwtUtils';

// 创建Axios实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 5000
});

// 请求拦截器:添加JWT认证头
service.interceptors.request.use(
  (config) => {
    // 生成令牌(实际项目中可缓存令牌,避免每次请求都重新生成)
    const token = generateJwtToken({ app: 'weather-app', timestamp: Date.now() });
    // 设置Bearer Token:格式必须为 "Bearer {token}"(空格不可省略)
    config.headers.Authorization = `Bearer ${token}`;
    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截器:统一处理认证错误
service.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      // 401错误:令牌无效/过期,可触发重新登录逻辑(此处简化处理)
      alert('认证失败,请刷新页面重试');
    }
    return Promise.reject(error);
  }
);

export default service;

(3)移除调试日志(代码规范优化)

  • 优化原因 :开发阶段的console.log(token)console.log(secret)等日志,可能在生产环境泄露敏感信息(如私钥、令牌),且增加代码冗余;通过eslint配置可强制禁止生产环境输出调试日志。
  • 辅助配置(.eslintrc.js)
TypeScript 复制代码
module.exports = {
  rules: {
    // 生产环境禁止console.log(开发环境可放行)
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn'
  }
};

相关官方文档

相关推荐
A24207349302 小时前
JavaScript学习
前端·javascript·学习
阿蒙Amon2 小时前
JavaScript学习笔记:1.JavaScript简介
javascript·笔记·学习
副露のmagic2 小时前
更弱智的算法学习day 10
python·学习·算法
Ada大侦探2 小时前
新手小白学习Power BI第五弹--------产品分析以及产品毛利率报表、条件式标红、饼图、散点图
学习·数据分析·powerbi
深海章鱼2 小时前
MD 基础学习2
学习·md
西岸行者2 小时前
学习Hammerstein-Wiener 模型,以及在回声消除场景中的应用
人工智能·学习·算法
Vincent_Zhang2333 小时前
专题:通过时间轴解释区分各种时态
笔记
鲨莎分不晴3 小时前
强化学习第四课 —— 深度强化学习:Policy Gradient 入门
人工智能·学习·机器学习
"YOUDIG"3 小时前
番茄钟网站:科学管理时间,重塑高效工作与学习节奏
学习