なになれ

IT系のことを記録していきます。

AngularFireを使うと遭遇するRxJSでのハマりどころ

Angularを使うと漏れなくRxJSを使うことになります。
非同期処理を命令的に書くことがなくなるメリットがある反面、
宣言的プログラミングに慣れていない人にとってはRxJSを理解することが難しいです。
AngularFireを使ってWebアプリを作成する中で、遭遇したハマりどころを紹介します。
この記事では、RxJSの基本的なことは説明しません。

意図せずに処理が呼ばれる

AngularFireを使った場合にストリームがどういったタイミングで流れるかを意識しないと意図しないタイミングで処理が実行されてしまいます。
例えば、DBからデータを取得して、そのデータを条件にしてデータを削除する場合などです。

悪い例

// getUserはユーザの情報を取得して、レスポンスをvalueChangesメソッドによるObservableで返す関数です
// deleteItemは項目を削除する疑似関数です

getUser(id).subscribe((user) => {
    deleteItem(user.itemId);
});

getUser(id: string): Observable<{}> {
    return this.db.doc(`users/${id}`).valueChanges();
}

この場合、ユーザの情報を常に変更検知しているため、ユーザを作成したタイミングで、またこの処理が実行されてしまう可能性があります。

良い例

getUser(id).take(1).subscribe((user) => {
    deleteItem(user.itemId);
});

takeで処理を実行する回数を1回と指定することで、予期しない処理の実行を防止することができます。

コードのネストが深くなる

Observableによる連続的な非同期処理をsubscribeで処理すると非同期処理ごとにネストが深くなってしまいます。

悪い例

// getUserはユーザの情報、getUserDetailはユーザの詳細情報を取得し、レスポンスをObservableで返す疑似関数です。

getUser(id).subscribe(user => {
    getUserDetail(user.id).subscribe(detail => {
        // 処理内容
    });
});

良い例

getUser(id)
    .mergeMap(user => getUserDetail(user.id))
    .subscribe(detail => {
        // 処理内容
    });

mergeMapを使用することで元のストリームから異なるストリームに切り替えることができます。
イメージ的にはPromiseのthenを使って非同期処理を変えるのに似ていると思います。
このようにストリームを切り替えるオペレータにはmergeMapの他にconcatMap、switchMap、exhaustMapなどがあります。

mergeMapでデータが存在しない場合の条件分岐

mergeMapではデータが存在しない場合でもObservableを返す必要があります。

問題

// getUserはユーザの情報、getUserDetailはユーザの詳細情報を取得し、レスポンスをObservableで返す疑似関数です。

getUser(id)
    .mergeMap(user => {
        // userが存在しなかった場合にuser.idでundefinedで落ちる
        return getUserDetail(user.id);
    });

解決策

getUser(id)
    .mergeMap(user => {
        if(!user) {
            return Observable.empty();
        }
        return getUserDetail(user.id);
    });

emptyで何も値を持たないObservableを返します。

DBから取得したリストそれぞれの項目にデータを付与したい

リスト内の各データを条件に他のデータを参照して、リスト内に参照したデータを追記したいというケースがあります。
例えば、ユーザの一覧とユーザの詳細を結合して、一つの一覧で表示したい場合です。

解決策

// getUsersはユーザの一覧、getUserDetailはユーザの詳細情報を取得し、レスポンスをObservableで返す疑似関数です。

getUsers().map(users => {
    return users.map(user => {
        return getUserDetail(user.id).map(detail => {
            return {...user, ...detail};
        });
    });
}).mergeMap(users => {
    return Observable.combineLatest(users);
});

mapをネストさせることで、ユーザの一覧の各データに対して、ユーザの詳細情報を追記しています。
mapのネストによってObservableの中にObservableがある状態になりますが、最終的にcombineLatestでObservableに変えてます。
combineLatestはストリーム内の値を結合することができるので、結果的に中身のObservableだけを取り出しています。

おわりに

最後の例はこれで問題ないのか結構怪しいです。
もっと良い方法があれば知りたいです。

AngularとFirebaseでWebアプリを作った

AngularとFirebaseでWebアプリを作ったので、その記録です。
本の情報を登録して、一覧できるアプリです。
f:id:hi1280:20180310180818p:plain

Webアプリの画面を作成するのに利用できるAngularですが、
さらにFirebaseを活用することでバックエンドのアプリなしでWebアプリを作成できます。

以下、Webアプリを作るのに利用したものについて紹介していきます。

Angular

画面を作るためのベースのフレームワークとして利用しました。
Angular Docs

Angular Material

