再利用可能なUIコンポーネントを作成するためのヒントとコツ

ファーザード・ナジフィによる写真

この記事では、Ember.jsを使用してコアフロントエンドライブラリを構築するときに使用するヒントとコツを共有します。以前は連絡がなかったので、これは素晴らしい学習の機会でした。皆さんも楽しんでくださいね!記事のアイデアを例示するために使用されるコードには、ポイントを得るのに十分な情報しか含まれていません。また、いくつかのEmber.jsの用語を使用しますが、概念はフレームワークに依存しないことを意図しています。

目的

簡単に言うと、ライブラリを構築するための要件は次のとおりです。

  1. 生産的でなければなりません。
  2. 保守可能でなければなりません。
  3. 一貫している必要があります。

アプローチ

ビジネスロジックを最小限に抑える

プロジェクトで最も頻繁に発生する問題の1つは、ロジックが非常に多く含まれているコンポーネントです。したがって、理論的には範囲外のタスクを実行します。

機能を実装する前に、コンポーネントが担当する義務の一部を概説することをお勧めします。

ボタンコンポーネントを作成しているとします。

次のことができるようになりたい:

  • ボタンのタイプを通知します—プライマリまたはレギュラー
  • ボタン内に表示されるコンテンツを通知します(アイコンとテキスト)
  • ボタンを無効または有効にする
  • クリック時に何らかのアクションを実行する

この小さなアウトラインがあるので、このコンポーネントを構築するプロセスに関係するさまざまな部分を引き離します。物を置くことができる場所を特定するようにしてください。

1 —タイプとコンテンツはコンポーネント固有であるため、コンポーネントファイルに配置できます。

タイプは(ある程度)必須であるため、値が提供されない場合に備えて検証を追加しましょう。

const type = get(this、 'type');
constタイプ= {
  プライマリ: 'btn--primary'、
  regular: 'btn--regular'、
}
return(type)? types [type]:types.regular;

プロパティをオブジェクトにマッピングするのが好きです。なぜなら、危険なボタンなどが必要になった場合に、多くの労力をかけずに物事を拡大できるからです。

2 —無効状態は、入力などのさまざまなコンポーネントで見つけることができます。繰り返しを避けるために、この動作をモジュールまたは共有構造に移動することができます-人々はそれをミックスインと呼びます。

3 —クリックアクションはさまざまなコンポーネントにあります。したがって、別のファイルに移動することもでき、その中にロジックを含める必要はありません。開発者が提供するコールバックを呼び出すだけです。

このようにして、拡張をサポートする基本アーキテクチャの概要を説明しながら、コンポーネントが対処する必要があるケースを把握できます。

個別の再利用可能なUI状態

特定のUIの相互作用は、次のようなさまざまなコンポーネント間で共通です。

  • 有効/無効-例ボタン、入力
  • 展開/縮小-例折りたたみ、ドロップダウンリスト
  • 表示/非表示-ほとんどすべて

これらのプロパティは、多くの場合、視覚状態を制御するためだけに使用されます。

異なるコンポーネント全体で一貫した命名法を維持します。視覚状態に関連するすべてのアクションは、ミックスインに移動できます。

