未熟学生エンジニアのブログ

TetsuFeの個人開発ブログ

TetsuFeはテツエフイー と読みます。FlutterやWeb周り全般、チーム開発について語るブログ

SPA・サーバレスハンズオン part3 React/Firebase Cloud Firestoreでチャットアプリを作る

シリーズ

前提

  • part1part2をやっている
  • html,javascriptを一度でも書いたことがある
  • プログラミングの「クラス」と「継承」を使ったことがある
  • Firebaseに登録している

今回できるようになること

  • Reactの基本の流れがわかる
  • Reactのstate管理の方法がわかる
  • Firestoreと連携してデータベースを使ったチャットを作れる
  • 次にやるべきことがわかる

チャットアプリを作る

今回はチャットアプリを作ります。

f:id:swiftfe:20190501071018p:plain

f:id:swiftfe:20190501073412p:plain

今回のアプリは以下の記事のFirestore版のようなものになります。

サンプル

デモページ: https://huit-tetsufe-0506.firebaseapp.com/ ソースコード: https://github.com/TetsuFe/huit_react_handson_20190506

参考

FirebaseのFirestoreを使うための初期設定をする

FirebaseにはHosting以外にもいろんな便利機能があります。

Cloud Firestore(Firestore)は、Firebaseのデータベースサービスで、web上にデータを保存したり、取得したりするのに使えます。

Firestoreの立ち位置としては以下の図のようになります。

f:id:swiftfe:20190501120118p:plain

f:id:swiftfe:20190501120121p:plain

さて、Hostingの際と同じように、Firebaseのセットアップをしましょう。

Firebaseのコンソール画面から、Firestoreを選択します。

f:id:swiftfe:20190429132020p:plain

f:id:swiftfe:20190429132026p:plain

f:id:swiftfe:20190429132031p:plain

これでひとまず初期設定は完了です。

今回はテストモードとして誰でもデータの改変が行えるようにしましたが、これは非常に危険です。Firestore側での認証設定やセキュリティルールは適切に設定しておかないとデータの改変が自由に行えてしまいます。(今回のように重要なデータでない場合は問題ありません)

reactページ側からFirestoreにアクセスするためのモジュールをインストールします。

$ npm install firebase --save

FirestoreにアクセスするためにはapiKeyなどの情報が必要です。

Firebaseの画面から、APIキーとprojectidをコピーします。これはサイトに埋め込んでも大丈夫です。

参考:Firebase apiKey ってさらしていいの? ほんとに? - Qiita

f:id:swiftfe:20190429133406p:plain

f:id:swiftfe:20190429132949p:plain

上の例では、以下のようにファイルを作成します。(値は全員少し違うはずです)

firebaseフォルダを作り、その中にconfig.jsを作りましょう。

firebase/config.js

export const firebaseConfig = {
    apiKey: 'AIzaSyBTXINX3dCCSCQcwypUxk7wqRiEYbxqgw0',
    projectId: 'huit-tetsufe-0506'
};

フォルダの構成は以下のようになっているはずです。

huit_handson_20190506
├── firebase
│   └── config.js

さらに、firebase/index.js というファイルを作り、データベースを操作するためのfirebaseDbというインスタンスを他のファイルでも使い回せるようにします。

firebase/index.js

import firebase from 'firebase';
import { firebaseConfig } from './config.js';

export const firebaseApp = firebase.initializeApp(firebaseConfig);
export const firebaseDb = firebaseApp.firestore();

フォルダの構成は以下のようになっているはずです。

huit_handson_20190506
├── firebase
│   └── config.js
│   └── index.js

Reactとコンポーネント

Reactは、コンポーネントという単位で表示する要素を小さなモジュール(ファイル)に分割することを推奨しています。

要は要素ごとに細かく分けるだけです。難しいことではありません。分ける単位もとりあえずは適当でいいでしょう。

コンポーネントを作る前に、srcフォルダの中に、componentsフォルダを作成しましょう。

componentsというフォルダ名に意味はありません。なぜ作る必要があるのでしょうか?

単にわかりやすいようにフォルダわけしただけです。ただ、これは地味に重要なことです。画面が複雑になればファイル数も多くなり、ファイルはどんどん見つけにくくなるからです。複雑になっていけば、componentsの中にさらにフォルダを作るなどする必要が出てくると思います。

src/components/Message.jsというファイルを作りましょう。

import React from "react";

export default class Message extends React.Component {
    render() {
        return (
            <div className="Message">
                <img className="" src={this.props.message.profile_image} />
                <div className="">
                    <p className="">@{this.props.message.user_name}</p>
                    <p className="">{this.props.message.text}</p>
                </div>
            </div>
        );
    }
}

さらに、src/components/ChatForm.jsというファイルを作りましょう。

import React from "react";

export default class ChatForm extends React.Component {
    render() {
        return (
            <div className="ChatForm">
                <div className="">
                    <input name='user_name' onChange={this.props.onTextChange} className="" placeholder="名前" />
                    <input name='profile_image' onChange={this.props.onTextChange} className="" placeholder="プロフィール画像URL" />
                </div>

                <textarea name='text' className="" onChange={this.props.onTextChange} />
                <button className="" onClick={this.props.onButtonClick}>送信</button>
            </div>
        );
    }
}

こんな感じの構造になっているはずです

$ tree src
src
├── App.css
├── App.js
├── App.test.js
├── components
│   ├── ChatForm.js
│   └── Message.js
├── index.css
├── index.js
├── logo.svg
└── serviceWorker.js

src/App.jsを編集して、MessageコンポーネントとChatFormコンポーネントを使いましょう。

import React from 'react';
import './App.css';
import { firebaseDb } from './firebase/index.js'
import Message from './components/Message.js'
import ChatForm from './components/ChatForm.js'

const messages = firebaseDb.collection('messages')

class App extends React.Component {
  constructor(props) {
    super(props);
    this.onTextChange = this.onTextChange.bind(this)
    this.onButtonClick = this.onButtonClick.bind(this)
    this.state = {
      text: "",
      user_name: "",
      profile_image: "",
      messages: []
    }
  }

  componentDidMount() {
    messages.onSnapshot
      ((querySnapshot) => {
        // クエリが非同期処理のため、この中にsetStateなどを書かないと空になってしまう
        let msgs = []
        querySnapshot.forEach((doc) => {
          // 新しい順に取得される
          const m = doc.data();
          msgs.push({
            'text': m.text,
            'user_name': m.user_name,
            'profile_image': m.profile_image,
          })
        });
        this.setState({
          messages: msgs
        });
      });
  }

  onTextChange(e) {
    if (e.target.name == 'user_name') {
      this.setState({
        "user_name": e.target.value,
      });
    } else if (e.target.name == 'profile_image') {
      this.setState({
        "profile_image": e.target.value,
      });
    } else if (e.target.name == 'text') {
      this.setState({
        "text": e.target.value,
      });
    }
  }

