老樹發新芽—運用 mobx 加快你的 AngularJS 運用

老樹發新芽—運用 mobx 加快你的 AngularJS 運用

原文: https://github.com/kuitos/kui…

1月尾的時刻,Angular 官方博客宣布了一則音訊:

AngularJS is planning one more significant release, version 1.7, and on July 1, 2018 it will enter a 3 year Long Term Support period.

即在 7月1日 AngularJS 宣布 1.7.0 版本以後,AngularJS 將進入一個為期 3 年的 LTS 時代。也就是說 2018年7月1日 起至 2021年6月30日,AngularJS 不再兼并任何會致使 breaking changes 的 features 或 bugfix,只做必要的題目修復。詳細信息見這裏:Stable AngularJS and Long Term Support

看到這則音訊時我照樣感想頗多的,作為我的前端發矇框架,我從 AngularJS 上吸取到了異常多的營養。雖然 AngularJS 作為一款優異的前端 MVW 框架已精彩的完成了本身的歷史使命,但斟酌到即使到了 2018 年,許多公司基於 AngularJS 的項目依舊處於服役階段,連繫我過去一年多在 mobx 上的探究和實踐,我決定給 AngularJS 強行再續一波命🙃。(乘車求治遷延症良方,二月初草擬的文章五月份才寫完,消息都要逾期了😑)

準備事情

在最先之前,我們須要給 AngularJS 搭配上一些現代化 webapp 開闢套件,以便背面能更輕易地裝載上 mobx 引擎。

AngularJS 合營 ES6/next

現在是2018年,運用 ES6 開闢運用已成為事實規範(有能夠的引薦直接上 TS )。怎樣將 AngularJS 搭載上 ES6 這裏不再贅述,能夠看我之前的這篇文章:Angular1.x + ES6 開闢作風指南

基於組件的運用架構

AngularJS 在 1.5.0 版本后新增了一系列激動人心的特徵,如 onw-way bindings、component lifecycle hooks、component definition 等,基於這些特徵,我們能夠輕易的將 AngularJS 體系打形成一個純組件化的運用(如果你對這些特徵很熟習可直接跳過至 AngularJS 搭配 mobx)。我們一個個來看:

  • onw-way bindings 單向綁定

AngularJS 中運用 <來定義組件的單向數據綁定,比方我們如許定義一個組件:

angular
    .module('app.components', [])
    .directive('component', () => ({
        restrict: 'E',
        template: '<p>count: {{$ctrl.count}}</p><button ng-click="$ctrl.count = $ctrl.count + 1">increase</button>'
        scope: {
            count: '<'
        },
        bindToController: true,
        controllerAs: '$ctrl',
    })

運用時:

{{app.count}}
<component count="app.count"></component>

當我們點擊組件的 increase 按鈕時,能夠看到組件內的 count 加 1 了,然則 app.count並不受影響。

區分於 AngularJS 賴以成名的雙向綁定特徵 scope: { count: '='},單向數據綁定能更有用的斷絕操縱影響域,從而更輕易的對數據變化溯源,下降 debug 難度。
雙向綁定與單向綁定有各自的上風與劣勢,這裏不再議論,有興緻的能夠看我這篇回覆:單向數據綁定和雙向數據綁定的優缺點,合適什麼場景?

  • component lifecycle hooks 組件生命周期鈎子

    1.5.3 最先新增了幾個組件的生命周期鈎子(目的是為更輕易的向 Angular2+ 遷徙),分別是 $onInit $onChanges $onDestroy $postLink $doCheck(1.5.8增添),寫起來也許長如許:

    class Controller {
        
        $onInit() {
            // initialization
        }
        
        $onChanges(changesObj) {
            const { user } = changesObj;
            if(user && !user.isFirstChange()) {
                // changing
            }
        }
        
        $onDestroy() {}
        
        $postLink() {}
        
        $doCheck() {}   
    }
    
    angular
        .module('app.components', [])
        .directive('component', () => ({
            controller: Controller,
            ...
        }))

    事實上在 1.5.3 之前,我們也能藉助一些機制來模仿組件的生命周期(如 $scope.$watch$scope.$on('$destroy')等),但基本上都須要藉助$scope這座‘‘橋樑’’。但現在我們有了框架原生 lifecycle 的加持,這對於我們構建更地道的、框架無關的 ViewModel 來說有很大輔佐。更多關於 lifecycle 的信息能夠看官方文檔:AngularJS lifecycle hooks

  • component definition

    AngularJS 1.5.0 后增添了 component 語法用於更輕易清楚的定義一個組件,如上述例子中的組件我們能夠用component語法改寫成:

    angular
        .module('app.components', [])
        .component('component', {
            template: '<p>count: {{$ctrl.count}}</p><button ng-click="$ctrl.onUpdate({count: $ctrl.count + 1})">increase</button>'
            bindings: {
                count: '<',
                onUpdate: '&'
            },
        })

    實質上component就是directive的語法糖,bindings 是 bindToController + controllerAs + scope 的語法糖,只不過component語法更簡樸語義更清楚明了,定義組件變得更輕易,與社區盛行的作風也更一致(熟習 vue 的同硯應當已發明了😆)。更多關於 AngularJS 組件化開闢的 best practice,能夠看官方的開闢者文檔:Understanding Components

AngularJS 搭配 mobx

準備事情做了一堆,我們也該最先進入本文的正題,即怎樣給 AngularJS 搭載上 mobx 引擎(本文假定你對 mobx 中的基本觀點已有肯定水平的相識,如果不相識能夠先移步 mobx repo mobx official doc):

1. mobx-angularjs

引入 mobx-angularjs 庫連接 mobx 和 angularjs 。

npm i mobx-angularjs -S

2. 定義 ViewModel

在規範的 MVVM 架構里,ViewModel/Controller 除了構建視圖本身的狀況數據(即部份狀況)外,作為視圖跟營業模子之間溝通的橋樑,其重要職責是將營業模子適配(轉換/組裝)成對視圖更友愛的數據模子。因而,在 mobx 視角下,ViewModel 重要由以下幾部份構成:

  • 視圖(部份)狀況對應的 observable data

    class ViewModel {
        @observable
        isLoading = true;
    
        @observable
        isModelOpened = false;
    }

    可視察數據(對應的 observer 為 view),即視圖須要對其變化自動做出響應的數據。在 mobx-angularjs 庫的輔佐下,一般 observable data 的變化會使關聯的視圖自動觸發 rerender(或觸髮網絡要求之類的副作用)。ViewModel 中的 observable data 一般是視圖狀況(UI-State),如 isLoading、isOpened 等。

  • 由 運用/視圖 狀況衍生的 computed data

    Computed values are values that can be derived from the existing state or other computed values. 

    class ViewModel {
        @computed
        get userName() {
            return `${this.user.firstName} ${this.user.lastName}`;
        }
    }

    盤算數據指的是由其他 observable/computed data 轉換而來,更輕易視圖直接運用的衍生數據(derived data)。 在重營業輕交互的 web 類運用中(一般是種種企業效勞軟件), computed data 在 ViewModel 中應當占重要部份,且基本是由營業 store 中的數據(即運用狀況)轉換而來。 computed 這類數據推導關聯形貌能確保我們的運用遵照 single source of truth 準繩,不會湧現數據不一致的狀況,這也是 RP 編程中的基本準繩之一。

  • action
    ViewModel 中的 action 除了一小部份轉變視圖狀況的行動外,大部份應當是直接挪用 Model/Store 中的 action 來完成營業狀況的流轉。發起把一切對 observable data 的操縱都放到被 aciton 裝潢的要領下舉行。

mobx 合營下,一個相對完全的 ViewModel 也許長如許:

import UserStore from './UserStore';
  
class ViewModel {
      
    @inject(UserStore)
    store;
    
    @observable
    isDropdownOpened = false;

    @computed
    get userName() {
        return `${this.store.firstName} ${this.store.lastName}`;
    }
  
    @action
    toggel() {
        this.isDropdownOpened = !isDropdownOpened;
    }
      
    updateFirstName(firstName) {
        this.store.updateFirstName(firstName);
    }
}

3. 連接 AngularJS 和 mobx

<section mobx-autorun>
    <counter value="$ctrl.count"></counter>
    <button type="button" ng-click="$ctrl.increse()">increse</button>
</section>
import template from './index.tpl.html';
class ViewModel {
    @observable count = 0;
    
    @action increse() {
        this.count++;
    }
}

export default angular
    .module('app', [])
    .component('container', {
        template,
        controller: Controller,
    })
    .component('counter', {
        template: '<section><header>{{$ctrl.count}}</header></section>'
        bindings: { value: '<' }
    })
    .name;

能夠看到,除了通例的基於 mobx 的 ViewModel 定義外,我們只須要在模板的根節點加上 mobx-autorun 指令,我們的 angularjs 組件就可以很好的運作的 mobx 的響應式引擎下,從而自動的對 observable state 的變化實行 rerender。

mobx-angularjs 加快運用的魔法

從上文的示例代碼中我們能夠看到,將 mobx 跟 angularjs 連接運轉起來的是 mobx-autorun指令,我們翻下 mobx-angularjs 代碼:

const link: angular.IDirectiveLinkFn = ($scope) => {

  const { $$watchers = [] } = $scope as any
  const debouncedDigest = debounce($scope.$digest.bind($scope), 0);

  const dispose = reaction(
    () => [...$$watchers].map(watcher => watcher.get($scope)),
    () => !$scope.$root.$$phase && debouncedDigest()
  )

  $scope.$on('$destroy', dispose)
}

能夠看到 中心代碼 實在就三行:

reaction(
    () => [...$$watchers].map(watcher => watcher.get($scope)),
    () => !$scope.$root.$$phase && debouncedDigest()

思緒異常簡樸,即在指令 link 以後,遍歷一遍當前 scope 上掛載的 watchers 並取值,由於這個行動是在 mobx reaction 實行高低文中舉行的,因而 watcher 里依靠的一切 observable 都邑被網絡起來,如許當下次个中任何一個 observable 發作變動時,都邑觸發 reaction 的副作用對 scope 舉行 digest,從而到達自動更新視圖的目的。

我們曉得,angularjs 的機能被廣為詬病並非由於 ‘臟搜檢’ 本身慢,而是由於 angularjs 在每次異步事宜發作時都是無腦的從根節點最先向下 digest,從而會致使一些不必要的 loop 形成的。而當我們在搭載上 mobx 的 push-based 的 change propagation 機制時,只需當被視圖真正運用的數據發作變化時,相關聯的視圖才會觸發部份 digest (能夠理解為只需 observable data 存在 subscriber/observer 時,狀況變化才會觸發關聯依靠的重算,從而防止不必要資本斲喪,即所謂的 lazy),區分於異步事宜觸發即無腦地 $rootScope.$apply, 這類體式格局明顯更高效。

進一步壓榨機能

我們曉得 angularjs 是經由過程挾制種種異步事宜然後從根節點做 apply 的,這就致使只需我們用到了會被 angularjs 挾制的特徵就會觸發 apply,其他的諸如 $http $timeout 都好說,我們有許多替代設計,然則 ng-click 這類事宜監聽指令我們沒法防止,就像上文例子中一樣,如果我們能根絕躲藏的根節點 apply,想必運用的機能提拔能更進一步。

思緒很簡樸,我們只需把 ng-click 之流替代成不觸發 apply 的版本即可。比方把本來的 ng event 完成如許改一下:

forEach(
  'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
  function(eventName) {
    var directiveName = directiveNormalize('native-' + eventName);
    ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
      return {
        restrict: 'A',
        compile: function($element, attr) {
          var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
          return function ngEventHandler(scope, element) {
            element.on(eventName, function(event) {
              fn(scope, {$event:event})
            });
          };
        }
      };
    }];
  }
);

