1. 程式人生 > >vue項目的一些最佳實踐提煉和經驗總結

vue項目的一些最佳實踐提煉和經驗總結

bus object 提交參數 -- 對象 創建 復用 mage routes

  • 項目組織結構
  • ajax數據請求的封裝和api接口的模塊化管理
  • 第三方庫按需加載
  • 利用less的深度選擇器優雅覆蓋當前頁面UI庫組件的樣式
  • webpack實時打包進度
  • vue組件中選項的順序
  • 路由的懶加載
  • 路由模塊拆分化管理

項目組織結構

清晰的項目結構能讓別人開發進來更容易理解,當然,每個人都有一定的代碼風格習慣。但基於vue開發框架的項目,vue-cli腳手架搭建的項目組織結構大同小異。同時,預想到後面的需求變更及功能增加進展得更有效率,下面截圖是我覺得比較好的項目組織結構:

技術分享圖片

這個截圖只是針對個人覺得比較通用的vue工程結構,不過這個結構要根據具體的項目情況調整,不必為了模塊化而模塊化。模塊化的優勢就是體現在項目業務比較復雜的情況,如果項目業務邏輯並不復雜,可以適當的刪減部分模塊或文件。

相關說明:

assets: 存放圖片、UI設計的圖標文件

componets:自研的業務型及通用型組件

router:項目的路由管理模塊

store:基於vuex的狀態管理容器,api存放各模塊的數據請求,modules存放將store分割成模塊(module),按官網的說法,每個模塊應該擁有自己的 state、mutation、action、getter,主要是解決應用的所有狀態如果全部集中到一個比較大的store對象,當應用變得非常復雜時,store 對象就有可能變得相當臃腫而難以維護。

技術分享圖片

例子:

其中的一個模塊configManage.js

import {
  configManageService
} from 
"../api/index" // state const state = { accountMenuList:[] } // getters const getters = { // 菜單 menuTree: state => { return state.accountMenuList; }, } // actions const actions = { async GET_ACCOUT_MENU({ state, commit }, model) { // 參數 state為當前局部狀態,commit響應式改變當前綁定的菜單數據 const res = await configManageService.getACountMenu(model); commit(
"CHANGE_MENU", res.data); } } // mutations const mutations = { CHANGE_MENU: (data) => { state.accountMenuList = data; } } export default { state, getters, actions, mutations }

index.js,統一出口,導出全部的store模塊

import Vue from ‘vue‘
import Vuex from ‘vuex‘
import index from ‘./modules/index‘
import report from ‘./modules/report‘
import createLogger from ‘vuex/dist/logger‘ // 控制臺輸出當前變化的某個狀態
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== ‘production‘ // 生產或開發環境打包
export const indexStore = new Vuex.Store({
  modules: {
    report,
    index
  },
  strict: debug, // 按照官網建議,改變state的狀態只能通過getter
  plugins: debug ? [createLogger()] : []
})

style:

存放重寫UI庫的樣式和不同組件公共樣式文件

util:

存放用es6封裝的工具類,http請求類,配置類、校驗類、事件類等

技術分享圖片

views:

存放各路由模塊頁面

技術分享圖片

static:

存放全局配置文件,環境域名等

iconfont:

存放字體圖標文件

ajax數據請求的封裝和api接口的模塊化管理

基於vue的項目,與後臺請求數據我們通常使用的是axios,它是基於promise的http庫,其提供的優秀的特性被廣泛運用在項目當中,官方已推薦使用axios,放棄原有的vue-resource。

1、axios的封裝,在很多業務場景下用來進行請求的攔截、響應的攔截及請求超時等;