  onButtonClick() {
    // 簡単なバリデーション
    if (this.state.user_name == "") {
      alert('user_name empty')
      return
    } else if (this.state.text == "") {
      alert('text empty')
      return
    }
    messages.add({
      "user_name": this.state.user_name,
      "profile_image": this.state.profile_image,
      "text": this.state.text,
    })
      .then(function (docRef) {
        console.log("Document written with ID: ", docRef.id);
      })
      .catch(function (error) {
        console.error("Error adding document: ", error);
      });
  }

  render() {
    return (
      <div className="App">
        <div className="App-header">
          <h2>Chat</h2>
        </div>
        <div className="MessageList">
          {this.state.messages.map((m, i) => {
            return <Message key={i} message={m} />
          })}
        </div>
        <ChatForm onTextChange={this.onTextChange} onButtonClick={this.onButtonClick} />
      </div>
    );
  }
}

export default App;

Reactのコードを読み解く

ここからはコードの解説に入ります。

src/App.js を見てください。src/App.jsでは、Appというクラスが宣言されています。今回の場合はこれだけです。ここからはAppクラスについて解説します。

まず、Appクラスですが、これはReact.Componentというクラスを継承する形で宣言されています。

import React from 'react';
// 省略

class App extends React.Component {
// 省略
}

React.Componentを継承する理由とはなんでしょうか。どういう意味を持っているかこれから説明します。

React.Componentを継承すると起こること

  • 1.<App/>などと書くことでReactのコンポーネントとして好きな場所に埋め込むことができる
  • 2.ライフサイクルメソッドを自動的にいいタイミングで実行してくれる
    • componentDidMound()
    • render()
    • その他

1. <App/>などと書くことでReactのコンポーネントとして好きな場所に埋め込むことができる

src/index.jsで、<App/>として使われています。React.Componentを継承すると、Reactのコンポーネントとして扱うことができます。これはとりあえずそういうルールなのだと覚えましょう。(おまじないが嫌い?ならばこれを読みなさい。React.Component – React

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
// 省略

ReactDOM.render(<App />, document.getElementById('root'));

/// 省略
serviceWorker.unregister();

「好きな場所に埋め込むことができる」とはどういうことでしょうか。これはAppクラスのrender()内を見ると意味がわかります。

以下の<ChatFrom>もReact.Componentを継承しています。(src/components/ChatForm.jsを見ればわかります)

src/App.js

// 省略
class App extends React.Component {
  // 省略
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <h2>Chat</h2>
        </div>
        <div className="MessageList">
          {this.state.messages.map((m, i) => {
            return <Message key={i} message={m} />
          })}
        </div>
        <ChatForm onTextChange={this.onTextChange} onButtonClick={this.onButtonClick} />
      </div>
    );
  }
}

以上のように書くことで、<ChatFrom><div className="App-header">よりも下になるように配置できます。

f:id:swiftfe:20190501034849p:plain

2. ライフサイクルメソッドを自動的にいいタイミングで実行してくれる

1はまあそういうものか、という感じだと思ってもらえればOKです。2の方が重要です。

React.Component を継承している Appクラスには、componentDidMound()などの、あるタイミングで自動的に実行されるメソッドがあります。

これらの あるタイミングで自動的に実行されるメソッド群を、ライフサイクルメソッドといいます。

モバイルアプリやフレームワークを使ったフロントエンド開発では同じような概念が出てくるので、ここでライフサイクルメソッドというものの存在に慣れておくとReact以外を使う際に対応しやすいです。

Reactのライフサイクルメソッドの流れは、以下のようになります。

https://github.com/iktakahiro/react-component-lifecycle-diagram/blob/master/react-v16.2/react-component-lifecycle.png?raw=true

引用元:GitHub - iktakahiro/react-component-lifecycle-diagram

初期化とマウントに関するライフサイクルメソッド

参考:React コンポーネントのライフサイクルイベント その1 - React 入門

Appの初期化の際、まず最初に自動的に実行されるのがconstructor(props)というメソッドになります。

constructor(props)は、Appが作成されたときに一回だけ実行されます。このメソッドでは、this.stateなどの変数の初期化を主に行います。

src/App.js

class App extends React.Component {
  constructor(props) {
    super(props);
    this.onTextChange = this.onTextChange.bind(this)
    this.onButtonClick = this.onButtonClick.bind(this)
    this.state = {
      text: "",
      user_name: "",
      profile_image: "",
      messages: []
    }
  }
  // 省略
}

次がrender()です。render()は返り値を持ちます。returnで値を返していることに気をつけてください。

render()は、htmlを模したjsxという記法で書かれたオブジェクトを返します。つまり、これはhtmlではありません。jsxという独特の記法で、これはjavascriptのオブジェクトです。<div className="App">は、htmlではありません。javascriptです。

class App extends React.Component {
  // 省略
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <h2>Chat</h2>
        </div>
        <div className="MessageList">
          {this.state.messages.map((m, i) => {
            return <Message key={i} message={m} />
          })}
        </div>
        <ChatForm onTextChange={this.onTextChange} onButtonClick={this.onButtonClick} />
      </div>
    );
  }
}

とはいっても、とりあえずは以下のように書けばよいと考えていればOKです。

class App extends React.Component {
  // 省略
  render() {
        return (
          // ここにhtmlっぽいjsxというものを書く
        );
  }
}

jsxの書き方は、色々書いてみてなれるしかないと思います・・

次がcomponentDidMount()です。今回は、Firestoreからのデータ取得と、React内のthis.stateの更新をしています。(実際には、データが更新された際にその処理をするという意味になります)

src/App.js

class App extends React.Component {
  // 省略
  componentDidMount() {
    messages.onSnapshot
      ((querySnapshot) => {
        // クエリが非同期処理のため、この中にsetStateなどを書かないと空になってしまう
        let msgs = []
        querySnapshot.forEach((doc) => {
          // 新しい順に取得される
          const m = doc.data();
          msgs.push({
            'text': m.text,
            'user_name': m.user_name,
            'profile_image': m.profile_image,
          })
        });
        this.setState({
          messages: msgs
        });
      });
  }
  // 省略
}

Reactの状態管理:setStateとライフサイクルメソッド

ここまででAppクラスの初期化の際の流れを知ることができたと思います。ここからは、ReactのsetStateとライフサイクルメソッドの関係について説明します。

Reactを使ったWebアプリでは、ユーザのアクションやDBのデータの変更に応じて変数(状態)を更新し、画面(ページ)を適宜書き換えるということを行います。例えば、チャット中に新しい投稿があった時、それを画面に反映するという場合をイメージしてください。

このような場合に使うのが、setStateメソッドです。

this.setState({
      messages: msgs
});

先ほどの図を再掲します。

https://github.com/iktakahiro/react-component-lifecycle-diagram/blob/master/react-v16.2/react-component-lifecycle.png?raw=true

引用元:GitHub - iktakahiro/react-component-lifecycle-diagram

ここで、setStateを実行した時、図の中央の黒丸の「state changed」が起こり、ライフサイクルメソッドが順に実行されます。図中ではshouldComponentUpdateなどのメソッドがありますが、今回はrender()だけ考慮すればOKです。

簡潔に言えば、setStateは、変数(状態)を変更した上で、render()などのライフサイクルメソッドを起動し、画面を更新するためのメソッドです。

src/App.jsをみてみましょう。

