ヒキダスブログ

テック系や最近見たもの感じたことを書いて残す引き出しスペースです

React x TypeScript 学習 (4) - Reactの基本構文

概要

久しぶりにReactを触った備忘録として、Reactの基本構文についておさらいしてみようと思います。
今回触れる内容としてはこちらになります。

  1. エントリーポイントの記述
  2. コンポーネントの基本構文 (Class ComponentとFunctional Component)
  3. コンポーネントの状態管理 (State)
  4. コンポーネントより受け取るプロパティ (Props)
  5. エレメント取得 (Refs)

基本構文

1. エントリーポイントの記述

Reactで作ったコンポーネントを描画するにあたり、まずは「どのDOM要素に描画するか」をエントリーポイントで指定する必要があります。第3回までのソースコードでいうと、/src/index.tsxがエントリーポイントに相当します。

import React from 'react'
import ReactDOM from 'react-dom'

import App from './App'

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

コンポーネント作成なら react パッケージで良いのですが、DOM要素への描画にあたり react-dom も必要となります。上述ですと、「id="app"のDOM要素の中に Appコンポーネントを出力する」処理となります。

2. コンポーネントの基本構文

ReactにしてもVueにしても、コンポーネントについて個人的ざっくりイメージで、ページからUIパーツまで流用しやすい粒度でビュー・スタイル・ロジックをセットにしたもの と定義しています。

これまでのフロントエンド開発を振り返ると、html, css, jsを「関心の分離(Separation of concerns)」に基づいて管理していました。それより前だとhtmlのタグに対してonclickでJSのロジックを埋め込んだりインラインスタイルで書くこともありましたが、その場合htmlとcss, jsが密結合になり、コードの可読性や問題があった時の切り分けがしづらくなることから、html, css, jsをそれぞれ別ファイルで管理しようという流れになっていったようです。
一方で、UI開発において類似したパーツを流用しようとなると、そうして別ファイルとなっているビュー・スタイル・ロジックをそれぞれコピペする等になりコードも重複になり煩雑になってしまいます。
ReactやVueではそうした「関心の分離」から「技術の分離(Separation of technologies)」として、ビューとロジックをセットにして必要な時に読み込む書き方でコンポーネント開発を推し進めました。

話がそれてしまいましたが、Reactではコンポーネント開発にあたり「単一ファイル内にビューとロジックをセットに書く」ということを念頭においてもらうと良いでしょう。そして、そのReactコンポーネントの定義方法には2種類あります。
先ほどのエントリーポイントで読み込んだAppコンポーネント(/src/App.tsx)で書いてみようと思います。

Functional Component

import React from 'react'

const App = () => {
  return (
    <p>This is App component.</p>
  )
}

export default App

Functionalというように関数形式で書かれたコンポーネントの定義方法になります。reactパッケージを読み込んでからApp関数で返す要素を記述しそれをエクスポートする形になります。
関数には上述のような変数に代入するタイプもありますし、以下のようにfunctionを使った定義方法でもOKです。

import React from 'react'

function App() {
  return (
    <p>This is App component.</p>
  )
}

export default App

以下のケースでこの書き方が有用になります。

これまでこの書き方のネックなところは状態(State)を持つことができず、その場合はClass Componentに置き換えるなりして対応していたのが、React 16.8.0〜の機能である React Hooks でそれも解消されるようになりました。React Hooksは調べ次第後日アップしようと思います。

Class Component

import React from 'react'
  
export default class App extends React.Component {
  render() {
    return(
      <p>This is App component.</p>
    )
  }
}

こちらはclass構文を使ったコンポーネントの定義方法になります。Functional Componentの違いとしては以下が挙げられるでしょう。

  • ReactのComponentを継承する(他にもPureComponentを継承するケースもある)
  • renderメソッド内で返却する要素を記述する
  • Reactのライフサイクル(マウントされた直後に走るcomponentDidMount他)にフックして処理を書き足せる

class構文であることやライフサイクルフックもあり、コンポーネントの機能としてはもりもりでその分、機能によっては記述が肥大化しやすい面が見受けられます。その場合は、その中からコンポーネントとしてさらに切り出せないか検討する必要がありそうです。私もこの書き方で長尺なものを見るとウッとなってしまうのですが、「結論(描画結果)はrender見ればいいよね」と割り切るようにしました。この体験がReactから敬遠していた理由の一つですね。
ライフサイクルフックも時間があれば、別の回でまとめようと思います。

3. コンポーネントの状態管理 (State)

例えば、トグルボタンのコンポーネントだと「トグルがONになっているかどうか」、インプットフォームなら「現在のインプット情報」などなど、コンポーネントによって内部に閉じた状態を管理する必要が出てきます。
ここではトグルボタンを例にして、状態であるStateの扱いを書いてみます。

まずは、先ほど用意したAppコンポーネントを以下のように別ファイルのToggleコンポーネントを表示するよう書き換えます。

import React from 'react'

import Toggle from './components/Toggle'

const App = () => {
  return (
    <Toggle />
  )
}

export default App

そして、componentsディレクトリ配下にToggle.tsxファイルを作成し、以下のように記述します。

import React from 'react'

interface State {
  isToggleOn: boolean
}

export default class Toggle extends React.Component<{}, State> {
  state: State = {
    isToggleOn: false
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button>Toggle</button>
      </div>
    )
  }
}

状態を持たせる点よりClass Componentで定義しました。interfaceというのはTypeScriptで使用できる型定義方法の一つになります。

