6 React项目架构 - web server网络请求转发

 

相关commit

开发环境工程架构示意图

接口地址:https://cnodejs.org/api

accessToken

问题:

解决方案:

实现

安装工具

$ npm i body-parser express-session query-string -S

说明:

配置web服务器

server.js

...
const bodyParser = require('body-parser')
app.use(bodyParser.json())  // #1-1
app.use(bodyParser.urlencoded({ extended: false })) //  #1-2

const session = require('express-session')
app.use(session({   // #2
  maxAge: 10 * 60 * 1000,   // #2-1
  name: 'sid',  // #2-2
  resave: false,    // #2-3
  saveUninitialized: false,
  secret: '(‘◇’)?'  // #2-4
}))
...

说明:

  1. body-parser用于解析request的body中的参数
    1. 把application json的数据,转化成req.body的数据
    2. 对应formdate的数据 - 把这些数据也转换到req.body中,之后通过req.body就能拿到请求里的参数了
  2. 配置服务器session相关功能的
    1. 设置session的过期时长
      • 10分钟
      • 做测试用的,所以不需要太长
      • 线上的session是要存在数据库中的,如redis
      • 现在是存在内存中,服务关掉了,相关数据就没了,所有人都需要重新登录一遍
    2. 指定sessionID对应的cookieID,浏览器端可以通过cookieID拿到sessionID的值
    3. 是否每次请求都申请一个cookieID
    4. secret是一个密钥串,用于对session加密,保证cookie在浏览器端无法解密

验证accessToken

接口:

post /accesstoken 验证 accessToken 的正确性

接收 post 参数

  • accesstoken String 用户的 accessToken

如果成功匹配上用户,返回成功信息。否则 403。

返回值示例

{success: true, loginname: req.user.loginname, id: req.user.id, avatar_url: req.user.avatar_url}

逻辑:

server/util/handler_login.js

const router = require('express').Router()
const axios = require('axios')

const baseUrl = 'https://cnodejs.org/api/v1'

router.post('/login', function(req, res, next) {
  axios.post(`${baseUrl}/accesstoken`, {
    accesstoken: req.body.accessToken
  })
    .then((resp) => {
      if (resp.status === 200 && resp.data.success) {
        req.session.user = {
          accessToken: req.body.accessToken,
          loginName: resp.data.loginname,
          id: resp.data.id,
          avatarUrl: resp.data.avatar_url,
        }
        res.json({
          success: true,
          data: resp.data
        })
      }
    })
    .catch((err) => {
      if (err.response) {   // #1
        res.json({
          success: false,
          data: err.response.data   // #3
        })
      } else {  // #2
        next(err)
      }
    })
})


module.exports = router

说明:

  1. 如果cnode服务端返回了报错信息,表明是业务逻辑报错,直接返回err.response给用户
  2. 如果cnode没报错(发生了非业务逻辑错误),则把错误交给「全局错误处理器」处理
  3. 如果直接写err.response的话,res.json会把err.response这串东东整个转成字符串,但是response对象太大了,无法转成字符串,会导致响应一致无法返回客户端
    1. 报错信息:(node:9519) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): TypeError: Converting circular structure to JSON


其他接口的代理转发

server/util/proxy.js

const axios = require('axios')
const querystring = require('query-string') // #7

const baseUrl = 'https://cnodejs.org/api/v1'

module.exports = function(req, res, next) {
  const path = req.path
  const user = req.session.user || { }  // #2 防止undfine产生报错
  const needAccessToken = req.query.needAccessToken // #1

  if (needAccessToken && !user.accessToken) {
    res.status(401).send({
      success: false,
      msg: 'need login'
    })
  }

  const query = Object.assign({}, req.query, {
    accesstoken: (needAccessToken && req.method === 'GET') ? user.accessToken : '' // #8
  })

  if (query.needAccessToken) delete query.needAccessToken // #3

  axios(`${baseUrl}${path}`, {
    method: req.method,
    params: query,
    data: querystring.stringify(Object.assign({}, req.body, {
        (needAccessToken && req.method === 'POST') ? user.accessToken : ''  // #4 所有POST请求都加accessToken
    })),
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded' // #5 指定使用form-data格式发送请求-由于服务端设定导致的
    }
  }).then((resp) => {
    if (resp.status === 200) {
      res.send(resp.data)
    } else {
      res.status(resp.status).send(resp.data) // #6 链式调用,给client返回错误信息
    }
  }).catch((err) => {
    if (err.response) {
      res.status(500).send(err.response.data)
    } else {
      res.status(500).send({
        success: false,
        msg: '未知错误'
      })
    }
  })
}

说明:

  1. 通过客户端传过来的url中,是否有needAccessToken=true判断是否需要给服务端传accessToken

  2. 下面需要通过user.xxx调用相关方法,需要防止user为undefine时导致报错,故在此处做初始化

  3. 干掉web服务器和client之间的私自约定的query参数

  4. 所有发向cnode的POST请求,在body里拼上accessToken,即使cnode接口不需要,加上也OK
    1. 虽然表面上看上去,是从session.user中拿accessToken的,但是其实,user信息是存在web服务器中的,session.user这步操作,会通过sessionID,在web服务器中找到对应的user,然后拿accessToken
  5. cnode的api有些无法接收json,所以需要指定下Content-Type,用form-data的格式去发请求
  6. 链式调用,给client返回错误信息和状态码

  7. 如果不用querystring.stringify转码,会报500错误 - accesstoken不正确

    1. 插件:`npm i query-string -S`

    2. 如果不使用格式转换工具的话,格式会类似:{"accesstoken": "xxx"}

    3. 使用后,格式类似我们在浏览器端使用「表单」请求数据的格式一样

  8. GET方法,把accessToken拼在query后面...Object.assign语法蛮好玩的...


server.js调用相关转发方法

...
app.use('/api/user', require('./util/handler-login'))
app.use('/api', require('./util/proxy'))

if (!isDev) {
...

说明:


测试

普通接口:http://localhost:2333/api/topics


accessToken接口:

实现测试页面 client/views/test/api.test.js

import React from 'react'
import axios from 'axios'

/* eslint-disable */    // #1
export default class TestApi extends React.Component {

  getTopics() {
    axios.get('/api/topics')
      .then((resp) => {
        console.log(resp)
      }).catch((err) => {
        console.log(err)
    })
  }

  login() {
    axios.post('/api/user/login', {
      accessToken: 'xxxxxx-xxx-xxxx-xx'
    }).then(resp => {
      console.log(resp)
    })
  }

  markAll() {
    axios.post('/api/message/mark_all?needAccessToken=true')
      .then(resp => {
        console.log(resp)
      })
  }

  render() {
    return (
      <div>
        <button onClick={this.getTopics}>topics</button>
        <button onClick={this.login}>login</button>
        <button onClick={this.markAll}>markAll</button>
      </div>
    )
  }
}
/* eslint-enable */


说明:

  1. 禁止eslint检测区块


router.jsx添加测试页面入口

...
import TestApi from '../views/test/api.test'

export default () => [
...
  <Route path='/test' component={TestApi} key="test" />,
]

注意:

webpack.config.client.js

if (isDev) {
    ...
    config.devServer = {
        host: '0.0.0.0',
        port: '8787',
        ...
        proxy: {
          '/api': 'http://localhost:2333'
        }
    };
    ...
}

测试方式:

  1. 起服务:dev:client dev:web:start
  2. 访问http://localhost:8787/test
  3. 点击login
  4. 观察Network