ここでは、チャットのDBデータが更新されたとき、データに合わせて画面を更新するという例を考えます。

  1. setStateは、firestoreから受け取ったデータmsgsを使ってthis.state.messagesを更新
  2. その後、setStateに反応したライフサイクルメソッドrender()が自動的に実行され、更新されたthis.state.messagesを使ってメッセージリストを画面に表示することができる

setStateはcomponentDidMount()内で実行されているように見えますが、実際にはcomponentDidMount()が呼ばれたタイミングとsetStateが実行されるタイミングは違う(ややこしいですが)のに注意してください。

class App extends React.Component {
  // 省略
  componentDidMount() {
    messages.onSnapshot
      ((querySnapshot) => {
        // クエリが非同期処理のため、この中にsetStateなどを書かないと空になってしまう
        let msgs = []
        querySnapshot.forEach((doc) => {
          // 新しい順に取得される
          const m = doc.data();
          msgs.push({
            'text': m.text,
            'user_name': m.user_name,
            'profile_image': m.profile_image,
          })
        });
        this.setState({
          messages: msgs
        });
      });
  }
  // 省略
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <h2>Chat</h2>
        </div>
        <div className="MessageList">
          {this.state.messages.map((m, i) => {
            return <Message key={i} message={m} />
          })}
        </div>
        <ChatForm onTextChange={this.onTextChange} onButtonClick={this.onButtonClick} />
      </div>
    );
  }
}

Firestoreに保存するデータ構造

Firestoreに保存するデータ構造としては、messagesという箱(FirestoreではCollectionといいます)を用意して、その中にメッセージを追加していく形になります。以下がデータの例になります。

{
    "messages": [
        {
            "1": {
                "profile_image": "https://pbs.twimg.com/profile_images/1064542710327963648/eN983mCr_400x400.jpg",
                "text": "こんにちは",
                "user_name": "僕"
            }
        },
        {
            "2": {
                "profile_image": "http://hyohyolibrary.com/wp-content/uploads/2018/07/oregairuzoku9thumbnail.png",
                "text": "やっはろー",
                "user_name": "由比ヶ浜結衣"
            }
        }
    ]
}