Angularにマテリアルデザインのコンポーネントを提供するライブラリです。
Angular Material

Paginator
ページ数をつけるコンポーネントを活用しました。
こんな感じのコンポーネントを簡単に配置できます。 f:id:hi1280:20180310181123p:plain pageEventでページ数を変えた時の処理を記述できます。

htmlのコード

<mat-paginator [length]="length"
              [pageSize]="pageSize"
              [pageSizeOptions]="pageSizeOptions"
              (page)="pageEvent = $event">
</mat-paginator>

AngularFire

AngularからFirebaseを利用するためのライブラリです。
GitHub - angular/angularfire2: The official Angular library for Firebase.

Firebase

バックエンドの機能を備えたサービスです。
認証、データベース、ホスティングの機能などがあります。
Firebase

認証

Firebaseのデータベース機能を使うために認証が実用上必須になります。

Googleログインによる認証
認証をGoogleログインに任せる形で実装できます。

typescriptのコード

export class AppComponent {
  user: firebase.User;
  constructor(private afAuth: AngularFireAuth, private router: Router) {
    afAuth.authState.subscribe(user => this.user = user);
  }

  login() {
    this.afAuth.authState.subscribe((user) => {
      if (user && user.uid) {
        this.router.navigate(['main']);
      } else {
        this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider());
      }
    });
  }

  logout() {
    this.router.navigate(['home']);
    this.afAuth.auth.signOut();
  }
}

Googleだけでなく、FacebookTwitterGitHubのアカウントを利用することもできます。

データベース

クライアントから直接データベースを操作することができます。

Realtime Database
本の一覧を取得するのにクエリを活用して以下のようなコードを書きました。
データ作成の降順でソートしつつ、ページングに対応しています。

typescriptのコード

private getBooks(pageSize: number, page: number) {
  this.db.list('books', ref => ref.orderByKey()).snapshotChanges().subscribe((books) => {
    const book = books[pageSize * page];
    // orderByKeyはkeyで昇順にする。昇順によるソートしか対応していない。
    // limitToFirstはデータの取得するに制限をかける。
    // startAtは取得するデータ開始位置を表す。ここではページング対応のために利用している。
    this.booksRef = this.db.list('books', ref => ref.orderByKey().limitToFirst(this.pageSize).startAt(book.key));
    // keyを使うためのコード
    this.books = this.booksRef.snapshotChanges().map(changes => {
      return changes.map(c => ({ key: c.payload.key, ...c.payload.val() }));
    // 降順にする
    }).map((changes) => changes.reverse());
    this.length = books.length;
  });
}

ホスティング

Angularで作成したWebアプリをFirebaseでホスティングできます。

Angularのアプリをビルドします。

$ ng build --prod

FirebaseのCLIツールをインストールします。

$ npm install -g firebase-tools

Firebaseのプロジェクトとして初期設定をします。

$ firebase init

以下のように質問に答えます。

? What do you want to use as your public directory?
dist

? Configure as a single-page app (rewrite all urls to /index.html)?
y

? File dist/index.html already exists. Overwrite?
N

デプロイします。

$ firebase deploy

注意点

Realtime Databaseはクエリ操作が特殊な感じで、機能も充実していないので使う時には注意が必要です。
少なくとも複雑な検索はできません。

MEAN and Cosmos DBをVisual Studio Team ServicesでApp Service on Linuxにデプロイする

AzureのApp ServiceにはWindows版とLinux版があります。
OSSを使用するアプリケーションではLinux上で動作させる方がシンプルな形になりそうです。
Linux版のApp ServiceであるApp Service on Linuxを試してみます。

サンプルアプリの準備

MEAN構成のサンプルアプリとして以下を使用します。
GitHub - hi1280/angular-cosmosdb: Cosmos DB, Express.js, Angular, and Node.js app

前準備

Azure CLIをインストールします。
Azure CLI 2.0 のインストール | Microsoft Docs

Angular CLIをインストールします。

npm install -g @angular/cli

VSTSのプロジェクトを作成します。
Create a VSTS account with a Microsoft account or a work/organization account | Microsoft Docs

Cosmos DBの作成

Cosmos DBを以下のコマンドで作成します。
コマンドのオプションは環境に応じて適宜変更する必要があります。

az login
# リソースグループを作成する
az group create -n my-heroes-db-group -l "Japan East"

# Cosmos DBをMongoDBのAPIで作成する
az cosmosdb create -n my-cosmos-heroes-example -g my-heroes-db-group --kind MongoDB

サンプルアプリとCosmos DBの連携

Cosmos DBの接続文字列を確認します。