時候監聽的回調中只是簡樸觸發一下綁定的函數即可,不再 apply,bingo!

注意事項/ best practise

在 mobx 合營 angularjs 開闢過程當中,有一些點我們能夠會 遇到/須要斟酌:

  • 防止 TTL
    單向數據流長處許多,大部份場景下我們會優先運用 one-way binding 體式格局定義組件。一般你會寫出如許的代碼:

    class ViewModel {
        @computed
        get unCompeletedTodos() {
            return this.store.todos.filter(todo => !todo.compeleted)
        }
    }
    <section mobx-autorun>
        <todo-panel todos="$ctrl.unCompeletedTodos"></todo-panel>
    </section>

    todo-panel 組件運用單向數據綁定定義:

    angular
        .module('xxx', [])
        .component('todoPanel', {
            template: '<ul><li ng-repeat="todo in $ctrl.todos track by todo.id">{{todo.content}}</li></ul>'
            bindings: { todos: '<' }
        })

    看上去沒有任何題目,然則當你把代碼扔到瀏覽器里時就會收成一段 angularjs 捐贈的 TTL 毛病:Error: $rootScope:infdigInfinite $digest Loop。實際上這並非 mobx-angularjs 惹的禍,而是 angularjs 現在未完成 one-way binding 的 deep comparison 致使的,由於每次 get unCompeletedTodos 都邑返回一個新的數組援用,而<又是基於援用作對照,從而每次 prev === current 都是 false,末了天然報 TTL 毛病了(詳細能夠看這裏 One-way bindings + shallow watching )。

    不過幸虧 mobx 優化手腕中正好有一個要領能間接的處置懲罰這個題目。我們只須要給 computed 加一個示意要做深度值對照的 modifier 即可:

    @computed.struct
    get unCompeletedTodos() {
        return this.store.todos.filter(todo => !todo.compeleted)
    }

    實質上照樣對 unCompeletedTodos 的 memorization,只不過對照基準從默許的值對照(===)變成了構造/深度 對照,因而在第一次 get unCompeletedTodos 以後,只需盤算出來的效果跟上次的構造一致(只需當 computed data 依靠的 observable 發作變化的時刻才會觸發重算),後續的 getter 都邑直接返回前面緩存的效果,從而不會觸發分外的 diff,進而防止了 TTL 毛病的湧現。

  • $onInit$onChanges 觸發遞次的題目
    一般狀況下我們願望在 ViewModel 中藉助組件的 lifecycle 鈎子做一些事變,比方在 $onInit 中觸發副作用(網絡要求,事宜綁定等),在 $onChanges 里監聽傳入數據變化做視圖更新。

    class ViewModel {
        
        $onInit() {
            this.store.fetchUsers(this.id);  
        }
        
        $onChanges(changesObj) {
            const { id } = changesObj;
            if(id && !id.isFirstChange()) {
                this.store.fetchUsers(id.currentValue)
            }
        }
    }

    能夠發明實在我們在 $onInit$onChanges 中做了反覆的事變,而且這類寫法也與我們要做視圖框架無關的數據層的初志不符,藉助 mobx 的 observe 要領,我們能夠將上面的代碼改形成這類:

    import { ViewModel, postConstruct } from 'mmlpx';
    @ViewModel
    class ViewModel {
        
        @observable
        id = null;
        
        @postConstruct
        onInit() {
            observe(this, 'id', changedValue => this.store.fetchUsers(changedValue))
        }
    }

    熟習 angularjs 的同硯應當能發明,事實上 observe 做的事變跟 $scope.$watch 是一樣的,然則為了保證數據層的 UI 框架無關性,我們這裏用 mobx 本身的視察機制來替代了 angularjs 的 watch。

  • 遺忘你是在寫 AngularJS,把它當做一個簡樸的動態模板引擎

    不論是我們嘗試將 AngularJS 運用 ES6/TS 化照樣引入 mobx 狀況治理庫,實際上我們的初志都是將我們的 Model 以至 ViewModel 層做成視圖框架無關,在藉助 mobx 治理數據的之間的依靠關聯的同時,經由過程 connector 將 mobx observable data 與視圖連接起來,從而完成視圖依靠的狀況發作變化自動觸發視圖的更新。在這個過程當中,angularjs 不再飾演一個框架的角色影響全部體系的架構,而僅僅是作為一個動態模板引擎供應 render 才能罷了,後續我們完全能夠經由過程配套的 connector,將 mobx 治理的數據層連接到差別的 view library 上。現在 mobx 官方針對 React/Angular/AngularJS 均有響應的 connector,社區也有針對 vue 的處置懲罰設計,並不須要我們從零最先。

    在藉助 mobx 構建數據層以後,我們就可以真正做到規範 MVVM 中形貌的那樣,在 Model 以至 VIewModel 不改一行代碼的前提下輕鬆適配其他視圖。view library 的語法、機制差別不再成為視圖層 晉級/替代 的鴻溝,我們能經由過程改很少許的代碼來填平它,畢竟只是替代一個動態模板引擎罷了😆。