Firestoreのaddメソッドを使うことで投稿処理を実装します。(注意:addメソッドの場合、新しいデータはcollectionの中で雑な順番で保存されます。全てのメッセージを一気に取得する際、順番はaddした順にはならないということです。チャットなどの場合、上から新しい順に表示するか上から古い順に表示するかという順番は重要です。例えばLINEの場合は上から古い順に表示ですよね。order_byを使うなどすることで、この問題は解決できます。https://firebase.google.com/docs/firestore/query-data/order-limit-data?hl=ja

src/App.js

  onButtonClick() {
    // 簡単なバリデーション
    if (this.state.user_name == "") {
      alert('user_name empty')
      return
    } else if (this.state.text == "") {
      alert('text empty')
      return
    }
    messages.add({
      "user_name": this.state.user_name,
      "profile_image": this.state.profile_image,
      "text": this.state.text,
    })
      .then(function (docRef) {
        console.log("Document written with ID: ", docRef.id);
      })
      .catch(function (error) {
        console.error("Error adding document: ", error);
      });
  }

フォームのボタンを押したとき、onButtonClicked()が呼ばれて(関数が実行されることを関数が呼ばれるという言い方をします)、onButtonClicked()の中でFirestoreのaddメソッドによってDBにデータを追加します。

DBデータの変更に応じたリアルタイム更新

チャットの場合、相手からの返事が来たときに自動的にページに反映されると嬉しいですよね。Firestoreには、それを実現できる機能があります!

FirestoreではonSnapshotメソッドを使うことで、DB上のデータがアップデートされたとき、自動的に接続中のクライアントのデータにも反映することができます。(getメソッドではできません)

ちなみに内部ではwebsocket通信をしているようです

参考:https://firebase.google.com/docs/firestore/query-data/listen?hl=ja

src/App.js

class App extends React.Component {
  // 省略
  componentDidMount() {
    messages.onSnapshot
      ((querySnapshot) => {
        // クエリが非同期処理のため、この中にsetStateなどを書かないと空になってしまう
        let msgs = []
        querySnapshot.forEach((doc) => {
          // 新しい順に取得される
          const m = doc.data();
          msgs.push({
            'text': m.text,
            'user_name': m.user_name,
            'profile_image': m.profile_image,
          })
        });
        this.setState({
          messages: msgs
        });
      });
  }
  // 省略
}

stateとprops

参考:React における State と Props の違い - Qiita

最後に、stateとpropsについて説明します。といっても、以下の2点を覚えておけばとりあえず問題ありません。

  • state: クラス内でsetStateを使って変更・更新したい変数として使う(更新される)
  • props: 他のコンポーネント(親コンポーネント)から受け取った変数(更新されない)

またsrc/App.jsの例をみてみます。

class App extends React.Component {
  constructor(props) {
    super(props);
    this.onTextChange = this.onTextChange.bind(this)
    this.onButtonClick = this.onButtonClick.bind(this)
    this.state = {
      text: "",
      user_name: "",
      profile_image: "",
      messages: []
    }
  }

  componentDidMount() {
    messages.onSnapshot
      ((querySnapshot) => {
        // クエリが非同期処理のため、この中にsetStateなどを書かないと空になってしまう
        let msgs = []
        querySnapshot.forEach((doc) => {
          // 新しい順に取得される
          const m = doc.data();
          msgs.push({
            'text': m.text,
            'user_name': m.user_name,
            'profile_image': m.profile_image,
          })
        });
        this.setState({
          messages: msgs
        });
      });
  }

  render() {
    return (
      <div className="App">
        <div className="App-header">
          <h2>Chat</h2>
        </div>
        <div className="MessageList">
          {this.state.messages.map((m, i) => {
            return <Message key={i} message={m} />
          })}
        </div>
        <ChatForm onTextChange={this.onTextChange} onButtonClick={this.onButtonClick} />
      </div>
    );
  }
}

ここで、stateとして使われているのはtext, user_name, profile_image, messagesです。this.state.text, this.state.user_nameのようにアクセスすることができます。これら変数の値はこのクラス内で更新されます。そのため、このクラス内でthis.stateとして宣言します。これがstateです。

constructor(props) {
    super(props);
    this.onTextChange = this.onTextChange.bind(this)
    this.onButtonClick = this.onButtonClick.bind(this)
    this.state = {
      text: "",
      user_name: "",
      profile_image: "",
      messages: []
    }
  }

一方で、propsは、他のコンポーネントから受け取った変数のことを指します。もっというと、渡す側はstateとして扱っている変数が、渡された側ではpropsになります。

            <div className="MessageList">
              {this.state.messages.map((m, i) => {
                return <Message key={i} message={m} />
              })}
            </div>

この3行目のmessage={m}という部分で、変数の受け渡しが起こっています。Messageコンポーネント側から見ると、messageというpropsを受け取っています。

7行目をみてください。this.props.messageという形で使っていることがわかると思います。

src/components/Message.js

import React from "react";

export default class Message extends React.Component {
    render() {
        return (
            <div className="Message">
                <img className="" src={this.props.message.profile_image} />
                <div className="">
                    <p className="">@{this.props.message.user_name}</p>
                    <p className="">{this.props.message.text}</p>
                </div>
            </div>
        );
    }
}

propsとstateを分ける意味

propsはあくまで受け取るだけで、その値は受け取ってから変更されることがありません。これは、コードを読むときに変更されうる値かどうかがわかりやすくなるという効果があります。言い換えれば、そのコンポーネント内にpropsを更新する処理が書かれていないことを示します。propsの更新にバグがあると感じたときは、親コンポーネントを見ればいい、ということに瞬時に気づくことができます。

Firebase Hostingを使ってデプロイ(web公開)する

せっかくチャットを作ったので、友達や家族とチャットができるようにweb上に公開しましょう!

前提:Firebase Hostingの初期設定をpart1でやっていること

ターミナルを開いて、以下を実行します。($は省略してください。)

$ npm run build
$ firebase deploy --only hosting

これでみんなとチャットができます!

おまけ:CSSを書いてみる

ここまでのサンプルコードはかなりひどい見た目になっていると思います。

f:id:swiftfe:20190501071018p:plain

これをまともな見た目にして、恥ずかしくないようにします。

参考

src/components/Message.js

import React from "react";

export default class Message extends React.Component {

    render() {
        const style = {
            "item": {
                display: "flex",
                alignItems: "flex-start",
                marginBottom: "0.8em",
            },
            "itemImage": {
                width: "100px",
                height: "100px",
                objectFit: "cover",
                borderRadius: "20px",
            },
            "itemName": {
                textAlign: "left",
                fontSize: "75%"
            },
            "itemMessage": {
                position: "relative",
                display: "inline-block",
                padding: "0.8em",
                background: "#deefe8",
                borderRadius: "4px",
                lineHeight: "1.2em",
            }
        }

        return (
            <div className="Message" style={style.item}>
                <img style={style.itemImage} src={this.props.message.profile_image} />
                <div className="">
                    <p style={style.itemName}>@{this.props.message.user_name}</p>
                    <p style={style.itemMessage}>{this.props.message.text}</p>
                </div>
            </div>
        );
    }
}

f:id:swiftfe:20190501073307p:plain

「chat」という文字がデカすぎるので、なんとかします。

src/App.css

.App {
  text-align: center;
}

.App-header {
  background-color: #282c34;
  min-height: 10vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: white;
}

min-height: 10vh としたので、小さくなりました。

f:id:swiftfe:20190501073412p:plain

画面が大きいと不恰好ですが、疲れたのでここまでにします。後はcss職人のみなさんの頑張りに期待します。cssむずいです。

ちなみに、CSSは以下の資料がわかりやすいので一度見てみるといいと思います。

speakerdeck.com

saruwakakun.com

Next Step Hint 1: 複数ページのサイトを作る

今回作ったサイトは一ページだけでした。複数のページにまたがっているパターンが普通だと思います。

react-routerというライブラリを使えば、SPAでも複数のページを表現できます。

qiita.com

Next Step Hint 2: デザインをGoogleっぽくする

Google系のサイトで使われているMaterial Designというデザインルールに準拠したコンポーネントを使って見た目をいい感じにすることもできます。Youtubeなどを見るとイメージがわきやすいかと思います。

The world's most popular React UI framework - Material-UI というライブラリを使えば、簡単にMaterial Designなサイトをつくれます。

例えばボタンは以下のリンクに作り方がのってます。

material-ui.com

導入方法は以下を参考にすると良いかと思います。

rennnosukesann.hatenablog.com

公式サイトに色々なパーツの使い方が載っているので、色々試してみると面白いと思います。

f:id:swiftfe:20190501064044p:plain

Next Step Hint 3: Firestoreを使ってDBの処理に慣れる

以下の記事がわかりやすそうでした。

React.js Firebase Tutorial: Building Firestore CRUD Web Application

シリーズ

SPA・サーバレスハンズオン part2 一般的なwebページとReactアプリの構成を知る

シリーズ

Webページの主な構成要素

ブラウザでは主にhtml・cssjavascriptの3種類のファイルを扱う

  • HTML
    • ページの構造を決める。ブラウザ処理の起点になり、必要なcssやjsはここから読み込む
  • CSS
    • ページのレイアウト・色などを設定する
  • JavaScript(JS)
    • ページを書き換える、計算や通信などをするプログラムを実行する

Reactとは

Reactは、状況に応じて動的に書きかわるようなページを書きやすくするためのJavaScriptフレームワークです。使えばなんとなくわかります。

なぜReactを使うのか?

こちらの記事にまとめました。気になる方は読んでみてください。

なぜReact.jsを使うのか、jQueryやVue.jsとの違いをいまさら調べてみた - 未熟学生エンジニアのブログ

create-react-appのデフォルトコードについて

create-react-appをした場合、テンプレートコードが生成されます。

src、publicフォルダの中にあるコードが重要です。

$ tree src
src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── serviceWorker.js
$ tree public
public
├── favicon.ico
├── index.html
└── manifest.json

起点はsrc/index.jsです。create-react-appで作ったreactアプリは、まずこのindex.jsが実行されます。

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

ここで、以下の部分でAppというモジュールがインポートされています。要は、他のファイル(./App.js)にあるAppというモジュールをこのindex.jsでも利用できるようにしているということです。

import App from './App';

そして、以下の行でAppを利用して処理を走らせます。

ReactDOM.render(<App />, document.getElementById('root'));

これは、Appをrootというidを持つDOM要素の中に挿入するという意味になります。どういうことでしょうか?

実は、public/index.html というwebページのベースになるHTMLファイルがあります。

Appは、以下のテンプレートHTMLの中の <div id="root"></div><div id="root"></div>の間に挿入されます。

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

Appが、<div id="root"></div>の間に挿入されることがわかりました。

では、Appとはなんなのか?その答えはsrc/App.jsにあります。なぜここに答えがあるとわかったと思いますか?なぜsrc/App.jsというファイル(答え)を探しあてることができたと思いますか?順をおって説明します。

React.jsを使ったプログラムは、基本的には単にjavascriptで書かれたプログラムです。javascriptがわかっていれば、答えにたどり着くことができます。

index.jsでのAppのインポートの方法を思い出してください。

src/index.js

import App from './App';

この構文は、「./App(./App.js)というファイルの中から、Appというモジュールをインポートする」という意味になります。

つまり、Appは./App.jsの中にあるということがわかり、Appの詳細を知りたければ./App.jsというファイルをみればいいということがわかります。これはReact.js特有の話ではなく、javascript一般に通じる話です。

ちなみに、./App.jsは、src/App.jsという意味です。(src/index.jsファイルの中では、.はsrcを意味します。ただし、これはファイルパスの一部として.が使われたときに限った話で、例えば先ほど出てきた以下のような例の場合は、.は別の意味になります。)

src/App.jsをみればいいということがわかったので、見ていきましょう。

src/App.js

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

若干語弊はありますが、AppはApp()のことだったと考えてください。

ここで、App()は返り値として<div>で囲まれたオブジェクトを返しています。

実は、この<div>で囲まれた部分はreactのコード上ではjsのオブジェクトです。これを変換して、最終的にはHTMLの<div>タグと同じ意味のものになります。

ここで一度、ターミナルからサーバーを立ち上げて、ブラウザを見てみましょう。

$ npm start

以下のような画面が表示されるはずです。

f:id:swiftfe:20190430000840p:plain

ページを書き換えてみる

では、"Edit src/App.js and save to reload."を"Hello, my original app!"という文面に変えるには、どうしたらいいでしょうか?

f:id:swiftfe:20190430000932p:plain

先ほどのsrc/App.jsに、"Edit src/App.js and save to reload."という文字があるはずです。これを変えましょう。

src/App.js

        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>

以下のように変えます

        <p>
          Hello, my original app!
        </p>

編集して保存したら、ブラウザをみてみましょう。(実は、Reactを使うとブラウザのリロード操作がいりません!自動で更新されます。)

f:id:swiftfe:20190430001315p:plain

やりました!これでReactアプリを自分好みに編集していくことができるようになりました!

まとめ

  • javascriptの文法を読み解き、編集すべきファイルsrc/App.jsにたどり着けた
  • src/App.jsを編集すれば、自分のオリジナルアプリを書いていける
  • ベースとなるファイルindex.htmlに、Appというコンポーネントを挿入する形で要素を追加している

シリーズ

なぜReact.jsを使うのか、jQueryやVue.jsとの違いをいまさら調べてみた

巷の人はなぜReact.jsを使っているのか理解するため、他のJavaScript(js)ライブラリ・フレームワークとの違いなどを調べてみました。

最初にまとめ

  • SPAというページの一部を更新するタイプの構成が流行りだよ
  • Reactは仮想DOMを使うからページの一部を更新するのが速い
  • Reactは宣言的記述ができるから複雑なコードでも読みやすい。間違いにくい
    • jQueryは複雑になると読みにくい
  • 個人開発ではVue.jsとReact.js好きな方を使えば良い
    • Vue.jsも上の同じ特性を持ってるよ

なぜjsが必要なのか?

そもそもjsはなぜ必要なのでしょうか?答えは単純です。ユーザのアクション(入力・クリックなど)やDBのデータ更新などに応じて動的にページを書きかえたいからです。

SPAを作りたい

SPA(シングルページアプリケーション)ではページ遷移を行わず、ページの遷移・更新はjsによるdom(jsで操作できる、HTMLを表現するオブジェクト)操作による差分更新により擬似的に行うという特徴があります。これによりサーバーからのリクエストを最初の一回だけで全てやってしまうのでその後の動作が軽量になるというメリットがあります。

近年はこのSPAの導入によるwebページのパフォーマンスアップが常識になってきているということがReact.js導入の動機となっています。

ここで、なぜSPAならReact.jsが使われるのか、という話になると思います。先ほど述べたようにSPAはページの遷移・更新はjsによるdom操作による差分更新により擬似的に行います。

Reactは、この差分更新を適切にやってくれる+更新処理をわかりやすく書けるため、SPAのweb開発で用いられているのです。以下では、その仕組みについてお話しして行きたいと思います。

参考

仮想DOMという概念

Reactでは、仮想DOMという方法を使い、今までのDOM操作をより効率よく行うことで高速化を実現しています。

ともかく、仮想DOMというものを使ってそこを経由してDOM操作を行うことで、高速になるらしいです。詳しいことはわかりませんが、そういうことです。

ページの一部だけを更新するのがJavaScriptの基本の処理なので、そこが早くなるのは当然嬉しいというわけですね。要は高速な処理ができるからReactはいいぞということ。

参考: VirtualDOMの仕事ってなに?(Reactの表示速度がはやい理由) - Qiita

従来:jQueryなどの手続き型のDOM操作

React.jsでは「宣言的記述」がキーワードとしてあります。jQueryなどの手続き型の記述と違っています。

「宣言的記述」では、テンプレートに変数を書いておき、その変数の値の変更に応じて自動的にレンダリングされます。

「手続き型の記述」では、DOMを直接操作し、例えばリストの値を一つ追加する際には以下の様な手続き型のプログラムを書きます。

「宣言的記述」の例

引用:Reactを使うとなぜjQueryが要らなくなるのか - Qiita

// ItemListのコンポーネント定義(実体は関数)
const ItemList = props => {
    return <ul className="item-list">
        {props.items.map(item => <ItemDetail item={item} />)}
    </ul>;
};

// ItemDetailのコンポーネント定義
const ItemDetail = props => {
    const item = props.item;
    return <li className={'item' + item.stock === 0 ? ' soldout' : ''}>
        <div className="item-name">{item.name}</div>
        <div className="item-price">{item.price}</div>
    </li>;
};

fetch('/api/items').then(res => res.json()).then(data => {
    ReactDOM.render(
        <ItemList items={data.items} />, // これを
        document.getElementById('container') // ここにレンダリングしろ
    );
});

「手続き型の記述」の例

引用:Reactを使うとなぜjQueryが要らなくなるのか - Qiita

$.getJSON('/api/items').then(data => {
    const ul = $('ul.item-list').empty();
    data.items.forEach(item => {
        const li = $('<li>').addClass('item').appendTo(ul);
        if (item.stock === 0) li.addClass('soldout');
        $('<div>').addClass('item-name').text(item.name).appendTo(li);
        $('<div>').addClass('item-price').text(item.price).appendTo(li);
    });
});

一見、宣言的記述の方が記述量が多いだけのようにも思えるかもしれませんが、宣言的記述では見た目がほぼHTMLで、正しい構造であることを確認しやすいのです。

手続き型の記述の場合、とてもじゃないですがHTMLには見えません。複雑なコードになった際、正しい構造であることを確認しづらくなり、間違ったHTMLタグを混入させてしまう可能性などがあり、バグを生みやすくなります。

こういった点で宣言的記述が有利であり、複雑化しやすいフロントエンドのコードを管理しやすくしています。

類似フレームワークVue.jsとの違い

他のフレームワークとの比較 — Vue.js によると、Vue.jsとReact.jsとの違いはそれほど大きくなさそうです。

大きな違いとしては、htmlの書き方にあります。ReactはJSX、VueはTemplatesという書き方を利用することになります。

我らがReact様のJSXのメリットとしては、JSXはhtmlのように見えるjsであり、jsとしてhtmlを書くことができることです。ただし、見た目はHTMLとは少し違いますし、CSSもJSXに組み込むように書くことが多いので、慣れていない人にとっては慣れるまで時間がかかります。

VueのTemplatesのメリットとしては、HTML・CSSはほぼ普通のそれと同じように書くことができる点です。とにかく、とっつきやすい。

[React] JSXの例

引用:ReactとVueってどう違う?全く同じアプリをReactとVueで作成してみて分かった相違点 | コリス

createNewToDoItem = () => {
    this.setState( ({ list, todo }) => ({
      list: [
          ...list,
        {
          todo
        }
      ],
      todo: ''
    })
  );
};


handleInput = e => {
  this.setState({
    todo: e.target.value
  });
};


<input type="text" 
       value={this.state.todo} 
       onChange={this.handleInput}/>

[Vue] Templatesの例

引用:ReactとVueってどう違う?全く同じアプリをReactとVueで作成してみて分かった相違点 | コリス

<script>
createNewToDoItem() {
    this.list.push(
        {
            'todo': this.todo
        }
    );
    this.todo = '';
}
</script>

<template>
  <input type="text" v-model="todo"/>
</template>

個人開発においてはどちらを使っても大差はないでしょう。

参考

Flutterメモ 番外編 GitHubとCIツールの連携(Codemagic)

今回やること

  • CodemagicとGitHubを連携させてFlutterアプリをビルド
  • slack通知連携
  • iOSアプリAdHoc配信
  • Readmeにバッジをつける

Codemagicを選んだ理由

CIツールを使ってみたかったんですが、調べてみるとCodemagicというCIツールが評判がいいらしい。どうやらFlutterに特化しているらしく、競合(Flutterにおいては後発っぽい)のBitriseよりもいろいろ充実してるっぽい。

ただ、ビルドが若干遅いらしく、対応中とのことらしいです。

今回はこのCodemagicの方を試してみることにしました。

GitHubリポジトリを用意

今回はこのリポジトリを用意しました。今回はPublicリポジトリですが、CodemagicはPrivateでも使えるっぽいです。

https://github.com/TetsuFe/spring_app_2019github.com

Codemagicへの登録

以下のリンクから登録します。

codemagic.io

GitHub連携認証を使ったのですが、登録が完了すると同時に以下のような画面に。

f:id:swiftfe:20190423061550p:plain

CIを導入したいリポジトリに対して「Start your first build」ボタンを押すと、なんといきなりビルドが始まりました!

f:id:swiftfe:20190423061618p:plain

簡単すぎる・・(他のCIツールもそうだったかも?)

トリガー

このままでは手動でCIを回さないといけないので、トリガーを設定します。

App Settingsから、Build Triggerを展開します。

以下のように設定できます。

  • ブランチ名
  • タイミング
    • push時(Trigger on every push)
      • artifacts(appの生成)はあり
    • pull requestのupdate時(Trigger on pull request update)
      • artifacts(appの生成)はなし

f:id:swiftfe:20190424014144p:plain

また、説明は割愛しますが、走らせる処理をブランチごとに分岐させたい時は、workflow自体を複数作ることで可能らしいです。

参考:CodemagicでFlutterアプリのビルド・配信をする — iOS編 – Flutter 🇯🇵 – Medium

この記事の「トリガー」を参照してください。

slack連携

設定画面(App Settings?)を開いて、publishの項目を展開すると、以下の様にslack連携の項目が現れます。

f:id:swiftfe:20190423070320p:plain

連携に成功すると、以下の様にチャンネルと通知のトリガーを選択します。

f:id:swiftfe:20190423070427p:plain

次回のビルドから、以下のようにslack通知が来るようになります!ビルドは結構時間がかかるので、通知を設定しておくと便利そうです。

f:id:swiftfe:20190423070432p:plain:w500

Adhoc配信

Adhoc配信自体が正直よくわかっていませんが、とりあえずやってみます。

App Settingsから、publishの項目を展開し、さらに「iOS code signing」を展開します。ここのフォームにアカウント情報を登録します。今回はAutomaticを選択します。

f:id:swiftfe:20190423071327p:plain

もう一度「Start new build」してみます。

slackの通知にipaファイルが追加されています!

f:id:swiftfe:20190423072222p:plain

iPhoneからリンクをクリックしてみると・・(slackからsafariへ移動しましょう)

インストール確認ダイアログが!

ホーム画面にインストールされました!

ちゃんと起動しました!!簡単すぎる…

このリンクを配布すればいろんな人にダウンロードしてもらえるのでかなり便利です。

GitHubのReadmeにバッジをつける

f:id:swiftfe:20190423070612p:plain

README.mdにここにあるmarkdown用のスニペットを追加してコミットするだけです。

いい感じです!

f:id:swiftfe:20190424012813p:plain

参考

Flutterメモ 2. 開発環境の構築(Android)

Flutter環境構築、今回はAndroid編です。

Flutter sdkがインストール済みであることが前提です。よろしければこの記事をご参照ください。

swiftfe0.hatenablog.com

Android Studioのインストール

以下からインストールしましょう。

android用の build tool, ライセンスをインストールする

$ flutter doctor --android-licenses

Android StudioからFlutterプロジェクトを作ってみる

Android Studioを起動して、まずは Android Studio > Preference > Plugin と進み、以下の2つのプラグインをインストールしましょう。

f:id:swiftfe:20190422124832p:plain

成功したら、Android Studioを再起動しましょう。

再起動後は、Start a new Flutter project から、Flutter プロジェクトが作成できるようになっています。

f:id:swiftfe:20190422124804p:plain

project nameに適当な名前を入れます。

f:id:swiftfe:20190422124817p:plain

company domainには一意なドメインを入れます。特に公開を考えていない場合はexample.comなどで大丈夫だと思います

f:id:swiftfe:20190422124819p:plain

f:id:swiftfe:20190422124820p:plain

実行

Android Studioから、エミュレータを切り替えることができます。iOSバイスも選択することができます。

f:id:swiftfe:20190422125340p:plain

f:id:swiftfe:20190422125602p:plain

f:id:swiftfe:20190422125345p:plain

Android上でサンプルアプリが起動しました!(デフォルトでカウンターアプリが実装されています)

f:id:swiftfe:20190422130033p:plain

iOSも実行できます。

f:id:swiftfe:20190422124931p:plain

参考

Flutterメモ 1. 開発環境の構築(iOS)

flutterの環境構築

Android版の記事もあります。

swiftfe0.hatenablog.com

この記事を読んでできること

  • flutter sdkをインストールできる
  • flutter コマンドが使える様になる
  • Xcodeツールのインストール
  • コマンドからのflutterアプリの作成
  • flutterアプリをiOS Simulatorで動かす

環境

flutter sdkのインストール

https://flutter.io/setup-macos/ これに従ってインストールしました。

現在は少し変わっている様なので、こちらを参照した方が良いかもしれません。 MacOS install - Flutter

まずは上のサイトから、sdkをダウンロードしましょう。Downloads以下にダウンロードされるはず。

/Users/xxx/development に移動させます。ターミナルを開きましょう。

$ mkdir /Users/xxx/development
$ cd /Users/xxx/development
$ mv /Users/xxx/Downloads/flutter .

flutter コマンドを使える様に、パスを通します。

~/.bash_profile

#flutter
export PATH=/Users/xxx/development/flutter/bin:$PATH
$ source ~/.bash_profile

flutter docker

flutter doctorというコマンドで、うまくインストールできているか確認することができます。

$ flutter doctor

こんな感じならOKです。

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel dev, v1.2.1, on Mac OS X 10.14.4 18E194d, locale ja-JP)

