🏙️

モダンなjQueryに挑戦してみよう

に公開
6

はじめに

先日フロントエンドのリプレイスに、いつまでかけるんだ?という記事を書きました。jQueryフロントエンドをReact/Vue等のモダンフロントエンドにリプレイスするプロジェクトの多くは数年かかり、それでも完了しないことを実例を通して紹介しました。

要するにモダンフロントエンドにリプレイスするプロジェクトは思いの外にコストが高く、そしてリスクが高いのです。これだけ高リスクだということが最初からわかっていれば、そもそもこんなプロジェクトは許可しなかったというケースが大半ではないかと思います。

そうなるとじゃ〜どうすれば良いんだ、となります。

私も明確な答えは持っていませんが、ここでは「もう少しレガシーjQueryコードの改良をしてみませんか?」という提案をしたいと思います。

jQueryでTypeScriptやes moduleを使う方法など、モダンなツールの話もします。

用意したもの

例によって、デモコードをGitHubに公開しました

今回作るのは下記のリンクにあるUIです。

https://github.com/naofumi/modern_jquery/raw/refs/heads/main/documents/modern-jquery.mov

  • 電車の料金を計算するUIです
  • 入力: ユーザが入力する項目
    • 通常席かファーストクラスか
    • 乗客の人数
    • 割引率
  • 出力: 画面で変化する項目
    • 通常席かファーストクラスかのボタンの表示
    • 単価
    • 合計金額

jQueryが得意とするちょっとしたUIの動的アップデートではなく、ブラウザでステートとビジネスロジックを持たせるものを作っています。考えずに作るとスパゲッティーコード化しやすいものです。


複雑になりやすいUIの例: https://refactoring.guru/ja/design-patterns/mediator

こういうUIは入力と出力が絡まり合うので、Mediatorパターンなどを使って解決することが多いです。ReactなどではそのMediatorはステートを持ったコンポーネントであり、そこから単方向データフローで絨毯爆撃的なUI更新を行うのがReactの真骨頂だと私は理解しています。

つまり今回私が用意したのはjQueryが苦手としているタイプのUIです。一方でReactはまさしくこういうUIを作るために生まれたと理解しています。あえてjQueryが苦手なUIを作ってみることによって、jQueryをどうリファクタリングすれば良いかを考えたいと思います。

なお、GitHubのコードには多数のコメントをしていますので、これも見ていただければと思います。ここでは紹介的な意味でピックアップします。

現場のjQueryコードからヒントを得ている

ウェブで検索しても「jQueryのベストプラクティス」みたいな記事はあまりありません。しかし実際に現場に入って幾つかのjQueryのコードを見ていると、数多くの工夫を見ることができます。巷ではjQueryをディスっていますが、現場でコードを書いているレベルの高い人は、黙って結構しっかりと工夫しているのです。古いプロダクトだとjQueryコードの進化の過程を確認することもできます。徐々に工夫をしていった歴史が見えてきてとても楽しいです。

今回のコード例は、現場で見たコードを参考に、自分なりに一つのデモコードの中で表現してみたものです。(私が大袈裟にしているものもあるかもしれませんが)実際に使われているパターンを参考にしています。

jQueryをTypeScriptで書くことができる

これは現場で見たことがありませんが、今回のデモではTypeScriptを使ってjQueryを書いています。 これは実際に現場で見たことがあります。@types/jqueryのパッケージをインストールすればTypeScriptが使用できます。GitHubに公開しているpackage.json

私はRubyistで基本的には静的型付けは不要だと思っています。しかしjQueryの場合は生のHTML要素を扱っているのか、それともjQueryでラップしたものを使っているのかがわからなくなることが多々あります。だからjQueryに限って言えば、私でも静的型付けは便利だと思いました。

とても良いです。強くお勧めします。

Vite等の最新バンドラーが使用できる

これは現場で見たことがありませんが、jQueryはViteやesbuildでも使えます。HMRとまではいきませんが、Viteを使えば自動的にページのリフレッシュもしてくれます。 Webpackを使った例なら現場で見ています。jQueryを使ったライブラリーによっては対応しないものもありますが、それらは別途CDNから使えば良いので、問題になりません。自分で書いたjQueryコードだけでもViteやesbuild, Webpacker等でバンドルするのが便利そうです。

ES moduleが使える

これは現場で見たことがありませんが、 これは現場で見たことがあります。ES moduleを使って、コードを分割したりimportしたりできます。

jQueryを使うとグローバルの名前空間を荒らしてしまうという批判が昔からあります。実際にSprocketsがやるのは多数のJavaScriptファイルを連結するだけで、moduleごとに名前空間を分けたりということはしません。全て同じ名前空間になります。

IIFEパターンというのも大昔からありましたし、Webpacker等がmoduleのサポートを長く提供してきました。そして今ではjQueryでES moduleが使えますので、積極的にコードを分割したり整理したり再利用したりするのが良いでしょう。

