記一次 Vue 項目重構

0?wx_fmt=png

隨着公司項目越作越複雜,因前期團隊對 Vue 使用經驗不足,致使留下比較多坑。再這樣下去項目會變成愈來愈難以維護,因而我對主管說:「主管,我想重構」,便有了此次的重構經歷。通過對項目分析,主要存在如下問題:css

  • 全局樣式滿天飛
  • 組件愈來愈多,管理不方便
  • 核心頁面 1300 多行代碼,閱讀性很是差
本項目是一個金融類項目,採用可視化的資產架構描述方式,並根據資產架構生成稅務報告。使用 Vue 全家桶進行業務開發,並在 Element UI 基礎上進行定製化,可視化建模使用 mxGraph

減小全局樣式

項目出現全局樣式滿天飛的狀況,有如下緣由html

  • 組件內樣式想要覆寫子組件樣式,去除了 scoped 關鍵字
  • 爲了樣式在不一樣組件間複用,將樣式提到了全局

組件銷燬後,Vue是不會刪除對應樣式標籤的,因此組件內樣式不寫 scoped 存在污染全局樣式的風險。vue

爲了解決第一個問題,此次重構的作法是,堅定全部組件都使用 scoped。須要覆寫子組件時使用深度做用選擇器解決。這樣僅不會污染全局樣式,還對子組件覆寫樣式一目瞭然。node

對於彈窗這類確實要做用到全局的樣式,咱們統一寫在命名爲 global.scss 的文件,並使用 BEM 規範命名。ios

對於在組件間複用的樣式,分模塊地放到 modules 文件夾下,組件內使用時再用 @import 導入。git

來看看重構後的 style 文件夾長這個樣子程序員

全局樣式樣式只剩下 nomalize.css、一些自定義的 reset、element-ui 的默認樣式、上文提到的 global,還有就是圖標。github

分類管理組件

未重構前,全局基礎組件放置在 components/common 文件夾,業務組件與其餘未歸類的組件全放在 components 文件下,看起來很是混亂。編程

經重構後,將組件分爲五類: business、common、function、slot,還有一種就是爲某個頁面特定提供的,下面會提到。element-ui

business 爲業務組件跟業務有耦合,可在頁面間複用,但不適用於其餘項目。而 slot、function、common 這幾類是可脫離當前項目使用的。 slot、function 與 common 同樣,不一樣的是 common 使用頻率很是高是全局註冊的。而 slot、function 是局部註冊使用的。slot 的特別之處在於,這類組件只提供一個樣式外殼,無太多交互,能很好地被其餘組件利用。

像下圖所示其餘 Panel 組件均可以複用 slot目錄下的 Panel 組件。

此次重構經我總結後得出應該在這兩種狀況下建立組件

  • 可複用的,如上面提到 components 目錄下的組件
  • 不可複用的,純粹爲了減小某個頁面代碼,使 template 結構更清晰。

    • 例如僅僅是傳入 props 作數據展現
    • 又或者該組件直接與頁面進行交互,該組件無嵌套其餘組件

像下面 NodeDetai 頁面分離出來的 components 就是上面提到的不可複用組件。

拆分大文件

咱們系統核心頁面就是畫圖頁。該頁面共三個組件,左側的元素面板、右側的節點面板、右側的線條面板。交互與大多可視化建模軟件相同,用戶將左側元素拖拽到畫布,從節點拖出線條鏈接到另外一個節點,當用戶在畫布上選擇節點時右側面板顯示節點相關操做,選擇線條亦然(同一時刻只能選擇一個節點或者一條線條)。與 draw.io 有點類似,但咱們作的不是繪圖應用。

精簡 methods

通過我對該頁面代碼進行瀏覽後發現,該頁面之全部這麼多代碼是由於,在編寫方法過程當中咱們會習慣性地將大的方法拆分紅小的方法,結果這些小的方法都堆在 methods,致使 template 事件處理函數很是不顯眼。因此此次重構目標就是刪除 methods 對象中除頁面初始化方法外的全部非事件處理方法。也就是說 methods 對象中的每一個方法都應該對應一個 template 事件處理。