その状態ですが、stateというプロパティでReactでは管理しています。class構文直下に書かれているstateプロパティがToggleコンポーネントの「状態」にあたり、isToggleOnの値を保持しています。これは「ToggleがONになっているか」を意味していて、作るコンポーネントによって任意に保持するキー・値を決めていきます。この状態はthis.state.[state内のキー]で値を参照することができます。

state: State = {
  isToggleOn: false
}  

そして、renderメソッドではreturnの前にcurrentState変数を定義し、先のthis.state.[state内のキー]を使ってトグルがtrueなら"ON"、falseなら"OFF"を代入するようにしています。
return内で出力する要素内でJSの記述を使うには中波括弧({})で囲ってやる必要があり、currentState変数の値を出力しています。

render() {
  const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
  return (
    <div>
      <p>{currentState}</p>
      <button>Toggle</button>
    </div>
  )
}

出力結果は、Toggleという名前のボタンの上に「OFF」という文字列が表示されて見えると思います。

その次にstateの値を更新するにはどうするか。ここでは、Toggleボタンを押すたびに上の文字列が「ON」「OFF」と切り替わるようにしていきます。その変更がこちらになります。

import React from 'react'

interface State {
  isToggleOn: boolean
}

export default class Toggle extends React.Component<{}, State> {
  state: State = {
    isToggleOn: false
  }

  toggleState = () => {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button onClick={this.toggleState}>Toggle</button>
      </div>
    )
  }
}

変更点は、buttonタグにonClick={this.toggleState}、toggleStateメソッドを追加しました。状態更新はthis.setState({...})で行っていて、setState内で状態キー(ここではisToggleOn)の値をtrue, falseで逆転させています。
これにより、Toggleボタンを押すたびに状態が「ON」「OFF」に切り替わるようになりました。

ちなみに、このtoggleStateメソッドではアロー関数を使用しています。renderと同じくクラスメソッドにすることも可能ですが、その際以下のいずれかにする必要があります。

  • onClickの書き方をonClick={this.toggleState}のままにしておきたい場合
    記述で変更ない箇所は省略しています。ここではconstructerメソッドを追加し、その中でbindを使ってクラスメソッドと紐づけるようにしています。
...  

export default class Toggle extends React.Component<{}, State> {
  ...

  constructor(props: any) {
    super(props)
    this.toggleState = this.toggleState.bind(this)
  }

  toggleState() {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  ...
}
  • onClickの書き方を変えても良い場合
    buttonタグのイベントをonClick={() => {this.toggleState()}}にしています。アロー関数が入っていますが、要はやっていることは先のtoggleStateで使っていたアロー関数をonClick内で行ったものになります。
...  
  
export default class Toggle extends React.Component<{}, State> {
  ...

  toggleState() {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button onClick={() => {this.toggleState()}}>Toggle</button>
      </div>
    )
  }
}  

この辺りの使い分けは人によりますが、私は最初のアロー関数のままか、onClick内の記述が少し面倒になるものの後者が良いかなと思いました。都度メソッドが増えるたびconstructorに記述を追加する方が煩雑かなと思ったので。

4. 親コンポーネントより受け取るプロパティ (Props)

3では状態の扱いについて触れましたが、そのコンポーネントを外部から干渉する必要も往々にしてあります。例えばボタンコンポーネントだと配置する箇所に応じてボタンの色や文字色、サイズも異なってきます。見出しコンポーネントを作るにしても、見出しの内容は内部に閉じているとコンポーネントの柔軟性も損なわれるのでそれを読み込むコンポーネント側からパラメータを送ってやる必要があります。Reactではそうした読み込む(親)コンポーネントから子に渡すpropsというプロパティがあります。Propsと複数形になっているのは、複数のプロパティで渡されることもあるからですね。

では、先のToggleコンポーネントにpropsを追加してみましょう。親コンポーネントからnoteという名前で文字列を受け取り、ボタン下に注記として表示します。
Appコンポーネントでは、Toggleコンポーネントを呼び出すところにnote="(文字列)"を追加します。

import React from 'react'

import Toggle from './components/Toggle'

const App = () => {
  return (
    <Toggle note="※プロフィール設定をロックします" />
  )
}

export default App  

そして、Toggleコンポーネントではpropsで受け取ったnoteの値をもとに表示するよう更新しました。

import React from 'react'

interface Props {
  note?: string
}

interface State {
  isToggleOn: boolean
}

export default class Toggle extends React.Component<Props, State> {
  state: State = {
    isToggleOn: false
  }

  toggleState = () => {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button onClick={this.toggleState}>Toggle</button>
        { this.props.note && <p>{this.props.note}</p>}
      </div>
    )
  }
}

以下の変更点を行っています。
- interfaceにPropsを追加し、React.Componentに受け渡す
- buttonタグ下でnoteを判定し、文字列があった場合pタグで出力

stateと同じ要領で、propsもthis.props.[props内のキー]で値を参照することができます。一点、interfaceのPropsでnoteキーの後ろに?がついています。これは、noteというキーを「オプショナルで指定する」という意味になります。

www.typescriptlang.org

これをつけていないと、TypeScript側でnoteはあるもの前提でソースコードをチェックするので、もしApp.tsxで呼び出しているToggleコンポーネントにnoteプロパティの記述がなければエラーが表示されてしまいます。

オプショナルの?を外す代わりに、Propsにデフォルト値を指定できるdefaultPropsを指定したらこの問題も解消されます。

