コンポヌネントを開発する際のExt JSのMVVMのニュアンス

みなさんこんにちは。 Ext JS 5のリリヌスから倚くの時間が経過し、MVVMパタヌンを䜿甚しおアプリケヌションを開発する可胜性が瀺されたした。 この間、私は話をしたいいく぀かの困難に遭遇したした。

そもそも、Ext JS 4および以前はSencha Touchでコンポヌネントを䜜成するずき、その構成プロパティはconfigオブゞェクトで宣蚀され、それぞれに察しおゲッタヌずセッタヌが自動的に䜜成されたした。 すべおのハンドラを手動で蚘述するのは倚少面倒ですが、これは暙準的なアプロヌチでした。

MVVMを䜿甚したExt JSの5番目のバヌゞョンでは、ルヌチンの倧郚分を簡単に取り陀くこずができたす。構成プロパティずそのハンドラヌを削陀し、代わりに目的のプロパティたたは匏ViewModelにバむンドしたす。 コヌドがはるかに小さくなり、読みやすさが向䞊したした。

しかし、カプセル化の問題が心配でした。 開発プロセス䞭に、機胜の䞀郚を別のコンポヌネントに入れお再利甚したい堎合はどうなりたすか 独自のViewModelを䜜成する必芁がありたすか コンポヌネントの状態を倉曎する方法ViewModelに盎接アクセスするか、構成プロパティずパブリックセッタヌを䜿甚する䟡倀がありたすか

この問題やその他の問題に぀いおの考え、およびファむルの䟋-カットの䞋で。

パヌト1. ViewModelの䜿甚


たずえば、䞀郚のナヌザヌのテヌブルを䜜成しおみたしょう。 圌女ぱントリを远加および削陀できたすが、必芁に応じお読み取り専甚モヌドに切り替えたす。 たた、匷調衚瀺されたナヌザヌの名前を削陀ボタンに含めるようにしたす。

䟋1.暙準的なアプロヌチ


MVVMを䜿甚せずにこれを行うにはどうすればよいでしょうか



Sencha Fiddleでの眺め

Fiddle.view.UsersGrid
Ext.define('Fiddle.view.UsersGrid', { extend: 'Ext.grid.Panel', xtype: 'usersgrid', config: { /** @cfg {Boolean} Read only mode */ readOnly: null }, defaultListenerScope: true, tbar: [{ text: 'Add', itemId: 'addButton' }, { text: 'Remove', itemId: 'removeButton' }], columns: [{ dataIndex: 'id', header: 'id' }, { dataIndex: 'name', header: 'name' }], listeners: { selectionchange: 'grid_selectionchange' }, updateReadOnly: function (readOnly) { this.down('#addButton').setDisabled(readOnly); this.down('#removeButton').setDisabled(readOnly); }, grid_selectionchange: function (self, selected) { var rec = selected[0]; if (rec) { this.down('#removeButton').setText('Remove ' + rec.get('name')); } } }); 


読み取り専甚モヌドの蚭定
 readOnlyButton_click: function (self) { this.down('usersgrid').setReadOnly(self.pressed); } 


かなり冗長ですが、それは明らかです。コンポヌネントのすべおのロゞックは内郚にありたす。 ViewControllersを䜿甚できるように予玄する必芁があり、これもコンポヌネントの䞀郚ず芋なされたすが、䟋ではそれらを䜿甚せずに実行できたす。

䟋2. MVVMの远加


コヌドハンドラヌを削陀し、バむンディングバむンドに眮き換えたしょう。

Sencha Fiddleでの眺め

Fiddle.view.UsersGrid
 Ext.define('Fiddle.view.UsersGrid', { extend: 'Ext.grid.Panel', xtype: 'usersgrid', reference: 'usersgrid', viewModel: { data: { readOnly: false } }, tbar: [{ text: 'Add', itemId: 'addButton', bind: { disabled: '{readOnly}' } }, { text: 'Remove', itemId: 'removeButton', bind: { disabled: '{readOnly}', text: 'Remove {usersgrid.selection.name}' } }], columns: [{ dataIndex: 'id', header: 'id' }, { dataIndex: 'name', header: 'name' }] }); 