xcode用ツールのインストール

$ brew install --HEAD libimobiledevice
$ brew install ideviceinstaller
$ brew install ios-deploy
$ brew install cocoapods # cocoapodsをインストール済みの場合は、brew upgrade cocoapods
$ pod setup

flutter doctorで確認します。開発環境がちゃんと整っているか確認できるのは嬉しいですね。

$ flutter doctor

こんな感じならOKです。

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel dev, v1.2.1, on Mac OS X 10.14.4 18E194d, locale ja-JP)
[✓] iOS toolchain - develop for iOS devices (Xcode 10.2.1)

プロジェクト(アプリ)の作成・起動

$ flutter create first_sample

出力

(前略)

All done! In order to run your application, type:

  $ cd first_sample
  $ flutter run

Your main program file is lib/main.dart in the first_sample directory.

上のガイドにしたがって、以下のようにしてFlutterアプリを起動します。

$ cd first_sample
$ flutter run
Launching lib/main.dart on iPhone 8 in debug mode...
Starting Xcode build...                                          
 ├─Assembling Flutter resources...                    2.0s
 
 └─Compiling, linking and signing...                 13.2s
 
Xcode build done.                                           18.0s
Syncing files to device iPhone 8...                          6.4s

🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone 8 is available at: http://127.0.0.1:58361/
For a more detailed help message, press "h". To quit, press "q".

