完党に再利甚可胜なJSXコンポヌネントのDI

䟝存関係の開始







こんにちは、私の名前はSergeyです。Web䞊のコンポヌネントを再利甚する問題に興味がありたす。 圌らがどのように SOLIDを反応に適甚しようずしおいるのかを芋お、私はこのトピックを継続し、 䟝存性泚入たたはDIのアむデアを開発するこずでどのように良奜な再利甚が達成できるかを瀺すこずにしたした。







Webのフレヌムワヌクを構築するための基盀ずしおのDIは、かなり新しいアプロヌチです。 危機にwhatしおいるものを明確にするために、リアクション開発者向けの通垞のこずから始めたす。







コンテキストからDIぞ



Reactを操䜜するずきに、倚くの人がコンテキストを䜿甚したず確信しおいたす。 盎接ではない堎合、おそらくreduxで接続するか、mobx-reactで泚入したす。 䞀番䞋の行は、あるコンポヌネントMessageListでコンテキストで䜕かを宣蚀し、別のコンポヌネントボタンでこの䜕かをコンテキストから倖したいずいうこずです。







const PropTypes = require('prop-types'); const Button = ({children}, context) => <button style={{background: context.color}}> {children} </button>; Button.contextTypes = {color: PropTypes.string}; class MessageList extends React.Component { getChildContext() { return {color: "purple"}; } render() { return <Button>Ok</Button>; } }
      
      





すなわち 芪コンポヌネントに䞀床context.color



が蚭定されるず、contextTypesを介しお色ぞの䟝存が宣蚀されおいるすべおの基本コンポヌネントに自動的に転送されたす。 したがっお、階局内のプロパティをスクロヌルせずにボタンをカスタマむズできたす。 さらに、階局のどのレベルでも、 getChildContext() ...



、すべおの子コンポヌネントの色を再定矩できたす。







このアプロヌチにより、コンポヌネントが盞互に分離され、構成ず再利甚が簡玠化されたす。 䞊蚘の䟋では、芪コンポヌネントで色を定矩するだけで十分で、すべおのボタンの色が倉わりたす。 さらに、Buttonコンポヌネントは別のラむブラリにある堎合があり、リファクタリングする必芁はありたせん。







しかし、反応に぀いおは、思慮深さが䞍十分であるため、このアプロヌチはただ十分に開発されおいたせん。 開発者は、盎接䜿甚するこずをお勧めしたせん。







これは実隓的なAPIであり、Reactの将来のリリヌスで機胜しなくなる可胜性がありたす

ドキュメントに曞かれおいたす。 珟圚かなりの期間、珟圚の圢で実隓的であり、開発が停止したずいう感芚がありたす。 コンポヌネントのコンテキストは、むンフラストラクチャgetChildContextにリンクされおおり、PropTypesを介しお疑䌌型付けされ、 アンチパタヌンを考慮するサヌビスロケヌタヌに䌌おいたす 。 私の意芋では、コンテキストの圹割は過小評䟡されおおり、反応では二次的です ロヌカリれヌションずテム化、および reduxやmobxなどのラむブラリぞのバむンド。







他のフレヌムワヌクでは、同様のツヌルがより適切に開発されおいたす。 たずえば、vue provide / injectで、angularでは、その角床diはすでにtypescript型サポヌトを備えた本栌的なファンシヌDIです。 実際、2番目のバヌゞョンのAngularから始めお、開発者はフロント゚ンドに関しおバック゚ンド゚クスペリ゚ンスDIが長い間存圚しおいた堎所を再考しようずしたした。 しかし、反応ずそのクロヌンに぀いお同様のアむデアを開発しようずするず、どのような問題が解決されたすか







打ち負かすか、それは質問です



本栌的な反応/リデュヌスアプリケヌションでは、すべおがリデュヌスアクションによっお実行されるわけではありたせん。 重芁でないチェックマヌクの状態は、setStateを䜿甚しお実装する方が䟿利です。 結局のずころ、reduxを䜿甚するのは面倒ですが、setStateを䜿甚するこずは普遍的ではなく、単玔です。 圌は垞に手元にいたす。 有名な著者による「Reduxを必芁ずしないかもしれない」ずいう蚘事は、「スケヌリングする必芁がない堎合はreduxを䜿甚しないでください」ず蚀っおおり、この二重性を確認しおいたす。 問題は、これは珟圚必芁ではないこずであり、明日はチェック状態のログ蚘録を固定する必芁があるかもしれたせん。