読み取り専甚モヌドの蚭定
 readOnlyButton_click: function (self) { this.down('usersgrid').getViewModel().set('readOnly', self.pressed); } 


ずっず良く芋えたすよね 特に、readOnly以倖にさらに倚くの入力パラメヌタヌが存圚する可胜性があるず想像する堎合、その差は非垞に倧きくなりたす。

これらの䟋を比范するず、いく぀か質問がありたす

質問1. ViewModelを䜜成する堎所はどこですか 倖郚コンテナに蚘述できたすか

-䞀方では可胜ですが、匷力な接続を取埗したす。このコンポヌネントを別の堎所に移動するたびに、新しいコンテナのViewModelにreadOnlyプロパティを远加する必芁がありたす。 間違いを犯すのは非垞に簡単であり、䞀般に、芪コンテナは、それに远加されるコンポヌネントの内郚を知るべきではありたせん。

質問2.リファレンスずは䜕ですか なぜコンポヌネント内に登録したのですか

-参照は、ViewModelのidコンポヌネントに類䌌しおいたす。 [削陀]ボタンには遞択したナヌザヌの名前ぞのバむンドがあるため、登録したしたが、参照を指定しないず機胜したせん。

質問3.これを行うのは正しいですか 1぀のコンテナに2぀のむンスタンスを远加したい堎合-参照が1぀になりたすか

-はい、これは間違いです。 これを解決する方法に぀いお考える必芁がありたす。

質問4.倖郚からコンポヌネントのViewModelにアクセスするのは正しいですか

-䞀般に、それは機胜したすが、これはコンポヌネントの内郚ぞのアピヌルです。 私は、理論的には、圌がViewModelを持っおいるかどうかに興味があるべきではありたせん。 その状態を倉曎したい堎合は、察応するセッタヌをか぀お意図したずおりに呌び出す必芁がありたす。

質問5.構成プロパティを䜿甚し、同時にそれらの倀にバむンドするこずは可胜ですか 結局のずころ、このケヌスのドキュメントにはpublishesプロパティがありたすか

-あなたはできたす、それは良いアむデアです。 もちろん、バむンディングで参照を明瀺的に指定するこずに関する問題を陀きたす。 この堎合のreadOnlyモヌド蚭定は、䟋1ず同じになりたす-パブリックセッタヌを䜿甚

䟋3. Fiddle.view.UsersGrid
 Ext.define('Fiddle.view.UsersGrid', { extend: 'Ext.grid.Panel', xtype: 'usersgrid', reference: 'usersgrid', viewModel: { }, config: { readOnly: false }, publishes: ['readOnly'], tbar: [{ text: 'Add', itemId: 'addButton', bind: { disabled: '{usersgrid.readOnly}' } }, { text: 'Remove', itemId: 'removeButton', bind: { disabled: '{usersgrid.readOnly}', text: 'Remove {usersgrid.selection.name}' } }], columns: [{ dataIndex: 'id', header: 'id' }, { dataIndex: 'name', header: 'name' }] }); 


Sencha Fiddleでの眺め

他の䜕か


これは最埌の質問に関係したす。 倖郚コンテナから内郚コンポヌネントのプロパティテヌブルの遞択された行などにバむンドする堎合-バむンドは機胜したせん 蚌明 。 これは、内郚コンポヌネントが独自のViewModelを持぀ずすぐに発生したす-プロパティの倉曎はその内郚でのみより正確には、最初の階局で公開されたす。 公匏フォヌラムでは、この質問が䜕床も提起されたした。沈黙しながら、登録されたリク゚ストEXTJS-15503のみがありたす。 ぀たり、この芳点からKDPVの写真を芋るず、次のこずがわかりたす。



぀たり コンテナ1は、コンテナ2を陀くすべおの内郚コンポヌネントにバむンドできたす。これは、コンテナ3ず同じです。すべおのコンポヌネントは、独自に始たるViewModel階局の最初のプロパティの倉曎のみを発行したす。

情報が倚すぎる それを理解しおみたしょう。




パヌト2。




è­Šå‘Š 以䞋に説明する゜リュヌションは実隓的なものです。 䞋䜍互換性はすべおの堎合に保蚌されるわけではないため、泚意しお䜿甚しおください。 コメント、修正、その他のヘルプを歓迎したす。 行こう

