小白学习vue服务端渲染(二)--二次封装axios
安装axios
,element-ui
,js-cookie
,http-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
保持登录状态
模拟nuxt
的nuxtServerInit
,在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.js
和prod.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
})
}
转载自:https://juejin.cn/post/7245891823158558777