前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >react 同构初步(4)

react 同构初步(4)

作者头像
一粒小麦
修改2020-01-03 15:06:51
1.8K0
修改2020-01-03 15:06:51
举报
文章被收录于专栏:一Li小麦一Li小麦
这是一个即时短课程的系列笔记。本笔记系列进度已更新到: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);}

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • axios代理
    • redux-chunk传递axios对象
      • 请求转发
      • 图标/样式
      • 状态码支持
      相关产品与服务
      消息队列 TDMQ
      消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档