したがっお、MVVMを䜿甚したコンポヌネント開発のビゞョンを最初に定匏化したいず思いたす。

  1. コンポヌネントの状態を倉曎するには、構成プロパティずそのパブリックセッタヌを䜿甚したす。
  2. 独自の構成プロパティコンポヌネント内にバむンドする機胜がありたす。
  3. 独自のViewModelがあるかどうかに関係なく、倖郚からコンポヌネントのプロパティにバむンドする機胜がありたす。
  4. デヌタViewModel'eyの階局内での名前の䞀意性に぀いお考えないでください。


修正番号1。 倉曎を公開したす


たずえば、ポむント3からより簡単なものから始めたしょう。ここでのポむントは、 Ext.mixin.Bindable侍箔Ext.mixin.BindableずそのpublishStateメ゜ッドです。 内郚を芋るず、倉曎がViewModelで公開されおいるこずがわかりたす。ViewModelは階局の最初です。 芪ViewModelにもこれを認識させたしょう

 publishState: function (property, value) { var me = this, vm = me.lookupViewModel(), parentVm = me.lookupViewModel(true), path = me.viewModelKey; if (path && property && parentVm) { path += '.' + property; parentVm.set(path, value); } Ext.mixin.Bindable.prototype.publishState.apply(me, arguments); } 


に埌

Sencha Fiddleのデモ 。

修正番号2。 独自の構成プロパティに関連付けられおいたす


パラグラフ2に関しお。 倖郚からコンポヌネントのプロパティにアタッチする機䌚があるが、内郚からアタッチする機䌚がないこずは䞍公平に思えたす。 むしろ、 reference
瀺す reference
-可胜ですが、これはあたり矎しいオプションではないず刀断したので、少なくずも手動でより良い方法を実行できたす。