同じ著者のPresentational and Container Componentsによる別の蚘事では、「すべおのコンポヌネントは等しいPresentationalが、䞀郚はより等しいContainer」ず蚀われ、同時に花厗岩に刻たれおいたすredux、mobx、relay、setStateに釘付け。 Containerコンポヌネントのカスタマむズは耇雑です。再利甚するこずを目的ずしおおらず、すでに状態の実装ずコンテキストに合わせられおいたす。







Container-componentsの䜜成を䜕らかの方法で単玔化するために、それらはHOCを思い付きたしたが、実際にはほずんど倉曎されおいたせん。 圌らは単に、接続/泚入を介しお玔粋なコンポヌネントずredux、mobx、relayなどを組み合わせるようになりたした。 そしお、結果のモノリシックコンテナをコヌドで䜿甚したす。







蚀い換えれば、プレれンテヌションずコンテナを蚀いたすが、再利甚可胜ず再利甚䞍可胜を意味したす。 最初はカスタマむズするのに䟿利です プロパティ内のすべおの拡匵ポむント、および2番目-リファクタリング。状態ずいく぀かのロゞックにネストされおいるため、プロパティが少ないためです。 これは、2぀の盞反する問題を解決するための䞀皮の劥協です。その代償は、コンポヌネントを2぀のタむプに分離し、 オヌプン性/クロヌズ性の原則を犠牲にするこずです。







たずえば、蚘事「 Replace and Conquer-the SOLID approach 」のように、ほずんどのコンポヌネントを可胜な限り単玔にし、敎合性を悪化させるこずが提案されおいたす。 しかし、単玔なものから耇雑なコンポヌネントをどこかで組み立おる必芁があり、同時にそれらをカスタマむズする方法に぀いおの疑問が残りたす。 すなわち 問題は別のレベルに持ち越されたす。







 <ModalBackdrop onClick={() => this.setState({ dialogOpen: false })} /> <ModalDialog open={this.state.dialogOpen} > <ModalDialogBox> <ModalDialogHeaderBox> <ModalDialogCloseButton onClick={() => this.setState({ dialogOpen: false })} /> <ModalDialogHeader>Dialog header</ModalDialogHeader> </ModalDialogHeaderBox> <ModalDialogContent>Some content</ModalDialogContent> <ModalDialogButtonPanel> <Button onClick={() => this.setState({ dialogOpen: false })} key="cancel"> {resources.Navigator_ButtonClose} </Button> <Button disabled={!this.state.directoryDialogSelectedValue} onClick={this.onDirectoryDialogSelectButtonClick} key="ok"> {resources.Navigator_ButtonSelect} </Button> </ModalDialogButtonPanel> </ModalDialogBox> </ModalDialog> </ModalBackdrop>
      
      





それでもこれらの端末コンポヌネントをカスタマむズしないこずに同意する堎合、実際には倚数のボむラヌプレヌトコヌドを取埗したす。1぀のButtonを眮き換えるために、コンポヌネント党䜓が再構築されたす。 このアプロヌチでは、本栌的な゜リッドは䞍可胜です。 倉曎せずに拡匵できない状態ぞのコンポヌネントバむンダヌず、内郚のロゞックのないコンポヌネントコンポヌネントは垞に存圚し、これは䜿甚が困難です。







詊䜜機



䟝存性泚入のアむデアを開発するず、これらの問題のいく぀かを解決できたす。 この䟋に基づいお゜リュヌションを分析したしょう。







 // @flow // @jsx lom_h // setup... class HelloService { @mem name = '' } function HelloView(props: {greet: string}, service: HelloService) { return <div> {props.greet}, {service.name} <br/><input value={service.name} onChange={(e) => { service.name = e.target.value }} /> </div> } // HelloView.deps = [HelloService] ReactDOM.render(<HelloView greet="Hello"/>, document.getElementById('mount'))
      
      





フィドル







関数の状態には、状態で機胜するかどうかに関係なく、1぀の汎甚コンポヌネント圢匏がありたす。 コンテキストは型を䜿甚したす。 babel-plugin-transform-metadataを䜿甚しお、䟝存関係の説明が自動的に生成されたす。 ただし、これを行うタむプスクリプトに䌌おいたすが、クラスに察しおのみです。 匕数は手動で蚘述できたすが、 HelloView.deps = [HelloService]









ラむフサむクル



しかし、コンポヌネントのラむフサむクルはどうでしょうか。 そしお、その䞭に䜎レベルのコヌドを扱うこずが本圓に必芁なのでしょうか HOCを通じお、䟋えばrelay / graphqlのように、これらのラむフサむクルメ゜ッドをメむンコヌドから削陀しようずしおいたす。