az cosmosdb list-connection-strings -n my-cosmos-heroes-example -g my-heroes-db-group --query "connectionStrings[0].connectionString"

example-environment.jsを接続文字列の内容に沿って修正します。
example-environment.jsにはサンプルアプリからCosmos DB(MongoDB)に接続するためのパラメータが定義されています。
example-environment.jsのファイル名をenvironment.jsに変更します。

ローカルでのアプリ起動
# Angular CLIのビルド実行
npm run build

# Node.jsの実行
npm start

http://localhost:3000をブラウザで開きます。

App Service on Linuxの作成

# リソースグループを作成する
az group create --name my-heroes-group --location "Japan East"

# App Serviceプランを作成する
az appservice plan create --name my-app-heroes-plan --resource-group my-heroes-group --sku B1 --is-linux

# App Service on Linuxを作成する
az webapp create --resource-group my-heroes-group --plan my-app-heroes-plan --name my-app-heroes-example --runtime "NODE|8.1"

Visual Studio Team Services(VSTS)との連携とデプロイ

サンプルアプリのコードをVSTSリポジトリにPushします。
Git index to content for VSTS & TFS | Microsoft Docs

ビルド定義

ビルドタスクは以下の通りに定義します。

Get Sources

  • This accountを選択します
  • 該当のリポジトリとブランチを選択します

npmパッケージをインストール

  • Add Taskをクリックし、npmを追加します
  • installコマンドを選択します

Angular CLIのビルド実行

  • Add Taskをクリックし、npmを追加します
  • customコマンドを選択します
  • Command and argumentsrun buildと入力します

サーバのnpmパッケージをインストール

  • Add Taskをクリックし、npmを追加します
  • installコマンドを選択します
  • Working folder with package.jsondistと入力します

成果物を公開する

  • Add Taskをクリックし、Publish Build Artifactsを追加します
  • Path to Publishdistと入力します
  • Artifact Namedistと入力します
  • Artifact publish locationVisual Studio Team Services/TFSを選択します

f:id:hi1280:20180217232228g:plain

リリース定義

リリースタスクは以下の通りに定義します

Artifact

  • ビルドのArtifactの最新を選択します

Azure App Serviceにデプロイする

  • Run on agentの+マークをクリックし、Azure App Service Deployを追加します
  • Azure subscriptionを選択します
  • App typeLinux Web Appを選択します
  • App Service nameを選択します
  • Package or folder$(System.DefaultWorkingDirectory)/my-app-heroes-example-build/distと入力します
  • Runtime StackでNode.jsを選択します

f:id:hi1280:20180218002023g:plain

感想

App ServiceのWindows版とほぼ変わらずに構成することができました。
Linux版ではそのままNode.jsが動作しているのでVSTSの設定もシンプルになっています。
Windows版ではIIS上でNode.jsのアプリケーションを動かすようになっていて、IISの設定が必要といった勝手が違う部分があります。
今後はLinux版を使っていくことになりそうです。

Amazon Echoとラズパイでスマートホーム化

Amazon Echoとラズパイでスマートホームを実現します。
Amazon Echoに対して音声で呼びかけて、Alexaスキルからラズパイを通して家電を操作します。

構成図

f:id:hi1280:20180129215335p:plain

AWS IoTの準備

AWS IoTを使って、Alexaスキルとラズパイとを連携します。
バイスの設定を案内通りに進めることで、該当のデバイスAWS IoTに接続することができます。
AWS IoTに接続するのに必要な証明書が得られます。
f:id:hi1280:20180129225533p:plain

今回はNode.jsを使用しました。
Node.jsでAWS IoTを使用したメッセージングの簡単な例はこちらが参考になります。
aws-iot-device-sdk-js/README.md at master · aws/aws-iot-device-sdk-js · GitHub

ラズパイでのAWS IoTプログラムの実装

AWS IoTのdeviceオブジェクトに証明書の情報を設定します。

AWS IoTからのメッセージを受け取って、赤外線を送信するコード

subscribeメソッドで受け取るメッセージを指定するのと、メッセージを受け取った後に実施する処理をmessageイベントで定義します。

AWS IoTの動作確認

テスト > 発行 からメッセージを発行することができます。
f:id:hi1280:20180130000103p:plain

ラズパイでの家電の操作方法は過去記事を参照してください。
hi1280.hatenablog.com

Alexaスキルの実装

Alexaスキルの種類はカスタムスキルとして実装しました。
カスタムスキルは名前を指定して、スキルを呼び出します。
例えば、こんな呼びかけになります。
「アレクサ、リモコンで照明をつけて」

「リモコン」の部分がスキルの名前になります。

