Dockerでローカル開発用のMySQL DBを用意する

ローカルで開発を行う際、プロジェクト毎に独立したDBを使いたいのでDockerで立てる

config/database.yml

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  ? password
  host: 127.0.0.1
  port: 13306

docker-compose.yml

version: "3"
services:
  db:
    image: mysql:5.7
    volumes:
      - db:/var/lib/mysql
    ports:
      - 13306:3306
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"

volumes:
  ? db
  • $ docker-compose up で立てて 127.0.0.1:13306 として使う
  • GUIクライアントからも接続可

オンライン同期可能なシーケンサーを作った話 後編

今回作ったもの

DEMO
ソースコード

詳しい話は前編を参照

nomusiclife.hatenablog.jp

Firebase

個人開発の強い味方、Firebase。
HostingやAuthも使っているが、今回はデータの保存と同期のため、Cloud Firebaseを重点的に使った。

Cloud Firestore

Firebaseには従来からあるRealtime Databaseと
新しく追加されたCloud Firestoreという2つのデータベースがあり、今回は後者のCloud Firestoreを使った。

Realtime DatabaseとCloud Firestoreの違いは下記にまとまっている

データベースを選択: Cloud Firestore または Realtime Database  |  Firebase

Snapshot Listener

Cloud Firestoreではドキュメントへの変更を検知するために onSnapshot でスナップショットリスナーのコールバックを登録することができる。

const doc = firebase.firestore().doc(`/sessions/${sessionId}`)
doc.onSnapshot((changed: firebase.firestore.DocumentSnapshot) => {
  const data = changed.data() as Session
  actions.updateSession(data) // Reduxのstoreを更新
})

コールバックは以下の条件で発火する。

  • リッスンしているドキュメントに対してリモートで変更が行われる
  • ドキュメントをローカルで更新する

これが、Firestoreにおける同期やオフライン対応の基本となる。

データフローの変更

当初は、ローカルで行った操作の結果をリモートに送信する。つまり、Redux Stateにて行われた変更をCloud Firestoreに同期する想定だった。

f:id:nishaya:20180911133711p:plain

しかし、Snapshot listenerがローカルへの変更に対し即時に発火することがわかったのでフローを変更し、

  1. Firestoreドキュメントを変更
  2. ドキュメントのonSnapShotコールバックが発火
  3. コールバックからReduxのactionをdispatch
  4. Redux stateの変更をmapStateToPropsでpresentational componentに伝播させる
  5. リモートのCloud Firestore DBへの同期を行う

とした。

f:id:nishaya:20180911133725p:plain

DBへの書き込みは非同期で行われ、クライアントはドキュメントの更新が成功したものとして動作するため、楽観的UIの実現が可能になる。

追加実装なしでマルチクライアント同期を手に入れる

上記の変更により、マルチクライアント間の同期を追加実装なしで手に入れることができた。

f:id:nishaya:20180911133737p:plain

ドキュメントを変更したクライアントから、変更内容がFirestore DBに同期されると、同じドキュメントをlistenしている別のクライアントでもonSnapShotコールバックが発火し(7)、Redux stateの変更(8)を経てpresentational componentに変更内容が伝わる。

ただし、書き込みしすぎに注意

Firestoreでは、ドキュメントへの書き込みに1秒に2回までという制限がありスライダー等、連続的変化を伴うUIによる値の変化を逐次ドキュメントへのupdateで反映するとすぐにrate limitに達してしまう。

f:id:nishaya:20180911133931p:plain

そのため、UIへの反映とドキュメントの更新タイミングは、

  • 変更を適度に間引く
  • 一定期間中の変更を1回のupdateにまとめる
  • スライダーを動かしている最中の変更はRedux Stateに直接反映し、スライダーが止まったときの値のみをドキュメントに反映

といった工夫が必要になる。

基本的な戦略としては、永続化される必要のない変更をRedux Stateに直接渡し、永続化が必要な変更のみをドキュメントに反映させることになる。