ここまできたら、勝手にiOSシミュレータが起動して、サンプルアプリが起動します!(iOSシミュレータをインストールしていない人は、Xcodeからインストールしましょう)

f:id:swiftfe:20190422130201p:plain

ターミナル上で qを押すと、終了します。

SPA・サーバレスハンズオン part1 React/Firebase Hostingで初めてのwebサイト公開

シリーズ

part3までやるとできるもの

このようなチャットアプリが作れます。

デモページ: https://huit-tetsufe-0506.firebaseapp.com/ ソースコード: https://github.com/TetsuFe/huit_react_handson_20190506

f:id:swiftfe:20190501071018p:plain

f:id:swiftfe:20190501073412p:plain

事前準備

  • gmailアカウントを一つ作っておくこと

大事なこと(講習会用)

  • ついていけなくなったら周りの人にすぐ相談しよう
    • それで解決しないときには発表者に相談する
    • 参加者同士の交流を促すためで、発表者に質問をしないでほしいというわけではないです

1. 環境構築

参考 サーバーサイドエンジニアも知っておくべきフロントエンドの今

1.1. エディタインストール(VSCode)

ファイルを編集するためのエディタをインストールします。すでになんらかのエディタをインストールしている人は、改めてインストールする必要はありません。

今回はVSCodeを使います。VSCodeMicrosoft社製の軽量なエディタで、stackoverflowのランキングでもSublime TextやAtomなどの類似エディタを抑えて一位にランクインしています。

