JavaScript
blogger
reactjs

React初心者がコード写経する題材としてbloggerのテンプレート作り直したお話

More than 1 year has passed since last update.

bloggerのテンプレートをReactで作った私のブログ
ソースみると大体全部書いてあります。

nodejsをやったことがなく、javascriptはクロージャーがあまり理解できていない程度の人間が、興味本位でReactに手を出して痛い目にあったけど楽しいという話です。

作成前から作成後までの経緯をやった順に備忘録がてら記すよ

Reactを始める発端

QiitaでReactすげーという記事をちらほら見たので、いっちょやってみっか。
いろいろと見てみるも、なるほどわからん状態。
こういう時は実際にコード叩いてみるかと思いつつもReact実行の時点まず躓く。

ブログの記事を参考に実行するも全く動かない

ブログサイトのコードをコピペして実行するもののエラー連発でまず折れる。
>0.13からバージョンアップした際の注意事項ですが、今までReact.findDOMNode()と書いていたところも、
ReactDOM.findDOMNode()と書く必要があります。

引用元:今からはじめるReact.js〜React ver0.14〜
参考にしていたチュートリアルはReact ver0.13のものだったので読み込むライブラリのバージョンが現在のバージョン(ver0.15)だと動かないというオチ。
なんでライブラリのバージョンを最新版にした!言え!何でだ!!

