react 同构初步(4)

2020-01-03 15:06:51 浏览数 (1)

这是一个即时短课程的系列笔记。本笔记系列进度已更新到: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去看看它的源码:

代码语言: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);}

那么服务端的跳转逻辑就完成了。

0 人点赞