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

TetsuFeの個人開発ブログ

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

Flutter for web プロジェクトを作成し github pages で公開するまで

作成: 2019/8/26

環境

flutter for webとは

https://github.com/flutter/flutter_web

モバイルアプリ向けクロスプラットフォームフレームワーク Flutterを使ってwebページを作れるフレームワーク(?)です。 Hamming birdと言われていたものと同じものです。

詳しくは以下の記事などを参照ください。

rightcode.co.jp

ntaoo.hatenablog.com

flutter for web のプロジェクト作成

事前準備

>=2.3.0-dev.0.1 <3.0.0 が必要なので、Dart 2.4.1をインストールしましょう。

$ brew install dart # brew upgrade dart

プロジェクト作成には今回は WebStorm を使いました。

WebStormに Dart pluginを入れてください。

プロジェクト作成

Create a new project > Dart > Flutter web App

f:id:swiftfe:20190826115327p:plain

サーバー起動

$ flutter pub global activate webdev

ホットリロードを有効にしてサーバーを起動します

$ webdev serve --hot-reload
Creating build script, took 7010ms
[INFO] Setting up file watchers completed, took 10ms
[INFO] Waiting for all file watchers to be ready completed, took 201ms
[INFO] Building new asset graph completed, took 1.0s
[INFO] Checking for unexpected pre-existing outputs. completed, took 1ms
[INFO] Running build completed, took 25.8s
[INFO] Caching finalized dependency graph completed, took 184ms
[INFO] Succeeded after 26.0s with 571 outputs (3248 actions)
Serving `web` on http://localhost:8080

localhost:8080 でサーバーがたちあがります。ここにブラウザからアクセスすればOKです

f:id:swiftfe:20190826115905p:plain

トラブルシューティング

$ webdev serve --hot-reload
webdev could not run for this project.
The `build_web_compilers` version – 2.2.3 – is not within the allowed constraint – >=0.3.6 <2.0.0.

エラーに記載のバージョンが同じものを探そうとすると、以下のようなサイト(何語?)を参考にしないといけないあたり、Flutter for webの未熟さ具合が感じられます。

https://www.petanikode.com/flutter-web/

pubspec.yaml を編集します

dev_dependencies:
  build_web_compilers: ^1.0.0

Downloading build_web_compilers 1.2.2. がはいります

iOS simulatorでアクセス

Safariを起動し、普通に検索バーでpasteを使って貼り付けてアクセスします

f:id:swiftfe:20190826124214p:plain

なぜか読み込みが結構遅いです・・

githubpagesで公開

$ webdev build

.gitignoreを修正し、buildをコメントアウトして、buildをコミットします。

そのままコミットしてgithubにpushしましょう

$ git init
$ git add -A
$ git commit -m "init"
$ git remote add origin <githubのリポジトリ>
$ git push -u origin master

githubリポジトリのsettings > github pages で 「Source」のmasterにチェックをいれる

f:id:swiftfe:20190826123244p:plain

今回は flutter_hokuma_novel というリポジトリ名にしたので、

https://tetsufe.github.io/flutter_hokuma_novel/build/ にアクセスすれば、flutter for webで作ったページが表示されます!(webdev build によって build/ 以下にindex.htmlがあるので、/build/ をURLに含めるのを忘れないようにしましょう。)