import React from 'react'

interface Props {
  note: string
}

interface State {
  isToggleOn: boolean
}

export default class Toggle extends React.Component<Props, State> {
  state: State = {
    isToggleOn: false
  }

  static defaultProps: Props = {
    note: ''
  }

  toggleState = () => {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button onClick={this.toggleState}>Toggle</button>
        { this.props.note && <p>{this.props.note}</p>}
      </div>
    )
  }
}

qiita.com

5. エレメント取得 (Refs)

素のJSだと、document.getElementById('xxx')等でDOM要素を取得していました。それでボタンクリックのイベントを追加したり要素に文字列等を挿入していましたが、Reactでは先のサンプルのようにDOM取得を行う機会があまりないでしょう。

とはいえ、canvasではgetContextの前に要素を取得するし、ライブラリを組み合わせる際に要素を取得する必要が出てくるので全くないわけではありません。ReactのライフサイクルフックcomponentDidMountのタイミングで、コンポーネントが画面に描画されるのでそのタイミングで、document.getElementById('xxx')等で取得するのでも良いですが、ReactのRefsを使ってDOMを参照することができます。具体的には、取得したい要素にref属性を付与します。それでは、例としてcanvasによる要素取得をrefで試してみましょう。

まず、App.tsxではToggleコンポーネントを使っていましたが、Canvasコンポーネントを読み込み出力するよう変更しました。

import React from 'react'

import Canvas from './components/Canvas'

const App = () => {
  return (
    <Canvas />
  )
}

export default App

そして、Canvasコンポーネントを以下のように記述します。React.createRef()でRefsを作成してcanvasというクラス変数に代入、その後currentにアクセスすることで取得できるようになります。ここではcanvasの描画エリアを400px x 400pxに設定し、その中で100px x 100pxの黒い四角形を描画しています。

import React from 'react'

export default class Canvas extends React.Component {
  canvas = React.createRef<HTMLCanvasElement>()
  cvs: HTMLCanvasElement 
  ctx: CanvasRenderingContext2D

  componentDidMount() {
    this.initCanvas()
  }

  initCanvas() {
    this.cvs = this.canvas.current // get canvas element
    this.cvs.width = 400
    this.cvs.height = 400
    this.ctx = this.cvs.getContext('2d')
    this.ctx.fillStyle = '#000'
    this.ctx.fillRect(150, 150, 100, 100) // draw black rect with 100px * 100px
  }

  render() {
    return (
      <canvas ref={this.canvas}></canvas>
    )
  }
}

reactjs.org

tech-1natsu.hatenablog.com

振り返り

主だったReactの構文をまとめてみました。TypeScriptを使っていることからinterfaceや変数で型定義を行っているためシンプルにReactだけで書くものよりちょっと細かい記述が増えています。その辺りTypeScriptで書くときには留意する必要がありますね。。

React x TypeScript 学習 (3) - import module

概要

Reac x TypeScriptの1,2回目で、React x TypeScriptの開発環境を構築しました。

hikidasu.hatenablog.com

hikidasu.hatenablog.com

そこで、tsxのモジュール読み込みを以下のようにしていました。

import * as React from 'react'  
import * as ReactDOM from 'react-dom'  

ただ、import * as ...の「* as」の部分を都度書くことになり煩雑になりそうです。
今回はこのモジュール記述を簡潔にしていきたいと思います。

実装

tsconfig.json

tsconfig.jsonにいくつかオプション追加で対応できます。
第1回で作成したtsconfig.jsonに追記した結果がこちらです。

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es2015",
    "jsx": "react"
  },
  "include": [
    "./src/**/*"
  ]
}

"allowSyntheticDefaultImports" と "esModuleInterop" を追加してそれぞれ true にセットしています。これにより、先の記述が

import React from 'react'
import ReactDOM from 'react-dom'

と、このように書くことができます。

allowSyntheticDefaultImports

まず、tsconfig.jsonに追記したうちの一つ、allowSyntheticDefaultImports について見ていこうと思います。

www.typescriptlang.org

こちらによると、
Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
とあります。

trueに設定することで、モジュールのデフォルトインポートを許容するようになります。

esModuleInterop

次に、esModuleInterop ですがこちらもドキュメントによると、こう記載されています。
Emit importStar and importDefault helpers for runtime babel ecosystem compatibility and enable --allowSyntheticDefaultImports for typesystem compatibility.

こちらもデフォルトインポートの使用に関する設定になります。

両方の設定は必要か?

allowSyntheticDefaultImports と esModuleInterop を追加して、importの書き方が簡略化できましたが、両方の設定は必要なのでしょうか?実は今の環境でallowSyntheticDefaultImportsを外してesModuleInteropのみ残したままでもimportの書き方を簡略化しつつ、ローカルサーバやビルド時でも正常に動作しました。

esModuleInteropの説明にも、「... enable --allowSyntheticDefaultImports for typesystem compatibility.」とあり、esModuleInteropを有効にすると、allowSyntheticDefaultImportsも合わせて有効にしてくれるようです。

stackoverflowでも以下のやりとりがされていましたが、esModuleInteropは後から出てきた設定で、2018年のPull Requestで上述のようなallowSyntheticDefaultImportsの有効化も行えるようになった模様です。

stackoverflow.com

ちなみに、今回の追記をせずimport * as ...の「* as」部分を削除すると、tsでエラーメッセージが表示されます。

