FlywheelのRailsでUIコンポーネントを構築する方法

公開: 2019-11-16

大規模なWebアプリケーションで視覚的な一貫性を維持することは、多くの組織で共通の問題です。 Flywheelでも違いはありません。 私たちのメインのWebアプリケーションはRubyonRailsで構築されており、約15人のRails開発者と3人のフロントエンド開発者がいつでもコードをコミットしています。 私たちもデザインに力を入れており(会社としてのコアバリューの1つです)、スクラムチームの開発者と緊密に連携する3人のデザイナーがいます。

私たちの主な目標は、Flywheel開発者が障害物なしにレスポンシブページを作成できるようにすることです。 一般に、障害には、モックアップを構築するために使用する既存のコンポーネント(非常に類似した冗長コンポーネントでコードベースを膨らませる)がわからないことや、設計者と再利用性について話し合う時期がわからないことが含まれます。 これは、一貫性のない顧客体験、開発者のフラストレーション、および開発者と設計者の間の異なる設計言語の一因となります。

スタイルガイドとUIパターンおよびコンポーネントを構築/維持する方法を何度か繰り返しましたが、それぞれの繰り返しは、当時直面していた問題の解決に役立ちました。 私たちは今、新しい(私たちにとって)アプローチに取り組んでおり、これから長い間私たちを立ち上げることができると確信しています。 Railsアプリケーションで同様の問題に直面し、サーバー側からコンポーネントにアプローチしたい場合は、この記事がいくつかのアイデアを提供してくれることを願っています。

この記事では、以下について詳しく説明します。

  • 私たちが解決しようとしていること
  • コンポーネントの制約
  • サーバー側でのコンポーネントのレンダリング
  • サーバー側のコンポーネントを使用できない場合


私たちが解決しようとしていること

UIコンポーネントを完全に制約し、同じUIが複数の方法で作成される可能性を排除したかったのです。 顧客は(最初は)わからないかもしれませんが、コンポーネントに制約がないと、開発者のエクスペリエンスが混乱し、保守が非常に難しくなり、グローバルな設計変更を行うことが困難になります。

コンポーネントにアプローチする従来の方法は、特定のコンポーネントを構築するために必要な多くのマークアップをリストしたスタイルガイドを使用することでした。 たとえば、スラットコンポーネントのスタイルガイドページは次のようになります。

これはうまく機能し、数年間私たちに適していましたが、コンポーネントを使用するためのバリアント、状態、または代替方法を追加すると、問題が発生し始めました。 UIが複雑なため、スタイルガイドを参照して、使用するクラスと回避するクラス、および目的のバリエーションを出力するために必要なマークアップの順序を知るのは面倒になりました。 また、多くの場合、設計者は特定のコンポーネントにほとんど追加や調整を加えません。 スタイルガイドはそれを完全にはサポートしていなかったため、その微調整を正しく表示するための代替ハック(別のコンポーネントの一部を不適切に共食いするなど)がいらいらするほど一般的になりました。

制約のないコンポーネントの例

時間の経過とともに不整合がどのように表面化するかを説明するために、Flywheelアプリのコンポーネントの1つであるカードヘッダーの単純な(そして考案された)非常に一般的な例を使用します。

デザインのモックアップから始めて、これはカードヘッダーがどのように見えたかです。 タイトル、ボタン、下枠が付いたとてもシンプルなものでした。

.card__header
  .card__header-left
    %h2 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

コード化した後、タイトルの左側にアイコンを追加したいデザイナーを想像してみてください。 箱から出して、アイコンとタイトルの間に余白はありません。

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2 Backups
...

理想的には、カードヘッダーのCSSでそれを解決しますが、この例では、別の開発者が「ああ、わかっています! マージンヘルパーがいくつかあります。 タイトルにヘルパークラスをたたくだけです。」

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2.--ml-10 Backups
...

技術的にはモックアップのように見えますよね?! もちろんですが、1か月後、別の開発者がカードヘッダーを必要としていますが、アイコンは必要ありません。 彼らは最後の例を見つけ、それをコピーして貼り付け、アイコンを削除するだけです。

繰り返しますが、正しく見えますよね? 文脈から外れて、デザインに鋭敏な目がない人にとっては、確かに! しかし、オリジナルの隣でそれを見てください。 タイトルの左マージンは、左ヘルパーのマージンを削除する必要があることに気づかなかったため、まだ残っています。

