头条项目优化

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. 组件缓存

keep-alive

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/',
});

新手可以尝试 Netlifyvercel21 云盒子 等工具,下面是使用 21 云盒子的配置信息

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/dangpugui/article/details/114368345
今日推荐