0. 统一错误处理
src/utils/request.js
import {
Toast } from 'vant';
request.interceptors.response.use(
function (response) {
return response;
},
function (error) {
console.dir(error);
const status = error.response.status;
if (status === 400) {
Toast.fail('客户端参数错误');
} else if (status === 401) {
Toast.fail('TOKEN无效');
} else if (status === 403) {
Toast.fail('没有权限');
} else if (status === 404) {
Toast.fail('资源不存在');
} else if (status === 405) {
Toast.fail('请求方法错误');
} else if (status >= 500) {
Toast.fail('服务器抽风了');
}
// 需要直接返回错误,会走到真正请求的 catch 里面,若省略还是会触发真正请求的 then 回调,then 里面代码出错再走 catch
return Promise.reject(error);
}
);
1. 处理 401
src/utils/request.js
const login = () => router.replace('/login'); // replace 是直接替换,不会添加新纪录
const requestToken = axios.create();
request.interceptors.response.use(
function (response) {
return response;
},
async function (error) {
// console.dir(error)
const status = error.response.status;
if (status === 400) {
Toast.fail('客户端参数错误');
} else if (status === 401) {
// !#1. 如果没有 user 或 user.refresh_token 直接跳转到登录页
const {
user } = store.state;
if (!user || !user.refresh_token) {
// return login() // 相当于返回了 undefined,真正的接口请求处是会走 then 的代码
login(); // 相当于返回了下面的 Promise.reject(error),真正的接口请求处是会走 catch 的代码
} else {
// !#2. 如果有 refresh_token,则根据此 refresh_token 请求新的 token
// 不使用 request 是为了防止死循环,例如刷新 token 接口再 401 的,又会继续刷新 token...
try {
const {
data: {
data: {
token },
},
} = await requestToken({
method: 'PUT',
url: '/app/v1_0/authorizations',
headers: {
Authorization: `Bearer ${
user.refresh_token}`,
},
});
// !#3. 拿到新的 token 更新 store
user.token = token;
store.commit('setUser', user);
// !#4. 把失败的请求发出去,务必 return,这样才能走真正请求的 then 去拿到结果,始终都没有机会触发真正请求的 catch,被响应拦截器拦截了
return request(error.config);
} catch (e) {
error = e;
login();
}
}
} else if (status === 403) {
Toast.fail('没有权限');
} else if (status === 404) {
Toast.fail('资源不存在');
} else if (status === 405) {
Toast.fail('请求方法错误');
} else if (status >= 500) {
Toast.fail('服务器抽风了');
}
// 需要直接返回错误,会走到真正请求的 catch 里面,若省略还是会触发真正请求的 then 回调,then 里面代码出错再走 catch
return Promise.reject(error);
}
);
2. 再说登录成功后跳转到原页面
src/utils/request.js
const login = () =>
router.replace({
name: 'login',
query: {
// router.currentRoute => this.$route
redirect: router.currentRoute.fullPath,
},
});
3. 界面访问控制
例如有些页面需要登录后才能访问!路由元信息、next(false)
router.beforeEach(async (to, from, next) => {
// 是否需要权限
if (to.meta.requiresAuth) {
// 检测是否登录
// 已登录直接放行
if (store.state.user) return next();
// 未登录提示是否登录
const r = await Dialog.confirm({
title: '提示',
message: '需要登录才能访问,是否登录?',
})
.then((r) => r)
.catch((e) => e);
if (r === 'confirm') {
login();
} else {
// 中断
next(false);
}
} else {
next();
}
});
4. 组件缓存
src/App.vue
<template>
<div id="app">
<keep-alive>
<router-view />
</keep-alive>
</div>
</template>
4.1 keep-alive 对谁生效
缓存仅对该路由出口渲染的组件有效,例如详情(/article/139332
)到首页(/
),搜索(/search
)到首页(/
)等,但是 TabBar 之间的切换并没有缓存,所以需要如下操作:src/views/layout/index.vue
观察 devTools
<template>
<div class="layout-container">
<!-- 子路由出口 -->
<keep-alive>
<router-view></router-view>
</keep-alive>
<!-- ... -->
</div>
</template>
4.2 进入详情页显示的还是旧数据
因为 article 组件被缓存了,生命周期钩子 created 也就不再触发了,解决:只缓存特定的 LayoutIndex
组件
<keep-alive :include="['LayoutIndex']">
<router-view />
</keep-alive>
4.3 返回列表页滚动条的问题
详情页返回时需要记住滚动位置,src\views\home\components\article-list.vue
export default {
name: 'ArticleList',
mounted() {
const articleList = this.$refs['article-list'];
articleList.onscroll = debounce(() => {
this.scrollTop = articleList.scrollTop;
}, 50);
},
activated() {
// 从缓存中被激活
this.$refs['article-list'].scrollTop = this.scrollTop;
},
deactivated() {
// 从缓存中失去活动
},
};
4.4 切换账号登录还是原数据
1. store 中配置 cachePages 和对应的 mutation,store 里面指定要缓存的组件,以及操作缓存的方法,src\store\index.js
import Vue from 'vue';
import Vuex from 'vuex';
import {
getItem, setItem } from '@/utils/storage';
Vue.use(Vuex);
const TOKEN_KEY = 'TOUTIAO_USER';
export default new Vuex.Store({
state: {
// 存储当前登录用户信息(token等数据)
// user: null
// user: JSON.parse(localStorage.getItem(TOKEN_KEY))
user: getItem(TOKEN_KEY),
cachePages: ['LayoutIndex'],
},
mutations: {
setUser(state, data) {
state.user = data;
// 为了防止刷新丢失,需要把数据存储到本地
// localStorage.setItem(TOKEN_KEY, JSON.stringify(state.user))
setItem(TOKEN_KEY, state.user);
},
// 添加缓存页面
addCachePage(state, pageName) {
if (!state.cachePages.includes(pageName)) {
state.cachePages.push(pageName);
}
},
// 移除缓存页面
removeCachePage(state, pageName) {
const idx = state.cachePages.indexOf(pageName);
if (idx !== -1) {
// console.log(this.state.cachePages === state.cachePages)
state.cachePages.splice(idx, 1);
}
},
},
});
2. App 中使用 Store 中的 cachePages,根组件缓存指定的路由,src\App.vue
<template>
<div id="app">
<!-- 指定 include 代表只缓存这些,保证能显示文章详情 -->
<keep-alive :include="cachePages">
<router-view />
</keep-alive>
</div>
</template>
<script>
import {
mapState } from 'vuex';
export default {
name: 'App',
computed: {
...mapState(['cachePages']),
},
};
</script>
3. 登录成功后清除 cachePages 中对应的组件,src\views\login\index.vue
// 清除 LayoutIndex 缓存
this.$store.commit('removeCachePage', 'LayoutIndex');
// 跳转回原来的页面
// this.$router.back()
4. LayoutIndex 渲染完毕后添加组件名到 cachePages,即渲染完毕后添加缓存,src\views\layout\index.vue
export default {
name: 'LayoutIndex',
data() {
return {
active: 0,
};
},
mounted() {
// 渲染好之后再次添加缓存
this.$store.commit('addCachePage', 'LayoutIndex');
},
};
5. 打包优化
npx vue-cli-service build --report
5.1 no-console
npm run build
npm i babel-plugin-transform-remove-console -D
babel.config.js
// 项目发布阶段需要用的插件
const prodPlugins = []
if (process.env.NODE_ENV === 'production') {
prodPlugins.push('transform-remove-console')
}
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
...prodPlugins
]
}
5.2 externals and cdn
vue.config.js
module.exports = {
devServer: {
proxy: {
'/app': {
target: 'http://toutiao-app.itheima.net/',
ws: true,
changeOrigin: true,
pathRewrite: {
'^/app': '',
},
},
},
},
chainWebpack: config => {
// 发布模式
config.when(process.env.NODE_ENV === 'production', config => {
config.set('externals', {
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios',
lodash: '_',
vant: 'vant',
});
config.plugin('html').tap(args => {
args[0].isProd = process.env.NODE_ENV === 'production';
return args;
});
});
},
css: {
extract: true,
},
};
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="referrer" content="never">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<% if(htmlWebpackPlugin.options.isProd){ %>
<script src="https://cdn.staticfile.org/vue/2.6.11/vue.min.js"></script>
<script src="https://cdn.staticfile.org/vue-router/3.1.3/vue-router.min.js"></script>
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.staticfile.org/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/vant.min.js"></script>
<% } %>
</head>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
注意 vant 代码和样式使用的是 2.11.3
,测试发现低版本样式打包后有问题!
6. 部署上线
注意:不要每个具体的接口请求函数都以 /app
开头,可以统一设置 baseURL
为 /app
,方便上线的时候统一修改
const request = axios.create({
// baseURL: '/app', // 接口的基准路径
baseURL: 'http://toutiao-app.itheima.net/',
});