那麼問題來了拆分出來的小方法不放在 methods ,該放到什麼地方?根據我對畫布頁面代碼分析,發現這個頁面其實只對有三個東西進行操做:架構、節點、線條。因而按照這個思路獨立出有三個 js 文件,將 this 看成參數傳入到各自的模塊,用來操做 vm 對象。同時將 js 從 vue 文件中獨立出來。重構以後該頁面目錄長成了這個樣子

js/index.js 是頁面的 vm 對象,重構後代碼由原來的1000多行精簡成了300多行,提升了可維護性。

使用面向接口編程

這個頁面js還存在一個問題,大量的 if/else 判斷。這裏先扯一點 mxGraph 的東西,在 mxGraph 中節點與線條都統稱爲 Cell,當節點或線條被刪除時 mxGraph 會派發一個 CELL_REMOVE 事件,可是這個 Cell 是節點仍是線條仍是要程序員本身去判斷的,這也致使了咱們系統出現了不少下面這樣的判斷語句

functoin syncRemove(cell) {
    // 判斷是節點仍是線條
    const cellIsVertex = cell.vertex;
    if(cellIsVertex){
        // 執行刪除節點
    } else {
        // 執行刪除線條
    }
}

通過我思考許久後得出兩個方案

  1. 對 mxGraph 的每一個 Cell 事件進行細分紅節點與線條事件。好比接收到 CELL_REMOVE 事件後,判斷是節點仍是線條而後觸發自定義事件 VERTEX_REMOVE 或者 EDGE_REMOVE,以後咱們只需監聽 VERTEX_XXX 與 EDGE_XXX 即。這樣作雖然讓事件變得更加具體清晰,可是在細分過程當中一樣不可避免寫出多個 if/else 判斷是節點仍是線條,因而棄用了這個方案。
  2. 使用面向接口編程。上文提到我將該頁面的交互分紅有三個模塊:節點、線條、架構,既然節點、線條有相同的操做,那麼他們應該實現共同的接口。因而乎將代碼改形成這樣子

    // vertexOp.js
    const vertexOp = {
      // *********
      // Interface
      // *********
      handleActive(vertex) {
          // 節點被點出時該執行的操做
      },
      async syncAdd(vertex) {},
      syncRemove(vertex) {},
      // Others ....
    }
      
      
    // edgeOp.js
    const edgeOp = {
      // *********
      // Interface
      // *********
      handleActive(edge) {},
      async syncAdd(edge) {},
      syncRemove(edge) {},
      // Others ....
    }

    當用戶選擇不一樣 Cell 的時候,只須要在選擇事件處理器中作一次判斷便可。

    // index.js 
    let opContex = null;
    let activeCell = null;
    
    const listenSelectionChange = ()=> {
        activeCell = graph.getSelectionCell();
        const cellIsVertex = activeCell.vertex;
        if(cellIsVertex){
            opContex = vertexOp;
        } else {
            opContex = edgeOp;
        }
    }
    
    const handleRemoveEvent = ()=> {
        contexOp.syncRemove(activeCell);
    }

使用請求攔截將零碎的方法調用集中起來

這個頁面再一個問題是,出現這多零散的方法調用。好比像下面的需求

需求:當用戶作了任何改變架構外觀的操做都將當前架構截圖同步到服務端用做該架構的封面展現。

  • 重構前作法:

    • 添加節點,在相應處理方法最後加一句截圖發送請求
    • 修改節點信息,在相應處理方法最後加一句截圖發送請求
    • 移動節點,在相應處理方法最後加一句截圖發送請求
    • 添加線條,在相應處理方法最後加一句截圖發送請求
    • 修改線條信息,在相應處理方法最後加一句截圖發送請求
    • ........ 在相應處理方法最後加一句截圖發送請求
  • 重構後作法:攔截全局請求,判斷到是相關操做的請求則截圖發請求

具體作法是對請求進行命名,而後在每一個請求發送完成時使用全局 eventBus 發送一個請求完成事件。事件處理器只須要根據請求名稱判斷是否須要截圖發送請求。代碼思路以下

