likes
comments
collection
share

小白学习vue服务端渲染(二)--二次封装axios

作者站长头像
站长
· 阅读数 12

安装axioselement-uijs-cookiehttp-proxy-middleware

npm i axios element-ui js-cookie
npm i http-proxy-middleware@0.17.4 -D

app.js(项目入口文件)引入element-ui,添加如下内容:

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(ElementUI)

根目录下新建config目录

新建dev.env.js, 内容如下:

'use strict'
const { merge } = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
  BASE_URL: '"/mall"',
  VUE_APP_BASE_API: '"/user-management-service"'
})

新建prod.env.js,内容如下:

'use strict'

module.exports = {
  NODE_ENV: '"production"',
  BASE_URL: '"/mall"',
  VUE_APP_BASE_API: '"/user-management-service"'
}

配置webpack.base.config.js,添加内容如下:

const webpack = require('webpack')

new webpack.DefinePlugin({
  'process.env': isProd ? require('../config/prod.env') : require('../config/dev.env')
})

在src目录下新建utils目录

新建request.js,内容如下:

import axios from 'axios'
import { MessageBox, Message } from 'element-ui'

import { createStore } from '@/store'
import Cookies from 'js-cookie'

/**
 * 控制请求重试
 * @param {*} adapter 预增强的 Axios 适配器对象;
 * @param {*} options 缓存配置对象,该对象支持 2 个属性,分别用于配置不同的功能:
 *                      times:全局设置请求重试的次数;
 *                      delay:全局设置请求延迟的时间,单位是 ms。
 * @returns
 */
function retryAdapterEnhancer (adapter, options) {
  const { times = 0, delay = 300 } = options

  return async (config) => {
    const { retryTimes = times, retryDelay = delay } = config
    let __retryCount = 0

    const request = async () => {
      try {
        return await adapter(config)
      } catch (err) {
        if (!retryTimes || __retryCount >= retryTimes) {
          return Promise.reject(err)
        }

        __retryCount++

        // 延时处理
        const delay = new Promise((resolve) => {
          setTimeout(() => {
            resolve()
          }, retryDelay)
        })

        // 重新发起请求
        return delay.then(() => {
          return request()
        })
      }
    }

    return request()
  }
}
// create an axios instance
const service = axios.create({
  // 如果不存在跨域问题并且在dev.env.xxx文件中有配置的话baseURL的值可以直接使用:process.env.BASE_URL;
  // 如果使用proxy做了代理配置,那么baseURL的值直接填写'/'就可以了。
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  withCredentials: true, // send cookies when cross-domain requests
  timeout: 60000, // request timeout
  adapter: retryAdapterEnhancer(axios.defaults.adapter, {
    retryDelay: 1000
  })
})

service.defaults.withCredentials = true

// 请求拦截器
service.interceptors.request.use(
  config => {
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
  */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    const res = response.data
    const config = response.config

    // if the custom code is not 20000, it is judged as an error.
    if (config.responseType === 'blob') {
      // 下载请求,不抛错误
      return response
    }

    if (config.responseType === 'arraybuffer') {
      // 获取图片的二进制流
      return response
    }

    if (config.headers['Content-Type'] === 'application/octet-stream') {
      // 分片上传
      return response
    }

    if (res.code !== 200) {
      // 这里处理了非200错误,不需要再在每个页面里再去抛出请求错误
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })

      return Promise.reject(new Error(res.message || 'Error'))
    }

    return res
  },
  error => {
    console.log('err: ', error) // for debug

    const store = createStore()

    if (error.response) {
      if (error.response.status === 401) {
        // to re-login
        MessageBox.confirm('您的登录信息已过期,请重新登录', '确认登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          // 退出登录
          store.dispatch('user/logout').then(() => {
            location.href = '/mall'
          })
        })
      } else {
        // 主动取消请求,不提示错误
        const { message } = error.response.data

        if (!axios.isCancel(error)) {
          Message({
            message: message || error,
            type: 'error',
            duration: 3 * 1000
          })
        }
      }
    }

    return Promise.reject(error)
  }
)

export default service

保持登录状态

模拟nuxtnuxtServerInit,在store中新建nuxtServerInit这个action方法,内容如下:

nuxtServerInit ({ commit, state }, { request }) {
  let accessToken = null
  if (request && request.headers && request.headers.cookie) {
    const parsed = cookieParse(request.headers.cookie)
    try {
     accessToken = parsed.accessToken
    } catch (err) {
        // No valid cookie found
    }
    
    commit('initToken', accessToken)
  }
}

新建initToken这个mutation方法,内容如下:

import Cookies from 'js-cookie'

initToken: (state, accessToken) => {
    state.accessToken = accessToken
    Cookies.set('accessToken', accessToken)
}

修改server.js文件,内容如下:

/**
* 服务端入口,仅运行于服务端
*/
// 创建一个 express 的 server 实例
const express = require('express')
const server = express()
const fs = require('fs')
const cookieParser = require('cookie-parser')
const { createBundleRenderer } = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')

const isPro = process.env.NODE_ENV === 'production'
let renderer
let onReady

if (isPro) {
  const template = fs.readFileSync('./index.template.html', 'utf-8')
  // 生产模式,直接基于已构建好的包创建渲染器
  const serverBundle = require('./dist/vue-ssr-server-bundle.json')
  const clientManifest = require('./dist/vue-ssr-client-manifest.json')
  // 创建一个渲染器
  renderer = createBundleRenderer(serverBundle, {
    template, // (可选) 设置页面模板
    clientManifest // (可选) 客户端构建
  })
} else {
  // 开发模式 --> 监视打包构建(客户端 + 服务端) --> 重新生成 Renderer 渲染器
  onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {
    renderer = createBundleRenderer(serverBundle, {
      template, // (可选) 设置页面模板
      clientManifest // (可选) 客户端构建
    })
  })
}

server.use(cookieParser())

// 开头的路径,需要与 output 中设置的 publicPath 保持一致
server.use('/dist', express.static('./dist'))

const render = async (request, response) => {
  try {
    const context = {
      // entry-server.js用于设置服务器端router的位置
      url: request.url,
      cookies: request.cookies,
      request: request
    }

    // renderToString支持promise
    const html = await renderer.renderToString(context)

    response.setHeader('Content-Type', 'text/html; charset=utf-8')
    response.end(html)
  } catch (error) {
    console.log('err: ', error)
    res.status(500).end('Internal Server Error')
  }
}


/**
 * 添加路由
 * 服务端路由设置为 *,意味着所有的路由都会进入这里,不然会导致刷新页面,获取不到页面的bug
 * 并且vue-router设置的404页面无法进入
 */
server.get('*', async (request, response) => {
  if (!isPro) {
    await onReady
  }

  render(request, response)
})

server.listen(3000, () => {
  console.log('server running at port 3000')
})

修改entry-server.js文件,内容如下:

// entry-server.js
import { createApp } from './app'

// 使用async/await改造上述代码
export default async (context) => {
  const { url, cookies } = context
  const { app, router, store } = createApp(cookies && cookies.accessToken)
  const meta = app.$meta()

  // 每次刷新页面的时候,都将cookie中的token初始化到store中。保持登录状态,这一步必须在router.push(url)之前
  store.dispatch('user/nuxtServerInit', context)

  // 用于设置服务器端router的位置,这一步会发生路由跳转,如果在此之后去将cookie中的token初始化到store中,会导致axios的store获取不到
  router.push(url)
  context.meta = meta

  // this的指向router
  await new Promise(router.onReady.bind(router))

  const matchedComponents = router.getMatchedComponents()

  await Promise.all(
    matchedComponents.map((component) => {
      if (component.asyncData) {
        return component.asyncData({
          store,
          route: router.currentRoute,
        })
      }
    })
  )

  context.rendered = () => {
    // Renderer 会把 context.state 数据对象内联到页面模板中
    // 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state
    // 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中
    context.state = store.state
  }

  // async对于非Promise的数据,会将他把装在Promise中,成功后返回对应的数据
  return app
}

这里有个问题,服务端渲染时,会在服务端发起网络请求,服务端请求和客户端请求的区别是:客户端存在跨域问题,所以要进行反向代理,那么服务端网络请求就需要直接使用服务器地址了。 修改config目录下的dev.env.jsprod.env.js文件,添加如下内容:

// 开发环境
VUE_APP_SERVER_BASE_API: '"http://localhost:8088/user-management-service"'
// 生产环境
VUE_APP_SERVER_BASE_API: '"http://ip:port/user-management-service"',

在config目录下新建index.js,内容如下:

'use strict'

