【Vue專案總結】後臺管理專案總結

Mondo發表於2019-06-18

公司做的大部分都是後臺管理專案,剔除每個專案的業務邏輯,其實都可以用通用的一套模版來做。線上預覽地址

登入邏輯

每個系統都有自己的登入登出邏輯,而我們前端所要做的其實是請求後臺,拿到登入許可權,帶上登入許可權,獲取使用者資訊和選單資訊。 在vue專案開發當中,我們一般都是在全域性路由鉤子做這一系列判斷。

router.beforeEach(async(to, from, next) => {
  NProgress.start();
  await store.dispatch('SetConfigApi'); // 獲取配置
  await store.dispatch('SetApi'); // 設定基本配置
  const token = await store.dispatch('getToken'); // 獲取token
  if (token) {
    // 使用者資訊不存在
    if (!store.getters.userInfo) {
      await store.dispatch('GetUser'); // 獲取使用者資訊
      const menuList = await store.dispatch('GetMenu', localRoute); // 獲取選單
      await store.dispatch('GenerateRoutes', localRoute);
      router.addRoutes(store.getters.addRoutes);
      ...
    } else {
      next();
    }
  } else {
    if (whiteList.includes(to.path)) {
      // 在免登入白名單,直接進入
      next();
    } else {
      window.location.href = store.getters.api.IPORTAL_LOCAL_API;
      NProgress.done();
    }
  }
});
複製程式碼

當使用者進入系統的時候,先獲取系統的配置資訊,這個配置資訊可以是前端json檔案,或者是後臺介面;用這種方式可以靈活的修改專案中的配置,而不用每次都打包死進入專案,直接可以要運維童靴修改對應的配置資訊,就可以了。

選單許可權

以前的選單路由是直接寫死在前端,但是當我們直接訪問這個路由時,使用者還是可以進入到這個功能頁面;後來直接改成動態新增路由的方式router.addRoutes

  1. 前端先獲取選單列表
  2. 根據獲取的選單列表迴圈新增使用者選單路由集合
  3. 動態新增路由

具體可檢視

請求方案

專案請求是使用的axios,可以對它新增攔截器來處理我們的請求,也可以處理通過axios.CancelToken重複請求,具體可看程式碼:

// 設定請求統一資訊
import axios from 'axios';
import store from '@/store/index.js';
import qs from 'qs';
import { messages } from './msg-box.js';

const service = axios.create({
  timeout: 300000, // 超時設定
  withCredentials: true // 跨域請求
});

let hasLogoutStatus = false; // 是否某個請求存在需要退出的狀態

const queue = []; // 請求佇列

const CancelToken = axios.CancelToken; // axios內建的中斷方法

/**
 * 拼接請求的url和方法;
 * 同樣的`url + method` 可以視為相同的請求
 * @param {Object} config 請求頭物件
 */
const token = config => {
  return `${config.url}_${config.method}`;
};

/**
 * 中斷重複的請求,並從佇列中移除
 * @param {Object} config 請求頭物件
 */
const removeQueue = config => {
  for (let i = 0, size = queue.length; i < size; i++) {
    const task = queue[i];
    if (!task) return;
    // 出現401,403狀態碼中斷後續請求
    const isLogout = token(config).includes('logout');
    // 退出介面跳過中斷邏輯
    if (!isLogout && hasLogoutStatus) {
      task.token();
      queue.splice(i, 1);
    } else {
      const cancelMethods = ['post', 'put', 'delete']; // 需要中斷的請求方式
      const { method } = config;
      if (cancelMethods.includes(method)) {
        if (task.token === token(config)) {
          task.cancel();
          queue.splice(i, 1);
        }
      }
    }
  }
};

/**
 * 請求錯誤統一處理
 * @param {Object} response 錯誤物件
 */
const errorHandle = response => {
  // eslint-disable-next-line prettier/prettier
  const { status, data: { message = '' }} = response;
  let msg = message;
  if (!message) {
    switch (status) {
      case 401:
        msg = '您沒有許可權訪問此操作!';
        break;
      case 403:
        msg = '您的登入狀態已失效,請重新登入。';
        break;
      case 424:
        msg = response.data.error;
        break;
      default:
        msg = '服務請求異常,請重新整理重試。';
    }
  }
  hasLogoutStatus = status === 401 || status === 403;
  if (hasLogoutStatus) {
    messages('error', msg, () => {
      store.dispatch('Logout');
    });
  }
  messages('error', msg);
};

