react 同构初步(4)
- 2020 年 1 月 2 日
- 筆記

这是一个即时短课程的系列笔记。本笔记系列进度已更新到:https://github.com/dangjingtao/react-ssr
axios代理
用代理规避跨域其实是很简单的事情,在往期的文章中已经有过类似的案例。但现在需要用"中台"的角度去思考问题。当前的项目分为三大部分:客户端(浏览器),同构服务端(nodejs中台,端口9000)和负责纯粹后端逻辑的后端(mockjs,端口9001)。
到目前为止的代码中,客户端如果要发送请求,会直接请求到mock.js。现实中接口数据来源不一定是node服务器,很可能是java,php或是别的语言。因此,从客户端直接请求会发生跨域问题。而要求后端为他的接口提供的跨域支持,并非是件一定能够满足到你的事。
如果从server端(中台)渲染,跨域就不会发生。于是就衍生了一个问题:客户端能否通过中台获取mockjs的信息?
解决的思路在于对axios也进行同构(区分客户端和服务端)。
redux-chunk传递axios对象
在前面的实践中,我们用到了redux-chunk。
redux-chunk是一个redux中间件,它可以把异步请求放到action中,它实现非常简单,不妨打开node_modules去看看它的源码:
// 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,分别对应客户端和中台:
// 储存的入口 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对象:
// 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 })); }); } }
// 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。同时取消跨域设置
// 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下,你可以很直观地这么写:
// 监听所有页面 app.get('*', (req, res) => { // 增加路由判断:api下的路由全部做转发处理: if(req.url.startWith('/api')){ // 转发9001 } // ... });
但是这种面向过程编程的写法并不是最好的实践。因此考虑通过中间件处理这种逻辑。在express框架,http-proxy-middlewere可以帮助我们实现此功能。
文档地址:https://github.com/chimurai/http-proxy-middleware
npm i http-proxy-middleware -S
// 使用方法 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
安装好后,如法炮制:
// 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
* { color:red}
在container/user.js中引入css:
import '../style/user.css';
此时运行页面还是报错的,想要让它支持css样式,需要webpack层面的支持。
先配置客户端和服务端webpack:
// 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)
npm i isomorphic-style-loader -S
对server端webpack做单独配置:
{ test: /.css$/, use: [ 'isomorphic-style-loader', { loader: 'css-loader', options: { importLoaders: 1 } } ] }
刷新:

你会发现整个页面都红了。查看源代码,发现css是直接插入到header的style标签中的,直接作用于全局。
如何对样式进行模块化(BEM)处理?将在后面解决。
状态码支持
当请求到一个不匹配的路由/接口,如何优雅地告诉用户404?
现在把Index的匹配去掉,增加404NotFound组件:
// App.js import NotFound from './container/NotFound'; export default [ // ... { component:NotFound, key:'notFound' } ]
404页面:
// 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路由:
<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.
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空对象,传递给路由组件:
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赋值:
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了。
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组件来实现
// 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> );
然后在客户端也做一个同构处理:
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重定向:
// container/User.js import {Redirect} from 'react-router-dom'; function User(props){ // 判断cookie之类。。。 return <Redirect to={'/login'}></Redirect> // .. }
定义了redirect,此时context的action是替换("REPLACE"),url是跳转的地址。因此在服务端可以这么判断
if (context.action=='REPLACE') { res.redirect(301,context.url);}
那么服务端的跳转逻辑就完成了。