DeNA DESIGN BLOG
- 実装TIPS 前編 -

WP REST API + Riot.js - SPA構築 -

デザイン戦略室フロントエンドエンジニアの吉井です。DeNA DESIGN BLOG の実装を担当させて頂きましたので、これを機に実装の際に得た知見と、コードをご紹介したいと思います。

  • SPA(Single Page Application)について
  • Riot.jsについて
  • Without jQuery
  • コンポーネントのcss
  • 採用しなかった Riot way
  • Riot.js の大きな落とし穴

なお長文に渡るため2部構成とし、後編では以下の内容を綴りたいと思います。

  • FluxライクなRiotControl
  • WP REST API と routing
  • Pomiseとアニメーション
  • Mountによるトリック

SPA(Single Page Application)について

DeNA DESIGN BLOG はSPA(Single Page Application)になっており、ページのレンダリングをjavascriptで行っています。SPAでは入力されたURLに関係なく、一定のhtmlを表示する様にサーバー設定します。表示されたhtml(javascript)はURLを解析し、URLに応じたコンテンツを表示するものだと考えて貰えれば良いかと思います。ページのリロードなくコンテンツを読み込むため、URLが変わっているのにアニメーションし続けたり、BGMを流し続けたりと、従来の手法ではできなかったUI/UXを提供することが出来ます。

SPA実装は昨今のWEBサービスのトレンドになってきており、コーポレートサイト、広告のWEBサイトでも普及してきました。SPA実装のためのフレームワークは著名なものに、Angular・React・Vueなどがあり、フロントエンド界隈では、どのフレームワークを選び、どういった手法で実装するべきなのか、というところに多くの注目が集まっています。

バックエンドのブログシステムにはWordPressを利用しています。投稿は他のWordPressで作られたブログと相違ありませんが、表示側ではプラグインでWP REST APIから投稿を取得し、themeとして用意したSPAで表示を担う、これが DeNA DESIGN BLOG のアーキテクチャです。

20160920_yoshii_img1

どのフレームワークでもそうですが、内部実装やコーディングスタイルに賛否両論あり、自身もはっきりとしたpros/consを把握しておきたいと思っていました。実装を通して得た所感としては、DeNA DESIGN BLOG の様なサイト構築には向いているのかな、と思いました。

Riot.jsではカスタムタグと呼ばれる、アプリケーション独自のタグを.tagという拡張子で定義していきます。カスタムタグには、html、cssやjavascriptを含めて書くことが出来ます。このタグをネストしたり、条件に応じて表示する(マウントする)ことで、DOMを構築します。Riot.jsに含まれているrouterがURLを解析し、マウントするタグを決めることで、SPAとして機能します。

Riot.jsの実装方法など、詳しくは公式ドキュメントや既出の記事を参照ください。本稿ではまだあまり認知されていないRiot.jsの実装テクニック、コーディングガイドラインを綴っております。また、文中には自身が感じたRiot.jsを導入するにあたり注意しなければならない、改善しなければいけない、と思う点も記載しています。これを通じてOSSとして成長を続けているRiot.jsを応援できれば幸いです。

Without jQuery

アプリケーション構築のうえで状態管理に問題が発生しやすいこと、古いブラウザを意識しなくても良くなってきたことから、フロントエンド界隈ではjQueryは外そう、という空気感が出て来ています。Riot.jsで用意されているAPIと、javascriptのnativeAPIを利用すれば問題なさそうだったので、DeNA DESIGN BLOG もjQueryは使わないことに。jQuery.animateより高速なアニメーションライブラリを使ったり、レガシーブラウザの担保のためにpollyfillで賄いながら実装しました。

$.ajaxの代替

FetchAPIを利用しました。こちらのpollyfillでFetchAPIに対応していないブラウザをサポートをしています。fetch関数はPromiseをreturnします。

投稿者はユーザ認証を経て、記事の投稿やプレビューを行います。WordPressではCSRF対策としてnonceを利用するため、投稿者はAjaxリクエストヘッダにnonceを追加する必要があります。fetch関数での追加方法は以下のとおりです。

theme/index.php
<?php
$wpApiSettings = json_encode(array(
  nonce => wp_create_nonce('wp_rest')
));
?>
<script type='text/javascript'>
  var wpApiSettings = <?=$wpApiSettings?>;
</script>
routes.js
const request = 'path/to/api/endpoint';
const params = {
  mode: 'cors',
  credentials: 'include',
  headers: { 'X-WP-Nonce': wpApiSettings.nonce }
};

