- 投稿日:2021-01-27T22:42:50+09:00
【docker】Node.jsのコンテナ立ち上げてサンプルアプリを動かしたい
この記事の目標
dockerのコンテナ立ち上げて、このアプリ↓をローカル環境で立ち上げる
https://github.com/justadudewhohacks/face-api.js
GitHubからリポジトリcloneする
ローカルの任意のディレクトリにcloneする
$ git clone https://github.com/justadudewhohacks/face-api.jsdocker hubからnode.jsのイメージpullしてくる
alpineが軽量でオススメらしいので、v14のをpullする
$ docker pull node:14-alpineイメージをpullできたことの確認
$ docker imagesコンテナ立ち上げる
docker run -it -v $PWD:/workspace -p 8100:3000 --name node-face-api node:14-alpine /bin/ash
-itで、コンテナ内で入力可能な状態にする-v $PWD:/workspaceで、カレントディレクトリのファイルをコンテナ内の「workspace」ディレクトリにマウントするように指定
- そのため、リポジトリをcloneしたディレクトリで作業する必要あり
$PWDは$(pwd)といった指定方法もありらしい-p 8100:3000で、ホスト側のポート8100とコンテナ側のポート3000を繋ぐ
- このホスト側のポート番号は空いているものを適宜使う
- コンテナ側はよしなに設定
- ポート使用状況はMacの場合「ネットワークユーティリティ」で確認できる
--name node-face-apiで、コンテナ名を「node-face-api」に設定node:14-alpineで、イメージとバージョン(TAG?)の指定/bin/ashで、コンテナ内で使うコマンドを指定今回は使ってないが、以下のオプションもよく使われる
--rmで、コンテナから出た時にコンテナ削除※ちなみに、コマンドの記述順は、以下の通りらしい
$ docker run [OPTIONS] IMAGE [COMMAND] [ARG...]起動時にエラーが起こって上手くコンテナに入れなかった場合、以下の手順でコンテナ削除し、
runコマンド見直してから再度試す# コンテナ一覧表示 $ docker ps -a # 起動(作成)に失敗したコンテナのIDを指定してコンテナ削除 $ docker rm [CONTAINER ID]コンテナ内でアプリケーションを立ち上げる
コンテナ作成して中に入ると、ホストのファイル群がworkspaceディレクトリ内にあるはずなので、確認する
$ cd workspace $ lsあとは、face-ap.jsのチュートリアル通りに進めていくだけ
$ cd examples/examples-browser $ npm install $ npm startホストマシンのポートは8100に指定したので、以下にアクセス
それっぽいページが表示されればOK!
参考
- 投稿日:2021-01-27T18:23:24+09:00
pug内のパスをnodeの開発環境で切り替える
テスト環境/本番環境でディレクトリ階層が違というプロジェクトがあり、pugの変数をビルドのたびに手動で切り替えるようにしていました。
「nodeでビルドするんだからnodeの環境開発で切り替えれるっしょ!!」
と思ってやってみたらやっぱり出来たので、備忘録として書き残しておきます。環境
- pug ^3.0.0
- html-webpack-plugin ^4.5.1
- pug-loader ^2.4.0
pugは、
pug-loader。html-webpack-pluginを用いてwebpack内でビルドしています。
ここでのミッションは、webpack経由でどうやってpugに環境変数を埋め込むか?です。
(下記の方法はpug出なくても素のhtmlでも可能です。)pug内に環境変数を埋め込む
plugins: [ new HtmlWebpackPlugin({ title: 'My App', filename: 'assets/admin.html' }) ]これを
plugins: [ new HtmlWebpackPlugin({ title: 'My App', filename: 'assets/admin.html', environment: process.env.NODE_ENV }) ]こうですね。environmentはドキュメントなどにある提供されているプロパティではなく勝手につけたプロパティのようです。
つまりfooでもtestでもなんでもOKです。- var appEnv = htmlWebpackPlugin.options.environment;pug内で上記のよう先ほど追加したプロパティ経由で
process.env.NODE_ENVにアクセスすれば、dev/proでpugを分岐出来ます。もちろん、コマンドを叩くときはNODE_ENVのセットを忘れずに。
package.json"dev": "NODE_ENV=development webpack --mode production", "build": "NODE_ENV=production webpack --mode production"追記
ちなみに modeを指定した場合、コマンドで指定した
NODE_ENVは無視されて
mode production ではprocess.env.NODE_ENVにproductionが、
mode developmentではprocess.env.NODE_ENVにproductionが入ってしまいます。
これをやめたい場合、webpackにoptimizationのnodeEnvをfalseにし、
DefinePlugin経由でNODE_ENVを渡してあげるといいです。webpackoptimization: { nodeEnv: false, },webpackplugins: [ new webpack.DefinePlugin({ NODE_ENV: JSON.stringify(process.env.NODE_ENV), }), ],jsconsole.log(`node_env: ${NODE_ENV}`) // NODE_ENV=test とコマンドで叩くと testが取得できるおまけ HTMLでは?
<body class="<%= htmlWebpackPlugin.options.environment %>">参考サイト
https://stackoverflow.com/questions/39902197/how-can-i-pass-webpack-environment-variables-in-html
- 投稿日:2021-01-27T17:34:56+09:00
M1マシンでflowが起動しない問題の対応
事象
M1(apple silicon)マシンでrosettaを使って入れたnode.jsで
npx flowなどでflowを起動すると以下のようなエラーが出て起動に失敗するLaunching Flow server for /Users/sogasawara/progate Spawned flow server (pid=6001) Logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.log Monitor logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.monitor_log Launching Flow server for /Users/sogasawara/progate Spawned flow server (pid=6008) Logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.log Monitor logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.monitor_log Launching Flow server for /Users/sogasawara/progate Spawned flow server (pid=6012) Logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.log Monitor logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.monitor_log Lost connection to the flow server (0 retries remaining): -Out of retries, exiting!ログ
[2021-01-27 17:22:16.301] argv=/Users/sogasawara/progate/node_modules/flow-bi Unhandled exception: Unix.Unix_error(Unix.EINVAL, "ftruncate", "") Raised by primitive operation at file "hack/heap/sharedMem.ml", line 82, char Called from file "hack/heap/sharedMem.ml", line 146, characters 5-26環境
Mac Mini(M1), node v10.16.3 (rosetta), flow 0.90.0
解決策(workaround)
.flowconfigに以下のオプションを追加する
[options] sharedmemory.heap_size=2147483648なお、公式のissueに問題の対応方法が書いてあります
https://github.com/facebook/flow/issues/8538heap_sizeはマシンのメモリによって変わるとか。
私のマシンはMac miniのメモリ16GBですが上記の値で動きましたがマシンによって調整が必要そうです
- 投稿日:2021-01-27T17:18:49+09:00
Electronアプリの作り方
はじめに
Electronという、ChromiumとNode.jsを使ったWeb技術でデスクトップアプリを作ることができるフレームワークを使います。
最初にやること
Node.jsをインストールする
Node.jsをインストールします。
https://nodejs.org/ja/download/
から、自分のOSを選びます。詳しくはこちらから。
Node.jsをインストールすることでElectronアプリを作ることができます。フォルダ作成
アプリを作成するためのフォルダを作成します。
今回は、Electronとしました。$ cd フォルダのパスとしてフォルダを指定する。
package.jsonを作成する
$ npm init -ypackage.json{ "name": "Electron", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }このようなファイルが作成される。
アプリのバージョンを変更するには、"version": "1.0.0"を変更する。Electronをインストールする
$ npm i -D electron結構待たされる。
srcフォルダ等作成
srcフォルダを作り、その中に
- index.html
- main.js
- package.json
を作成。
index.htmlはご自由に書いてください。
今回はHello worldを表示します。index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Electron</title> </head> <body> <h1>Hello world</h1> </body> </html>main.jsconst { app, Menu, BrowserWindow } = require('electron'); const path = require('path'); const url = require('url'); let mainWindow; function createWindow() { mainWindow = new BrowserWindow({width: 1200, height: 675, 'icon': __dirname + 'favicon.ico'}) mainWindow.loadURL(url.format({ pathname: path.join(__dirname, 'index.html'), protocol: 'file:', slashes: true })); mainWindow.on('closed', () => { mainWindow = null; }); } app.on('ready', createWindow); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } });
'icon': __dirname + 'favicon.ico'でアイコンを指定している。なくても良い。package.json{ "main": "main.js" }main.jsを指定
試しに起動してみる
$ npx electron srcHello worldが表示されました。
パッケージング
electron-packagerを使ってパッケージングする
electron-packagerのインストール
$ npm i -D electron-packagerそれでは、パッケージングしていきます。
windows向けです。$ npx electron-packager src electron --platform=win32 --arch=x64 --overwrite --icon=src/favicon.icoここでもアイコンを指定しています。
Electronのデザイン
全画面&タイトル、メニューバーなし
main.jsmainWindow = new BrowserWindow({kiosk: true, 'fullscreen': true, 'frame': false});main.jsのnew BrowserWindowを指定しているところに追加する
展示用などにおすすめ。
Ctrl+Wでアプリがシャットダウンされる。メニューバーなし
main.jsmainWindow.setMenu(null);とてもシンプルで良い。
パッケージングを楽に
package.json"scripts": { "start": "electron ./src", "macos": "electron-packager ./src electron --platform=darwin --arch=x64 --overwrite --icon=src/favicon.ico", "windows": "electron-packager ./src electron --platform=win32 --arch=x64 --overwrite --icon=src/favicon.ico" }と、追加しておきます。
このとき、忘れずに"start"の前に,を追加してください。$ npm run windowsとするとかんたんにパッケージングできます。
windowsをmacosに変えるとMacOS向けにパッケージングされます。まとめ
Web技術でアプリを作ることができました。
サクっとアプリが作れるいい時代ですね。参考
- 投稿日:2021-01-27T14:32:09+09:00
ペアリングしてステレオ化したGoogle Home(Nest)にボイスコマンドでPodcastを流してもらう
2台をペアリングしてステレオスピーカー化したGoogle Nest mini(Google Home miniの新型)にボイスコマンドでPodcastをしゃべってもらおうと思ったら、意外とハマりどころが多く、結構な苦戦を強いられました。
苦労の末、どうにかやりたいことはできるようになったので、今回はその辺の話を書きたいと思います。
実現のためにいくつかのクラウドサービスを利用していますが、無料枠の範囲内で大丈夫です。この辺の記事を参考にしています。
Google Homeで好きなポッドキャストをスマートに再生する
google-home-notifier で スピーカーグループを喋らせるやりたいこと
google homeに「ok google、~~~を流して」とお願いすると、あらかじめ指定しておいたpodcastの最新話を再生してくれる。
必要なもの
- IFTTTのアカウント
- firebaseのプロジェクト(無料プランで大丈夫です)
- 自宅のLAN内で常時インターネットに接続されていてnodejsが動作するPCなど
- 僕はRaspberry pi を使っています
動作させる仕組み
- Google home(Google Assistant)へのボイスコマンド入力を、IFTTTでフックする。
- IFTTTのアクションでfirebaseのRealtime Databaseを更新する。
- nodejsで組んだスクリプトでfirebaseの更新を監視しておき、更新を検知したらPodcastのRSSを取得&パースし、最新話のMP3のURLをGoogle Homeに渡す。
- Google Homeが、受け取ったMP3を再生してくれる
実現手順
大まかな手順は「Google Homeで好きなポッドキャストをスマートに再生する」に書かれている通りですが、いくつか修正した方がよい箇所があります。
元記事からの変更点
環境によっては、いくつかの修正が必要です。
mdnsモジュールの一部機能がRaspberryPiで動かない問題の修正
依存ライブラリのgoogle-home-notifierの内部で利用されている「mdns」モジュールが、Raspberrypiでは正しく動作しないため、修正が必要です。
121行目を以下のように修正します
変更前
~/node_modules/mdns/lib/browser.js, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo()変更後
~/node_modules/mdns/lib/browser.js, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo({families:[4]})※ 引数に
{families:[4]}を追加しています。元記事に掲載されているRaspberrypiで動かすnodejsスクリプトの修正
podcastを再生するGoogle HomeをIP Addressで指定していますが、スピーカーのペアリングやグループ化をしているとうまく動きません。
IPアドレス指定ではなく、google-home-notifierは、対象のgoogle homeデバイスをIPアドレス指定ではなく内部名で指定することもできるようになっているため、内部名で指定するように変更します。
まず、再生したいGoogle Home(Nest)の内部名を調べます。
internal_name.jsvar mdns = require('mdns'); var browser = mdns.createBrowser(mdns.tcp('googlecast')); browser.start(); browser.on('serviceUp', function(service) { console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port); });実行結果は以下のようになります(デバイスの内部名の一部を伏字にしています)。
*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi. *** WARNING *** Please fix your application to use the native API of Avahi! *** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html> *** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi. *** WARNING *** Please fix your application to use the native API of Avahi! *** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html> Device "AQUOS-TV***************" at 192.168.0.111:8009 Device "Google-Home-Mini***************" at 192.168.0.22:8009 Device "Google-Nest-Mini***************" at 192.168.0.20:8009 Device "Google-Cast-Group***************" at 192.168.0.20:32000 Device "Google-Nest-Mini***************" at 192.168.0.21:8009 出力が終わったらCtrl+Cで終了(グループ化・ペアリングしたスピーカーの場合には、Google-Cast-Group-**** のような名前になります)
上で取得した情報をもとに、引用元記事で書かれているraspberrypiで動かすスクリプトを以下のように修正&保存します(nodeのバージョンにもよるかもしれませんが、実行時エラーが出たのでそこも修正しています)。
raspberrypi.jsvar FeedParser = require('feedparser'); var firebase = require('firebase'); var googleHome = require('google-home-notifier'); var request = require('request'); const lang = 'ja'; // const ip = '192.168.0.20'; //再生したいGoogle HomeのIPアドレス // googleHome.ip(ip, lang); const deviceName = 'Google-Cast-Group*************'; googleHome.device(deviceName, lang); const config = { apiKey: 'hoge', authDomain: 'fuga.firebaseapp.com', databaseURL: 'https://**************.firebaseio.com', projectId: 'fuga', storageBucket: '', messagingSenderId: 'piyo' }; firebase.initializeApp(config); var db = firebase.database(); var ref = db.ref('/'); ref.on('child_changed', function(snapshot) { // var url = ref.child('url').val(); var url = snapshot.val(); if (url) { playLatestPodcast(url); } ref.update({'podcast_url': ''}); // 変更をリセット }); function playLatestPodcast(url) { var req = request(url); var parser = new FeedParser(); var items = []; req.on('response', function(res) { this.pipe(parser); }); parser.on('readable', function() { while (item = this.read()) { items.push(item); } }); parser.on('end', function() { googleHome.play(getLatestPodcastUrl(items), function(notifyRes) {}); }); } function getLatestPodcastUrl(items) { for (item of items) { for (enclosure of item.enclosures) { var url = enclosure['url']; if (url) { return url; } } } return ""; }グループ化したスピーカーに対応させる。
再生するデバイスが1台のgoogle homeの場合には、これでうまくいきますが、Google homeがグループ化されていたり、ペアリングしているステレオ化されたスピーカーで再生しようとしている場合はうまく動きません。
「google-home-notifier で スピーカーグループを喋らせる」の記事を参考に、
~/node_modules/google-home-notifier/google-home-notifier.jsを修正します。変更前
~/node_modules/google-home-notifier/google-home-notifier.jsdeviceAddress = service.addresses[0];変更後
~/node_modules/google-home-notifier/google-home-notifier.jsdeviceAddress = {}; deviceAddress.host = service.addresses[0]; deviceAddress.port = service.port;※ 修正が必要な個所は2か所あります。
これで、作業はすべて完了です。
IFTTTで設定した「ok google、~~~~を流して」というフレーズを言えば、目的のpodcastの最新話が自動で再生されるかと思います。
同じ仕組みを利用して、miniDLNAなどで組んだDLNAサーバのプレイリストURLをgoogle homeに渡してあげれば、「ok google、NASの音楽を流して」という音声コマンドでNASに保存した音楽をシャッフル再生してくれるような仕組みなども作れそうです。
機会があればやってみたいと思います。
- 投稿日:2021-01-27T14:32:09+09:00
ペアリングしてステレオ化したGoogle Home(Nest)に、ボイスコマンドでPodcastを流してもらう
2台をペアリングしてステレオスピーカー化したGoogle Nest mini(Google Home miniの新型)にボイスコマンドでPodcastをしゃべってもらおうと思ったら、意外とハマりどころが多く、結構な苦戦を強いられました。
苦労の末、どうにかやりたいことはできるようになったので、今回はその辺の話を書きたいと思います。
実現のためにいくつかのクラウドサービスを利用していますが、無料枠の範囲内で大丈夫です。この辺の記事を参考にしています。
Google Homeで好きなポッドキャストをスマートに再生する
google-home-notifier で スピーカーグループを喋らせるやりたいこと
google homeに「ok google、~~~を流して」とお願いすると、あらかじめ指定しておいたpodcastの最新話を再生してくれる。
必要なもの
- IFTTTのアカウント
- firebaseのプロジェクト(無料プランで大丈夫です)
- 自宅のLAN内で常時インターネットに接続されていてnodejsが動作するPCなど
- 僕はRaspberry pi を使っています
動作させる仕組み
- Google home(Google Assistant)へのボイスコマンド入力を、IFTTTでフックする。
- IFTTTのアクションでfirebaseのRealtime Databaseを更新する(webhookを利用)。
- 自宅LAN環境内に配置したnodejs製スクリプトでfirebase realtime databaseの更新を監視しておき、更新を検知したらPodcastのRSSを取得&パースし、podcast最新話のMP3URLをGoogle Homeに渡す。
- Google Homeが、受け取ったMP3を再生してくれる
実現手順
大まかな手順は「Google Homeで好きなポッドキャストをスマートに再生する」に書かれている通りですが、いくつか修正した方がよい箇所があります。
元記事からの変更点
環境によっては、いくつかの修正が必要です。
mdnsモジュールの一部機能がRaspberryPiで動かない問題の修正
依存ライブラリのgoogle-home-notifierの内部で利用されている「mdns」モジュールがRaspberrypiでは正しく動作しないため、修正が必要です。
~/node_modules/mdns/lib/browser.js```の121行目を以下のように修正します__変更前__ ```~/node_modules/mdns/lib/browser.js , 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo()変更後
~/node_modules/mdns/lib/browser.js, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo({families:[4]})※ 引数に
{families:[4]}を追加しています。元記事に掲載されているRaspberrypiで動かすnodejsスクリプトの修正
元記事に掲載されているスクリプトでは、podcastを再生するGoogle HomeをIP Addressで指定していますが、この方法だとスピーカーのペアリングやグループ化をしているとうまく動きません。
google-home-notifierは、対象のgoogle homeデバイスをIPアドレス指定ではなく内部名で指定することもできるようになっているため、再生するデバイスを内部名で指定するようにスクリプトを変更します。
それには、対象のgoogle homeの内部名を調べなければなりません。
下記のようなスクリプトを用意し、nodeで動作させます。
internal_name.jsvar mdns = require('mdns'); var browser = mdns.createBrowser(mdns.tcp('googlecast')); browser.start(); browser.on('serviceUp', function(service) { console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port); });実行結果は以下のようになります(デバイスの内部名の一部を伏字にしています)。
*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi. *** WARNING *** Please fix your application to use the native API of Avahi! *** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html> *** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi. *** WARNING *** Please fix your application to use the native API of Avahi! *** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html> Device "AQUOS-TV***************" at 192.168.0.111:8009 Device "Google-Home-Mini***************" at 192.168.0.22:8009 Device "Google-Nest-Mini***************" at 192.168.0.20:8009 Device "Google-Cast-Group***************" at 192.168.0.20:32000 Device "Google-Nest-Mini***************" at 192.168.0.21:8009 出力が終わったらCtrl+Cで終了させてください(グループ化・ペアリングしたスピーカーの場合には、Google-Cast-Group-**** のような名前になります)
上で取得した情報をもとに、元記事で書かれているraspberrypiで動かすスクリプトを以下のように修正&保存します(nodeのバージョンにもよるかもしれませんが、実行時エラーが出たのでそこも修正しています)。
raspberrypi.jsvar FeedParser = require('feedparser'); var firebase = require('firebase'); var googleHome = require('google-home-notifier'); var request = require('request'); const lang = 'ja'; // const ip = '192.168.0.20'; //再生したいGoogle HomeのIPアドレス // googleHome.ip(ip, lang); const deviceName = 'Google-Cast-Group*************'; googleHome.device(deviceName, lang); const config = { apiKey: 'hoge', authDomain: 'fuga.firebaseapp.com', databaseURL: 'https://**************.firebaseio.com', projectId: 'fuga', storageBucket: '', messagingSenderId: 'piyo' }; firebase.initializeApp(config); var db = firebase.database(); var ref = db.ref('/'); ref.on('child_changed', function(snapshot) { // var url = ref.child('url').val(); var url = snapshot.val(); if (url) { playLatestPodcast(url); } ref.update({'podcast_url': ''}); // 変更をリセット }); function playLatestPodcast(url) { var req = request(url); var parser = new FeedParser(); var items = []; req.on('response', function(res) { this.pipe(parser); }); parser.on('readable', function() { while (item = this.read()) { items.push(item); } }); parser.on('end', function() { googleHome.play(getLatestPodcastUrl(items), function(notifyRes) {}); }); } function getLatestPodcastUrl(items) { for (item of items) { for (enclosure of item.enclosures) { var url = enclosure['url']; if (url) { return url; } } } return ""; }グループ化したスピーカーに対応させる。
再生するデバイスが1台のgoogle homeの場合には、これでうまくいきますが、Google homeがグループ化されていたり、ペアリングしているステレオ化されたスピーカーで再生しようとしている場合はうまく動きません。
そこで、「google-home-notifier で スピーカーグループを喋らせる」の記事を参考に、
~/node_modules/google-home-notifier/google-home-notifier.jsを修正します。変更前
~/node_modules/google-home-notifier/google-home-notifier.jsdeviceAddress = service.addresses[0];変更後
~/node_modules/google-home-notifier/google-home-notifier.jsdeviceAddress = {}; deviceAddress.host = service.addresses[0]; deviceAddress.port = service.port;※ 修正が必要な個所は2か所あります。
これで、作業はすべて完了です。
IFTTTで設定した「ok google、~~~~を流して」というフレーズを言えば、目的のpodcastの最新話が自動で再生されるかと思います。
このスクリプトを常時動かしておけば、google homeに「ok google ~~を流して」とお願いするだけで、目的のpodcastの最新話を聞くことができるようになります。
同じ仕組みを利用して、miniDLNAなどで組んだDLNAサーバのプレイリストURLをgoogle homeに渡してあげれば、「ok google、NASの音楽を流して」という音声コマンドでNASに保存した音楽をシャッフル再生してくれるような仕組みなども作れそうです。
機会があればやってみたいと思います。
- 投稿日:2021-01-27T14:18:39+09:00
Nodeで、SJIS文字コードで、かつ、行ごとに列数が変動するcsvを読み込んでみた
NodeでSJIS文字コードで、かつ列数が行ごとに変動するcsvを読み込んで使う必要があるため、
試行錯誤して実装できたコードを残しておきます。
結構試行錯誤して面倒だったので、どなたかの参考になれば幸いにて。const csv = require('csv') const csvFilePath = './path/to/csv_file.csv' const fs = require('fs') const iconv = require('iconv-lite') /** * csvデータの取得(SJISのファイル) */ const getCsv = async function() { return new Promise(resolve => { fs.readFile(csvFilePath, function (err, data) {//非同期処理なのでreadFileSyncではなくreadFile if (err) throw err const buf = Buffer.from(data, 'binary')//new BufferはdeprecatedなのでBuffer.fromを使う const ret = iconv.decode(buf, 'Shift_JIS')//SJISをdecodeする csv.parse(ret, {relax_column_count: true},//列数変動ファイルの読込オプション function (err, output) { resolve(output) }) }) }) } async function main() { const data = await getCsv()//非同期読込 console.log(data)//utf-8でのデータ取得 console.log(data.length)//行数の確認 } main()//実行
- 投稿日:2021-01-27T14:18:39+09:00
Nodeで、SJISで、かつ、行ごとに列数が変動するcsvを読み込んでみた
Nodeにて、SJISで、かつ列数が行ごとに変動するcsvを読み込んで使う必要があったため、
試行錯誤して実装できたコードを残しておきます。いろいろな参考コードがネットにありましたが、
要件そのままのものは見つからず、
結構試行錯誤しました。
どなたかの参考になれば幸いにて。const csv = require('csv') const fs = require('fs') const iconv = require('iconv-lite') const csvFilePath = './path/to/csv_file.csv' /** * csvデータの取得(SJISのファイル) */ const getCsv = async function() { return new Promise(resolve => { fs.readFile(csvFilePath, function (err, data) {//非同期処理なのでreadFileSyncではなくreadFile if (err) throw err const buf = Buffer.from(data, 'binary')//new BufferはdeprecatedなのでBuffer.fromを使う const ret = iconv.decode(buf, 'Shift_JIS') const ret2 = iconv.encode(ret, 'UTF-8') csv.parse(ret2, {relax_column_count: true},//列数変動ファイルの読込オプション function (err, output) { resolve(output) }) }) }) } async function main() { const data = await getCsv()//非同期読込 console.log(data) console.log(data.length) } main()それにしてもNode.jsって便利で面白いですね。
- 投稿日:2021-01-27T13:42:41+09:00
Raspberry pi zero w に node.js をインストール
はじめに
Raspberry pi zero w に node.js をインストールしたくなった。Raspberry pi zero w をサーバーにし、React.jsでアプリケーションを作ってLAN内の別端末から利用したい、というのがその理由だ。気を遣う部分は、今回の対象が、「Raspberry pi zero である」という点である。RaspberryPi3や4とはCPUが違うのだ。Raspberry pi zero に使われているCPUはArmv6, RaspberryPi3や4はArmv7だそうで・・・。あと、Rasberry pi OS は 32bitOS ですよね。
準備
Raspberry pi zero w を Raspberry pi OS lite でセットアップし、まずはインターネットにつながっているLANまでの接続は行った。apt のアップデートができれば、準備オッケーというところではないだろうか。
(この下準備については、前回記事等が参考になるはず・・・。ちないに今回の案件は、前回の4GBディスクとは別のSDカードでRaspberry Pi zero をセットアップ。)node.js インストール
いつものごとく、諸先輩方の知見を参照する。
【Node.js】Raspberry Pi Zeroに最新のNode.jsをインストールする
https://www.taneyats.com/entry/install-nodejs-on-raspberrypi-zero上述先輩の記録資料を真似して、OS,CPUに合うソースファイルを公式から取得することにする。
ファイルの取得
見てみると、node-v12 以降は、Armv6のファイルが無いようだ・・・(2021年1月時点)。また、node-v11だと、あとでeslint-typescriptライブラリをインストールできない(create-react-appでReact環境構築について)。そこで、次のとおりファイルを取得する。
# まずは任意のディレクトリを作成(取得ソース展開用) $ mkdir node_src $ cd node_src $ wget https://nodejs.org/dist/latest-v10.x/node-v10.23.2-linux-armv6l.tar.gz $ tar -zxf node-v10.23.2-linux-armv6l.tar.gzインストール
先達に倣って、/usr/local にディレクトリ丸ごとコピーして使えるようにする。
$ cd node-v10.23.2-linux-armv6l $ rm CHANGELOG.md LICENSE README.md $ sudo cp -R * /usr/local/確認
コマンドのバージョンを確認してみる。
$ node -v v10.23.2npm のバージョンを上げておく。
$ sudo npm install -g npm $ npm -v 6.14.10備考
yarn コマンドをインストールする。
$ sudo npm install -g yarnおわりに
Raspberry pi zero w に node.js をセットアップした。作業時間は1時間くらい。通信量もさほどなく、コンパイル等も不要だったため、コマンドを打って待つ時間もさほどなかった。
- 投稿日:2021-01-27T13:26:40+09:00
[エラー対処] Express チュートリアル populatedbのURLが機能しない
Express チュートリアルパート3: データベースの使用 (Mongooseを使用)
node populatedb <your mongodb url>がつまづきポイントでして
your mongodb url に以下のyour_user_name_passwordの部分を変更してコマンドを実行すると思いますが、失敗し時間を取られたので、備忘録として残しておきたいと思います。
mongodb+srv://your_user_name:your_password@cluster0.a9azn.mongodb.net/local_library?retryWrites=trueExampleとして
- password → iekud
など
失敗する例として
dbUserPassword推測ですが、デフォルトの文字列だと失敗する?12345数字だけでは失敗するらしい?失敗するがご存知の方がいらっしゃれば、コメントをしていただけると嬉しいです!
参考ページ
Express Tutorial Part 3: Using a Database (with Mongoose)
node populatedb url not working
- 投稿日:2021-01-27T12:56:22+09:00
Node.jsの基本 その2
Node.js備忘録として、書き始めました。その1はこちら
今回はほぼexpressの基本みたいな回です。HTTP
HTTPはインターネットで支配的なプロトコルです。Node.jsはサーバー側クライアント側双方に適したモジュールを持っています。
Node.jsに興味を持っている人なら多くの場合express.jsというウェブアプリケーションフレームワークが存在するのを知っていると思います。expressはとても便利で僕も一度覚えてしまうともう他へ浮気できなくなってしまいました。koaやhapiなどのオルタナティブも存在しますが、Qiitaにおけるexpress.js記事の数は他の追随を許していませんから、初学者はexpress一択でしょう。
しかし、何にせよ基礎の基礎を知っておくのは大事なことです。ビルトインモジュールであるhttpを使ったサーバーとクライアントの書き方を見てみましょう。
その1でも用いたHello Worldの例は最も簡素なNode.jsを用いたHTTPサーバーです。server.jsconst http = require('http') http.createServer((req, res) => { res.end("Hello World!") }).listen(3000, "127.0.0.1")httpを用いたルーティング
server.jsconst http = require('http') const url = require('url').URL const port = 3000 const hostName = "127.0.0.1" http.createServer((req, res) => { const path = new url(req.url, `http://${hostName}:${port}`) if(path.pathname === "/"){ res.writeHead(200, {'Content-Type': 'text/plain'}) res.end('This is root page\n') } else if(path.pathname === "/about"){ res.writeHead(200, {'Content-Type': 'text/plain'}) res.end('this is about page\n') } else { res.writeHead(404, {'Content-Type': 'text/plain'}) res.end('404姉さん') } }).listen(port, hostName) console.log(`Server running at http://${hostName}`)HTTPクライアント
client.jsconst http = require('http') http.get('http://www.google.co.jp', (res) => { if(res.statusCode == 200){ console.log('successful') } else { console.log('google is down again') } }).on('error', error => { console.log(error) })Express.js
Express.jsは最も採用率の高いウェブアプリケーションフレームワークです。これさえあればウェブアプリケーションを作成する際、まずほとんどの状況に対応できる機能が備わっています。
まずはインストールしてみましょう。npm install -g expressこれで環境のどこでもexpressのジェネレーター機能を使えるようになります。
express [web app name]このコマンドで雛形を作成できます。初学ではどのようなモジュールを用意するべきかわかりやすいでしょう。
起動
cd [web app name] npm install node app.js作成されたアプリのディレクトリを見ていきましょう。(express@4.17.1)
node_modules
インストールされているモジュールをここに保持しています。
package.json
ざっくり言えばこのウェブアプリケーションに関しての情報を保持しているファイルです。
binフォルダ
実行用のファイルを入れるためのフォルダです。
routes
ルーティング用のフォルダです。RESTfulなAPIを作成する上でルーティングは可視化面でもメンテナンス面でも重要で、ユーザーに関しての処理はすべて/userルートへ認証に関しての処理はすべて/authルートへなど個別のexpress.route関数ファイルを用意するのが良いでしょう。次の項目でより詳しく説明します。
public
スタイルシートやjavascriptファイル、画像などをこのフォルダに保存してアプリケーションにここを参照させるようにします。例としてはbootstrapやjQuerryなどでしょう。
views
ビューエンジンに指定された形式の同名ファイルを参照します。express generatorはjadeを使用しています。(個人的には少し可読性が低いにしろejsが好きです)
jadeというのはテンプレートエンジンで、いわば雛形です。動的なウェブページのレンダリングに向いています。ウェブアプリケーションにおけるルーティング
ルーターの設定は少なくともNode.jsでウェブアプリケーションを作成するうえでとても重要です。どのHTTPリクエストにどのように処理するかの振り分けがルーティングです。
HTTPメソッド
Express.jsはルート定義の際にhttpメソッドとパスを組み合わせて使用します。
GET, POST, PUT, DELETE, などなど
手始めにはサーバーからデータを受け取るGETとサーバーへデータを送るPOSTを使用するところからでしょう。GETとPOSTのリクエストのテストを例を書きますが、それに先立ってexpressのミドルウェアでHTTPリクエストの必要な情報を抽出してくれるbody-parserというモジュールをインストールしましょう。
npm install body-parserserver.jsconst express = require('express') const bodyParser = require('body-parser') const server = express() const port = 3000 server.use(bodyParser.urlencoded({extended:true})) server.use(bodyParser.json()) //root server.get('/', (req, res) => { res.send('hello world') }) //about get route server.get('/about', (req, res) => { res.send('about route get request called') }) //about post route server.post('/about', (req, res) => { console.log('called') const requestBody = req.body console.log(requestBody) res.send(req.body) }) server.listen(port, () => { console.log('server listening on:' + port) })ルートにおけるパラメータの扱い方
アプリケーションによってはRESTfulであるためにパスからパラメータを受け取る必要があります。expressにおいてはパスの最後に":[parameter_name]"で定義できます。
app.get('/api/users/:id', (req, res) => { res.send('受け取ったIDは' + req.params.id + 'です') })クエリの扱い方
アプリケーションによってはクエリによる検索機能などを導入する必要があるでしょう。
Expressにおいてはパスに"?[query_parameter_name]=[query_value]"で定義されたHTTPコールをreq.query.query_parameter_nameでその値を扱うことができます。
server.get('/search/', (req, res) => { res.send(req.query) })Routeを外部モジュールとして扱う
小規模なアプリの構築には必ずしも要るとは限りませんが、可読性の向上のため、たとえばユーザーについての処理はすべて/userのルートへ、認証のための処理はすべて/authのルートへ、それぞれ個別のファイルとしてまとめて大元のapp.jsへエクスポートするのが推奨されるでしょう。
route/users.jsvar express = require('express'); var router = express.Router(); router.get('/', function(req, res) { res.send('/users called'); }); router.get('/all', (req, res) => { res.send('/users/all called') }) module.exports = router;このルーターをserver.jsでインポートしましょう
server.jsconst express = require('express') const bodyParser = require('body-parser') const usersRoute = require('./routes/users') const server = express() const port = 3000 //users route server.use('/users', usersRoute) //root server.get('/', (req, res) => { res.send('hello world') }) server.listen(port, () => { console.log('server listening on:' + port) })ビューレンダリング
先程からずっと例に使っているres.send()という関数は受け取った値をテキスト及びHTMLとしてクライアントへ送ります。
その他に例えばres.json()という関数は受け取った値を有効なJSONファイルの形式で送信します。
res.render()という描画されてHTMLを返す関数に用いる、ビューテンプレートは非常に便利な機能です。たとえばブログを作成するとして、記事ごとにHTMLを書くのは冗長ですし非効率です。タイトルや本文のデータを受け取ってどのように当てはめて描画するかの雛形を作っておくことで、"/archives/:article_id" で対応するidの記事タイトルや本文を受け渡しするだけでよくて効率的です。ビューテンプレートエンジンと呼ばれるものは色々あります。僕は個人的にejsをプロジェクトで使ってきたので覚えがありますが、ここではexpress generatorに用いられているjadeで例を出していきましょう。
まずjadeのモジュールをインストールしましょう
npm install jade次にviewsフォルダを作成しindex.jadeファイルを作成しましょう
index.jadedoctype html html head title Jade Example body h1 #{message}jadeはインデントセンシティブなので開業とtabによる間隔で階層を形成する必要があり、閉じるためのタグがないHTMLを書いているような感じです。受け渡されたデータは#{}の中で変数名を指定します。
server.jsconst express = require('express') const bodyParser = require('body-parser') const server = express() const port = 3000 //set jade as its view engine server.set('view engine', 'jade') //root path // render index.jade with message = 'hello world!' server.get('/', (req, res) => { res.render('index', {message:"Hello World!"}) }) server.listen(port, () => { console.log('server listening on:' + port) })次へ
その3へ続きます。主にmongoDBに関してになると思います。
- 投稿日:2021-01-27T09:19:38+09:00
Next.js+TypeScriptでマルチプロセス対応カスタムサーバ作成
Next.js+TypeScriptでマルチプロセス対応カスタムサーバ作成
- リポジトリ
https://github.com/SoraKumo001/nextjs-custom- 原文リンク
https://ttis.croud.jp/?uuid=46c0f2f8-7db3-4ec4-ab86-5054aea70f49カスタムサーバ
Next.jsはWebServer機能を標準で内蔵していますが、マルチプロセスや特殊なセッション処理などを組み込む場合には、カスタムサーバという形でWebServer部分を自分で実装する必要があります。
公式にサンプルはある物の、以外に日本語の情報が少ない、それどころかマルチプロセスやfastifyでの実装記事は皆無だったので、書いていきたいと思います。
マルチプロセス化について
Next.jsを動かしているNode.jsは基本的にシングルスレッドで動作します。シングルスレッドといってもI/Oアクセスに関しては非同期で行われているため、無駄なブロックは起こらず、実用的な速度で動作することが可能です。
ところが計算処理などをしている間は当然他の仕事は出来ません。マルチコアCPUなどでハードウエア的に余裕があっても、シングルスレッドである限りはせっかくのリソースが活用できないのです。
これに対処するにはNext.jsをマルチスレッドではなく、マルチプロセス化するのが有効な手段となります。ありがたいことにNode.jsには、マルチプロセス化を簡単に実装するライブラリが標準提供されているので、カスタムサーバ化のコードを少し書くだけで、その恩恵を受けることが出来ます。
Fastifyに関して
Node.jsでWebServer機能を実装するフレームワークとして有名なのはExpressです。しかし古い実装を引きずっているため、応答速度が遅いといわれています。今回はベンチマークで上位に位置するFastifyを使ってカスタムサーバを作ります。
インストールが必要な最低限のパッケージ
yarn add cross-env fastify next react react-dom
yarn add -D @types/node @types/react @types/react-dom ts-node-dev typescriptカスタムサーバの実装コード
以下の二つのファイルを用意します。
ちなみに環境変数でINDEXというのを子プロセスに渡していますが、workerにIDが振られるので実は無くてもかまいませんserver/index.tsimport next from "next"; import * as os from "os"; import * as cluster from "cluster"; import { parse } from "url"; import fastify from "fastify"; const dev = process.env.NODE_ENV !== "production"; const clusterSize = Math.min(os.cpus().length, 4); const portNumber = 3000; if (cluster.isMaster) { for (let i = 0; i < clusterSize; i++) cluster.fork({ INDEX: i }); } else { const app = next({ dev }); const handle = app.getRequestHandler(); const server = fastify(); app.prepare().then(() => { server.all("*", (req, res) => { return handle(req.raw, res.raw, parse(req.url, true)); }); server.listen(portNumber).then(() => { console.log(`[${process.env.INDEX}]:http://localhost:${portNumber}`); }); }); }server/tsconfig.json{ "compilerOptions": { "module": "commonjs", "outDir": "../.next", "esModuleInterop": true } }tsconfig.jsonを作成しているのは、Next.js管理下のpagesファイルなどとはTypeScriptのビルドの扱いが異なるからです。
スクリプト関係
devはカスタムサーバ自体の自動リロードのため、ts-node-devを使っています。ただし.nextの中身はNext.js側が調整するので、無視指定が必要です。
package.json{ "name": "nextjs-custom", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "dev": "ts-node-dev --ignore-watch \\.next -P server/tsconfig.json server/index.ts", "build": "tsc -b server && next build", "start": "cross-env NODE_ENV=production node .next/index.js", "export": "next export" }, "devDependencies": { "@types/node": "^14.14.22", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "ts-node-dev": "^1.1.1", "typescript": "^4.1.3" }, "dependencies": { "cross-env": "^7.0.3", "fastify": "^3.11.0", "next": "^10.0.5", "react": "^17.0.1", "react-dom": "^17.0.1" } }まとめ
大したコード量も必要なくカスタムサーバが実装できました。マルチプロセスとFastifyのパワーによって、きっと快適SSRライフが送れることでしょう。
ただしベンチマークを取った結果、それなりの負荷をかけても効果が顕著に出るのは2プロセスまでというオチでした。ベンチマークに関しては別記事を書く予定です。
- 投稿日:2021-01-27T02:31:57+09:00
MyAnimListのアニメランキングをグラフ入りMDにする
使用例
個人的に、はてなブログで使っています。投稿する部分は割愛。
レポジトリ
https://github.com/and0ry0/myanimelist-email
前提
package.json"dependencies": { "json2md": "^1.9.2", "node-fetch": "^2.6.1", }アニメオブジェクト
index.jsconst fetch = require('node-fetch') function convertAnime(anime) { const jaTitle = anime.title .replace('Shingeki no Kyojin', '進撃の巨人') // 日本語翻訳適当ですいません return { rank: anime.rank, url: anime.url, title: jaTitle, score: anime.score, start: anime.start_date, image_url: anime.image_url, members: anime.members } }MDに変換
index.jsconst convertToMd = (anime) => { return [ { h2: anime.rank + '. ' + anime.title + ' (' + anime.members.toLocaleString('ja-JP') + '人視聴)' }, { link: { title: 'MyAnimeListで詳細を見る', source: anime.url } }, { addImage: { title: 'MyAnimeListのサムネ', source: anime.image_url } } { ul: [ '視聴者数: **' + anime.members.toLocaleString('ja-JP') + '人**', 'スコア: **' + anime.score, '放送開始時期: ' + anime.start, ] }, { p: '' /* 改行です */} ] }Jikan APIでランキング取得
詳しくはJikanのDocsを参照。
index.js// Get anime ranking async function MalRank() { // Jikan API https://jikan.docs.apiary.io/ const res = await fetch('https://api.jikan.moe/v3/top/anime/1/bypopularity') const data = await res.json() // アニメの配列を作る const topAnimes = data.top.map((anime) => convertAnime(anime)) const firstAnime = { title: topAnimes[0].title, members: topAnimes[0].members } const json2md = require('json2md') // 画像とグラフを用意する json2md.converters.addImage = function ({ title, source }) { return '<img width="150px" title="' + title + '" src="' + source + '" />' } json2md.converters.animeGraph = function ({ rank, title, members }) { // 相対的に長さを作る const relativeWidth = (members - topAnimes[10].members) / (firstAnime.members - topAnimes[10].members) return '<div class="graphBox"><div style="width: ' + relativeWidth * 100 + '%;" class="title">' + rank + '. ' + title + '</div><div class="members">' + members.toLocaleString('ja-JP') + '</div></div>' } // はてなブログ対策のspan https://blog.uchiten.info/entry/2017/01/30/174500 const style = `<span></span><style>.graphBox{width:100%;display:flex;padding:.3em;margin:0 0 .2em;position:relative}.graphBox>.title{overflow:visible;white-space:nowrap;background:#add8e6;font-weight:700;padding:.3em}.graphBox>.members{color:gray;position:absolute;right:.3em;padding:.3em}</style></span>\n\n` const graphMd = `## Top10はこんな感じ \n\n` + json2md(graphJson) + `\n\n11位のアニメの視聴者数を引いて、相対的にグラフを作っています。\n\n` const dataJson = topAnimes.map((anime) => convertToMd(anime)) const mainMd = json2md(dataJson) console.log(style + graphMd + mainMd) } MalRank();


