ニコ生用Chrome拡張「にこさぽ」を作った話と,jQueryからReactに移行した話

フロントエンドの学習をかねて作りました. フォローしている放送中のコミュニティを一覧表示したり,自動で次枠移動に移動したりする機能があります.

chrome.google.com

スクリーンショット

https://raw.githubusercontent.com/tsuyuno/resources/master/docs/nicosapo_min.gif

f:id:yurafuca:20170525204636p:plain

f:id:yurafuca:20170525204654p:plain

f:id:yurafuca:20170525204721p:plain

f:id:yurafuca:20170525204715p:plain

機能一覧

  • フォローしている放送中のコミュニティを一覧表示
  • 自動次枠移動
  • (コミュニティ|チャンネル|番組)に自動入場
  • デスクトップ通知
  • 延長通知
  • 放送検索

使い方

README.md を参照してください.

github.com

作ったきっかけ

Chrome ウェブストアに公開されている既存の Extension が不安定だったことが動機です.

ニコ生関係の Chrome Extension にはいくつかのよく知られたものがありますが,それらは長らくアップデートされておらず,機能がただしく動作しないものがある状態です.動くものがほしい,という要望は多いようでしたし,フロントエンドの学習の題材に丁度よいと考えて開発に着手しました.


以下は技術の話です.

モダン化

動くものを作ることが最優先事項だったので,開発当初は jQuery/ES5 で開発していました.途中から学習とメンテナビリティの向上を目的に一気にモダン化しました.移行期間は 2 ヶ月程度だったと記憶しています.

  • yarn
  • Babel/ES2015
  • webpack
  • React

この記事では jQuery から React への移行した経験を実際のコードを参照しながら紹介します.yarn や webpack などの導入についての説明は他の多くの丁寧な記事にゆずります.

jQuery から React への移行

この記事では最も基本的なコンポーネントのひとつとして「ボタン」を紹介します.

1. 最初期

React 導入前の状態です.開発当初はボタンの状態遷移といえば,

  • 「自動次枠移動」(autoRedirect)ボタン

のことさえ考えていれば問題ありませんでした.牧歌的な時代ですね.ところがユーザーの要望を取り入れるにつれ,下記の3つのボタンを管理しなければならなくなりました.

  • 「自動次枠移動」(autoRedirect)ボタン
  • 「(このコミュニティに) 自動入場」(autoEnterCommunity)ボタン
  • 「(この番組に) 自動入場」(autoEnterProgram)ボタン

これらのボタンの状態を ON にする箇所のコードは下記です.

    toggleOn(buttonType) {
        const classes = {
            'autoRedirect': 'auto_redirect_button',
            'autoEnterCommunity': 'auto_enter_community_button',
            'autoEnterProgram': 'auto_enter_program_button'
        };
        const link = $('.' + classes[buttonType]).find('.link');
        $(link).addClass('toggled_on');
        $(link).removeClass('toggled_off');
        const labels = {
            'autoRedirect': '自動次枠移動',
            'autoEnterCommunity': '(このコミュニティに) 自動入場',
            'autoEnterProgram': '(この番組に) 自動入場',
        };
        $(link).text(labels[buttonType] + 'ON');
    }

満開や散華1を繰り返したり,直したい欲求よりも公開したい欲求が強かったりするとこのようなコードが残ることが知られています.

2. 下準備

コンポーネントを作るためにクラス設計をします.React におけるコンポーネントとは React.Component を継承したクラスなので,クラスに分割しさえすればコンポーネントを作るのが簡単になります.具体的におこなったことは下記です.

UIパーツに分割する

  • Button: ボタン
    • AutoRedirectButton: 自動次枠移動
    • AutoEnterCommunityButton: コミュニティへの自動入場
    • AutoEnterProgramButton: 番組への自動入場
  • InfoBar: 情報バー(放送の残り時間などを表示する)
  • Thumbnails: コミュニティ/チャンネルのサムネイル一覧
  • Thumbnail: コミュニティ/チャンネルのサムネイル
  • etc…
  • コンポーネントはあとでさらに細かく分割できる.とにかく分割するのが大事.