TypeScript

TypeScriptは create-react-app-typescript を使用して導入した。

User-Defined Type Guard

シンセのプリセットごとに表示させるコンポーネントを切り替えるため、実行時型チェック用にUser-Defined Type Guardを用意した。

Advanced Types · TypeScript

export type SynthPreset = OscSynthPreset | FmSynthPreset | DrumsSynthPreset

export interface BaseSynthPreset {
  type: SynthPresetType
}

export function isOscSynthPreset(v: any): v is OscSynthPreset {
  return v.type === 'osc'
}

export interface OscSynthPreset extends BaseSynthPreset {
  type: 'osc'
  oscillator: OscillatorType
  aeg: ADSR
  cutoff: number
  resonance: number
}

所感

  • マルチクライアント間の同期を前提とする場合、クライアント内のデータフローを再考する必要がある
    • Firebase Cloud Firestoreは近道の一つ
    • APIがGraphQLであれば、Apollo Clientを使って同じようなパターンの実装ができそう
  • TypeScriptはいいぞ
    • これ以降に作ったプロダクトは全てTypeScriptを使うようになったのだが、それについてはまた別の機会に書く

TSLint 普段使っている最小限の設定

プライベートで何か作ったりするときの TSLint config についてまとめておく。

tslint.json

{
  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts",
      "coverage/lcov-report/*.js"
    ]
  },
  "rulesDirectory": ["tslint-plugin-prettier"],
  "rules": {
    "no-default-export": true,
    "interface-name": false,
    "ordered-imports": true,
    "jsx-no-lambda": false,
    "object-literal-sort-keys": false,
    "prettier": true
  }
}

rules

create-react-app のデフォルトからあまり触っていないが、以下の rules を弄っている。

  • no-default-export: named export を強制するため true にしている
  • interface-name: 大文字 I から始まる interface 名を禁止
  • ordered-imports: インポート順をアルファベット順にして探しやすいように true
  • jsx-no-lambda: Query Component pattern などで使うので false
  • object-literal-sort-keys: object の key はアルファベット順よりも重要度で並べたり、分類ごとにまとめたりしたいので false
  • prettier: tslint-plugin-prettier をインストール($ yarn add -D tslint-plugin-prettier)し "rulesDirectory": ["tslint-plugin-prettier"] を追加して true にするとプロジェクトの .prettierrc(.json) が効くようになる

メンバーの多いプロジェクトでは、さらに必要に応じてrulesを追加したり無効化したりする

Visual Studio CodeでReact Appを開発する際の準備

Visual Studio Codeを使ってReact Appの開発を行う際に、
最低限追加しておきたいextensionsと設定をまとめておく

前提

プロジェクトでは以下を使用するものとする

  • create-react-app
  • TypeScript
  • styled-components

extensions

TSLint

TSlintの結果をインライン表示する。

settingsに以下を追加すると(修正可能なものは)ファイル保存時に自動的に修正されるようになる

"tslint.autoFixOnSave": true,

Prettier - Code formatter

Prettierによるコードフォーマットを行う。

  • formatOnSave: 保存時にフォーマット
  • requireConfig: プロジェクトに .prettierrc(.json) が存在する場合のみフォーマットを行う
"editor.formatOnSave": true,
"prettier.requireConfig": true,

Jest

コードの変更によって影響を受けたテストを即時実行できるようにする

vscode-styled-components

styled-componentsCSSをハイライトする

settings

ロケールの変更

TypeScriptの警告は日本語だと読みづらく、検索効率も悪いため英語にしておく

Visual Studio Code の表示言語を英語に戻した

CLI "code" のインストール

以下に記載された手順により code で起動できるようになる

Running Visual Studio Code on macOS

$ cd project_dir
$ code .

以上

思いついたら追記するが、他は好みで導入するのがよいと思っている

オンライン同期可能なシーケンサーを作った話 前編

