- 投稿日:2019-02-26T23:35:07+09:00
[AWS Lambda Tips] 自作ライブラリの保管場所は Lambda Layer じゃなくて GitHub にホストした npm package でも良かった
はじめに
以下の記事で「Lambda の共通モジュールを配置する場所は Lambda Layer くらいしかない」と書いたのですが、Github 上で公開した npm package でも同様の要件が満たせることがわかりました(なんでこんな初歩的なことを見落としてたのだろう。。)。
要件
- 複数のLambda関数から利用できる、共通のライブラリーを1箇所で管理したい
解決方法
- 共通のライブラリーを Lambda Layer にデプロイする
- 共通のライブラリーを npm package として GitHub 上で公開する
共通のライブラリーを Lambda Layer にデプロイする
共通のライブラリーを Lambda Layer にデプロイすれば、他の Lambda Function から参照することができます。
pros
- デプロイ箇所が Lambda Layer 1箇所なので、個々のLambda Function のそれぞれにデプロイするよりも早くデプロイできる。
cons
- 管理するインフラリソースが1つ増えるので、アーキテクチャの複雑さは少し上昇する
- つまり、考えないといけないことが増える
- Lambda Layer にデプロイする資産のビルドやデプロイをどのように行うか?
- デプロイされた資産と手元の資産で差分が生じた場合に確実に新しいリソースがデプロイされるか?
- etc.
共通のライブラリーを npm package として GitHub 上で公開する
GitHub リポジトリーを npm のリポジトリのように使える方法があることを最近知りました。
https://qiita.com/billthelizard/items/b9571a3f276fdf24597cこの方法を使うと、Lambda Layer を使わなくても、ビルドプロセスで npm install するさいに共通のライブラリーを lambda function に含めることができます。
pros
- 管理するインフラ要素が1つ減るので、アーキテクチャの複雑度が減少する
- それによって、考えるべきことが減る
cons
- 個々の Lambda Function に対して同じ資材を配布するので、デプロイに時間がかかる
- デプロイ時間は Lambda Function の数に比例して増えます
- とくに、 dependency として含まれる npm modules のファイル数が多い場合には注意です
まとめ
Lambda Function の共通モジュールの保管場所として、 Lambda Layer は1つの選択肢ですが、
管理するインフラ要素が増えることで考えることも増えます。
少ない数の Lambda Function にデプロイする場合や、 dependency に含まれる資材のファイル数が少ない場合は、 npm package として GitHub リポジトリで配布することも検討に入れましょう。
- 投稿日:2019-02-26T22:57:39+09:00
やはり俺のnodeの理解は間違っている
tl;dr
やっはろー!!ノンエンジニア、ノンプログラマな友人知人がプログラミングを始めるぞい!ってなったときに、
だいたいProgateったりudemyったりドットインストールったりするものの、
そもそもノンプログラマのファーストチャレンジ用に
"こいつらなんなん?"
的なことをまとめてあるドキュメントが(自分の観測範囲に)なかったので、まとめてしまおうと思う。
nodeの話じゃないこともするかもしれない(そもそもCLIとかエディタとかがよくわからないレベルの方向けなので)けど、
なんとなくファーストトライの時に「node?npm?うぉー!!わけわからん!!」って人向けの説明記事になればいいなと思っている。
あとはなんか小中高生とかがプログラミングするときの理解の端緒になればいいかな?的な。そして上述の目的ゆえ、正確性よりも、こういうもんだよ。こう理解しとけばいいよ。的な不正確性を多分に孕む。
エンジニア的には間違った情報断固許さない。となりがちだけど、これを読む対象はもうすでにコード書いてる人じゃなくて、
これからコード書き始める人たちで、一週間もしたらこんな記事いらなくなる人たちなので、
そういう不正確性や雑な書き方、雑な説明も一旦腹落ちさせて先に進むためのものだと割り切って許してやってもらえれば嬉しい。あと、タイトルとか小ネタは某ガガガ文庫のラノベから引っ張ってくる所存なので、
わかる人だけわかってもらえれば嬉しいです。チャプタータイトルはタイトルの元ネタにしてる某ラノベ1巻の各章から取ってきたので内容とは必ずしも合っていない。すまんな。
1.とにかくnode.jsは流行っている
Q. node.jsないしnodeと呼ばれる謎のツール。ソフト。アプリ。なんでもいいけど、アレ。アレって一体なんなの?
A. JavaScript(ECMA Script)って呼ばれるプログラミング言語の実行環境。
Q. よくわかりません。
A. それな。
というわけで、雑にnodeとかJSとかESとかそういう話をしようと思う。
HTML,CSS,JSの最低限をProgateとかでやった人とか、Chromeのコンソールとかで
console.log('hello world!!');
とかした人は、
なんかChrome(またはその他のブラウザ)で、JavaScriptというプログラミング言語が動くっぽいぞ。
というのを体験したと思う。
そこで書いたconsole.log(hello world!!);
がプログラムで、
その文法がJavaScriptというやつ。
ECMA Scriptというのは、JavaScriptの標準規格。例えるなら、
自動車はメーカーが違っても車種が違っても自動車だし、
アクセル踏んだら加速するし、
ブレーキ踏んだら減速するし、
ハンドル切ったら曲がるみたいな自動車ってなんだっけの決まりごとがECMA Scriptっていう規格で、
「ブラウザで動くJavaScriptはこういう風に書いたらこういう風に動くようにしてね。」
っていう決まりごとが定義されている。そのECMA ScriptをもとにGoogleがChrome作ったり、AppleがSafari作ったり、MozillaがFirefox作ったりしている。
トヨタのアクアも日産のノートもホンダのフィットも別の車だけど、ハンドルがついてて、アクセルとブレーキがついてて、アクセルが右、ブレーキが左にあるのは一緒。みたいな感じで、
ECMA Script(のきまりごと)を守って作ってあれば基本的には同じようなJavaScriptが使える。けどアクアとノートとフィットが違うように、各ブラウザによって、必ずしも全部が全部同じではなかったりもするのがJavaScriptなので、
ECMA Scriptの決まりごと以外の部分ではそれぞれ違う機能があったり、
ECMA Scriptの決まりごとの、どのバージョンまで対応しているかとかもブラウザによって違ったりする。そして、ブラウザの話に戻ると、ブラウザはそもそもウェブサイトを見るためのソフト(今時はアプリか?)なので、
JavaScriptを動かすだけじゃなくて、HTMLを表示させたり、それをCSSで装飾したりする機能など、たくさんの機能がついている。そのブラウザのなかでも、GoogleのChromeというブラウザに使われているJavaScriptを動かす部分がV8というソフトで、
そのV8をブラウザから引っこ抜いてきて、ちょこちょこ機能を足すことで、
ChromeというブラウザのJavaScriptを動かす部分だけを取り出したアプリがnode.jsだと理解しておけば、
これからプログラミングを勉強する人たちにとっては十分正確な理解だと思う。
JavaScriptを動かす部分を取り出してきたものなので、できることはJavaScriptを動かすだけなので、
node.js(またはnode)はJavaScriptの実行環境ということになる。node.jsはJavaScriptの実行環境なので、例えば
このHTMLをChromeで開いた時に実行される
index.jsconsole.log('hello world!!');がこんな感じでブラウザで実行されるように
nodeで実行しても同じように"hello world!!"と表示される。
まとめ:node.js(とかnodeって言われてるやつ)は、ブラウザからJavaScriptを動かす機能だけを引っこ抜いてきてたやつ。
2.いつでもnpmは付属している
Q. nodeが"ブラウザからJavaScriptを動かす部分だけを抜き取ってきたやつ"だということを上で説明したけれど、それ入れるとくっついてくるnpmってなんなんですか。
A. node package manager のことです!
Q. nodeはともかく"package manager"ってなんですか。
A. スマートフォンのAppStoreみたいなもんだと思ってくれ!
つまり、npmはnodeで動くアプリのストアみたいなもの。npmからいろんなソフト(アプリ)をインストールしたりアンインストールしたりできる。
と、言い切ってしまうと誤解が爆発的に産まれそうなので、もっと詳しく解説してみる。
- パッケージマネージャーって何?
- npmは何ができるの?
の2点についてざっくり説明するので、npmがなんなのかふわっとわかっていただければと思います!!
パッケージマネージャーって何?
パッケージマネージャーは、各種ソフトウェアのインストール、アンインストール、依存関係管理を行うツール。
依存関係管理?
依存関係とは、ソフトAをインストールする際、ソフトAはソフトBがないと動かない。という時に"AはBに依存している"(Bがないと動かない)という関係。
つまり依存関係管理をしてくれるというのは、
「ソフトAをインストールしてね!」
と言うコマンドを実行した時に、
「AはB,C,Dに依存しているのでB,C,Dもインストールしますね!」
というのを自動でやってくれたり、Aのバージョンアップの時に、
「B,C,DだけじゃなくてEも追加しないと!」とか、「Bもバージョンアップしなきゃ!」
などを勝手に管理して、ダウンロードしてインストールしてくれる仕組みのこと。また、逆の操作に対しても管理を行ってくれるため、Aをアンインストールすると、
一緒にB,C,Dもアンインストールしてくれたり、Bを削除しようとすると、
「AがBに依存しているのでBを削除するとAが動かなくなりますが削除しますか?」と言ったことを聞いてくれるなどして、
ソフトウェア同士の依存関係の管理をパッケージマネージャーがしておいてくれるというもの。なので、例えば
npm install react
などでreactをインストールすると、reactが依存している数多くのパッケージ(ソフト)がnode_modules
というところに自動でインストールされる。もちろん、パッケージマネージャーで管理されるものはいろんなプログラムなので、コマンドラインで実行するコマンドのプログラムをインストールすることもできる。
そのため、
npm install create-react-app
とすると、create-react-app
というコマンドがインストールされて、
create-react-app {appname}
とコマンドを打つと、appname
という名前でreactのアプリの雛形が作られる。というようなこともできる。
この時も、create-react-app
というプログラムを実行するために必要な依存関係にあるソフト群は自動でインストールされて、npmが管理してくれるため、create-react-appを使う時に、create-react-appが依存しているプログラムを意識したり、手動でインストールすることなく利用することができる。npmは何ができるの?
上述のようなパッケージ管理に加えて、npmでは
npm script
というものを書くことで、コマンドを自作したりできる。
自作するコマンドはpackage.json
というファイルに記述することで、npmがpackage.jsonを読み込んで、コマンドを実行できるようにしてくれる。また、
create-react-app
のようなコマンドに関しては、npx
というコマンドを使うことで、コンピューターにインストールすることなくインターネット上にあるコマンドを実行することができる。そのため、
create-react-app
がインストールされていなくても、このようにnpxコマンドを実行すると
こう言った感じで
package.json
が作られ、reactのアプリケーションに必要な依存関係パッケージが
node_modules
に展開されて必要なパッケージと、初期状態の雛形ファイルが自動的に作られる。
この時、
package.json
の"scripts"
以下に"start":"react-scripts start"
というコマンドが作られるので、
ここで、npm start
を実行すると、実際にはnode ./node_modules/react-scripts/bin/react-scripts.js start
が実行される。といった具合に、
$ npm {自分で作ったコマンドの名前}
で$ node {実行するJavaScriptのスクリプト}
が実行されるようにできるというのがパッケージ管理以外のnpmにできることになる。まとめ:npmはnodeで実行できるプログラム各種の依存関係を管理したり、インストールしたりアンインストールしたり、あとpackage.jsonに自作コマンドを書くことでnpmのコマンドを作ったりできるパッケージ管理ツール
3.つねにnodeとnpmは使われている
ここまでで、nodeはJavaScriptを実行する部分をブラウザから分離したものということとnpmはnodeの各種パッケージを管理したりスクリプト作ったりするものということを説明したけれど、
Q.じゃあそれがあるとプログラムを書く時に何が嬉しいの?
という疑問が出てくると思う。
なので、プログラムを作る時にnodeとnpmがあると何が嬉しくて、nodeとnpmが何に使われているのかを簡単に説明しようと思う。
nodeがあると何が嬉しいの?
nodeがあるとできること、それはブラウザの外側でJavaScriptが動かせること。
nodeはブラウザから取り出したJavaScriptの実行環境に、I/O(入出力)の仕組みを追加してあるので、
コンピュータの中にあるファイルを取り込んで、JavaScriptで書いたプログラムを使って処理を行って、
実行結果を出力することができる。今まではブラウザの中でしか動かすことができず、できることが限られていたJavaScriptというプログラミング言語で、
ブラウザ関係なく動作するプログラムが作れることで、
コマンドラインツールや汎用性のあるプログラムなど、必ずしもブラウザというUIやユーザーによる操作を必要としないプログラムが書けて、実行できるようになったのがすごく大きい。例えば、nodeで実行できるプログラムであれば、インターネットに繋がっていなくても、通信を必要としないプログラムが書けたり、
通信を必要とするけど操作を必要としない常駐型のプログラムを作ることができるようになる。node.jsがサーバーサイドJavaScriptと言われるのは、ブラウザ上でのユーザー操作に対して反応するプログラムだけでなく、
サーバー(コンピューター)上で常時実行され続けるプログラムをJavaScriptで書くことができることで、サーバーサイドとクライアントサイドの開発言語を統一することができることが大きなメリットであり、
JavaScriptが基本的にはHTML,CSSと合わせて学ばれる"難しくない"プログラミング言語であるために、ウェブアプリケーション開発において、UI開発の必須項目であるJavaScriptさえある程度できれば、同じ言語でサーバーサイドが開発できることが大きい。npmがあると何が嬉しいの?
パッケージマネージャがあることで依存関係の管理をしなくていい、
独自のコマンドを作れることで長いコマンドを短いコマンドにできる。
というすでにあげたメリットだけでなく、
パッケージ管理システムとインターネット、中央リポジトリを使うことで、他人が今までに作ってきたプログラム資産を自分のソフトウェア開発に使用できるというのがすごく嬉しい。
こういう機能が追加したいな。とか、こういう機能が欲しいな。という時に、0から自分で作ることなく既に誰かが作ってあるものを使えることで、
時間はすごい短縮できるし、ソフトウェアの品質は向上させられるし、自分が作りたいものを作ることに集中できる。
車を作る時に、タイヤとかホイールとか鉄板から自分で全部作っていると大変なので、
タイヤメーカーが作ったタイヤを使う、ホイールメーカーが作ったホイールを使う、鉄鋼メーカーが作った鉄板を使う。
それと同じようにソフトウェアを作る時に(ネットに繋がっていれば)外部から既に他の誰かが作ってあるものを取り込んで使うことができるのが、
エンジニアとしては嬉しいというのがある。まとめ:nodeがあると、サーバーサイドとクライアントサイドのプログラムが両方JavaScriptで書けて、npmがあると自分が過去に作ったプログラムや他人が作ったプログラムを利用できるので新しく作るところに集中できて良い。
あとがき
雑かつ後半とか体力が尽きてだいぶ適当な感じになってしまったが、
nodeってなんだ!npmってなんだ!ウォォオオオーン!!という人たちがこれを読んで少しでもわかった気になってもらえたらいいなと思います。エンジニア向け解説はもっと強くてJS詳しくて強い、つよつよエンジニアが書いたほうが正確性が高まって良いと思うので、
僕は右も左もわかりません!という方々が、どっちが右でどっちが左かを判別できるようになる程度の情報を提供できればなと思います。p.s.
渡航先生、やはり俺の青春ラブコメはまちがっている14巻発売日決定&14巻で本編完結おめでとうございます。
発売日を心待ちにしてます。
あと14巻以降も短、中編集などで八幡や他のキャラ達の姿が楽しめることを期待してます。
- 投稿日:2019-02-26T22:47:41+09:00
Xubuntuでpuppeteerがまともに動かなかったときの対処
ことの起こり
妻(プログラミングに興味はあるけど、特別ITに詳しいわけではない普通の主婦。どちらかというとむしろ理数系は苦手)がどこかで、スクレイピングなる単語を聞いてきたらしく、「puppeteerというソフトを使えば、Webサイトを自動的に定期取得したりということができるらしい」「面白そうだからやってみたい」と言われたことが始めたきっかけ。
それからどした
とりあえず、私がpuppeteerなるものを知らなかったので調べてみました。
- node.js のモジュールで、chromeをプログラム的に操作するものらしい。
- 使いこなせたらテストとか楽そう。
- 自分も興味が出たから、まぁ、暇なときにやってみるか。
という流れで、ひとまず環境を作ってあげることに。
本格的な開発で使うわけではないので、古くてもいい代わりに、プライバシーをとかく気にしがちな妻が自由にできる環境が良いだろうということで、自宅で眠っていた13年前のLet's noteを引っ張り出してみました。
元々、すでに数年前にXubuntuに入れ替えて眠らせていた端末だったのでアップデートかけるくらいで使えたらめっけ物だなぁ〜と思って動かしてみたら、意外と動く。でも、リポジトリが古すぎてインストールできたnodeのバージョンが古く、肝心のpuppeteerが動かない。
ということで、まぁ、折角の機会なのでXubuntuのLTSでもある18.04.2をクリーンインストール。
まっさらな環境を立ち上げました。詰まったこと
あとはドキュメントなり関連情報調べながら適当に入れていけばいいだろう〜と思って、qiitaなりを参考に、
npm i puppeteerと実行してから、サンプルをカキカキ。
node example.jsと実行してみたら、もろにエラー。あれ?
しかもエラーメッセージがnode example.js (node:10017) UnhandledPromiseRejectionWarning: Error: Failed to launch chrome! /home/tezuka/node/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome: 1: /home/tezuka/node/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome: Syntax error: "(" unexpected TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md at onClose (/home/tezuka/node/node_modules/puppeteer/lib/Launcher.js:360:14) at Interface.helper.addEventListener (/home/tezuka/node/node_modules/puppeteer/lib/Launcher.js:349:50) at emitNone (events.js:111:20) at Interface.emit (events.js:208:7) at Interface.close (readline.js:370:8) at Socket.onend (readline.js:149:10) at emitNone (events.js:111:20) at Socket.emit (events.js:208:7) at endReadableNT (_stream_readable.js:1064:12) at _combinedTickCallback (internal/process/next_tick.js:138:11) (node:10017) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1) (node:10017) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.なんて感じでリンクされてるトラブルシューティングのドキュメント読んでも今ひとつわからない。
というより何より、一番最初のエラーメッセージ、明らかにlaunch関数で詰まっているし、エラーメッセージが出るにしてもこれ、構文エラーって明らかにおかしいよね?
当初npmの環境作った場所が日本語パスの下にあったので、そのへんが問題かと思って別のパスはいかに環境作り直してみたりもしたが状況は変わらず。
どうもpuppeteer配下に一緒にセットアップされているchromeが壊れてるんじゃないかなぁ〜ということで、なんとかこの下のchromeを使わずに動かす方法がないか調査。で、どうした。
公式文書を見ると、どうもpuppeteerの他に、ブラウザをバンドルしないpuppeteer-coreというバージョンが提供されている様子。
これを使えばいけそうだが、connectしたりなんだかんだと、ちょっと使い方が複雑になりそう。
私はともかく、本を頼りになんとかしようとしている妻はきっと挫折するだろう。どうにかならないかと思ってもう公式ドキュメントをプラプラ見ていたらこんな一文を見つけた。const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});https://www.npmjs.com/package/puppeteer-core
お?これってもしかして任意の場所のChrome立ち上げられるんじゃ?
ということで、Xubuntuのaptでインストールできた、chromiume-browserを指定する以下の形のexample.jsを作成。const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({executablePath: '/usr/bin/chromium-browser'}); const page = await browser.newPage(); await page.goto('https://www.yahoo.co.jp'); await page.screenshot({path: 'yahoo.png'}); await browser.close(); })();
node example.js
を実行したところ、無事、yahoo.pngの画像が作成されました。ひとまずこれで動くようになりましたので、もし同じようなことでお困りの方がいましたら参考にどうぞ。
結局、なんでバンドルされていたブラウザがエラーになったのかは不明なんですが、どなたかご存じの方がいましたら教えていただけますと幸いです。
- 投稿日:2019-02-26T17:44:31+09:00
socket.io で Web空間に神社を建立
は?
兎にも角にも成果物です。
https://web-shrine.herokuapp.com/はじめに
会社でWebSocket勉強会が開催されたので、試しに遊んでみようということで作成しました。
あまり時間もなく凝ったことはできないので、代わりにネタに走った結果がこれです。使用技術は以下の通りです。
- HTML5
- CSS3(flexbox)
- Node.js(express.js)
- jQuery(Angularで書き直したい)
- socket.io
作成したプロダクトの公開にHerokuを用いています。
コード
フロント側とバック側に分けて説明します。
フロントエンド
今どきjQuery使うのもどうかとは思いましたが、あまり時間がなかったので許してください。
そのうちAngular使って書き直したいです。HTML部は特にこれといって解説する部分はないので、scriptタグ内に絞って解説していきます。
index.htmlconst socket = io(); socket.on('connect', () => { console.log('Server Connected') }) socket.on('receive_money', (obj) => { console.log('現在のお賽銭額', obj.value) $('#money').animate( { opacity: "toggle" }, 0 ); $('#money').text(obj.value.toLocaleString()) $('#money').animate( { opacity: "toggle" }, 200 ); }) socket.on('receive_throw', (obj) => { if ($('#throw-money-list li').length >= 50) { $('#throw-money-list li:last-child').remove(); } $('#throw-money-list').prepend( $('<li>').text(obj.value.toLocaleString() + '円が投げ込まれました!') ) }) socket.on('receive_money_list', (array) => { $('#money-list').empty(); array.reverse(); $.each(array, (index, element) => { $('#money-list').append( $('<li>').text(element.amount.toLocaleString() + '円 奉納'), ) }); }) socket.on('receive_throw_time_by_amount', (obj) => { $('#time_1').text(obj['1']) $('#time_5').text(obj['5']) $('#time_10').text(obj['10']) $('#time_50').text(obj['50']) $('#time_100').text(obj['100']) $('#time_500').text(obj['500']) $('#time_1000').text(obj['1000']) $('#time_5000').text(obj['5000']) $('#time_10000').text(obj['10000']) }) socket.on('receive_wish', (array) => { console.log('お願いを受信', array) $('#wish-list').empty(); $.each(array, (index, element) => { $('#wish-list').prepend( $('<div style="margin: 12px;">').append( $('<dt>').text(element.name + ' さん - ' + new Date(element.time).toLocaleString()), $('<dd class="ema">').append( $('<span class="ema-text">').text(element.wish) ) ) ) }); }) socket.on('receive_worshipers', (num) => { $('#worshipers').text(num); }) function throwMoney(num) { console.log('賽銭', num, '円') socket.emit('throw_money', { value: num }); if ($('#throw-money-list li').length >= 50) { $('#throw-money-list li:last-child').remove(); } $('#throw-money-list').prepend( $('<li>').text(num + '円を投げ込みました!') ) } function sendWish() { console.log('お願いの送信') const name = $('#wish-form [name=name]').val(); const wish = $('#wish-form [name=wish]').val(); console.log(name, wish) if (!wish) { return; } socket.emit('send_wish', { name: name || '名無し', wish: wish, time: new Date() }); }socket.ioはon関数で受信、emit関数で送信時の動作を定義することができます。
両関数は第一引数で名前を設定することができ、フロントエンドとバックエンドで同じ名前のon-emitの対を作ることで、双方向データ通信を実現しています。
receive_money
は現在の合計賽銭額が変更された時に発火するon関数です。
#moneyをアニメーションさせながら書き換えているだけです。
receive_throw
`は賽銭が投げ入れられた時に発火します。
こちらも#throw-money-listが50件までに収まるように追加された額をリストに追加しているだけ。
receive_money_list
はreceive_throw
と一見同じように見えますが、こちらはサーバーが起動してから現在までの奉納の履歴を取得して発火します。throwは「ユーザーがサイトにアクセスした後に、だれかが賽銭を投げ入れたとき」に発火するので、自分がアクセスしたときにはリストは空の状態ですが、こちらはサーバー起動時からの履歴を表示することができます。
receive_throw_time_by_amount
は、上記で用いた方法を応用して、サーバーが起動してから賽銭箱に何回何円を入れたのかを取得して発火します。
receive_wish
もまた同様の方法で、ユーザーが入力したお願いを取得して発火します。
receive_worshipers
は、現在サイトを閲覧しているユーザー数を取得して発火します。
人数を書き換えているだけです。続いてemit関数ですが、全てボタンのクリックイベントに紐づけてあります。
throw_money
は賽銭を投げ込む動作です。賽銭の額を引数にとり、バックエンド側にemit関数として送信しています。
送信履歴をフロント側で書き換えることで、「hoge円を投げ込みました!」と「hoge円が投げ込まれました!」の文書の差を作っています。
これはバックエンド側でemitする対象を選択することが出来る機能によるものです。後述します。
send_wish
は単純にバックエンド側にお願いを送信するものです。バックエンド
app.jsconst express = require('express') const app = express(); const http = require('http').Server(app); const io = require('socket.io')(http); let num = 0; const moneyList = []; const wishList = []; const throwTimeByAmount = { '1': 0, '5': 0, '10': 0, '50': 0, '100': 0, '500': 0, '1000': 0, '5000': 0, '10000': 0 } app.use('/', express.static('public')) io.on('connection', (socket) => { console.log('socket connected', socket.client.conn.server.clientsCount); io.emit('receive_money', {value: num}) io.emit('receive_wish', wishList) io.emit('receive_money_list', moneyList) io.emit('receive_worshipers', socket.client.conn.server.clientsCount) io.emit('receive_throw_time_by_amount', throwTimeByAmount) socket.on('throw_money', (obj) => { console.log('賽銭が投げられました', obj) moneyList.push({amount: obj.value}); num += obj.value; throwTimeByAmount[obj.value]++; io.emit('receive_money', {value: num}) io.emit('receive_money_list', moneyList) io.emit('receive_throw_time_by_amount', throwTimeByAmount) socket.broadcast.emit('receive_throw', {value: obj.value}) }) socket.on('send_wish', (obj) => { console.log('お願いが送信されました', wishList, obj) wishList.push(obj); io.emit('receive_wish', wishList) }) socket.on('disconnect', () => { io.emit('receive_worshipers', socket.client.conn.server.clientsCount) }) }) http.listen(process.env.PORT || 3000, () => console.log('Server Open'))
const io = require('socket.io')(http);
とおまじないをかけることで、ioオブジェクトが使えます。ただ、バックエンド側では最初に
io.on('connection', (socket) => {})
を定義して、接続が確立したことを確認する必要があります。
コールバック関数の中にon-emit関数を書くことで、フロント側と同様に使えます。最初数行のemit関数では、単純にバックエンドで管理されている変数をフロント側に送信しています。
app.jssocket.on('throw_money', (obj) => { console.log('賽銭が投げられました', obj) if (moneyList.length >= 50) { moneyList.shift(); } moneyList.push({ amount: obj.value }); num += obj.value; throwTimeByAmount[obj.value]++; io.emit('receive_money', { value: num }) io.emit('receive_money_list', moneyList) io.emit('receive_throw_time_by_amount', throwTimeByAmount) socket.broadcast.emit('receive_throw', { value: obj.value }) })先ほど「バックエンド側でemitする対象を選択することが出来る機能」があると記しましたが、さっそく
throw_money
で用いています。
単純にsocket.emit()
としてしまうと、自分が送信した賽銭履歴も自分に返ってきてしまい、「投げ入れました」と「投げ込まれました」が同時に出力されてしまうのですが、socket.broadcast.emit
とすることで、自分以外にemitすることができます。
この機能にはほかにもバリエーションがあるので、公式ドキュメントで探してみてください。その他のemit関数は説明するまでもないと思いますので割愛します。
感想
小学生の時に、Java appletで実装されていたチャットでインターネットに触れた私としては、こんなに簡単にリアルタイム双方向通信が実装できるとは思わず、なんとも隔世の感を禁じえません。
socket.io、すっげー(小並感)
参考資料
- 投稿日:2019-02-26T12:35:54+09:00
Paizaのチュートリアル問題をTDDしてみるメモ
概要
Paizaのスキルチェックテストのチュートリアルを、JavaScript(Node.js)とテスト駆動開発(TDD)でやってみたよ、ってメモ書き。
スキルチェックテストの実際の問題を解く上でも、「これで動くかな?」を確認するのは「テストで」やりたいよね、と思ったのでやってみた。
なお、ESLINT+VSCode環境をお勧めする。文法エラーで詰まるとかアホらしいので。 by 詰まった人※スキルチェックテストの設問自体を公開するのはルール違反だが、チュートリアルなら良かろう。ほぼ「標準入出力の扱い方」でしかないし。
取り上げる設問
各問題とも「提出いただいたコードの実行(標準入力による値の取得、計算処理)→標準出力→正誤の判定」
とのことである。
チュートリアルでJavaScript言語を選択すると、次のようにデフォルトコードが出力される。
これは、標準入出力の扱い方のサンプルコード。
https://paiza.jp/challenges/practicetutorial.jsprocess.stdin.resume(); process.stdin.setEncoding('utf8'); // 自分の得意な言語で // スキルチェックの基本となる、標準入力で値を取得し、 // 出力するコードを書いてみよう! var lines = []; var reader = require('readline').createInterface({ input: process.stdin, output: process.stdout }); reader.on('line', (line) => { lines.push(line); }); reader.on('close', () => { console.log(lines[0]); });これを「標準入出力から入力された複数行の文字列のうち、終了時(Ctrl+C押下時)に最初の1行の文字列を標準出力へ出力する」と言う「機能を実装する」と捉えて、話を進める。
今後を見据えて、これをテスト駆動開発してみる。
機能をテストする枠組み
まず、次のような基本クラスを作った。
paiza_stdio.js// var PAIZA_STANDARD_IO = require("paiza_stdio.js").PAIZA_STANDARD_IO; /** * Readlineオブジェクトのイベント"line"と"close"の発火時に * これを呼び出すように実装する。 * 標準入出力に対する機能実装は、このクラスを継承して行う。 * * @param {Object} Consoleオブジェクトのインスタンスを指定。通常は"console"を書けばよい。 */ var PAIZA_STANDARD_IO = function ( consoleInstance ) { this._lines = []; this._consoleInstancce = consoleInstance; }; PAIZA_STANDARD_IO.prototype.onInputLine = function (lineStr) { // ↑呼び出し元のインスタンスをthisに欲しいので、アロー関数は使わない。 this._lines.push( lineStr ); }; PAIZA_STANDARD_IO.prototype.onClose = function () { }; PAIZA_STANDARD_IO.prototype.writeConsole = function (value) { var str = value.toString(); this._consoleInstancce.log( str ); }これを、サンプル―コードに倣って入出力から呼び出す。
index.js/** * [index.js] */ process.stdin.resume(); process.stdin.setEncoding('utf8'); // 自分の得意な言語で // スキルチェックの基本となる、標準入力で値を取得し、 // 出力するコードを書いてみよう! // IO for target environment. var reader = require('readline').createInterface({ input: process.stdin, output: process.stdout }); reader.on('line', (line) => { instance.onInputLine( line ); }); reader.on('close', () => { instance.onClose(); }); // Paizaへの提出時は、忘れずに【以下を】ロード先のコードに置き換えること! var TUTORIAL_SHOW_1ST_LINE = require("./src/01_tutorial.js").TUTORIAL_SHOW_1ST_LINE; var instance = new TUTORIAL_SHOW_1ST_LINE( console );機能の実装、今回のサンプルコードであれば「標準入力の最初の1行目を、終了後に標準出力へ表示」は、
PAIZA_STANDARD_IO
を継承して、例えば次のように行う。なお、現時点では敢えて出力は「期待値に対して未実装」にしてある。01_tutorial.js// Implement var TUTORIAL_SHOW_1ST_LINE = function ( consoleInstance ) { PAIZA_STANDARD_IO.call(this, consoleInstance); }; TUTORIAL_SHOW_1ST_LINE.prototype = Object.create( PAIZA_STANDARD_IO.prototype ); TUTORIAL_SHOW_1ST_LINE.prototype.onClose = function () { this.writeConsole( "終了時に出力する文字列" ); }; exports.TUTORIAL_SHOW_1ST_LINE = TUTORIAL_SHOW_1ST_LINE;作りたい機能を検証(テスト)するコードを作成
続いてテストコード。
Mocha+Chai+Sinon環境が好きなので、次コマンドを叩いてサクッとインストールする。cross-envは「テスト用の環境変数の取り扱いをクロスOSで容易にする」モジュールで、今は使わないかもしれないが後々を考えて入れておく。npm init
は特にこだわりなければ全部デフォルトで「Yes」でOK。npm init npm install --save-dev mocha chai cross-env sinonpackage.jsonの
"script"
を次のように編集する。package.json"scripts": { "start" : "node index.js", "test": "cross-env NODE_ENV=development mocha --recursive" },今回の「標準入出力から入力された複数行の文字列のうち、終了時(Ctrl+C押下時)に最初の1行の文字列を標準出力へ出力する」機能を検証(テスト)するテストコードは、次のように書く。
01_tutorial_test.js/** * [01_tutorial_test.js] * encoding=utf-8 */ var chai = require("chai"); var expect = chai.expect; var sinon = require("sinon"); describe( "01_tutorial.js", function(){ var target = require("../src/01_tutorial.js"); describe("instance.",function () { var instance = new target.TUTORIAL_SHOW_1ST_LINE( console ); it("After input 'hoge', 'fuga' & ctrl+c`, output `hoge`.", function () { var consoleStubLog = sinon.stub( console, "log" ); var INPUT1 = "hoge", INPUT2 = "fuga"; instance.onInputLine(INPUT1); instance.onInputLine(INPUT2); instance.onClose(); consoleStubLog.restore(); expect( consoleStubLog.getCall(0).args[0] ).to.equal( INPUT1 ); }); }); });これを実行すると次のようになる。現時点では、「失敗」が期待値。ここから開始。
npm testqiita_tdd_on_tutorial_of_paiza_scrshot1_failed.png
機能を実装してテストする。
改めて実装する。
01_tutorial.js// Implement var TUTORIAL_SHOW_1ST_LINE = function ( consoleInstance ) { PAIZA_STANDARD_IO.call(this, consoleInstance); }; TUTORIAL_SHOW_1ST_LINE.prototype = Object.create( PAIZA_STANDARD_IO.prototype ); TUTORIAL_SHOW_1ST_LINE.prototype.onClose = function () { this.writeConsole( this._lines[0] ); }; exports.TUTORIAL_SHOW_1ST_LINE = TUTORIAL_SHOW_1ST_LINE;テストを実行すると、成功する。
npm testqiita_tdd_on_tutorial_of_paiza_scrshot2_ok.png
実際の動作も確認してみる。
実際のコマンドプロンプトからの入力をしてみる。
npm start hoge fuga ctrl + cqiita_tdd_on_tutorial_of_paiza_scrshot3_index.png
実際に、Paizaに以下のコードを提出してみる。
「成功」になる。/** * [index.js] */ process.stdin.resume(); process.stdin.setEncoding('utf8'); // 自分の得意な言語で // スキルチェックの基本となる、標準入力で値を取得し、 // 出力するコードを書いてみよう! // IO for target environment. var reader = require('readline').createInterface({ input: process.stdin, output: process.stdout }); reader.on('line', (line) => { instance.onInputLine( line ); }); reader.on('close', () => { instance.onClose(); }); // Paizaへの提出時は、忘れずに【以下を】ロード先のコードに置き換えること! // var TUTORIAL_SHOW_1ST_LINE = require("./src/01_tutorial.js").TUTORIAL_SHOW_1ST_LINE; /** * [01_tutorial.js] */ // var PAIZA_STANDARD_IO = require("paiza_stdio.js").PAIZA_STANDARD_IO; var PAIZA_STANDARD_IO = function ( consoleInstance ) { this._lines = []; this._consoleInstancce = consoleInstance; }; PAIZA_STANDARD_IO.prototype.onInputLine = function (lineStr) { this._lines.push( lineStr ); }; PAIZA_STANDARD_IO.prototype.onClose = function () { }; PAIZA_STANDARD_IO.prototype.writeConsole = function (str) { this._consoleInstancce.log( str ) } // Implement var TUTORIAL_SHOW_1ST_LINE = function ( consoleInstance ) { PAIZA_STANDARD_IO.call(this, consoleInstance); }; TUTORIAL_SHOW_1ST_LINE.prototype = Object.create( PAIZA_STANDARD_IO.prototype ); TUTORIAL_SHOW_1ST_LINE.prototype.onClose = function () { this.writeConsole( this._lines[0] ); }; exports.TUTORIAL_SHOW_1ST_LINE = TUTORIAL_SHOW_1ST_LINE; var instance = new TUTORIAL_SHOW_1ST_LINE( console ); exports.instance = instance;以上ー。
雑感
今回に、次のコードを書いたときに、「あぁ、そうか!Sinon::stub()って、この用途がスタート地点なんだ!」て妙に腑に落ちたの印象的。実際にどうかは知らんが、私にはしっくりきた。
var instance = new target.TUTORIAL_SHOW_1ST_LINE( console ); // (中略) var consoleStubLog = sinon.stub( console, "log" ); // (中略) consoleStubLog.restore();これ、
sinon.stub()
をしないと当然ながら、そのままコンソールにconsole.log()
した値が出力される。でも、上記のようにstub()
ってから実行すると、出力されない。そして、restore()
した以降は、元のように出力されるようになる。あぁ、そういうことなのね。シノンのスタブ、完全に理解した()。
※なお、
sinon.stub()
してると、デバッグ目的でのconsole.log()は使えなくなります、当然ながら。その場合はsinon.spy()
にすれば解決するかな? いや、そもそもconsole.log()じゃなくて、.writeConsole()
をsinon.stub()
するのが適切なのかな?w補足
Googleで「paiza javascript テスト駆動」を検索したけど、javvascriptに関して、これってのはヒットせず。
Ruby向けだと見つかった。
エッ、今さら!?練習問題と具体的コード例によるTDD超入門。
C#版だと、こんなのもあった。
【C#】paizaの煩わしいテスト(入力処理)を自動化したい最初は、ReadLineをフックすることを考えた。
https://nodejs.org/api/readline.html#readline_event_line次に、Readable Streamsをフックするも検討してみた。
https://nodejs.org/api/stream.html#stream_readable_streamsでも、どっちもピンとこなかった。
そこまで考えて、「呼び出しのところだけ切り離せばよいのでは?標準入出力のところを律義にエミュレートする必要は無い」と考え直した。そして書いたコードが上記である。
これでいいのかは分からないが、とりあえずしっくりは来た。
「それは違う」とか「こうした方が良い」とかのコメント歓迎。補足の補足(サンプルで実施例)
コーディングサンプル問題として「入力された整数がグレゴリオ暦でうるう年であるか判定する」があったので、やってみた。
https://paiza.jp/challenges/practice書いたテストコードは以下。実装は省略する。
02_tutorial_practice_test.js/** * [02_tutorial_practice_test.js] * encoding=utf-8 */ var chai = require("chai"); var assert = chai.assert; var expect = chai.expect; var sinon = require("sinon"); describe( "02_tutorial_practice.js", function(){ var target = require("../src/02_tutorial_practice.js"); describe("instance.",function () { var instance = new target.IS_LEAP_YEAR( console ); it("入力された整数がグレゴリオ暦でうるう年であるか判定する", function () { var consoleStubLog = sinon.stub( console, "log" ); // 【コーディングサンプル問題】 // 1行目には、入力される行数Tが入ります。 // 1回のテストケースは、1行に1つずつ整数Nが入っている複数行の標準入力(stdin)による入力になります。 // https://paiza.jp/challenges/practice instance.onInputLine("4"); instance.onInputLine("1000"); instance.onInputLine("1992"); instance.onInputLine("2000"); instance.onInputLine("2001"); instance.onClose(); consoleStubLog.restore(); expect( consoleStubLog.callCount ).to.equal( 4 ); expect( consoleStubLog.getCall(0).args[0] ).to.equal( "1000 is not a leap year" ) expect( consoleStubLog.getCall(1).args[0] ).to.equal( "1992 is a leap year" ) expect( consoleStubLog.getCall(2).args[0] ).to.equal( "2000 is a leap year" ) expect( consoleStubLog.getCall(3).args[0] ).to.equal( "2001 is not a leap year" ) }); }); });実装しながらのユニットテストの「失敗と成功」は次のように進んだ(実装例は省略)。
とりあえず枠だけ実装してテスト。もちろん失敗。
qiita_tdd_on_practice_scrshot1.png
判定無しで、とりあえず文字列出力まで実装。意図通りの失敗。
qiita_tdd_on_practice_scrshot2.png
実装終わって、無事にテストも成功。
ついでなので、実際のコンソール入力からの(システム)テストもやってみた。成功。qiita_tdd_on_practice_scrshot3.png
今度こそ、以上ー。
- 投稿日:2019-02-26T11:18:47+09:00
Slackの絵文字をimport/exportする方法
概要
Slackのカスタム絵文字(emoji)にまつわる問題を解決します。
- ある人が作った絵文字を一括ダウンロードしたい
- 絵文字をまとめてアップロードしたい
- あるワークスペースの絵文字を別の場所でも使いたい
背景
ある人の退職に伴い、絵文字の引継ぎをしたいと思ったのが背景です。
Slackのカスタム絵文字はアカウントと紐づいているため、それを作成したアカウントが退会するとただの文字として表示されるようになってしまいます。そこで絵文字をバックアップできるツールを探していました。しかし、"特定ユーザの作成した絵文字"ではなく、ワークスペースにある絵文字全部を取得してしまうツールがほとんどで、6000個余りのカスタム絵文字が存在する弊社では不要な絵文字が多すぎ、役に立ちませんでした。そこで特定ユーザの作成した絵文字をバックアップできるツールであるemojmeを見つけたので紹介します。
emojme
https://github.com/jackellenberger/emojme
何をするツール?
ユーザ別に絵文字を取得したり、取得した絵文字をアップロードしたりするツールです。
なぜ動くの?
Slackの公開APIである emoji.listではなく、非公開のAPIであるemoji.adminListを叩いて絵文字リストを取得するため、ユーザ別で取得できるようになっています。
したがって動作にはUser tokenが必須になっており、bot用のトークンでは動作しません。動かし方
導入
npm install -g emojme
動作に必要なAPIトークンの取得方法(https://github.com/jackellenberger/emojme#slack-for-web)
${SUBDOMAIN}
にSlackのワークスペースのサブドメイン名が入っているものとします。
1.https://${SUBDOMAIN}.slack.com/messages
にアクセスします。
2. 右クリックして「検証」→上のタブの「Console」→下の文字列を入力しEnter
window.prompt("your api token is: ",/api_token: "(.*)"/.exec(document.body.innerHTML)[1])
3. APIトークンが表示されるので、これをコピーします。ある人が作った絵文字を一括ダウンロードしたい
emojme download --subdomain $SUBDOMAIN --token $TOKEN --save $USER
*$SUBDOMAIN
: Slackのワークスペースのサブドメイン名
*$TOKEN
: APIトークン
*$USER
: ユーザのスクリーンネーム(@〜 ではなく表示される名前)
カレントディレクトリ以下にbuild/$subdomain/$userというディレクトリが作られ、その中に絵文字がダウンロードされます。絵文字をまとめてアップロードしたい
emojme add --subdomain $SUBDOMAIN --token $TOKEN --src $DIR
*$SUBDOMAIN
: Slackのワークスペースのサブドメイン名
*$TOKEN
: APIトークン
* アップロードしたいディレクトリ名これ以上の使い方についてはREADMEを参照してください。
- 投稿日:2019-02-26T08:07:11+09:00
AWS LambdaでPuppeteerを動かす
はじめに
AWS LambdaでPuppeteerを動かすためのメモです。
npm の @serverless-chrome/lambda と chrome-remote-interface を使用した例はいくつかありますが、開発停止の影響のためか、一部のPuppeteer APIが動作しなかったので、chrome-aws-lambda パッケージを使用します。環境
- AWS Lambda
- ランタイム:Node.js 8.10
- ローカル
- macOS Mojave(10.14.3)
- npm 6.8.0
手順
Lambda Layerに登録するパッケージの作成
$ mkdir nodejs && cd !$ $ npm i chrome-aws-lambda puppeteer-core $ cd .. $ zip -r modules.zip nodejsLambda Layer登録
AWSのコンソールからLambdaを開く。
Layer設定画面を開く
Layerの作成
Layerのアップロード
- 名前: 任意のLayer名
- 説明: 任意の説明文
- コードエントリタイプ: .zipファイルをアップロード
- アップロード: 作成したzipファイルを選択
- 互換性のあるランタイム: Node.js 8.10
- 「作成」をクリック
Lambda関数の作成
関数設定画面を開く
関数の作成
- 名前: 任意の関数名
- ランタイム: Node.js 8.10
- ロール: 1つ以上のテンプレートから新しいロールを作成
- ロール名: 任意のロール名
- ポリシーテンプレート: 未選択で可
- 「関数の作成」をクリック
Layerの追加
- ランタイムと互換性のあるレイヤーのリストから選択
- レイヤー: 登録したLayer名を選択
- バージョン: 登録したLayerの任意のバージョンを選択
- 「追加」をクリック
関数コードを作成
node.jsconst chromium = require('chrome-aws-lambda'); const puppeteer = require('puppeteer-core'); exports.handler = async (event, context) => { let result = null; let browser = null; try { browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath, headless: chromium.headless, }); let page = await browser.newPage(); await page.goto(event.url || 'https://yahoo.co.jp/'); result = await page.title(); } catch (error) { return context.fail(error); } finally { if (browser !== null) { await browser.close(); } } return context.succeed(result); };基本設定を調整
- メモリ: 512MB〜1600MB(chrome-aws-lambdaパッケージ推奨値)
- タイムアウト:任意の時間
保存および実行
- イベントテンプレート: Hello World
- イベント名: 任意のイベント名
- 「作成」をクリック
再度、画面右上テストをクリック
実行結果
まとめ
ブラウザ操作を自動化できるPuppeteerをLambdaで動かせると、API Gateway(API化)やCloudWatch Events(定期バッチ処理)など、他のAWSサービスとの連携が容易になります。
ただでさえ便利なPuppeteerがさらに便利になるので、よければ試してみてください。参考