f:id:pujoru35:20190626233948p:plain

エラーメッセージは使用するモジュールによって変わるでしょうが、esModuleInteropフラグを使ったらデフォルトインポートでなんとかなるような具合です。

振り返り

細かな設定がありますが、import文の書き方もtsconfig.jsonのオプション指定で調整できることが分かりました。

React x TypeScript 学習 (2) - DevServer (webpack-dev-server)

概要

Reac x TypeScriptの1回目では開発環境を作ってみました。

hikidasu.hatenablog.com

npm startを実行してTypeScriptファイルをJSへビルド出力しましたが、できるならブラウザで出力結果の画面を見ながらファイル更新を検知してビルドするといった流れにした方が効率的ですよね。
今回は、webpackのオプションでローカルサーバ立ち上げ・ライブリロード(ファイル変更を検知して自動リロードし)をしてくれるDevServer、 webpack-dev-server を導入していきます。

第1回の環境をベースに進めます。

実装

npm

webpack-dev-serverをnpmでインストールします。

npm install --save-dev webpack-dev-server

次に、npm scriptsにwebpack-dev-serverコマンドを追加します。第1回ではnpm startに対しwebpackコマンドにしていましたが、今回webpack-dev-serverコマンドに対応づけをして、npm run buildを叩いたらwebpackコマンドを実行するようにしました(この辺りの対応づけや命名はプロジェクトに合わせて任意で設定してください)。

{
  "name": "project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/react": "^16.8.22",
    "@types/react-dom": "^16.8.4",
    "awesome-typescript-loader": "^5.2.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "source-map-loader": "^0.2.4",
    "ts-loader": "^6.0.4",
    "typescript": "^3.5.2",
    "webpack": "^4.35.0",
    "webpack-cli": "^3.3.4",
    "webpack-dev-server": "^3.7.2"
  }
}

ターミナルでnpm startを実行すると、以下のようなログが出てくるのでブラウザを立ち上げて http://localhost:8080/ にアクセスすると、index.htmlが表示されるようになります。

「wds」: Project is running at http://localhost:8080/
「wds」: webpack output is served from /

DevServerオプション

ローカルサーバを立ち上げることはできたものの、npm run buildを実行してdist配下に出力しない限り、「Hello from TypeScript and React!」の文字列が表示されません。つまり、ローカルサーバを立ち上げた状態でビルド結果を読み込むようにしつつ、ファイル変更を監視し更新分を反映する必要があります。それにはDevServerにオプションとして設定を加える必要があります。

設定方法としては2通りあります。

  1. webpack.config.jsにdevServerオブジェクトを追加しその中にオプションを追加
  2. npm scriptsで実行する、webpack-dev-serverコマンドの後ろにオプションを追加

先ほどローカルサーバを立ち上げましたが、逐一ブラウザを立ち上げてURLにアクセスする必要がありました。例えば、コマンド実行したら自動的にブラウザを立ち上げるようにする場合、 1.ではwebpack.config.jsのdevServerオブジェクトにopen: trueを追加します。

module.exports = {
  mode: 'development',
  entry: './src/index.tsx',
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  },
  devServer: {
    open: true
  },
  ...
}

2.の場合、package.jsonnpm startに対し、webpack-dev-serverコマンドの後ろに--openを追加します。

{
  "name": "project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --open",
    "build": "webpack"
  },
  ...
}  

ビルド結果の読み込み

実はローカルサーバを立ち上げた際、ビルド結果は以下のURLで確認することができます。

http://localhost:8080/bundle.js

実ファイルとして出力されてはいませんが、なぜこのURLになっているかというとデフォルト設定でpublicPathが /になっているためです。
index.htmlでは ./dist/bundle.jsのファイルを参照するようにしているので、DevServerでもこのパスに合わせる必要が出てきます。

そこで、以下のようにpublicPathキーを追加し/dist/にしておけば、ローカルサーバ立ち上げ後「Hello from TypeScript and React!」が表示されるようになります。

module.exports = {
  mode: 'development',
  entry: './src/index.tsx',
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  },
  devServer: {
    open: true,
    publicPath: '/dist/'
  },
  ...
}

Hot Module Replacement

モジュールの更新に合わせてその更新のみ画面に反映させたい場合、webpackのHot Module Replacement(HMR)を使用します。

webpack.js.org

HMRを使うには、webpack.config.jsのdevServerにhotキーを追加します。

module.exports = {
  mode: 'development',
  entry: './src/index.tsx',
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  },
  devServer: {
    open: true,
    publicPath: '/dist/',
    hot: true
  },
  ...
}  

似たようなもので変更を受けてリロードを行うliveReloadもありますが、こちらを使う場合HMRはdisableにしておくのが良いそうです。

webpack.js.org

ファイル変更検知

DevServerで提供されるモジュール以外のhtmlファイル等の変更を検知するには、watchContentBaseキーを追加する必要があります。こちらをtrueにしておくと、変更を受けて画面のリロードを行ってくれます。

module.exports = {
  mode: 'development',
  entry: './src/index.tsx',
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  },
  devServer: {
    open: true,
    publicPath: '/dist/',
    hot: true,
    watchContentBase: true
  },
  ...
}  

上述の設定まで行ったら、ビルド結果を読み込みファイル変更を検知して反映までをローカルサーバで行なえるでしょう。

その他オプションの詳細はこちらを参照すると良いと思います。

webpack.js.org

振り返り