/ * UIStateMixin * /
disable(){
  set(this、 ‘disabled’、true);
  これを返す;
}、
enable(){
  set(this、 'disabled'、false ');
  これを返す;
}、

各メソッドは、特定の変数の切り替えのみを担当し、次のように、チェーンの現在のコンテキストを返します。

ボタン
  .disable()
  .showLoadingIndicator();

このアプローチは拡張できます。内部コンテキストを使用する代わりに、異なるコンテキストを受け入れて外部変数を制御できます。例えば:

_getCurrentDisabledAttr(){
  return(isPresent(get(this、 'disabled')))
    ? 'disabled' / *外部パラメーター* /
    : '無効になっています'; / *内部変数* /
}、
enable(context){
  set(context || this、this._getCurrentDisabledAttr()、false);
  これを返す;
}

基本機能の抽象化

すべてのコンポーネントには特定のルーチンが含まれています。これらのルーチンは、コンポーネントの目的に関係なく実行する必要があります。たとえば、コールバックをトリガーする前に検証します。

これらのデフォルトのメソッドは、次のように独自のミックスインに移動することもできます。

/ * BaseComponentMixin * /
_isCallbackValid(callbackName){
  constコールバック= get(this、callbackName);
  
  return !!(isPresent(callback)&& typeof callback === 'function');
}、
_handleCallback(callback、params){
  if(!this._isCallbackValid(callback)){
    新しいエラーをスロー(/ *メッセージ* /);
  }
  this.sendAction(callback、params);
}、

そして、コンポーネントに含まれます。

/ *コンポーネント* /
onClick(params){
  this._handleCallback( 'onClick'、params);
}

これにより、基本アーキテクチャの一貫性が維持されます。また、サードパーティソフトウェアとの拡張や統合も可能です。でも、哲学的なアブストラクトにはならないでください。

コンポーネントの構成

機能をできる限り書き換えないでください。専門化を達成できます。これは、構成とグループ化によって実行できます。新しいコンポーネントを作成するために小さなコンポーネントを一緒に微調整するだけでなく。

例えば:

基本コンポーネント:ボタン、ドロップダウン、入力。
ドロップダウンボタン=>ボタン+ドロップダウン
オートコンプリート=>入力+ドロップダウン
選択=>入力(読み取り専用)+ドロップダウン

このように、各コンポーネントには独自の義務があります。それぞれが独自の状態とパラメーターを処理し、ラッパーコンポーネントが特定のロジックを処理します。

最高の懸念の分離。

懸念事項の分割

より複雑なコンポーネントを構成する場合、懸念事項を分割する可能性があります。コンポーネントの異なる部分間で懸念を分割できます

選択コンポーネントを構築しているとしましょう。

{{form-select binding = productId items = items}}
アイテム= [
  {説明:「製品#1」、値:1}、
  {説明:「製品#2」、値:2}
]

内部的には、単純な入力コンポーネントとドロップダウンがあります。

{{form-input binding = _description}}
{{ui-dropdown items = items onSelect =(action 'selectItem')}}

私たちの主なタスクは、ユーザーに説明を提示することですが、アプリケーションにとっては意味がありません。価値はあります。

オプションを選択する場合、オブジェクトを分割し、内部変数を介して入力に説明を送信し、値をコントローラーにプッシュして、バインドされた変数を更新します。

この概念は、数値、オートコンプリート、選択フィールドなど、バインドされた値を変換する必要があるコンポーネントに適用できます。 Datepickersもこの動作を実装できます。マスクされた値をユーザーに提示しながら、バインドされた変数を更新する前に日付のマスクを解除できます。

リスクは、変換の複雑さが増すにつれて高くなります。過度のロジックまたはイベントをサポートする必要があるため、このアプローチを実装する前によく考えてください。

プリセットと新しいコンポーネント

開発を促進するために、コンポーネントとサービスを最適化する必要がある場合があります。これらは、プリセットまたは新しいコンポーネントの形で提供されます。

プリセットはパラメーターです。通知されると、コンポーネントに事前定義された値を設定し、その宣言を簡素化します。ただし、新しいコンポーネントは通常、ベースコンポーネントのより特殊なバージョンです。

難しいのは、いつプリセットを実装するか、新しいコンポーネントを作成するかを知ることです。この決定を行う際には、次のガイドラインを使用します。

プリセットを作成するタイミング

1 —繰り返し使用パターン

特定のコンポーネントが同じパラメーターでさまざまな場所で再利用される場合があります。これらの場合、特に基本コンポーネントに過剰な数のパラメーターがある場合、新しいコンポーネントよりもプリセットを優先します。

/ *通常の実装* /
{{form-autocomplete
    binding = productId
    url = "products" / *取得するURL * /
    labelAttr = "description" / *ラベルとして使用される属性* /
    valueAttr = "id" / *値として使用される属性* /
    apiAttr = "product" / *要求に応じて送信されるパラメータ* /
}}
/ *プリセット* /
{{form-autocomplete
    preset = "product"
    binding = productId
}}

プリセットの値は、パラメーターが通知されていない場合にのみ設定され、柔軟性が維持されます。

/ *プリセットモジュールの単純な実装* /
constプリセット= {
  製品: {
    url:「製品」、
    labelAttr:「説明」、
    valueAttr: 'id'、
    apiAttr:「製品」、
  }、
}
const attrs = presets [get(this、 ‘preset’)];
Object.keys(attrs).forEach((prop)=> {
  if(!get(this、prop)){
    set(this、prop、attrs [prop]);
  }
});

このアプローチは、コンポーネントのカスタマイズに必要な知識を減らします。同時に、デフォルト値を1か所で更新できるため、メンテナンスが容易になります。

2 —ベースコンポーネントが複雑すぎる

より具体的なコンポーネントを作成するために使用する基本コンポーネントが受け入れるパラメーターが多すぎる場合。したがって、それを作成するといくつかの問題が発生します。例えば:

  • すべてではないにしても、ほとんどのパラメータを新しいコンポーネントからベースコンポーネントに注入する必要があります。より多くのコンポーネントが派生するため、ベースコンポーネントの更新は膨大な量の変更を反映します。したがって、バグの発生率が高くなります。
  • 作成されるコンポーネントが増えるにつれて、さまざまなニュアンスを文書化して記憶することが難しくなります。これは特に新しい開発者に当てはまります。

新しいコンポーネントを作成するタイミング

1 —機能の拡張

より単純なコンポーネントから機能を拡張する場合、新しいコンポーネントを作成することが可能です。コンポーネント固有のロジックが別のコンポーネントにリークするのを防ぐのに役立ちます。これは、追加の動作を実装するときに特に役立ちます。

/ *宣言* /
{{ui-button-dropdown items = items}}
/* フードの下 */
{{#ui-button onClick =(action 'toggleDropdown')}}
  {{label}}  
{{/ ui-button}}
{{#if isExpanded}}
  {{ui-dropdown items = items}}
{{/ if}}

上記の例では、ボタンコンポーネントを使用しています。これにより、レイアウトが拡張され、ドロップダウンコンポーネントとその表示状態を含む固定アイコンがサポートされます。

2 —装飾パラメーター

新しいコンポーネントを作成する別の理由が考えられます。これは、パラメータの可用性を制御したり、デフォルト値を装飾する必要がある場合です。

/ *宣言* /
{{form-datepicker onFocus =(action 'doSomething')}}
/* フードの下 */
{{form-input onFocus =(action '_onFocus')}}
_onFocus(){
  $(this.element)
    .find( 'input')
    .select(); / *フォーカスのフィールド値を選択* /
  this._handleCallback( 'onFocus'); / * paramコールバックをトリガーします* /
}

この例では、フィールドがフォーカスされたときに呼び出されることを意図した関数がコンポーネントに提供されました。

内部的には、コールバックをベースコンポーネントに直接渡す代わりに、内部関数を渡します。これは特定のタスク(フィールド値の選択)を実行し、提供されたコールバックを呼び出します。

基本入力コンポーネントが受け入れるすべてのパラメーターをリダイレクトしているわけではありません。これは、特定の機能の範囲を制御するのに役立ちます。また、不必要な検証も回避します。

私の場合、onBlurイベントは別のイベントonChangeに置き換えられました。これは、ユーザーがフィールドに入力するか、カレンダーで日付を選択するとトリガーされます。

結論

コンポーネントを構築するときは、あなたの側だけでなく、日常生活でそのコンポーネントを使用している人を考慮してください。このように、誰もが勝ちます。

最良の結果は、グループの全員が自分自身とグループに最適なことをすることから得られます— John Nash

また、フィードバックを求めることを恥じないでください。常に作業できるものを見つけるでしょう。

ソフトウェアエンジニアリングのスキルをさらに磨くために、エリックエリオットのシリーズ「Composing Software」に従うことをお勧めします。それは素晴らしいです!

さて、あなたがこの記事を楽しんだことを願っています。これらの概念を取り入れて、独自のアイデアに変えて、私たちと共有してください!

また、twitter @gcolombo_でお気軽にご連絡ください!あなたの意見を聞き、一緒に仕事をしたいです。

ありがとう!