AWS IoTのメッセージを送信するAlexaスキルのコード

AWS IoTのdeviceオブジェクトのpublishメソッドを実行することで、メッセージを送信します。

Alexaスキルの作成方法は過去記事を参照してください。
hi1280.hatenablog.com

まとめ

ラズパイでの赤外線による家電操作とAlexaスキルを組み合わせることでスマートホームっぽくなりました。
今回はスキルをカスタムスキルとして実装しましたが、スキルの名前をいちいち呼ぶのは面倒です。
スマートホームスキルとして実装すると名前を呼びかける必要がなくなるようです。
ただし、スマートホームスキルに対応しているデバイスがまだ少ないようなので、まだこれからという状況かと思います。
スマートホームスキルを複数の言語で開発する | ASK

Web画面経由でコマンドを操作する

Web画面経由でコマンドを操作してみました。
Node.jsでExpressを利用しています。

今回の実装時のポイントは以下の通りです。

Node.jsでコマンドを実行する

Node.jsの標準APIであるchild_processを使用します。
以下のようにexecメソッドでコマンドを実行することができます。

const { exec } = require('child_process');
exec('echo "The \\$HOME variable is $HOME"', (error, stdout, stderr) => {
  console.log(stdout);
});

画面を描画する

画面の描画にはejsを使いました。
ejsファイルではありますが、内容はHTMLそのものです。

ejs.renderFile('./views/index.ejs', (err, str) => {
  if (err) {
    res.send(err.message);
  }
  res.send(str);
});

感想

簡単なWebアプリケーションを作るのにExpressは楽だと思います。
今回の方法を使ってラズパイで作成した赤外線リモコンをWeb画面経由で操作しています。

hi1280.hatenablog.com

参考情報

Child Process | Node.js v9.4.0 Documentation

ラズパイの赤外線リモコンを高性能にした

hi1280.hatenablog.com

以前に作成したラズパイの赤外線リモコンを高性能なものにするために改良しました。
遠距離でも赤外線が届くようにするのと複数のリモコンに対応しました。

配線を変更

赤外線LEDを高出力なものに変更しました。

akizukidenshi.com

赤外線LEDを2つ付けて、広範囲に赤外線を出力するようにしました。

f:id:hi1280:20180123225338p:plain

複数のリモコンに対応

リモコンの数だけ以下のコマンドで赤外線のデータを記録します。

irrecord -f -d /dev/lirc0 --driver default  ~/lircd.conf

記録したlircd.conf全てを/etc/lirc/lircd.conf.d/に配置します。
/etc/lirc/lircd.confは以下の内容にします。

include "lircd.conf.d/*.conf"

これによって、/etc/lirc/lircd.conf.d/にある全ての赤外線のデータが読み込まれます。

感想

LEDを変えたことで、2mくらい離れていても反応するようになり、便利になりました。
赤外線を広範囲にし、複数のリモコンの赤外線データを扱えるようにしたことで、複数の機器を操作できるようになりました。

参考情報

複数のリモコンに対応
LIRC - Linux Infrared Remote Control

Electronでデスクトップを録画するアプリが簡単に作れました

ElectronはAtomに使われていたり、VSCodeに使われていたりとPCネイティブなアプリを作るために最近よく使われているライブラリです。
今まで使ったことがなかったので使ってみました。今回作ったアプリのリポジトリこちらです。
アプリ作成のポイントを紹介します。

題材

デスクトップのウィンドウやスクリーンを録画するアプリを作ります。

作り方

環境構築

Electronアプリを作るにあたってのテンプレートにはelectron-vueを使用します。
導入 · electron-vue

コマンド操作だけでElectron製のアプリを起動するところまで準備してくれます。

デスクトップの録画処理

Electronに用意されているAPIであるdesktopCapturerを使用すると、ウィンドウやスクリーンの動画を取得することができます。
desktopCapturer | Electron

// In the renderer process.
const {desktopCapturer} = require('electron')

desktopCapturer.getSources({types: ['window', 'screen']}, (error, sources) => {
  if (error) throw error
  for (let i = 0; i < sources.length; ++i) {
    if (sources[i].name === 'Electron') {
      navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          mandatory: {
            chromeMediaSource: 'desktop',
            chromeMediaSourceId: sources[i].id,
            minWidth: 1280,
            maxWidth: 1280,
            minHeight: 720,
            maxHeight: 720
          }
        }
      }, handleStream)
      return
    }
  }
})

function handleStream (stream) {
  document.querySelector('video').src = URL.createObjectURL(stream)
}