// axios請求類,一些基礎化配置
class AjaxRequestModel {
  constructor(model) {
    this.url = model.url || "";
    this.data = model.data || {};
    this.method = model.method || "POST";
    // this.success = model.success || function () {};
    // this.fail = model.fail || function () {};
    // this.slientSuccess = model.slientSuccess || true;
    this.failMsg = model.failMsg || true;
    this.baseUrl = model.baseUrl || window.sysConfig.baseUrl;
    this.loading = model.loading || true;
    // this.setData();
    this.setUrl();
  }
  setData() {
    // let options = {
    //   sessionid: ""
    // };
    this.data = Object.assign({}, this.data);
  }
  setUrl() {
    this.url = this.baseUrl + this.url;
  }
}
// 實例化axios,配置請求超時時間
const axiosInstance = axios.create({
  timeout: 1000 * 20
});
// 封裝ajaxService函數,以更少的代碼處理get、post、delete、put請求方式,同時支持async、await異步處理方案,返回promise
const ajaxService = param => {
  let model = new AjaxRequestModel(param);
  let o = {
    url: model.url,
    data: model.data,
    method: model.method
  };
  // if (model.loading) {
  //   ak.Msg.showLoading();
  // }
  if (model.method === "GET") {
    o = {
      url: model.url,
      params: model.data,
      method: model.method
    };
  }
  return new Promise((resolve, reject) => {
    axiosInstance
      .request(o)
      .then(res => {
        if (res.data.code === 200 || res.data.code === 0) {
          resolve(res.data);
        } else {
          ak.Msg.toast(res.data.message, "error");
          reject(res.data);
        }
      })
      .catch(err => {
        httpResponseHandle.call(err);
        reject(err);
      });
  });
};

2、在請求的攔截中,可以攜帶用於接口身份驗證的token,配置headers請求頭、提交參數的序列化等

// 請求頭相關配置
axiosInstance.interceptors.request.use(
  function (config) {
    const info = ak.Utils.getSessionStorage("USER_INFO");
    config.headers.common[‘token‘] = info ? info[0].token : "";
    // config.headers.common[‘Content-Type‘] = "application/json";
    return config;
  },
  function (error) {
    return Promise.reject(error);
  }
);

3、在響應的攔截中,可以進行根據各種狀態碼來進行錯誤的統一處理等

const httpResponseHandle = err => {
  const opt = err.response;
  // 請求超時
  if (err.code === "ECONNABORTED") {
    ak.Msg.toast("請求超時,請稍後再試", "error");
  }
  if (opt.status === 401) {
    ak.Msg.confirm("用戶登錄超時,請重新登錄", () => {
      sessionStorage.removeItem("USER_INFO");
      window.utryVue.$router.replace("/login");
      location.reload();
    });
  } else {
    ak.Msg.toast(opt.data.message, "error");
  }
};

4、api接口模塊化管理,業務邏輯和數據請求分層,這樣可以很方便統一管理我們的接口

如圖,把不同的功能拆分,實現代碼模塊化管理,全部的接口均放在api文件夾下面。index.js是一個api接口的導出的出口,這樣就可以把api接口根據功能劃分為多個模塊,利於多人協作開發,比如一個人只負責一個模塊的開發等,還能方便每個模塊中接口的命名

技術分享圖片

index.js:

import report from ‘./report‘; // 報表模塊
import accountService from ‘./accountService‘; // 登陸、用戶信息相關
// 導出接口
export {
  accountService,
  report
}

API請求service層:

// 報表管理請求模塊,與後臺請求的參數、請求方式、url均看作一個model
import http from "@/util/http.js";
const API_CONTEXT = "sys/"; // 請求的上下文
const report = {
  async getMenuList() {
    let model = {};
    model.url = API_CONTEXT + "category/getCategoryTree";
    model.method = "GET";
    let res = await http.ajaxService(model);
    return res;
  },
  async removeMenu(model) {
    model.data = { ...model };
    model.url = API_CONTEXT + "category/removeCategory";
    let res = await http.ajaxService(model);
    return res;
  }
}
export default report;

組件的業務邏輯層調用方式:

// 說明:async、await的寫法省去了不少的回調,在有些必須請求兩個接口或者兩個接口以上場景下,async、await優勢就顯示出來了
import { reportService } from "../../store/api/index";
async getMenuList() {
      const param = {
        role: ""
      };
      const res = await reportService.getMenuList(param);
      // 下面代碼返回成功時才執行,錯誤由上面所講的axios封裝ajaxService統一處理
      this.menuList = res.data;
 }

5、如果後期維護需要修改的接口,我們就直接在api.js中找到對應的修改就好了,而不用去每一個頁面查找我們的接口然後再修改會很麻煩,如果修改的量比較大,難免會自測不充分產生bug,直接gg。還有就是如果直接在我們的業務代碼修改接口,一不小心還容易動到我們的業務代碼造成不必要的麻煩