この例をさらに一歩進めて、下の境界線のないカードヘッダーを要求した別のモックアップを考えてみましょう。 「ボーダーレス」と呼ばれるスタイルガイドにある状態を見つけて、それを適用するかもしれません。 完全!

次に、別の開発者がそのコードを再利用しようとする場合がありますが、この場合、実際には境界線が必要です。 仮に、スタイルガイドに記載されている適切な使用法を無視していると仮定して、ボーダレスクラスを削除するとボーダーが与えられることに気づかないとしましょう。 代わりに、水平方向のルールを追加します。 タイトルと境界線の間に余分なパディングがあるので、時間と出来上がりにヘルパークラスを適用します!

元のカードヘッダーに対するこれらすべての変更により、コードが混乱するようになりました。

.card__header.--borderless
  .card__header-left
    %h2.--ml-10 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

  %hr.--mt-0.--mb-0

上記の例は、制約のないコンポーネントが時間の経過とともに乱雑になる可能性がある点を説明するためのものであることに注意してください。 私たちのチームの誰かがカードヘッダーのバリエーションを出荷しようとした場合、それはデザインレビューまたはコードレビューによって捕らえられるべきです。 しかし、このようなものは時々亀裂をすり抜けてしまうので、防弾する必要があります!


コンポーネントの制約

上記の問題は、コンポーネントによってすでに明確に解決されていると思われるかもしれません。 それは正しい仮定です! ReactやVueのようなフロントエンドフレームワークは、まさにこの目的のために非常に人気があります。 これらは、UIをカプセル化するためのすばらしいツールです。 ただし、私たちが常に気に入らない問題が1つあります。それは、UIをJavaScriptでレンダリングする必要があるということです。

Flywheelアプリケーションは、主にサーバーでレンダリングされたHTMLを使用して非常にバックエンドが重いですが、幸いなことに、コンポーネントはさまざまな形式で提供されます。 結局のところ、UIコンポーネントは、ブラウザにマークアップを出力するスタイルとデザインルールのカプセル化です。 この実現により、JavaScriptフレームワークのオーバーヘッドなしで、コンポーネントに対して同じアプローチをとることができます。

以下では、制約付きコンポーネントを構築する方法について説明しますが、それらを使用することで得られる利点のいくつかを次に示します。

  • コンポーネントをまとめるのに本当に間違った方法はありません。
  • コンポーネントはあなたのためにすべてのデザイン思考を行います。 (オプションを渡すだけです!)
  • コンポーネントを作成するための構文は非常に一貫しており、簡単に推論できます。
  • コンポーネントの設計変更が必要な場合は、コンポーネントで一度変更すれば、どこでも更新できると確信できます。

サーバー側でのコンポーネントのレンダリング

では、コンポーネントを制約することで何について話しているのでしょうか。 掘り下げましょう!

前述のように、Flywheelアプリケーションで作業している開発者は、ページのデザインモックアップを確認し、障害なくそのページをすぐに作成できるようにする必要があります。 つまり、UIを作成する方法は、A)非常によく文書化され、B)非常に宣言的で、当て推量がない必要があります。

救助への部分的(またはそう思った)

過去に試した最初の試みは、Railsパーシャルを使用することでした。 パーシャルは、Railsがテンプレートで再利用できる唯一のツールです。 当然、それらは誰もが最初に到達するものです。 ただし、ロジックを再利用可能なテンプレートと組み合わせる必要がある場合は、パーシャルを使用するすべてのコントローラーにロジックを複製するか、パーシャル自体にロジックを埋め込むかの2つの選択肢があるため、これらに依存することには重大な欠点があります。

パーシャルは、コピー/貼り付けの重複ミスを防ぎ、何かを再利用する必要がある最初の数回は問題なく機能します。 しかし、私たちの経験から、パーシャルはすぐに、ますます多くの機能とロジックのサポートで雑然とします。 しかし、ロジックはテンプレートに存在するべきではありません!

細胞入門

幸いなことに、コード再利用し、ロジックを表示しないようにする、パーシャルのより良い代替手段があります。 これはCellsと呼ばれ、Trailblazerによって開発されたRubygemです。 セルは、ReactやVueなどのフロントエンドフレームワークで人気が高まるかなり前から存在しており、ロジックテンプレートの両方を処理するカプセル化されたビューモデルを作成できます。 これらはビューモデルの抽象化を提供しますが、Railsにはそのままではありません。 私たちは実際にFlywheelアプリでCellsをしばらく使用していますが、グローバルで再利用可能な規模ではありません。

最も単純なレベルでは、Cellsを使用すると、次のようにマークアップのチャンクを抽象化できます(テンプレート言語にはHamlを使用します)。