前回に続いて、開発環境よりの話でしたがローカルサーバまで組み込んだので、ReactとTypeScriptについて次回から書いていこうと思います。とはいえ、webpack周りの設定はつまんだ程度のものなので、こちらも知見あれば追加で上げていこうと思います。

React x TypeScript 学習 (1) - 開発環境

背景

現在、ReactとTypeScriptを扱う案件に加わっています。Reactは前職で3案件関わりましたが、そのうち1つが一部UIで取り入れ、他2案件は他のエンジニアのヘルプで参画した程度でした。 一方、TypeScriptは現職前に軽く電子書籍で見た位で、実践になるとVSCodeで色々エラー吐いて四苦八苦している状態です。

いずれも本質的な理解に至っていないので、この際少しずつでも学び備忘録としても書き留めて置こうと思った次第ではじめようと思いました。現状の理解レベルからシリーズ的な構成ではなく、都度感じた疑問や不明点を調べつらつら挙げていこうと考えています。

環境構築

今回は、React + TypeScriptの開発環境を作ってみようと思います。
ReactだとCreate React Appという雛形テンプレートを作るものがあり、オプションでTypeScriptも追加することができるようです。

npx create-react-app my-app --typescript  
  
# or  
  
yarn create react-app my-app --typescript  

facebook.github.io

でも、実案件だとカスタマイズしたものになりがちなので、一から作る前提で用意してみようと思います。
なお、こちらのサイトを参考にしました。

www.typescriptlang.org

ディレクトリ構造

参考サイトに沿って以下の階層で進めます。

project  ... プロジェクトルート  
  |- dist ... tsxファイルのビルド先ディレクトリ
  |- src ... エントリファイル(index.tsx)を格納
  |  |- components ... エントリファイルから読み込まれるコンポーネントファイルを格納
  |- index.html ... dist内のjsを読み込む  
  |  
  |- package.json ... npm initで作成
  |- tsconfig.json ... TypeScriptの設定情報
  |- webpack.config.js ... tsxのビルド設定

npm

以下用途別のパッケージが必要になります。
[ Reactセットアップに使用 ]
- react
- react-dom

[ ReactをTypeScript上で扱う際に必要な型定義ファイル ]
*@types/ というプレフィックスが付いていたりする
- @types/react
- @types/react-dom

[ webpackでtsxをビルドするawesome-typescript-loader、ビルド前ファイルでインスペクトできるsource-map-loader ]
* awesome-typescript-loaderの代わりにts-loaderでも良い
- awesome-typescript-loader
- source-map-loader

[ TypeScript、webpack環境 ]
- typescript
- webpack
- webpack-cli

これらを以下のようにパッケージ名の間をスペースで区切って複数パッケージをインストールします。個別にインストールしてもOK。

npm install --save-dev typescript webpack webpack-cli react react-dom @types/react @types/react-dom

参考サイトでは webpack をグローバルインストールしていますが、ローカルに閉じて管理しておきたかったのでその辺りを変えています。その関係で webpack-cli というパッケージも追加でインストールしています。

パッケージをインストールしたら、package.jsonがこのようになっていると思います(バージョンの違いはありますが)。
npm scriptsを使い、npm startをターミナルで叩いたらwebpackコマンドが実行されるようにしています。

{
  "name": "project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/react": "^16.8.22",
    "@types/react-dom": "^16.8.4",
    "awesome-typescript-loader": "^5.2.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "source-map-loader": "^0.2.4",
    "typescript": "^3.5.2",
    "webpack": "^4.35.0",
    "webpack-cli": "^3.3.4"
  }
}

tsconfig.json

次は、プロジェクト内のts, tsxファイルの設定を司るtsconfig.jsonを見ていきます。

{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es6",
    "jsx": "react"
  },
  "include": [
    "./src/**/*"
  ]
}

compilerOptionsでは「どのディレクトリにどういう形式で出力するのか」を指定できます。他にもtsxファイルでJSXをサポートするjsxオプションや、ソース上any型を許容しないnoImplicitAnyオプションも設定することができます。細かいオプション情報は以下リンクを参照すると良さそうです。

www.typescriptlang.org

webpack.config.js

そして、webpackコマンドを実行した際のwebpack設定を以下のように設定します。

module.exports = {
  mode: 'development',
  entry: './src/index.tsx',
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  },
  devtool: 'source-map',
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.json']
  },
  module: {
    rules: [
      { test: /\.tsx?$/, loader: 'awesome-typescript-loader' },
      { enforce: 'pre', test: /\.js?$/, loader: 'source-map-loader' }
    ]
  }
}

entry, outputオプションで読み込むエントリファイルと出力先とファイル名を指定しています。また、moduleオプションではtsxファイルに対し実行するloader、そして変換されたjsファイルに適用するloaderも設定しています。
なお、modeオプションも設定していないとビルド実行時にwarningが出てしまうので、忘れずに設定しておくと良さげです。

tsx

まずはエントリファイルである、src/index.tsx
Helloコンポーネント<div id="example"></div>要素にレンダリングしています。

import * as React from 'react'
import * as ReactDOM from 'react-dom'

import { Hello } from './components/Hello'

ReactDOM.render(
  <Hello compiler="TypeScript" framework="React" />,
  document.getElementById('example')
)