(例えば、あなたのユーザ名が xxx で、リポジトリ名が yyy なら、https://xxx.github.io/yyyl/build/ となります。)

【Hardening II SU】セキュリティインシデント対応を体験しました

目次

2019年7月4日、5日に北海道札幌で開催されたHardening II SUというセキュリティ関係の協議会に参加してきました。

Hardening II SUとは

wasforum.jp

t.co

Hardening(ハードニング)とは、1日かけてセキュリティインシデントに対応する協議会です。今回は札幌開催でした。 ちなみに SUは「スピード感のある動き」 という意味だそうです。どこからこのワードが出てきたんでしょうか。笑 ISUCONとかもそうですが、バカっぽいけどイカした略語とかではなく 単にネーミングセンスのない略語を使っている ことが多いように思います…(でも多分札幌開催だからSはSapporo、UはUnion(連合。ハードニングには連合制というものがある)とかなんだと信じたい。「これが真の意味」とかだったら個人的にかっこよかったのに。)

チームを組んでセキュリティインシデントに対応する

この大会では、6人(7人)1組で攻撃者から用意されたサーバーを攻撃から守りつつ、ECサイトを更新して売り上げを維持しつつ、ビジネス的な問題にも対処(謝罪会見・メール対応など)もするという内容になっています。最終的に売り上げや技術点などを加味したポイントで評価されます。

事前準備

この大会では事前準備をします。いきなり集まって対応するのではなく、チームで事前にミーティングなどを行い、当日やるべきこと・そのために準備すべきことなどを確認する場合が多いようで、僕のチームも事前にそれらを行いました。

正直DNSFirewall、監視、検知などインフラ系のセキュリティは何も分からなかったのでCMSwordpressdrupalを調べたり(といってもこれらも全然知りませんでしたが)、サーバーwindowsにどう繋げるかなどかなり簡単なことを調べるだけで終わってしまいました…

ただ、この事前準備・ミーティングのお陰で、 「チームメンバーがお互いの得意分野やレベル感、参加の目的を知り、各自適切な役割を分担できる」 という点でかなり当日動きやすくて良かったです。

またミーティングを事前にしていたので、 「初対面の6人チームでも当日コミュニケーションが取りやすい」 という点も良かったです。

この太字の2点はハードニングの素晴らしいところかなと思います!

前日

ハードニングは「攻撃者から用意されたサーバー」を使いますが、 このサーバー群の構成は前日に発表されます。

そのため、前日にサーバー群の構成資料をチームで読み込み、対策を練ります。遅くまでやる人はかなり深夜までやる人もいます・・

当日(1日目)

競技時間は10:00-17:45の7時間45分です。(僕は18:00までと勘違いしていました)

当日の競技はなんだかんだで慌ただしく始まります。事前に打ち合わせはしているとはいえ、 競技が始まってから用意されたサーバー群を初めて扱うことになるので、色々と手間取ります。 実際にどんなことが起こるかは競技が始まってみないとわからないところもあるので、結構コミュニケーションが大切になります。ここで事前ミーティングの大切さがわかりました。話しやすい雰囲気を作ってくださっていた皆さんに感謝です。

僕がやったことはだいたい以下です。

  • ECサイトのコンテンツのアップデート
  • 商品紹介サイトのコンテンツアップデート
  • 改ざんされたサイトの修復
    • と言っても画像をアップロードし直したり
  • ブラウザ経由での目でのサイトモニタリング(死活監視・改ざん発見)
  • メール対応(少しだけ)

全然インシデント対応っぽいことできなかったですね・・

ハードニングは「売り上げをあげる」という目標もあるので、僕のように勉強が間に合わなかった人でも活躍の場があるのも参加のハードルを下げるよいポイントかなと思います。次はちゃんと準備を間に合わせたいですが・・

ハードニングは2日間の日程ですが、1日目で競技は終わりです。

当日起こったトラブル

  • ネットワークが不調でなかなかwindows remote desktopが繋がらない

意外とこれくらいでしたね(影響は割と大きかったですが)。事前準備の期間も長いので、皆さんも忘れ物等なく参加できていました。

Softening day(2日目)

2日目は競技はありません。前日の競技の結果発表、競技チームごとの振り返り発表、攻撃者チームであるKuromame6からの講評などがあります。

思った以上に賞品が充実しており、普通に チーム6人全員に1万円を超える景品が出ている場合も4、5例ありました。(多分です) スポンサーの力も大きいですね。(人気のないMPの商品を買うと狙いやすいのかも。と思ったり)

よかったこと

「勝つことが全て」の競技会ではない

競技参加者の方も発言していたのですが、 あくまで勉強のための場であるということ。結果が全てではない雰囲氣なので、初めての方も参加しやすいようになっています。

実際に僕もあまり大したことはできませんでしたが、競技終了後はチーム内で順位的な話はほとんど出ず、なぜこの対策がうまくいかなかったのか、このインシデントの原因はなんだったのかという内容についての話が多かったです。(もしかするとチームにもよるのかもしれませんが、そういうチームの方が多いように思いました)

実際のセキュリティインシデントを体感できた

本職の方も言っていたのですが、なかなか実際に何かインシデントが起こるということは珍しいので、 インシデント対応が実際にできた ということが大きかったです。

どんな攻撃があるのか、それにどうやって対処するのかは非常に勉強になりましたし、結構泥臭い手順(落とされたらとりあえず再起動する、改ざんされたらとりあえず手動で修正するなど)を踏む必要があるということも経験できました。今回はほとんど根本の改善には対応できませんでしたが、この経験からどんなことを勉強に取り組めばいいかがなんとなくわかったような気がします。

社会人の方との交流の機会にもなった

僕のチームは自分以外全員が社会人の方でした。なので中々貴重なお話が聞けたのも良かったです。ホワイト企業群「ホワイト500」であるアピールや、一方で過去の残業時間が長かった話、社会人になってから住む家の話など、面白い話が聞けました。

反省点

もっと事前に勉強をちゃんとしておけばよかったです。次回はしっかりと準備をして挑みたいと思います。

終わりに

まだハードニングは知名度が低い印象ですが、かなり貴重な体験ができる素晴らしいイベント(協議会)でした! 普段あまりセキュリティに関心がない方でも、勉強意欲と興味がある人は一度参加してみるべきだと思います!

合わせて読みたい

関係性は伏せておきますが、強い友達が書いた記事をシェアします。

cha-shu00.hatenablog.com

Rustの本も書いてるらしいので勝手に紹介します

【開発参加検討者向け】フリマサービス「ホクマ」の技術構成

今回は、北大IT研究会メンバーが開発・運営する「ホクマ」の技術構成についてお話しします。

技術的な話に興味がないという方はブラウザバックしてください。

今回はホクマの開発を引き継ぎたい方に向けての記事になります。

ホクマとは

まず、ホクマとは何なのか、知らない方に3行で説明します。

  • 北大生だけが使えるフリマWebサービス
  • 使わなくなった教科書などが売り買いされている
  • 直接会って渡すので完全手数料0。高く売れる、安く買える!

もっと知りたい方は以下の記事をお読みください。

hu-jagajaga.com

技術構成図

f:id:swiftfe:20190420175841j:plain
ホクマ技術構成図

一つずつ解説

1. Django

  • これは何?:Python製のWebアプリケーションフレームワーク(WAF)。ほぼ全ての処理はまずここから始まる。
  • なぜ必要?:サーバー側でPythonプログラムを動かすために必要。商品情報を保存するためにデータベースにアクセスしたり、ブラウザにHTMLを返したりする。ここにhtml,css,jsも含まれる。

2. PostgreSQL

  • これは何?:データベース。データベースを管理するシステムおよび、データベースを操作する言語。商品データを保存・更新するとき等に使う。
  • なぜ必要?:これがないとユーザが新しいデータを追加したり更新したりできない。また、効率よく保存したデータを検索したりするために必要。

3. Nginx

  • これは何?:Webサーバ。ユーザ(クライアント)からのリクエストをDjangoに伝え、リクエストを受け取ったDjangoから返されたHTML・CSSなどのファイルにメタ情報を追加した情報をブラウザに返す。ユーザからのリクエストをうまくさばくための色々な機能ももっている。
  • なぜ必要?:複数のユーザからの同時接続に耐えるために必要。HTTPS通信のために必要。ブラウザ側に快適な動作をさせるためにDjangoから受け取った情報に何かを追加したり加工したりする。

もっと詳しく知りたい

これらの記事を読むために必要な予備知識:DjangoなどのことをAPサーバー(アプリケーションサーバー)、NginxなどのことをWebサーバーと呼ぶことがあります。

4. Bootstrap(4)

  • これは何?:css・jsラリブラリ。Webページの見た目や使いやすさを向上させるために必要。htmlのclass名を指定するだけで、cssやjsを書かずに便利な機能をつけることができる。
  • なぜ必要?:見た目をかっこよくさせるために必要。PC・スマホ両対応も簡単にできる。Webページでインタラクティブな操作は必須だが、これを簡単に作れる。

5. Let's Encript

  • これは何?:HTTPS通信をするための証明書の管理をしてくれるツール(無料)。コマンドを使って証明書を更新できて便利。
  • なぜ必要?:ブラウザから送られるユーザのパスワードなどを暗号化して、通信路上で盗まれてもわからないようにする通信(HTTPS通信)を行うために必要。

6. Amazon AWS Sinple Storage Service(S3)

  • これは何?:画像やCSS・JSなどを保管しておく場所。不特定多数用のDropboxみたいな感じ。
  • なぜ必要?:画像などの容量が多いものをこのように別サーバに保管しておくことで費用を抑えるため。また、CloudFrontなどの別AWSサービスと連携させ、ユーザの位置と近いサーバーに画像などのファイルを置いて物理的にアクセス速度を高めるためにも必要。

docs.aws.amazon.com

7. Linuxサーバ(ConohaVPS)

  • これは何?:いろんなプログラムを動かすためのOS及びコンピュータ本体。
  • なぜ必要?:1,2,3など複数のプログラムを並行的に動かすために必要。自分のPCではないコンピュータで外部公開するという意味でも必要。

8. (Optional) Docker

  • これは何?:VMよりも軽量な仮想環境を作成できるツール。本番用サーバーと本番の一個手間サーバ(QAサーバー)、ローカルPCなどでほぼ同じ環境をコピーして使うことができて便利。
  • なぜ必要?:本番用サーバーとQAサーバー、ローカルPCなどでほぼ同じ環境をコピーして使いたいときに必要。仮想環境なので、開発初期に色々試して失敗してもすぐ消してやり直したりできる点も良い。

9. postfix

  • これは何?:メールの送受信用のツール。
  • なぜ必要?:ユーザへの通知をメールで送ったり、お問い合わせメールを受け取ったりする際に利用します。

おまけ

CI(継続的インテグレーション)ツール:CircleCI

  • これは何?:Djangoなどのプログラム修正したときに自動的にテスト(プログラムが意図した動作をするか自動確認すること)などを行ってくれるツール。
  • なぜ必要?:ホクマのように日々改善を続けるサービスに対してバグがないか改善のたびにチェックするために必要。

GitHub

  • これは何?:Gitというソースコード自体と更新差分を管理するツールをweb上で操作できる+タスク管理・コードレビューもできるwebサービス
  • なぜ必要?:ホクマのように日々改善を続けるサービスに対して複数人で更新状況を管理しやすくするために必須。使っていない企業は時代遅れ扱いされるが授業では習わない。

終わりに

ホクマはある程度saleorというリポジトリを意識した構成にしており、割とまともなサービス構成になっているかと思います。ホクマをまともに理解し運用・改善し続けることができれば、まともな個人開発経験があると言ってもよいのではないかと思っています。ホクマはまともに稼働してくれる開発・運用メンバーを募集しています。興味ある方はぜひご連絡ください!

もっと知りたい方へ

もう少し細かく書いたシリーズがあるので、そちらもよろしければご覧ください。

swiftfe0.hatenablog.com

連絡先

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

参考