- 投稿日:2019-12-14T23:44:33+09:00
Node-RED で黒板機能と操作ノードを実装してみた
Node-RED の右にあるサイドバー領域に表示される黒板と、それを操作するノードを作成してみましたので、ご紹介します。
コードは yamachan/node-red-contrib-rtk-board リポジトリにあります。少しずつ、地味に機能追加していたりします。2019年内に npm に登録するのが目標だったり。
作成した理由
Node-RED Advent Calendar 2019 の投稿ネタとして作成しました。ちょっと趣味に走って、想定を超えて拡張しつつありますが…
昨年の Node-RED Advent Calendar 2018 では「Node-RED パズルで遊んでフローに親しんでもらいたい」 という記事を投稿しました。Node-RED のフロー編集画面を生かし、パズル的なものを用意することで楽しく学んでもらいたい、という感じで。
ただその際、フロー中の正解/不正解をステータスのアイコンだけで表現しているのが地味で、もう少し派手な表現方法が欲しいな、何か作ってみよう、とネタを練っていました。まさか1年も寝かすことになるとは思いませんでしたが…
node-red-dashboard の利用を考えましたが、Node-RED お馴染みのフロー画面とは別ページ(別タブ)になります。そうではなくて、デバッグメッセージのように、フロー画面の右にあるサイドバー領域に何かを表示したいと思っていました。
将来的には Scratchキャット のように、見た目にわかりやすい反応で、プログラミング初心者が学習時に最初に遊んでみるノードのひとつになればいいな、などと妄想しております。
Virtual TJBot から学ぶ
サイドバー領域に何かを表示したい!と資料を探していたところ、発見したのが「How to Train Virtual TJBot in Node-RED」記事を訳して試してみた」の記事で紹介した、node-red-contrib-virtual-tjbot です。これはフロー画面の右にあるサイドバー領域に、TJBot の画像を表示して動かしています。まずはこのリポジトリのコードを読んで、サイドバーの仕組みを理解しました。
パネルを表示する
パネル表示のキーとなるのは /tjbot/config.html ファイル 55 行目あたりにある以下のコードです。
/tjbot/config.htmlonpaletteadd: function () { RED.sidebar.addTab({ // ココ! id: "vtjbot", label: "Virtual TJBot", // the label is displayed on the tab name: "Virtual TJBot", // displayed in the view menu content: '<div id="vtjbot" style="width: 100%; height: 100%"><iframe src="/tjbot" style="min-width: 260px; height: 500px; border: 0px" border="0"></iframe></div>', // content of the tab //toolbar: "toolbar", // content for the footer of the tab closeable: true, // can be closed enableOnEdit: true, iconClass: 'fa fa-android' }); }
RED.sidebar.addTab()
関数を用いてパネルを追加しているのがわかります。ググったら API Reference に載っていましたが、詳細な説明はありませんでした。パネルの中身を生成する
実際に表示されるコンテンツは
<iframe src="/tjbot"
で指定されている以下のページです。
そして、このページを表示するためのhttpサーバー機能を提供しているのが /tjbot/ui.js ファイル 52行目あたりにある以下のコードです。
/tjbot/ui.jsfunction init(server, app, log, redSettings) { tjbotPath = join(redSettings.httpNodeRoot, "tjbot"); const socketIoPath = join(tjbotPath, "socket.io"); const bodyParser = require("body-parser"); app.use(bodyParser.json({ limit: "50mb" })); app.use(tjbotPath, serveStatic(path.join(__dirname, "dist"))); // ココ! io = socketio(server, { path: socketIoPath }); io.on("connection", socket => { socket.emit("config", state); }); }なおこの /tjbot/ui.js は package.json ファイルで定義されていないので、自動的には読み込まれません。各ノードの定義部分の先頭で呼び出すことで、初期化が実施されています。
/tjbot/shine.jsmodule.exports = function (RED) { const ui = require("./ui.js")(RED); // 以下略 }操作ノードとの連携
頭の LED を光らせる shine ノードのコード 41行目あたりを見てみます。
/tjbot/shine.jsswitch (mode.toLowerCase()) { case "shine": if (color == "random") { const randIdx = Math.floor(Math.random() * colors.length); ui.emit("shine", { color: colors[randIdx] }); } else { ui.emit("shine", { color: color }); } break; // 省略 }emit 関数は /tjbot/ui.js ファイル 67行目あたりで定義されています。
/tjbot/ui.jsfunction emit(command, params) { io.emit(command, params); switch (command) { case "shine": state.led.color = params.color; break; case "pulse": state.led.color = "off"; break; case "armBack": case "lowerArm": case "raiseArm": case "wave": state.arm.position = command; break; } }
io
はさきほどのinit()
関数で定義されていました。 WebSopcket 技術を使った socket.io でノードと表示パネルを連携させていることがわかります。開発用の Node-RED 環境を準備
さて、ローカルで開発用の Node-RED 環境を用意しましょう。私の場合、適当な作業用のフォルダで、以下のようにコマンドを実行します。(Windowsコマンドプロンプトの場合)
md node-red-dev cd node-red-dev npm init -y npm install --unsafe-perm --save node-red md .node-red echo 2> .node-red/settings.js node_modules\.bin\node-red -u .node-red -s .node-red/settings.jsまあ普通のやり方だと思いますが、
-u
-s
オプションで設定ファイルも同じフォルダ内に指定することで、既にインストール済みの Node-RED 環境に影響しないようにしています。これなら利用後はフォルダごと削除すればokです。手抜きで setting.js を空にしているので、既存の設定を引き継ぎたい場合にはホームディレクトリにある .node-red フォルダからちゃんとコピーしてください。
そしてノード開発用のリポジトリ (今回は c:\work\GitHub\node-red-contrib-rtk-board) を作成し、管理者権限のコマンドプロンプトからシンボリックリンクを作成すれば開発準備は完了です。
mklink /d node_modules\node-red-contrib-rtk-board c:\work\GitHub\node-red-contrib-rtk-board今回の機能を実際に試してみたい方は、まだ npm に登録されていないので、node_modules フォルダに必要ファイルを用意するため、ローカルで準備したNode-RED のフォルダで以下を実施してください。
cd node_modules git clone https://github.com/yamachan/node-red-contrib-rtk-board.gitなお開発中に動作がおかしくなったら、.node-red 設定フォルダを settings.js ファイルだけにして、Node-RED を再起動すればok。
開発を始めよう
さて、準備ができたところでノード開発を始めましょう。基本的な構成は Virtual TJBot を参考に進めていきます。
設定ノード
まずは基本となる設定ノードから進めましょう。開発ガイド がとても参考になります。
後でいくらでも拡張できるので、最初は最低限の項目だけ設定できるようにします。
- ボードの種類 (いわゆる黒板、ホワイトボード、ブラックボードなど)
- ボードの大きさ (横幅、高さ)
以下が実際に開発した設定画面です。
わりと基本に忠実で、特別なことはやっていません。短いですし、詳細は実際のコードを見てください。Output ノード
これが今回の主役ノードで、黒板への出力を全て担っています。
実はループや条件判断もある、簡易言語として実装されていて、テキストとしてプログラムを与える(payloadに指定する)といろんな操作ができたりします。詳しくは README を参照してください。時間があったら日本語の解説記事を書きますね!入力を payload で受けるだけなので、設定画面は最低限です。「RTK Board Config」は前の章で説明した黒板全体の設定ノードです。
実装コードは以下で、dist フォルダに配置した黒板用の html と、そこで利用している描画用の js コードも含みます。
- rtk-board/output.html
- rtk-board/output.js
- rtk-board/ui.js
- rtk-board/dist/index.html
- rtk-board/dist/rtk-face.js
- rtk-board/dist/rtk-board.js
Output ノードの動作例 (基本的な使い方)
Output ノードの使い方は簡単で、Inject ノードを用意し、ペイロードを文字列に設定して、描画用のコマンドを記載すればokです。
以下はコマンドを記載した Inject ノードを幾つか接続して、上から順にクリックしていった結果です。
cls rect 80 60 50 50 red fillRect 100 80 70 70 blue bp;arc 30 200 40 0 2 yellow;stroke bp;arc 60 200 160 0 2 pink;fill
コマンドは;
文字で繋げることで、複数指定することができます。今回の例ではわかりやすいように Inject ノードを幾つも並べていますが、1つの Inject ノードのテキストに、全部指定してしまってもokです。Output ノードの動作例 (ステートの利用)
Output ノードで処理する際、処理系はステートフルに実装されていて、過去の描画パラメータを覚えています。
各コマンドのパラメータは省略されるか、省略を意味する
_
文字が指定されると、最後に実施した値を再利用します。また color のような命令は、実際に描画を実施せず、この保存された値を更新します。以下の例を参照してください。これも上から順に Inject をクリックした結果です。
cls rect 100 100 50 50 red rect 80 80 color blue rect 40 40 color white; go 10 10 rect 40 40
rect
は四角形を描画する命令で、最初の2つの引数は縦横のサイズを、次の2つの引数は表示位置を、そして次の引数は描画色を指定します。例えば Inject (3) にある
rect
はサイズしか指定されていないため、表示位置と表示色は (2) のrect
の描画と同じになっています。Inject (5) にあるrect
も同様にサイズしか指定されていませんが、その前の (4) でcolor
命令で描画色が青に変更されているため、青い四角形を描画しています。Inject (7) にある最後の
rect
もやはりサイズしか指定されていませんが、その直前 (6) でcolor
で描画色が白に変更されており、またgo
で表示位置も移動されているので、左上に白い四角形として描画されています。Output 処理系のステートを利用して指定を省略することで、指定するコマンドの総量を減らすことができます。また修正箇所が減り、変更も容易になります。
Output ノードの動作例 (ランダム表示)
ステートはランダム系のコマンドとも相性が良いです。
例えば以下はランダムに位置を指定する
goRand
コマンドと、colorRand
コマンドを利用した例になります。cls goRand; colorRand; rect 10 10Inject (2) をクリックすると、ランダムな位置に、ランダムな色で、小さな四角形が表示されます。何度もクリックすると、上記のようにカラフルな画面になりました。
後で説明のある
loop
コマンドと組み合わせると、いろいろな表示が楽しめそうです。Output ノードの動作例 (ステートの演算)
ステートを利用した、もう少し複雑なサンプルを見てみましょう。
まず (2) のコマンドに注目してください。円の半径に45%
を指定されていて、これは表示する黒板のサイズの 45% の長さを指定したことになります。縦横の長さが違う場合には小さいほうの値をもとに計算されます。そして表示位置として
50% 50%
が指定されていて、これはそれぞれ、黒板の横幅の50%、縦幅の 50% を意味しており、結果として黒板の中心を示しています。今回のサンプルでは Inject を以下の順でクリックしています。
(1) → (2) → (3) → (4) → (3) → (4) → (3) → (4) → (3) → (4)
つまりは以下を実行したことになります。
cls bp;arc 45% 50% 50%;stroke let _r $($._r * 0.8) bp;arc;stroke let _r $($._r * 0.8) bp;arc;stroke let _r $($._r * 0.8) bp;arc;stroke let _r $($._r * 0.8) bp;arc;strokeそして鍵となるのが (3) の
let
コマンドです。let _r $($._r * 0.8)
let
コマンドは、ステートの内部値(描画を実施する際の変数の値)をセットするためのコマンドです。そしてパラメータを処理する際に$(
と)
で囲まれた範囲は、式として評価され演算が実行されます。そして式のなかで、内部値は
$
オブジェクトの値として参照できます。今回のlet
コマンドを実施すると、円の半径を意味する_r
という内部値の値に対して0.8
の数を積算していますから、つまりは半径が少し小さくなります。(3) と (4) を繰り返し Inject してコマンドを実行するたび、より半径の小さい円が描画される、というわけです。
Output ノードの動作例 (ループの例)
さきほどは手動で Inject (3) (4) を何回かクリックしました。これをループを使って自動化してみましょう。
cls bp;arc 45% 50% 50%;stroke let l 4 let _r $($._r * 0.8);bp;arc;stroke;loop l -5ここでは
let
コマンドを使って、ループ回数をカウントする内部値(変数)であるl
を定義しています。4回実施したいので、値は4
をセットしています。その後の
loop
コマンドが繰返しを実施する命令で、これは指定された内部値(変数)、今回はl
を使って繰り返しを実施します。具体的にはl
から1を引き、0より大きければその後にある値-5
を実行カウンタに加える、つまり5つ命令を戻しています。昔のアセンブリ言語にあったような、原始的な繰り返し命令です。現時点ではまだエラーチェックが甘く、下手するとブラウザがフリーズしかけるので、注意して扱ってください。
ステートの仕組みや、相対的な移動である
move
コマンドと組み合わせてうまく使えば、記述を大きく減らせる可能性をもっています。まぁ、趣味の機能なのでゆるーく楽しむ程度にしてください。Face ノード
今回の黒板機能は、Output ノードだけで完結しています。この Face ノードは、その機能を簡単に使うための、サポート用のノードになります。
次のサンプルをみてください。
(3) の Face ノードの設定は以下のようになっています。
また (4) の Inject のペイロードは以下の文字列になっていて、face コマンドを記述しています。
face smile rf 80% 80% 10% 10%このサンプルにおいて、(2) の smile を設定した Inject をクリックしても、(4) の face コマンド設定した Inject をクリックしても、描画される画像は同じです。
つまり Face ノードは、Output に実装された face コマンドを簡単に記述するためのノードです。基本的な情報を設定すれば、後は smile(笑顔), ugly(憂鬱顔), sad(悲しい顔), safe(安心した顔), angly(怒り顔), usual(素の顔) などの使いたい表情(face_mode)をペイロードで指定するだけで描画できます。
実際のコードはこちら。テキストのコマンドを並べるのも楽しいですが、やはりこの Face のようなサポート用のノードを使ったほうが簡単に楽しめますし、Node-RED っぽいですよね。今後も、サポート用のノードを増やしていく予定です。
縦長の顔はいやだ
今回の Face ノードのサンプルを見て「縦長の顔は可愛くない」との指摘をいただきました。表示サイズを % で指定しているので、表示する黒板のサイズにあわせて顔が縦長に表示されています。
とりあえずの解決策なのですが、縦のサイズを
80%
という黒板からの相対値ではなく、$_w
という内部値を参照としたものに変更してください。``_w'' は横のサイズを示す内部値で、それを指定することで、表示する際に縦横同じ、つまり正方形の領域に顔が表示されるようになります。
Inject に指定したコマンドのほうも同様に変更しておきます。
というわけで
とりあえずはサイドバー領域に黒板的な表示パネルを追加し、そこに描画するためのノードを定義できました。ノード開発楽しいです。
まだ実装できそうな描画コマンドはありますし、それらを簡単に扱えるようにサポートノードの数も増やしたいところです。しばらくはコードを修正して、機能を拡張し、年内の npm への公開まで頑張ります!
とりあえずはステータスちゃんと表示して、国際化して日本語追加して、あとは画像表示コマンドの実装とラベル機能の完成、はクリスマスまでには終わらせる予定。などと宣言して自分を追いつめてみる。
それではまた!
- 投稿日:2019-12-14T22:14:09+09:00
Express + Vue.jsでhelloを出してみよう
おはようございますこんにちはこんばんは!
最近Front-endのフレームワークが色々出て流行ってるらしくて、
友達となんかやってみよー!になってはじめました。
最初、どのフレームワークを使おうかと、
'react / vue が人気らしいー何が違う?'とざっと検索してみたら
- Vue
- テンプレート形式でアプリの制作したいなら
- 簡単で「一旦動作」ができるのが好きなら
- 早くて軽量のアプリが作りたいなら
- React
- 大きい規模のアプリを作るなら
- もっと大きい情報が欲しいなら
こんな差がありました。
簡単なプロジェクトだから、VueにしようーでVueを選びました。
(実は韓国で何もわからずVueの本を買ってきたので、、最初からvueにしようと決めたこともあり、、笑)back-endはnode.jsのexpress フレームワークを使います(なんでもjsでやってみよう感)
やってみましょう^0^
node.js install
https://nodejs.org/
installします。ltsの方が安定的らしくてltsの方インストールしました。コマンドラインでインストる確認
$ node -v
バージョンが出力されたらokpackage.jsonでパッケージ管理
package.json生成
$npm init
express 設置 node_modulesというフォルダが生成されます
$npm install --save express
4.htmlファイルを置いておくpublic directoryをnode_modulesと一緒の段階に生成
生成
$mkdir public
入る
$cd public
index.htmlページ生成
$vi index.html
index.html<!DOCTYPE html> <html> <head> <title>Vue.js Sample</title> </head> <body> <div id="app"> <h1>{{ message }}</h1> <input v-model='message'> </div> <!-- vue.js読み込み--> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> new Vue({ el: '#app', data: { message: 'Hello Vue.js!' } }) </script> </body> </html>v-modelを使ってinputとtextareaのエレメントに両方向のデーターバインディングの生成が出来ます。
javascriptのjqueryだと inputのvalueを持って、、それをh1に適用して、、みたいなことを
nue Vue ~~ だけで出来ます。5.node.jsを実行するindex.jsを生成(node_modulesとpublicと一緒の段階)
$cd ../
$vi index.js
index.jsconst express = require('express'); const app = express(); const PORT = process.env.PORT = 8000; //port8000に指定、変えてもok app.use(express.static('public')); // app.listen(PORT, () => { console.log('Server is running at:',PORT); });6.index.jsのところで実行
$node index.js
consoleServer is running at: 8000表示されたら localhost:8000に接続すると
こういう画面が出て、下のinputに内容を変更するとHello Vue.js!の大文字が変更されます。
とても簡単!vue.jsの解説なんかより、一応、、画面表示してみよう!になっちゃったんですが、(汗)
これで終わり!参考
https://jp.vuejs.org/index.html
自分は韓国人なので、https://kr.vuejs.org/index.html こちら参照しました (笑)
https://joshua1988.github.io/web_dev/vue-or-react/ 韓国語です (汗)
- 投稿日:2019-12-14T21:18:41+09:00
npmパッケージを公開するならnpが便利!
よくある困りごと
npmパッケージを公開・更新する場合、手順を忘れがちですよね(少なくとも私はそうです)。
これから紹介するnpコマンドを使えば、楽にnpmパッケージの公開、更新、さらにはGitHubのtagまで切ってくれる。最高です。npコマンドの導入
yarn global add npnpを実行すると、バージョンの選択画面が表示され、major, minor, patchのアップデートかどうかの選択肢が出る。
patchを選択した場合$ np Publish a new version of パッケージ名 (current: 1.0.3) Commits: - コミット内容 Commit Range: v1.0.3...master ? Select semver increment or specify new version patch 1.0.4 ✔ Prerequisite check ✔ Git ↓ Cleanup [skipped] ✔ Installing dependencies using Yarn ✔ Running tests using Yarn ✔ Bumping version using Yarn ✔ Publishing package using Yarn ↓ Pushing tags [skipped] → Upstream branch not found; not pushing. ✔ Creating release draft on GitHub パッケージ名 1.0.4 published ?参照
- 投稿日:2019-12-14T20:50:27+09:00
「File ./dist/index.js that is expected to define function doesn't exist」と言われた(TypeScript)
Google Cloud FunctionsとTypeScriptを使ってとあるSlackボットを作っていたら、正しく依存関係を指定しているはずなのに「File ./dist/index.js that is expected to define function doesn't exist」というエラーが出てうまくデプロイできず、結構ハマったので原因と解決策を。
先に結論。
ここに書いてあることがすべて
構成
├── .gitignore ├── README.md ├── dist │ └── index.ts ├── package-lock.json ├── package.json ├── src │ └── index.ts └── tsconfig.jsonsrc配下にTypeScriptのファイルが配置されていて、それをコンパイルするとdist配下にJSが吐かれるというよくあるかんじの構成です。
package.jsonの
main
にはdist/index.js
が指定されています。また、.gitignoreにはdistが指定されています。これもTypeScriptのプロジェクトではよくあることかなと思います。
デプロイしようとした
完成したのでデプロイしようとして以下のようなコマンドを実行しました。
tscでコンパイルを行い、2行目のgcloudを使ってCloud Functionsへデプロイを実行しました。
$ npx tsc $ gcloud functions deploy hoge --runtime nodejs8 --trigger-http --project hoge-projすると、「File ./dist/index.js that is expected to define function doesn't exist」というエラーが発生しました。
間違いなくdistの下にindex.tsファイルはあるのにおかしいと思い、いろいろ調べたり悩んだ結果たどり着いたのが上記に貼ったStackOverflowでした。
原因
gcloud functions deploy
を実行すると.gcloudignore
というファイルが自動的に作成されます。こんなファイルです
# This file specifies files that are *not* uploaded to Google Cloud Platform # using gcloud. It follows the same syntax as .gitignore, with the addition of # "#!include" directives (which insert the entries of the given .gitignore-style # file at that point). # # For more information, run: # $ gcloud topic gcloudignore # .gcloudignore # If you would like to upload your .git directory, .gitignore file or files # from your .gitignore file, remove the corresponding line # below: .git .gitignore node_modules #!include:.gitignore.gitやnode_modulesなど、Cloud Functions側にあげる必要のないファイルを.gitignoreと同じような形式で書いておくことで、無視してくれるようになります。
今回原因となったのは一番最後の行、
#!include:.gitignore
これです。これは.gitignoreの内容を取り込んでくれるものになります(多分)。今回私は.gitignoreにdistを指定していたため、gcloud function deployを実行しても肝心のdist以下がアップロードされずにエラーが出た、というのが原因でした。
解決策
最後の行に
!dist
と書き足すとよいです。これだけでdist以下を読んでくれるようになります。ついでにsrcはあげなくていいのでsrc
と書き足しておくといいかもしれません。まとめ
普通にJS書いてた時はうまくいってたので、油断してたらこんな罠が…と言う感じでした。
FirebaseのFunctionsはfirebase-toolsで初期化するときにTypeScriptを使う選択をすると、自動的にこの辺のファイルとかのテンプレを吐き出してくれるので、困ったことなかったのですが、今回使ったのはGCPの方のFunctionsだったのでハマってしまいました。
調べ方がわるかったのか、なかなか上記StackOverFlowにも辿りつかず、結構苦労しました。
この記事が誰かの役に立つといいなあ。
おわり。
- 投稿日:2019-12-14T20:50:27+09:00
CloudFuntionsで「File ./dist/index.js that is expected to define function doesn't exist」と言われた(TypeScript)
Google Cloud FunctionsとTypeScriptを使ってとあるSlackボットを作っていたら、正しく依存関係を指定しているはずなのに「File ./dist/index.js that is expected to define function doesn't exist」というエラーが出てうまくデプロイできず、結構ハマったので原因と解決策を。
先に結論。
ここに書いてあることがすべて
構成
├── .gitignore ├── README.md ├── dist │ └── index.ts ├── package-lock.json ├── package.json ├── src │ └── index.ts └── tsconfig.jsonsrc配下にTypeScriptのファイルが配置されていて、それをコンパイルするとdist配下にJSが吐かれるというよくあるかんじの構成です。
package.jsonの
main
にはdist/index.js
が指定されています。また、.gitignoreにはdistが指定されています。これもTypeScriptのプロジェクトではよくあることかなと思います。
デプロイしようとした
完成したのでデプロイしようとして以下のようなコマンドを実行しました。
tscでコンパイルを行い、2行目のgcloudを使ってCloud Functionsへデプロイを実行しました。
$ npx tsc $ gcloud functions deploy hoge --runtime nodejs8 --trigger-http --project hoge-projすると、「File ./dist/index.js that is expected to define function doesn't exist」というエラーが発生しました。
間違いなくdistの下にindex.tsファイルはあるのにおかしいと思い、いろいろ調べたり悩んだ結果たどり着いたのが上記に貼ったStackOverflowでした。
原因
gcloud functions deploy
を実行すると.gcloudignore
というファイルが自動的に作成されます。こんなファイルです
# This file specifies files that are *not* uploaded to Google Cloud Platform # using gcloud. It follows the same syntax as .gitignore, with the addition of # "#!include" directives (which insert the entries of the given .gitignore-style # file at that point). # # For more information, run: # $ gcloud topic gcloudignore # .gcloudignore # If you would like to upload your .git directory, .gitignore file or files # from your .gitignore file, remove the corresponding line # below: .git .gitignore node_modules #!include:.gitignore.gitやnode_modulesなど、Cloud Functions側にあげる必要のないファイルを.gitignoreと同じような形式で書いておくことで、無視してくれるようになります。
今回原因となったのは一番最後の行、
#!include:.gitignore
これです。これは.gitignoreの内容を取り込んでくれるものになります(多分)。今回私は.gitignoreにdistを指定していたため、gcloud function deployを実行しても肝心のdist以下がアップロードされずにエラーが出た、というのが原因でした。
解決策
最後の行に
!dist
と書き足すとよいです。これだけでdist以下を読んでくれるようになります。ついでにsrcはあげなくていいのでsrc
と書き足しておくといいかもしれません。まとめ
普通にJS書いてた時はうまくいってたので、油断してたらこんな罠が…と言う感じでした。
FirebaseのFunctionsはfirebase-toolsで初期化するときにTypeScriptを使う選択をすると、自動的にこの辺のファイルとかのテンプレを吐き出してくれるので、困ったことなかったのですが、今回使ったのはGCPの方のFunctionsだったのでハマってしまいました。
調べ方がわるかったのか、なかなか上記StackOverFlowにも辿りつかず、結構苦労しました。
この記事が誰かの役に立つといいなあ。
おわり。
- 投稿日:2019-12-14T20:44:31+09:00
yarn link実行後、cliでコマンドを入力してもpermission deniedになる場合
- 投稿日:2019-12-14T20:44:31+09:00
yarn link実行後、cliでコマンドを入力してもPermission deniedになる場合
- 投稿日:2019-12-14T16:31:47+09:00
Firestoreの対話型シェルをいきなり起動する
Firestoreでちょっとしたクエリの実験するときにNode.jsのREPL(Read-Eval-Print-Loop)使って対話型シェルを利用している人は多いと思います。その時、毎回
node
コマンドを実行したあとに.load スクリプト名
などとして接続をしている人はまさかいませんね? 人によってやり方は様々だと思いますが私のスクリプトを以下に貼っておきます。db#!/bin/bash node --require await-outside/repl -i -e " const firebase = require('firebase-admin'); var serviceAccount = require('./.service-account.json'); firebase.initializeApp({ credential: firebase.credential.cert(serviceAccount) }); var db = firebase.firestore(); "このスクリプトを開発環境のFirebaseプロジェクトのトップにでも設置しておけば
./db
のコマンドでFirestoreへの対話型シェルが出現します。以下のような感じで実行できます。便利でしょ?$ ./db > await db.collection('tests').doc('hoge').set({name: 'oreore'}); WriteResult { _writeTime: Timestamp { _seconds: 1576307204, _nanoseconds: 63907000 } } > (await db.collection('tests').doc('hoge').get()).data() { name: 'oreore' }設置方法
設置方法を簡単に開設しておきます。やるべきことはパッケージのインストールとFirebaseサービスアカウントのキーの配置です。
パッケージのインストール
ここで使うのはこの2つ
npm i firebase-admin --save npm i await-outside --saveサクッとインストール。
サービスアカウントのキーの配置
Firebaseコンソールにログインしてサイドメニューの
Settings>ユーザーと権限
からたどって新しい秘密鍵の生成
をクリックします。ここから得られるjsonを
.service-account.json
という名前で、冒頭のdb
スクリプトと同じディレクトリに配置します。このファイルは機密情報を含んでいるので忘れずにgitignoreしときましょう。あ、dbスクリプトに実行権限を与えることもお忘れなく。
以上
Special Thanks
- 投稿日:2019-12-14T15:57:00+09:00
初心者が Googleアシスタント と Vue.js を使ってスマートディスプレイアプリを作ってみた
はじめに
いちあき(@ichiaki_kazu)と言います。初めてのQiita記事です。
僕はこれまでアセンブラやHTMLくらいしか触ってこなかった(ほぼ)ノンプロフリーランスです。
それをフリーランスというのか置いといて…11月に行った「Google Nest Hub対応スマートディスプレイスキルを作ろう!【Vue.js】」というハンズオンが楽しかったので、勉強も兼ねてハンズオン内容を参考にしつつ自分で作ってみました。
この記事の目的
- 自身のやったことを整理して定着させる
- 共に音声アプリの概要を掴んでもらえたらな
もしかしたらこの通りやっても動かないかもしれないので鵜呑みにしないでください。
※知識不足により誤っている部分がある可能性があります。その時は優しく指摘してください…参考資料
・「Google Nest Hub対応スマートディスプレイスキルを作ろう!【Vue.js】」(2020/1に大阪でも開催)
・Qiita「滑舌チェックスキルをGoogle Nest Hubで実装してみた」完成物
【Google Nest Hub対応アプリつくった】
— いちあき@ぷちスマーティスト (@ichiaki_kazu) December 6, 2019
出てきた果物を英語で呼んであげるクイズアプリを作ってみました!
がおまるさん(@gaomar)が作られた滑舌チェックスキルを参考にさせていただいてます。元ネタは続きに…#GoogleNestHub #駆け出しエンジニアとつながりたい pic.twitter.com/87ZMpjG2DX音声でアプリを呼び出して、出てきたくだものの画像を見て名前を当てるゲームです。
幼児になら使ってもらえるかなって…今回使ったもの
- Google Nest Hub:実機動作確認のため
- Dialogflow:自然言語処理
- Vue.js(VueCLI):アプリの画面生成
- Azure Functions(CLI):内部の処理(クイズ部分とか入出力とか)
- Vuetify:Vueで使えるフレームワーク
- IntaractiveCanvas:(重要)Googleアシスタントアプリで画面描写に必要なライブラリ
実際に作ってみる
作るに当たってポイント部分だけを解説していきます。
実際に作ってみたい人は「滑舌チェックスキルをGoogle Nest Hubで実装してみた」をまず確認すると良いと思います。1.Dialogflow(言語処理をする)
Googleアシスタントによって入力された音声の処理を行います。
Intent(インテント)を作成することで、言葉に反応して何かを返します。1-1.インテント作成
Intent名 内容 Default Welcome Intent アプリ起動の言葉 EndIntent アプリ終了の言葉 MainIntent アプリ起動中の言葉 StartIntent アプリ起動後に開始する言葉 1-2.実際にインテントを作る(例:StartIntent)
- Training phrases:反応する言葉を登録
- Fulfillment:webhookでやりとりするためチェックを入れる
その他のインテントも同じように作ります。
ちなみに「Response」には反応時返すメッセージを登録できます。
詳しくは参考記事をご覧ください。2.Vue.js(アプリ画面を作る)
アプリの画面描写部分を作っていきます。
ざっくりとやることは以下の通り
- プロジェクト作成(vuetifyも入れる・今回はrouterも)
- index.htmlにIntaractivCanvasのAPI追加
- App.vueの修正
- Home.vueの修正
- HelloWorld.vueの修正
2-1.プロジェクト作成
プロジェクトを作ってvuetifyを入れます。
vue create kudamonoquiz-app vue add vuetify今回はプロジェクト作成時にrouterも入れておきました。
2-2.index.htmlにIntaractiveCanvasのAPI追加
画面描写のキモである「InteractiveCanvas」のAPIを引っ張ってきます。
public/index.html(12行目あたり)<script type="text/javascript" src="https://www.gstatic.com/assistant/interactivecanvas/api/interactive_canvas.min.js"></script>2-3.App.vueの修正
ここではヘッダー部分とrouterへの連携をしています。
routerは初期状態でHome.vueに流れます。src/App.vue<template> <v-app> <v-app-bar app> <v-toolbar-title class="headline text-uppercase"> <span>くだものくいず</span> </v-toolbar-title> </v-app-bar> <v-content> <router-view /> </v-content> </v-app> </template>2-4.Home.vueの修正
コンポーネント「HelloWorld.Vue」を表示するようにします。
src/Home.vue<template> <HelloWorld /> </template> <script> import HelloWorld from '../components/HelloWorld.vue' export default { name: 'home', components: { HelloWorld } } </script>2-5.HelloWorld.vueを修正(ポイント)
app側のメインコンテンツです。
src/Home.vue<template> <v-container> <!-- スタートページ --> <v-layout text-center wrap v-show="target === 'top'" > <v-flex xs12 md-10 > <h3 class="display-3 font-weight-bold mb-10"> </h3> </v-flex> <v-flex xs12> <h1 class="display-2 font-weight-bold mb-10"> くだものえいご </h1> </v-flex> <v-flex xs12 mb-4> <v-btn color="success" @click="start">スタート</v-btn> </v-flex> </v-layout> <!-- 問題表示ページ --> <v-layout text-center wrap v-show="target === 'kudamono'" > <v-flex xs12 md-10 > <h3 class="display-3 font-weight-bold mb-10"></h3> </v-flex> <v-flex xs12 mb-4> <h1 class="display-1 font-weight-bold mb-3"> 【くだものえいご】 </h1> </v-flex> <v-flex xs12 mb-4> <img class="img" :src="imgurl" alt="くだもの画像"> </v-flex> <v-flex xs12 mb-4> <h5 class="display-1 font-weight-bold mb-3"> このくだものはなーんだ? </h5> </v-flex> </v-layout> <!-- 正解後の表示ページ --> <v-layout text-center column align-center v-show="target === 'congratulation'" > <v-flex xs12 mb-10 > <h3 class="display-3 font-weight-bold"> </h3> </v-flex> <v-flex xs12 mb-4 > <img class="img" alt="congratulation" src="../assets/congratulation.png"> </v-flex> <v-flex xs12 mb-4> <img class="img" :src="imgurl" alt="くだもの画像"> </v-flex> <v-flex xs12 mb-4> <h2>だいせいかい!<br>これは{{kotae}}({{tango}})だよ!</h2> </v-flex> <v-flex xs12 mb-10 > <v-btn large color="success" @click="start">スタート</v-btn> </v-flex> </v-layout> </v-container> </template> <style scoped> .img{ width: 300px; } .endimg{ width: 200px; } </style> <script> export default { data () { return { target: 'top' } }, created(){ var me = this const callbacks = { onUpdate(data){ if('kudamono' in data){ me.kotae = data.kudamono.kotae, me.tango = data.kudamono.tango, me.imgurl = data.kudamono.imgurl, me.target = data.kudamono.target } }, } interactiveCanvas.ready(callbacks) }, methods: { start(){ interactiveCanvas.sendTextQuery('スタート'); } } }; </script>ざっくり解説するとこのファイルでは3つがポイントだと思います。
- targetの状態で表示するコンテンツ(開始時・ゲーム時・正解時)を分ける
- functionから送られてくるdata(画像URLや問題)をセットし、使用する
- 最初にスタートボタンを押した時
interactiveCanvas.sendTextQuery('スタート');
でDialogflowに「スタート」という文字列を送る特に文字列を送る昨日はinteractiveCanvasならではなので、ポイントかと思います。
2-6.package.json確認
後々動かす際にnpmインストールするのでpackage.jsonの確認をします。
{ "name": "kudamonoquiz-app", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build" }, "dependencies": { "core-js": "^3.4.3", "vue": "^2.6.10", "vue-router": "^3.1.3", "vuetify": "^2.1.0", "vuex": "^3.1.2" }, "devDependencies": { "@vue/cli-plugin-babel": "^4.1.0", "@vue/cli-plugin-router": "^4.1.0", "@vue/cli-plugin-vuex": "^4.1.0", "@vue/cli-service": "^4.1.0", "sass": "^1.19.0", "sass-loader": "^8.0.0", "vue-cli-plugin-vuetify": "^2.0.2", "vue-template-compiler": "^2.6.10", "vuetify-loader": "^1.3.0" } }おそらくこの状態だと思いますが、念のための確認です。
3.Azure Functions(内部のプログラムを作る)
3-1.CLIツールをインストールする
とりあえずCLIツールをインストールします
npm install -g azure-functions-core-tools3-2.プロジェクトを作成する
このあたりは完全にこの記事と同じです。
$ mkdir kudamonoquiz-app-functions $ cd kudamonoquiz-app-functions $ func init # 選択肢が出てくるのでnodeとjavascriptを選ぶ $ func new # Http triggerを選択し、kudamonoquiz-appという名前で作成する $ npm init -y $ npm i -s actions-on-google@2.10.0 # 2.10.0を入れる $ npm i -s azure-function-express # azure-function-expressを入れる $ npm i -s express $ npm i -s firebase-admin3-3.functions.jsonを編集
完全にこの記事と同(ry
GETのみに変更して、どこからでもアクセスできるようanonymousにします。kudamonoquiz-app/functions.json{ "bindings": [ { "authLevel": "anonymous", // anonymousにしておく "type": "httpTrigger", "direction": "in", "name": "req", "methods": [ "post" // getは使わないので消しておく ] }, { "type": "http", "direction": "out", "name": "res" } ] }3-4. index.jsを編集
ワードが入力された際にインテントに応じて処理を変えます。
kudamonoquiz-app/index.jsconst createHandler = require("azure-function-express").createHandler; const express = require("express"); const {dialogflow, HtmlResponse} = require('actions-on-google'); const app = dialogflow({debug: false}); //スタート処理 app.intent('StartIntent', async (conv) =>{ //問題の生成 const kudamonoquiz = [ {"imgurl": "/img/banana.jpg", "kotae": "バナナ", "tango": "banana"}, {"imgurl": "/img/cherry.jpg", "kotae": "チェリー", "tango": "cherry"}, {"imgurl": "/img/grape.jpg", "kotae": "グレープ", "tango": "grape"}, {"imgurl": "/img/melon.jpg", "kotae": "メロン", "tango": "melon"}, {"imgurl": "/img/orange.jpg", "kotae": "オレンジ", "tango": "orange"}, {"imgurl": "/img/peach.jpg", "kotae": "ピーチ", "tango": "peach"}, {"imgurl": "/img/remon.jpg", "kotae": "レモン", "tango": "remon"}, {"imgurl": "/img/strawberry.jpg", "kotae": "ストロベリー", "tango": "strawberry"} ]; const wordIndex = Math.floor(Math.random() * kudamonoquiz.length); const selKudamono = kudamonoquiz[wordIndex]; conv.contexts.set('game', 5, selKudamono); conv.ask('このくだものを英語で言ってみてね'); selKudamono["target"] = "kudamono"; conv.ask(new HtmlResponse({ data: { kudamono: selKudamono } })); }); //ゲーム処理 app.intent('MainIntent', async (conv, {any}) => { const context = conv.contexts.get('game'); if(context.parameters.kotae === any){ conv.contexts.delete('game'); conv.ask(`大正解。答えは ${context.parameters.kotae} でした! もう一度クイズをするなら「する」終了するなら「終了」と言ってください。`); context.parameters["target"] = "congratulation" }else{ conv.ask('よくわかりませんでした。もういちど言ってみてね。'); } conv.ask(new HtmlResponse({ data: { kudamono: context.parameters } })); }); //起動時 app.intent('Default Welcome Intent', (conv) => { conv.ask('果物英語をはじめるには、スタートボタンを押してください。'); conv.ask(new HtmlResponse({ url: 'https://{表示させたいホームページのURL}', supperss: true })); }); //functionの名前を一致させておく const expressApp = express(); expressApp.post('/api/kudamonoquiz-app', app); module.exports = createHandler(expressApp);実際に動かしてみる
プログラムを動かしていきます。
Vue.jsの起動
npmインストール
kudamonoquiz-appでnpmインストールします。
cd ./kudamonoquiz-app npm installプログラム実行
npm run serveこれでhttp://localhost:8080にアクセスできます。
トップ画面が表示されます。
僕はこのあとngrokを使いましたがとりあえずこれでも動くと思います。Azurefunctionsの起動
npmインストール
kudamonoquiz-functionでnpmインストールします。
cd ./kudamonoquiz-function npm installindex.js内にアプリのURLを記述する
kudamonoquiz-function/kudamonoquiz-app/index.js(抜粋)//起動時 app.intent('Default Welcome Intent', (conv) => { conv.ask('果物英語をはじめるには、スタートボタンを押してください。'); conv.ask(new HtmlResponse({ url: 'http://localhost:8080', supperss: true })); });ローカルサーバを起動する
func host starthttp://localhost:8080/でアプリが起動します。
Dialogfrowの設定
FulfillmentのwebhookURLを指定する
テストする
IntegrationからGoogle Assistantを選択
出てきたポップアップで「Auto-preview changes」にチェック入れて「TEST」
Deployで「category」を「Games&fun」にする。※InteractiveCanvasに必須
Testから実際に動作を確認する。
Androidスマホがある方はそちらからでも確認できます。
今回はテストまでなのでデブロイはなしで、ここまでとなります。
ここまで実施すれば実機でテストバージョンとして動作確認もできます。以上です。
もしかしたら手順漏れなどあるかもしれませんが大体こんな流れです。
興味ある方は是非是非試してください〜僕も答えられるかわかりませんが、不明点がありましたらお願いします!
宣伝みたいな
https://atlabo.connpass.com/event/157824/
2020年の1/23(木)に僕がスマートディスプレイにハマったきっかけのハンズオンが大阪でも開催されます。
興味ある方はおすすめですのでぜひ!※僕もスタッフとして行きます
- 投稿日:2019-12-14T13:39:01+09:00
linux版 nodeとnpmとprettier
まず確認
Nodejs
ローカルでもjavascriptを使えるようにしたもの
npm
パッケージ管理
n
Nodejsのバージョン管理aptからインストール
インストール先:
/usr/bin/
sudo apt install nodejs npmnpmでnをインストール
インストール先:
/usr/local/bin/
sudo npm instal -g nn
nでインストールされるNodeの場所は以下になります。
/usr/local/bin/
インストール可能なバージョンを調べる
最新の20件取得 n lsr すべて取得 n lsr --allnodeインストール&削除&バージョン切り替え
インストール sudo n バージョン インストール済みNodejs一覧 n ls バージョン切り替え sudo n バージョン 削除 n rm バージョンヘルプ
npm help npm
n help
npm
npm本体
バージョン一覧 npm info npm versions アップデート sudo npm update -g npm インストール&ダウングレード sudo npm install -g npm@バージョンパッケージ(モジュール)管理
package.jsonをつくる
npm init or npm init -yローカル
プロジェクト/node_modulesにインストールnpm install パッケージ名グローバル
/usr/local/bin/npm/node_modulesにインストール
sudo npm install -g パッケージ名
開発環境
・インストールはローカルと同じ
・gitなどで管理する時は必須
・本番環境と開発環境を区別したい場合
・package.jsonの[devDependencies]に追記される。sudo npm install --save-dev パッケージ名package.json通りにすべてインストール
npm install依存関係を満たすようにインストール
npm ci依存の問題は必ずぶつかる壁なので、頭の片隅に置いといてください。npm lsするとよくerr missingがます。これは依存してるパッケージをインストールして欲しい、というお願いです。
ちなみに、package.json通りにインストールしたときに、生成されるのがpackage-lock.jsonですが、lockファイルに依存パッケージが全て記載されています。助かりますね。こちらの記載どおりにインストールしてくれるコマンドがciコマンド
です。
`パッケージのインストール場所確認
ローカル npm root グローバル npm root -gPrettier
最近はやっているフォーマッター
ローカル npm install prettier -D※しかし、これだけではまだ使えない。そこで次のやり方をします。
とても参考になったパスの通し方
ローカル環境にあるツールをバージョン毎に使い分けるnpmにもってこいのテクニックだと思います。
プロジェクトのあるパスのモジュールを参照する必殺技でもなんでもないなんの変哲のない相対参照(./)を使うやり方です。export PATH=$PATH:./node_modules/.bin
- 投稿日:2019-12-14T13:39:01+09:00
linux版 nodeとnpmをインストール
まず確認
Nodejs
ローカルでもjavascriptを使えるようにしたもの
npm
パッケージ管理
n
Nodejsのバージョン管理aptからインストール
インストール先: /usr/bin/
sudo apt install nodejs npm
npmでnをインストール
インストール先: /usr/local/bin/
sudo npm instal -g n
n
nでインストールされるNodeの場所は以下になります。
/usr/local/bin/
インストール可能なバージョンを調べる
最新の20件取得
n lsr
すべて取得
n lsr --all
nodeインストール&削除&バージョン切り替え
インストール
sudo n バージョン
インストール済みNodejs一覧
n ls
バージョン切り替え
sudo n バージョン
削除
n rm バージョン
ヘルプ
npm help npm
n help
npm
npm本体
バージョン一覧
npm info npm versions
アップデート
sudo npm update -g npm
インストール&ダウングレード
sudo npm install -g npm@バージョン
パッケージ(モジュール)管理
package.jsonをつくる
npm init
ローカル
プロジェクト/node_modulesにインストール
npm install パッケージ名
グローバル
/usr/local/bin/npm/node_modulesにインストール
sudo npm install -g パッケージ名
開発環境
・インストールはローカルと同じ
・gitなどで管理する時は必須
・本番環境と開発環境を区別したい場合
・package.jsonの[devDependencies]に追記される。
sudo npm install --save-dev パッケージ名
package.json通りにすべてインストール
npm install
パッケージのインストール場所確認
ローカル
npm root
グローバル
npm root -g
Prettier
最近はやっているフォーマッター
ローカルインストール
npm install prettier -D
※しかし、これだけではまだ使えない。そこで次のやり方をします。
とても参考になったパスの通し方
ローカル環境にあるツールをバージョン毎に使い分けるnpmにもってこいのテクニックだと思います。
プロジェクトのあるパスのモジュールを参照する必殺技でもなんでもないなんの変哲のない相対参照(./)を使うやり方です。
export PATH=$PATH:./node_modules/.bin
とても気に入ったので、
これ./
を
今日から執拗に使っていこうかなと思います。
- 投稿日:2019-12-14T11:32:02+09:00
VSCodeのREST ClientでCognitoを利用するためのツールを作ってみた
はじめに
皆さんはVisual Studio Codeの拡張機能の一つのREST Clientは利用していますか?
VSCode上でREST APIを呼び出すツールとして人気があると思います。
しかし、このツールではOAuth2.0がサポートされおらず、Cognitoで認証し、APIをコールすることが現時点ではできません。(2019年12月14日時点)
また、似たツールとしてPostmanが有名ですが、あちらはCognitoで認証してAPIをコールすることができますが、私は次の点でPostmanを利用せずにREST Clientを利用しています。
- テキストベースでAPIを定義することができるので、Gitで管理することができる。
Gitで管理することで、APIも変更があった場合もそのバージョンでコールするための情報が残っているので、APIのバージョンごとの呼び出しデータを管理する必要がなくなるのは大きかったです。
実装方法
ローカルにCognitoで認証を行いアクセストークンを取得してきてくれる認証サーバをローカルに立ててるだけです。
インストール方法
必要としているディレクトリで
npm i -D rest-client-cognito
でインストールすることができます。
Nodeの開発環境でななければ、グローバルインストールしていただければ利用できると思います。(未検証)使い方
プロジェクトディレクトリに
.env
ファイルを作成してください。
次の4つの項目を追加してください。.envRCC_USER_POOL_ID={CognitoのユーザープールID} RCC_CLIENT_ID={アプリクライアントID} RCC_USER_NAME={ログインするユーザー名} RCC_PASSWORD={ログインするユーザーのパスワード}次にコンソールで
rcc
コマンドを実行することで、Cognitoの認証サーバをローカルに立ち上げることができます。
rcc
で立ち上がる認証サーバはデフォルトで3000番ポートで起動するようになっています。
localhost:3000/login
をGETすることでアクセストークンを取得することができます。
(普通ログインといったらPOSTですが、深夜テンションで作成したのですっかりそんなことも忘れて作ってしまいました。。。指摘しないでもらえると助かります。。。)このAPIで返却されるデータの構造は次の形になります。
.json{ "token": "アクセストーン" }なので、これをREST Clientで利用する場合は次のように記述することで利用できるようになります。
RestClient.rest@rccPort = 3000 ### # @name login GET http://localhost:{{rccPort}}/login @authToken = {{login.response.body.token}}あとは
authToken
を色々なAPIで利用してもらえれば、Cognitoからもらった認証情報を用いてAPIをコールできるようになります。オプション
デフォルトで3000番ポートで起動しますが、だいたいの方は3000番ポートは使っていると思います。
なので、-p
か--port
オプションを利用することで、ポート番号を変更することができるようになっています、
例:rcc -p 4000
、rcc --port 4000
最後に
簡単な動作確認程度のことしかしていないので、何かありましたらGitの方でコメントお願いします。
- 投稿日:2019-12-14T10:58:41+09:00
axiosのヘッダーのconfigでちょっとハマった
某APIを試しててaxiosのconfig指定をミスってたので、自戒の意味を込めて残しておきます。
完全に自分用メモっぽいやつです。
ミスったコードなど
//省略 class Hoge { constructor() { //省略 } //ミスった方 methodA(IMAGE_PATH){ const file = fs.createReadStream(IMAGE_PATH); const form = new FormData(); form.append('image', file); form.append('entrance', 'detection'); const config = { 'X-ClovaOCR-Service-ID': this.SERVICE_ID, ...form.getHeaders() }; return axios.post(this.URL.RECOGNITION, form, config) } //うまくいった方 methodB(IMAGE_PATH){ const file = fs.createReadStream(IMAGE_PATH); const form = new FormData(); form.append('image', file); form.append('entrance', 'detection'); const config = { headers: { 'X-ClovaOCR-Service-ID': this.SERVICE_ID, ...form.getHeaders(), } } return axios.post(this.URL.RECOGNITION, form, config) } } const hoge = new Hoge(); hoge.methodA(); // エラー hoge.methodB(); // 成功axiosのconfigミス
慣れてつかってるうちにconfig=headersっぽいイメージで使ってたみたい。
通常configの中には他にもmethod指定やbodyデータなども入ってくるのでconfig.headers
な指定にしないとですね。const config = { 'X-ClovaOCR-Service-ID': this.SERVICE_ID, ...form.getHeaders() };const config = { headers: { 'X-ClovaOCR-Service-ID': this.SERVICE_ID, ...form.getHeaders(), } }
- 投稿日:2019-12-14T01:18:13+09:00
WASI (WebAssembly system interface) を Wasmtime と Node.js で試す
この記事は ZOZO テクノロジーズ Advent Calendar 2019 #1, #2, #3, #4, #5 のうち #4 の16日目の記事です。
昨日は @jon20 さんの「Buildkitを使ってDockerfile以外からビルドする」でした。
Buildkit
触ったことなかったので参考になりました!本日は ZOZO テクノロジーズ来年4月入社予定の @takewell がお送りします。
2019 年自分の中で一番注目のテクノロジーはなんだろうかと考えてみたのですが、WebAssembly 関連の記事は目に入ればだいたい読むようにしてると気づきました。
WebAssembly が掲げている能率的で高速, メモリセーフ, オープンに標準化されたテキストフォーマットによるデバッグやテストのやりやすさ はどれもワクワクさせられる特性です。
Web 信者としてはブラウザがネイティブアプリと遜色ない環境になるかもしれないという面で期待が膨らんでいます。ということで、今回は表題の通り、WebAssembly の中でも WASI とはなんであるかを WASI tutorial を参考に実際に手をうごかして試してみました。
WASI (WebAssembly system interface)
WebAssembly は Web ブラウザで新たな機能と大幅なパフォーマンス向上を提供する新しい種類のコードですが、今年に入ってStandardizing WASI: A system interface to run WebAssembly outside the web が発表されました。
この WASI とは何か、一言でいうと、セキュリテーとポータビリティに主眼において標準化、開発されているシステムインターフェースです。詳しい内容は wasi.dev にまとまっており、標準化も進んでいるところのようです。こちら要ウォッチですね。
WebAssembly そのものはガベコレやスレッド管理など未実装なものも残しており、速度の面でまだ物足りないと言われることがあります。さらに既存の JS Engine が十分高速なためプロダクションで有意差が出しにくい面があるため、ブラウザ環境では存在感はありません。一部 eBay (WebAssembly at eBay: A Real-World Use Case) がプロダクションで使っているらしく皆無というわけではありません。
一方ブラウザの外では WASI のコンセプトは既存のものと比べて画期的なものであるように感じます。Rust公式ガイドの著者 Steve Klabnik さんのブログで述べられているように Edge Computing や 組み込みデバイスの分野などの方がむしろ先に WebAssembly の存在感を出してくるかもしれないなどの妄想が膨らみます。
それでは本当に WASI が様々な環境で実行可能かを試していきたいと思います。
実行環境
$ sw_vers ProductName: Mac OS X ProductVersion: 10.14.6 BuildVersion: 18G87 $ rustup --version rustup 1.18.3 (435397f48 2019-05-22) $ cargo --version cargo 1.37.0 (9edd08916 2019-08-02) $ node -v v13.3.0WebAssembly 形式(.wasm) にコンパイルする
まず C, C++, Rust, Go, Kotlin, Lua などのプログラミング言語、または AssemblyScript(TypeScript ライクな言語) で .wasm 形式にコンパイルします。
LLVM が WebAssembly に対応しているので、LLVM が基盤になっている言語は対応される可能性が高いと思われます。
私は Rust からコンパイルしました。
以下のようにwasm32-wasi
というツールチェインの追加してビルドするだけです。$ cargo --bin new demo $ rustup target add wasm32-wasi $ cargo build --target wasm32-wasi $ file target/wasm32-wasi/debug/demo.wasm target/wasm32-wasi/debug/demo.wasm: , created: xxxxfn main() { println!("Hello, world!"); }これで上記コードを
demo.wasm
にコンパイルできました。WebAssembly Runtime で実行
もちろん WebAssembly の主な実行環境はブラウザと Node.js ですが、それ以外にもAwesome WebAssembly Runtimes にまとまっているだけで 26 個(2019/12/16 日時点)もあります。盛り上がってますね!
WASI を推進する文脈で Mozila, Fastly, Intel, RedHat が Bytecode Allianceというファウンデーションを作って結んでいます。このファウンデーションで開発されているランタイムが wasmtimeです。今回はこれを使います。以下でインストールと実行ができます。
$ curl https://wasmtime.dev/install.sh -sSf | bash $ source ~/.bash_profile $ wasmtime ./target/wasm32-wasi/debug/demo.wasm Hello, world!本当に動きました!
WebAssembly のテキストフォーマット (.wat)
先ほど、WebAssembly にコンパイルできる言語をいくつか挙げましたが、それに加えて WebAssembly にはS式ベースのテキスト表現も存在します。
コンパイルするとhello, world
と表示するhello.wat
です。hello.wat(module (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) (memory 1) (export "memory" (memory 0)) (data (i32.const 8) "hello world\n") (func $main (export "_start") (i32.store (i32.const 0) (i32.const 8)) (i32.store (i32.const 4) (i32.const 12)) (call $fd_write (i32.const 1) (i32.const 0) (i32.const 1) (i32.const 20) ) drop ) )wat から wasm へのコンパイルには wabt を使います。
$ git clone --recursive https://github.com/WebAssembly/wabt $ cd wabt $ make $ bin/wat2wasm ./hello.wat $ wasmtime hello.wasm逆にこれを
.wasm
から.wat
にデコンパイルしたりもできます。$ bin/wasm2wat (imputwasmpath) > (output.wat)output.wat(module (type (;0;) (func (param i32 i32 i32 i32) (result i32))) (type (;1;) (func)) (import "wasi_unstable" "fd_write" (func (;0;) (type 0))) (func (;1;) (type 1) i32.const 0 i32.const 8 i32.store i32.const 4 i32.const 12 i32.store i32.const 1 i32.const 0 i32.const 1 i32.const 20 call 0 drop) (memory (;0;) 1) (export "memory" (memory 0)) (export "_start" (func 1)) (data (;0;) (i32.const 8) "hello world\0a"))これを再コンパイルしてみましたが、error になってしまいました。可換性があるのだとしたらすごいと思って試してみたのですが、流石に無理みたいです。
Node.js で実行
node.js では version 8 からデフォルトで WebAssembly オブジェクトを実行できますが、最新版
v13.3.0
系で WASI API が追加されました。これをを使ってでコンパイルしたdemo.wasm
実行します。'use strict'; const fs = require('fs'); const { WASI } = require('wasi'); const wasi = new WASI(); const importObject = { wasi_unstable: wasi.wasiImport }; (async () => { const wasm = await WebAssembly.compile(fs.readFileSync('./demo.wasm')); const instance = await WebAssembly.instantiate(wasm, importObject); wasi.start(instance); })();まだ安定してない機能なので、実行時にオプションをつける必要があります。
$ node --experimental-wasi-unstable-preview0 --experimental-wasm-bigint wasm.js (node:94068) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time Hello, world!実行できました!
所感
今回は WASI のポータビリティーについてを Rust => Wasm => Wasmtime & Node.js と試してみました。まだ Hello world だけですが実行できました。
速度比較や、サンドボックス化された環境でのセキュリティー性能など、他にも手を動かせる余地はありますが、今回は扱いません。
また機会があれば続きをやります。(そもそもすぐやり方が変わりそうな領域ではありますが...)今回手を動かしたことで WASI の未来がますます楽しみになりました。
なんとか何かしらに使えないか野心を燃やしていきたいと思います!それでは以上。
明日は @kurarararara さんになります。