それから、src/components/Hello.tsxのHelloコンポーネントコメントアウトしていますが、state管理していないため、関数型コンポーネントの書き方でもOKです。 TypeScriptは変数名: 型名で型指定を行うので、関数型は割とイメージしやすかったですが、クラス型の場合 React.Component<Props, State>のように第一引数にprops、第二引数にstateを指定するので、個人的に少し理解に手間取ってしまった感じです。

import * as React from 'react'

export interface Props {
  compiler: string
  framework: string
}

// - Functional Component
// export const Hello = (props: Props) => {
//   <h1>Hello from {props.compiler} and {props.framework}!</h1>
// }

// - Class Component
export class Hello extends React.Component<Props, {}> {
  render() {
    return (
      <h1>Hello from {this.props.compiler} and {this.props.framework}!</h1>
    )
  }
}

html

最後に、プロジェクト直下にindex.htmlを格納。ビルドして/dist/bundle.jsに出力されたファイルを読み込むようにしています。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>React x TypeScript practice</title>
</head>
<body>
  <div id="example"></div>

  <script src="./dist/bundle.js"></script>
</body>
</html>

ここまでやって、npm startをプロジェクト直下で実行した後ブラウザで見ると、「Hello from TypeScript and React!」という文字列が出ているでしょう。

振り返り

開発環境についてはこの辺りで、そこからオプショナルで追加していったり、React /TypsScriptの不明なところをピンポイントで消化していこうと思います。

Webブラウザで物体距離を測るサンプルを作ってみた

またもブログ更新がご無沙汰になってしまいました。。
仕事の兼ね合いからちょっとしたサンプルを作ってみました。

f:id:pujoru35:20180917011604p:plain

経緯

最近、フロントエンドでできる認識系のJSライブラリを色々と試しています。
認識系というのは、例えばよく使われるものだとQRコードの認識だったり、Webコンテンツで比較的取り上げられてきている顔認識から、物体、色、表情etcと色々とあります。色だけだとCanvasを使って静止画像から色を抽出したりできますが、顔や表情等は専用のライブラリやパーツを識別するデータとなる検出器も必要だったりします。

以下、私の方で調べてみたJSライブラリの一部を列記します。それ以外にもいろんなライブラリがありますし、バックエンドも含めると機械学習も関連して認識領域が広がりそうではあります。

認識系JSライブラリ

QRコード
- jsQR

バーコード:
- quaggaJS

顔認識:
- clmtrackr
- face-api.js
- tracking.js
- jquery.facedetection
- js-objectdetect

ポーズ認識(姿勢):
- PoseNet

ここまで調べてきて、「カメラに映った物体距離を識別できないか」と思いました。スマホアプリだと、ARKitやARCoreを使って良い精度で床の位置や2点間の距離まで認識できています。
ただ、通常のWebブラウザだとカメラの深度センサが備わってなかったりするのか、そこまでできていないのが難しいところです。。
そこで、現状のブラウザ仕様で擬似的にでも物体距離を測れないか探ってみました。

アイディア編

着想のヒントになったのが、「画像の2値化」でした。「画像のモノクロ化」と例えてみると良いでしょうか。閾値を超えたかどうかで色を白黒に二分する方法で、OCRによる文字認識や物体の輪郭検出と、認識実装でよく使われる前処理といえます。

カメラに近づくと画面が暗く、光源であれば逆に明るくなっていくでしょう。となると、カメラに映った画像を白黒化し、白の割合が多い(もしくは黒の割合が多い)かで物体が近づいている、離れているかが大雑把ながら識別できるのではと考えました。

実装編

まずは、画像の2値化処理を作ってみました。WebRTCでPC/スマホのカメラにアクセスし、取得したストリーム情報をVideoタグに流し、Canvasタグでその静止画を受け取って2値化処理を行っています。

Image Threshold with canvas | Computer Vision

2値化処理は、こちらの記事を参考にしました。

let src = ctx.getImageData(0, 0, canvas.width, canvas.height)
let dst = ctx.createImageData(canvas.width, canvas.height)

for(let i = 0; i < src.data.length; i = i + 4){
  let y = ~~(0.299 * src.data[i] + 0.587 * src.data[i + 1] + 0.114 * src.data[i + 2])
  let ret = (y > threshold.value) ? 255: 0
  dst.data[i] = dst.data[i + 1] = dst.data[i + 2] = ret
  dst.data[i + 3] = src.data[i + 3]
}

ctx.putImageData(dst, 0, 0)

「STOP」ボタンはWebカメラからのストリームが停止されもう一度押すと再開します。「FRONT」と「REAR」はスマホのみ対応で、フロント・リアカメラの切り替えボタンになります。「Threshold」のチェックを外すと2値化が解除され、横のレンジで2値化の閾値を変更することができます。

そして、先の「白黒の割合をみて近づいたか判定する」工程を以下のようにしてみます。近づいたかどうかをインタラクティブに表現したかったので、Web Audio APIを使って周波数を変更するようにしています(updateAudioFrequency関数のところです)。

let src = ctx.getImageData(0, 0, canvas.width, canvas.height)
let dst = ctx.createImageData(canvas.width, canvas.height)

let isWhite = 0 // 白の割合

/**
 * 画像の2値化
 */
function canvasBinarization(){
  for(let i = 0; i < src.data.length; i = i + 4){
    let y = ~~(0.299 * src.data[i] + 0.587 * src.data[i + 1] + 0.114 * src.data[i + 2])
    let ret = (y > threshold.value) ? 255: 0
    dst.data[i] = dst.data[i + 1] = dst.data[i + 2] = ret
    dst.data[i + 3] = src.data[i + 3]
    isWhite += ret === 255? 1: 0 
  }
}

