20191214のNode.jsに関する記事は14件です。

Node-RED で黒板機能と操作ノードを実装してみた

Node-RED の右にあるサイドバー領域に表示される黒板と、それを操作するノードを作成してみましたので、ご紹介します。
qiita.gif

コードは 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.html
onpaletteadd: 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" で指定されている以下のページです。
image.png

そして、このページを表示するためのhttpサーバー機能を提供しているのが /tjbot/ui.js ファイル 52行目あたりにある以下のコードです。

/tjbot/ui.js
function 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.jspackage.json ファイルで定義されていないので、自動的には読み込まれません。各ノードの定義部分の先頭で呼び出すことで、初期化が実施されています。

/tjbot/shine.js
module.exports = function (RED) {
  const ui = require("./ui.js")(RED);
  // 以下略
}

操作ノードとの連携

頭の LED を光らせる shine ノードのコード 41行目あたりを見てみます。

/tjbot/shine.js
      switch (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.js
function 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 を参考に進めていきます。

設定ノード

まずは基本となる設定ノードから進めましょう。開発ガイド がとても参考になります。

後でいくらでも拡張できるので、最初は最低限の項目だけ設定できるようにします。

  • ボードの種類 (いわゆる黒板、ホワイトボード、ブラックボードなど)
  • ボードの大きさ (横幅、高さ)

以下が実際に開発した設定画面です。
image.png
わりと基本に忠実で、特別なことはやっていません。短いですし、詳細は実際のコードを見てください。

Output ノード

これが今回の主役ノードで、黒板への出力を全て担っています。
image.png
実はループや条件判断もある、簡易言語として実装されていて、テキストとしてプログラムを与える(payloadに指定する)といろんな操作ができたりします。詳しくは README を参照してください。時間があったら日本語の解説記事を書きますね!

入力を payload で受けるだけなので、設定画面は最低限です。「RTK Board Config」は前の章で説明した黒板全体の設定ノードです。
image.png
実装コードは以下で、dist フォルダに配置した黒板用の html と、そこで利用している描画用の js コードも含みます。

Output ノードの動作例 (基本的な使い方)

Output ノードの使い方は簡単で、Inject ノードを用意し、ペイロードを文字列に設定して、描画用のコマンドを記載すればokです。
image.png

以下はコマンドを記載した 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

image.png
コマンドは ; 文字で繋げることで、複数指定することができます。今回の例ではわかりやすいように 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

image.png
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 10

image.png

Inject (2) をクリックすると、ランダムな位置に、ランダムな色で、小さな四角形が表示されます。何度もクリックすると、上記のようにカラフルな画面になりました。

後で説明のある loop コマンドと組み合わせると、いろいろな表示が楽しめそうです。

Output ノードの動作例 (ステートの演算)

ステートを利用した、もう少し複雑なサンプルを見てみましょう。
image.png
まず (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

image.png
ちょっと複雑になってきました。

ここでは let コマンドを使って、ループ回数をカウントする内部値(変数)である l を定義しています。4回実施したいので、値は 4 をセットしています。

その後の loop コマンドが繰返しを実施する命令で、これは指定された内部値(変数)、今回は l を使って繰り返しを実施します。具体的には l から1を引き、0より大きければその後にある値 -5 を実行カウンタに加える、つまり5つ命令を戻しています。

昔のアセンブリ言語にあったような、原始的な繰り返し命令です。現時点ではまだエラーチェックが甘く、下手するとブラウザがフリーズしかけるので、注意して扱ってください。

ステートの仕組みや、相対的な移動である move コマンドと組み合わせてうまく使えば、記述を大きく減らせる可能性をもっています。まぁ、趣味の機能なのでゆるーく楽しむ程度にしてください。

Face ノード

今回の黒板機能は、Output ノードだけで完結しています。この Face ノードは、その機能を簡単に使うための、サポート用のノードになります。
image.png
次のサンプルをみてください。
image.png
(3) の Face ノードの設定は以下のようになっています。
image.png

また (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)をペイロードで指定するだけで描画できます。
qiita.gif
実際のコードはこちら。

テキストのコマンドを並べるのも楽しいですが、やはりこの Face のようなサポート用のノードを使ったほうが簡単に楽しめますし、Node-RED っぽいですよね。今後も、サポート用のノードを増やしていく予定です。

縦長の顔はいやだ

今回の Face ノードのサンプルを見て「縦長の顔は可愛くない」との指摘をいただきました。表示サイズを % で指定しているので、表示する黒板のサイズにあわせて顔が縦長に表示されています。

とりあえずの解決策なのですが、縦のサイズを 80% という黒板からの相対値ではなく、$_w という内部値を参照としたものに変更してください。``_w'' は横のサイズを示す内部値で、それを指定することで、表示する際に縦横同じ、つまり正方形の領域に顔が表示されるようになります。
image.png
Inject に指定したコマンドのほうも同様に変更しておきます。
image.png

というわけで

とりあえずはサイドバー領域に黒板的な表示パネルを追加し、そこに描画するためのノードを定義できました。ノード開発楽しいです。

まだ実装できそうな描画コマンドはありますし、それらを簡単に扱えるようにサポートノードの数も増やしたいところです。しばらくはコードを修正して、機能を拡張し、年内の npm への公開まで頑張ります!

とりあえずはステータスちゃんと表示して、国際化して日本語追加して、あとは画像表示コマンドの実装とラベル機能の完成、はクリスマスまでには終わらせる予定。などと宣言して自分を追いつめてみる。

それではまた!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Express + Vue.jsでhelloを出してみよう

おはようございますこんにちはこんばんは!

最近Front-endのフレームワークが色々出て流行ってるらしくて、
友達となんかやってみよー!になってはじめました。
最初、どのフレームワークを使おうかと、
'react / vue が人気らしいー何が違う?'とざっと検索してみたら

  • Vue
    • テンプレート形式でアプリの制作したいなら
    • 簡単で「一旦動作」ができるのが好きなら
    • 早くて軽量のアプリが作りたいなら
  • React
    • 大きい規模のアプリを作るなら
    • もっと大きい情報が欲しいなら

こんな差がありました。
簡単なプロジェクトだから、VueにしようーでVueを選びました。
(実は韓国で何もわからずVueの本を買ってきたので、、最初からvueにしようと決めたこともあり、、笑)

back-endはnode.jsのexpress フレームワークを使います(なんでもjsでやってみよう感)

やってみましょう^0^

  1. node.js install
    https://nodejs.org/
    installします。ltsの方が安定的らしくてltsの方インストールしました。

  2. コマンドラインでインストる確認
    $ node -v
    バージョンが出力されたらok

  3. package.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.js
const 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

console
Server is running at: 8000

表示されたら localhost:8000に接続すると

스크린샷 2019-12-14 오후 9.52.52.png

こういう画面が出て、下の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/ 韓国語です (汗)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmパッケージを公開するならnpが便利!

よくある困りごと

npmパッケージを公開・更新する場合、手順を忘れがちですよね(少なくとも私はそうです)。
これから紹介するnpコマンドを使えば、楽にnpmパッケージの公開、更新、さらにはGitHubのtagまで切ってくれる。最高です。

npコマンドの導入

yarn global add np

npを実行すると、バージョンの選択画面が表示され、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 ?

参照

https://zellwk.com/blog/publish-to-npm/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「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」というエラーが出てうまくデプロイできず、結構ハマったので原因と解決策を。


先に結論。

GCloud Functions: load error: File ./dist/index.js that is expected to define function doesn't exist - Stack Overflow

ここに書いてあることがすべて

構成

├── .gitignore
├── README.md
├── dist
│   └── index.ts
├── package-lock.json
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

src配下に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にも辿りつかず、結構苦労しました。

この記事が誰かの役に立つといいなあ。

おわり。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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」というエラーが出てうまくデプロイできず、結構ハマったので原因と解決策を。


先に結論。

GCloud Functions: load error: File ./dist/index.js that is expected to define function doesn't exist - Stack Overflow

ここに書いてあることがすべて

構成

├── .gitignore
├── README.md
├── dist
│   └── index.ts
├── package-lock.json
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

src配下に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にも辿りつかず、結構苦労しました。

この記事が誰かの役に立つといいなあ。

おわり。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

yarn link実行後、cliでコマンドを入力してもpermission deniedになる場合

実行環境

  • node: v12.13.1
  • yarn: 1.19.2

自前で作成したcliツールをローカルで試したい場合、yarn link を使用しますが、Permission deniedで実行できないことがあるようです。

$ yarn link
$ パッケージ名
bash: /usr/local/bin/パッケージ名: Permission denied

実行権限を付与する必要があるとのこと。

$ chmod +x /usr/local/bin/パッケージ名

で実行可能になる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

yarn link実行後、cliでコマンドを入力してもPermission deniedになる場合

実行環境

  • node: v12.13.1
  • yarn: 1.19.2

自前で作成したcliツールをローカルで試したい場合、yarn link を使用しますが、Permission deniedで実行できないことがあるようです。

$ yarn link
$ パッケージ名
bash: /usr/local/bin/パッケージ名: Permission denied

実行権限を付与する必要があるとのこと。

$ chmod +x /usr/local/bin/パッケージ名

で実行可能になる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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>ユーザーと権限からたどって新しい秘密鍵の生成をクリックします。

image.png

ここから得られるjsonを.service-account.jsonという名前で、冒頭のdbスクリプトと同じディレクトリに配置します。このファイルは機密情報を含んでいるので忘れずにgitignoreしときましょう。

あ、dbスクリプトに実行権限を与えることもお忘れなく。

以上

Special Thanks

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者が 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:実機動作確認のため
  • Dialogflow:自然言語処理
  • Vue.js(VueCLI):アプリの画面生成
  • Azure Functions(CLI):内部の処理(クイズ部分とか入出力とか)
  • Vuetify:Vueで使えるフレームワーク
  • IntaractiveCanvas:(重要)Googleアシスタントアプリで画面描写に必要なライブラリ

実際に作ってみる

作るに当たってポイント部分だけを解説していきます。
実際に作ってみたい人は「滑舌チェックスキルをGoogle Nest Hubで実装してみた」をまず確認すると良いと思います。

1.Dialogflow(言語処理をする)

Googleアシスタントによって入力された音声の処理を行います。
Intent(インテント)を作成することで、言葉に反応して何かを返します。

1-1.インテント作成

20191213_056_dialogflow.cloud.google.com.png
作成するIntentは4つ

Intent名 内容 
Default Welcome Intent アプリ起動の言葉
EndIntent アプリ終了の言葉
MainIntent アプリ起動中の言葉
StartIntent アプリ起動後に開始する言葉

1-2.実際にインテントを作る(例:StartIntent)

アプリ起動後にゲームを開始するための言葉を登録します。
20191213_061_dialogflow.cloud.google.com.png

  • 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-tools 

3-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-admin

3-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.js
const 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 install

index.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 start

http://localhost:8080/でアプリが起動します。

Dialogfrowの設定

FulfillmentのwebhookURLを指定する

20191214_062_dialogflow_cloud_google_com.png

テストする

IntegrationからGoogle Assistantを選択
Dialogflow.png

出てきたポップアップで「Auto-preview changes」にチェック入れて「TEST」
Dialogflow2.png

Developから名前(アプリ呼び出しの呼び名)
Develop.png

Deployで「category」を「Games&fun」にする。※InteractiveCanvasに必須
Deploy2.png

InteractiveCanvasをYesにする。
Deploy3.png

Testから実際に動作を確認する。
Androidスマホがある方はそちらからでも確認できます。
Test.png

今回はテストまでなのでデブロイはなしで、ここまでとなります。
ここまで実施すれば実機でテストバージョンとして動作確認もできます。

以上です。
もしかしたら手順漏れなどあるかもしれませんが大体こんな流れです。
興味ある方は是非是非試してください〜

僕も答えられるかわかりませんが、不明点がありましたらお願いします!

宣伝みたいな

【大阪】Google_Nest_Hub対応スマートディスプレイスキルを作ろう!【Vue_js】_-_connpass.png
https://atlabo.connpass.com/event/157824/
2020年の1/23(木)に僕がスマートディスプレイにハマったきっかけのハンズオンが大阪でも開催されます。
興味ある方はおすすめですのでぜひ!※僕もスタッフとして行きます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

linux版 nodeとnpmとprettier

まず確認

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 
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 -g

Prettier

最近はやっているフォーマッター 

ローカル
npm install prettier -D

※しかし、これだけではまだ使えない。そこで次のやり方をします。

とても参考になったパスの通し方

ローカル環境にあるツールをバージョン毎に使い分けるnpmにもってこいのテクニックだと思います。
プロジェクトのあるパスのモジュールを参照する必殺技でもなんでもないなんの変哲のない相対参照(./)を使うやり方です。

export PATH=$PATH:./node_modules/.bin
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

とても気に入ったので、
これ ./ を
今日から執拗に使っていこうかなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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つの項目を追加してください。

.env
RCC_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 4000rcc --port 4000

最後に

簡単な動作確認程度のことしかしていないので、何かありましたらGitの方でコメントお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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(),
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.0

WebAssembly 形式(.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: xxxx
fn 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 さんになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む