如何優雅使用 vuex

来源:https://www.cnblogs.com/dasusu/archive/2023/11/16/17835470.html
-Advertisement-
Play Games

大綱 本文內容更多的是講講使用 vuex 的一些心得想法,所以大概會講述下麵這些點: Q1:我為什麼會想使用 vuex 來管理數據狀態交互? Q2:使用 vuex 框架有哪些缺點或者說副作用? Q3:我是如何在項目里使用 vuex 的? 初識 vuex 對於 vuex,有人喜歡,有人反感 喜歡的人覺 ...


大綱

本文內容更多的是講講使用 vuex 的一些心得想法,所以大概會講述下麵這些點:

Q1:我為什麼會想使用 vuex 來管理數據狀態交互?

Q2:使用 vuex 框架有哪些缺點或者說副作用?

Q3:我是如何在項目里使用 vuex 的?

初識 vuex

對於 vuex,有人喜歡,有人反感

喜歡的人覺得它可以很好的解決複雜的數據交互場景

反感的人覺得它有各種缺點:繁瑣冗餘的代碼編寫、維護性差的字元串形式變數註入、過於依賴 vue 框架導致非同步擴展場景差

這其中,有一個很模糊的點,複雜的數據交互場景並沒有一個衡量標準,每個人都有自己的見解

再加上不同人有著不同的項目經歷,這就造成了經常會出現有趣的現象:你體會不到我為什麼非要使用 vuex,他理解不了這種場景何須使用 vuex,我也講不明白選擇 vuex 的緣由

借用官網文檔一句話:

您自會知道什麼時候需要它

很玄乎,更通俗來講就是,多踩點坑,多遭遇些痛點,當你的最後一根稻草被壓垮時,自然就會去尋找更好的方案解決

我一直都不喜歡 vuex,因為我覺得它的 mapMutations 或者 mapState 註入到 vue 里的變數和方法都是字元串,極大的破壞了代碼的可讀性和維護性,沒辦法通過 idea 快速的跳轉到變數定義的地方

當然,你也可以定義一些靜態變數來替換這些字元串就可以解決跳轉問題,但代價就是代碼更繁瑣了,本來使用 vuex 時就需要寫一堆繁瑣的代碼,這下更麻煩

還有一個不想使用 vuex 的原因是因為我的項目業務邏輯挺複雜,除了 vue 單文件外,項目里還劃分了來負責業務邏輯或非同步任務的 js 層代碼,而 vuex 是為 vue 框架而設計的,存放在 vuex 數據中心的變數可以通過它的一些工具方法來註入到 vue 組件的 computed 計算屬性里方便直接使用,比如

import { mapState } from 'vuex'

export default {
  // 映射 this.count 為 store.state.count
  computed: mapState({
    count: state => state.count
  })
}

但如果想在 js 文件里使用 vuex 里的數據,就會比較繁瑣:

import store from 'store'

console.log(store.state.count);

基於以上種種原因,我遲遲未在項目里使用 vuex

那麼,我最後為什麼又選擇使用了 vuex 呢?

一,項目的一些數據交互場景使用 vue 原生的輸入輸出方案已經忍不下去了

二,我想辦法解決了我沒法忍受的 vuex 的幾個缺點了

三,這是個新項目,並沒有複雜的業務場景,項目基本由 vue 單文件來書寫即可

簡單來說,就是有個新項目符合適用 vuex 的場景,而且一些組件交互場景使用原生方案過於繁瑣,vuex 剛好能夠解決這個問題,雖然 vuex 有一定的使用成本,但這些缺點恰好又被我想了一些法子解決或簡化掉

這樣一來,引入 vuex 既能解決我的訴求,又不會引入太多我無法接受的缺點,那自然可以來玩一玩

背景

vue 框架是基於組件機制來組裝成頁面,所以頁面數據是分散到各個組件間,而組件間的數據交互使用的是 vue 自帶的輸入(props)輸出($emit)機制

這種數據交互方案有個特點,數據對象都存儲在組件內部,交互需要通過輸入和輸出

而輸入輸出機制有個缺點:頁面複雜時,經常需要層層傳遞數據,因為非父子組件間的交互,只能尋找最近的祖先組件來做中轉