Stack Overflow Developer Survey 2018

どれくらい便利かは使ってみればわかります。以下からダウンロードしましょう。

https://code.visualstudio.com

やらなくていいです:興味のある人向け

コードのフォーマットなどを気をつけたい場合は以下リンク等を参考にESLint・Pretitterなどを導入すると良いらしいです。

Prettier 入門 ~ESLintとの違いを理解して併用する~ - Qiita

1.2. Vscodeの統合ターミナルを開く(ターミナル・コマンドプロンプト・端末などでも良い)

インストールしたVS Codeを開きましょう。

そして、ctrl + shift + @ または Terminal > New Terminal からターミナルを起動します。

起動したら以下のように出てきます。(緑色の部分がターミナル)

f:id:swiftfe:20190501122122p:plain

1.3. homebrewインストール(mac)

windowslinux系OSを使っている人は飛ばしてください。

macOSの人は、homebrewというツールをインストールします。これは、後にnode.jsをインストールするために使います。https://brew.sh/index_ja に書いてあるコマンドをターミナル上にコピー&ペーストして、実行します。以下のような感じです。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

以下、$ から始まるものはターミナル上で実行してください。

1.4. node.jsのインストール

$ brew install node

windowの人は、このリンクからインストールしてください。 Node.js

node.jsとnpmというパッケージ管理ツールがインストールされます。

要は、reactやその他jsライブラリを使った開発を快適に使うための環境をセットアップしたと思ってください。

1.5. chromeのインストール(開発用ツール)

人によって動作しないということをなくすために、Chromeをインストールしてください。本当はバージョンも合わせるべきですが面倒なのでそこまではやりません。

Google Chrome ウェブブラウザ

2. 初めてのReactサイト作成

2.1. とりあえずhello, world

まずはとりあえずサンプルプログラムを動かしてみましょう!

今回は公式インストールガイド Create a New React App – React にしたがって、create-react-appを使います。

ターミナルを開いて、以下を実行します。

$ npm -v
6.7.0

$ node -v
v8.11.4


$ npx create-react-app huit_handson_20190506

出力

We suggest that you begin by typing:

  cd huit_handson_20190506
  yarn start

Happy hacking!

こんな感じに出たら、以下のようにターミナル上でコマンドを実行します。

$ cd huit_handson_20190506
$ npm start

これでReactアプリケーションが実行できました! $ npm start というコマンドを打つと、Reactアプリケーションのサーバーが起動します。これにwebブラウザからアクセスすれば、webページとして見ることができます。

Chromeを開いて、localhost:3000 と入力しましょう。(自動的に開く場合はしなくてもよいです)

localhost:3000 はlocalhostが自分のサーバーの名前で、3000はポート番号のことです。後に説明しますが、localhost:3000 とURLバーに入力することで、ブラウザは僕らが実行しているReactアプリケーションを見つけ出すことができます。

うまくいけば、ブラウザ上に以下のような画面が表示されているはずです。$ npx create-react-app huit_handson_20190506 というスクリプトによってデフォルトのアプリケーションコードが生成されたので、今はこんな表示になっています。

f:id:swiftfe:20190429095759p:plain
create-react-appでのhelloworld

まとめ

  • create-react-appというツールを使って、reactアプリケーションを作った。
  • npm start というコマンドを打つとreactアプリケーションサーバーが起動する。
  • ChromeのURLバーにlocalhost:3000と入力するとwebページとして表示できる。

2.2 公開しよう

デフォルトのままですが、いきなり全世界公開してしまいましょう!

いきなり公開までやってしまうのには以下のような理由があります。

  • コードが複雑でない方が、公開が簡単・失敗した時の原因が特定しやすい
  • 先に公開までやっておけばローカル(自分のPC)で完成した時にすぐ公開できて、他の人に見てもらえる

2.2.1 webサイト公開の仕組み

今現在の方法は、自分のPCだけで完結しています。 f:id:swiftfe:20190501121419p:plain

後述するFirebase Hostingのようなサービスを使って外部のサーバーを使ってサイトをweb上に公開する場合、以下のようになります。

f:id:swiftfe:20190501121435p:plain

さらに詳しくするとこんな感じです。

f:id:swiftfe:20190501121437p:plain

以上の図から、webサイトを公開するためには、だいたい以下のような手順が必要になります。

2.2.2 Firebase Hostingを使う

今回は、公開のためにFirebase Hostingというサービスを使います。

firebase.google.com からアカウント登録しましょう。アカウント登録が終わったら、一旦ターミナルに戻ります。

まずは、reactアプリケーションを公開用のコンパクトなhtml・css・jsファイルに変換します。