Why MobX

React and MobX together are a powerful combination. React renders the application state by providing mechanisms to translate it into a tree of renderable components. MobX provides the mechanism to store and update the application state that React then uses.

Both React and MobX provide optimal and unique solutions to common problems in application development. React provides mechanisms to optimally render UI by using a virtual DOM that reduces the number of costly DOM mutations. MobX provides mechanisms to optimally synchronize application state with your React components by using a reactive virtual dependency state graph that is only updated when strictly needed and is never stale.

MobX 官方的引見,把上面一段引見中的 React 換成恣意其他( Vue/Angular/AngularJS ) 視圖框架/庫(VDOM 部份恰當調解一下) 也都實用。得益於 MobX 的觀點簡樸及獨立性,它異常合適作為視圖中立的狀況治理設計。簡言之是視圖層只做拿數據襯着的事情,狀況流轉由 MobX 幫你治理。

Why Not Redux

Redux 很好,而且社區也有許多跟除 React 以外的視圖層集成的實踐。純真的比較 Redux 跟 MobX 也許須要再寫一篇文章來論述,這裏只簡樸說幾點與視圖層集成時的差別:

  1. 雖然 Redux 實質也是一個視察者模子,然則在 Redux 的完成下,狀況的變化並非經由過程數據 diff 得出而是 dispatch(action) 來手動關照的,而真正的 diff 則交給了視圖層,這不僅致使能夠的襯着糟蹋(並非一切 library 都有 vdom),在處置懲罰種種須要在變化時觸發副作用的場景也會顯得過於煩瑣。
  2. 由於第一條 Redux 不做數據 diff,因而我們沒法在視圖層接辦數據前得知哪一個部份被更新,進而沒法更高效的挑選性更新視圖。
  3. Redux 在 store 的設想上是 opinionated 的,它推行 單一 store 準繩。運用能夠完全由狀況數據來形貌、且狀況可治理可回溯 這一點上我沒有看法,但並非只需單一 store 這一條前途,多 store 依舊能殺青這一目的。明顯 mobx 在這一點上是 unopinionated 且靈活性更強。
  4. Redux 觀點太多而本身做的又太少。能夠對照一下 ngReduxmobx-angularjs 看看完成龐雜度上的差別。

末了

除了給 AngularJS 搭載上更高效、準確的高速引擎以外,我們最重要的目的照樣為了將 營業模子層以至 視圖模子層(統稱為運用數據層) 做成 UI 框架無關,如許在面臨差別的視圖層框架的遷徙時,才能夠做到游刃有餘。而 mobx 在這個事變上是一個很好的挑選。

末了想說的是,如果前提許可的話,照樣發起將 angularjs 體系晉級成 React/Vue/Angular 之一,畢竟大部份時刻基於新的視圖技術開闢運用是能帶來確切的收益的,如 機能提拔、開闢效力提拔 等。即使你短期內沒法替代掉 angularjs(多種要素,比方已基於 angularjs 開闢/運用 了一套完全的組件庫,代碼體量太大改形本錢太高),你依舊能夠在部份運用 mobx/mobx-angularjs 革新運用或開闢新功能,在 mobx-angularjs 輔佐你提拔運用機能的同時,也給你後續的晉級設計製造了能夠性。

PS: mobx-angularjs 現在由我和另一個 US 小哥儘力保護,如果有任何運用上的題目,迎接隨時聯絡😀。

    原文作者:kuitos
    原文地址: https://segmentfault.com/a/1190000014783224
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