LoginSignup
8

More than 5 years have passed since last update.

Chrome 拡張で React + Redux 使ってみたよ

Posted at

最近、不動産広告のキャッチコピーを見るのがマイブームな tearoom6 ですw

概要

さて、2年ぶりくらいにフロントエンドをキャッチアップした内容を、備忘録を兼ねて書き綴ります。というか、備忘録に過ぎません・・・。。
タイトルと裏腹に内容が広範になっております。メインとしては React + Redux の記事になります。誤りがあれば、ご指摘頂けると嬉しいですmm

材料として、2年前くらいにオレオレ流 Angular1 で作った Chrome 拡張を同一仕様で書き直すことにしました。Google Drive のファイル・フォルダに簡単にアクセスする機能をもったもので、書き直した後のソースコードを以下に置いています。

技術的には、以下のようなものを使用しています。
一応、 Chrome 拡張ならではのところは、デバッグがやりづらいのと、Build スクリプトを結構カスタムで作っているところとかですかね〜

  • ES2015
  • Yarn 0.16.1
  • React 15.3.2
  • Redux 3.6.0
  • webpack 1.13.3
  • gulp 3.9.1
  • babel 6.5.2

ES2015

JS の2015年版仕様。もうこれについては、巷に情報が溢れているので、あえて細かく書くことはないでしょう。
他の選択肢としては TypeScript / CoffeeScript を使う等があるかと思います。
また Flow を使った型チェックを行うのもありですが、この程度の規模ならまず要らないかなと。

行末に;を付けるか否か問題

JS の仕様的には、 ES2015 であろうと、;を付けて、文を分けるのが安心安全なはずです。ただ、普段 Ruby とか Python を書いていると、この;は鬱陶しく感じてくるんですよね。
改行を挟んでいる場合、;を明示的に付けなくてもいい場合が圧倒的に多いので、ここに:付ける派と付けない派が生まれるわけです。
Redux のオフィシャルチュートリアルは付けていないですね。ちゃんと分かって付けない分にはいいんじゃないでしょうか。ということで、今回は付けずに書いてみました。

Arrow 関数

ES2015 で書けるようになった Arrow 関数は、 JS の function 書きまくる問題から開放してくれる素敵な存在です。
ただし、注意しなければならないのが、 Arrow 関数と通常の関数は全く同じではないという事実です。というのは this の意味が両者で異なるというもので、以下のように従来よく使われた jQuery を使った $(this) としてイベントの発生源を得るコードは、通常の関数でなければ機能しないという事になります。(あるいは、このような書き方は避けるべきか...)

$('#item-list').scroll(function() {
  const scrollTop = $(this).scrollTop()
  //...
})

Promise

Redux との絡みで、 非同期処理部分で Promise オブジェクトを使う箇所がありました。ベーシックな構成は以下のようになります。

return new Promise((resolve, reject) => {
  // ... (何らかの時間のかかる処理) (API呼び出しなど)

  // コールバックの中で resolve (正常系) or reject (異常系) を呼び出す
  // 引数複数渡したいときは配列に入れる
  resolve(params)
  reject(params)
}).then( (params) => {
  // 正常系の場合
  // resolve が呼び出された時点で、 then 内部が呼ばれる
  // 引数も受け取れる
}).catch( (params) => {
  // 異常系の場合
  // reject が呼び出された時点で、 catch 内部が呼ばれる
  // 引数も受け取れる
})

Yarn

package.json がそのまま使えるので、 NPM からの移行は非常に簡単でした。 Yarn の方が速いので、基本的に乗り換えて良いのではないかと思います。
ただ、依存性の問題か、 gulp-imagemin というモジュールがうまく機能せず、 Yarn のバージョンを上げると解決したということがありました。安定するまでは移行は慎重に行ったほうがいい場合もあるかもしれません。

Build (gulp + webpack + babel)

Build スクリプトは Chrome 拡張機能特有のクセのある処理も行いたいため、汎用性のある gulp を引き続きベースに利用しました。
(単純に通常のwebアプリで React 使うには create-react-app を使うのが簡単だと思います)