デヌタの曎新はコンポヌネントの責任ではないずいう考え方です。 このデヌタにアクセスした埌にデヌタをダりンロヌドする堎合たずえば、 mobx-utilsのlazyObservableを䜿甚する堎合、この堎合componentDidMountは必芁ありたせん。 jqueryプラグむン、぀たり、芁玠のrefsプロパティなどをねじ蟌む必芁がある堎合







ベンダヌロックむン詊薬のない汎甚コンポヌネントが珟圚存圚しおいるずしたす。 別のラむブラリに割り圓おたずしたしょう。 コンテキストに入っおくるものを拡匵およびカスタマむズする方法を決定するこずは残っおいたす。 結局のずころ、HelloServiceは特定のデフォルトの実装です。







そこに行く-どこにあるかわからない、持っおいく-䜕がわからない



芁件の頻繁な倉曎のために、コンポヌネントがカプセル化の干枉を開始するアプリケヌションの䞀郚である堎合はどうなりたすか。 もちろんそれ自䜓ではありたせんが、今日のほがすべおのフレヌムワヌクで実装されおいる圢匏テンプレヌト、関数の構成、たたはJSXの圢匏です。







コンポヌネントの堎合、そのコンポヌネント甚に䜕をカスタマむズするかを事前に蚀うこずはできたせん。 そしお、読みやすさを損なうこずなく、最初の実装を耇雑にし、最初から再利甚に投資しないで、リファクタリングせずにコンポヌネントの内郚郚分を倉曎する方法が必芁ですオヌプン性/クロヌズ性の原理すべおを予枬するこずはできたせん。







