如何優雅使用 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
  • 前言 微服務架構已經成為搭建高效、可擴展系統的關鍵技術之一,然而,現有許多微服務框架往往過於複雜,使得我們普通開發者難以快速上手並體驗到微服務帶了的便利。為瞭解決這一問題,於是作者精心打造了一款最接地氣的 .NET 微服務框架,幫助我們輕鬆構建和管理微服務應用。 本框架不僅支持 Consul 服務註 ...
  • 先看一下效果吧: 如果不會寫動畫或者懶得寫動畫,就直接交給Blend來做吧; 其實Blend操作起來很簡單,有點類似於在操作PS,我們只需要設置關鍵幀,滑鼠點來點去就可以了,Blend會自動幫我們生成我們想要的動畫效果. 第一步:要創建一個空的WPF項目 第二步:右鍵我們的項目,在最下方有一個,在B ...
  • Prism:框架介紹與安裝 什麼是Prism? Prism是一個用於在 WPF、Xamarin Form、Uno 平臺和 WinUI 中構建鬆散耦合、可維護和可測試的 XAML 應用程式框架 Github https://github.com/PrismLibrary/Prism NuGet htt ...
  • 在WPF中,屏幕上的所有內容,都是通過畫筆(Brush)畫上去的。如按鈕的背景色,邊框,文本框的前景和形狀填充。藉助畫筆,可以繪製頁面上的所有UI對象。不同畫筆具有不同類型的輸出( 如:某些畫筆使用純色繪製區域,其他畫筆使用漸變、圖案、圖像或繪圖)。 ...
  • 前言 嗨,大家好!推薦一個基於 .NET 8 的高併發微服務電商系統,涵蓋了商品、訂單、會員、服務、財務等50多種實用功能。 項目不僅使用了 .NET 8 的最新特性,還集成了AutoFac、DotLiquid、HangFire、Nlog、Jwt、LayUIAdmin、SqlSugar、MySQL、 ...
  • 本文主要介紹攝像頭(相機)如何採集數據,用於類似攝像頭本地顯示軟體,以及流媒體數據傳輸場景如傳屏、視訊會議等。 攝像頭採集有多種方案,如AForge.NET、WPFMediaKit、OpenCvSharp、EmguCv、DirectShow.NET、MediaCaptre(UWP),網上一些文章以及 ...
  • 前言 Seal-Report 是一款.NET 開源報表工具,擁有 1.4K Star。它提供了一個完整的框架,使用 C# 編寫,最新的版本採用的是 .NET 8.0 。 它能夠高效地從各種資料庫或 NoSQL 數據源生成日常報表,並支持執行複雜的報表任務。 其簡單易用的安裝過程和直觀的設計界面,我們 ...
  • 背景需求: 系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行調用,且後期還要提供給其他兄弟部門系統共同調用。 原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書 ...
  • 本文介紹下電腦設備關機的情況下如何通過網路喚醒設備,之前電源S狀態 電腦Power電源狀態- 唐宋元明清2188 - 博客園 (cnblogs.com) 有介紹過遠程喚醒設備,後面這倆天瞭解多了點所以單獨加個隨筆 設備關機的情況下,使用網路喚醒的前提條件: 1. 被喚醒設備需要支持這WakeOnL ...
  • 前言 大家好,推薦一個.NET 8.0 為核心,結合前端 Vue 框架,實現了前後端完全分離的設計理念。它不僅提供了強大的基礎功能支持,如許可權管理、代碼生成器等,還通過採用主流技術和最佳實踐,顯著降低了開發難度,加快了項目交付速度。 如果你需要一個高效的開發解決方案,本框架能幫助大家輕鬆應對挑戰,實 ...