在剛剛加入這家公司的時候,技術
Leader
就和我說過一件事情,但願可以落地前端的自動化,但願我可以出一個可行方案。而此時,在公司前端團隊還很是年輕,可是業務的發展致使團隊規模擴大了一倍多。加上toC
業務變幻無窮,致使各類bug
滿天飛。前端自動化測試的成本是很是高昂的,在長期加班感需求的狀況下還要去顧及自動化測試的腳本開發幾乎是很難實施的。如何去尋找一個低成本可行的自動化測試技術方案就是我作這個mini
版redux
的初衷。之因此叫mini
版。一方面是本身能力的不足還沒法實現一套完整的小程序版本redux
的技術實現,另外一方面以漸進迭代的原則,現知足基本須要前提下,根據後續使用狀況,逐步優化。也有可能不久後dan
大神本身出了一個也說不定。前端
實例和源碼vue
缺人,缺人,咱們缺乏高質量前端,這多是絕大多數技術管理者的訴求。面對系統中漫天的bug
,蟑螂同樣殺不盡的低級錯誤,是否老是那麼的無能爲力。雖然咱們有不少測試工具以及自動化測試的庫,可是咱們依然會困惑於爲何作前端自動化測試實施起來這麼難?react
在十一期間,我開發了一個mini
版本的Redux
用於解決這個問題,而且在這周團隊內部討論後方案是可行的,也開始準備運用到項目中去驗證。所以也將思路分享給你們git
Javacript
語言是一種若類型腳本語言,並且不少錯誤是運行時纔會被發現。所以這種特性也就致使了前端代碼質量難以保證。所以有的團隊會毅然決然的選擇了TS
,TS
確實極大的改善了這一情況。Vue
項目最爲嚴重。導出都是耦合在一塊兒的UI交互邏輯和業務邏輯。也致使了前端目前的一個困境,耦合嚴重。耦合就意味着每一個地方的影響範圍都特別大。常常會致使,明明一樣的業務,這裏改了,另外一個地方不應改的地方也受影響了。(這一點咱們不得不認可Angular
爲何會被稱爲企業級前端框架,它自身已經提供了本身的模塊劃分標準和規則,React
也有本身的Flux
架構方法去指導你們,而Vue
在這塊的缺失也使它在日漸複雜的系統中產生混亂,而尤大也說了Vuex
並不適用於大型應用。也側面反映了這一點)mvvm
,vdom
,雙向綁定的實現,單項數據流講的頭頭是道,甚至是本身均可以當場給你寫一套實現。可是在實踐中,由於缺少對業務數據建模的理解。常常會發生混亂。這也是不少團隊實踐react-redux
遇到的最大困惑UI
邏輯和業務邏輯耦合,那麼咱們只能經過虛擬頁面DOM
來進行測試,可是對於toC
業務基本上每兩三個月就大變臉的UI
來講,這種自動化測試方式的開發成本是很是巨大的。可是對業務邏輯的變化實際上是很小的。所以針對以上幾種問題,尋找一個方法將業務與視圖層解偶,只對業務層單獨進行測試,這樣既能夠大大下降toC
應用自動化測試開發成本,又能夠極大的提升項目中業務的準確性。至於視圖層,由於基於MVVM
的前端應用大多都是數據驅動的系統,所以只要業務數據模型的正確就能夠極大的保證系統的健壯性。github
其實模塊拆分一直都是一個軟件開發領域的難題,如何去解偶各個模塊,雖然咱們有各類方法論去指導咱們去實施,可是方法論畢竟只是一個理論。真正作起來的時候會受各類因素影響,而並非每一個團隊都有具有這種能力的牛人。算法
而在前端領域,MVC
過於複雜,對於前端來講過重,而長期關注與視圖的呈現,大部分人是缺少這方面設計能力的。而MVVM
在這方面給前端提供了一個方向(Flux全文翻譯):編程
Flux原文:redux
Flux eschews MVC in favor of a unidirectional data flow. When a user interacts with a React view, the view propagates an action through a central dispatcher, to the various stores that hold the application's data and business logic, which updates all of the views that are affected. This works especially well with React's declarative programming style, which allows the store to send updates without specifying how to transition views between states.小程序
譯文:設計模式
Flux
避開了MVC
,採起了單向數據流,當用戶與React
視圖進行交互的時候,視圖經過dispatcher
方法傳遞一個action
對象到保存數據和業務邏輯的各個存儲對象區store
中。這些存儲區的數據變化會影響全部視圖,並致使視圖發生更新。這與React
的編程風格有關,該風格容許經過數據的變化來改變視圖,而不須要指定如何經過狀態切換視圖。
經過Flux
的講解,咱們能夠清楚的意識到視圖中,在對用戶各類行爲的響應中,經過派發器(dispatch
)將新的業務數據以動做(action
)爲載體灌入用於處理和存儲業務數據模型(store
)中。以下圖:
而在基於Flux的架構方法論基礎上衍生出來的Redux就是其中被人普遍熟知的,而Vuex也隨着Vue的火爆而被人普遍認識。
可是Redux
和Vuex
雖然在開發上起着一樣的做用,可是在本質上卻存在着很大的不一樣,這也是爲何Vuex
不適用於大型前端應用:
Redux
是框架無關的,Vuex
須要依託於Vue
的響應式屬性Redux
是純原生JS
,與視圖無關,這也就意味着它能夠幫助咱們方便的剝離業務到Redux
中。從而能夠方便的複用到任何前端技術中去。而Vuex
很難作到這一點。Redux
強調reduce
必須是純函數
,純函數意味着相同的參數會致使相同的結果,也就是結果是能夠預知的,從而具備很是好的可測性,這也就知足了咱們對業務進行自動化測試的需求。而Vuex
是依託於修改參數引用(mutations
)的方式,而且actions
是支持異步致使了返回值的不肯定性。結合以上緣由,一個小程序版本的Redux
纔是咱們須要的。
Redux
我相信大部分人都閱讀過Redux
源碼,固然我也寫過一篇關於Redux源碼的文章,我相信原理你們都懂,可是如何去實現一個小程序版本的Redux
的難點是咱們如何實現一個相似於react-redux
的東西將Redux
結合到小程序裏面來。
咱們面臨如下幾個技術問題:
store
存在哪?store
中的數據與Page
中的數據進行響應式?其實就是作一個發佈訂閱模式的實現,可是咱們要保證咱們
store
內部的數據不能被隨意修改,這樣才能保證咱們的業務穩定性。
我想,一提到小程序內部數據共享,你們確定會想到globalData
。可是globalData
是依託於全局app
對象,而全局變量的影響你們是心知肚明的,不必定哪一個新手給你搞壞了也是說不定的。因此也就致使了狀態變化的不可跟蹤。
那麼如何避免使用全局變量又能解決數據存儲的問題呢?答案是 ---- 沙盒模式
沙盒模式,是JS很是廣泛的一個設計模式,它經過閉包的原理將數據維持在一個函數做用於中,而經過返回值內的函數引用這個函數包體內的變量的方式,造成閉包,而只有經過該函數的返回函數才能訪問和修改該閉包內的數據,從而起來了數據保護的做用。
function initMpState () { // mp-redux初始化函數,在這裏造成一個獨立做用域
const reducers = {}; // 該函數做用域內的數據
const finalState = {}; // 該函數做用域內的數據
const listeners = []; // 該函數做用域內的數據
let injectMethod = null; // 該函數做用域內的數
function getStore() { // 用於訪問沙盒內數據的接口
return finalState;
}
function createStore(modules, injectFunc) { // 用於初始化沙盒內數據的接口
...
}
function dispatch(action) { // 用於操做沙盒內數據的接口
...
}
function connect(mapStoreToState, component) { // 用於關聯小程序Page對象的高級API
...
}
return {
createStore,
dispatch,
connect,
getStore
}
}
module.exports = initMpState();
複製代碼
經過沙盒模式,咱們很好的保護了咱們的數據,而且提供了有限的操做手段,安全又可靠的保存了咱們的業務數據模型中的數據。
store
?既然須要暴露接口,又要保持這個函數內的閉包。好複雜呀。可是commonjs
在這方面起到了很好的幫助:
當咱們
require
一個模塊的時候,commonjs
會維持這個模塊在一個獨立的做用域中。而且一直存在。典型的應用場景就是Nodejs
因此經過commonjs
,咱們使用module.exports
將咱們的mp-redux
初始化函數返回的api
集合暴露給調用方:
// mp-redux/index.js
function initMpState() {
...
}
module.exports = initMpState();
複製代碼
這樣咱們就能夠在任何地方視無忌憚的搞事情了(使用api
操做store
數據).
store
中的數據是根據業務而來,如何保存業務模型將是咱們的重點。而這些業務模型又會帶有不少業務邏輯數據處理。同時,咱們還要保證業務的可測性。
所以redux
的reduce
方式是咱們需求的絕佳選擇,所以每個model
必須是一個純函數,它須要每次操做後都要返回一個純對象,也就是業務數據模型。
/* modules,這裏參考redux,咱們能夠拆分不少業務模塊,每一個業務模塊會有本身的業務模型,所以這裏的modules是一個對象,而key就是業務模塊的名字value就是處理業務模型的純函數。 這裏提供一injectFunc 主要是由於小程序在系統加載後就初始化, 所以咱們須要劫持特定api來在這個api中同步store中數據到當前顯示的頁面中。爲何不寫死成小程序的onShow?主要是之後考慮百度小程序,支付寶小程序。這樣更靈活。 */
function createStore(modules, injectFunc) {
if (injectFunc && typeof injectFunc === 'string') {
injectMethod = injectFunc;
}
// 咱們將用戶本身定義的業務模型(model)保存到沙盒內的reducers中
if (modules && typeof modules === 'object') {
const keys = Object.keys(modules);
const len = keys.length;
for (let i = 0; i < len; i++) {
const key = keys[i];
if (modules.hasOwnProperty(key) && typeof modules[key] === 'function') {
reducers[key] = modules[key];
}
}
}
// 對store進行初始化
dispatch({type: '@MPSTATE/INIT'});
}
複製代碼
store
的數據到小程序頁面中,而且進行響應式處理?小程序會自動訂閱Page
參數中的data
對象,所以咱們只要在提供一個包裹函數將咱們須要訂閱的store
中的數據模型反映到小程序Page
函數構建須要的參數中便可。而且注入dispatch
方法,以及數據映射函數mapStoreToState
。
由於每一個頁面只訂閱本身關心的業務數據狀態 ,所以咱們不能把整個
store
都扔給人家。因此咱們須要經過mapStoreToState
來僅僅將用戶須要的業務數據狀態注入到頁面中去。
/* *mapStoreToState,用於用戶本身將本身關注的業務數據狀態訂閱到本身的頁面中 */
function connect(mapStoreToState, component) {
if (!component || typeof component !== 'object') {
throw new Error('mpState[connect]: Component must be a Object!');
}
if (!mapStoreToState || typeof mapStoreToState !== 'function') {
throw new Error('mpState[connect]: mapStoreToState must be a Function!');
}
// 咱們須要將redux相關的函數和狀態注入到用戶的page定義中
const newComponent = { ...component };
// 拿到用戶本身在頁面定義的data,咱們須要保留原來的狀態
const data = component.data || {};
// 獲取用戶訂閱的store中的狀態
const extraData = mapStoreToState(finalState);
if (!extraData || typeof extraData !== 'object') {
throw new Error('mpState[connect]: mapStoreToState must return a Object!');
}
// 合併用戶本身頁面中的狀態,和經過connect注入的store中的狀態,這裏個人實現有點很差
let newData = null;
if (typeof data === 'function') {
newData = {
...data(),
...extraData
}
} else {
newData = {
...data,
...extraData
}
}
// 注入到Page對象中
if (newData) {
newComponent.data = newData;
}
// 獲取須要劫持的生命週期鉤子,由於每一個頁面不必定都劫持同一個生命週期,所以提供了一個各個頁面能夠自定義修改劫持鉤子的方法
const injectFunc = component.getInjectMethod;
const methods = component.methods || {};
const newLiftMethod = injectFunc && injectFunc() || injectMethod;
const oldLiftMethod = component[newLiftMethod];
// 注入dispatch api
methods.dispatch = dispatch;
newComponent.methods = methods;
newComponent.dispatch = dispatch;
newComponent.mapStoreToState = mapStoreToState;
//生命週期鉤子劫持
if (newLiftMethod) {
newComponent[newLiftMethod] = function() {
if (this) {
// 在劫持的鉤子中同步store的數據到頁面
this.dispatch({});
oldLiftMethod && oldLiftMethod.call(this, arguments);
}
}
}
// 返回新的Page對象
return newComponent;
}
複製代碼
// 使用connect來注入須要訂閱的狀態,而且mp-redux會在頁面對象中自動注入dispatch方法
const mpState = require('./../../mp-redux/index.js');
const util = require('../../utils/util.js');
const logActions = require('./../../action/logs.js');
Page(mpState.connect((state) => {
return {
userInfo: state.userInfo.userInfo,
logs: state.logs.logs
}
},
{ // 在這裏全部的業務數據都保存在store中,因此頁面若是隻有業務數據的話,是不須要data屬性的。
clearLogs() {
this.dispatch({ // 經過dispatch方法來發出action,從而更新store中的數據
type: logActions.clearLogs
})
}
}))
複製代碼
store
中的數據,而且反應到小程序的頁面中來?由於小程序的狀態更新須要經過setData
這個api
,所以,咱們就須要在dispatch
中經過該api
來同步store
中的數據狀態
/* * 這裏必定要注意,action是一個原生JS對象,而不是函數,Redux的異步是經過redux-thunk來實現的,可是個人訴求是須要讓咱們應用中的業務邏輯更加容易被測試,所以也就沒有去提供支持,其實實現起來也很簡單。能夠參考我作的[vue-with-redux源碼](https://github.com/ryouaki/vue-with-redux/blob/master/src/index.js) */
function dispatch(action) {
// debugger
const keys = Object.keys(reducers);
const len = keys.length;
// 這個循環用於遍歷model來從新計算出新的store
for (let i = 0; i < len; i++) {
const key = keys[i];
const currentReduce = reducers[key];
const currentState = finalState[key];
const newState = currentReduce(currentState, action);
finalState[key] = newState;
}
if (this) {
// 這裏是根據組件內部的訂閱規則來將新的數據模型經過setData注入到頁面中
const componentState = this.mapStoreToState(finalState) || {};
// 這裏提供了對react和vue的支持,所以也就致使代碼多了幾行,還在測試中。
if (this.setData) { // 小程序
this.setData({ ...componentState })
} else if (this.setState) { // react什麼的吧
this.setState({ ...componentState })
} else { // VUE
const propKeys = Object.keys(componentState);
for ( let i = 0; i < propKeys.length; i++) {
this[propKeys[i]] = componentState[propKeys[i]];
}
}
}
}
複製代碼
其實經過上面的代碼咱們基本上就完成了一個簡單的發佈訂閱了。
action
和model
(我以爲model
比reduce
更容易理解,因此我叫model
,哈哈)不過這裏沒什麼好說的,都和redux
同樣
const actions = require('./../action/logs.js');
const initState = {
logs: []
}
module.exports = function (state = initState, action = {}) {
const newState = { ...state };
switch (action.type) {
case actions.addLogs:
const now = new Date();
newState.logs.push({
time: now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds(),
value: action.data
});
return newState;
case actions.clearLogs:
newState.logs = [];
return newState;
default:
return newState;
}
}
複製代碼
經過這個mp-redux
,實現了業務邏輯,數據與視圖的分離,而業務邏輯與數據都保存在純js代碼中。方便多平臺移植,而要作的只是作一個平臺數據響應式的適配。
更大的好處是解決了視圖與業務層耦合的痛點,而且將數據業務剝離到純函數中,大大提升了業務代碼的可測是性。
因爲提供了業務數據的獨立測試途徑,也下降了總體的測試成本。
給團隊招人,途家網,地點國家會議中心,目前前端團隊很是年輕,咱們有不少需求是沒有既有庫可以知足的,因此咱們有不少技術創新的機會。愛折騰的就聯繫我吧。
另外我在搞前端微服務的實踐,並且已經成功,有興趣的必定要聯繫我呀。
再者,咱們技術要求不高,我不在意什麼Vue源碼原理研究多深,也不在意算法多麼牛,JS用多溜,我指望那些熱愛技術,喜歡專研技術,喜歡經過團隊業務開發中痛點挖掘出技術創新點,提升團隊總體生產效率的人加入咱們 (這是個人觀點,不表明老大是否贊同(-_-!)
)。