ターミナルを見ると、以下のように表示されていて、何もキー入力を受け付けない状態かもしれません。これはサーバーが起動しているためです。

f:id:swiftfe:20190429110539p:plain
npm start中の入力を受け付けないターミナル

ctrl + c を押して一旦サーバーを停止すると、入力できるようになります。

入力できるようになったら、以下のコマンドを入力します。

$ npm run build

なんか色々出てきますが最後に以下のように出てくればOKです。

The build folder is ready to be deployed.
You may serve it with a static server:

  yarn global add serve
  serve -s build

Find out more about deployment here:

  https://bit.ly/CRA-deploy

次に、Firebaseのサーバーのセットアップをします。

コンソールに移動します。

f:id:swiftfe:20190429111215p:plain

「プロジェクトを追加」をクリックします

f:id:swiftfe:20190429111551p:plain

こんな感じに入力します。

f:id:swiftfe:20190429112230p:plain

プロジェクト名は自分の好きな名前にすればOKです。

「プロジェクトの作成」を押すと、プロジェクトが作成されます。

f:id:swiftfe:20190429112235p:plain

続いて、Firebase Hostingの初期設定をします。

f:id:swiftfe:20190429112412p:plain

f:id:swiftfe:20190429112601p:plain

このポップアップにしたがってターミナルに以下を実行します

$ npm install -g firebase-tools

終わったら、「次へ」をクリックします

f:id:swiftfe:20190429112822p:plain

f:id:swiftfe:20190429112800p:plain

このポップアップにしたがってターミナルに以下を実行します

$ firebase login

ログインできたら、

$ firebase init

今度は、処理の途中で入力することがあります。

f:id:swiftfe:20190429113216p:plain

Hostingを選択しましょう。(カーソルキーで切り替え、「半角」スペースで選択、エンターで入力を完了)

f:id:swiftfe:20190429113620p:plain

紐づけるプロジェクトを選択しましょう。これはカーソルキーで選択してエンターを押せばOKです。

f:id:swiftfe:20190429113725p:plain

最後に2つ聞かれますが、一つ目の質問には「build」、二つ目の質問には「y」を入力しましょう。(5/8修正:画像にはpublicとありますが、buildと入力してください)

ここでpublicと表示されていたとき、index.htmlが書き変わってしまうことがあるようです。なのでその際はindex.htmlの中に<div id="root"></div>を追記してみてください。

f:id:swiftfe:20190429114111p:plain

これでfirebase hostingの初期セットアップは完了です!さっそくwebページを公開(デプロイ、deploy)しましょう!

$ firebase deploy --only hosting

以下のように出力が出ます。

=== Deploying to 'huit-tetsufe-0506'...

i  deploying hosting
i  hosting[huit-tetsufe-0506]: beginning deploy...
i  hosting[huit-tetsufe-0506]: found 1 files in public
✔  hosting[huit-tetsufe-0506]: file upload complete
i  hosting[huit-tetsufe-0506]: finalizing version...
✔  hosting[huit-tetsufe-0506]: version finalized
i  hosting[huit-tetsufe-0506]: releasing new version...
✔  hosting[huit-tetsufe-0506]: release complete

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/huit-tetsufe-0506/overview
Hosting URL: https://huit-tetsufe-0506.firebaseapp.com

最後の Hosting URL: https://huit-tetsufe-0506.firebaseapp.com を見てください。ここにアクセスすると、、、

あれ・・?

f:id:swiftfe:20190429114720p:plain

どうやら公開するディレクトリの設定が間違っていたようです。

この設定を変更するには、reactアプリフォルダ内に作られた設定ファイル「firebase.json」を編集すればOKです。

VS Codeでreactアプリのフォルダを開いて、編集できるようにしましょう。

f:id:swiftfe:20190429115103p:plain

Open Folderから、reactアプリのフォルダを選択しましょう。

f:id:swiftfe:20190429115108p:plain

ターミナルを開き直しましょう。(ctrl + shift + @ または Terminal > New Terminal)

f:id:swiftfe:20190429121811p:plain

左の部分を左クリックして、New Fileから、firebase.jsonを作成

f:id:swiftfe:20190429121825p:plain

以下のように書いて保存します。

{
  "hosting": {
    "public": "build"
  }
}

ここにbuildとするのは、先ほど $ npm run build で生成されたコンパクトなhtml・css・jsファイル群のフォルダ名がbuildだからです。

VS Codeのフォルダツリー(左側に出ているやつ)をみれば、buildフォルダがあることがわかると思います。

参考:(初心者向け)Firebase HostingへReactプロジェクトを公開する手順 - Qiita

次に、以下のようにしてこのターミナル上で設定をもう一度行います。(ここの説明は割愛します)

$ firebase use --add

f:id:swiftfe:20190429120139p:plain

f:id:swiftfe:20190429120449p:plain

その後、もう一度公開してみましょう。

$ firebase deploy --only hosting

終わったら、https://<自分の設定した名前>.firebaseapp.com/ にアクセスすると、今度こそ作成したReactページが表示されるはず!

f:id:swiftfe:20190429120539p:plain

2019/06/15 追記:これでうまくいかない場合は、firebase initをした時などにindex.htmlがfirebaseによって書き換えられていることが原因かもしれません。$ npm run build をもう一度行い、index.htmlを最新の状態にしましょう。

Firebase hostingは自動でURLを発行してくれる(自動DNS設定)

Firebase hostingは自動でURLを発行してくれる ことがわかったと思います。

これは、先に説明したDNSの設定を自動で行ってくれていると言い換えられます。

実際にはあるIPアドレスを持ったサーバー上にreactアプリのファイルがあるという状態になっています。

これによって、IPアドレスという覚えにくい名前ではなく、ドメイン名というわかりやすい名前でユーザからアクセスしてもらえることができるようになっています。

また、ターミナルでnslookupコマンドを実行すると、ドメイン名とIPアドレスの対応を調べることができます。

$ nslookup huit-tetsufe-0506.firebaseapp.com

出力

Server:         192.168.0.254
Address:        192.168.0.254#53

Non-authoritative answer:
Name:   huit-tetsufe-0506.firebaseapp.com
Address: 151.101.1.195
Name:   huit-tetsufe-0506.firebaseapp.com
Address: 151.101.65.195

Addressという項目がIPアドレスです。ちなみに、IPアドレスが複数あるのは、サーバーの数を複数台にしてアクセスの際にどちらにもアクセスできるようにすることで、負荷を分散させるためらしいです。

参考:DNSのラウンドロビン設定を確認する − @IT

やってみよう:IPアドレスを直接ブラウザに打ち込んだらどうなる?

繋がるか繋がらないか自分の頭の中で一度考えてから、やってみてください。

まとめ

  • firebase hostingを使えばサイトをwebに公開できる
  • firebase hostingは主にターミナルでコマンドを実行して設定する
  • firebase hostingは自分のHTMLファイルなどを公開サーバーにおくことができる
  • firebase hostingは勝手にそのサーバのIPアドレスドメイン名をDNSに登録してくれる
    • わかりやすい名前でクライアントから見つけてもらえるように設定してくれる

参考

シリーズ