本文旨在探索基於Vuex封裝模型的Vue業務模塊化設計,並試圖提出漸進加強架構設想。css
基於Observable模型下的Vue有着簡單直觀的API,藉助MVVM架構模式,在中小Web應用中使用Vue有自然優點。隨着Vue的流行度日益增加,Vue在大型項目中的運用略顯捉襟見肘;尤爲在一些高度複雜的前端應用中,Vue2在TypeScript的支持狀況不甚樂觀,更重要的是Vuex狀態邏輯在模塊化設計上也有至關的優化空間。前端
那麼,到底怎樣的編程模式才更適合Vue中大型業務的開發需求?而基於Vuex的狀態管理又有什麼更好的模型化設計?vue
在數年前,前端常談論先後端分離並逐漸將其解決。但前端開發演進到當前,前端領域更進一步須要解決核心業務邏輯與UI的分離,它讓邏輯與UI解耦,它帶來的通用性上的代碼或服務複用。而模塊化即是一個解決UI與業務邏輯分離的巨大契機,它同時爲了更全面的自動化測試策略來更好地保證產品質量,另外Team成員協做效率與產品迭代速度也將所以明顯提高,最後它也將明顯增強工程項目的自治與分治。node
時下最流行的MVVM框架之一:Vue,即是咱們今天想要探索模塊化設計的目標。webpack
在探索模塊化以前不得不先從前端的狀態模型分類上進行分析,我把它概括成以下圖所示的五層模型:git
對於大多數前端應用而言,都須要處理這五層狀態模型其中的至少一種,但並不意味着這五層模型在中大型應用都會被設計實現。github
下面就從Vue應用角度來談談這五層狀態。web
Vue官方提供不少UI邏輯複用的API:Mixins / Custom Directives / Plugins / Filters。尤爲是Mixins在組件內的狀態邏輯複用起到重要做用。但組件的邏輯複用至少還包括:Renderless Components / HOC。雖然HOC並不像React的HOC直觀便利,雖然Vue的HOC使用並不如Mixins頻繁,但在一些相似通用container組件的props注入,它倒是不可取代。ajax
在倡導組合大於繼承的今天,對於最近受到普遍討論的Vue3.0 Function-based Component RFC也算是迎合這樣的趨勢,相信它在將來Vue3.0 組件邏輯複用性將帶來巨大便利。值得注意在React hooks剛出來的時,Vue團隊便藉助mixin來mock一個vue-hooks的PoC。若是是有致於Vue2在組件內相似hooks概念上封裝以便對將來Vue3有更平滑的遷移過程,或許也是一個不錯的探索小方向。vuex
在共享狀態邏輯複用上,Vuex的模塊化設計對於大中型Vue項目中應該是相當重要的。對於Vuex更接近Object-based或者叫JSON-based設計定義,它與一般OOP模型契合程度並不算很好。那麼,在我所理解的Vuex模塊化定義中至少有如下幾個方面:
首先,OO徹底迎合模塊顆粒的設計,尤爲是經過DDD方式獲得多個領域下的modules實現。模塊間相互注入,並定義彼此間的上下文,甚至是定義各自的貧血/富血模型等等。
經常模塊間的初始化等動做存在相互依賴的關係,所以有可選且恰當的模塊生命週期的APIs將提供相互依賴的模塊間在初始化或者重置等依賴邏輯注入,固然這部分也能夠徹底由某個統一的事件系統來代替,這取決因而否但願模塊生命週期標準化。
有可選或是可自定義的事件系統,以便模塊間在複雜系統中有更便利地交互和消息傳遞。
根據以上這些定義,那麼這裏我試圖推出一個全新的解決方案 -——— usm-vuex
Class-based Module
在上圖左側的code是Vuex的官方Todo例子,一樣的邏輯基於usm-vuex
的實如今右側。左側的Object-based的形式包括getters等computed API並不符合OO直覺;而基於usm-vuex
的實操代碼更接近OO直覺,同時又利用usm-vuex
裝飾器@state
和@action
進行定義並保持state和action的直接關聯。
Dependency injection & Object-oriented
基於usm-vuex
的module有直接注入的機制,不管是手動注入仍是利用IoC注入,它能以諸如this._modules.foobar
這樣的方式直接調用依賴的模塊。而基於class的module形式,又很是適合實踐上下文/富血模型等OO概念。
Optional Life Cycle/Event System
usm-vuex
提供瞭如下5個模塊生命週期APIs,而且支持異步:
固然,它是可選的,只有在必要狀況下,你才須要使用它。
至於事件系統,能夠根據本身的實際須要選擇events
和rxjs
,或者自定義實現本身須要的事件系統模型,這徹底取決於模塊信息交互的複雜度和事件形式。
因爲Vue和Vuex都是基於Observable模型,那麼在中大型Vue應用中把所有的數據和狀態都進行Observable實例化顯然並不現實,畢竟如此巨大的數據量進行Observable實例化所消耗的性能將形成程序初始化緩慢,所以分離出以Mutable爲主的services等狀態層也算是一種可選擇的手段之一。
這裏主要能夠分紅三種類型以上:
在必要狀況下,Services能夠藉助Service Worker以及Web Worker等隔離運行環境,優化應用的運行效能;一些通用化JavaScript的類庫也能夠運行於此,進行各類通用邏輯的代碼複用;而對於高性能模塊,在現代瀏覽器的新特性支持愈來愈良好的狀況下,徹底能夠利用WebAssembly進行顯著的性能提高。
一般在非必要狀況下,通常不會單獨抽離出持久層,而更多的狀況是是直接在Vuex狀態管理中使用相關持久化插件,例如vuex-persist
等。但當中大型應用的持久化須要更多的緩存自定義邏輯時,引入存儲包裝庫諸如localforage
或dexie
等進行必定程度的持久層設計也是必要考慮。
對於大多數Vue這樣SPA而言,基於http等網絡請求進行先後端交互,一般都會造成後端數據與前端狀態的映射 關係,當後端的接口定義相對標準化,爲了更高效的進行請求封裝與接口管理,引入諸如vuex-orm
/type-orm
等庫將顯著提高開發效率和維護成本。
請求模塊或應用可能涉及到的SDK,均可能須要一層完整的遠程交互模塊的封裝,例如ajax/websocket/fetch等Web API的獨立封裝與整合,以提供按需配套的外部遠程交互參數化配置與調度控制。
從API數據查詢類型分類,不管是RESTful仍是GraphQL,在API與對應的響應數據結構均可以造成一整套的數據解析與再分發的管理,利用後端提供的schema並藉助normalizr
等相似數據解析庫進行解析。它造成與後端徹底一致的領域劃分與映射關係,且解析後的數據集合幾乎應該是後端的數據庫的子集。這樣作還能帶來前端狀態一致性的巨大便利。
按類型劃分:
在大部分狀況,類型模塊間的劃分將決定模塊處於狀態分層位置是不是同層狀態,是否須要主線程隔離運行等問題,甚至是代碼庫模塊的分離與分類管理。固然,它也徹底取決於對模塊的顆粒度的控制。
按用途劃分:
View Module負責UI的大部分渲染邏輯與少許UI共享狀態管理,而Service Module更可能是負責核心的業務邏輯模塊以及其餘基礎服務模塊。它們的分類有助於將UI與業務徹底隔離開,它同時也更有助於後續的自動化測試實踐更完整的測試策略。
在中大型Vue應用開發的最初階段,在少數幾個或十幾個之內的模塊間,手動模塊初始化依賴注入或許更簡便。但隨着業務需求膨脹,模塊逐漸變多,且它們之間的依賴關係愈加複雜的時候,不管本身設計Dependency Injection(如下簡稱DI)/Service Locator或是採納社區成熟方案,適時引入IoC相關的依賴管理庫變得很是有必要。目前前端社區中的IoC庫,以inversify
最爲知名成熟,固然也有一些類Angular DI庫也是一個不錯選擇。
對於大部分的項目起步,模塊間事件關係並不複雜時,確實無需引入一個複雜的事件系統,簡單的封裝便可;但當應用的模塊間的事件關係升級到至關複雜度,甚至是須要事件流的方式處理彼此之間的復瑣事件時,根據項目實際狀況引入RxJS
也算是固然不錯的選擇。部分其餘觀點上文已經說起,這裏再也不敷述。
從軟件開發方法而言,Domain-Driven Design是咱們在分析和設計業務模型時很是重要的有效工具。合理的領域模型設計,對於越大型的應用變得越是相當重要。
從程序編程原則而言,諸如SOLID這樣經典的OOP原則,以及像KISS或者DRY等原則,咱們都應當進行充分地理解和運用這些原則,以儘量減小一些反模式的狀況出如今通用業務邏輯設計與實現中,避免後續因業務模型設計不合理致使的低效重構的可能。
項目工程是否選擇Multi-repo或是Monorepo,這徹底取決於一開始分包後子包之間的依賴關係以及開發團隊組織管理和協做模式,同時也包括一整套CI/CD/AT/UserStory/AC等開發流程相關部分的版本控制管理的完整定義。
若採用Monorepo,那麼如何使用lerna時恰當地處理子包共享配置以及配置差別化則變得尤爲重要。
懶加載在大中型應用中是常見的一種必要手段。那麼在Vue應用中,至少能夠有如下幾方面能夠進行相關處置:
registerModule
import()
不管是Mixins-based或者是將來Vue3.0的Function-based Component,Vue的UI組件應該儘量的分離UI與業務邏輯,將UI組件變得更加純粹化,僅保留必要的UI組件內的狀態與邏輯。這樣的組件顆粒將變得更爲輕便、通用與高可複用,並在一些細節上注意隔離,例如能在利用usm-vuex
進行computed的就儘可能別在組件內進行。如此UI組件,目的就是變得更加簡潔的「模版化」。
模塊化工程須要注意細節點至少有如下幾方面:
對因而否採用Monorepo/Multi-repo,選擇哪些工程構建工具Webpack/Glup, 業務邏輯如何分包,等問題,它們取決於Team的技術棧熟悉度與工程屬性與相關社區基礎設施建設成熟度,同時也取決於業務領域的拆分。
這裏應該思考和注意到涉及的CI/CD的腳本管理是否進行通用化配置封裝,以及根據項目的業務特色設計更符合工程項目自己的全面自動化策略(MT/E2E/IT/UT,等)。
從開發模式角度更多應該考慮到開發的Workflow定義、業務拆分和開發人員劃分,它們對於總體開發週期以及業務領域劃分後對最後落地的模塊實現模塊顆粒進行並行開發/測試等的影響。固然,從軟件項目的版本控制角度,也應一併考慮Team使用何種恰當的Branch Model,例如Branch by Abstraction等模型。
就算是總體項目OOP爲主,但並不意味這OOP就應該項目的所有的編程模式,適當的FP/FRP都是應當和合理的。尤爲是OOP下的module徹底能夠配合少許的核心可測的業務型helper function,以及一些徹底不含業務的抽象工具型的util function。
對複雜的中大型應用而言,小到module間依賴的function,大到項目的領域模型、module間的依賴關係與工程技術棧,甚至是完整的項目構架,若是這些信息可以不一樣以層次的模型可視化,那麼這將對設計、管理和維護項目的方方面面都有極大的幫助。
本文的架構完整Demo: github.com/unadlib/usm…
如下是Demo項目中幾個說明要點:
lerna初始化後,進行領域驅動設計,獲得大的領域模塊。在必要狀況下,將能夠進行分包,同時啓用動態import懶加載,以提升構建時性能和運行性能。
在覈心應用子項目的初始化使用vue-cli3建構,選擇TypeScript做爲主要語言,它將自動引入Webpack的ts-loder。
這是核心目錄結構:
|-- App.vue
|-- main.ts
+-- modules/
|-- Todos/
|-- Navigation/
|-- Portal/
|-- Counter/
...
+-- lib/
|-- loader.ts
|-- moduleConnect.ts
...
+-- components/
...
複製代碼
main.ts
是默認的entry。
// ...
// omit some modules
import { load } from './lib/loader';
const { portal, app } = load({
bootstrap: "Portal",
modules: {
Counter,
Todos,
Portal,
Navigation
},
main: App,
components: {
home: {
screen: TodosView,
path: "/",
module: "todos"
},
counter: {
screen: () => import("./components/Counter"),
path: "/counter",
module: "counter"
}
}
});
Vue.prototype.portal = portal;
new Vue(app).$mount("#app");
複製代碼
App.vue
是主視圖文件。
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/counter">Counter</router-link>
</div>
<router-view />
</div>
</template>
複製代碼
modules
包含所有的業務邏輯,也包括視圖層狀態和導航模塊等,它將由Vuex來啓動,例如如下是Counter模塊:
import { injectable } from "inversify";
import Module, { state, action, computed } from "../../lib/baseModule";
@injectable()
export default class Counter extends Module {
@state count: number = 0;
@action
calculate(num: number, state?: any) {
state.count += num;
}
getViewProps() {
return {
count: this.count,
calculate: (num: number) => this.calculate(num)
}
}
}
複製代碼
lib/loader.ts
是應用配置加載器,它根據中心化配置來啓動整個項目。
import { Container } from 'inversify';
export function load(parmas: any = {}) {
const { bootstrap, modules, ...option } = parmas;
const container = new Container({ skipBaseClassChecks: true });
Object.keys(modules).forEach(key => {
container.bind(key).to(modules[key]);
});
container.bind("AppOptions").toConstantValue(option);
const portal: any = container.get(bootstrap);
portal.bootstrap();
const app = portal.createApp();
return {
portal,
app,
};
}
複製代碼
lib/moduleConnect.ts
是ViewModule的View鏈接器,這是一個高階組件形式的鏈接器。
import { Component, Vue } from "vue-property-decorator";
export default (ViewContainer: any, module: string) => {
@Component({
components: {
ViewContainer
}
})
class Container extends Vue {
props = ViewContainer.props;
get module() {
const portal = this.portal as any;
return portal[module];
}
render(createElement: any) {
const slots = Object.entries(this.$slots)
.map(([_, node]: [string, any]) => {
node.context = (this as any)._self
return node
});
const props = this.module.getViewProps(this.$props, this.$attrs);
return createElement(ViewContainer, {
props,
scopedSlots: this.$scopedSlots,
on: this.$listeners,
attrs: this.$attrs,
}, slots);
}
}
return Container;
};
複製代碼
components/Counter/index.tsx
是Counter的組件。
import { Component, Vue, Prop } from "vue-property-decorator";
import './style.scss';
type Calculate = (sum: number) => void;
@Component
export default class CounterView extends Vue {
@Prop(Number) count!: number;
@Prop(Function) calculate!: Calculate;
render(){
return (
<div class="body">
<button onClick={()=> this.calculate(1)}>+</button>
<span>{this.count}</span>
<button onClick={()=> this.calculate(-1)}>-</button>
</div>
)
}
}
複製代碼
配合TSX的View組件模塊,同時基於此架構等總體設計將很大程度上契合TypeScript的類型檢查和推導。
在該Demo架構中最核心的設計部分應該是usm-vuex
,它讓Vuex進行業務模塊化變得簡單明瞭,配合View層的ViewModule,它可以讓當前的架構設計變得高內聚低耦合,在複用性與維護性上大大提升,同時配合DI,讓模塊間的依賴變得清晰易懂。
看完該Demo架構設計或許會有這樣的疑問:
既然如此相似Angular,那麼爲何不直接使用Angualr而是Vue呢?
首先,從GUI State Model角度而言,我認同這樣的說法:Mutable < Immutable < Observable,基於Mutable的髒檢查機制的Angular,雖然從Anuglar已經極大優化了性能,可是事實上在某些關鍵的state邏輯處置上,有時候Angular仍是不得不借助RxJS進行Observable才能進行更好的處理。而Vue自然的基於Observable,這在State模型上便佔據上風。固然,這只是單純的從GUI State Model上比較,這並不意味着Vue就所以是比Angular更優秀的框架。
而基於相似Demo架構的Vue應用,帶來的即是更靈活和可自定義,幾乎不少部件都是可選的。例如,IoC不滿意,那麼它是能夠適時調整。可是若是是Angular,事實上可調整的空間並不大。不少時候,它已經提供完整的一整套解決方案。
最後從架構的演進來講,它的架構徹底能夠根據業務項目的成長和進行按部就班地調整和優化,最終獲得一個更契合項目業務自身的架構。這樣的漸進加強架構方案和Vue所提出的漸進式框架是徹底契合的,它符合一個業務的不斷成長和變化的客觀。固然,若是一個業務項目已經很是成熟和定義清晰且需求穩定,那麼從一開始便選擇使用Angular2+也無可厚非,甚至應該要徹底支持這樣節省資源的高效選擇。
在探索Vue的業務模塊化設計過程當中,Vuex的模塊化方案是相當重要的一個環節,它將盡量解決業務邏輯實現層面的高內聚和低耦合,並且在並行開發和自治分治上又有明顯改善;同時基於此方案下的漸進加強架構理念,又能迎合不斷快速變化的業務需求,使其不斷的演進和優化工程,對於總體項目與業務模型間的契合起到顯著地捏合做用。
usm-vuex
Repo: github.com/unadlib/usm