/**
 * 周波数の更新(2値化した値を以って計算)
 */
function updateAudioFrequency(){
  const total = canvas.width * canvas.height;
  oscillator.frequency.value = BASE_FREQUENCY + ((isWhite / total) - 0.5) * 800
} 

デモ

そうして作ったのがこちらのサンプル。「AUDIO START」のテキストをクリックすると開始します。
カメラの前に手を近づけたり離したりすると音階が変わってきます。もしくは壁や床もありかも?
音の切り替わりを顕著にしようと思ったので、周波数調整の際係数を大きめにしています。この辺りも調整アリかと。

Example 01 | Image Threshold with canvas | Computer Vision

また、音に加えて画面の色味を動的に変えてみたのがこちら。「STOP」ボタンと「Threshold」のUIは割愛しました。

Example 02 | Image Threshold with canvas | Computer Vision

振り返り

作ってみた所感としては、高い精度でなく擬似的な形であれば出来なくはないといったところです。カメラの性能によるところか、近づけて暗くなっても自動的に補正がかかるみたいで、2値化された画像より一瞬明るくなり音域が低くなっていたのが一時的に戻ったりする部分が見受けられました。

また、スマホでの画像2値化は割と処理を食うみたいです。静止画ならそこまで気にならないもののカメラからのストリーム情報、かつ処理結果に応じてWeb Audioや色を動的に変えようとして、WebGLでなくCanvasでやっていたこともあってか、毎フレーム実行すると私の実機がカクカクでまともに動かなかったわけで。。 ひとまず、スマホでは毎フレームでなく30-60フレームに一回処理するという対応をとりました。なので、PCほどリアルタイムではないですが、割と動くようにはなりました。

それから、スマホでやるとジャイロセンサーと違う形のインタラクションが出来そうに感じました。壁や手に近づけるか離すと何かが見えるコンテンツとか、向き・加速度以外のアプローチができそう。

これを作って音まで出せた時、直接触れずに空間中の手の位置によって音の調整ができる、楽器のテルミンを連想しました。
なので、テルミンみたいなWebコンテンツ作りたい方いらっしゃいましたら、お声がけいただけると嬉しいです(笑)

参考

Amazon Echoを触ってみた - 導入編 -

f:id:pujoru35:20180522160134j:plain

こんにちは。
GWの頃から徐々に夏らしい気温になってきました。
インプットだけでなく、このブログを使ってアウトプットしないと、、と次第に焦る昨今です。

さて、今回表題にあるようにこないだAmazon Echoを購入しまして、
初回導入のゆるめなところかもしれませんが、記事に残そうと思いました。

背景

なぜAIスピーカーを買おうと思ったのか、そしてなぜいくつもある中からAmazon Echoを選んだのかを整理してみます(深い理由ではないのですがw)。