desktopCapturer.getSourcesでウィンドウやスクリーンを取得することができます。
navigator.mediaDevices.getUserMediachromeMediaSourceIdに取得したsourceのidを指定することで各ウィンドウやスクリーンのStreamデータを取得することができます。
minWidthmaxWidthminHeightmaxHeightを変更することで動画のサイズを変更することができます。
maxWidthmaxHeightに指定した値のサイズまでの動画になります。

動画の保存にはWeb標準APIのMediaStream Recording APIを使用します。
MediaStream Recording API - Web APIs | MDN

<a id="download">Download</a>
<script>
  const downloadLink = document.getElementById('download');
  var handleSuccess = function(stream) {
    const options = {mimeType: 'video/webm'};
    const recordedChunks = [];
    const mediaRecorder = new MediaRecorder(stream, options);
    mediaRecorder.ondataavailable = (e) => {
      recordedChunks.push(e.data);
    };
    mediaRecorder.onstop = (e) => {
      downloadLink.href = URL.createObjectURL(new Blob(recordedChunks));
      downloadLink.download = 'video.webm';
    };
    mediaRecorder.start();
  };
  navigator.mediaDevices.getUserMedia({ audio: true, video: true })
      .then(handleSuccess);
</script>

navigator.mediaDevices.getUserMediaで取得したStreamデータでMediaRecorderインスタンスを作成することで、Streamデータを動画として保存することができます。

工夫したところ

録画開始や録画停止の際にショートカットキーを使えるようにしました。

Electronでは、ショートカットキーの設定にいくつかのアプローチがあります。
Keyboard Shortcuts | Electron

今回は、Local ShortcutsとGlobal Shortcutsを使いました。

Local Shortcuts

アプリのメニューを作り、メニュー項目を実行するショートカットキーを設定する方法です。
まずはメニューを作る必要があります。
Menu | Electron

メニューを作る場合、通常はMain processというElectronアプリのエントリーポイントとなるプロセスでメニューを作成する必要があります。
今回はメニュー項目の設定変更を画面描画を担当するプロセスであるRender process上で行う必要があったため、Render processでメニューを作成しました。
Electronのremoteモジュールを使用することでRender processでもメニューを作成することができます。

// In the renderer process.
import * as electron from 'electron'
const {Menu} = electron.remote

const template = [
  {
    label: '画面',
    submenu: [
      {
        label: '画面収録を開始',
        accelerator: 'CmdOrCtrl+P',
        click: function (menuItem, browserWindow, event) {
          browserWindow.webContents.send('startRecord')
        }
      }
    ]
  }
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)

メニューの作成は、buildFromTemplateメソッドを使用すると簡単です。
メニューの仕様に準拠したJavaScriptオブジェクトを渡すことで、オブジェクトの値に応じたメニューを作成してくれます。

acceleratorプロパティでショートカットキーを設定することができます。
Accelerator | Electron

clickプロパティでメニュー項目が選択された時の動作を記述することができます。
引数のbrowserWindowがRender processが動作するウィンドウなので、browserWindow.webContentsを介することで、Render processに処理を渡すことができます。
webContents | Electron

アプリのウィンドウがスクリーン上に存在しない場合、browserWindownullになるため注意が必要です。
今回のアプリでは、browserWindownullの場合、Main processからRender processに処理が流れるようにしています。
ElectronではIPCのAPIでプロセス間の通信を行うことが可能です。
ipcMain | Electron

Global Shortcuts

ショートカットキーのみを設定する方法です。アプリにフォーカスがなくても機能します。
Keyboard Shortcuts | Electron

// In the main process.
import { BrowserWindow, globalShortcut } from 'electron'
let mainWindow = new BrowserWindow({
  height: 563,
  useContentSize: true,
  width: 1000
})
globalShortcut.register('CommandOrControl+E', () => {
  mainWindow.webContents.send('stopRecord')
})

globalShortcutでショートカットキーを設定することができます。

感想

Webの技術でPCネイティブなアプリを作ることができました。
Webの技術でネイティブなアプリというとモバイル向けのCordovaが思い浮かびます。
Cordovaは複雑というイメージだったので、Electronもそんな感じかなというイメージでした。
触ってみると、Electronはシンプルで分かりやすかったですし、electron-vueのファイル構成もそんな感じでした。
Electronを使うとメニューを作るといったネイティブな機能が簡単に実現できるのが驚きでした。

アプリを使って実際に録画した動画です。
youtu.be

参考

MediaStream Recording APIの使い方を参考にしました
Recording Video from the User  |  Web  |  Google Developers

アプリの画面デザインを参考にしました
デスクトップを録画するアプリを書いた - Qiita