ES moduleを使うと以下のメリットがあります。コードはGitHubにありますので、実際にご覧ください。

  • IIFEパターンを使わないで済むので、コードがわかりやすくなります
  • ES moduleをHTMLから読み込むときはtype="module"をつけます。このおかげでブラウザはすぐにファイルをフェッチしに行きますが、実行はDOMがロードされてから行われます。つまりloadDOMContentLoadedreadyなどのイベントハンドラを作ってコードを実行する必要がありません。コードをそのまま書けば、一番適切なタイミングで自動的に実行されます
  • コード分割が気軽に行えるので、コードの構成を工夫しやすくなります。つまり綺麗でわかりやすいコードに徹することができます(今回はビジネスロジックをcalculator/price.tsに切り分けました)

index.html GitHub

<script type="module" src="./src/main.ts"></script>

Behavior hooksの工夫

ここからは現場で実際に見たものになります。

Behavior hookとは、jQueryをHTMLに繋げるものです。例えば今回はHTML側で<div data-jquery="calculator">と書いていて、jQuery側ではconst calculators = $("[data-jquery='calculator']")と書いています。こうやってHTMLの<div>がjQuery側からはcalculators定数でアクセスできるようになっています。

歴史的にはclassやIDをbehavior hookにすることが多く、経験が浅い人のコードの場合はこれを使っています。経験のある人が書いたコードの場合は、例えばjs-*という名前のclassにしたり、data-*属性を使ってCSSとは完全に分けています。

今回はStimulusを参考にdata-*属性を使ってみました。かなり冗長な名前にしていますが、HTMLを見ただけでどのようなbehavior hookかがわかるように工夫しています。

src/jquery/calculator.ts GitHub

const gradeButtonRegularTarget = calculator.find("[data-jquery-calculator-target='grade-selector-regular']")
const gradeButtonFirstClassTarget = calculator.find("[data-jquery-calculator-target='grade-selector-first-class']")
const passengerCountTarget = calculator.find("[data-jquery-calculator-target='passenger-count']")
const discountTarget = calculator.find("[data-jquery-calculator-target='discount']")
const unitPriceTarget = calculator.find("[data-jquery-calculator-target='unit-price']")
const totalPriceTarget = calculator.find("[data-jquery-calculator-target='total-price']")
const messageTarget = calculator.find("[data-jquery-calculator-target='message']")

スコープの限定

これも成熟した現場でよく見るスタイルです。

Reactのコンポーネントの大きな魅力は、そのコンポーネントだけに集中すれば良い点です。コンポーネントの影響範囲が明確に規定されているので、予想外の影響範囲を心配する必要がありません。子要素に対してはpropsを渡して影響を当てることができますが、いきなり完全に独立のHTML要素を操作することは御法度です。

それに対してjQueryの場合はCSSセレクタが自由に使えるので、ウェブページに表示されているどの要素にもアクセスできてしまいます。一方で優れたjQueryのコードでは、明確にスコープを制限する工夫がされています。

下記のコードでは$("[data-jquery='calculator']")を使って、jQueryコードの影響範囲を明確に規定しています。この要素の外には影響を与えないという宣言です。そしてgradeButtonRegularTargetを定義するときはcalculator.find(...)を使っています。つまりcalendar要素の子要素に限定して対象のHTML要素を探しています。

src/jquery/calculator.ts GitHub

...
const calculators = $("[data-jquery='calculator']")
...
const gradeButtonRegularTarget = calculator.find("[data-jquery-calculator-target='grade-selector-regular']")
...

このようにスコープを限定したコードは非常に読みやすくなります。ウェブページの一部だけを見れば十分だからです。それに対してスコープが不明瞭なコードは、常に全体を意識しないと不安になります。

メイン関数の構造

src/jquery/calculator.tsは意図的に下記の構造を持たせています。GitHub

  1. 先頭ではCSSセレクタを使って、このjQueryコードを起動するHTML要素およびこのjQueryコードによって変更を受けるHTML要素を定義しています。そして定数に持たせています
  2. 次のセクションではステートを定数として定義しています。ここで初期値を設定しています
  3. 次のセクションではイベントハンドラを定義しています。
  4. 最後のセクションでは、HTML要素を変更するコードをまとめています。

経験豊かな人が書いたjQueryのコードは、自然とこのような構成になっています。ステートを内部に持たずにイベントハンドラから直接HTML要素を変更する場合はもっと簡略化されていますが、それでも概ねこの構成になっています。

ステート

ステート管理はReactなどのモダンフロントエンドの専売特許ではありません。JavaScriptの場合はconst一発でステートが作れます。もちろんZustand等をインストールすれば、グローバルなステート管理も簡単にできます。

