React-Slingshot 全栈集成教程:6 步把 React 前端和 Node.js 后端 API 打通
【免费下载链接】react-slingshotReact + Redux starter kit / boilerplate with Babel, hot reloading, testing, linting and a working example app built in项目地址: https://gitcode.com/gh_mirrors/re/react-slingshot
浏览器控制台里一行红色报错:CORS policy: No 'Access-Control-Allow-Origin'。页面能渲染,表单能填,唯独一提交就被拦下来。如果你的前端项目是 React-Slingshot(React + Redux starter kit),这篇教程带你按真实联调顺序把前端集成后端的全栈链路走通:本地双服务跑起来 → axios 请求层 → Express 最小后端 → Redux 数据流 → 生产构建注入 API 地址,终点是「页面按钮一按,数据从后端 4000 端口的接口流进 store 并渲染出来」。
先复现一次:前端为什么调不通后端接口
联调卡在第一步时,90% 的情况不是代码写错,而是两个服务压根没对齐。拿 React-Slingshot 来说,它的开发服务器由 tools/srcServer.js 驱动,默认占用3000 端口,同时 BrowserSync 的 UI 面板占掉3001——这里第一个坑就埋好了:如果你习惯让后端也跑 3001,两个服务会直接打架。
先明确本文的终点状态,后面每一步都以它为验收标准:
- 前端跑在
http://localhost:3000,后端跑在http://localhost:4000; - 打开页面时,DevTools 的 Network 面板里能看到对
/api/fuel/rates的请求返回 200; - Redux DevTools 里能看到
FETCH_FUEL_RATES_SUCCESS这个 action 被派发,payload 是接口返回的 JSON。
后面每一步都围绕这个状态推进,任何一步做完都可以对照检查,不用等全部搭完才发现问题。
画出链路:请求从页面到后端的 6 个节点
动手前把链路在脑子里过一遍,出问题时才能快速定位是哪一段断的:
本地环境(3000/4000) → 请求层(src/utils/api.js) → Express 后端(4000) ↑ ↓ 生产构建(Dist+PM2) ← 验证(curl/jest/DevTools) ← 状态流(thunk action → reducer → 容器组件)用一句话串起来:axios 实例负责"怎么发",thunk action 负责"什么时候发、发完往 store 里放什么",reducer 负责"数据落到 state 的哪个位置",容器组件负责"从 state 里取出来渲染"。环境、请求层、后端、状态流、验证、上线,后文按这个顺序逐段搭建,每段都有可复制的代码和一句验证方法。
克隆仓库并跑通最小环境
先把前端自身跑起来,确认工具链没问题,再谈联调。
# 克隆 starter kit(仓库自带示例应用 FuelSavings,正好当联调载体) git clone https://gitcode.com/gh_mirrors/re/react-slingshot cd react-slingshot npm install # npm start 会同时起测试、lint 和开发服务器(见 package.json 里 concurrently 的定义) npm start预期看到什么:浏览器自动打开http://localhost:3000,显示 FuelSavings 示例表单;终端里能看到 test:watch、lint:watch、open:src 三路输出。如果卡在npm install,先按 docs/FAQ.md 里 "How do I debug?" 一节排查。
跑通之后,花两分钟定位三个关键文件,后面改哪段都靠它们:
| 文件 | 作用 | 后面会动它吗 |
|---|---|---|
| package.json | start/build等脚本、依赖清单 | 只加一个 axios |
| webpack.config.dev.js / webpack.config.prod.js | 开发/生产构建,process.env注入点 | 都加一行 API 地址 |
| src/store/configureStore.js | Redux store 装配,thunk 中间件已注册 | 不动 |
注意 src/store/configureStore.js 里 dev 和 prod 两条分支都已经挂上了thunk——这意味着异步 action 不用额外配置中间件,这是这个 starter 白送的好处,很多教程会在这一步劝退你。
最后补一个 starter 里没有的依赖:
npm install axios验证方式:npm start的 lint:watch 输出里没有新增报错,终端重新编译完成即可,说明依赖图正常。
环境干净了,接下来把"怎么发请求"收敛到一个地方,避免每个组件各自 new 一个 axios。
封装统一的请求层:axios 实例加认证拦截器
新建src/utils/api.js:
import axios from 'axios'; // baseURL 由 webpack 的 DefinePlugin 注入(dev/prod 配置里各加一行,见"上线"一节) // 兜底值保证本地开发没配环境变量时也能直连 4000 端口的后端 const api = axios.create({ baseURL: process.env.API_URL || 'http://localhost:4000/api', timeout: 5000, headers: {'Content-Type': 'application/json'}, }); // 请求拦截器:本地有登录态时自动附带 Bearer token, // 业务代码里就不用每个请求手写 Authorization 头 api.interceptors.request.use(config => { const token = localStorage.getItem('authToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器:非 2xx 统一抛错,action 里只写 catch 分支 api.interceptors.response.use( response => response, error => Promise.reject(error) ); export default api;为什么集中管理:baseURL 换环境只改一处;token 注入、错误归一化只写一遍;将来换掉 axios 或加重试逻辑,改动范围被锁死在这个文件里。
验证方式:npm start触发热更新,lint:watch 输出无新报错;文件被引用后构建能正常通过(下一节马上就会引用它)。
请求层就位后,另一端该立起来了——一个十分钟能搭完、curl 能打通的 Express 服务。
搭一个十分钟能 curl 通的 Express 最小后端
后端刻意放在前端仓库外面,两个项目独立部署、独立生命周期。
# 与 react-slingshot 平级建目录 mkdir fuel-api && cd fuel-api npm init -y npm install express corsserver.js保持最小化,只保留联调必需的三样东西:
// server.js const express = require('express'); const cors = require('cors'); const app = express(); // 只放行前端的真实来源。开发期是 localhost:3000, // 生产环境换成前端域名;不要图省事写 '*',带 cookie 时不安全 app.use(cors({origin: 'http://localhost:3000'})); app.use(express.json()); // 解析 JSON 请求体,处理 POST 时必备 // 最小只读接口:返回当前油价,和前端 FuelSavings 场景对得上 app.get('/fuel/rates', (req, res) => { res.json({ gasoline: 3.89, diesel: 4.12, lastUpdated: new Date().toISOString(), }); }); // 端口走环境变量,默认 4000——避开前端占用的 3000/3001 const PORT = process.env.PORT || 4000; app.listen(PORT, () => console.log(`API listening on ${PORT}`));启动并验证:
node server.js curl http://localhost:4000/api/fuel/rates预期看到什么:curl 返回一段 JSON,包含"gasoline":3.89和时间戳。到这一步,后端这一段就算通了——先保证 curl 能通,再谈浏览器能不能通,排查顺序反了会浪费一倍时间。
后端有了,最后一段是把接口数据接进 Redux,让组件消费它。
把 Redux 异步 action 接到容器组件上
数据流走 starter 既有的四件套:常量 → action → reducer → 容器组件。逐段加,每段都不超过五行。
1)常量:在 src/constants/actionTypes.js 追加两个:
export const FETCH_FUEL_RATES_SUCCESS = 'FETCH_FUEL_RATES_SUCCESS'; export const FETCH_FUEL_RATES_FAILURE = 'FETCH_FUEL_RATES_FAILURE';2)异步 action:在 src/actions/fuelSavingsActions.js 里追加一个 thunk。starter 里的saveFuelSavings已经是 thunk 写法(注释里写着 "thunks allow for pre-processing actions, calling apis"),这里照同样的形状扩展:
import api from '../utils/api'; import * as types from '../constants/actionTypes'; // thunk 里发请求:成功把数据 dispatch 回 store,失败派发错误 action export function fetchFuelRates() { return async dispatch => { try { const {data} = await api.get('/fuel/rates'); dispatch({ type: types.FETCH_FUEL_RATES_SUCCESS, payload: data, }); } catch (error) { dispatch({ type: types.FETCH_FUEL_RATES_FAILURE, error: error.message, }); } }; }3)reducer:在 src/reducers/fuelSavingsReducer.js 的 switch 里加一个分支,保持现有objectAssign的不可变写法:
case FETCH_FUEL_RATES_SUCCESS: // 接口数据落到 state.fuelSavings.fuelRates,组件从这里读 return objectAssign({}, state, {fuelRates: action.payload});4)容器组件:在 src/components/containers/FuelSavingsPage.js 里把新 action 绑进 dispatch,并在挂载时触发一次拉取:
componentDidMount() { this.props.actions.fetchFuelRates(); // 页面挂载时向 4000 端口要一次油价 } // mapDispatchToProps 里把 action 绑上 dispatch(starter 现成写法是 // bindActionCreators,直接 import fetchFuelRates 一起绑即可)验证方式:npm start热更新后刷新页面,DevTools → Redux 面板应出现FETCH_FUEL_RATES_SUCCESS,state.fuelSavings.fuelRates里有gasoline: 3.89。到这里,整条链路在开发环境闭环了。
能跑起来不等于稳,下一步用 mock 测试锁住行为,再演练一遍双服务启动。
用 mock 测试和双服务启动验证全链路
先补一个单测,把"成功后 dispatch 带数据的 action"这个行为锁死。新建src/actions/fuelSavingsActions.spec.js(starter 的惯例是测试文件与被测文件同目录,见 docs/FAQ.md):
import api from '../utils/api'; import {fetchFuelRates} from './fuelSavingsActions'; import * as types from '../constants/actionTypes'; // 不真的发 HTTP 请求,整个 api 模块 mock 掉 jest.mock('../utils/api'); describe('fetchFuelRates', () => { it('成功后 dispatch 带接口数据的 action', async () => { const mockRates = {gasoline: 3.89, diesel: 4.12}; api.get.mockResolvedValue({data: mockRates}); const dispatch = jest.fn(); await fetchFuelRates()(dispatch); // 断言派发的是 success 类型 + 完整 payload expect(dispatch).toHaveBeenCalledWith({ type: types.FETCH_FUEL_RATES_SUCCESS, payload: mockRates, }); }); });npx jest fuelSavingsActions预期看到什么:Tests: 1 passed。这个测试保证后端挂掉时行为可预期——失败走FETCH_FUEL_RATES_FAILURE分支,不会让组件白屏。
再演练一遍完整的双服务启动(真实联调,非 mock):
# 终端 1:前端(3000 页面 / 3001 BrowserSync UI) cd react-slingshot && npm start # 终端 2:后端(4000) node fuel-api/server.js预期看到什么:打开http://localhost:3000,DevTools → Network 里/api/fuel/rates状态 200,Redux 面板出现对应 action。如果 Network 里是红色 CORS 报错,先查后端cors({origin})里写的来源和浏览器地址栏是否一字不差——这是本联调流程里最高频的一个坑。
本地稳定之后,剩下的事就是把环境差异固化到构建里。
生产构建注入 API 地址并部署上线
React-Slingshot 生产构建的环境变量入口是 webpack.config.prod.js 顶部现成的GLOBALS对象,被DefinePlugin消费。加一行:
const GLOBALS = { 'process.env.NODE_ENV': JSON.stringify('production'), // 构建时写死生产 API 地址,随 bundle 一起发布 'process.env.API_URL': JSON.stringify('https://your-api-domain.com/api'), __DEV__: false };开发侧同理,在 webpack.config.dev.js 的plugins数组里补一个:
new webpack.DefinePlugin({ // 开发环境指向本地 4000 端口的 Express 'process.env.API_URL': JSON.stringify('http://localhost:4000/api'), }),为什么不用 .env 文件:这套 starter 的构建里没有接 dotenv,process.env里的值只有走DefinePlugin才会被替换进 bundle,直接读.env是读不到的。
构建与部署:
# 构建(prebuild 会先跑 lint + 全量测试 + 清理 dist,再输出带 contenthash 的产物) npm run build # 前端:dist/ 交给任意静态服务器(Nginx 即可), # 或先本地预览:npm run open:dist # 后端:PM2 托管,进程挂掉自动拉起 pm2 start fuel-api/server.js --name fuel-api pm2 save预期看到什么:dist/下出现bundle.[hash].js和 HTML;把dist/丢给静态服务器后,浏览器 Network 面板里对生产 API 域名的请求返回 200,且响应头里有Access-Control-Allow-Origin指向你的前端域名。最后用npm start的 test:watch 和 CI 里的npm run test兜底,确认生产构建前测试全绿。
到这一步功能闭环了,但上线前后还有几个高频坑值得单独列出来。
上线前过一遍这份坑清单
联调踩过的坑,按出现频率排个序,每条都对应本文前面某个环节:
- 端口撞车:前端占 3000,BrowserSync UI 占 3001,后端默认 3001 必挂。改后端端口比改 starter 省事。
- CORS origin 别写
*:带认证头或 cookie 时会被浏览器拒绝;生产环境 origin 必须换成前端真实域名,和部署地址一字不差。 - thunk 已经注册好了:src/store/configureStore.js 的 dev/prod 两条中间件链里都有
thunk,异步 action 直接写,别再叠 redux-promise 之类的中间件。 - 派生数据用选择器:接口数据进 store 之后,组件里反复算同一份派生值时,换成 reselect 缓存选择器,别在 render 里重复计算。
- localStorage 里的 token 防君子不防小人:它对所有 JS 可见,安全要求高时改用 httpOnly cookie + 后端校验,拦截器那层只负责透传。
回头看这 6 步,其实就一句话:请求层收口、后端最小化、状态流走 starter 现成的 thunk 通道、环境差异交给 DefinePlugin。整条链路每一段都独立可验证——curl 通后端、jest 锁行为、DevTools 看 store,哪段红了修哪段,不用猜。
想继续深入,docs/FAQ.md 里有 "How do I deploy this?" 和 "How do I debug?" 两节,前者讲 dist 产物的部署细节,后者含 VS Code 调试配置,比本文的口径更细。
【免费下载链接】react-slingshotReact + Redux starter kit / boilerplate with Babel, hot reloading, testing, linting and a working example app built in项目地址: https://gitcode.com/gh_mirrors/re/react-slingshot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考