LoginSignup
23
16

More than 5 years have passed since last update.

僕的なRiotプロジェクト構成のお話

Last updated at Posted at 2016-12-19

Riot.js Advent Calendar 2016 の19日目です。
今まで書いた人と似通ってたりしますが、よろしくお願いします。

<- 前日 @clown0082 さん: karma + mocha + Riot.jsでのテスト はじめの第一歩
-> 翌日 @potato4d さん: Riot.jsを通じて気軽に翻訳とOSSへの貢献を体験してみよう

はじめに

今回は、僕的なriotのプロジェクト構成についてディレクトリ構成を晒しながらざっくり説明します。
SPAしつつ、クローラー対策としてSSR(サーバーサイドレンダリング)してます。
ふんわり話す程度なのでリファレンスっていうより読み物と思っていっただけたらと。

  • 今回説明すること

    • ディレクトリ構成
    • それぞれのディレクトリが何してるかの解説
    • 使ってるライブラリ(ちょっとだけ)
  • 今回説明しないこと

    • ソースコード解説

さっそく

さっそくではありますがプロジェクトのディレクトリツリーです。

/src 
  ┣━ common
  ┃     ┣━ action
  ┃     ┃     ┣━ UserAction.js
  ┃     ┃     ┗━ ...
  ┃     ┣━ Core.js
  ┃     ┣━ Dispatcher.js
  ┃     ┣━ store
  ┃     ┃     ┣━ User.js
  ┃     ┃     ┗━ ...
  ┃     ┣━ tags
  ┃     ┃     ┣━ atoms
  ┃     ┃     ┃     ┣━ text-input.tag
  ┃     ┃     ┃     ┗━ ...
  ┃     ┃     ┣━ molecules
  ┃     ┃     ┣━ organisms
  ┃     ┃     ┗━ page
  ┃     ┃           ┣━ page-user.tag
  ┃     ┃           ┗━ ...
  ┃     ┣━ util
  ┃     ┃     ┣━ index.js
  ┃     ┃     ┗━ ...
  ┃     ┗━ ...
  ┃
  ┣━ frontend
  ┃     ┣━ main.js
  ┃     ┗━ route
  ┃          ┣━ user.js
  ┃          ┗━ ...
  ┃
  ┗━ backend
        ┣━ index.js
        ┣━ page
        ┃    ┣━ UserPage.js
        ┃    ┗━ ...
        ┣━ route
        ┃    ┣━ user.js
        ┃    ┗━ ...
        ┗━ view
             ┣━ main.jade
             ┣━ isomorphic.jade
             ┗━ ...

今回触れるものに絞っておりますが、このほかutilやlogなど必要に応じていろいろ追加して使っています。
(なんか長くてみづらいのであとで分けるかもです。)

基本構成

commonがプロジェクトの根幹になっています。基本的には普通にfluxを用いてSPAを組んでいます。frontendはクライアントサイドのルーティングを主に担っており、backendはwebサーバでルーティングとSSRをします。それぞれ詳しい話は後述します。

共有ファイルとSSR

frontendで使うもの, backendで使うもの、そしてfrontend,backend共有で使うcommonに分けてます。分岐が必要なタイミングでそれぞれnodeとbrowserの判定をいれても良かったんですが、よく忘れるのとif文が多くなって汚くみえちゃったので分けました。common以下にはブラウザでもnodeでも使えるmethodのみを用いて記述します。基本的には呼び出しもすべてcommon以下で完結するようにしますが、どうしてもfrontendのみ、serverのみで動くコードを使いたいときは、common以下にラッパーをつくりフォールバックを書いてます。
ブラウザアクセスの際のエントリーポイントはfrontend/main.jsでクライアントサイドでルーティングをして、frontend/pageをrenderingをしています。
クローラからアクセスされたときはpathごとにbackend/pageでエントリーを分けています。

Core.js

riot.jsでは、ブラウザではriot.mount, サーバではriot.renderを用いてDOMのレンダリングするため、同一のコードでは動かないので、wrapperを作ってます。それがcommonにあるCore.jsで、globalなmixinやmount前後で共通で行いたい処理など書いてます。

Core.js
export default class Core {

    ...

    mount(selector, tagName, opts) {

        // 共通処理など
        ...

        if (util.isBrowser()) {
            riot.mount(selector, tagName, opts)
        } else {
            riot.render(tagName, opts)
        }
    }

    ...
}

タグの中でも動的にprogramaticalyにmountしたかったりfluxでstoreをタグに渡したかったりがあるので、僕の場合はCoreにdispacherおよびstoreを管理させた上で、GlobalMixinですべてのタグに渡してます。

Core.js
export default class Core {