src/jquery/calculator.tsでは下記のようにステートを宣言して初期化しています。こうするとステートを中心にUIを一気に更新できます。今回のようにあっちこっちからステートを更新され、あっちこっちにステートを反映させるUIの場合は有効です。GitHub

数は多くありませんが、複雑なUIの場合はこのようなコードが使われているのを見かけます。

  const state = {
    grade: 'regular',
    passengerCount: 1,
    discount: 20,
  }

ビジネスロジックとプレゼンテーションの分離

jQueryの場合はビジネスロジックをフロントエンドに持たせることはあまりありませんが、現場でも見ることがあります。ES modulesを使えば簡単に実現できるので、意識しておいた方が良いパターンだと思います。

下記では価格計算のビジネスロジックを全てPriceクラスに押し込めて、切り分けています。今回はテストを書いていませんが、こうすることでPriceのユニットテストが簡単に書けるようになっています。GitHub

お勧めです!

src/jquery/calculator.ts

  function render() {
    ...
    const price = new Price({
      grade: gradeButtonRegularTarget.attr('aria-selected') === 'true' ? 'regular' : 'firstClass',
      passengerCount: state.passengerCount,
      discount: state.discount,
    })

    ...
    unitPriceTarget.text(price.unit())
    totalPriceTarget.text(Math.round(price.total()))
    ...
  }

最後に

いかがでしたでしょうか?

皆さんのプロジェクトの中に眠っているjQueryコードを見直して、少しリファクタリングしたり、ビルド環境を工夫する気になりましたか?カバーできていないところも多いはずなので、ぜひ手元のjQueryコードを見て、そこからリファクタリングのヒントを得るのも良いと思います。

jQueryが一番有名だったので批判が集中したのですが、実際には昔のツールが不十分だったのが原因という問題も多いです。jQueryが悪いのではなく、モダンなツールと組み合わせれば、まだまだjQueryもいけそうです。

「jQueryをReact/Vueに書き換えようぜ!」という流行はありましたが、現実には思うように進まず、意図せずに複数年にまたがる大プロジェクトになってしまってケースが多いと思います。少なくとも「jQueryを無くそうぜ!」というのは、それなりに大きいコードベースなら蜃気楼ではないでしょうか?

どこまでjQueryをリファクタリングできるか?そのとき、手元のレガシーコードの課題を果たして解決できるのかどうか?開発スピードを向上させられるのだろうか?

本記事がそれを考えるためのヒントになったのであれば、嬉しいです。

そして一応お約束として、jQueryの市場シェアがいまだに>90%であることの確認です

Discussion

ioioioioioio

素晴らしい記事と筆者のスタンス。私もフロントエンドをReactにするとかのつまらん流行で時間を無駄にしました。そんなことより今後来るよりドラスティックな変化に備えるべきだと思います。

たぬきの教祖たぬきの教祖

個人的には、フロントエンドの寿命は精々が5年とかその程度で、例えばDBの寿命などと比較すれば圧倒的に短いと思います。
それに対して長期的な視点で挑むのはズレていると感じます。
失敗例は、何故か今あるレガシーを活かしてリプレイスしようとするために、複雑にもなれば、無駄が増えているように見えます。
根本的に、バックエンドはそのままでも、フロントは数年ごとに設計から全く作り替えるべきと思います。
そう考えれば、延命措置はあまり良い効果を生まないと思っています。

NaofumiNaofumi

ありがとうございます。

先の記事でも紹介しましたが、現実問題としてフロントエンドをリプレイスするのに複数年かけているところは多く、それでも完了していないのが大半ではないかと思っています。私も現場で見ていますし、そのような現場が多いことは間違いありません。

そのようなところに対して、「フロントは数年ごとに設計から全く作り替えるべき」というのは流石に酷かなと思います。

他方で一休のように、数ヶ月でフロントエンドフレームワークを変えるところもあります。ここはフレームワークに依存しないフロントの書き方をしていたと思いますので、それが可能なのでしょう。これなら数年ごとに全面的に変えるのは現実的です。

おっしゃるように、レガシーを生かそうとするから失敗するのかと言えば、私が見ている限りではそうではないと感じます。例えばRailsにReactを載せること自体は難しくありませんし、それでフロントの開発が遅くなったとい話は聞きません。現実問題としてReact + ERB混在プロダクトは多いです。

私の感覚でいうと、一休のようにフロントエンドをフレームワーク非依存に作ってあるならリプレイスは早いと思います。そうでないのなら遅くなると思います。自分たちのプロダクトがどっちかを把握した上でリプレイス戦略を考える必要があると思います。

そして短時間ではリプレイスできないと判断したら、延命策を講じるのがビジネス的に正しいと思います。

もう一つ大切なことは、リプレイスしてもUI/UXがあまり改善しないことです。中途半端に混在しているプロダクトで日々開発をすると、これは強く感じます