fetch(request, params)
.then((response) => { return response.json() })
.then((json) => {
  console.log(json) // 認証済みのユーザに限るレスポンス内容
});

jQuerySelectorの代替

jQuerySelectorの代替として、querySelector、querySelectorAllがありますが、考慮した利用をしなければ意図しない挙動が発生するので注意が必要です。例えば以下の様な親タグがあるとします。

my-parent.tag
<my-parent>
  <div class="name">parent</div>
  <script>
	this.on('mount',function(){
      console.log(this.root.querySelector('.name').innerHTML)
    })
  </script>
</my-parent>

このタグをマウントすると結果はparentとなりますが、以下の様に子タグが追加されたとします。

my-parent.tag
<my-parent>
  <div class="name">parent</div>
  <my-child /> // 重複したclass名をもつタグをマウント
  <script>
	this.on('mount',function(){
      console.log(this.root.querySelector('.name').innerHTML)
	})
  </script>
</my-parent>
my-child.tag
<my-child>
  <div class="name">child</div>
</my-child>

このタグをマウントすると結果はchildとなってしまい、意図した挙動を得られなくなります。セレクタ名の工夫で賄おうとしても、いずれ衝突が起きる危険性があるため、この方法は避けなければいけません。この問題を解決するため、Riot.jsでは、タグのコンテキストに限った一意セレクタとして、name属性とid属性からDOMを参照することが出来ます。

my-custom-tag.tag
<my-custom-tag>
  <h1 name='title'>{ text }</h1>
  <script>
    this.text = 'タイトル文字列'
    this.title.style['background-color'] = '#ccc'
  </script>
</my-custom-tag>

コンポーネント実装では、定義したタグに将来的に子コンポーネントをマウントしたり、別の親にマウントされることを意識しなければいけません。プロダクトの成長に応じて、新しいコンポーネントが積み重なり、入り混じり、利用されていくからです。

また、id属性でも同様のことが可能ですが、id属性はページ内の一意セレクタなので利用しません。先述のとおり、そのコンポーネントが1つしか使われない、ということは将来約束されないため、ページ内に一意なセレクタはコンポーネント実装では避けるべきです。

jQueryPluginの代替

筆者は既にjQueryが導入されているプロダクトでは、実装のほとんどを自作pluginの中に閉じる様にしています。Riot.jsでもmixinを利用することで、同等の振る舞いをしてくれます。再利用生の高い機能は共通のmixinに切り出し、DRYな実装を心掛けます。

React.Componentをextendsする(継承する)様な方法がRiot.jsのカスタムタグにはありません※が、mixinで代替することが出来ます。※同等のものにriot.Tag.extends があるが現状の安定版では非推奨

my-mixin.tag
var = OptsMixin = {
  init: function() {
    console.log('hellow mixin!')
  }
}
<my-mixin>
  this.mixin(OptsMixin)
</my-mixin>

DeNA DESIGN BLOG では、babelを用いた実装をしているため、mixinを外部ファイルとして以下の様に定義しています。また、objectではなく、functionで定義しているのは、mixin適用時に引数をとることが出来るからです。

my-custom-tag.tag
import OptsMixin from './opts-mixin'

<my-custom-tag>
  <script>
    this.mixin( OptsMixin('hellow export mixin!') );
  </script>
</my-custom-tag>
opts-mixin.js
export default function(message) {
  return ({
    init() {
      // init関数は必ず実行される
      console.log(message);
    }
  })
}
hellow export mixin!

また、mixinは複数適用することが可能です。これを利用してタグ固有の実装はmixin内に実装する方針に。共通の機能を提供するmixin(jQueryPluginの代替にあたるもの)では、インターフェース関数を用意しておき、あとはタグ固有の実装箇所でインターフェースをoverrideします。各々のmixinのinit関数が上書きされることがないため、各mixinで担保しておきたいメンバはここで定義します。

my-custom-tag/view.tag
import AnimalMixin from './animal-mixin'
import ViewModel      from './view-model'

<my-custom-tag>
  <script>
    this.mixin( ViewModel('cat'), AnimalMixin('dog') );
  </script>
</my-custom-tag>
my-custom-tag/animal-mixin.js
export default function(name) {
  return ({
    init() {
      this.name = name;
      this.whoAmI();
    },
    whoAmI() {
      console.log(this.name);
    }
  })
}
view-model.js
// タグ固有の実装をタグからmixinに切り出すことで見通しが良くなる