少し前になるが、また音の出るおもちゃを作ったので記録を残す。
長くなりそうなので前編/後編に分ける。

作ったもの

DEMO
ソースコード

  • ブラウザで動くマルチトラックシーケンサ
  • 作成中の楽曲をオンライン保存できる
  • URLシェアにより、別のユーザと楽曲の同時編集ができる
    • 編集内容は複数クライアント間でリアルタイム同期される

f:id:nishaya:20180906180720p:plain

使ったもの

  • React/Redux
  • Firebase
    • Cloud Firestore
    • Authentication
    • Hosting
  • TypeScript
  • Web Audio API

発表資料

speakerdeck.com

モチベーションと課題

Reactで音の出るおもちゃを作るのが好きなので、Reactでシンセサイザーを作ったり、Reactでパターンシーケンサを作ったりしてきた。

今まで作ってきたものは音を出したり、パターンの変化を楽しんだりとどちらかといえば即興性の高いものだったが、「曲を作る」となると保存しておき続きを作り進められることや、他者と協力して作曲ができることが求められるだろうと考え、その検証として今回はオンライン保存、同期編集が可能なシーケンサを作るに至った。

  • ブラウザ上で音を鳴らせる
  • タイミング通りにシーケンス(作成した曲データ)を鳴らせる

という要件については今までの成果物で検証できていたので、今回は

  • ブラウザ上でシーケンスをグラフィカルに編集できる
  • 編集したものをオンライン保存できる
  • 保存したものをシェアして複数のクライアントから同時編集できる

といった点にフォーカスを当てて実装を行った。

完成したものについてはデモを触ってみていただきたいが、実装を進める中で発見のあった箇所について書きたいと思う。

ピアノロールの実装

f:id:nishaya:20180906180930p:plain

シーケンス編集の手段としては、一般的なDAW(音楽制作ソフトウェア)なら大抵備えているピアノロールを採用した。
ピアノロールはx軸で時間、y軸で音階を表現するUIだ。

探してみると、Reactでピアノロールを実装している例はいくつかあったものの、そのまま利用できそうなものがない、理想とするピアノロールと違うといった理由から、自前で実装することにした。

ちなみに、私が理想とするピアノロールは、かつて存在したOpcode社のEZ Visionというシーケンサのものなので、今回作ったものもそれに似た挙動になっている。

SVG as JSX

ピアノロールの実装はSVGをReact Componentに内包する形で実装した。
SVGのタグをJSXの中で使うことで、以下のようなメリットが得られた。

  • 図形のグループをReact Componentに切り出しやすい
  • svg elementを普通に記述するとそれなりに多くのattributesを渡す必要あるが、Spread Attributesを使うとシンプルに書ける

例):

<rect {...rectProps, x } />

また、クライアント座標とは別の座標系を扱うことができるので、音楽のコンテキストにマッチした座標を使えるといったメリットもある。

  • 例: 1小節あたりのclient widthが137pxといった半端な値でも、SVG内ではwidthを480とし、端数を出さずに16分割や12分割を行う(=16分音符や3連8分音符を表現する)ことができる
  • client座標 -> SVG座標への変換には以下のようなメソッドを用意した
mouse2svgPoint(e: MouseEvent): SVGPoint {
  const pt = this.svgElement.createSVGPoint()
  const matrix = this.svgElement.getScreenCTM()
  if (!matrix) {
    pt.x = 0
    pt.y = 0
    return pt
  }
  pt.x = e.clientX
  pt.y = e.clientY
  return pt.matrixTransform(matrix.inverse())
}

FirebaseやTypeScriptについては後編で書く

nomusiclife.hatenablog.jp

Ant Designのカスタマイズ内容をStorybookに反映するための設定

Ant Design(antd)のテーマをカスタマイズした内容をStorybookに反映する場合、少しややこしい設定が必要になるのでメモ

前提

  • プロジェクト本体はcreate-react-appで作成
  • TypeScriptを使用
  • antdのバージョンは3.7.3

