⑤ ReactSSR node中间层代理请求

代理请求

由于客户端和服务端公用一套请求的接口,所以需要接口同时适应客户端和移动端,这里可以选用 cross-fetchaxios

当在异步 action 中请求数据时,我们希望请求的是同域的服务

1
2
3
export const loadData =
() => (dispatch: Dispatch, getState: any, request: AxiosInstance) =>
axios("/api/products").then(({ data }) => dispatch(loadDataAction(data)));

而不是直接请求后端服务器。所以需要将 api 开头的请求转发到后端服务器请求。用到了一个 koa 的中间件 koa-proxies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import Koa from 'koa';
import koaBody from 'koa-body';
import koaStatic from 'koa-static';
import proxy from 'koa-proxies';
import path from 'path';
import router from '../router'

import errorHandle from './errorHandle'

const app = new Koa();

app
.use(async (ctx:any, next:()=>Promise<any>) => {
try {
await next();
} catch (err) {
ctx.app.emit('error', err, ctx);
}
})
.use(proxy('/api',{
target: 'https://fakestoreapi.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/,''),
}))
.use(koaStatic(path.join((process.env as any).PWD,'./static')))
.use(koaBody())
.use(router.routes())
.use(router.allowedMethods())

app.on('error', errorHandle);

export default app;

处理请求

现在客户端正常访问是可以的,但是服务端会报出错误,因为当刷新页面的时候服务端会做服务端渲染,这时直接调用了组件中获取数据的方法。

由于组件中的路径是以 /api 开头的绝对路径,所以会尝试在服务器中查找根路径下api文件夹,因为找不到报错错误。

一个思路是,区分服务端的请求和客户端的请求,分别为其创建不同的 axios 实例用于请求,但是为了避免像上一章中,每个请求分两种写,可以考虑在项目初始化的时候创建不同的 axios 实例,并通过参数传递到请求方法中,从而避免业务逻辑太多冗余。

src/util/request.ts

定义一个请求方法,为服务端和客户端创建不同实例

由于后端服务并不是api开头的接口,所以后端访问时,需要为其重写url路径.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import axios from "axios";

const serverInstance = axios.create({
baseURL: "https://fakestoreapi.com",
adapter: function (config) {
/* ... */
config.url = config.url?.replace(/^\/api/, "");
delete config.adapter;
return new Promise((resolve) => {
resolve(axios(config));
});
},
});

const clientInstance = axios.create({
baseURL: "/",
});

export { serverInstance, clientInstance };

在初始化 store 的时候,通过中间件把 axios 实例传入,让所用的异步 action 在请求前可以通过第三个参数拿到 axios 实例

src/store/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import reducers from './reducers';
import { StoreType as HelloStoreType } from '../components/hello';
import {clientInstance,serverInstance} from '../util/request'

const browserStore = ()=> createStore(reducers, (window as any).__HYDRATE_DATA__, applyMiddleware(thunk.withExtraArgument(clientInstance)));

const serverStore = () => createStore(reducers, applyMiddleware(thunk.withExtraArgument(serverInstance)));

export type StoreType = {
hello: HelloStoreType
}

export {
Provider,
browserStore,
serverStore
}

src/components/hello/action.ts

修改 action 方法,通过 axios 实例请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Dispatch, ActionCreator } from "redux";
import { AxiosInstance } from "axios";
import {} from "redux";

export const LOAD_DATA = "LOAD_DATA";

export type LOAD_DATA_TYPE = typeof LOAD_DATA;

const loadDataAction: ActionCreator<{ type: LOAD_DATA_TYPE }> = (payload) => ({
type: LOAD_DATA,
payload,
});

export type ActionTypes = LOAD_DATA_TYPE;
export const loadData =
() => (dispatch: Dispatch, getState: any, request: AxiosInstance) =>
request("/api/products").then(({ data }) => dispatch(loadDataAction(data)));

export const serverLoadData = loadData;

最后一步,由于我们统一了调用方法,现在服务端也会通过异步 action 方法调用接口

所以需要让服务端调用方法的时候,也像客户端一样通过bindActionCreators传入dispatch方法

通过服务端创建的store传入了dispatch方法,并且让中间件的参数生效。这时也不需要再组合不同接口返回的state,通过异步action方法,在拿到返回值之后,dispatch会触发并更新store

当所有的组件异步数据请求之后,在通过getState获取最新的store渲染页面

src/router/index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const router = new Router();

router.get("/(.*)", async (ctx) => {
const store = serverStore();
const promises: Array<any> = matchRoutes(routes, ctx.request.path).map(
({ route, match }) => {
return route.loadData
? bindActionCreators(route.loadData, store.dispatch)()
: Promise.resolve();
}
);
await Promise.all(promises);

ctx.body = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='root'>${ReactDOMServer.renderToString(
<App {...{ ctx, store }} />
)}</div>
<script>
window.__HYDRATE_DATA__ = ${JSON.stringify(
store.getState()
)}
</script>
<script src='/index.js' defer></script>
</body>
</html>
`;
});

export default router;
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信