    constructor(opts) {

        this.dispatcher = new Dispatcher();

        // global mixin
        riot.mixin({core: this});

        ...
    }

    /**
     * storeを追加
     * @param store
     */
    addStore(store) {
        this.dispatcher.addStore(store);
    }

    /**
     * storeを取得
     * @return store
     */
    getStore(key) {
        return this.dispatcher.getStore(key);
    }

    /**
     * カスタムtagのmountのラッパー
     */
    mount(selector, tagName, opts) {

        // 共通処理など
        ...

        if (util.isBrowser()) {
            riot.mount(selector, tagName, opts)
        } else {
            riot.render(tagName, opts)
        }
    }
}

API Client

API ClientはSuperagentを使ってます。NodeでもBrowserでも動いてくれるので、特に気にせず使えて便利です。一点だけ、rollupを使ってbundleするときにバグるっていう罠があります(解決策求ム)。

backend

backendでは前述の通り、要はwebサーバです。expressを使っています。各routeでクローラー判定を行って、ブラウザからの場合はmain.jadeを、クローラーからの場合は/pageをrenderingし、それを用いてhtmlを作って返してます。また、riotのサーバサイドレンダリングではcssを返してくれないのと、クローラー対策でしか使用しないため、frontが作るdomとは若干異なっており、ここではrenderingされたtagを並べてるだけのhtmlがcallbackのdomに入ります。

backend/route/user.js
import UserPage from '../page/UserPage'

export default class User {

    constructor() {
        super();
        this.uri = '/user';
    }

    use(router) {
        router.route(this.uri)
            .get(this.get)
        ;
    }

    get(req, res, next) {

        if (!res.locals.bot) {
            // Browser-side rendering
            res.render("main", res.locals);
            return;
        }

        res.render("main", res.locals);
        let page = new UserPage(res.locals);
        page.render((err, data) => {
            res.render('isomorphic', {
                dom: data.dom,
                local: res.locals
            });
        });
    }
}

PageではCoreの初期化、Storeの準備を行い必要なデータをAPI叩いて取ってきてレンダリングしてます。

UserPage.js
class UserPage extends Page {

    constructor(locals) {
        super();

        this.core = new Core();
        this.core.addStore(new UserStore());
    }

    render(callback) {
        let self = this;

    Promise.all([
        new Promise((resolve, reject) => {

            // user get
            self.core.getStore('user').on('GET_USER', (state, store) => {
                resolve();
            })
            let action = new UserAction()
            action.get('imakei')
        },
    ]).then((result) => {
        let raws = [];

        // 適当に必要なものをrendering
        // header
        raws.push(self.core.mount(null, 'header-tag');

        ...

        // page本体
        raws.push(self.core.mount(null, 'page-user');

        callback(null, {
          dom: raws.join(''),
        });
    })
}

frontendとルーティング

フロントエンドのルーティングはriot-routeを用いてます。backendと構成を似させるためにfrontend/routeを作っていますがやっていることはルートに合わせてapp.tagのcontainerにmountするtagを決定しているだけです。
下記の例では(actionに隠蔽化されちゃってますが)、http://hoge.com/userにアクセスしたときにpage-userをmountするってことをします。

frontend/route/user.js
export default class UserRoute extends Route {
    constructor() {
        super();

        this.uri = '/user'
    }

    use(route, action) {
        route(this.uri, (...args) => {
            action.pageChange('page-user', args);
        });
    }
}

Component

componentですが、最近はAtomicDesignを採用することが多いです。AtomicDesignなんだよって方は AdventCalendarの7日目を書かれている79さんのこの記事とか読んだらわかると思います。AtomicDesignを採用する理由はriotにあってるっていうのとUIComponentのライブラリを使いやすいってところです。
MaterialDesignLiteを例に上げると

text-input.js
<text-input class="mdl-textfield mdl-js-textfield">
  <input class="mdl-textfield__input" name="{ opts.name }" type="text" value="{ opts.value }">
  <label class="mdl-textfield__label" for="{ opts.name }">{ opts.label }</label>

  <style scoped>
  </style>

  <script>
    this.on('mount', () => {
      componentHandler.upgradeElement(this.root);
    });
  </script>

</text-input>

こんな感じでatomとして簡単に使えるのでとてもいい感じです。

終わりに

駆け足になってしまいましたが、最近はこんな構成でriotで開発しております。あまり細かく説明してないですが、何かの参考になれば幸いです。(時間なさすぎたので後で加筆しよ。。。)
また、もし要望があればサンプルのリポジトリとかつくろうかなぁって思ってます。
"ここ詳しく知りたい"とかありましたら気軽にコメントとかメッセとかください。

最後に、遅刻さーせん。

23
16
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
23
16