JS のビルドに関しては、後述の CSS Modules を使いたいなどの理由もあり、 webpack に任せることに。gulp から webpack を呼び出すために gulp の webpack プラグインである webpack-stream を利用しました。

また、 ES2016 ベースのコードをビルドするため、 webpack の Babel プラグインである babel-loader を使いました。 JSX を含む React コードのコンパイルも Babel に任せました。

さらに webpack で CSS Modules (CSS の JS での import) を利用するため、 style-loader + css-loader を使いました。

まとめると、こんな感じ。

gulp
  ->webpack (webpack-stream)
    ->babel (babel-loader)
      ->babel-preset-es2015
      ->babel-preset-react
    ->CSS Modules (style-loader + css-loader)

React + Redux

平たく言えば、 React は web アプリを再利用可能な部品に分けて整理して書くもの、 Redux は Flux アーキテクチャに基づいてデータの流れを一方向に流すことでデータの整合性を保つもの、でしょうか。別に React や Redux でなくてもいい訳ですが、一応代表選手ということでこれらを使うことにします。

ディレクトリ構成

まず、ディレクトリ構成を以下のように分けました。

  • actions
  • reducers
  • components (Presentational Components = Component の見た目)
  • containers (Container Components = Component の動き)

極論を言えば、 JS のファイルは分けずに1つでもいいわけですが、ファイルを分けることで役割がはっきりして見通しが良くなります。

上記の分類方法のうち、特に components と containers は必ずしも分けなくてもいいかなーとは思いましたが、分けておくことで "How things look" と "How things work" がはっきり区別できる効果はありそうです。

References

では、1つ1つみていきます。

actions

Redux アプリケーション上で起こりうる何らかの状態変化 (action) を定義します。action 関数の戻り値には、type 属性を持ったオブジェクトを返します。(というか、このオブジェクト自身を action と呼びます)
type 属性以外にも任意のデータを action に詰め込むことが可能です。

const showItems = (itemType, items) => {
  return {
    type: TYPE_SHOW_ITEMS,
    itemType,
    items
  }
}

action 関数の戻り値 (つまり action) を dispatch の引数にして呼び出すことで、 action を引き起こすことが可能です。

dispatch(showItems(itemType, items))

action は後述の reducer で受け取ることができます。

非同期 action を定義する場合には、後述のミドルウェアとして、redux-thunk を利用するのがデファクトスタンダードになっているようです。
すると、 action オブジェクトを返す代わりに、以下のように dispatch を引数を持つ関数を返す感じで書けます。

function asyncAction() {
  return (dispatch) => {
    // ... (非同期処理)
    // callback で dispatch を呼び出す
    dispatch(increment());
  };
}

reducers

reducers は state (現在の状態) および action を受け取って、次の状態を返す関数として定義します。 action の前後で、状態がどう変化するかを定義するわけです。
reducers は複数定義可能で、それぞれは、単一の state に対してどう変化するかだけを記載すれば OK です。たとえば、ボタンを押しているかどうかとか、表示しているアイテムのリストとか、それらが1つ1つ state として定義されることになります。

const items = (state = [], action) => {
  switch (action.type) {
    case actions.TYPE_SHOW_ITEMS:
      return action.items

    case actions.TYPE_SHOW_ADDITIONAL_ITEMS:
      return state.concat(action.items)

    default:
      return state
  }
}

複数定義した reducers は、最後に combineReducers でまとめ上げて、createStore で Store (全ての状態を管理するところ) に登録します。

const reducer = combineReducers({
  items,
  activeItemType,
  resetAuthDisabled,
  isLoading
})

const store = createStore(
  reducer,
  applyMiddleware(...middlewares)
)

components

Component の見た目を定義するところで、ようやく React を使った部分になります。 JSX による記述をするのはここになります。

React と Redux の連携を簡単に行うため React Redux を用いることにします。