export default function(name) {
  return ({
    init() {
      this.name = name;
      this.whoAmI();
    },
    whoAmI() {
      console.log(`my name is ${this.name}.`);
    }
  })
}
dog
my name is cat.

コンポーネントのcss

cssもコンポーネントの責任範囲で閉じる指定にしておけば、後に定義するスタイルで打ち消す様な記述を削減することができます。以下の指定は、どこが危険か察しがつくでしょうか?

_my-parent.scss
.my-parent {
  .name {
    margin: 40px;
  }
}

この指定も先述の「将来どこにマウントされ、何をマウントするのか約束されない」という観点からすると、ネストする子コンポーネントに影響を及ぼすことが予想されます。これを防ぐことは簡単で、以下の様に直下タグ指定を追記するだけで、コンポーネントのコンテキストに限定した指定をすることが出来ます。

_my-parent.scss
.my-parent {
  > .name {
    margin: 40px;
  }
}

直下タグ指定をすることでもう一つ得られる利点として、安易な名称でも影響を受ける可能性が少なくなるため、冗長なセレクタを命名しなくても良くなります。もしネストが深くなり可読性が悪くなったら、それはコンポーネントの粒度を細かくするサインです。

採用しなかったRiot way

DeNA DESIGN BLOG はレスポンシブデザインのWEBサイトです。cssのコーディングガイドを検討した際、Riot.jsのscoped css やタグ内に含めるスタイルは選択肢から外しました。Riot.jsに限った話ではないですが、css in js はレスポンシブデザインに適していないと考えています。その仕組みは、コンポートがマウントされたタイミングでheadのstyleタグに定義が蓄積されていくものです。

カスタムタグに書かれたstyleタグからでは、mediaQueryによる読み分けが出来ないため、インラインmediaQueryをタグの分だけ読みむ必要があります。これでは無駄なメモリを確保することになり、非力なスマートフォンにはエコではありません。このため、css in js は利用しない方針とし、従来どおりcssとjavascriptは別ファイルでbundleしました。

また、Riot.jsのscoped cssは、他のMVVMフレームワークでプリコンパイルされたものと比べハッシュによるscopeの生成ではなく、タグ名とそれに付随するものが付与されただけのものになっています。

webpackを利用したscoped css のハッシュ生成設定
module: {
  loaders: [ stylus-loader?localIdentName_[hash:base64:5] ]
}
app.tag
<app>
  <style scoped>
    :scope {
      max-width: 1680px;
    }
  </style>
</app>
index.html
// 該当タグがマウントされた際にheadタグ内に挿入される
<style type="text/css">
  app,
  [riot-tag="app"],
  [data-is="app"]{
    max-width: 1680px;
  }
</style>

何故これだけでscopedに出来るのでしょうか? それは次に提起している問題点に紐づいています。

Riot.js の大きな落とし穴

__Riot.jsではタグ名が重複すると後者に上書きされてしまいます。__安易なタグ名をつけてしまうと、気づかないうちに定義済みのタグが上書きされてしまい、アプリケーションを破壊してしまいます。この問題点から、ディレクトリに準じた命名規則でタグ名を決めるガイドラインで実装を進めました。

DeNA DESIGN BLOG のコンポーネントディレクトリ構成
/js/modules/
├── /header/view.tag   # <module-header>
├── /footer/view.tag   # <module-footer>
└── /aside/view.tag   # <module-aside>

筆者はプリコンパイラにwebpackとriotjs-loaderを利用していますが、プリコンパイル時のタグ名重複によるエラーハンドリングが出来れば、この問題点は解決されるかもしれません。

逆手にとると、タグ名とクラス名を同じにしてしまえば、cssでのコンフリクトが起こらなくなり、ネストの浅いセレクタでスタイルを指定することができる利点もあります。

DeNA DESIGN BLOG のスタイルシートディレクトリ構成
/css/modules/
├── /header.sccs  # .module-header
├── /footer.sccs   # .module-footer
└── /aside.sccs   # .module-aside

実装TIPS 前編 総括

コーンポーネント実装における留意点、Riot.jsにおける留意点を、導入時から検討出来ているかどうかで後の開発工数に大きく影響することがお伝え出来たかと思います。Riot.jsでは直感的で自由な実装が可能なだけに、スケールを想定しているプロダクトでは特に初期のガイドライン考慮が重要だと感じました。