実は、AIスピーカーを触ったのは今回が初めてではなく、会社貸与のAIスピーカーの体験会で触ったことがあります(この辺りは以前書いた記事「Bonfire Next #1を通じて感じる、次世代インターフェースの行く末」の末尾にも触れておりました)。
そこでは、Googleが開発したGoogle HomeとLineのClovaがあり、ちょっとした対話でのやり取りをしてみた所感としては以下のようなものでした。

これは日常生活で重用されるんだろうか。。?

割と懐疑的な印象だったような記憶があります。
確かに声をかけるだけで、ニュースを読み聞かせたり音楽を流してくれるというのはスマホで操作するのに比べ簡便ですが、頻度上そんなにやり取りをするのだろうか、たいして利用されないのではという疑問がありました。

ただ、そのあと参加したBonfireのイベントで、こうしたAIスピーカーは筐体に関わらず機能特化型な側面含めVUI(Voice User Interface)領域が盛り上がってくるのでは、という期待を持ちました。それに、限定的な体験会という機会だけで判断するのもどうなのだろう、実際家に置くことで思っていないケースでの利用もできないだろうかという興味がありました。

そういう意味で、AIスピーカーに興味を持ったのですが、ではAmazon Echoを選んだ理由は何だったのか?
単純な話、Amazonが期間限定でEchoを割引販売していたのを見つけたのが直接的な理由ですw。他の理由としては、

  • 体験会の時に使っていないAIスピーカーAmazon Echoだった(当時、招待制で手に入りづらかった)
  • Alexaスキルがあり、開発者向けのカスタマイズができるかなというおぼろげな期待感(Google HomeもそういったものがあるのですがEchoの方に目がいっちゃいました)

Echoのセットアップ

Amazonで購入後、Amazonダンボールに梱包されて届きました。青のベースカラーに黒いスピーカーとスマート感あるパッケージになっています。
開封すると、ACアダプターと薄い冊子上のセットアップ手順書が入っていました。
本体と付属品含め計3点とシンプルな内容に逆に盛り上がってきました。

Amazon Echoと付属品

Echo本体とACアダプターをつなげ、アダプターをコンセントに挿すと起動です。最初青色のランプがスピーカー上部の縁をぐるぐる回り、しばらくしてオレンジ色のランプに変わり、Alexaが挨拶のメッセージを音声でお知らせしてくれます。

f:id:pujoru35:20180522160848j:plain

次に、Alexaアプリをダウンロードします。スマホでアプリストアからダウンロードするか、PCの場合はブラウザでアクセスし、Amazonアカウントでサインインします。今回、PCで試してみました。

f:id:pujoru35:20180524020001p:plain

サインインしたら、新しい端末のセットアップを開始します。 左メニューの「設定」から、「新しいデバイスをセットアップ」を選択します。

f:id:pujoru35:20180524020430p:plain

それから、セットアップするデバイスの種類(Echo / Echo Dot / Echo Plus)、次に言語の種類を選択します。

f:id:pujoru35:20180524020721p:plain

そして、Echoをインターネット接続するため、対象デバイスを自宅のWi-Fiネットワークに接続します。

f:id:pujoru35:20180524020802p:plain

一連の作業が終わったら、セットアップ完了です。
正味10-20分程度とセッティングも手軽でした。

あとは、「Alexa、〇〇して」と呼びかけたら、その日のニュースや天気を教えてくれたり、音楽を流してくれます。また、「ただいま」や「おやすみ」といった簡単な呼びかけにも応答してくれたりします。

振り返り

セットアップをメインにお伝えしましたが、手順も簡単ですぐに扱えるというがいいですね。
セットアップ後に少しばかりやりとりしてみましたが、まだどういうバリエーションに対応できるのか探り探りの状態だったりするので、これからのスキル開発と合わせて色々と試してみようと思います。

気になった広告事例 その1 ( Sapeurs, Mistakes )

こんにちは。
仕事や資料まとめでブログ更新が止まってしまっていたので、そろそろ再開しようと思います。。

はじめに

フロントエンドエンジニアになる前は、デジタルハリウッドに通っており、その時に出会った講師やチューターに様々な展示やイベントを紹介してもらったことがきっかけでアートやデザインといった技術分野外に目を向け、インプットする習慣が身についてきました。

当時、新宿ミラノ座で毎年行われていた 世界のCMフェスティバル に行くのが楽しみだったりしました。世界のCMフェスティバルとは、国内外の傑作CMをオールナイトで鑑賞するというもので、場内にはコーヒーやお酒、フードが売られ、お祭り気分ながらも夜11時くらいから朝5時まで休憩が何回かあるものの、ぶっ続けで観るハードかつ刺激的なイベントでした。

ミラノ座閉館以降、CMフェスティバルに行く機会がなくなってしまったのですが、最近行ったアドミュージアム東京の常設展 で、当時観たCMのいくつかが紹介されていたのに印象を受けました。

観て印象に残ったCMを記憶にとどめとくのもなんなので、備忘録かつなぜ良かったのかを言語化して整理する意味も込め不定期ながらブログに書きとどめていこうと思います。

今回は、先の常設展に展示されCMフェスティバルにも流れていた作品の一部になります。

Sapeurs : Guinness Beer

youtu.be

これは、コンゴで生活する労働者でありながら、サップというカラフルなファッションで身をつつみエレガントに過ごしている「サプール」を取り上げたギネスビールのCMです。

彼らは、自身を鮮やかに彩ることで、戦争の惨禍から抜け出し平和を過ごす「今を生きる喜び」を体現しています。リッチな富裕層かというとそのようでもなく、普段は他の人たちと同じ生活です。なかには、他の人と服を貸借りしている人もいるそうです。

それでもなぜこのような生活をするのか。自身が生きている喜びを表現したい、自分の生き方は他人に左右されるものでなく自分で決めるもの、から来ているのでしょう。困難の中でエネルギーを爆発するというのは、これまでの人の歴史の中にもあったもので、それにも通じているのかもしれません。

youtu.be

また、本CMに流れている曲も非常に印象に残りました。The Heavyの 'What Makes a Good Man?' という曲だそうですが、曲の力強さが彼らサプールの生き方にも通じているように感じます。

www.youtube.com

Mistakes : New Zealand Transport Agency

youtu.be

郊外を進む2台の車。片方は後部座席に子供を乗せた父親が運転し、もう片方はスーツを着て仕事で急いでいると思われます。父親は相手がスピードを緩めてくれると思ったのか、スーツの男も車が横切るのを気づけなかったかで危うく衝突しそうになります。

その瞬間、父親とスーツの男以外の時間が停止し、お互い車を降りて会話をします。ただ、それでもどうにもならず、2人はそれぞれ自分の車に戻った後、時間が再開し結果衝突するCMでした。

本来ならばぶつかるドライバー同士でやりとりすることのない光景を、英語の聞き取りが下手な自分でも直感的にわかりやすくしたCMになっています。そして、衝突するまでの時間に想起する後悔、憤りが強く伝わり、CM最後のメッセージにもあるように、New Zealand Transport Agencyはそうしたスピードの出し過ぎに警鐘を鳴らしています。

振り返り

こうしたCMをみてみると、日本のCMはクライアントのサービスの機能性やメリット(新機能の〇〇とか、半額を謳った感じ)に直結したCMが多いような気がしました。ただ、それは海外のCMにも多少なりあるでしょうし、日本のCMにもトミー・リー・ジョーンズが出演する缶コーヒーのボスやカロリーメイトのCMだったりと、製品自体にフィーチャーするより観る人の意識を変えるようなものもいくつかあります。

また、国内の課題に則したCMになりがちで、海外の課題を取り上げるCMが現実的に難しいのもあるのかもしれません。それもまた、海外のCMにおいても、各国の中での課題になってしまっているのかもしれません。

とはいえ、今はネットで海外の広告を知って観ることもできるため、こうした事例に目を向けつつ自分ができそうな表現を模索してみようと思います。

参考