6、處理接口域名、端口有多個情況

// 無需前端打包,運維環境快速修改配置,eg:
window.sysConfig = {
  // 運維平臺
  baseUrl: ‘http://10.0.33.97:7083/‘,
  // 租戶平臺
  tenantUrl: ‘http://10.0.33.96:7082/‘
}
// 區分不同平臺的url地址在http.js文件下的AjaxRequestModel類實例化會統一處理
this.baseUrl = model.baseUrl ? window.sysConfig.baseUrl :  window.sysConfig.tenantUrl

第三方庫按需加載

按需加載是針對某些第三方庫體積比較大的情況下,優化webpack打包後的js體積,減少頁面的加載時間

以echart為例子:

優化前:

// 全導入
import * as echarts from "echarts";

webpack打包後:

技術分享圖片

優化後(主js體積減少了400kb,同時build編譯打包速度也得到了減少)

import echarts from "echarts/lib/echarts";
// 依賴註入,目前項目只用到折線圖、餅圖和柱形圖,故只需引入對應的模塊即可,tooltip是提示類,title是鼠標懸停顯示的對應的圖表名稱
import ‘echarts/lib/chart/bar‘;
import ‘echarts/lib/chart/line‘;
import ‘echarts/lib/chart/pie‘;
import ‘echarts/lib/component/tooltip‘;
import ‘echarts/lib/component/title‘;

技術分享圖片

利用less的深度選擇器優雅覆蓋當前頁面UI庫組件的樣式

技術分享圖片

vue頁面組件的樣式基本是寫在<style scoped lang="less"></style>中,增加scoped屬性的目的讓其樣式只在當前頁面有效。按照這些寫的方式,編譯後當前標簽會加上類似於[data-v-]這樣的屬性,但是第三方的UI組件庫並沒有編譯為帶[data-v-]這樣的屬性,所以就遇到了當前頁面覆蓋的樣式沒生效的情況,有沒有方法處理這種問題呢。有些小夥伴可能會想到我在公共樣式裏面寫,額外添加類名來覆蓋當前組件的樣式,其實,這也不失為一種方案,但是會引來樣式全局汙染和命名可能重名的情況。下面列舉更簡單粗暴的方式,同時避免了樣式汙染和命名沖突的問題:

.menu-tree {
    /deep/ .el-tree-node__content {
      height: 32px;
    }
    /deep/ .is-current .el-tree-node__content {
      background-color: #f2f2fa;
    }
  }

編譯後,默認給menu-tree加上了[data-v-3c93a211]

技術分享圖片

/deep/深度選擇器支持less或者sass,如果你用的是原生的css,可以用<<<符號

webpack實時打包進度

在項目用jenkins自動化打包前端項目的時候,常常會遇到打包速度慢而體驗很差,在優化減負依賴包的情況下,同時沒有一個測試環境或生產環境當前打包進度捉雞。這裏推薦一個第三方的插件包

progress-bar-webpack-plugin。

用法:
// 需安裝依賴 npm install progress-bar-webpack-plugin --save-dev
const ProgressBarPlugin = require(‘progress-bar-webpack-plugin‘)
// 在生產環境webpack配置文件的plugin是加上
new ProgressBarPlugin(), // 打包進度 

技術分享圖片

vue組件中寫選項的順序

這裏純屬個人觀點,可能有些小夥伴用vue開發不是遵從這個。為什麽要規定組件的寫法順序呢,或者說它是官方要求的規範,不如說是能讓的代碼更加優雅,更易於維護,因為你寫的代碼不僅是你一個人維護。要是一個團隊都按這個規範來,大家在維護代碼的時候認知一樣,那效率就提高了。

組件依賴:

components(自研的子組件或第三方組件)

service(api請求類,其他服務類)

utils(工具類等)

事件傳遞(vue eventBus)

mixins(復用的屬性或方法)

組合:

mixins

組件的屬性、接口:

components

props

本地響應式屬性、狀態:

data

computed

事件註冊:

watch

組件生命周期:

created

mounted