UIパーツの親子関係を考える

  • React はコンポーネント間のデータの受け渡しを親子間でおこなう
  • 適切に親子関係を決めるとデータの受け渡しがスムーズになる
  • 親から子へ向かって分割するのではなく,子から親へ向かってまとめあげるイメージで構成する方がうまくいく気がする
  • にこさぽでは AutoRedirectButton の親は Widgets
    • Widgets は子に AutoRedirectButtonInfoBar をもつ
  • Widgets の親は CastPage を継承したクラス
    • NormalCastPage || ModernCastPage || StandByPage

上記のコードは下記のようになりました.

export default class AutoRedirectButton extends Buttons {
  ・
  ・
  ・
  toggleOn() {
    const $link = $($(`.${this._className}`).find('.link'));
    $link.addClass('toggled_on');
    $link.removeClass('toggled_off');
    $link.text(`${this._label}ON`);
  }

  isToggled() {
    const $link = $($(`.${this._className}`).find('.link'));
    const isToggled = $link.hasClass('toggled_on');
    return isToggled;
  }

比較的マシになってきました.この時点では DOM 操作に依然として jQuery が使われています. とはいえ,この時点での目標はあくまで React.Component を意識してクラスを分けることなので jQuery が使われていてもよいこととします.

3. 現在

残る主な作業は下記です.

  • React.Component を継承したクラスに書き換える
  • コンポーネントに自身の状態(state)を保持させる
    • フィールドを2種類に大別する
      • 親から受け取るもの・自身が変更しないもの -> props
      • 親から受け取らないもの・自身が変更するもの -> state
    • DOM から状態を拾ってこない
  • render(){}を実装する

コードは最終的に下記のようになりました.

export default class AutoRedirectButton extends React.Component {
  ・
  ・
  ・
  onClick(e) {
    this.toggle();
  }

  toggle() {
    this.props.notify(!this.state.isToggled);
    this.setState({ isToggled:!this.state.isToggled });
  }

  render() {
    return (
      <span className={this._className + (' nicosapo_button')} onClick={this.onClick}>
          <a className={'link ' + (this.state.isToggled ? 'toggled_on' : 'toggled_off')}
            data-balloon={this._balloonMessage}
            data-balloon-pos={this._balloonPos}
            data-balloon-length={this._balloonLength}>
            {(this.state.isToggled ? `${this._label}ON` : `${this._label}OFF`)}
          </a>
      </span>
    );
  }

ようやく見慣れた React の外見になりました.初期と比較するとかなり見通しがよくなったのではないでしょうか.コードには示していませんが ES6 を使用する場合は contsructor 内で .bind(this) しなければならない点に注意してください2

this.props.notify(!this.state.isToggled)notifyは親コンポーネントからpropsで渡されたメソッドです3.)

何が変わったか

さて,このコードと初期のコードとの本質的な違いは何でしょう.

a. 自身の状態を自身のみが管理する

たとえば,自身が 押下状態である かは,React を使えば this.state.isToggled で管理できます.this.state.isToggled は外部から変更されることはないため,自身の state について疑心暗鬼にならずにすみます.

一方,もし最初期のコードのように hasClass('toggled_on') を使用して 押下状態である ことを判定するとどうでしょうか.DOM はグローバル変数と同等ですから .toogled_on は外部から不意に変更されてしまうかもしれません.

b. オブジェクトに対して一意のビューが出力されることが保証される

React を使用すれば className={this.state.isToggled ? 'toggled_on' : 'toggled_off'}this.state.isToggled ? 'ON' : 'OFF' のように記述すれば,任意の構造体の入力に対してかならず一意のビューが出力されることが保証されます.これはオブジェクトの状態とビューの関係を render で定義しているからです.

一方,最初期のような手法では,.toogled_on.text() の遷移の差分はプログラマが記述します.そのため 押下状態である にもかかわらず「OFF」と表示されてしまうかもしれません.

まとめ

一度に書き換えるのではなく,段階的に書き換え最後に React を導入すると比較的うまくいく気がします.「結局のところ何をすればいいのか」「何から手を付けるべきか」をイメージすることが難しい方の参考になれば幸いです.


  1. 勇者は満開を使用すると『散華』と呼ばれる現象を起こして身体の機能の一部を損失する.散華 (さんげ)とは【ピクシブ百科事典】

  2. React Without ES6 - React

  3. React ではこのように,propsで渡されたメソッドを通じて子コンポーネントstate を親コンポーネントへ通知します.そして親コンポーネントは受け取った子コンポーネントstate を必要に応じて自身の state に反映します.こうして変更された親コンポーネントstate は子コンポーネントたちの props として伝搬していきます.