- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
本文基于 Redwood v6 官方文档 "Using a Third Party API" 展开。Redwood 是一个全栈 JS 框架,前端(web)基于 React,后端(api)自带 GraphQL 服务层,二者天然打通。当应用需要从外部服务获取数据时,可以选择在浏览器端直接调用第三方 API,也可以让 Redwood 的 serverless 函数代为请求,再通过自家 GraphQL 接口暴露给前端。本文将以构建一个「输入美国邮编、显示当前天气」的小应用为例,完整演示两种集成路径,并深入结合仓库源码讲解 SDL、Service、Cell、表单校验与错误处理背后的实现机制。读完本文,你将掌握在 Redwood 应用中安全、规范地集成任意第三方 HTTP API 的完整套路。
场景与准备工作
我们将构建一个简单的天气应用:用户输入一个美国邮编(zip code),页面展示该地区当前的天气状况。为此需要从 OpenWeather 获取实时天气数据。
1. 注册 OpenWeather 并获取 API Key
- 在 OpenWeather 官网创建一个免费账户并验证邮箱;
- 进入 API keys 页面,复制默认生成的 API Key;
- 免费账户每天可调用 1,000 次,足够示例应用使用。
注意:新注册的 Key 可能需要等待最长 30 分钟才会被激活。等待期间可以使用 OpenWeather 提供的示例响应端点来预览数据结构。
2. 理解 OpenWeather 的响应结构
调用https://api.openweathermap.org/data/2.5/weather?zip=94040,us&appid=YOUR_API_KEY返回的标准 JSON 结构如下(示例数据,城市为 Mountain View):
{ "coord": { "lon": -122.09, "lat": 37.39 }, "weather": [ { "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" } ], "base": "stations", "main": { "temp": 280.44, "pressure": 1017, "humidity": 61, "temp_min": 279.15, "temp_max": 281.15 }, "visibility": 12874, "wind": { "speed": 8.2, "deg": 340, "gust": 11.3 }, "clouds": { "all": 1 }, "dt": 1519061700, "sys": { "type": 1, "id": 392, "country": "US", "sunrise": 1519051894, "sunset": 1519091585 }, "id": 0, "name": "Mountain View", "cod": 200 }对页面展示有用的字段包括:
name:邮编对应的城市名;main.temp:当前温度(单位为开尔文 Kelvin,需要转换为华氏度或摄氏度);weather[0].main:英文天气状况描述(如 Rain);weather[0].icon:天气图标代码,可拼接为图标 URL:https://openweathermap.org/img/wn/{icon}@2x.png。
创建 Redwood 应用与首页表单
与创建其他 Redwood 应用完全一致:
yarn create redwood-app weatherstation cd weatherstation yarn rw dev浏览器会自动打开http://localhost:8910。接着生成首页路由与页面:
yarn rw generate page home /也可以用完整命令:
yarn redwood generate page home /。
生成后首页位于web/src/pages/HomePage/HomePage.js。现在为首页添加一个输入邮编的表单。Redwood 提供了@redwoodjs/forms表单库,底层是对react-hook-form的封装(见仓库 packages/forms/src/Form.tsx,其中<Form>组件通过useForm+FormProvider建立表单上下文),因此可以享受声明式的校验能力:
import { Form, TextField, Submit } from '@redwoodjs/forms' const HomePage = () => { const onSubmit = (data) => { console.info(data) } return ( <Form onSubmit={onSubmit} style={{fontSize: '2rem'}}> <TextField name="zip" placeholder="Zip code" maxLength="5" validation={{ required: true, pattern: /^\d{5}$/ }} /> <Submit>Go</Submit> </Form> ) } export default HomePagevalidation对象中的pattern: /^\d{5}$/会在提交时校验输入必须是 5 位纯数字。Redwood 的 forms 包还会根据字段名、required、emptyAs等条件自动做值类型转换(coercion),相关逻辑可见 packages/forms/src/coercion.ts。在浏览器开发者工具中点击Go,即可在控制台看到提交的{ zip: "94040" }数据。
接下来,我们需要真正调用 API。Redwood 提供了两条路径:
- 客户端集成:由浏览器中的 React 应用直接调用第三方 API;
- 服务端集成:由 Redwood 的服务端(serverless function / GraphQL Service)调用第三方 API,客户端只与自家 GraphQL 接口通信。
下面分别展开。
客户端集成:浏览器直连第三方 API
方案取舍
优点
- 设计最简单:无需搭建任何服务端逻辑;
- 网络请求最少:客户端一次请求直达第三方;
- 速度快:无中间层转发。
缺点
- 不安全:用户查看页面源码即可拿到 API Key;
- 无法限流:恶意脚本可以每秒成千上万次地轰炸该页面。
真实项目中需要结合风险权衡选择。
使用 Fetch 拉取数据
表单的onSubmit已经拿到了邮编,直接在回调中调用浏览器内置的 Fetch API:
const onSubmit = (data) => { fetch( `https://api.openweathermap.org/data/2.5/weather?zip=${data.zip},us&appid=YOUR_API_KEY` ) .then((response) => response.json()) .then((json) => console.info(json)) }如果 API Key 尚未激活,不要尝试把 URL 换成示例响应端点——跨域(CORS)会导致请求失败,只能等待 Key 生效。
用 React state 渲染天气
引入useState保存 API 结果并触发界面刷新(注意表单和输出需要包裹在<> </>片段中):
import { useState } from 'react' import { Form, TextField, Submit } from '@redwoodjs/forms' const HomePage = () => { const [weather, setWeather] = useState() const onSubmit = (data) => { fetch( `https://api.openweathermap.org/data/2.5/weather?zip=${data.zip},us&appid=YOUR_API_KEY` ) .then((response) => response.json()) .then((json) => setWeather(json)) } return ( <> <Form onSubmit={onSubmit}> <TextField name="zip" placeholder="Zip code" maxLength="5" validation={{ required: true, pattern: /^\d{5}$/ }} /> <Submit>Go</Submit> </Form> {weather && JSON.stringify(weather)} </> ) } export default HomePage格式化并展示真实天气
最后补充辅助函数:将开尔文转为华氏度、提取天气状况、拼装图标 URL,然后渲染到页面上:
import { useState } from 'react' import { Form, TextField, Submit } from '@redwoodjs/forms' const HomePage = () => { const [weather, setWeather] = useState() const onSubmit = (data) => { fetch( `https://api.openweathermap.org/data/2.5/weather?zip=${data.zip},us&appid=YOUR_API_KEY` ) .then((response) => response.json()) .then((json) => setWeather(json)) } const temp = () => Math.round(((weather.main.temp - 273.15) * 9) / 5 + 32) const condition = () => weather.weather[0].main const icon = () => { return `https://openweathermap.org/img/wn/${weather.weather[0].icon}@2x.png` } return ( <> <Form onSubmit={onSubmit}> <TextField name="zip" placeholder="Zip code" maxLength="5" validation={{ required: true, pattern: /^\d{5}$/ }} /> <Submit>Go</Submit> </Form> {weather && ( <section> <h1>{weather.name}</h1> <h2> <img src={icon()} style={{ maxWidth: '2rem' }} /> <span> {temp()}°F and {condition()} </span> </h2> </section> )} </> ) } export default HomePage功能已经跑通。但正如前文所述,把 API Key 暴露在浏览器端存在明显安全风险,接下来看更稳妥的服务端方案。
服务端集成:通过 Redwood GraphQL 代理第三方 API
服务端方案要做两件事:
- 为客户端提供访问自家服务端(serverless function)的接口;
- 让服务端去访问第三方 API。
Redwood 内置 GraphQL 集成,因此使用 GraphQL SDL 定义面向客户端的接口,用 Service 实现调用第三方 API 的业务逻辑。
为什么不用 SDL 生成器?Redwood 的
yarn rw g sdl生成器默认假设你在api/db/schema.prisma中定义了数据模型,生成的 SDL 面向的是数据库表结构。当需要自定义一个与数据库无关的 API 接口时,需要手写 SDL。
定义 GraphQL SDL
我们可以自定义返回的数据结构,把 OpenWeather 响应中无关的字段剔除,只保留客户端需要的部分,甚至可以在服务端提前完成单位转换与图标 URL 拼装:
export const schema = gql` type Weather { zip: String! city: String! conditions: String! temp: Int! icon: String! } type Query { getWeather(zip: String!): Weather! @skipAuth } `说明:
zip定义为String!而非Int,因为邮编可能以0开头;@skipAuth指令表示该查询无需登录即可访问。Redwood 通过createValidatorDirective机制将 SDL 中的指令与校验函数绑定(见 packages/graphql-server/src/directives/makeDirectives.ts),默认生成的@requireAuth用于鉴权场景。
编写 Service(GraphQL 解析器)
在 Redwood 中,GraphQL Query 类型会自动映射到同名 Service 中导出的同名函数。因此创建api/src/services/weather/weather.js,导出getWeather。先用假数据验证整个链路:
export const getWeather = ({ zip }) => { return { zip, city: 'City', conditions: 'Hot Lava', temp: 1000, icon: 'https://placekitten.com/100/100', } }Redwood 自带 GraphQL Playground(GraphiQL),在浏览器打开http://localhost:8911/graphql,左上输入查询、左下输入变量,点击 Play 即可验证:
query GetWeatherQuery($zip: String!) { getWeather(zip: $zip) { zip city conditions temp icon } }变量:{ "zip": "94040" }。
接入真实的 OpenWeather 请求
服务端环境没有浏览器内置的fetch,需要安装一个符合 Fetch API 规范的包:
yarn workspace api add @whatwg-node/fetch然后改造 Service。fetch返回 Promise,用async/await简化异步逻辑:
import { fetch } from '@whatwg-node/fetch' export const getWeather = async ({ zip }) => { const response = await fetch( `https://api.openweathermap.org/data/2.5/weather?zip=${zip},US&appid=YOUR_API_KEY` ) const json = await response.json() return { zip, city: json.name, conditions: json.weather[0].main, temp: Math.round(((json.main.temp - 273.15) * 9) / 5 + 32), icon: `https://openweathermap.org/img/wn/${json.weather[0].icon}@2x.png` } }再次在 GraphQL Playground 点击 Play,即可看到来自 OpenWeather 的真实数据。与客户端方案相比,API Key 完全保留在服务端,不向浏览器暴露。
用 Cell 在客户端展示天气
Redwood Cell 封装了查询、加载、空态、失败、成功等全部状态渲染逻辑,是消费自家 GraphQL 接口的标准方式。先用生成器创建 Cell 骨架:
yarn rw generate cell weather生成web/src/components/WeatherCell/WeatherCell.js,初始内容为:
export const QUERY = gql` query FindWeatherQuery($id: Int!) { weather: weather(id: $id) { id } } ` export const Loading = () => <div>Loading...</div> export const Empty = () => <div>Empty</div> export const Failure = ({ error }) => ( <div style={{ color: 'red' }}>Error: {error.message}</div> ) export const Success = ({ weather }) => { return <div>{JSON.stringify(weather)}</div> }把QUERY改为匹配我们自定义的 API 签名:
export const QUERY = gql` query GetWeatherQuery($zip: String!) { weather: getWeather(zip: $zip) { zip city conditions temp icon } } `注意weather: getWeather的别名用法:实际调用的是getWeather端点,但返回结果会被重命名为weather,并作为Success组件的 props 传入。
在HomePage中使用该 Cell,并引入 state 记录用户何时提交了邮编:
import { Form, TextField, Submit } from '@redwoodjs/forms' import { useState } from 'react' import WeatherCell from 'src/components/WeatherCell' const HomePage = () => { const [zip, setZip] = useState() const onSubmit = (data) => { setZip(data.zip) } return ( <> <Form onSubmit={onSubmit} style={{ fontSize: '2rem' }}> <TextField name="zip" placeholder="Zip code" maxLength="5" validation={{ required: true, pattern: /^\d{5}$/ }} /> <Submit>Go</Submit> </Form> {zip && <WeatherCell zip={zip} />} </> ) } export default HomePage浏览器中应能看到 GraphQL 返回的 JSON。最后美化Success组件:
export const Success = ({ weather }) => { return ( <section> <h1>{weather.city}</h1> <h2> <img src={weather.icon} style={{ maxWidth: '2rem' }} /> <span> {weather.temp}°F and {weather.conditions} </span> </h2> </section> ) }进阶:处理无效邮编(错误校验)
如果用户输入了不存在的邮编(如11111),Service 在解析 OpenWeather 响应时找不到weather数组中的数据点,前端会抛出一个难以阅读的异常。查看此时 OpenWeather 的实际响应:
{ "cod": "404", "message": "city not found" }因此在 Service 中检查cod字段,若为404则抛出一个对用户友好的 GraphQL 错误。UserInputError由 Redwood 的 GraphQL 服务器提供(见 packages/graphql-server/src/errors.ts,其扩展错误码为BAD_USER_INPUT):
import { fetch } from '@whatwg-node/fetch' import { UserInputError } from '@redwoodjs/graphql-server' export const getWeather = async ({ zip }) => { const response = await fetch( `https://api.openweathermap.org/data/2.5/weather?zip=${zip},US&appid=YOUR_API_KEY` ) const json = await response.json() if (json.cod === '404') { throw new UserInputError(`${zip} isn't a valid US zip code, please try again`) } return { zip, city: json.name, conditions: json.weather[0].main, temp: Math.round(((json.main.temp - 273.15) * 9) / 5 + 32), icon: `https://openweathermap.org/img/wn/${json.weather[0].icon}@2x.png`, } }再次提交11111,错误信息会以可读的形式返回。最后,在 Cell 的Failure组件中把错误渲染得更像一条真正的错误提示,去掉 "Error: " 前缀:
export const Failure = ({ error }) => ( <span style={{ backgroundColor: '#ffdfdf', color: '#990000', padding: '0.5rem', display: 'inline-block', }} > {error.message} </span> )总结
本文以「邮编查天气」为例,走通了 Redwood 应用中集成第三方 API 的完整链路:
- 客户端直连:简单、快速,但 API Key 暴露且无法限流,仅适合低风险场景;
- 服务端代理:通过自建 GraphQL SDL + Service 屏蔽第三方 API 的细节,可以在服务端完成字段裁剪、单位转换、错误归一化,API Key 安全地保存在服务端;
- Cell 消费:
yarn rw g cell生成器配合QUERY/Loading/Empty/Failure/Success组件约定,一站式处理数据获取与各状态渲染; - 错误处理:借助
UserInputError与 Cell 的Failure组件,把上游 API 的原始错误转化为对用户友好的提示。
理解背后机制时,可以深入阅读本仓库中以下源码:
- packages/graphql-server/src/errors.ts:
UserInputError等 GraphQL 错误类定义(错误码BAD_USER_INPUT); - packages/graphql-server/src/directives/makeDirectives.ts:
@requireAuth、@skipAuth等校验指令的创建与绑定机制; - packages/forms/src/Form.tsx 与 packages/forms/src/coercion.ts:
<Form>组件基于react-hook-form的封装、校验与空值转换策略。
掌握了这套「客户端消费自家 GraphQL、服务端代理第三方 API」的模式,任何 HTTP 风格的第三方服务都可以平滑接入你的 Redwood 应用。
- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
相关推荐
Redwood 应用集成第三方 API 实战:以 OpenWeather 天气查询为例的客户端与服务端双方案
Redwood 应用集成第三方 API 实战:以 OpenWeather 天气查询为例的客户端与服务端双方案 本篇技术指南以 Redwood 框架(Redwoo
后端前端Web框架开发工具Redwood 实战:在前后端集成第三方 API(以 OpenWeather 天气应用为例)
Redwood 实战:在前后端集成第三方 API(以 OpenWeather 天气应用为例) 导读 在真实业务中,数据往往并不都在你自己的数据库里——你可能需要
后端前端Web框架开发工具Redwood 接入第三方 API 实战:基于 OpenWeather 构建天气查询应用(客户端与服务端双方案)
Redwood 接入第三方 API 实战:基于 OpenWeather 构建天气查询应用(客户端与服务端双方案) 导读 本文基于 Redwood 官方 How
后端前端Web框架开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考