// 請求攔截器
service.interceptors.request.use(
  config => {
    // 中斷之前的同名請求
    removeQueue(config);
    // 新增cancelToken
    config.cancelToken = new CancelToken(c => {
      queue.push({ token: token(config), cancel: c });
    });
    // 登入後新增token
    if (store.getters.token) {
      config.headers['Authorization'] =
        store.getters.token.token_type + ' ' + store.getters.token.access_token;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// 響應攔截器
service.interceptors.response.use(
  response => {
    // 在請求完成後,自動移出佇列
    removeQueue(response.config);
    // 關閉全域性按鈕Loading響應
    store.dispatch('CancalLoading');
    // 錯誤碼處理
    if (response.status !== 200) {
      return Promise.reject(response);
    }
    return response;
  },
  error => {
    const { response } = error;
    if (response) {
      // 錯誤處理
      errorHandle(response);
      return Promise.reject(response);
    } else {
      // 請求超時
      if (error.message.includes('timeout')) {
        console.log('超時了');
        messages('error', '請求已超時,請重新整理或檢查網際網路連線');
      } else {
        // 斷網,可以展示斷網元件
        console.log('斷網了');
        messages('error', '請檢查網路是否已連線');
      }
    }
  }
);

export default {
  get: (url, data = {}) => {
    return new Promise((resolve, reject) => {
      service
        .get(store.getters.api.API + url, { params: data })
        .then(response => {
          resolve(response.data);
        })
        .catch(error => {
          reject(error);
        });
    }).catch(error => {
      throw new Error(error);
    });
  },
  post: (url, data = {}) => {
    return new Promise((resolve, reject) => {
      service
        .post(store.getters.api.API + url, data, {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
          },
          withCredentials: true,
          transformRequest: [
            data => {
              return qs.stringify(data);
            }
          ]
        })
        .then(response => {
          resolve(response.data);
        })
        .catch(error => {
          reject(error);
        });
    }).catch(error => {
      return Promise.reject(error);
    });
  },
  ...
  /**
   * blob下載
   * @param {String} url 請求地址
   * @param {String} method 請求方式 預設`get`
   * @param {Object} data 請求資料
   */
  exportFile({ url = '', data = {}, method = 'get' }) {
    return new Promise((resolve, reject) => {
      const isPost =
        method.toLocaleUpperCase() === 'POST'
          ? {
            headers: { 'Content-Type': 'application/json' },
            data
          }
          : {
            params: data
          };
      const downConfig = {
        withCredentials: true,
        responseType: 'blob',
        ...isPost
      };
      service
        // eslint-disable-next-line no-unexpected-multiline
        [method](store.getters.api.API + url, downConfig)
        .then(response => {
          resolve(response);
        })
        .catch(error => {
          reject(error);
        });
    }).catch(error => {
      return Promise.reject(error);
    });
  }
};

複製程式碼

當需要使用請求時,可以引用檔案http.js,也可以掛在到vue原型上,在元件內使用this.$http

// user.js
import http from '@/utils/http.js';

export function getUser() {
  return http.get('/user');
}

// main.js
Vue.prototype.$http = http;
複製程式碼

按鈕Loading處理

按鈕的loading效果可以處理後臺響應時間有點長場景,這裡使用store封裝了下處理方式。

// loading.js
import Vue from 'vue';

const loading = {
  state: {},
  mutations: {
    SET_LOADING: (state, data) => {
      const isObject =
        Object.prototype.toString.call(data) === '[object Object]';
      if (!isObject) return;
      Object.keys(data).forEach(key => {
        Vue.set(state, key, data[key]);
      });
    },
    CANCAL_LOADING: state => {
      Object.keys(state).forEach(key => {
        Vue.delete(state, key);
      });
    }
  },
  actions: {
    SetLoading({ commit }, data) {
      commit('SET_LOADING', data);
    },
    CancalLoading({ commit }, data) {
      commit('CANCAL_LOADING', data);
    }
  }
};

export default loading;

// http.js
service.interceptors.response.use(
  response => {
    // 關閉全域性按鈕Loading響應
    store.dispatch('CancalLoading');
    ...
})    
複製程式碼

在元件內定義

<el-button :loading="btn.save" @click="handleClick">儲存</el-button>

computed: {
    btn() {
        return this.$store.state.loading;
    }
}
methods: {
    handleClick() {
        this.$store.dispatch('SetLoading', { save: true });    
    }
}
複製程式碼

以上就可以完美的使用loading,而不用每個都在data中定義了。

總結

以上都是後臺系統中可以用到的一些處理方式,具體程式碼可檢視

其他總結文章:

相關文章