同時,輸入輸出機制還有一個局限性:當不同頁面的組件需要進行數據交互時,它就無能為力了

平常開發,這種輸入輸出的方案也基本滿足了

但如果有需要跨頁面的數據交互或者說,有需要將數據做持久化處理的場景時;以及如果頁面組件層次複雜,存在props 和 $emit 層層傳遞時,那這時候如果忍不了輸入輸出方案的用法,那麼就可以研究新方案了

解決這種場景的方案很多,但從本質上來講,都可以統歸為:數據中心方案

這種方案思路就是將數據對象從組件內部移出到外部存儲維護,需要使用哪個數據變數的組件自行去數據中心取

vue 其實也有機制可以達到這種效果,如:依賴註入,但慎用,太破壞數據流的走向了

我們也可以自己用 js 來實現一個數據中心,專門建個 js 文件來存儲維護數據模型,需要數據變數的組件引入 js 文件來讀取

但每個數據中心都必須解決兩個問題:數據復用和數據污染,通俗來講就是數據初始化和重置,也就是數據的生命周期

數據復用是為了確保不同組件間從數據中心裡讀取時,是同一份數據副本,這才能達到數據交互目的

而數據污染是指不同模塊間使用同個數據中心時,數據模型是否可以達到相互獨立,互不影響的效果,這通常是某個功能在不同模塊間被覆用時會出現的場景;如果這種場景不好理解,那麼也可以想想這種場景:再次載入該頁面,組件再次被創建後,從數據中心裡讀取的數據副本是否是相互獨立的

如果數據存儲在 vue 組件內部,那數據的生命周期就是跟隨著組件的創建和銷毀,這也是為什麼 data 是一個返回對象的函數,因為這樣可以藉助 js 的函數作用域機制來解決數據的復用和污染問題

但數據從 vue 組件內部移出,存儲到數據中心,那麼這些處理就需要自己來實現

所以,數據中心並不是簡單建個 js 類,裡面聲明下數據對象就完事了的

基於此,我選擇使用 vuex

vuex 副作用

先看個使用 vuex 的簡單例子:

// 聲明
const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 變更狀態
      state.count++
    }
  }
})

// vue 里使用
import { mapMutations } from 'vuex'
import { mapState } from 'vuex'

export default {
  // ...
  computed: {
     ...mapState({
         // 將 `this.count` 映射為 `this.$store.state.count`
         count: state => state.count
     })   
  },
  methods: {
    ...mapMutations([
      'increment', // 將 `this.increment()` 映射為 `this.$store.commit('increment')`
    ])
  }
}

僅僅簡單定義個數據對象,就需要聲明對象模型 state,聲明對象操作方法 mutation,然後在相應的 vue 組件內先通過 mapState 註入變數的讀方法,再通過 mapMutations 註入變數的寫方法

而以上這麼多繁瑣的代碼,在原本的 vue 機制里,就是簡單的在 data 里聲明下變數就完事,這一對比,vuex 的使用上,複雜度和繁瑣度很大,有一定的使用成本

所以很多人不喜歡用它,官方也說簡單的頁面也沒有必要去使用它

這是我覺得 vuex 的第一個缺點,或者說副作用:繁瑣冗餘的代碼編寫

第二個我覺得 vuex 的缺點就是,mapState 或 mapMutation 註入的變數,都是字元串的

字元串就意味著,你在 vue 單文件內其他地方通過 this.xxx 使用這些變數時,當你想查看變數的聲明時,idea 無法識別!

這是我特別無法接受的一點,降低我的維護、開發效率

不過這點因人而異,有人覺得它不是個問題,或者使用個靜態變數來替換字元串也可以解決,但這些我個人是沒辦法接受

然而 vue 原生輸入輸出的數據交互又不足夠支撐我的一些需求場景,自己用 js 實現個數據中心吧,又擔心沒強制規範,沒處理好,後期跑偏掉更難維護,那就想想辦法搞定 vuex 的這兩個缺點吧

如何更簡易的使用 vuex

先說下,我雖然用了些方法,讓我使用 vuex 可以達到我的預期,既滿足我的需求場景,又不至於引入太多副作用

但實際上,這種方式也許就偏離了 vuex 官方的推薦方式了,別人不一定能接受我的這種用法