%div
  %h1 Hello, world!

再利用可能なビューモデル(この時点ではパーシャルに非常に似ています)に変換し、次のように変換します。

= cell("hello_world")

これは最終的に、セル自体を変更せずにヘルパークラスまたは誤った子コンポーネントを追加できない場所にコンポーネントを制限するのに役立ちます。

セルの構築

すべてのUIセルをapp/cells/uiディレクトリに配置します。 各セルには、_cell.rbという接尾辞が付いたRubyファイルが1つだけ含まれている必要があります。 技術的には、content_tagヘルパーを使用してRubyでテンプレートを直接作成できますが、ほとんどのセルには、コンポーネントによって指定されたフォルダーにある対応するHamlテンプレートも含まれています。

ロジックのない超基本セルは次のようになります。

// cells/ui/slat_cell.rb

module UI
  class SlatCell < ViewModel
    def show
    end
  end
end

showメソッドは、セルをインスタンス化したときにレンダリングされるものであり、セルと同じ名前のフォルダー内で対応するshow.hamlファイルを自動的に検索します。 この場合、それはapp / cells / ui / slatです(すべてのUIセルをUIモジュールにスコープします)。

テンプレートでは、セルに渡されたオプションにアクセスできます。 たとえば、セルが= cell( "ui / slat"、title: "Title"、subtitle: "Subtitle"、label: "Label")のようなビューでインスタンス化されている場合、optionsオブジェクトを介してこれらのオプションにアクセスできます。

// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      %h4= options[:title]
      %p= options[:subtitle]
      = icon(options[:icon], color: "blue")

多くの場合、オプションが存在しない場合に空の要素がレンダリングされないように、単純な要素とその値をセル内のメソッドに移動します。

// cells/ui/slat_cell.rb
def title
  return unless options[:title]
  content_tag :h4, options[:title]
end

def subtitle
  return unless options[:subtitle]
  content_tag :p, options[:subtitle]
end
// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      = title
      = subtitle

UIユーティリティでセルをラップする

これが大規模に機能する可能性があるという概念を証明した後、セルを呼び出すために必要な無関係なマークアップに取り組みたいと思いました。 流れが悪く、覚えにくいです。 だから私たちはそれのために少しヘルパーを作りました! これで、= ui“ name_of_component”を呼び出して、オプションをインラインで渡すことができます。

= ui "slat", title: "Title", subtitle: "Subtitle", label: "Label"

オプションをインラインではなくブロックとして渡す

UIユーティリティをもう少し進めてみると、多数のオプションがすべて1行に表示されているセルは、追跡が非常に難しく、見苦しいことがすぐに明らかになりました。 インラインで定義された多くのオプションを持つセルの例を次に示します。

= ui “slat", title: “Title”, subtitle: “Subtitle”, label: “Label”, link: “#”, tertiary_title: “Tertiary”, disabled: true, checklist: [“Item 1”, “Item 2”, “Item 3”]

これは非常に面倒で、Cellsセッターメソッドをインターセプトしてハッシュ値に変換し、オプションにマージするOptionProxyというクラスを作成することになります。 それが複雑に聞こえても、心配しないでください。私にとっても複雑です。 これが、シニアソフトウェアエンジニアの1人であるAdamが書いたOptionProxyクラスの要点です。

セル内でOptionProxyクラスを使用する例を次に示します。

module UI
  class SlatCell < ViewModel
    def show
      OptionProxy.new(self).yield!(options, &block)
      super()
    end
  end
end

これで、面倒なインラインオプションをより快適なブロックに変えることができます!

= ui "slat" do |slat|
  - slat.title = "Title"
  - slat.subtitle = "Subtitle"
  - slat.label = "Label"
  - slat.link = "#"
  - slat.tertiary_title = "Tertiary"
  - slat.disabled = true
  - slat.checklist = ["Item 1", "Item 2", "Item 3"]

ロジックの紹介

この時点まで、例にはビューの表示に関するロジックは含まれていません。 これはCellsが提供する最高のものの1つなので、それについて話しましょう!

スラットコンポーネントに固執するため、リンクオプションが存在するかどうかに基づいて、全体をリンクとしてレンダリングする場合と、divとしてレンダリングする場合があります。 これは、divまたはリンクとしてレンダリングできる唯一のコンポーネントであると思いますが、Cellsのパワーの非常に優れた例です。

以下のメソッドは、オプション[:link]の存在に応じて、link_toまたはcontent_tagヘルパーのいずれかを呼び出します。