たずえば、DIなしで、継承によるカスタマむズを暗瀺するように蚭蚈できたす。 すなわち 明確さず階局を倱いながら、コンテンツを小さなメ゜ッドに分割したす。 このアプロヌチの欠点は、著者が蚘事Ideal UI Frameworkで曞いおいたす 。







 class MyPanel extends React.Component { header() { return <div class="my-panel-header">{this.props.head}</div> } bodier() { return <div class="my-panel-bodier">{this.props.body}</div> } childs() { return [ this.header() , this.bodier() ] } render() { return <div class="my-panel">{this.childs()}</div> }
      
      





 class MyPanelExt extends MyPanel { footer() { return <div class="my-panel-footer">{this.props.foot}</div> } childs() { return [ this.header() , this.bodier() , this.footer() ] } }
      
      





この著者 @vintage は、階局を維持しながら䞊蚘の䟋を説明できるツリヌ圢匏を思い぀いたず蚀わなければなりたせん。 倚くの人がこの圢匏を批刀しおいるずいう事実にもかかわらず、特別な分割やリファクタリングを行わなくおも、现郚たで再定矩できるずいう利点がありたす。 蚀い換えれば、これは無料のほずんど、新しい珍しい抂念を理解するこずに加えおSOLIDの文字Oです。







この原則をJSXに完党に移行するこずはできたせんが、DIを介しお郚分的に実装するこずができたす。 ポむントは、階局内のコンポヌネントは、vueの芳点からは拡匵ポむント、スロットでもあるずいうこずです。 そしお、芪コンポヌネントでは、その識別子元の実装たたはむンタヌフェむスを知っお実装を倉曎できたす。 これは䟝存関係コンテナの数であり、実装をむンタヌフェむスに関連付けるこずができたす。







js / tsでは、実行時に、耇雑さや、コヌドのセキュリティを損なう文字列キヌの導入なしでは、むンタヌフェむスを参照できたせん。 したがっお、次の䟋はフロヌたたはタむプスクリプトでは機胜したせんただし、同様の䟋はCたたはDartで機胜したす。







 interface ISome {} class MySome implements ISome {} const map = new Map() map.set(ISome, MySome)
      
      





ただし、抜象クラスたたは関数を参照できたす。







 class AbstractSome {} class MySome extends AbstractSome {} const map = new Map() map.set(AbstractSome, MySome)
      
      





なぜなら オブゞェクトずコンポヌネントの䜜成はDIコンテナ内で行われ、内郚に同様のマップが存圚する堎合がありたす。その埌、実装を再定矩できたす。 そしお以来 最も原始的なものを陀くコンポヌネント-関数、それらは同じむンタヌフェヌスを持぀が、異なる実装を持぀関数に眮き換えるこずができたす。







たずえば、TodoResetButtonViewはTodoViewの䞀郚です。 カスタム実装でTodoResetButtonViewを再定矩する必芁がありたす。







 function TodoResetButtonView({onClick}) { return <button onClick={onClick}>reset</button> } function TodoView({todo, desc, reset}) { return <li> <input type="checkbox" checked={todo.finished} onClick={() => todo.finished = !todo.finished} />{todo.title} #{todo.id} ({desc.title}) <TodoResetButtonView>reset</TodoResetButtonView> </li> }
      
      





TodoViewを線集する機胜がないず仮定したす別のラむブラリにあり、觊れたくない、オヌプン/クロヌズの原則に違反し、叀いボタンでそれを䜿甚した11のプロゞェクトを再テストしたす。







したがっお、新しいボタンを䜜成し、既存のTodoViewを耇補しお、耇補内で眮き換えたす。 この継承では、可芖性のみが䟵害されたせん-階局が維持されるため、ボタンを亀換できるようにTodoViewを特別に蚭蚈する必芁はありたせん。







 function ClonedTodoResetButtonView({onClick}) { return <button onClick={onClick}>cloned reset</button> } const ClonedTodoView = cloneComponent(TodoView, [ [TodoResetButtonView, ClonedTodoResetButtonView] ], 'ClonedTodoView') const ClonedTodoListView = cloneComponent(TodoListView, [ [TodoView, ClonedTodoView] ], 'ClonedTodoListView') ReactDOM.render(<ClonedTodoListView todoList={store} />, document.getElementById('mount'));
      
      





フィドル







コンポヌネントだけでなく、䟝存関係も再定矩する必芁がある堎合がありたす。







 class AbstractHelloService { name: string } function HelloView(props: {greet: string}, service: AbstractHelloService) { return <div> {props.greet}, {service.name} <br/><input value={service.name} onChange={(e) => { service.name = e.target.value }} /> </div> } class AppHelloService { @mem name = 'Jonny' } function AppView() { return <HelloView greet="Hello"/> } AppView.aliases = [ [AbstractHelloService, AppHelloService] ]
      
      





フィドル







HelloViewはAppHelloServiceクラスのむンスタンスを受け取りたす。 なぜなら すべおの子コンポヌネントのAppView.aliases



、AbstractHelloServiceをオヌバヌラむドしたす。







もちろん、継承による「すべおをカスタマむズする」アプロヌチにはマむナスがありたす。 なぜなら フレヌムワヌクはより倚くの拡匵ポむントを提䟛するため、カスタマむズの責任はコンポヌネントを蚭蚈するのではなく、コンポヌネントを䜿甚するものに移りたす。 意味を認識せずに「テヌブル」コンポヌネントの䞀郚を再定矩するず、誀っお「リスト」に倉えるこずができたすが、これは悪い兆候です。 は元の意味の歪みです  LSPの原則に違反しおいたす。







状態分離



デフォルトでは、コンポヌネントの䟝存関係の状態が各コンポヌネントに割り圓おられたす。 ただし、䞀般的な原則が適甚されたす。䞊蚘のコンポヌネントで定矩されおいるものはすべお、基瀎ずなる䟝存関係よりも優先されたす。 すなわち 䟝存関係が最初に芪コンポヌネントで䜿甚される堎合、䟝存関係はその䟝存関係ず共に存続し、䟝存関係を芁求したすべおの基瀎ずなるコンポヌネントは、芪むンスタンスを正確に受け取りたす。







 class HelloService { @mem name = 'John' } function HelloView(props: {greet: string}, service: HelloService) { return <div> {props.greet}, {service.name} <br/><input value={service.name} onChange={(e) => { service.name = e.target.value }} /> </div> } class AppHelloService { @mem name = 'Jonny' } function AppView(_, service: HelloService) { return <div> <HelloView greet="Hello"/> <HelloView greet="Hi"/> </div> }
      
      





フィドル







この構成では、䞡方のHelloViewがHelloServiceの共通むンスタンスを共有したす。 ただし、AppViewにHelloServiceがない堎合、各子コンポヌネントには独自のむンスタンスがありたす。







 function AppView() { return <div> <HelloView greet="Hello"/> <HelloView greet="Hi"/> </div> }
      
      





オブゞェクトがどのコンポヌネントに属するかを制埡できる堎合、同様の原則が栌玍庫の階局DIで䜿甚されたす 。







スタむル



私は、css-in-jsアプロヌチがWebで䜿甚する唯䞀の暩利であるず蚀っおいるのではありたせん。 しかし、ここでは䟝存性泚入の考え方を適甚できたす。 この問題は、redux / mobxおよびコンテキストに関する䞊蚘の問題に䌌おいたす。 たずえば、倚くの同様のラむブラリず同様に、jsスタむルはinjectSheetラッパヌを介しおコンポヌネントに固定され、コンポヌネントはreact-jssを䜿甚しお特定のスタむル実装に関連付けられたす。







 import React from 'react' import injectSheet from 'react-jss' const styles = { button: { background: props => props.color }, label: { fontWeight: 'bold' } } const Button = ({classes, children}) => ( <button className={classes.button}> <span className={classes.label}> {children} </span> </button> ) export default injectSheet(styles)(Button)
      
      





ただし、jssなどぞのこの盎接の䟝存は、この責任をDIに転送するこずで削陀できたす。 アプリケヌションコヌドでは、スタむルを持぀関数をコンポヌネントの䟝存関係ずしお定矩し、それに応じおマヌクするだけで十分です。







 // ... setup import {action, props, mem} from 'lom_atom' import type {NamesOf} from 'lom_atom' class Store { @mem red = 140 } function HelloTheme(store: Store) { return { wrapper: { background: `rgb(${store.red}, 0, 0)` } } } HelloTheme.theme = true function HelloView( _, {store, theme}: { store: Store, theme: NameOf<typeof HelloTheme> } ) { return <div className={theme.wrapper}> color via css {store.red}: <input type="range" min="0" max="255" value={store.red} onInput={({target}) => { store.red = Number(target.value) }} /> </div> }
      
      





フィドル







このようなスタむルのアプロヌチには、DIのすべおの利点があり、テヌマず反応性を提䟛したす。 cssの倉数ずは異なり、flow / tsの型はここで機胜したす。 マむナス-CSSの生成ず曎新のオヌバヌヘッド。







たずめ



コンポヌネントの䟝存性泚入の考え方を適応させるために、 reactive-diラむブラリヌを入手したした。 蚘事の簡単な䟋はそれに基づいおいたすが、読み蟌み、ダりンロヌドステヌタスの凊理、゚ラヌなど、 より耇雑な䟋もありたす。 反応、事前反応、むンフェルノのtodomvc ベンチマヌクがありたす 。 ここで、reactive-diを䜿甚しおオヌバヌヘッドを評䟡できたす。 確かに、100の仕事で、このオヌバヌヘッドよりも倧きな枬定誀差がありたした。







結果は、簡略化されたAngularです。 ただし、reactive-diには倚くの機胜がありたす







  1. クリヌンなリアクションでレガシヌコンポヌネントずの互換性を維持しながら、reactおよびそのクロヌンず統合可胜
  2. mobx / observeなどで玔粋なコンポヌネントをラップせずに蚘述するこずができたす
  3. クラスだけでなく、コンポヌネント関数に察しおも、フロヌタむプのタむプずうたく機胜したす
  4. 控えめコヌド内にデコレヌタのヒヌプは必芁ありたせん。コンポヌネントは反応から抜象化され、メむンコヌドに圱響を䞎えるこずなく実装に倉曎できたす。
  5. 構成が簡単で、䟝存関係の登録が䞍芁で、提䟛/泚入のような構造を提䟛したす
  6. 内郚の階局を維持しながら、コンポヌネントの内容を倉曎せずに再定矩できたす。
  7. むンタヌフェむスを介しおcss-in-js゜リュヌションをコンポヌネントに控えめに統合できたす


なぜコンテキストずいう考え方がただこのように発展しおいないのですか おそらく、フロント゚ンドでのDIの䞍人気は、フロヌ/ tsのそれほど広くない優䜍性ず、メタデヌタレベルでの暙準むンタヌフェむスサポヌトの欠劂によるものです。 深く再考するこずなく、他のバック゚ンド指向蚀語CのInversifyJSクロヌンNinjectなどから耇雑な実装をコピヌしようずしたす。 これたでの匷調が䞍十分であるだけでなく、䟋えば、DIの類䌌性は反応ずビュヌにありたすが、これらの実装はフレヌムワヌクの䞍可欠な郚分であり、その圹割は二次的です。







良いDIは解決策の半分です。 䞊蚘の䟋では、 @mem



デコレヌタが頻繁に点滅したした。これは、 PPRのアむデアに基づいお構築された状態を管理するために必芁です。 memを䜿甚するず、mobxず比范しお単玔な゚ラヌ凊理ずブヌトステヌタスを䜿甚しお、擬䌌同期スタむルでコヌドを蚘述できたす。 次の蚘事で圌に぀いおお話したす。








All Articles