所以,這篇更多的是分享我的一些思路和想法,有一說一,並不通用,歡迎拍磚

就我個人對於 vuex 的缺點,我所不能接受的就兩點:

  • 繁瑣冗餘的代碼編寫
  • 維護性差、可讀性差的字元串變數註入

那麼,就是得想辦法解決這兩個問題,先來說第一個

封裝自動生成代碼解決 vuex 使用繁瑣問題

用 vuex 需要編寫很多繁瑣的代碼,這些代碼是少不了的,既然少不了,那換個思路,不用我來編寫不就好了

想辦法提取共性,封裝個工具方法,讓它來生成每次使用 vuex 的那些繁瑣代碼,這樣一來,使用就方便了

state 里聲明的數據對象模型,這些代碼是沒辦法自動生成的,畢竟數據模型都不大一樣

而修改數據變數的 mutation 代碼就可以想辦法來自動生成了

/**
 * 根據 state 對象屬性自動生成 mutations 更新屬性的方法,如:
 * state: {
 *  projectId: '',
 *  searchParams: {
 *      batchId: ''
 *  }
 * }
 *
 * ===>
 *
 * {
 *  updateProjectId: (state, payload) => { state.projectId = payLoad }
 *  updateSearchParams: (state, payload) => { state.searchParams = {...state.searchParams, ...payload} }
 *  updateBatchId: (state, payload) => { state.searchParams.batchId = payload }
 * }
 *
 * 非對像類型的屬性直接生成賦值操作,對象類型屬性會通過擴展運算符重新生成對象
 * 且會遞歸處理內部對象的屬性,扁平化的生成 updateXXX 方法掛載到 mutations 對象上
 * @param {Object} stateTemplate
 */
export function generateMutationsByState(stateTemplate) {
  let handleInnerObjState = (parentKeyPath, innerState, obj) => {
    Object.keys(innerState).forEach(key => {
      let value = innerState[key];
      let updateKey = `update${key[0].toUpperCase()}${key.substr(1)}`;
      if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        obj[updateKey] = (state, payload) => {
          let target = state;
          for (let i = 0; i < parentKeyPath.length; i++) {
            target = target[parentKeyPath[i]];
          }
          target[key] = { ...target[key], ...payload };
        };
        handleInnerObjState([...parentKeyPath, key], value, obj);
      } else {
        obj[updateKey] = (state, payload) => {
          let target = state;
          for (let i = 0; i < parentKeyPath.length; i++) {
            target = target[parentKeyPath[i]];
          }
          target[key] = payload;
        };
      }
    });
  };
  let mutations = {};
  Object.keys(stateTemplate).forEach(key => {
    let obj = {};
    let value = stateTemplate[key];
    let updateKey = `update${key[0].toUpperCase()}${key.substr(1)}`;
    if (typeof value === 'object' && value != null && !Array.isArray(value)) {
      obj[updateKey] = (state, payload) => {
        state[key] = { ...state[key], ...payload };
      };
      handleInnerObjState([key], value, obj);
    } else {
      obj[updateKey] = (state, payload) => {
        state[key] = payload;
      };
    }
    Object.assign(mutations, obj);
  });
  return mutations;
}

然後是 mapState 和 mapMutation 註入到 vue 組件的這些代碼也可以通過 computed 計算屬性的特性來自動生成,這樣使用上更加方便,畢竟使用 computed 計算屬性的方式就跟使用 data 里聲明的變數一樣,沒有什麼區別

import store from './index';
/**
 * 將 store 里指定的 state 轉成計算屬性 computed 的 set() get()
 * vue 里就可以直接類似操作 data 屬性一樣使用 state
 *
 * @param {String} moduleName state 所屬的 store 的 module 名
 * @param {Array} states 待處理的 states e.g: ['project', 'searchParams.projectName'] 其中,
 * 掛載在 computed 上的屬性名,預設等於 state,當 state 結構多層時,取最後一層的屬性名
 *
 * ps: state 對應的 mutation 必須以 updateXXX 方式命名
 */