module.exports = {
  port: 3000,
  dev: {
    proxyTable: {
      '/user-management-service': {
        target: 'http://localhost:8088/user-management-service',
        secure: false, // 如果是https接口,需要配置该参数,表示是否校验证书,开发环境改为false
        changeOrigin: true,
        pathRewrite: {
          '^/user-management-service': ''
        }
      }
    }
  }
}

修改server.js文件,引入http-proxy-middleware,内容如下:

/**
* 服务端入口,仅运行于服务端
*/
// 创建一个 express 的 server 实例
const express = require('express')
const server = express()
const fs = require('fs')
const cookieParser = require('cookie-parser')
const { createBundleRenderer } = require('vue-server-renderer')
const proxyMiddleware = require('http-proxy-middleware')
const setupDevServer = require('./build/setup-dev-server')

const isPro = process.env.NODE_ENV === 'production'
let renderer
let onReady

if (isPro) {
  const template = fs.readFileSync('./index.template.html', 'utf-8')
  // 生产模式,直接基于已构建好的包创建渲染器
  const serverBundle = require('./dist/vue-ssr-server-bundle.json')
  const clientManifest = require('./dist/vue-ssr-client-manifest.json')
  // 创建一个渲染器
  renderer = createBundleRenderer(serverBundle, {
    template, // (可选) 设置页面模板
    clientManifest // (可选) 客户端构建
  })
} else {
  // 开发模式 --> 监视打包构建(客户端 + 服务端) --> 重新生成 Renderer 渲染器
  onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {
    renderer = createBundleRenderer(serverBundle, {
      template, // (可选) 设置页面模板
      clientManifest // (可选) 客户端构建
    })
  })
}

server.use(cookieParser())

// 开头的路径,需要与 output 中设置的 publicPath 保持一致
server.use('/dist', express.static('./dist'))

const render = async (request, response) => {
  try {
    const context = {
      // entry-server.js用于设置服务器端router的位置
      url: request.url,
      cookies: request.cookies,
      request: request
    }

    // renderToString支持promise
    const html = await renderer.renderToString(context)

    response.setHeader('Content-Type', 'text/html; charset=utf-8')
    response.end(html)
  } catch (error) {
    console.log('err: ', error)
    res.status(500).end('Internal Server Error')
  }
}

if (!isPro) {
  const config = require('./config')

  Object.keys(config.dev.proxyTable).forEach(function (context) {
    var options = config.dev.proxyTable[context]
    if (typeof options === 'string') {
      options = { target: options }
    }

    server.use(proxyMiddleware(options.filter || context, options))
  })
}

/**
 * 添加路由
 * 服务端路由设置为 *,意味着所有的路由都会进入这里,不然会导致刷新页面,获取不到页面的bug
 * 并且vue-router设置的404页面无法进入
 */
server.get('*', async (request, response) => {
  if (!isPro) {
    await onReady
  }

  render(request, response)
})

server.listen(3000, () => {
  console.log('server running at port 3000')
})

修改request.js,引入js-cookie,配置axios的baseUrl、headers的``,内容如下:

import Cookies from 'js-cookie'

const service = axios.create({
  // 如果不存在跨域问题并且在dev.env.xxx文件中有配置的话baseURL的值可以直接使用:process.env.BASE_URL;
  // 如果使用proxy做了代理配置,那么baseURL的值直接填写'/'就可以了。
  baseURL: process.env.VUE_ENV === 'client' ? process.env.VUE_APP_BASE_API : process.env.VUE_APP_SERVER_BASE_API, // url = base url + request url
  withCredentials: true, // send cookies when cross-domain requests
  timeout: 60000, // request timeout
  adapter: retryAdapterEnhancer(axios.defaults.adapter, {
    retryDelay: 1000
  })
})

service.interceptors.request.use(
  config => {
    // do something before request is sent
    const store = createStore()
    // 服务端请求,取不到客户端存储的accessToken,但已将accessToken预缓存至vuex
    const accessToken = store.getters['user/accessToken'] || Cookies.get('accessToken')

    if (accessToken && config.url.indexOf(process.env.IMAGE_PREFIX) === -1) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['Authorization'] = 'Bearer ' + accessToken
    }

    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

使用封装好的axios

import request from '@/utils/request'

export const getArticle = (params) => {
  return request({
    url: '/article/getArticleByArticleId',
    method: 'get',
    params
  })
}