destroyed等

組件的方法:

methods

例子:

// 例子
import utryTree from "@/components/utry-tree/utry-tree.vue";
import { reportService } from "@/store/api/index";
import Validation from "../../util/Validation";
import eventBus from "@/util/eventBus";
import reportMixins from "@/mixins/reportMixins"; export
default { mixins: [], components: { }, props: { menuList: { type: Array, default() { return []; } } }, data(){}, computed:{}, watch:{}, mounted(){}, methods:{}, }

路由的懶加載

有時候,針對有些復雜組件,初始化頁面其實並不需要把全部組件資源加載進來,把業務復雜的組件抽離出來,從而能減少初始化頁面的加載時間

優化前:

import reportManage from ‘@/views/reportManage/index‘;
import reportPreview from ‘@/views/reportManage/reportPreview‘;
export default [
  { path: ‘reportManage/index‘, name: ‘reportManage‘, component: reportManage },
  { path: ‘reportManage/reportPreview‘,  name: ‘reportPreview‘, component: reportPreview }
];

初始化頁面的加載耗時:

技術分享圖片

優化後:

import reportManage from ‘@/views/reportManage/index‘;
export default [
  { path: ‘reportManage/index‘, name: ‘reportManage‘, component: reportManage },
  { path: ‘reportManage/reportPreview‘, name: ‘reportPreview‘, component: () => import(‘@/views/reportManage/reportPreview‘),
    meta: { keepAlive: false }
  }
];

初始化頁面加載耗時:

技術分享圖片

時間的差別主要是在js的解析上,主要是是因為初始化頁面沒有加載當前模塊的二次組件的js,等到跳轉到二次頁面再去解析靜態資源,總體優化後初始化頁面的加載時間快了100多毫秒。

路由模塊的拆分化管理

這裏的路由拆分,是指按模塊拆分成不同的路由文件,針對單頁面應用這樣更方便團隊的多人協調同步開發,自己寫的功能模塊互不影響。如果當業務需求多起來的時候,它的優勢就越能體現出來。我們並不想就在一個router.js寫整個工程的路由,這樣會是單文件代碼量龐大而變得很槽糕,同時也會帶來其他同事誤改的問題。

技術分享圖片

我們在router文件夾下面創建router.js作為路由的入口文件,其他以router.js後綴的文件存放著各個模塊的路由。

router.js:

import Vue from "vue";
import Router from "vue-router";
import NProgress from "nprogress"; // 引入nprogress,每次路由變化網頁頂端有個加載條效果
import ak from "@/util/ak.js";
// 業務路由
import login from "@/views/index/login"; // 租戶平臺
import oamLogin from "@/views/index/oamLogin"; // 運維平臺
import indexRouter from "./index.router"; // 首頁相關
import reportManage from "./reportManage.router"; // 報表管理
Vue.use(Router);
// 默認登錄
let routes = [
  {
    path: "/",
    redirect: "login"
  },
  {
    path: "/login",
    name: "login",
    component: login
  },
  {
    path: "/oamLogin",
    name: "oamLogin",
    component: oamLogin
  }
];
routes = routes.concat(
  indexRouter,
  reportManage
);

// router register
const router = new Router({
  routes
});
// 路由相關的攔截操作,在這裏處理,之前有的router相關操作寫在main.js,並不是很友好
router.beforeEach((to, from, next) => {
  // 每次切換頁面時,調用進度條
  NProgress.start();
  // cache機制
  const info = ak.Utils.getSessionStorage("USER_INFO");
  const token = info ? info[0].token : "";
  if (token) {
    next();
  } else {
    if (to.path === "/oamLogin") {
      next();
    } else if (to.path === "/login") {
      next();
    } else {
      next("/login");
    }
  }
});
router.afterEach(() => {
  // 在即將進入新的頁面組件前,關閉掉進度條
  NProgress.done();
}); 

index.router.js:

import home from ‘@/views/index/home‘; 
export default [
  { path: ‘/index/home‘, name: ‘home‘, component: home }
];

這裏把首頁的路由放在一個數組裏,然後導出去,有router.js統一引入,並實例化當前路由

未完待續......

vue項目的一些最佳實踐提煉和經驗總結