72
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LIFULLAdvent Calendar 2020

Day 6

第三の答え、Stimulusjs

Last updated at Posted at 2020-12-05

昨今のフロントエンドトレンドを鑑みてフロントエンドの技術構成を考えると今はどんな選択肢が主流でしょう?
それぞれ並列なものではないですが、React? Vue? Next? Nuxt?そんなワードがきっと頭に浮かぶでしょう。

今の時代、jQueryでゴリゴリUIを記述するとなんだか白い目でみられる。
そう、令和とはそういう時代です。

界隈の最先端おじさんをみると、build processがあるのは当然で、HTML/CSSはjsが吐き出すもので、それが最大限正しいように語られています。

確かにExising Domのアプローチで界隈から評価されてるライブラリはなく、さほどいい選択肢には見えません。(個人の主観)
ではそれらのアプローチの何がそんなにいいんでしょう?

過去から現在まで我々は何に苦心して、それらはそれをどう解決してくれたのでしょう?
雑に紐解いていきましょう。(今回はUI構築の側面にのみフォーカスを当てます)

2008年頃、jQueryが普及し始めた頃

jQuery登場以前、ブラウザが提供する低レベルなDOM APIでのUI構築は現在からすると想像を絶するほどの苦行でした。

当時はquerySelectorはおろか、getElementByClassNameすらも実装有無がブラウザによってわかれる時代でした。

本質的に書きたいUI/ビジネスロジックはクロスブラウザ部分対応のコードにまみれ、さらにはそのクロスブラウザ対応部分も人によって対応方針が異なり、大雑把にUA判定してるような対応であったり、丁寧にメソッド有無を調べて分岐記述されてるものもあったりまちまちで、その対応の差が数年後に悲劇を起こすことも少なくはありませんでした。

そんな憂鬱から我々を救うメシアのようにjQueryは現れました。

jQueryにはSizzle(後に一体化する)というセレクタパーサが搭載されており、今では当然となったセレクタでの要素探索、クロスブラウザ対応不要のイベントリスナなど、我々が開発する上で湯水のように時間を奪われ、コードを汚してしまっていた部分を随分とスリムにしてくれました。

これによりUIの振る舞いについての記述の可読性は、それ以前に比べて随分と見通しがよくなりました。

設計を考える時代に

jQuery登場により、より複雑なWebUIを開発する機会が増え、1ページにつき1jsでの開発はしだいに限界を迎えていきました。
UIの複雑な振る舞いに対応すべく、コードはスパゲッティのように長くなり、このUIパーツが動いているのはこの巨大なjsの何行目が起因しているのか、そんな当たり前のことを知ることが困難になっていきました。

それまでのJavaScript界隈では、あまり設計という文脈で物事を語られることは多くなかったように思います。

こういった問題が顕著になるにつれ、貧弱なDOM APIを補助することにフォーカスをあてたライブラリが脚光を浴びてた時代が終わり、設計に注目が集まるようになってきました。

そんな中、徐々に市民権を集めて行ったフレームワークにBackbone.jsがありました。

BackboneはUI実装においていくつかのスタンダードを作り出してくれました。

// item_list.js
let ItemList = Backbone.View.extend({
  events: {
    'click .remove': 'onItemRemove',
    'click .add': 'onItemAdd'
  },
  onItemRemove() {
    this.$el.remove(...);
  },
  onItemAdd() {
    this.$el.append(...);
  }
});

// main.js
let itemList1 = new ItemList({el: $('#itemList1')});
let itemList2 = new ItemList({el: $('#itemList2')});

UI単位をclassっぽく定義し、newキーワードと共に再利用できるようにしたことはもちろん、イベント割当を機構(events)として用意したことにより、そのUIのもつ振る舞いが、まるでサーバサイドのルーティングファイルのように一元管理され、処理の流れを格段に追いやすくしました。

可読性はさらなる向上を遂げ、処理単位としても明確な区切りができたことにより、ファイル分割の指針としても有用なものとなりました。

一方でまだ課題も残されていました。

上記のコードのmain.jsを見ると、HTMLでできたUIパーツに振る舞いを適応させて使用可能にするのに、まだ要素探索し、それらに手動でclassをnewしてアタッチする必要があります。

xhrなどで追加されたHTMLに対してもまた、その中に含まれるUIパーツごとに頂点要素を探索し、それぞれ振る舞いをアタッチする必要があることになります。

HTMLとJSの概念的距離が遠く感じますね。