この場合、もっとも単純な React Component は単なる関数として定義可能です。

const Menu = ({ [properties...] }) => (
  // ... JSX
)

ただし、 componentDidMount などのライフサイクルフックを利用したい場合は、 React.Component を extends したクラスとして定義します。

class ItemList extends React.Component {
  componentDidMount() {
    // ... 初期化処理
  }

  render() {
    const { [properties...] } = this.props
    return (
      // ... JSX
    )
  }
}

Component が持つプロパティの型等について定義することもできますが、これは必須では無さそう。

Menu.propTypes = {
  onMenuClick: PropTypes.func.isRequired,
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired
}

containers

containers では、 Component の動きの部分を組み合わします。
mapStateToProps および mapDispatchToProps の2つの関数 (どちらか片方でも良い) を定義して、これを connect により先程定義した component と組み合わせてやります。

const SearchBoxSet = connect(
  mapStateToProps,
  mapDispatchToProps
)(SearchBox)

mapStateToProps は、Store から state を受け取って、 Component の状態に反映を行う処理、 mapDispatchToProps は逆に Component でイベントが起こった場合に、 dispatch を呼び出して action を発生させる処理になります。

なお、定義した components / containers はさらに階層的に組み合わせて上位の components / containers を作り、最終的に最上位の component を render します。

Middlewares

Redux におけるミドルウェアは、いわゆる Plugins 的な、便利機能を提供してくれる存在です。
Store を生成する時に一緒に渡してあげます。
今回は、先述した thunk の他、 dev 環境の場合に、redux-logger を入れて、開発時のデバッグをやりやすくしました。

const middlewares = [thunk]
if ('production' !== process.env.NODE_ENV) {
  const logger = createLogger({
    level: 'info',
    duration: true,
    diff: true
  })
  middlewares.push(logger)
}
const store = createStore(
  reducer,
  applyMiddleware(...middlewares)
)

備考

React / Redux で

  • Adjacent JSX elements must be wrapped in an enclosing tag というエラーが起きた場合、"Component のトップレベル (一番親となる階層) の要素を一つにする必要がある" という制約に違反しているので、修正する必要があります

  • エラーではないけど、 Each child in an array or iterator should have a unique "key" prop という warning が出る場合、Component 内のリスト要素に一意の key 属性を持たせていない場合に出力されるので、 key 属性を追加してあげます

    • レンダリングの際のキャッシュ利用のために使われるみたいです
    • Lists and Keys

本番環境にする!

  • NODE_ENV をセットせずに使うと、以下のような warnings が出ました。
  Warning: It looks like you're using a minified copy of the development build of React. When deploying React apps to production, make sure to use the production build which skips development warnings and is faster. See https://fb.me/react-minification for more details.

ナウいやり方に移行しきれなかったポイント

  • 今回 jQuery や Bootstrap を従来通り使ったのですが、 ES2015 の import の枠組みではやらず、 CDN を使って global に load しました。できないことはないんでしょうが、結構大変そうなのを感じたので、そこは折れました。
  • また、 gapi も初期化処理の中の load 時に onload パラメータとして global なコールバック関数を指定する必要があり、これは window 配下に置くことで global スコープに配置しました。

CSS Modules

React によって HTML と JS を含めた Component 化が達成できているのだから、そこに CSS も含めたいと思うのは当然です。
CSS のファイルをインポートを JS からできるようにするのが CSS Modules という考え方です。
他の選択肢としては、 BEM や SMACSS などの考え方でスタイルを整理したり、 Sass や Less でネストさせてスタイルを整理するなどがあるかと思いますが、 CSS Modules は React との親和性が高いんじゃないかと。

先述した通り、 webpack で CSS Modules 実装をビルドするには css loader (および style loader) を用います。
Browserify で CSS Modules 実装をビルドするには css-modulesify を用います。

References

(画像とかもこの考えでやる方法がきっとあるんだろうけど、やる必然性が無いので調べてません(キリッ))

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
8