// api層
// api/nodes.js
import http from '@/config/axios/index';

export default {
  all: data => http('/nodes', data, 'GET'),
  one: id => http(`/nodes/${id}`, 'GET'),
  save: data => http('/nodes', data, 'POST', 'nodes-save'),
  del: id => http(`/nodes/${id}`, 'DELETE', 'nodes-del'),
  // .....
};


// 封裝 axios
// config/axios/index.js
export default function (...args) {
  const url = args[0];
  let data;
  let method;
  let name;
  // 參數重載
  if (args.length === 2) {
    method = args[1];
  } else if (args.length === 3) {
    if (_.isString(args[1]) && _.isString(args[2])) {
      method = args[1];
      name = args[2];
    } else {
      data = args[1];
      method = args[2];
    }
  } else if (args.length === 4) {
    data = args[1];
    method = args[2];
    name = args[3];
  } else {
    throw new Error('http support max 4 args');
  }

  if (_.isNil(name)) {
    // 沒有命名的請求,默認命名爲當前時間戳
    name = String(Date.now());
  } else {
    // 有命名的請求,爲了保證請求的惟一性,加上時間戳後綴
    name = `${name}__${Date.now()}`;
  }
  return $axios({ url, data, method }, name);
}

async function $axios(initialOptions, requestName) {
  const options = getOptions(initialOptions);
  initialOptions.requestName = requestName;
  requestManager.addReq({
    name: requestName,
    config: initialOptions,
  });
    
  try {
    const response = await axios(options);
    requestManager.popReq({
      name: requestName,
      response,
    });
    return response.data;
  } catch (error) {
    // 保證即使請求出錯也要使該請求彈出隊列
    requestManager.popReq({
      name: requestName,
      error,
    });
    return {};
  }
}

// 發送請求相關事件
// requestManager.js
import Vue from 'vue';

const $bus = Vue.prototype.$bus;

class RequestManager {
  constructor() {
    this._updateRequests = [];
  }

  addReq(req) {
    if (req.config.method.toLowerCase() === 'get') {
      return;
    }
    this._updateRequests.push(req);
    $bus.$emit('before-modify-req-send', req);
  }

  popReq({ name, response }) {
    if (response && response.config.method.toLowerCase() === 'get') {
      return;
    }
    const idx = this._updateRequests.findIndex(item => item.name === name);
    if (idx >= 0) {
      this._updateRequests.splice(idx, 1);
      $bus.$emit('modify-req-finished', name, response);
      if (this._updateRequests.length === 0) {
        $bus.$emit('modify-req-empty');
      }
    }
  }
}

// RequestManager是一個全局的單例對象
export default new RequestManager();

最終只須要對請求進行攔截,就能夠大量減小零散的方法調用

// xxx.vue
  this.$bus.$on('modify-req-finished', (name, response) => {
    const reqs = ['c-transitions-updateRatio',
      'c-transitions-save',
      'c-transitions-del',
      /*..........*/];
    const reqName = name.split('__')[0];
    if (reqs.includes(reqName)) {
      // 截圖,發送請求
    }
  });

除此以外請求攔截還適用於這個場景: 當用戶作了操做,實時提示用戶操做保存中,保存完成後提示用戶操做已保存。
使用請求攔截很是輕鬆完成這個功能,只須要監聽發送請求事件、請求隊列清空事件作相應提示便可。

總結

此次重構一人完成,用時一星期,作了以下工做

  • 使用劃分模塊的方式減小全局樣式
  • 歸類組件
  • 使用以下方式拆分大文件

    • 精簡 methods
    • 拆分模塊
    • 使用面向接口編程
    • 使用請求攔截

感悟比較深的是,網上雖然不少文章教你怎樣用 Vue 作好項目,但實際狀況仍是要從項目自身出發,本身必定要對項目進行思考,我相信沒有適合全部項目的 "Vue最佳實踐"。只要不斷對項目進行思考、改進,才能找到最適合自身項目的架構方式。

相關文章
相關標籤/搜索