設定方法

例: デフォルトのfont-familyを変更したい場合

antdTheme.js

module.exports = {
  '@font-family': '-apple-system, Sans-Serif'
}

.storybook/webpack.config.js

const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js')
const tsImportPluginFactory = require('ts-import-plugin')
const modifyVars = require('../antdTheme')
const path = require('path')

module.exports = (baseConfig, env) => {
  const config = genDefaultConfig(baseConfig, env)

  config.module.rules.push({
    test: /\.tsx?$/,
    exclude: /node_modules/,
    include: [/src/],
    loader: 'ts-loader',
    options: {
      getCustomTransformers: () => ({
        before: [
          tsImportPluginFactory({
            libraryName: 'antd',
            libraryDirectory: 'lib',
            style: true,
          }),
        ],
      }),
    },
  })
  config.resolve.extensions.push('.ts', '.tsx')

  config.module.rules.push({
    test: /\.less$/,
    use: [
      {
        loader: 'style-loader',
      },
      { loader: 'css-loader' },
      {
        loader: 'less-loader',
        options: { modifyVars },
      },
    ],
    include: path.resolve(__dirname, '../src/', '../node_modules/'),
  })

  return config
}

Apollo ServerとNowで作るGraphQLモックサーバ #4 モニタリング編

前回の続き。

nomusiclife.hatenablog.jp

作成したGraphQLサーバをモニタリング可能な状態にする。

Apollo Engine

Apollo Engine | The GraphQL Gateway with essential features like caching and performance tracing.

  • GraphQL caching
  • Query execution tracing
  • Error tracking
  • Trends

などの機能を備える

  • モニタリングしたいだけなのでcachingは今回使わない
  • アラートは有償版でのみ有効
  • 有償版は$99/mo~(割と高い)

導入

最初にGraphQLのスキーマをアップロードする

npm i -g apollo
apollo schema:publish --endpoint=<your graphql endpoint here> --key="<your key>"

次に、App Engineを利用するためのセットアップを行う

API Reference: apollo-server | Apollo Server

Provided the ENGINE_API_KEY environment variable is set, the engine reporting agent will be started automatically.

apollo-serverは環境変数 ENGINE_API_KEY がセットされていれば、 その値を使ってapollo-engineを有効化するようになっている。

環境変数.env に記述したものを nowの --dotenv オプションに渡してセットする。

ENGINE_API_KEY=service:foo-bar-baz:xxx...
$ now --dotenv=.env.production
$ now alias

確認

Usage Metrics

デプロイされた環境にアクセスし、Playgroundでいくつかクエリを実行した後、 Apollo Engineの "Metrics" の項目を見ると実行したクエリのログが確認できる。

  • リクエストの頻度
  • パフォーマンスの概要
  • エラー率

f:id:nishaya:20180904081654p:plain

クエリ名ごとにさらに詳細な情報を確認することもできる。

f:id:nishaya:20180904081742p:plain

Daily Report

Slack integrationを設定しておくとdaily reportでモニタリングのサマリを受け取ることもできる。

f:id:nishaya:20180904081807p:plain

Schema Explorer

Schema Explorerではschemaに含まれるtypeの一覧と、それぞれの使用状況を確認することができる。

f:id:nishaya:20180904081821p:plain

例えば、これを見るとPerson.addressは全く使われていないことがわかるので @deprecated にする、といった判断に使える。

Schema History

schemaの変遷を確認できる。 例えば、Person.addressを @deprecated に変更した場合、以下のように表示される。

f:id:nishaya:20180904081832p:plain

まとめ

  • 小一時間くらいあればここまで準備できる
  • あとはSchema足していってResolverをひたすら書く
    • とりあえずクライアントの動作を確認するためだけのモックサーバならmock書いていくだけでもOK
  • GraphQLのサーバ運用するのはそれなりに労力かかる(と聞く)が、モニタリングあればただ闇雲にやるよりはずっと安心