axios代理
用代理规避跨域其实是很简单的事情,在往期的文章中已经有过类似的案例。但现在需要用"中台"的角度去思考问题。当前的项目分为三大部分:客户端(浏览器),同构服务端(nodejs中台,端口9000)和负责纯粹后端逻辑的后端(mockjs,端口9001)。
到目前为止的代码中,客户端如果要发送请求,会直接请求到mock.js。现实中接口数据来源不一定是node服务器,很可能是java,php或是别的语言。因此,从客户端直接请求会发生跨域问题。而要求后端为他的接口提供的跨域支持,并非是件一定能够满足到你的事。
如果从server端(中台)渲染,跨域就不会发生。于是就衍生了一个问题:客户端能否通过中台获取mockjs的信息?
解决的思路在于对axios也进行同构(区分客户端和服务端)。
redux-chunk传递axios对象
在前面的实践中,我们用到了redux-chunk。
redux-chunk是一个redux中间件,它可以把异步请求放到action中,它实现非常简单,不妨打开node_modules去看看它的源码:
代码语言:javascript复制// node_modules/redux-chunk/src/index
function createThunkMiddleware(extraArgument) {
// 高阶函数
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
// 注意以下两句代码:
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
发现thunk是createThunkMiddleware()的返回值。
我们之前引入chunk时,都是引入直接使用。但是它还有一个withExtraArgument
属性,又刚好提供了createThunkMiddleware()
方法。
顾名思义,withExtraArgument
就是提供额外的参数。当你调用此方法时,createThunkMiddleware
就会被激活。非常适合拿来传递全局变量。
我们在store.js中添加两个axios,分别对应客户端和中台:
代码语言:javascript复制// 储存的入口
import { createStore, applyMiddleware, combineReducers } from "redux";
import thunk from 'redux-thunk';
import axios from 'axios';
import indexReducer from './index';
import userReducer from './user';
const reducer = combineReducers({
index: indexReducer,
user: userReducer
});
// 创建两个axios,作为参数传递进createStore
const serverAxios=axios.create({
baseURL:'http://localhost:9001'
});
// 客户端直接请求服务端(中台),因此不需要再加个前缀
const clientAxios=axios.create({
baseURL:'/'
});
// 创建store
export const getServerStore = () => {
return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));
}
export const getClientStore = () => {
// 把初始状态放到window.__context中,作为全局变量,以此来获取数据。
const defaultState = window.__context ? window.__context : {};
return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)))
}
回到store/index.js和user.js,在定义请求的地方就会多出一个参数,就是我们定义的axios对象:
代码语言:javascript复制
// store/index.js
// 不再需要引入axios,直接用参数中的axios
export const getIndexList = server => {
return (dispatch, getState, $axios) => {
return $axios.get('/api/course/list').then((res)=>{
const { list } = res.data;
console.log('list',list)
dispatch(changeList(list));
}).catch(e=>{
// 容错
return dispatch(changeList({
errMsg:e.message
}));
});
}
}
代码语言:javascript复制
// store/user.js
export const getUserInfo = server => {
return (dispatch, getState, $axios) => {
// 返回promise
return $axios.get('/api/user/info').then((res) => {
const { info } = res.data;
console.log('info', info);
dispatch(getInfo(info));
}).catch(e => {
console.log(e)
// 容错
return dispatch(getInfo({
errMsg: e.message
}));
})
}
}
留意到这里接口多了一个/api/,是为了对路由做区分。我们在mockjs中也增加api。同时取消跨域设置
代码语言:javascript复制// mockjs单纯模拟接口
const express=require('express');
const app=express();
app.get('/api/course/list',(req,res)=>{
res.json({
code:0,
list:[
{id:1,name:'javascript 从helloworld到放弃'},
{id:2,name:'背锅的艺术'},
{id:3,name:'撸丝程序员如何征服女测试'},
{id:4,name:'python从入门到跑路'}
]
});
});
app.get('/api/user/info',(req,res)=>{
res.json({
code:0,
info:{
name:'党某某',
honor:'首席背锅工程师'
}
});
});
app.listen('9001',()=>{
console.log('mock has started..')
});
此时,当数据为空时,前端就会对9000端口发起api请求。
请求转发
现在来处理服务端(中台)的逻辑,在server/index.js下,你可以很直观地这么写:
代码语言:javascript复制
// 监听所有页面
app.get('*', (req, res) => {
// 增加路由判断:api下的路由全部做转发处理:
if(req.url.startWith('/api')){
// 转发9001
}
// ...
});
但是这种面向过程编程的写法并不是最好的实践。因此考虑通过中间件处理这种逻辑。在express框架,http-proxy-middlewere可以帮助我们实现此功能。
文档地址:https://github.com/chimurai/http-proxy-middleware
代码语言:javascript复制npm i http-proxy-middleware -S
代码语言:javascript复制
// 使用方法
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use(
'/api',
proxy({ target: 'http://www.example.org', changeOrigin: true })
);
app.listen(3000);
// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar
安装好后,如法炮制:
代码语言:javascript复制
// server/index.js
import proxy from 'http-proxy-middleware';
// ...
app.use(
'/api',
proxy({ target: 'http://localhost:9001', changeOrigin: true })
);
这时候在客户端接口,就会看到中台9000转发了后台9001的数据了:
由此,中台代理后台请求功能完成。
图标/样式
现在的同构应用,有个不大不小的问题:在network中,请求favicon.ico总是404。
我们从百度盗一个图标过来:https://www.baidu.com/favicon.ico
下载下来然后塞到public中即可。
当前的应用实在太丑了。客户说:"我喜欢字体那种冷冷的暖,暖暖的冷。"
在src下面创建style文件夹,然后创建user.css
代码语言:javascript复制* { color:red}
在container/user.js中引入css:
代码语言:javascript复制import '../style/user.css';
此时运行页面还是报错的,想要让它支持css样式,需要webpack层面的支持。
先配置客户端和服务端webpack:
代码语言:javascript复制
// webpack.client.js
// webpack.server.js
{
test:/.css$/,
use:['style-loader','css-loader']
}
配好之后,你满心欢喜地npm start:
document对象在server 层根本是不存在的。因此需要安装专门做同构应用的style-loader:isomorphic-style-loader(https://github.com/kriasoft/isomorphic-style-loader)
代码语言:javascript复制npm i isomorphic-style-loader -S
对server端webpack做单独配置:
代码语言:javascript复制
{
test: /.css$/,
use: [
'isomorphic-style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1
}
}
]
}
刷新:
你会发现整个页面都红了。查看源代码,发现css是直接插入到header的style标签中的,直接作用于全局。
如何对样式进行模块化(BEM)处理?将在后面解决。
状态码支持
当请求到一个不匹配的路由/接口,如何优雅地告诉用户404?
现在把Index的匹配去掉,增加404NotFound组件:
代码语言:javascript复制
// App.js
import NotFound from './container/NotFound';
export default [
// ...
{
component:NotFound,
key:'notFound'
}
]
404页面:
代码语言:javascript复制
// container/NotFound.js
import React from 'react';
function NotFound(props){
return <div>
<h1>404 你来到了没有知识的星球..</h1>
<img id="notFound" src="404.jpeg" />
</div>
}
export default NotFound;
然后在header组件中加上一条404路由:
代码语言:javascript复制<Link to={`/${Math.random()}`}>404</Link>
刷新,看到了404的请求:
为什么是200?此时应该是404才对。
去官网学习下:
https://reacttraining.com/react-router/web/guides/server-rendering
We can do the same thing as above. Create a component that adds some context and render it anywhere in the app to get a different status code.
代码语言:javascript复制function Status({ code, children }) {
return (
<Route
render={({ staticContext }) => {
if (staticContext) staticContext.status = code;
return children;
}}
/>
);
}
// Now you can render a Status anywhere in the app that you want to add the code to staticContext.
function NotFound() {
return (
<Status code={404}>
<div>
<h1>Sorry, can’t find that.</h1>
</div>
</Status>
);
}
function App() {
return (
<Switch>
<Route path="/about" component={About} />
<Route path="/dashboard" component={Dashboard} />
<Route component={NotFound} />
</Switch>
);
}
你可以传递一个全局的context对象给你创建的notfound组件。
在server/index.js的promise循环中定义一个context空对象,传递给路由组件:
代码语言:javascript复制Promise.all(promises).then(data => {
// 定义context空对象
const context={};
// react组件解析为html
const content = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
...
回到NotFound.js,看下它的props,客户端多了一个undefined的staticContext。但是在server端打印的是{}。这是在服务端渲染路由StaticRouter
的独有属性:所有子路由都能访问。
在Notfound中定义一个Status组件用来给staticContext赋值:
代码语言:javascript复制import React from 'react';
import { Route } from 'react-router-dom';
function Status({ code, children }) {
return <Route render={(props) => {
const { staticContext } = props;
if (staticContext) {
staticContext.statusCode = code;
}
return children;
}} />
}
function NotFound(props) {
// props.staticContext
// 给staticContext赋值 statusCode=404
console.log('props', props)
return <Status code={404}>
<h1>404 你来到了没有知识的星球..</h1>
<img id="notFound" src="404.jpeg" />
</Status>
}
export default NotFound;
回到server/index.js就可以在renderToString之后拿到带有statusCode的context了。
代码语言:javascript复制const context = {};
const content = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<Header />
{routes.map(route => <Route {...route} />)}
</StaticRouter>
</Provider>
);
if (context.statusCode) {
res.status(context.statusCode)
}
这时候就看到404状态了。现在404是非精确匹配的。想要渲染,可以用switch组件来实现
代码语言:javascript复制// server/index.js
import { StaticRouter, matchPath, Route, Switch } from 'react-router-dom';
const content = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<Header />
<Switch>
{routes.map(route => <Route {...route} />)}
</Switch>
</StaticRouter>
</Provider>
);
然后在客户端也做一个同构处理:
代码语言:javascript复制
import { BrowserRouter, Switch} from 'react-router-dom';
const Page = (<Provider store={getClientStore()}>
<BrowserRouter>
<Header/>
<Switch>
{routes.map(route => <Route {...route} />)}
</Switch>
</BrowserRouter>
</Provider>);
404功能搞定
又比如说我要对user页面做登录支持,当访问user页面时,做301重定向:
代码语言:javascript复制
// container/User.js
import {Redirect} from 'react-router-dom';
function User(props){
// 判断cookie之类。。。
return <Redirect to={'/login'}></Redirect>
// ..
}
定义了redirect,此时context的action是替换("REPLACE"),url是跳转的地址。因此在服务端可以这么判断
代码语言:javascript复制if (context.action=='REPLACE') { res.redirect(301,context.url);}
那么服务端的跳转逻辑就完成了。