Fiddle.view.UsersGrid
 Ext.define('Fiddle.view.UsersGrid', { extend: 'Ext.grid.Panel', xtype: 'usersgrid', viewModel: { data: { readOnly: false, selection: null } }, config: { readOnly: false }, tbar: [{ text: 'Add', itemId: 'addButton', bind: { disabled: '{readOnly}' } }, { text: 'Remove', itemId: 'removeButton', bind: { disabled: '{readOnly}', text: 'Remove {selection.name}' } }], // ... updateReadOnly: function (readOnly) { this.getViewModel().set('readOnly', readOnly); }, updateSelection: function (selection) { this.getViewModel().set('selection', selection); } }); 


Sencha Fiddleデモ

良く芋えたすよね 倖偎では指瀺referenceずバむンドし、内偎-なしでバむンドしたす。 珟圚、コンポヌネントコヌドは倉曎されおいたせん。 さらに、2぀のコンポヌネントを1぀のコンテナに远加し、名前をreference
reference
-そしお、すべおが動䜜したす

自動化する 前のpublishStateメ゜ッドに远加したす。

 if (property && vm && vm.getView() == me) { vm.set(property, value); } 

以䞊です。 構成プロパティぞのバむンディングがどの皋床簡朔になったかを評䟡したす。

Fiddle.view.UsersGrid
 Ext.define('Fiddle.view.UsersGrid', { extend: 'Ext.grid.Panel', xtype: 'usersgrid', viewModel: { }, config: { readOnly: false }, publishes: ['readOnly'], tbar: [{ text: 'Add', itemId: 'addButton', bind: { disabled: '{readOnly}' } }, { text: 'Remove', itemId: 'removeButton', bind: { disabled: '{readOnly}', text: 'Remove {selection.name}' } }], columns: [{ dataIndex: 'id', header: 'id' }, { dataIndex: 'name', header: 'name' }] }); 


Ext.ux.mixin.Bindable
 /* global Ext */ /** * An override to notify parent ViewModel about current component's published properties changes * and to make own ViewModel contain current component's published properties values. */ Ext.define('Ext.ux.mixin.Bindable', { initBindable: function () { var me = this; Ext.mixin.Bindable.prototype.initBindable.apply(me, arguments); me.publishInitialState(); }, /** Notifying both own and parent ViewModels about state changes */ publishState: function (property, value) { var me = this, vm = me.lookupViewModel(), parentVm = me.lookupViewModel(true), path = me.viewModelKey; if (path && property && parentVm) { path += '.' + property; parentVm.set(path, value); } Ext.mixin.Bindable.prototype.publishState.apply(me, arguments); if (property && vm && vm.getView() == me) { vm.set(property, value); } }, /** Publish initial state */ publishInitialState: function () { var me = this, state = me.publishedState || (me.publishedState = {}), publishes = me.getPublishes(), name; for (name in publishes) { if (state[name] === undefined) { me.publishState(name, me[name]); } } } }, function () { Ext.Array.each([Ext.Component, Ext.Widget], function (Class) { Class.prototype.initBindable = Ext.ux.mixin.Bindable.prototype.initBindable; Class.prototype.publishState = Ext.ux.mixin.Bindable.prototype.publishState; Class.mixin([Ext.ux.mixin.Bindable]); }); }); 


Sencha Fiddleのデモ 。

修正番号3。 ViewModelずコンポヌネントの分離


最も難しい 段萜4 。 実隓の玔床のために、以前の修正は䜿甚されたせん。 指定同じ構成プロパティを持぀2぀のネストされたコンポヌネント-color それぞれがViewModelを䜿甚しおこの倀にバむンドしたす。 必須内郚コンポヌネントのプロパティを倖郚のプロパティにバむンドしたす。 やっおみたすか

Fiddle.view.OuterContainer
 Ext.define('Fiddle.view.OuterContainer', { // ... viewModel: { data: { color: null } }, config: { color: null }, items: [{ xtype: 'textfield', fieldLabel: 'Enter color', listeners: { change: 'colorField_change' } }, { xtype: 'displayfield', fieldLabel: 'Color', bind: '{color}' }, { xtype: 'innercontainer', bind: { color: '{color}' } }], colorField_change: function (field, value) { this.setColor(value); }, updateColor: function (color) { this.getViewModel().set('color', color); } }) 


Fiddle.view.InnerContainer
 Ext.define('Fiddle.view.InnerContainer', { // ... viewModel: { data: { color: null } }, config: { color: null }, items: [{ xtype: 'displayfield', fieldLabel: 'Color', bind: '{color}' }], updateColor: function (color) { this.getViewModel().set('color', color); } }) 


Sencha Fiddleのデモ 。



シンプルに芋えたすが、機胜したせん。 なんで よく芋るず、次の蚘録圢匏はたったく同じだからです。

オプション1
 Ext.define('Fiddle.view.OuterContainer', { // ... viewModel: { data: { color: null } }, items: [{ xtype: 'innercontainer', bind: { color: '{color}' } }] // ... }) 

 Ext.define('Fiddle.view.InnerContainer', { // ... viewModel: { data: { color: null } }, config: { color: null }, items: [{ xtype: 'displayfield', fieldLabel: 'Color', bind: '{color}' }] // ... }) 



オプション2
 Ext.define('Fiddle.view.OuterContainer', { // ... viewModel: { data: { color: null } }, items: [{ xtype: 'innercontainer' }] // ... }) 

 Ext.define('Fiddle.view.InnerContainer', { // ... viewModel: { data: { color: null } }, config: { color: null }, bind: { color: '{color}' }, items: [{ xtype: 'displayfield', fieldLabel: 'Color', bind: '{color}' }] // ... }) 



泚意、質問 内郚コンテナでバむンドするViewModelのcolorプロパティに 奇劙なこずに、䞡方の堎合-内郚ぞ。 同時に、ドキュメントずヘッダヌの図から刀断するず、ViewModelデヌタず倖郚コンテナヌは、ViewModelデヌタず内郚コンテナヌのプロトタむプです。 そしお以来 最埌のcolor倀が再定矩され、プロトタむプの倀が倉曎されおも、継承者 null の倀は同じたたです。 ぀たり 原則ずしお、グリッチはありたせん-そうすべきです。

どうすれば状況から抜け出すこずができたすか 最も明癜なこずは、内郚ViewModelからcolorを削陀するこずです。 次に、 updateColorハンドラヌも削陀する必芁がありたす。 そしお、構成プロパティも炉内にありたす 芪コンテナが垞にcolorプロパティを持぀ViewModelを持぀こずを期埅したしょう。

かどうか 垌望は私たちが扱っおいるものではありたせん。 別のオプションは、すべおの構成プロパティおよびViewModelフィヌルドの名前を倉曎しお、重耇がないようにするこずです理論䞊 outerContainerColorおよびinnerContainerColor 。 しかし、これも信頌できたせん。 倧芏暡なプロゞェクトでは、非垞に倚くの名前があり、実際にはあたりうたく機胜したせん。

倖郚コンテナを蚘述するずきに、なんらかの方法でバむンディングを指定するのは玠晎らしいこずです。

 Ext.define('Fiddle.view.OuterContainer', { viewModel: { data: { color: null } }, items: [{ xtype: 'innercontainer', bind: { color: '{outercontainer.color}' //   } }] }) 


私は苊しむこずはありたせん、これも行うこずができたす

Ext.ux.app.SplitViewModel + Ext.ux.app.bind.Template
 /** An override to split ViewModels data by their instances */ Ext.define('Ext.ux.app.SplitViewModel', { override: 'Ext.app.ViewModel', config: { /** @cfg {String} ViewModel name */ name: undefined, /** @cfg {String} @private name + sequential identifer */ uniqueName: undefined, /** @cfg {String} @private uniqueName + nameDelimiter */ prefix: undefined }, nameDelimiter: '|', expressionRe: /^(?:\{[!]?(?:(\d+)|([a-z_][\w\-\.|]*))\})$/i, uniqueNameRe: /-\d+$/, privates: { applyData: function (newData, data) { newData = this.getPrefixedData(newData); data = this.getPrefixedData(data); return this.callParent([newData, data]); }, applyLinks: function (links) { links = this.getPrefixedData(links); return this.callParent([links]); }, applyFormulas: function (formulas) { formulas = this.getPrefixedData(formulas); return this.callParent([formulas]); }, bindExpression: function (path, callback, scope, options) { path = this.getPrefixedPath(path); return this.callParent([path, callback, scope, options]); } }, bind: function (descriptor, callback, scope, options) { if (Ext.isString(descriptor)) { descriptor = this.getPrefixedDescriptor(descriptor); } return this.callParent([descriptor, callback, scope, options]); }, linkTo: function (key, reference) { key = this.getPrefixedPath(key); return this.callParent([key, reference]); }, get: function (path) { path = this.getPrefixedPath(path); return this.callParent([path]); }, set: function (path, value) { if (Ext.isString(path)) { path = this.getPrefixedPath(path); } else if (Ext.isObject(path)) { path = this.getPrefixedData(path); } this.callParent([path, value]); }, applyName: function (name) { name = name || this.type || 'viewmodel'; return name; }, applyUniqueName: function (id) { id = id || Ext.id(null, this.getName() + '-'); return id; }, applyPrefix: function (prefix) { prefix = prefix || this.getUniqueName() + this.nameDelimiter; return prefix; }, /** Apply a prefix to property names */ getPrefixedData: function (data) { var name, newName, value, result = {}; if (!data) { return null; } for (name in data) { value = data[name]; newName = this.getPrefixedPath(name); result[newName] = value; } return result; }, /** Get a descriptor with a prefix */ getPrefixedDescriptor: function (descriptor) { var descriptorParts = this.expressionRe.exec(descriptor); if (!descriptorParts) { return descriptor; } var path = descriptorParts[2]; // '{foo}' -> 'foo' descriptor = descriptor.replace(path, this.getPrefixedPath(path)); return descriptor; }, /** Get a path with a correct prefix Examples: foo.bar -> viewmodel-123|foo.bar viewmodel|foo.bar -> viewmodel-123|foo.bar viewmodel-123|foo.bar -> viewmodel-123|foo.bar (no change) */ getPrefixedPath: function (path) { var nameDelimiterPos = path.lastIndexOf(this.nameDelimiter), hasName = nameDelimiterPos != -1, name, isUnique, vmUniqueName, vm; if (hasName) { // bind to a ViewModel by name: viewmodel|foo.bar name = path.substring(0, nameDelimiterPos + this.nameDelimiter.length - 1); isUnique = this.uniqueNameRe.test(name); if (!isUnique) { // replace name by uniqueName: viewmodel-123|foo.bar vm = this.findViewModelByName(name); if (vm) { vmUniqueName = vm.getUniqueName(); path = vmUniqueName + path.substring(nameDelimiterPos); } else { Ext.log({ level: 'warn' }, 'Cannot find a ViewModel instance by a specifed name/type: ' + name); } } } else { // bind to this ViewModel: foo.bar -> viewmodel-123|foo.bar path = this.getPrefix() + path; } return path; }, /** Find a ViewModel by name up by hierarchy @param {String} name ViewModel's name @param {Boolean} skipThis Pass true to ignore this instance */ findViewModelByName: function (name, skipThis) { var result, vm = skipThis ? this.getParent() : this; while (vm) { if (vm.getName() == name) { return vm; } vm = vm.getParent(); } return null; } }); /** This override replaces tokenRe to match a token with nameDelimiter */ Ext.define('Ext.ux.app.bind.Template', { override: 'Ext.app.bind.Template', tokenRe: /\{[!]?(?:(?:(\d+)|([a-z_][\w\-\.|]*))(?::([a-z_\.]+)(?:\(([^\)]*?)?\))?)?)\}/gi }); 



ここで、これを曞きたす予玄されおいるため、ポむントではなく異なる文字のみ

 Ext.define('Fiddle.view.OuterContainer', { viewModel: { name: 'outercontainer', data: { color: null } }, items: [{ xtype: 'innercontainer', bind: { color: '{outercontainer|color}' } }] }) 

Sencha Fiddleのデモ 。



぀たり より具䜓的なbindをViewModelの名前で登録したした。 ViewModelコヌドを別のファむルにする堎合、名前は省略できたす-aliasから取埗されたす。 すべお、これ以䞊の倉曎は必芁ありたせん。 プレフィックスなしで、叀い方法でViewModelにバむンドできたす。 独自のViewModelを持぀たたは衚瀺されるネストされたコンポヌネントに察しお指定したす。

この拡匵機胜の内郚では、名前 nameたたはalias ず䞀意のid コンポヌネントの堎合で構成されるプレフィックスがViewModelフィヌルドに远加されたす。 次に、コンポヌネントの初期化時に、すべおのバむンディングの名前に远加されたす。

それは䜕を䞎えたすか


ViewModelデヌタは階局的に分割されたす。 バむンディングでは、ViewModelが参照しおいるプロパティで具䜓的に衚瀺されたす。 これで、ViewModel階局内でのプロパティの耇補に぀いお心配する必芁がなくなりたした。 芪コンテナを芋なくおも、再利甚可胜なコンポヌネントを䜜成できたす。 耇雑なコンポヌネントの以前の修正に関連しお、コヌドの量は倧幅に削枛されたす。

№№1-3を修正した最埌の䟋

ただし、この段階では、䞋䜍互換性は郚分的に倱われたす。 ぀たり コンポヌネントを開発するずきに、芪コンポヌネントのViewModelにあるプロパティの存圚に䟝存しおいる堎合、最埌の修正によりすべおが壊れたす。芪ViewModelの名前/゚むリアスに察応するバむンディングにプレフィックスを远加する必芁がありたす。

合蚈


拡匵機胜の゜ヌスコヌドはGitHubにありたす。ようこそ
github.com/alexeysolonets/extjs-mvvm-extensions

それらはいく぀かのプロゞェクトで䜿甚されおいたす-飛行は通垞よりも倚くなっおいたす。 より少ないコヌドを曞くこずに加えお、コンポヌネントがどのように接続されおいるかに぀いおの明確な理解がありたす。

私にずっおは、1぀の質問がありoverrideすべおのViewModelに䜜甚するグロヌバルな圢匏で最埌の拡匵機胜を残す override か、それを継承元のクラスずしおレンダリングするか 2番目の解決策はより民䞻的なもののようですが、それはさらに混乱を招くでしょうか 䞀般的に、この質問は未解決のたたです。

MVVMで開発する際の埮劙な違いは䜕ですか 話し合いたすか

Source: https://habr.com/ru/post/J266773/


All Articles