export function storeToComputed(moduleName, states) {
  if (!store) {
    throw new TypeError('store is null');
  }
  if (!moduleName) {
    throw new TypeError("state's module name is null");
  }
  if (!states || !Array.isArray(states) || states.length === 0) {
    throw new TypeError('states is null or not array');
  }
  let computed = {};
  states.forEach(state => {
    if (state.indexOf('.') !== -1) {
      let _states = state.split('.');
      let _key = _states[_states.length - 1];
      computed[_key] = {
        get() {
          let res = store.state[moduleName];
          for (let i = 0; i < _states.length; i++) {
            res = res[_states[i]];
          }
          return res;
        },
        set(value) {
          store.commit(
            `${moduleName}/update${_key[0].toUpperCase()}${_key.substr(1)}`,
            value
          );
        },
      };
    } else {
      computed[state] = {
        get() {
          return store.state[moduleName][state];
        },
        set(value) {
          store.commit(
            `${moduleName}/update${state[0].toUpperCase()}${state.substr(1)}`,
            value
          );
        },
      };
    }
  });

  return computed;
}

那麼最終可以達到的效果就是:

  • 只需在 store 文件里聲明 state 數據變數
  • 然後再需要註入的 vue 組件里註入即可
// 聲明
import { generateMutationsByState } from './helper';

const global = {
    state: {
        count: 0
    }
}
global.mutations = generateMutationsByState(global.state);

const store = new Vuex.Store({
    modules: {
        global
    }
})
// vue里使用
import { storeToComputed } from '@/store/storeToComputed';

export default {
  // ...
  computed: {
      // 將 this.$store.state.global.count 映射成 this.count
     ...storeToComputed('global', ['count'])
  },
}

我的這種用法,其實就只是單純將 vuex 拿來作為數據中心使用而已,在 store 文件里不編寫邏輯代碼,也不使用 action

這種用法的好處,我是覺得,會跟原本在 vue 的 data 里聲明變數後的用法比較類似。因為就是將原本定義在 data 里的變數換成定義在專門的 store 文件里,然後再多一步將變數通過工具方法註入到 vue 的 computed 里,接下去的使用變數的任何場景,在哪賦值,在哪取值,哪裡處理非同步請求等等的代碼,原本怎麼寫,現在還是怎麼寫,完全不影響

這就意味著,這種方案後續如果有缺陷,或者用不習慣,那麼想切換到 vue 原生的輸入輸出方案非常方便,影響點、改動點都會比較少,就是將 storeToComputed 註入到 computed 的變數換到 data 就完事了

甚至說,後續想換掉 vuex 也會比較方便,畢竟只是單純用它當做數據中心而已

然後再配合上 vuex 的動態掛載和卸載的用法,這個數據中心就可以像 angular 框架那樣做到精確控制數據對象的作用域和生命周期,全局共用、模塊間共用、頁面內共用、組件內共用等都可以很方便做到,這樣一來,數據交互就不怕複雜場景了

這是我之所以會這麼使用 vuex 的考慮

自定義 vscode 插件解決字元串變數的跳轉問題

繁瑣的代碼編寫問題搞定了,接下去就是看看怎麼解決字元串變數註入的跳轉問題了

先來說說,我為什麼會在意變數支不支持利用 idea 直接跳轉到聲明的地方

這是因為,有些頁面比較複雜,數據變數比較多,或者時間久了,很容易忘記一些變數的命名、含義

而我們通常都只會在聲明的地方加上一些註釋

所以利用 idea 直接快速跳轉到聲明的地方,第一,有註釋可以快速幫助回憶、理清變數含義;第二,忘記變數命名全稱可以快速複製使用;第三,方便我查看其它數據變數

那麼,怎麼解決這個問題呢?

自然就是自己擴展開發個 vscode 插件來支持了,面向百度的話,vscode 插件開發並不困難,看幾篇教程,清楚插件的生命周期和一些 API 就可以上手了

關鍵是,如何識別 vuex 註入的這些變數?如何跳轉到 store 文件里聲明數據變數的 state 位置?

如果想做成通用的插件,那可能需要多花點精力

但如果只是基於自己當前的項目來解決這個問題,那就簡單多了,因為項目有一定的規範,比如 vuex 的 store 文件存放的目錄地址,比如註入到 vue 組件里的使用方式,這些都是有規範和規律的

