本文首發於 vivo互聯網技術 微信公衆號
連接: https://mp.weixin.qq.com/s/Ka1pjJKuFwuVL8B-t7CwuA
做者:悟空中臺研發團隊
經過《揭祕 vivo 如何打造千萬級 DAU 活動中臺 - 啓航篇》的技術揭祕,相信咱們對於 RSC 有了更多的瞭解。RSC(remote service component) 即遠程服務化組件,經過熱插拔的機制,可視化配置,即插即用,快速構建活動頁面,是活動頁面的核心組成單元。javascript
RSC 是一個高效的對活動頁組成單元的抽象設計方案,最大程度上提高了開發效率,下降了開發者的心智負擔。咱們但願開發者在開發中遵循【高內聚,弱耦合】的設計理念,只需關心 RSC 組件內部的展現和邏輯的處理。前端
(圖1)vue
但在咱們實際的業務開發中發現,如上圖1 ,咱們天天都要面對大量的類似場景,用戶經過參與【大富翁遊戲】獲取了遊戲的點數,而後【大富翁組件】就須要把遊戲結果點數通知給【集卡組件】,而後【集卡組件】獲取相應卡片,點擊【翻卡】,通知【大富翁組件】更新剩餘遊戲次數。在這個活動頁場景中涉及大量的組件之間的協做和數據共享。因此若是把活動當作一個小型的前端系統,RSC 只是構成系統的一個基本要素,還有一個很是重要的要素不能忽略,那就是 RSC 組件之間的鏈接。固然這種鏈接還和場景上下文相關聯。因此在對 RSC 組件進行治理的過程當中,首先須要解決的就是活動頁內組件之間的數據狀態的管理。java
經過不斷的深刻思考問題,探索現象背後的本質原理,從架構設計層面上很好的解決了組件在不一樣的場景上下文中的鏈接(狀態管理)。例如:算法
今天就重點聊聊,在活動頁內,RSC 組件與組件之間的鏈接。下一篇咱們一塊兒聊聊平臺和沙箱環境下的 RSC 組件鏈接。vue-router
由於咱們使用 Vue 做爲咱們前端的 ui 基礎框架,因此下面技術方案都是基於 Vue 。vuex
(圖2)編程
一圖勝千言,如圖 2 。固然咱們想到的最簡單的方案,經過實現一箇中心化的事件處理中心,來記錄組件內的訂閱者,當須要協同時就經過自定義事件通知到各個相關的組件內部的訂閱者。固然通知中能夠攜帶 payload 參數信息,達到數據共享的目的。其實 Vue 自己也自帶一個自定義事件系統, Vue 組件之間的自定義事件就是基於此來實現,詳細 api 請參與 Vue 文檔。咱們能夠基於 Vue 自己實現 EventBus 的機制,不須要引入新的依賴,減小 bundle 體積,api使用以下述代碼。json
const vm = new Vue() // 註冊訂閱者 vm.$on('event-name', (payload) => {/*執行業務邏輯*/}) // 註冊訂閱者,執行一次後自動取消訂閱者 vm.$once('some-event-name', (payload) => {/*執行業務邏輯*/}) // 取消某事件的某個訂閱者 vm.$off('event-name',[callback]) // 通知各個訂閱者執行相對應的業務邏輯 vm.$emit('event-name',payload)
在實踐中發現基於 EventBus 的數據狀態管理模式的優勢:api
固然EventBus方案的也會有些不足:
在認識到 EventBus 的架構設計上的不足時,咱們也會 Eating our own dog food,實現了一套可視化的機制,經過對代碼的抽象語法樹的分析,提取訂閱者和發送者的信息,可視化顯示他們之間的關聯關係,幫助咱們快速理解問題。
另外,對於複雜的業務邏輯設計出【前置腳本】的改進方案。例如,活動頁面雖然是由多個RSC組件構成,可是請求的服務端接口仍是一個,包含了頁面初始化狀態的全部的數據,此時咱們就能夠在前置腳本中統一處理獲取數據的邏輯,而後再同步到各個RSC組件內部。【前置腳本】的方式,就是抽取一個全局的對象,包含共享的狀態和業務邏輯。多個組件依賴這個全局的對象,架構設計如圖3,是對 EventBus 方案的一個補充。
(圖3)
經過前置腳本,能夠解決複雜業務難以維護理解的問題,可是也帶來一些風險點如須要暴露全局對象,有被覆蓋或者被修改的風險。通過前置腳本的改進以後,咱們愈來愈清晰的感覺到咱們須要的狀態管理模式是什麼樣子,那就是 Vuex 。那接下來咱們就聊聊Vuex。
Vuex 是什麼?
Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也集成到 Vue 的官方調試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀態快照導入導出等高級調試功能。
Vuex 有哪些特色?
Vuex 是一個通用狀態管理框架,怎麼無縫融入到咱們的 RSC 組件體系中呢?咱們須要在項目中引入對 Vuex 的依賴和支持,在頂層的 Vue 中添加對 store 的依賴。
咱們項目的基本結構:
. └── src ├── App.vue ├── app.less ├── assets ├── component ├── directive ├── main.js ├── stat ├── store └── utils ├── babel.config.js ├── package.json ├── public
根據規範,首先在咱們的項目目錄中的 package.json 中添加對 Vuex 的依賴
{ "dependencies": { "vuex": "^3.0.1" } }
//store/index.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export const store = new Vuex.Store({ // 狀態數據 state() { return {} }, getters: {}, mutations: {}, actions: {}, })
將上述建立 store對象注入到頂層的 Vue 對象中,這樣全部的 Vue 組件就會經過 this.$store 獲取頂層的 store 對象。另外, Vuex 還提供了好用的工具類方法 ( mapState , mapActions , mapGetters , mapMutations ) 來進行數據共享和協同。
// App.vue import { store } from './store' new Vue({ // 注入 store // 在全部的改 Vue 管理的 vue 對象中均可以經過 this.$store 來獲取 store, })
咱們仍是但願在開發組件時,開發者大部分時間只關注本身的展示和業務邏輯,只是在組件在活動頁中被渲染時,纔將自身狀態共享到頂層的 store 中去。因此組件具備自身的獨立 store 狀態管理,經過 namespace 命名空間進行模塊的狀態隔離,而後在組件的 beforeCreate 生命週期方法內,經過 Vuex 的 registerModule 進行動態的 store 的註冊。
能夠經過抽取公共 StoreMixin 來簡化這一過程,還能夠自動開啓 namespaced: true 和針對當前的命名空間擴展快捷方法和屬性。代碼以下:
// store-mixn.js export default function StoreMixin(ns, store) { return beforeCreate() { // 保證 namespace 惟一性 // 開發者能夠經過函數生成惟一的namespace // 框架能夠生成惟一的namespace const namespace = isFn(ns) ? ns(this) : gen(ns) this.$ns = namespace store.namespaced = true this.$store.registerModule(namespace, store) // 擴展快捷方法和屬性 this.$state = this.$store.state[namespace] this.$dispatch = (action, payload) => this.$store.dispatch(`${namespace}/${action}`, payload) this.$getter = //... this.$commit = //... } }
//store.js // 當前組件自有store export default { // 組件自身的狀態 state() { return {} }, mutations: {}, getters: {}, //...other things } // code.vue // 組件對外的入口模塊 import store from './store' export default { mixins: [StoreMixin(/*namespace*/ 'hello', /* 組件的 store */ store)], }
由於在一個活動中 RSC 組件會被重複加載屢次,全部也會致使相同 namespace 的 store 模塊重複加載致使模塊覆蓋。怎麼保證 namespace 的惟一性呢?咱們能夠,在 StoreMixin 中進行 namespace 註冊的時候,判斷有沒有相同的 namespace ,若是有就對 namespace 作一次重命名。好比在已經註冊了 hello 爲命令空間的 store 時,再次註冊 namspace hello 自動會變成 hello1 ,自動作區分。簡單的算法實現以下,
// gen.js // 生成惟一的 namespace const g = window || global g.__namespaceCache__ = g.__namespaceCache__ || {} /** * 生成惟一的 moduleName, 同名 name 默認自動增加 * @param {*} name */ export default function genUniqueNamespace(name) { let cache = g.__namespaceCache__ if (cache[name]) { cache[name].count += 1 } else { cache[name] = { count: 0, } } return name + (cache[name].count === 0 ? '' : cache[name].count) }
另外,開發者能夠經過 store-mixin 中傳遞自定義函數來生成惟一的 namespace 標識。好比,以下代碼,根據 vue-router 中的路由動態參數來設置 namespace
export default { mixins: [StoreMixin((vm) => vm.$router.params.spuId), store], }
由於動態 namespace 就會帶來不肯定性的問題,以下代碼示例,假如hello被重命名爲hello1, 另外在 Vuex 中 mapXXX ( mapState , mapMutations 等)方法時,須要精確傳遞 namespace 才能獲取組件內 store 的上下文。
// code.vue export default { mixins: [StoreMixin('hello', store)], computed: { ...mapGetters('hello', [ /* hello namespace store getter */ ]), ...mapState('hello', [ /* hello namespace state property */ ]), }, methods: { ...mapActions('hello', [ /* hello namespace actions method */ ]), ...mapMutations('hello', [ /* hello namespace mutations method */ ]), }, }
怎麼解決 Vuex mapXXX 方法中動態 namespace 的問題?首先咱們咱們想到的是在 StoreMixin 中將 namespace 設置在 Vue 的 this.$ns 對象上,這樣被 StoreMixin 混入的組件就就能夠動態獲取 namespace 。
// store-mixn.js export default function StoreMixin(ns, store) { return beforeCreate() { // 保證 namespace 惟一性 const namespace = gen(ns) // 將重命名後的 namespace 掛載到當前 vue 對象的$ns 屬性上 this.$ns = namespace //... } }
雖然咱們能夠在組件內經過 this.$ns 獲取組件中的 store 的命名空間,假想着咱們能夠:
// code.vue export default { computed: { ...mapGetter(this.$ns, [ /* hello namespace store getter */ ]), ...mapState(this.$ns, [ /* hello namespace state property */ ]), }, methods: { ...mapActions(this.$ns, [ /* hello namespace actions method */ ]), ...mapMutations(this.$ns, [ /* hello namespace mutations method */ ]), }, }
很遺憾,在這個時刻 this 根本就不是當前 Vue 的實例,this.$ns 華麗麗的 undefined。那怎麼辦呢?JS 有不少函數式編程的特色,函數也是值,能夠做爲參數等進行傳遞,其實函數除了具備值特性外還有一個很重要的特性就是 lazy computed 惰性計算。基於這樣的思考,對 mapXX 方法進行擴展,支持動態的 namespace 。而後在 mapXXX 方法中,等到 vm 是當前 Vue 的組件實例時,纔去獲取當前的組件的 namespace 。
// code.vue import { mapGetters, mapState, mapActions, mapMutations } from 'vuex-helper-ext' export default { computed: { ...mapGetters((vm) => vm.$ns, [ /* hello namespace store getter */ ]), ...mapState((vm) => vm.$ns, [ /* hello namespace state property */ ]), }, methods: { ...mapActions((vm) => vm.$ns, [ /* hello namespace actions method */ ]), ...mapMutations((vm) => vm.$ns, [ /* hello namespace mutations method */ ]), }, }
我相信你,確定發現了其中一個問題,this.$ns 只能 StoreMixin 的組件內獲取到,那該組件的子組件怎麼辦呢?怎麼解決子組件獲取父組件的 namespace ?這個時候咱們就須要藉助 Vue 強悍的 mixin 的體系了,設計一個全局 mixin ,在組件建立的時候判斷父組件有沒有 $ns 對象,若是存在就將當前的組件的 $ns 設置爲父組件一致,若是沒有就跳過。
function injectNamespace(Vue) { Vue.mixin({ beforeCreate: function _injectNamespace() { const popts = this.$options.parent; if (popts && popts.$ns) { this.$ns = popts.$ns; const namespace = this.$ns; // 爲組件擴展快捷方法和屬性 this.$state = this.$store.state[namespace] this.$dispatch = (action, payload) => this.$store.dispatch(`${namespace}/${action}`, payload) this.$getter = //... this.$commit = //... } } }); } // main.js Vue.use(injectNamespace);
這樣子組件就會默認獲取父組件設置的 namespace ,有了這個 mixin 的魔力,咱們就能夠把 mapXXX 方法的設計的擴展更優雅的一點,由於在 mapXX 方法中能夠以 $ns 屬性爲默認的 namespace 。更清爽一點,保持和官方一致的風格, 這樣才把 Vuex 更好的融入咱們體系中去。
// code.vue export default { computed: { ...mapGetter([ /* hello namespace store getter */ ]), ...mapState([ /* hello namespace state property */ ]), }, methods: { ...mapActions([ /* hello namespace actions method */ ]), ...mapMutations([ /* hello namespace mutations method */ ]), }, }
經過下面的小栗子,咱們能夠看到對於開發者來講,只要按照標準的 Vuex 的開發方式來開發就能夠了,好似什麼都沒有發生過 ^_^。其實在內部咱們作了不少的努力,架構設計的目的就是【讓簡單的事情變得更加簡單 , 讓複雜的事情變得可能】。
store.js RSC 組件自有 store
export default { state() { return { mott: 'hello vue' } }, mutations: { changeMott(state) { state.mott = 'hello vuex' }, }, }
text.vue text 子組件,mapState 自動動態獲取命名空間
<template> <div @click="changeMott">{{ mott }}</div> </template> <script> import { mapState, mapMutations } from 'vuex-helper-ext' export default { computed: { ...mapState(['mott']), }, methods: { ...mapMutations(['changeMott']), }, } </script>
code.vue
<tempalte> <text></text> </template> <script> import store from './store'; import text from './text'; export default { mixins: [StoreMixin('hello', store)], components: { text }, methods: { // .... } } </script>
本文寫到了這裏,漸進尾聲,感謝相伴。咱們一塊兒回顧了RSC組件化方案,在解決悟空活動中臺實際業務場景上走過的路,團隊在技術上爲努力解決 RSC 組件與組件之間狀態管理上的思考。下一篇咱們聊聊 RSC 組件與平臺之間,與跨沙盒環境的鏈接上的狀態管理,歡迎一塊兒交流討論。
更多內容敬請關注 vivo 互聯網技術 微信公衆號
注:轉載文章請先與微信號:Labs2020 聯繫。