何はともあれ公式のチュートリアルを最後までやりなさい(戒め

Reactの公式Tutorial(英語)
公式のチュートリアルが死ぬほどわかりやすい。
Reactは記事を読む前に公式のチュートリアルを全てやった後に読んだ方が理解度が段違いでした。
英語だからって読まずに入門記事とか見て分かった気になってると時間の無駄だって、はっきりわかんだね。

React動いた!これで何か作りたい!あっ、そうだ今のbloggerをReactにしよう(名案

Reactでbloggerのテンプレートを作りたい、SEOとかはgoogleさんは賢いっぽいから大丈夫だろ。
まずはbloggerの情報をjsonで取得したいなあと思いAPI調べるも面倒くさそうだからhtmlに書き出して、dataタグからjsで取得して連想配列にすればええやろ(最低

デザインはBulmaでやりました。

Material Design Liteは簡単でかっこいいデザインできるけどjavascriptでスクロール位置取れないし、なんか挙動がビミョいのでクビだクビだクビだ。
そこで、FlexBoxに対応していて、簡単で、なおかつ軽いCSSのテンプレートないかなーないよなーBootstrap4はまだ怖いしなー。
あったよ!FlexBox対応のCSSフレームワーク Bulmaが!
でかした!
ヘッダーフッターおいて左側にカードレイアウト、右側にボックスとタグおいて完成。
参考元:Flexboxを使って作られてるCSS framework「BULMA」で、でろぐのテーマをひさかたぶりにリニューアルしましたよ。

フッターを表示させるためにJSXで書いてみたよ

ReactでHtml表示させるくらいできらあ!→できねえ!
classをclassNameにしないとクラスは表示されないんじゃよ...
これ何回も繰り返して、その都度CSS当たってない!?アバーッ!?とかなったので備えよう

// フッターコンポーネント
// html丸出し
var Footer = React.createClass({
    render: function () {
        return (
            <footer className="footer">
                <div className="container">
                    <div className="content is-centered">
                        <a id="rss-button" target="_blank" href="/feeds/posts/default?alt=rss" className="icon sns-icon">
                            <i className="fa fa-rss"></i>
                        </a>
                        <atarget="_blank" href="https://plus.google.com/115125673113897175058/" className="icon sns-icon">
                            <i className="fa fa-google-plus"></i>
                        </a>
                        <a target="_blank" href="https://twitter.com/the_larch88/" className="icon sns-icon">
                            <i className="fa fa-twitter"></i>
                        </a>
                        <a target="_blank" href="https://thelarch88.tumblr.com/" className="icon sns-icon">
                            <i className="fa fa-tumblr"></i>
                        </a>
                        <a target="_blank" href="https://www.pinterest.com/thelarch88/" className="icon sns-icon">
                            <i className="fa fa-pinterest"></i>
                        </a>
                        <a id="instagram-button" target="_blank" href="https://instagram.com/thelarch88/" className="icon sns-icon">
                            <i className="fa fa-instagram"></i>
                        </a>
                        <a id="flickr-button" target="_blank" href="https://www.flickr.com/photos/thelarch88/" className="icon sns-icon">
                            <i className="fa fa-flickr"></i>
                        </a>
                        <a id="youtube-button" target="_blank" href="https://www.youtube.com/channel/UCNf7rqtmn0hnSFHxGMpPGDw/" className="icon sns-icon">
                            <i className="fa fa-youtube"></i>
                        </a>
                    </div>
                </div>
            </footer>
        );
    }
});

ヘッダーに検索ボックスおいてStateの使い方を覚えたよ

inpuに入力された値をonChangeで監視してHeaderRightSideの持つstateに更新続ける、
検索ボタンがクリックされた際に記事情報の取得と表示更新行う(後述)
ここは唯一と言っていいくらい順調に終わった、チュートリアルまんまだろ、その通りですね。

// ヘッダーレフトサイドコンポーネント
// ロゴ表示するだけ
var HeaderLeftSide = React.createClass({
    render: function () {
        return (
            <div className="header-left">
                <a className="header-item" href="/" data-url="/" onClick={this.props.getItem}>
                    <img
                        src="https://2.bp.blogspot.com/-KVtyn3vF69Q/VzAPFgOpO2I/AAAAAAAM7zs/No0iC2ThgZUNiRTTTycSymGEJSe4oltCgCLcB/s1600/tsuirakulogo.png"/>
                </a>
            </div>
        );
    }
});

// ヘッダーライトサイドコンポーネント
// いわゆる検索ボックスだよ
// 検索キーワードをステートで保持しといて、
// 検索ボタン押されたら親から持ってきたの関数を実行する
var HeaderRightSide = React.createClass({
    // Reactの組み込み関数
    // stateの初期値設定する
    getInitialState() {
        return {
            text: ''
        };
    },
    // inputの値監視関数
    onChange: function (e) {
        this.setState({text: e.target.value});
    },
    // DOM作ってくれる関数
    render: function () {
        var url = "/search?q=" + this.state.text;
        return (
            <div className="header-right header-menu is-active">
                <div className="header-item">
                    <p className="control has-addons">
                        <input className="input"
                                     onChange={this.onChange}
                                     value={this.state.text}
                                     type="text"
                                     placeholder="Find a post"/>
                        <button className="button"
                                        data-url={url}
                                        onClick={this.props.getItem}>Search
                        </button>
                    </p>
                </div>
            </div>
        );
    }
});

// ヘッダーコンポーネント
// ヘッダーに必要なのまとめるコンポーネント
var Header = React.createClass({
    render: function () {
        return (
            <header className="header">
                <div className="container">
                    <HeaderLeftSide getItem={this.props.getItem}/>
                    <HeaderRightSide getItem={this.props.getItem}/>
                </div>
            </header>
        );
    }
});

2カラム右側の人気の投稿とラベルを並べるのでforが使いたい

Reactでコンポーネント繰り返しするときはforとかじゃなくて、チュートリアルではmapで書いてあったのでそうする。
>常に 配列の中でコンポーネントに直接提供されるべきで、その配列の中でそれぞれのコンポーネントのHTMLの子要素の入れ物に提供されるべきではありません。
引用元:React | 複数のコンポーネント
mapとかreduceがよく分かってないけどみんなforよりそっちがいいよって書いてあるからそうしてるけど、なんでそうなのかが分かってないからいつか事故りそう。

値を渡そうとするとthisにpropsが存在しませんとエラーが出てきた。

スコープ変わっているので var self = this;で乗り切る、
これいいのかわからないけどこれ以上に簡単な方法思いつかないのでそのまま。
JavaScriptの「this」は「4種類」??
この記事読んだけど未だにthisがよくわからない、よく事故る。

keyがないよってReactさんが教えてくれたのでググる

>keyにはそのリストの中で必ずユニークになる値を指定する必要があります。なのでkeyにする値の候補としてはユーザー一覧におけるユーザーIDなどになります。
引用元:React.jsの地味だけど重要なkeyについて
そっかー←わかってない。

JSXに直接スタイル書いたらエラー出た

>また、直接コンポーネントのスタイルを指定したい場合はstyleを指定することもできます。

<button className="btn btn-default" style={{color:"gray"}}/>

のように、{{}}で囲んで指定することになります。
引用元:今からはじめるReact.js〜スタイルの適用〜

// 人気の投稿単体
var PopularPost = React.createClass({
    render: function () {
        // スタイルをJSXに直接書くとエラー出るのでオブジェクトにして書く
        var img_style = {width: "72px", height: "72px"};
        return <div className="box">
            <article className="media">
                <div className="media-left">
                    <figure className="image is-72x72"
                                    onClick={this.props.getItem}
                                    data-url={this.props.popular_post.href}>
                        <img src={this.props.popular_post.thumbnail}
                                 style={img_style}
                                 alt={this.props.popular_post.snippet}/>
                    </figure>
                </div>
                <div className="media-content">
                    <div className="content">
                        <a href={this.props.popular_post.href}
                             onClick={this.props.getItem}
                             data-url={this.props.popular_post.href}>{this.props.popular_post.title}</a>
                    </div>
                </div>
            </article>
        </div>;
    }
});

// 人気の投稿コンポーネント
var PopularPosts = React.createClass({
    render: function () {
        var self = this;
        return <div>
            {Array.prototype.map.call(this.props.popular_posts, function (e, i, arr) {
                return <PopularPost key={e.id} popular_post={e} getItem={self.props.getItem}/>;
            })}
        </div>;
    }
});

// ラベル単体
var Label = React.createClass({
    render: function () {
        // ラベルに記事数があれば表示する
        // 記事一覧のラベルには記事数あるけど、記事単体のラベルにはないから
        var label_text = this.props.label.name;
        if (typeof this.props.label.count !== "undefined") {
            label_text = label_text + '(' + this.props.label.count + ')';
        }
        return <li className="label tag is-medium is-info"
                             onClick={this.props.getItem}
                             data-url={this.props.label.url}>
            <a href={this.props.label.url}>{label_text}</a>
        </li>;
    }
});

// ラベルコンポーネント
var Labels = React.createClass({
    render: function () {
        var self = this;
        return <ul className="labels">
            {Array.prototype.map.call(this.props.labels, function (e, i) {
                return <Label key={i} label={e} getItem={self.props.getItem}/>;
            })}
        </ul>;
    }
});


// ライトサイドコンポーネント
// 人気の投稿とラベルをまとめるコンポーネント
var ContentsRightSide = React.createClass({
    render: function () {
        return (
            <div className="column right-side">
                <p className="block-title">
                    <svg fill="#42afe3" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
                        <path
                            d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/>
                        <path d="M0 0h24v24H0z" fill="none"/>
                    </svg>
                    人気の投稿
                </p>
                <PopularPosts getItem={this.props.getItem} popular_posts={popular_posts}/>
                <p className="block-title">
                    <svg fill="#42afe3" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
                        <path d="M0 0h24v24H0z" fill="none"/>
                        <path
                            d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16zM16 17H5V7h11l3.55 5L16 17z"/>
                    </svg>
                    ラベル
                </p>
                <Labels getItem={this.props.getItem} labels={labels}/>
            </div>
        );
    }
});

記事一覧では画像を動的読み込みでstateを更新し、表示に適応するよ

bloggerで渡される画像URLは72x72のサムネイルなのでカードレイアウトに使おうとするには無理がある。
そこでbloggerでは画像URLに画像サイズが書いてあり、書き換えると画像サイズも変わります。
その仕組み利用して、最初は転送容量の少ないサムネイルを表示させて、
本命の重い画像はonloadのタイミングでstate書き換えて、
サムネイルの上にバックグラウンドでかぶせた。
ここ作った時自分は賢いなあとか思ってたけど改めて見るとどこでもやってそう。

// 記事一覧の単体
// bloggerで取得できる画像は実際の画像ではなく
// URL上の数値が画像の大きさなので
// url差し替えで72x72から横幅640x*の画像へ変更して、
// 画像読み込み後にバックグラウンドに指定し、画像のz-index下げる
var Item = React.createClass({
    getInitialState() {
        return {
            url: this.props.item.thumbnailUrl,
            isLoad: false
        };
    },
    render: function () {
        var self = this;
        // 72x72から横幅640x*の画像URLに変更
        var img_url = this.props.item.thumbnailUrl.replace("/s72-c/", "/w" + 640 + "/");
        // 画像を読み込んだらstate更新
        var imageObj = new Image();
        imageObj.onload = function () {
            self.setState({
                url: img_url,
                isLoad: true
            });
        };
        imageObj.src = img_url;
        // 画像とバックグラウンドのスタイルをstateで画像の読み込み完了したら書き換える
        var imgStyle = {};
        var backgroundStyle = {};
        if (this.state.isLoad) {
            imgStyle = {
                zIndex: "-1"
            };
            backgroundStyle = {
                backgroundImage: 'url(' + this.state.url + ')',
            };
        }
        return (
            <div className="card card--original">
                <div className="card-image">
                    <figure className="image is-4by3"
                                    style={backgroundStyle}
                                    data-url={this.props.item.url}
                                    onClick={this.props.getItem}>
                        <img src={this.props.item.thumbnailUrl}
                                 alt={this.props.item.snippet}
                                 style={imgStyle}/>
                    </figure>
                </div>
                <div className="card-content">
                    <div className="media">
                        <div className="media-content">
                            <a href={this.props.item.url}
                                 className="title is-5"
                                 data-url={this.props.item.url}
                                 onClick={this.props.getItem}>{this.props.item.title}</a>
                        </div>
                    </div>
                    <div className="content">
                        <p>{this.props.item.snippet}</p>
                        <br/>
                        <time>{this.props.item.timestamp}</time>
                        <span className="button is-info"
                                    data-url={this.props.item.url}
                                    onClick={this.props.getItem}>記事を読む</span>
                    </div>
                </div>
            </div>
        );
    }
});

記事単体ではJSONのHTMLを表示させたいけど、そのまま値を書き出すとエスケープされた

>dangerouslySetInnerHTMLで入れ込みます。
引用元:react.js HandleBars Angular.js HTML-escapesさせない値の表示方法
bloggerでマークダウンとか考えたけど、画像登録が面倒になりそうだからやめた。

// 記事単体
var Article = React.createClass({
    render: function () {
        return (
            <article className="article">
                <section className="hero is-info">
                    <div className="hero-content">
                        <div className="container">
                            <h1 className="title">{this.props.item.title}</h1>
                            <time className="subtitle">{this.props.item.timestamp}</time>
                        </div>
                    </div>
                </section>
                <div className="article-body content"
                         dangerouslySetInnerHTML={{__html: this.props.item.body}}></div>
                <Labels labels={this.props.item.label} getItem={this.props.getItem}/>
            </article>
        );
    }
});

記事一覧と記事を同じコンポーネントで、stateの値で切り替えするよ

記事単体と記事一覧用の参照staetは同じなので、値によってコンポーネントを切り替えたい。
ざっくり言うとbloggerではpage_typeがitemの時は記事表示でそれ以外は一覧表示になる、
そこでpage_typeに応じてコンポーネントを切り替える。
ついでにナビゲーションもコンポーネントで追加した。

// ナビゲーターコンポーネント
// いわゆる次へ前へ
var Navigator = React.createClass({
    render: function () {
        // 次へ前へのリンク先があれば表示する
        var newerStyle = {display: "none"};
        if (this.props.blog_data.newerPageUrl) {
            newerStyle = {}
        }
        var olderStyle = {display: "none"};
        if (this.props.blog_data.olderPageTitle) {
            olderStyle = {}
        }
        return (
            <div className="navigator columns">
                <a className="column"
                     href={this.props.blog_data.newerPageUrl}
                     onClick={this.props.getItem}
                     data-url={this.props.blog_data.newerPageUrl}>
                    <p className="notification is-info"
                         style={newerStyle}>{this.props.blog_data.newerPageTitle}</p>
                </a>
                <div className="column"></div>
                <a className="column"
                     href={this.props.blog_data.olderPageUrl}
                     onClick={this.props.getItem}
                     data-url={this.props.blog_data.olderPageUrl}>
                    <p className="notification is-info"
                         style={olderStyle}>{this.props.blog_data.olderPageTitle}</p>
                </a>
            </div>
        );
    }
});

// レフトサイドコンポーネント
// いわゆるツーカラムの左側
// global_dataのpage_typeがitemの時は記事表示に差し替え
// それ以外の場合には記事一覧を表示させる
var ContentsLeftSide = React.createClass({
    render: function () {
        var self = this;
        if (this.props.global_data.pageType === "item") {
            // 記事単体
            return (<div className="column is-three-quarters blog-left-side">
                {Array.prototype.map.call(this.props.posts, function (e, i) {
                    if (i == 0) {
                        return <Article key={e.id} item={e} getItem={self.props.getItem}/>;
                    }
                })}
                <Navigator blog_data={this.props.blog_data} getItem={this.props.getItem}/>
            </div>);
        } else {
            // 記事一覧
            return (<div className="column is-three-quarters blog-left-side">
                {Array.prototype.map.call(this.props.posts, function (e, i) {
                    return <Item key={e.id} item={e} getItem={self.props.getItem}/>;
                })}
                <Navigator blog_data={this.props.blog_data} getItem={this.props.getItem}/>
            </div>);
        }
    }
});

state更新で画面遷移せずに表示を更新したい

Reactの真骨頂というかやりたいことがようやく目の前まで来た。
クリックイベントでリンク先のURLを親に渡して、ajaxでリンク先を取得し、パースした値でstateを更新をやりたい。

子のイベントを親へ送りstateを書き換えたい

そもそも、この考えが間違ってた。
>子は使うだけで管理しているのは親
引用元:React.jsでPropやStateを使ってComponent間のやりとりをする
親が関数作って子に渡して使わせてあげると親で実行されるから親の持ってるstateもそこで変更すればいいよね。
この考え当たり前だけど,理解してコードにかけるまでかーなーりー時間がかかった。

画面遷移しないでurl書き換えて、ブラウザ戻る進むボタン押したら前のstateに戻したい

>pushStateを使ってURLを書き換えた場合ブラウザバックで戻れるのですが、ちゃんとブラウザバックのイベントを取得して処理してあげないとURLが書き換わるだけでなにも起きません。イベントを処理をするには popstate で対応します。

$(window).on('popstate', function(e) { <ブラウザの戻る、進むで処理したい内容> }

引用元:javascriptを使ったSEO対策まとめ

画面更新したらスクロール位置が変わらず画面遷移感無いと寂しい

スクロール位置初期値に戻せばええんやで。

window.scrollTo(0, 0);

event.targetでデータタグ取れない時がある

event.currentTargetでイベントがバインドされてるDOM取りましょう。
event.curretTargetとevent.targetの違い

// コンテンツコンポーネント
// ヘッダーフッター以外をまとめる
var Contents = React.createClass({
    render: function () {
        return (
            <section className="section">
                <div className="container card-container">
                    <div className="columns">
                        <ContentsLeftSide
                            getItem={this.props.getItem}
                            posts={this.props.posts}
                            global_data={this.props.global_data}
                            blog_data={this.props.blog_data}/>
                        <ContentsRightSide getItem={this.props.getItem}/>
                    </div>
                </div>
            </section>
        );
    }
});

// 全てのコンポーネントをまとめるレンダリングコンポーネント
var Index = React.createClass({
    componentDidMount(){
        var self = this;
        // ブラウザの進む戻るボタンで移動先のurlでstate更新する
        $(window).on('popstate', function (e) {
            var ajax_url = location.pathname;
            $.ajax({
                    type: "GET",
                    url: ajax_url
                })
                .done(function (data) {
                    // 取得した値をhtmlパースして値を取得して、整形関数にかける
                    var state = {
                        global_data: getGlobalData($(data).find("#js-data")[0]),
                        blog_data: getBlogData($(data).find("#js-blog")[0]),
                        posts: getPosts($(data).find(".js-post"))
                    };
                    // 値更新すると画面も更新されるよ
                    self.setState(state);
                    // ブラウザと違って現在のスクロール位置で画面切り替わると画面更新したことはわかって楽しいけど、
                    // ブログを見るときはなんか変な感じするのでスクロール位置をリセットする
                    window.scrollTo(0, 0);
                })
                .fail(function (jqXHR, textStatus, errorThrown) {
                    console.log("ajax_error");
                    console.log(textStatus);
                });
        });
    },
    getInitialState() {
        return {
            // Blogger共通の値
            global_data: global_data,
            // ブログの値
            blog_data: blog_data,
            // 記事の値
            posts: posts
        };
    },
    // 子でクリックがあったらdata-urlのリンク先をajaxで取得して
    // htmlでパースして整形してstataを更新する関数
    getItem: function (e) {
        // aタグとかデフォルトの挙動をさせない
        e.preventDefault();
        var self = this;
        // リンク先を取得
        var ajax_url = e.currentTarget.getAttribute("data-url");
        $.ajax({
                type: "GET",
                url: ajax_url
            })
            .done(function (data) {
                // URL欄を更新先に書き換え
                history.pushState(null, self.state.global_data.pageTitle, ajax_url);
                // 取得した値をhtmlパースして値を取得して、整形関数にかける
                var state = {
                    global_data: getGlobalData($(data).find("#js-data")[0]),
                    blog_data: getBlogData($(data).find("#js-blog")[0]),
                    posts: getPosts($(data).find(".js-post"))
                };
                // 値更新すると画面も更新されるよ
                self.setState(state);
                // ブラウザと違って現在のスクロール位置で画面切り替わると画面更新したことはわかって楽しいけど、
                // ブログを見るときはなんか変な感じするのでスクロール位置をリセットする
                window.scrollTo(0, 0);
            })
            .fail(function (jqXHR, textStatus, errorThrown) {
                console.log("ajax_error");
                console.log(textStatus);
            });

    },
    render: function () {
        return (
            <div>
                <Header getItem={this.getItem}/>
                <Contents
                    getItem={this.getItem}
                    global_data={this.state.global_data}
                    blog_data={this.state.blog_data}
                    posts={this.state.posts}/>
                <Footer />
            </div>
        );
    }
});

ReactDOM.render(
    <Index/>,
    document.getElementById('content')
);

作った感想

楽しかった(小並感

新しいことを始めるストレスからの出来た時の達成感は何よりの快感です。
サーバー建てるの面倒だからbloggerにインラインで全て書き出すのはどうなのかとか、
nodejsを使わずreactだけで作ることの意味はあったのかとか、
APIちゃんと使えばもっと高速になるだろとか、
そもそもjavascriptをよくわかってないだろお前などのツッコミはありますが、
初めてのreactでbloggerのテンプレートを作った馬鹿の経緯が知りたいという奇特な人は参考にしてもらえればと思います。

また、ページの速さと軽さがここまで体感で感じられるとは思いませんでした。
いくらバーチャルDOMすごいからって、javascriptでフルで叩くと重いだろうなとか勝手に思ってましたがすみませんでした。

こうなってくるとReduxやnodejsにも興味出てきたので次はそっちで遊んでみたいと思います。

独学で書いているんで見た人が勘違いする変なとこあったら教えていただけたらと思います。