要素が出現したら即座に振る舞いがアタッチされ、使用可能なUIとして動くような世界線が望ましいですよね

そんな中、WebComponentsが生まれました。

WebComponents

生のWebComponentsの話は日本の界隈ではあまり取り上げられてないような気もしますが、WebComponentsは上記のような問題を解決してくれる存在となります

<custom-counter></custom-counter>
<custom-counter></custom-counter>
<custom-counter></custom-counter>
customElements.define('custom-counter', class extends HTMLElement {
  constructor() {
    super();

    let shadow = this.attachShadow({mode: 'open'});
    shadow.innerHTML = `
      <style>:host { display: block; padding: 20px; border: 1px solid #000; }</style>
      <p class="value">0</p>
      <button class="increment">increment</button>
    `;

    this.attachEvent();
  }
  attachEvent() {
    this.shadowRoot.querySelector('.increment')
    	.addEventListener('click', evt => this.increment(evt))
  }
  increment() {
    this.value += 1;
  }
  get value() {
    return Number(this.shadowRoot.querySelector('.value').innerHTML);
  }
  set value(v) {
    this.shadowRoot.querySelector('.value').innerHTML = String(v);
  }
});

eventの割当てに強制力がなくなった感は否めませんが、customElementsとしてUIパーツを定義しておくことで、HTML中に対象のUIパーツが出現したのと同時に振る舞いが適応される世界がやってきました。

ShadowRootの中で閉じたscoped cssも魅力的ですね。

classとして定義できるのでgetter/setterを利用することもでき、ビジネスロジックとDOMを切り離すことにもある程度成功してるように思います。

データバインディング系ライブラリの登場

平成の終わり頃に、さきほどのWebComponentsの世界線に、さらにデータバインディング機構と、宣言的UIとを組み合わせたReact/Vueらへんのライブラリが登場しました。

class Item extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: 0};
  }
  
  increment() {
    this.setState(state => ({value: state.value + 1}))
  }
  
  render() {
    const hostStyle = {padding: '20px', border: '1px solid #000'};
    return (
      <div style={hostStyle}>
        <p>{this.state.value}</p>
        <button onClick={() => this.increment()}>increment</button>
      </div>
    )
  }
}

function App() {
  return (
    <div>
      <Item />
      <Item />
      <Item />
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));

テンプレート部分をみるとすぐさまその振る舞いが想起でき、振る舞いのアタッチは完全に自動化されています。

さらにはデータバインディング機構によるDOM操作の排除までもなされており、これからは控えめにいっても最高のソリューションと言えます。

彼らはJavaScriptでHTMLを生成することによってHTMLとJavaScriptの概念的距離を限りなく0にすることに成功したのです。

これらの選択はいついかなる時でも最善なのでしょうか?

その答えは常にアプリケーションの置かれてる環境に左右されるので、かなり難しいと言えます。

というのも、これらのライブラリはそれまでのJavaScript設計と違い、テンプレートエンジンの領域までをJavaScript側にもってくるというアプローチになります。

故にサーバサイドからテンプレート変数をアサインしてテンプレートをレンダリングするという古典的な実装パターンから大きく離れることになります。

これらのライブラリを利用したサービス開発の時、SPA設計することが最も自然なアプローチとなり、サーバはJSONを作るマシンのように振舞うことが期待されるでしょう。
(もちろんhypernovaのようにレンダーサーバを立ち上げてデータのやり取りする方法もあるとは思いますが)

そうなった場合、ブラウザバックした時のフォームの状態復元、スクロール位置の復元など、今までブラウザという巨人の肩にのっていたおかげで、自然と享受できていた部分を自分たちで再実装していくことも必要になってきます。

すでに運用している巨大なアプリケーションがあったとして、それが古典的な実装パターンで成り立っている時、このアプローチへの転換はあまりに大きな変革を取り入れることになります。

また、それらを導入し運用することがやりすぎと映るような控えめなWebサービスもたくさんあると思います。

そういったケースの時にもこれらが最高のアプローチというのはやや強すぎる意見なようにも思います。

サーバサイド設計にもフロントエンド設計にも多様性があってよく、フロントエンドのトレンドとの相性が悪いという理由でそれまでのフレームワークを叩くのもやややりすぎに映ります。

古典的な実装パターンと相性がよく、これまでの多くの問題を解決してくれるライブラリはあるのか?

どう考えても長すぎましたがここからが本題です。
ここで私が推したいのはBasecamp謹製の**Stimulus**です。