簡單說下我的思路:

  1. 先掃描項目 store 目錄下文件,識別出有數據模型 (state) 的文件,解析並存儲數據模型各個變數名和位置
  2. 註冊 vscode 的變數操作響應,當按住 ctrl 並將滑鼠移到變數上時,響應我們的插件程式
  3. 判斷當前聚焦操作的變數是否是通過在 computed 里註入的變數,是則繼續下一步尋找變數聲明的文件位置
  4. 通過變數名和模塊名到 store 里匹配變數,匹配到後,記錄變數的聲明信息和文件位置,當點擊左鍵時,響應跳轉行為

github 地址:vuex-peek

總結

最後簡單總結下,項目里並不是必須要使用 vuex,vuex 所解決的場景,用 vue 原生的輸入輸出機制想想辦法也能解決,區別可能就是代碼的可讀性、維護性上的區別,數據流走向是否清晰等

vuex 作為三方庫,自然就是一個可選的服務,用不用,怎麼用,都因人而異;考慮好自己的訴求,對比好引入前後的影響點,權衡好自己能接受的點就好

比如我,使用 vuex 的方式上說得難聽點,也有點不倫不類,畢竟並沒有按照官方示例來使用,反而是自己搞了套使用規範,這也增加了別人的上手成本

所以寫這篇,不在於強推使用 vuex,只是從自己的一些經歷分享自己使用一些三方庫的心路歷程,所思所想

很多時候,當你開始吐槽某某方案、當你開始無法接受某某用法時,這其實意味著,這是一次絕佳的探索機會

吐槽完就想辦法去優化、去尋找新方案;接受不了時,就想辦法去研究看能否解決這些痛點

人嘛,總是在一次次的踩進坑裡,再爬出來


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文以GaussDB資料庫為平臺,將詳細介紹SQL中DROP、TRUNCATE和DELETE等語句的含義、使用場景以及註意事項,幫助讀者更好地理解和掌握這些常用的資料庫操作命令。 ...
  • 作為一款火山引擎推出的雲原生數據倉庫,ByteHouse基於開源ClickHouse構建,併在位元組跳動內外部場景的檢驗下,對OLAP引擎能力、性能、運維、架構進一步升級。除此之外,ByteHouse也在Serverless方向探索,基於cloud-native 雲原生的理念構建了全新一代的數據倉庫,... ...
  • 本章將介紹如何在 HarmonyOS 上進行實際項目開發。我們將從項目需求分析開始,逐步完成項目的設計、開發、測試和上線過程。 ...
  • 目錄準備界面:view控制項LayoutCreator事件監聽OnClickListener轉跳頁面IntentIntent傳遞數據Toast和AlertDialogGson使用OKhttp3的基本使用post方法get方法輕量級存儲SharedPreferenceListView基本使用1、Simp ...
  • 作為一名全棧工程師,在日常的工作中,可能更側重於後端開發,如:C#,Java,SQL ,Python等,對前端的知識則不太精通。在一些比較完善的公司或者項目中,一般會搭配前端工程師,UI工程師等,來彌補後端開發的一些前端經驗技能上的不足。但並非所有的項目都會有專職前端工程師,在一些小型項目或者初創公... ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 在我們寫項目代碼時,應該更加專註於業務邏輯的實現,而把定式代碼交給js庫或工程化自動處理,而我想說的是,請求邏輯其實也是可以繼續簡化的。 你可能會說,用axios或fetch api就夠了啊,哪有什麼請求邏輯,那可能是你還沒有意識到這個問 ...
  • 需求:客戶的電腦都只能訪問內,伺服器可以訪問外網,客戶電腦使用的項目中用到了高德webapi2.0。10.200.31.45:32100是我們的web伺服器。 網上基本上都是對高德webapi1.4的配置方式,而web2.0有一些差別。 1.前端修改高德地圖的js應用 如果是index.html引入 ...
  • 最近做的幾個項目經常遇到這樣的需求,要在表格上增加一個自定義表格欄位設置的功能。就是用戶可以自己控制那些列需要展示。在幾個項目里都實現了一遍,每個項目的需求又都有點兒不一樣,迭代了很多版,所以抽時間把這個功能封裝了個組件:@silverage/table-custom,將這些差別都集成了進去,方便今... ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...