注:これは、Cellsを使用したUI開発のこの方法全体の構築を支援してくれたAdamLassekに触発されて作成されました。

def container(&block)
  tag =
    if options[:link]
      [:link_to, options[:link]]
    else
      [:content_tag, :div]
    end

  send(*tag, class: “slat__inner”, &block)
end

これにより、テンプレートの.slat__inner要素をコンテナブロックに置き換えることができます。

.slat
  = container do
  ...

私たちがよく使用するCellsのロジックの別の例は、条件付きでクラスを出力するものです。 セルに無効なオプションを追加するとします。 セルの呼び出しでは、disabled:trueオプションを渡すことができ、全体が無効状態になるのを監視する以外に何も変更されません(クリックできないリンクでグレー表示されます)。

= ui "slat" do |slat|
  ...
  - slat.disabled = true

無効化されたオプションがtrueの場合、目的の無効化された外観を取得するために必要なテンプレート内の要素にクラスを設定できます。

.slat{ class: possible_classes("--disabled": options[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ class: possible_classes("--alt": options[:disabled]) }= options[:title]
      %p{ class: possible_classes("--alt": options[:disabled]) }=
      options[:subtitle]
      = icon(options[:icon], color: "gray")

従来は、無効な状態ですべてを正しく機能させるために、どの個々の要素に追加のクラスが必要かを覚えておく(またはスタイルガイドを参照する)必要がありました。 セルを使用すると、1つのオプションを宣言してから、手間のかかる作業を行うことができます。

注: possible_classesは、Hamlで条件付きでクラスを適切に適用できるようにするために作成したメソッドです。


サーバー側のコンポーネントを使用できない場合

セルアプローチは、特定のアプリケーションと作業方法に非常に役立ちますが、問題が100%解決されたと言っても過言ではありません。 私たちはまだJavaScript(その多く)を作成しており、アプリ全体でVueでかなりの数のエクスペリエンスを構築しています。 75%の時間、VueテンプレートはまだHamlに存在し、Vueインスタンスを包含要素にバインドします。これにより、セルアプローチを引き続き利用できます。

ただし、コンポーネントを単一ファイルのVueインスタンスとして完全に制約する方が理にかなっている場所では、Cellsを使用できません。 たとえば、私たちの選択リストはすべてVueです。 でも大丈夫だと思います! CellsコンポーネントとVueコンポーネントの両方にコンポーネントの複製バージョンを用意する必要は実際にはありません。そのため、一部のコンポーネントはVueで100%構築され、一部はCellsで構築されていても問題ありません。 コンポーネントがVueでビルドされている場合、それはDOMでコンポーネントをビルドするためにJavaScriptが必要であることを意味し、そのためにVueフレームワークを利用します。 ただし、他のほとんどのコンポーネントでは、JavaScriptは必要ありません。必要な場合は、DOMが既に構築されている必要があり、イベントリスナーをフックして追加するだけです。

セルアプローチを進めていく中で、コンポーネントを作成して使用する唯一の方法が得られるように、セルコンポーネントとVueコンポーネントの組み合わせを確実に実験していきます。 まだどうなっているのかわからないので、そこに着いたらその橋を渡ります!


私たちの結論

これまでに、最もよく使用されるビジュアルコンポーネントの約30をCellに変換しました。 それは私たちに生産性の巨大なバーストを与え、開発者に彼らが構築している経験が正しく、一緒にハッキングされていないという検証の感覚を与えます。

私たちのデザインチームは、アプリのコンポーネントとエクスペリエンスがAdobe XDでデザインしたものと1:1であることにこれまで以上に自信を持っています。 コンポーネントの変更または追加は、設計者およびフロントエンド開発者との対話によってのみ処理されるようになりました。これにより、チームの他のメンバーは、設計モックアップに一致するようにコンポーネントを微調整する方法を知らなくても、集中できます。

UIコンポーネントを制約するアプローチを常に繰り返していますが、この記事で説明する手法によって、私たちにとって何がうまく機能しているかを垣間見ることができれば幸いです。


Flywheelで働きに来てください!

Flywheelでは、すべての部門がお客様と収益に有意義な影響を与えています。 カスタマーサポート、ソフトウェア開発、マーケティング、またはその間の何かであるかどうかにかかわらず、私たちは皆、人々が本当に恋に落ちることができるホスティング会社を構築するという私たちの使命に向けて協力しています。

私たちのチームに参加する準備はできましたか? 採用中です! こちらからお申し込みください。