海外のとあるブログで、界隈で犯罪的に過小評価されてるとまで書かれたライブラリです、一部をのぞいてほぼ知られていないライブラリでしょう。

これまでに上がった昨今のフロントエンドに最低限求められるUI設計のポイントをまとめ直しましょう。

  • UIの振る舞いの自動アタッチ
  • HTML側からみて振る舞いが宣言的に記述されていること
  • ロジック部分からのDOM操作、過度なDOM探索の排除

React/VueはHTMLをJavaScript側から生成することで、この自動アタッチ、宣言的記述、DOM探索の省略などをクリアしました。

しかし、Stimulusはその戦略とは全く違った方法でこの問題を解決しようとしたのです。

DHHはMutationObserver(DOMの変更を監視するAPI)に注目しました。

StimulusはMutationObserverでDOMの変更を監視し、変更差分に独立したUIパーツが存在していればその振る舞いを自動でアタッチするようにできています。

これまでの例をStimulusを利用したコードで表現するとこうなります。

<div data-controller="counter" class="counter">
  <p data-counter-target="value">0</p>
  <button data-action="click->counter#increment">increment</button>
</div>

<div data-controller="counter" class="counter">
  <p data-counter-target="value">0</p>
  <button data-action="click->counter#increment">increment</button>
</div>
const {Application, Controller} = Stimulus;
const app = Application.start(); // mutation observerが起動する

app.register('counter', class extends Controller {
  static targets = ['value'];

  increment() {
  	this.value += 1;
  }
  
  get value() {
  	return Number(this.valueTarget.textContent);
  }
  
  set value(v) {
  	return this.valueTarget.textContent = String(v);
  }
})

これはかなりWebComponentsと似た書き味になります。

ShadowRootで内部が隠蔽されていないところを除くとかなり近いです
(WebComponentsにあるconnect/disconnect等のLifecycleCallbackもあります)

実際、DHHも最初trixというウェブエディタを生のWebComponentsで書いてて、その過程でこれを生み出したと語っています。
(その後、かき捨てたプロトタイプをjavanとsamstephensonに渡して仕上げてもらったらしい)

生のWebComponentsとの違いは、そのUIの振る舞いがHTML側からみて宣言的であることや、data-identifier-targetによる参照で表面的にDOM探索がなくなっていることでしょうか。

Rect/Vue側からすると中途半端にDOM操作の概念を捨て切れてないように思えるかもしれませんが、これほどHTMLとJavaScriptの概念的距離を圧縮させられていれば私は十分及第点に思います。

そして、なによりStimulusはHTMLをレンダリングをしません。

故にサーバサイド設計はどんな選択肢をとってもよく、テンプレートエンジンも好きなものを使えばよいのです。

ただHTMLの属性値にいくつかのStimulusのルールを加えるだけで振る舞いは自動的にアタッチされます。

これは本当に痛みが少なく、大きな資産をすでにもっているアプリケーションにおいてもなんら躊躇うことなく導入できる素晴らしさを持っています。

Stimulusのドキュメントを見ると驚くことにほんの数ページで終わりです。

英語が苦手でも30分あれば読み終わるレベル感です。

  • lifecycle method(initialize/connect/disconnect)
  • data-controller
  • data-identifier-target
  • data-action

Stimulus2を使うのであればそれに加えて以下の二つ(執筆時点で昨日リリースされました🎉)

  • data-identifier-class
  • data-identifier-value

これらについての説明があるだけで他はほぼ語られません、それがほとんど全てだからです。

さらに、これにうまくCustomEventを組み合わせて構築すると表面的なDOM探索がほぼなくなります。
(CustomEventを用いた実装のサンプル)

Basecampがこの夏公開したメールクライアントのhey.comのソースコードはsourcemap付きで吐かれており公開されてますが、そのコードは驚異的にシンプルで表面的なDOM探索を可能なかぎり排除し、明快にUIの振る舞いを定義することに成功しているように見えます。

ReactやVueほど強力なデータバインディング機構はなく、自然な振る舞いの記述方法ではないかもしれませんが、これで十分なユースケースも少なくないでしょう。

過剰なビルドプロセスもなく(CDNで読み込むだけで動きます)、Webのこれまでの流れを大きく逸脱しない実装でいられることも私は好きです。
そして、サーバサイドフレームワークがなんであってもよいという多様性が何より魅力的です。

第三の選択肢としてStimulusがより多くの市民権をえられる未来がくることを期待しています。

おしまい。

72
42
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?