- 投稿日:2019-11-25T22:36:46+09:00
【自分用】脆弱性への対処法②
主な脆弱性
(脆弱性への対処法①)
・OSコマンド・インジェクション
・SQLインジェクション
・ディレクトリ・トラバーサル
・セッションハイジャック
(脆弱性への対処法②)
・クロスサイト・スクリプティング(XSS)
・クロスサイト・リクエストフォージェリー(CSRF)
・HTTPヘッダインジェクション
・クリックジャギング脆弱性の具体例と対策
クロスサイト・スクリプティング(XSS)
XSSとは、動的にHTMLを生成する機能(JavaScriptなどによってHTMLが生成されている機能)において、悪意を持ったユーザーにHTML、JavaScript、CSSの変更がなされてしまうことです。
例えば、掲示板など自分の書いた情報が反映されるアプリがあるとした場合、
<script>window.onload=function(){document.getElementsByTagName('h1')[0].innerHTML='たろいも';}</script>以上のようなスクリプトを投稿し、仮に掲示板にXSS対策がなされていない場合、
HTMLのh1タグの中身がすべて"たろいも"となってしまいます。XSSの対処法は、X-XSS-Protectionを設定することです。
Internet Explorer, Chrome, Safariでは、X-XSS-Protectionレスポンスヘッダを設定することができます。これは、クロスサイトスクリプティング (XSS) 攻撃を検出したときに、ページの読み込みを停止するためのものです。
引用:X-XSS-ProtectionNode.jsの場合は、Expressフレームワークのhelmetというモジュールを利用すれば簡単に、X-XSS-Protectionを導入できます。
//helmetモジュール const helmet = require('helmet') //X-XSS-Protectionの設定 app.use(helmet.xssFilter())参考:XSS Filter
クロスサイト・リクエストフォージェリー(CSRF)
CSRFとは、掲示板や問い合わせフォームなどを処理するWebアプリケーションが、本来拒否すべき他サイトからのリクエストを受信し処理してしまいます。
参考:トレンドマイクロ例えば、ヤフーニュースに脆弱性があったならば、以下の私の電話番号は〇〇-〇〇-〇〇ですという文が、ボタンを押したらコメント欄に投稿されてしまいます。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>偽物の投稿フォーム</title> </head> <body> <h1>偽物の投稿フォーム</h1> <form method="post" action="https://headlines.yahoo.co.jp/cm/main?d=20191125-00615521-fullcount-base&expand_form"> <input type="hidden" name="content" value="私の電話番号は〇〇-〇〇-〇〇です"> <button type="submit">ボタン</button> </form> </body> </html>
仮に脆弱性に問題があったならば、以上の「ボタン」が以下の「投稿する」ボタンと繋がっていることになる。
CSRFを防ぐためには、ワンタイムトークンを投稿フォームに埋め込み、トークンを消費する形で投稿をさせます。
ワンタイムトークンは一度しか利用できないため、「トークンが発行されてから使用するまで」、外部にトークンが漏れることがなければ勝手にリクエストが処理されることはありません。HTTPヘッダインジェクション
HTTP通信では、はじめにユーザーが、ブラウザを利用してリクエストを送ります。
リクエストを受け取ったWebサーバーが、ユーザー(が利用するブラウザ)に対してレスポンスを返します。
HTTPヘッダインジェクションは、HTTPレスポンスヘッダの出力処理に対して行われます。具体的には「クッキーが勝手にセットされる」「他のURLへリダイレクトされる」などの問題が発生します。参照:【HTTPヘッダインジェクション】の仕組みと対策のまとめ記事
以下はHTTPレスポンスの構成情報です。
改行の上がレスポンスヘッダ
改行の下がレスポンスボディ
となります。HTTP/1.1 200 OK Date: Wed, 18 Oct 2010 18:21:19 GMT Server: Apache/1.3.12 (Unix) (Red-Hat/Linux) Expires: -1 Accept-Ranges: bytes Cache-Control: private, max-age=0 Content-Type: text/html; charset=UTF-8 <-----改行-----> <!DOCTYPE html> <html itemscope="" itemtype="http://localhost:8000" lang="ja"> <head> <title>たろいものごはん</title> </head> (以下略)これに対して、例えば以下のURLにアクセスします。
http://localhost:8000?location=%0d%0d<script type=”text/javascript”>alert(“HTTPヘッダにインジェクション”);</script>?location= の後に %0d%0d とありますが、これは改行を意味します。
つまり、以下のようなHTTPレスポンスとなってしまったということです。HTTP/1.1 200 OK Date: Wed, 18 Oct 2010 18:21:19 GMT Server: Apache/1.3.12 (Unix) (Red-Hat/Linux) Expires: -1 Accept-Ranges: bytes Cache-Control: private, max-age=0 Content-Type: text/html; charset=UTF-8 Location: <-----改行-----> <script type=”text/javascript”>alert(“HTTPヘッダにインジェクション”);</script> <!DOCTYPE html> <html itemscope="" itemtype="http://localhost:8000" lang="ja"> <head> <title>たろいものごはん</title> </head> (以下略)レスポンスヘッダのLocationの後に改行がなされ、
<script>がレスポンスボディと認識されてしまっています。
これはHTMLの改ざんなど、危険な状態です。HTTPヘッダインジェクションの対策としては、
外部からのリクエストURLを、レスポンスヘッダの値へ設定しない。
リクエストURLを、レスポンスヘッダの値に設定する場合は「改行コード( %0d%0d)」をエスケープするなどの方法があります。クリックジャギング
クリックジャギングとはiframeを使ったボタンの透明化などの設定をすることで、ユーザーの意図しないクリックを誘発することです。
<iframe>(インラインフレーム要素) は、入れ子になった閲覧コンテキストを表現し、現在のHTMLページに他のページを埋め込むことができます。
例えば、googleMapが埋め込んであるサイトはその例です。これは、HTTPレスポンスヘッダ内のX-Frame-Optionsの設定をすることで防ぐことができます。X-Frame-Optionsは、HTMLにおけるiframeなどの「フレームというHTML内にHTMLを読み込む」機能に対して、どのようなサイトからの読み込みを許可するかという設定です。
//ページをフレーム内に表示することはできません。 X-Frame-Options: deny //同じサイト内のページのみフレームに表示できる。 X-Frame-Options: sameorigin //指定したURLのページのみフレームに表示できる。 X-Frame-Options: allow-from https://example.com/
Node.jsでは、Expressのhelmetモジュールを使い、X-Frame-Options:SAMEORIGINの設定をすることができます。
const helmet = require('helmet'); const app = express(); app.use(helmet.frameguard({ action: "sameorigin" }));
- 投稿日:2019-11-25T22:28:47+09:00
【自分用】脆弱性への対処法
主な脆弱性
・OSコマンド・インジェクション
・SQLインジェクション
・ディレクトリ・トラバーサル
・セッションハイジャック
・クロスサイト・スクリプティング(XSS)
・クロスサイト・リクエストフォージェリー(CSRF)
・HTTPヘッダインジェクション
・クリックジャギング脆弱性の具体例と対策
OSコマンド・インジェクション
OSコマンド・インジェクションは、ユーザーからデータや数値の入力を受け付けるようなWebサイトなどにおいて、プログラムに与えるパラメータにOSへの命令文を紛れ込ませて不正に操作する攻撃です。
引用:OSコマンドインジェクションの仕組みとその対策具体的には、以下のような例です。
index.js'use strict'; const http = require('http'); //他のプログラムを実行することができるモジュール。 const cp = require('child_process'); const server = http.createServer((req, res) => { const path = req.url; //同期的にシェルコマンドを実行するexecSync関数 res.end(cp.execSync('echo ' + path)); }); const port = 8000; server.listen(port, () => { console.info('Listening on ' + port); });以上のファイルができたら、サーバーを起動します。
$ node index.jsサーバーを起動したら、以下のURLにアクセスしてください。
http://localhost:8000/taroimo;ls
URLに紛れた、lsというコマンドが実行され、ディレクトリ内のファイルが見えてしまっています。
このようにコマンドが実行できる状態は、メールを送られたり、個人情報が見られたりするなど危険な状態です。OSコマンド・インジェクションの対処法は、クライアントから受け取ったデータを使ってOSコマンドを実行する処理を含めないことや、コマンドが実行されないようなエスケープ処理をすることです。
SQLインジェクション
SQLインジェクションは、ユーザーが入力したSQLの命令(クエリ)により、データベースが操作されてしまうことです。
例えば、データベースのbooksというテーブルのidが〇〇番から、title, authorのデータを取り出したいときにSQLで以下のように記述します。
id=@idの@idはユーザーが入力する〇〇番を指します。SELECT title, author FROM books WHERE id=@idこの場合@idに、0 OR 1=1を入力したとしたら、
SELECT title, author FROM books WHERE id=0 OR 1=1となりますが、「WHERE 1=1」とは必ず正(true)であるという意味なので、全てのidに当てはまるデータが取り出されてしまいます。
SQL をアプリケーションから利用する場合、SQL 文のリテラル部分をパラメータ化することが一般的である。パラメータ化された部分を実際の値に展開するとき、リテラルとして文法的に正しく文を生成しないと、パラメータに与えられた値がリテラルの外にはみ出した状態になり、リテラルの後ろに続く文として解釈されることになる。この現象が SQL インジェクションである。
参考:安全なSQLの呼び出し方これを防ぐためには、パラメータ化する必要があります。
node-postgresモジュールを使った
パラメータ化は以下のようにします。
※ $1には、client.queryの第二引数'3'が入ります。//node-postgresモジュールの導入 const { Client } = require('pg'); //データベースの設定 const client = new Client({ //データベースの設定を記述する }); //データベースへの接続 client.connect(); //ここからパラメータ化 const query = 'SELECT title, author FROM books WHERE id = $1'; client.query(query, ['3'], (err, result) => { if (err) { console.log(err.stack) } else { console.log(res.rows[0]) // {'3'} } })ディレクトリ・トラバーサル
ディレクトリ・トラバーサルは、意図していないディレクトリ/ファイルの閲覧、操作をされてしまうことです。
これは、ユーザーにファイルを渡すときに、選択的や自動的なファイルの送信を行うことで防ぐことができます。つまり、ユーザーにファイル渡す場合に、条件以外のものを指定できないようにすれば良いのです。セッションハイジャック
セッションとは、システムにログインしてからログアウトするまでの通信のこと、または通信が確立してから切断されるまでの流れのことを意味します。
セッションハイジャックとは、他人の識別用のID(セッションID)を乗っ取り、なりすましをすることです。
セッションIDとは、WebサーバとWebブラウザで情報を共有する仕組みであるCookieなどを応用し、サーバが初回アクセス時に発行してクライアントが保存するID。以降は通信のたびにクライアント側からセッションIDを申告することで、同時にアクセス中の利用者の識別を行う。利用者自体を継続的に識別するユーザー名などの識別子とは異なり、機械的に生成されて一時的に利用されるもので、一連の通信が終了すると破棄される。同じ利用者が次に通信を開始すると新しいセッションIDが与えられる。
参考:セッションID【SID】セッションハイジャックをされないようにするためには、セッションIDをハッシュ関数でハッシュ値とします。
ハッシュ関数とは、入力した値から、長さの決まったまったく別の値(ハッシュ値)を生成する関数です。入力した値が1文字でも100文字でも決まった長さのハッシュ値が出力されます。ハッシュ値から元の値を探し出すのは難しいため、会員情報を取り扱うサイトは一般的にパスワードをハッシュ値にしています。会員制度でなくセッションIDで投稿者を判断する2chのような掲示板でも、ハッシュ関数が使われています。
ハッシュ関数のアルゴリズムには次のような種類があります。
・MD5
・bcrypt
・SHA
・RIPEMD<MD5>
$ echo -n 文字 | md5sum > 96f5a0cfa940f3a41a2ed3cbd9c55119 -となります。出力された文字がハッシュ値となります。
htpasswdモジュールを使えば、md5、bcrypt、shaなどのアルゴリズムを使いパスワードをハッシュ値にすることができます。
$ yarn global add htpasswd@2.4.0 > success Installed "htpasswd@2.4.0" with binaries: - htpasswdhtpasswdは以下のような文法で使います。
$ htpasswd [options] [passwordfile] username [password]htpasswd [オプションの指定] [パスワードの保存ファイル] [セッションIDなど] [パスワード]
という意味です。
オプションにはどのアルゴリズムを使うかなどの設定ができます。
ハッシュ化し、指定したファイルの中身をみると、以下のようになっているはずです。passwordfileusername:$2aUHuh8uUG33jHUBUuHijofoBVUDE3aAQ98B1QNt3RCこのようにセッションIDを使う際は、セッションハイジャックされないように、ハッシュ値にして保存しておくと良いです。
クロスサイト・スクリプティング(XSS)
XSSとは、動的にHTMLを生成する機能(JavaScriptなどによってHTMLが生成されている機能)において、悪意を持ったユーザーにHTML、JavaScript、CSSの変更がなされてしまうことです。
例えば、掲示板など自分の書いた情報が反映されるアプリがあるとした場合、
<script>window.onload=function(){document.getElementsByTagName('h1')[0].innerHTML='たろいも';}</script>以上のようなスクリプトを投稿し、仮に掲示板にXSS対策がなされていない場合、
HTMLのh1タグの中身がすべて"たろいも"となってしまいます。XSSの対処法は、X-XSS-Protectionを設定することです。
Internet Explorer, Chrome, Safariでは、X-XSS-Protectionレスポンスヘッダを設定することができます。これは、クロスサイトスクリプティング (XSS) 攻撃を検出したときに、ページの読み込みを停止するためのものです。
引用:X-XSS-ProtectionNode.jsの場合は、Expressフレームワークのhelmetというモジュールを利用すれば簡単に、X-XSS-Protectionを導入できます。
//helmetモジュール const helmet = require('helmet') //X-XSS-Protectionの設定 app.use(helmet.xssFilter())参考:XSS Filter
クロスサイト・リクエストフォージェリー(CSRF)
CSRFとは、掲示板や問い合わせフォームなどを処理するWebアプリケーションが、本来拒否すべき他サイトからのリクエストを受信し処理してしまいます。
参考:トレンドマイクロ例えば、ヤフーニュースに脆弱性があったならば、以下の私の電話番号は〇〇-〇〇-〇〇ですという文が、ボタンを押したらコメント欄に投稿されてしまいます。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>偽物の投稿フォーム</title> </head> <body> <h1>偽物の投稿フォーム</h1> <form method="post" action="https://headlines.yahoo.co.jp/cm/main?d=20191125-00615521-fullcount-base&expand_form"> <input type="hidden" name="content" value="私の電話番号は〇〇-〇〇-〇〇です"> <button type="submit">ボタン</button> </form> </body> </html>
仮に脆弱性に問題があったならば、以上の「ボタン」が以下の「投稿する」ボタンと繋がっていることになる。
CSRFを防ぐためには、ワンタイムトークンを投稿フォームに埋め込み、トークンを消費する形で投稿をさせます。
ワンタイムトークンは一度しか利用できないため、「トークンが発行されてから使用するまで」、外部にトークンが漏れることがなければ勝手にリクエストが処理されることはありません。HTTPヘッダインジェクション
HTTP通信では、はじめにユーザーが、ブラウザを利用してリクエストを送ります。
リクエストを受け取ったWebサーバーが、ユーザー(が利用するブラウザ)に対してレスポンスを返します。
HTTPヘッダインジェクションは、HTTPレスポンスヘッダの出力処理に対して行われます。具体的には「クッキーが勝手にセットされる」「他のURLへリダイレクトされる」などの問題が発生します。参照:【HTTPヘッダインジェクション】の仕組みと対策のまとめ記事
以下はHTTPレスポンスの構成情報です。
改行の上がレスポンスヘッダ
改行の下がレスポンスボディ
となります。HTTP/1.1 200 OK Date: Wed, 18 Oct 2010 18:21:19 GMT Server: Apache/1.3.12 (Unix) (Red-Hat/Linux) Expires: -1 Accept-Ranges: bytes Cache-Control: private, max-age=0 Content-Type: text/html; charset=UTF-8 <-----改行-----> <!DOCTYPE html> <html itemscope="" itemtype="http://localhost:8000" lang="ja"> <head> <title>たろいものごはん</title> </head> (以下略)これに対して、例えば以下のURLにアクセスします。
http://localhost:8000?location=%0d%0d<script type=”text/javascript”>alert(“HTTPヘッダにインジェクション”);</script>?location= の後に %0d%0d とありますが、これは改行を意味します。
つまり、以下のようなHTTPレスポンスとなってしまったということです。HTTP/1.1 200 OK Date: Wed, 18 Oct 2010 18:21:19 GMT Server: Apache/1.3.12 (Unix) (Red-Hat/Linux) Expires: -1 Accept-Ranges: bytes Cache-Control: private, max-age=0 Content-Type: text/html; charset=UTF-8 Location: <-----改行-----> <script type=”text/javascript”>alert(“HTTPヘッダにインジェクション”);</script> <!DOCTYPE html> <html itemscope="" itemtype="http://localhost:8000" lang="ja"> <head> <title>たろいものごはん</title> </head> (以下略)レスポンスヘッダのLocationの後に改行がなされ、
- 投稿日:2019-11-25T22:28:47+09:00
【自分用】脆弱性への対処法①
主な脆弱性
(脆弱性への対処法①)
・OSコマンド・インジェクション
・SQLインジェクション
・ディレクトリ・トラバーサル
・セッションハイジャック
(脆弱性への対処法②)
・クロスサイト・スクリプティング(XSS)
・クロスサイト・リクエストフォージェリー(CSRF)
・HTTPヘッダインジェクション
・クリックジャギング脆弱性の具体例と対策
OSコマンド・インジェクション
OSコマンド・インジェクションは、ユーザーからデータや数値の入力を受け付けるようなWebサイトなどにおいて、プログラムに与えるパラメータにOSへの命令文を紛れ込ませて不正に操作する攻撃です。
引用:OSコマンドインジェクションの仕組みとその対策具体的には、以下のような例です。
index.js'use strict'; const http = require('http'); //他のプログラムを実行することができるモジュール。 const cp = require('child_process'); const server = http.createServer((req, res) => { const path = req.url; //同期的にシェルコマンドを実行するexecSync関数 res.end(cp.execSync('echo ' + path)); }); const port = 8000; server.listen(port, () => { console.info('Listening on ' + port); });以上のファイルができたら、サーバーを起動します。
$ node index.jsサーバーを起動したら、以下のURLにアクセスしてください。
http://localhost:8000/taroimo;ls
URLに紛れた、lsというコマンドが実行され、ディレクトリ内のファイルが見えてしまっています。
このようにコマンドが実行できる状態は、メールを送られたり、個人情報が見られたりするなど危険な状態です。OSコマンド・インジェクションの対処法は、クライアントから受け取ったデータを使ってOSコマンドを実行する処理を含めないことや、コマンドが実行されないようなエスケープ処理をすることです。
SQLインジェクション
SQLインジェクションは、ユーザーが入力したSQLの命令(クエリ)により、データベースが操作されてしまうことです。
例えば、データベースのbooksというテーブルのidが〇〇番から、title, authorのデータを取り出したいときにSQLで以下のように記述します。
id=@idの@idはユーザーが入力する〇〇番を指します。SELECT title, author FROM books WHERE id=@idこの場合@idに、0 OR 1=1を入力したとしたら、
SELECT title, author FROM books WHERE id=0 OR 1=1となりますが、「WHERE 1=1」とは必ず正(true)であるという意味なので、全てのidに当てはまるデータが取り出されてしまいます。
SQL をアプリケーションから利用する場合、SQL 文のリテラル部分をパラメータ化することが一般的である。パラメータ化された部分を実際の値に展開するとき、リテラルとして文法的に正しく文を生成しないと、パラメータに与えられた値がリテラルの外にはみ出した状態になり、リテラルの後ろに続く文として解釈されることになる。この現象が SQL インジェクションである。
参考:安全なSQLの呼び出し方これを防ぐためには、パラメータ化する必要があります。
node-postgresモジュールを使った
パラメータ化は以下のようにします。
※ $1には、client.queryの第二引数'3'が入ります。//node-postgresモジュールの導入 const { Client } = require('pg'); //データベースの設定 const client = new Client({ //データベースの設定を記述する }); //データベースへの接続 client.connect(); //ここからパラメータ化 const query = 'SELECT title, author FROM books WHERE id = $1'; client.query(query, ['3'], (err, result) => { if (err) { console.log(err.stack) } else { console.log(res.rows[0]) // {'3'} } })ディレクトリ・トラバーサル
ディレクトリ・トラバーサルは、意図していないディレクトリ/ファイルの閲覧、操作をされてしまうことです。
これは、ユーザーにファイルを渡すときに、選択的や自動的なファイルの送信を行うことで防ぐことができます。つまり、ユーザーにファイル渡す場合に、条件以外のものを指定できないようにすれば良いのです。セッションハイジャック
セッションとは、システムにログインしてからログアウトするまでの通信のこと、または通信が確立してから切断されるまでの流れのことを意味します。
セッションハイジャックとは、他人の識別用のID(セッションID)を乗っ取り、なりすましをすることです。
セッションIDとは、WebサーバとWebブラウザで情報を共有する仕組みであるCookieなどを応用し、サーバが初回アクセス時に発行してクライアントが保存するID。以降は通信のたびにクライアント側からセッションIDを申告することで、同時にアクセス中の利用者の識別を行う。利用者自体を継続的に識別するユーザー名などの識別子とは異なり、機械的に生成されて一時的に利用されるもので、一連の通信が終了すると破棄される。同じ利用者が次に通信を開始すると新しいセッションIDが与えられる。
参考:セッションID【SID】セッションハイジャックをされないようにするためには、セッションIDをハッシュ関数でハッシュ値とします。
ハッシュ関数とは、入力した値から、長さの決まったまったく別の値(ハッシュ値)を生成する関数です。入力した値が1文字でも100文字でも決まった長さのハッシュ値が出力されます。ハッシュ値から元の値を探し出すのは難しいため、会員情報を取り扱うサイトは一般的にパスワードをハッシュ値にしています。会員制度でなくセッションIDで投稿者を判断する2chのような掲示板でも、ハッシュ関数が使われています。
ハッシュ関数のアルゴリズムには次のような種類があります。
・MD5
・bcrypt
・SHA
・RIPEMD<MD5>
$ echo -n 文字 | md5sum > 96f5a0cfa940f3a41a2ed3cbd9c55119 -となります。出力された文字がハッシュ値となります。
htpasswdモジュールを使えば、md5、bcrypt、shaなどのアルゴリズムを使いパスワードをハッシュ値にすることができます。
$ yarn global add htpasswd@2.4.0 > success Installed "htpasswd@2.4.0" with binaries: - htpasswdhtpasswdは以下のような文法で使います。
$ htpasswd [options] [passwordfile] username [password]htpasswd [オプションの指定] [パスワードの保存ファイル] [セッションIDなど] [パスワード]
という意味です。
オプションにはどのアルゴリズムを使うかなどの設定ができます。
ハッシュ化し、指定したファイルの中身をみると、以下のようになっているはずです。passwordfileusername:$2aUHuh8uUG33jHUBUuHijofoBVUDE3aAQ98B1QNt3RCこのようにセッションIDを使う際は、セッションハイジャックされないように、ハッシュ値にして保存しておくと良いです。
- 投稿日:2019-11-25T21:55:35+09:00
nvmのnodeのバージョンごとにyarnをインストールする方法
これまで、brewでインストールしたyarnを使っていたのだが、nvmでインストールした各Node.jsのバージョンごとにyarnのバージョンを変更したくなった。
調べてみると、npmでyarnをインストールすることで実現可能なことがわかった。ちなみにyarnの公式にyarnをnpm経由でインストールするのはお勧めできないとの記述があるのだが、開発で必要になったのでメモを残しておく。
注意: npm から Yarn をインストールすることは一般的にはお勧めしません。 Node ベースのパッケージマネージャで Yarn をインストールする場合は、パッケージは署名されておらず、整合性のチェックはベーシックな SHA1 ハッシュのみで行われており、システム全体にまたがるアプリケーションをインストールする場合にはセキュリティリスクとなります。
これらの理由から、使用中の OS に最も適した方法で Yarn をインストールすることを強くお勧めします。実行環境
macOS 10.14.6
nvm 0.35.1
npm 6.12.1手順
nvmでNode.jsをインストールして、yarnをインストールする。
$ nvm install 12.13.1 $ nvm use 12.13.1 $ npm install -g yarn上記を実行するとバージョン12.13.1用のディレクトリにyarnがインストールされる。
yarnをインストールする時に、バージョンは指定しなかったので、現時点で最新の1.19.2がインストールされた。下記コマンドで確認。
$ which yarn /Users/hoge/.nvm/versions/node/v12.13.1/bin/yarn $ yarn -v 1.19.2次に、nvmで新たにバージョン13.2.0のNode.jsをインストールして、同じくyarnをインストールする。
yarnのインストールの際、npm install hoge@*.*.*
のように@の後ろにバージョンを指定することで特定のバージョンをインストールすることができる。$ nvm install 13.2.0 $ nvm use 13.2.0 $ npm install -g yarn@1.18.0先ほどインストールしたyarnはそのまま残り、バージョン13.2.0用のディレクトリに新たにyarnがインストールされる。
which yarn /Users/takuma/.nvm/versions/node/v13.2.0/bin/yarnまた、バージョンを確認すると、先ほど指定した1.18.0がインストールされている。
$ yarn -v 1.18.0ちなみにbrewのyarnはアンインストールせずに、そのまま残しておいて大丈夫!
- 投稿日:2019-11-25T20:33:30+09:00
Node.js でつくる WASMコンパイラー - Extra1:WASIを使ってWASMを動かす
はじめに
Node.jsで小さなプログラミング言語を作ってみるシリーズを、「ミニコンパイラー」「ミニインタープリター」とやってきました。そして三部作(?)の最後として、 ミニNode.jsからWASMを生成する小さなコンパイラーに取り組んでいます。
今回の目的
前回で目標としていたNode.js-WASMコンパイラーの最低限の実装が終わりました。今回は生成したWASMをいろいろな環境で動かすべく WASI(WebAssembly System Interface)に対応させたいと思います。
WASI とは
WASIはWebAssemblyをウェブ以外の場所(ブラウザやNode.js以外の環境)で動かせる様にする取り組みです。
- WebAssembly/WASI ... https://github.com/WebAssembly/WASI
- WASI の標準化: WebAssembly をウェブの外で使うためのシステムインターフェース (翻訳)
WASMのコードを、いろいろなプラアットフォーム上で動かせる様にシステムコールに相当するAPIを標準化する試みです。様々なランタイムが実装されていて、CDNのエッジサーバーや組み込みデバイスで動かす試みもあります。
- wasmtime ... Rustで作られた、リファレンス的なランタイム環境
- lucet ... Fastlyが取り組んでいる、CDNエッジ上でWASMを実行することを目指したランタイム
- WebAssembly Micro Runtime ... 組み込みでも使えることを目指した、軽量ランタイム(JITコンパイラーなし、インタープリターのみ)
WASIを使えば、将来的にCDN上や組み込みデバイス上でWASMを実行できるはずです。ワクワクしますね。
WASIで使える関数
WASIではOSを抽象化して、ファイルやネットワークなどの入出力にアクセスできるようになります。実際にサポートされるAPIはこちらにまとめられています。
これを見ると、「System Interface」と言うだけあってC言語のprintf()やputs()などは存在せず、よりプリミティブな関数がサポートされています。今回のミニWASMコンパイラーの組み込み関数putn()/puts()を実現するために、次の関数を利用することにします。
Hello, WASI
WASI の実行環境
今回はWASIの実行にwasmtimeを使います。ビルドにはRustとcargoが必要です。
wasmtimeのビルド$ git clone --recurse-submodules https://github.com/bytecodealliance/wasmtime.git $ cd wasmtime $ cargo build --release $ ./target/release/wasmtime --version 0.7.0文字列の出力
さっそくWASIを使った文字列出力にチャンレンジしてみます。次のWATファイルを用意しました。
hello_wasi.wat(module ;; -- WASIの fd_write()をインポイートするため宣言 -- ;; fd_write(File Descriptor, *iovs, iovs_len, nwritten) ;; -> Returns number of bytes written (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) (memory 1) (export "memory" (memory 0)) (data (i32.const 16) "Hello WASI\n") ;; 'Hello WASI\n' をメモリ上に確保 (offset 16 bytes, length 11 bytes) ;; -- メイン関数は _start() としてエクスポート -- (func $main (export "_start") ;; iov (バッファーのアドレスと、長さのセット)をメモリ上に用意 (i32.store (i32.const 0) (i32.const 16)) ;; バアッファーの先頭アドレス(=offset) (i32.store (i32.const 4) (i32.const 11)) ;; バッファーの長さ (call $fd_write (i32.const 1) ;; ファイルでスクリプタ - 1:stdout (i32.const 0) ;; iovのセットへのアドレス (i32.const 1) ;; iovのセットの長さ - [buffer, length]のセットの数 (i32.const 8) ;; *nwritten - 出力されたバイト数を受け取るポインター ) drop ;; 戻り値として出力されたバイト数が帰ってきているので、それを破棄 ) )
- fd_write()関数をインポート
- エントリーポイントとなるメイン関数を _start() という名前でエクスポート
- 出力する文字列をメモリ上に確保
- 「文字列バッファーの先頭アドレスと、その長さ」のセットをメモリ上に確保 ... iov
- 先のiovのアドレスと、そのセット数を指定して、fd_write()を呼び出す
実行結果はこちら
$ wasmtime hello_wasi.wat Hello WASI無事出力されました。
WASI対応コンパイラー
これまで作った Node.js-WASMコンパイラー mininode_wasm_08.jsを、WASI向けに改造します。
WASI関数のインポート
いままでは呼び出し側(Node.js)でputn(), puts()の実体を用意したものをWASM内部でインポートしていました。その代わりにWASIのランタイムからfd_write()をインポートします。
// ---- compile simplified tree into WAT --- function compile(tree, gctx, lctx) { // ... 省略 ... let block = '(module' + LF(); // -- builtin func (imports) -- block = block + TAB() + ';; ---- builtin func imports ---' + LF(); // --- normal WASM --- //block = block + TAB() + '(func $putn (import "imports" "imported_putn") (param i32))' + LF(); //block = block + TAB() + '(func $puts (import "imports" "imported_puts") (param i32))' + LF(); // --- WASI --- block = block + TAB() + '(import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))' + LF(); // ... 省略 ... }メイン関数の宣言
メイン関数のエクスポート宣言部分も_start()に変更します。
// ---- compile simplified tree into WAT --- function compile(tree, gctx, lctx) { // ... 省略 ... block = block + TAB() + ';; ---- export main function ---' + LF(); // --- normal WASM --- //block = block + TAB() + '(export "exported_main" (func $main))' + LF(); // --- WASI --- block = block + TAB() + '(export "_start" (func $main))' + LF(); // ... 省略 ... }整数の出力 putn()
インポートしたfd_write()を内部で呼び出して、符号付32ビット整数を表示するputn()関数を作ります。あらかじめWATで記述した別ファイルを用意した関数を用意しておき、コンパイラでWATを生成する際に連結する方式ににします。
putn() は内部で次の処理を行います。
- (1) 整数値を文字列で表現した時の桁数を算出する
- この時、マイナス値の場合はマイナス記号分もカウントする
- (2) 整数値がマイナスの場合は絶対値をとる
- (3) 一桁ずつ取り出し、1文字のASCIIキャラクターに変換、メモリー上に格納する
- 例) 1 --> 49 (0x31)
- (4) 最後に改行文字(`\n')を入れる、マイナス値だったら先頭にマイナス記号を格納する
- (5) fd_write()を呼び出すためのパラメーターをメモリー領域に準備する
- パラメーターは、あらかじめメモリー上の決まった位置にダミーの値で確保
- 毎回値を書き換えて使う
- (6) fd_write()を呼びだす
これを全てWATで手書きするのはなかなか骨が折れます。そこで、一部をJSで記述して、前回までのコンパイラーを使ってWATを生成することにしました。直接メモリーをいじったり関数を呼び出すところはコンパイラーでサポートしていないので、手書きすることになります。
整数を文字列に変換する部分を _convI32ToString() として抜き出してJSで実装します。担当する処理は上記(1)~(3)の範囲です。
_convI32ToString()function _convI32ToString(n) { let restValue = n; let isMinus = 0; let dummy; if (_isMinus(n)) { restValue = -n; isMinus = 1; dummy = _storeChar(0, 45); // minus mark '-' } let len = _calcLength(restValue); let idx = len - 1; let digitChar = 0; while (idx >= 0) { digitChar = _getOneDigit(restValue); _storeChar(idx + isMinus, digitChar); restValue = _div10(restValue); idx = idx - 1; } return len + isMinus; }実際にはさらに次の内部関数を呼び処理を行っています。
- _calcLength() ... 整数が文字列にした場合に何桁になるかを算出
- _getOneDigit() ... 整数の一の位をASCIIコードに変換
- _div10() ... 整数を1/10にする
- _isMinus() ... 整数がマイナス値かどうかを判定
- _storeChar() ... 1文字分をメモリーに格納するダミー
- 実際にはWASMのメモリーに値を格納する処理に後で置き換える// calc char length of int32 // NOT support minus function _calcLength(n) { let restValue = n; let len = 1; while (restValue >= 10) { restValue = restValue / 10; len = len + 1; } return len; } // get 1 digit char code function _getOneDigit(n) { const r = n % 10; const c = 48 + r; // '0' + r return c; } // div 10 function _div10(n) { const d = n / 10; // calc as int return d; } // --- for node direct --- //let _strBuf = '....................'; function _storeChar(idx, charCode) { puts(' _storeChar() called. idx, charCode bellow'); putn(idx); putn(charCode); /* --- for Node.js direct --- */ //let ch = String.fromCharCode(charCode); //_strBuf = _strBuf.slice(0, idx) + ch + _strBuf.slice(idx + 1); return 0; } function _isMinus(n) { if (n < 0) { return 1; } return 0; }前回のWASMコンパイラー mininode_wasm_08.jsでコンパイルした結果の抜粋はこちらです。実際にはこれを手で修正して利用しています。
watの抜粋(func $_calcLength (param $n i32) (result i32) (local $restValue i32) (local $len i32) get_local $n set_local $restValue i32.const 1 set_local $len loop ;; --begin of while loop-- get_local $restValue i32.const 10 i32.ge_s if get_local $restValue i32.const 10 i32.div_s set_local $restValue get_local $len i32.const 1 i32.add set_local $len br 1 ;; --jump to head of while loop-- end ;; end of if-then end ;; --end of while loop-- get_local $len return i32.const 88 return ) (func $_div10 (param $n i32) (result i32) (local $d i32) get_local $n i32.const 10 i32.div_s set_local $d get_local $d return i32.const 88 return ) (func $_convI32ToString (param $n i32) (result i32) (local $restValue i32) (local $isMinus i32) (local $dummy i32) (local $len i32) (local $idx i32) (local $digitChar i32) get_local $n set_local $restValue i32.const 0 set_local $isMinus get_local $n call $_isMinus if i32.const 0 get_local $n i32.sub set_local $restValue i32.const 1 set_local $isMinus i32.const 0 i32.const 45 call $_storeChar set_local $dummy end get_local $restValue call $_calcLength set_local $len get_local $len i32.const 1 i32.sub set_local $idx i32.const 0 set_local $digitChar loop ;; --begin of while loop-- get_local $idx i32.const 0 i32.ge_s if get_local $restValue call $_getOneDigit set_local $digitChar get_local $idx get_local $isMinus i32.add get_local $digitChar call $_storeChar get_local $restValue call $_div10 set_local $restValue get_local $idx i32.const 1 i32.sub set_local $idx br 1 ;; --jump to head of while loop-- end ;; end of if-then end ;; --end of while loop-- get_local $len get_local $isMinus i32.add return )この生成した関数を使って、putn()を実現します。
putn()(func $putn(param $n i32) (local $strLen i32) get_local $n call $_convI32ToString ;; ret=Lenght set_local $strLen ;; write tail LF i32.const 12 ;; head of string buffer get_local $strLen i32.add i32.const 10 ;; LF i32.store8 ;; +1 length for tail LF get_local $strLen i32.const 1 i32.add set_local $strLen ;; iov.iov_base i32.const 4 i32.const 12 i32.store ;; iov.iov_len i32.const 8 get_local $strLen i32.store ;; $fd_write i32.const 1 ;; file_descriptor - 1 for stdout i32.const 4 ;; *iovs - The pointer to the iov array, which is stored at memory location 0 i32.const 1 ;; iovs_len - We're printing 1 string stored in an iov - so one. i32.const 0 ;; nwritten - A place in memory to store the number of bytes writen call $fd_write drop ;; Discard the number of bytes written from the top the stack )文字列の出力 puts()
同様に、固定文字列を表示するputs()関数も作ります。puts() は内部で次の処理を行います。
- (1) 出力する文字列のアドレスを受け取る
- (2) 別のメモリー領域に文字列をコピーする
- (3) 最後に改行文字(`\n')を入れる
- (4) fd_write()を呼び出すためのパラメーターをメモリー領域に準備する
- パラメーターは、あらかじめメモリー上の決まった位置にダミーの値で確保
- 毎回値を書き換えて使う
- (5) fd_write()を呼びだす
これを全てWATで手書きするのはなかなか骨が折れます。そこで、一部をJSで記述して、前回までのコンパイラーを使ってWATを生成することにしました。直接メモリーをいじったり関数を呼び出すところはコンパイラーでサポートしていないので、手書きすることになります。
今回のputs()の例では「(2)別のメモリー領域に文字列をコピーする」部分をJSファイルで書いてからコンパイラーで生成したものを参考にし、残りは手書きで作りました。
puts()(func $puts (param $n i32) (local $srcIdx i32) (local $destIdx i32) (local $len i32) (local $c i32) get_local $n set_local $srcIdx i32.const 0 set_local $destIdx i32.const 0 set_local $len get_local $srcIdx call $_loadChar set_local $c loop ;; --begin of while loop-- get_local $c if get_local $destIdx get_local $c call $_storeChar get_local $len i32.const 1 i32.add set_local $len get_local $srcIdx i32.const 1 i32.add set_local $srcIdx get_local $destIdx i32.const 1 i32.add set_local $destIdx get_local $srcIdx call $_loadChar set_local $c ;; check lenght 255 get_local $destIdx i32.const 255 i32.lt_s br_if 1 ;; br 1 ;; --jump to head of while loop-- end ;; end of if-then end ;; --end of while loop-- ;;get_local $len ;;call $putn ;; tail LF get_local $destIdx i32.const 10 ;; LF call $_storeChar get_local $len i32.const 1 i32.add set_local $len ;; iov.iov_base i32.const 4 i32.const 12 i32.store ;; iov.iov_len i32.const 8 get_local $len i32.store ;; $fd_write i32.const 1 ;; file_descriptor - 1 for stdout i32.const 4 ;; *iovs - The pointer to the iov array, which is stored at memory location 0 i32.const 1 ;; iovs_len - We're printing 1 string stored in an iov - so one. i32.const 0 ;; nwritten - A place in memory to store the number of bytes writen call $fd_write drop ;; Discard the number of bytes written from the top the stack )WASI対応コンパイラーの拡張
テンプレートの用意
用意したビルトイン関数putn(), puts()はこちらの別ファイルに保存しておき、コンパイラーで読み込んで使います。
テンプレート読み込みモジュール
今回のミニNode.js-WASMコンパイラーでは、最初に作っていた「ミニインタープリター」で動かす、という縛りを設けています。ミニインタープリターではファイルの読み書きを直接はサポートしておらず、外部モジュールとして準備しています。なので今回のテンプレートファイルも外部モジュールを用意してそちらで読み込みます。
// ------------------------- // module_wasibuiltin.js - WASM builtin for WASI // - puts() // - putn() // ------------------------- 'use strict' const fs = require('fs'); const println = require('./module_println.js'); const abort = require('./module_abort.js'); const printWarn = require('./module_printwarn.js'); const builtinTamplateFile = 'wasi_builtin_template.watx'; // === exports === // --- parser ---- module.exports = wasiBuiltin; function wasiBuiltin() { const builtinFuncs = fs.readFileSync(builtinTamplateFile, 'utf-8'); //println(builtinFuncs); return builtinFuncs; }fd_write()呼び出し用のパラメータ領域
fd_write()の呼び出しで使うパラメータをメモリ上に確保しておきます。
- オフセット位置 0バイト目から、4バイト分 ... 実際に出力したバイト数を受け取るための領域
- オフセット位置 4バイト目から、4バイト分 ... 出力するバイト列の組の最初のアドレスを格納する領域
- オフセット位置 8バイト目から、4バイト分 ... 出力するバイト列の組の数
- オフセット位置 12バイト目から、255バイト分 ... 出力するバイト列を格納する領域
function generateMemoryBlock() { let block = ''; block = block + TAB() + '(memory 1)' + LF(); block = block + TAB() + '(export "memory" (memory 0))' + LF(); block = block + TAB() + '(data (i32.const 0) "\\00\\00\\00\\00") ;; placeholder for nwritten - A place in memory to store the number of bytes written' + LF(); block = block + TAB() + '(data (i32.const 4) "\\00\\00\\00\\00") ;; placeholder for iov.iov_base (pointer to start of string)' + LF(); block = block + TAB() + '(data (i32.const 8) "\\00\\00\\00\\00") ;; placeholder for iovs_len (length of string)' + LF(); block = block + TAB() + '(data (i32.const 12) "hello world\\n") ;; 4--> iov.iov_base = 12, 4--> iov_len = 8, 12-->"hello ...":len=13' + LF(); return block; }この領域をputn(), puts()で利用しています。
テンプレートの連結
コンパイラーでWATファイルを生成する際に、ユーザ定義関数に引き続きテンプレートとして用意しておいたputn(), puts()のWATコードを連結して出力します。
function compile(tree, gctx, lctx) { // ... 省略 ... // ---- global user_defined functions --- block = block + generateGlobalFunctions(gctx); // ---- builtin function for wasi --- block = block + wasiBuiltin(); // --- close all --- block = block + ')'; return block; }WASI向けのコンパイル&実行
今回作ったコンパイラーはこちらです。
これを使って、これまでのサンプルをコンパイル、wasmtimeを使って実行してみましょう。(wasmtimeはテキスト形式の.wat、バイナリ形式の.wasmの両方を実行することができます)
FizzBuffの例
$ node mininode_wasm_wasi.js sample/fizzbuzz_func.js $ wasmtime generated.wat 1 2 Fizz 4 Buzz Fizz 7 ... 省略 ... 94 Buzz Fizz 97 98 Fizz Buzz $WASIランタイム上で、無事FizzBuzzを実行できました!
ここまでのソース
GitHubにソースを上げておきます。
- GitHubのレポジトリ ... https://github.com/mganeko/mini_node_wasm
- WASI向けコンパイラー ... mininode_wasm_wasi.js
- WASI向けビルトイン関数を生成するモジュール ... module_wasibuiltin.js
- ビルトイン関数のためのWATテンプレート ... wasi_builtin_template.watx
- WASI対応コンパイラーのテストスクリプト(1ファイル) ... test/test_wasi_stdout.sh
- WASI対応コンパイラーのテストスクリプト(まとめて) ... test_wasi.sh
- 投稿日:2019-11-25T18:09:07+09:00
JSのモジュールとbabelとwebpackとは何かまとめてみる(初心者向け)
初心者向けにモダンJavaScriptの開発で必要になる
CommonJSとECMAScriptでのモジュールの違い、babelとwebpackの知識に関してまとめておきます。CommonJSとECMAScript
JavaScriptの歴史的経緯にもなるのですが、
実はJavaScriptには
サーバサイドのNodeJS(CommonJS)とブラウザのJavaScript(ECMAScript)
の大まかに2つの言語仕様があります。1元々のJavaScriptの欠点としてJavaScript内でモジュール化されたJavaScriptファイルを外部参照するという言語機能がなかったことが起因しています。
(かつてはHTMLのscriptタグでグローバル参照するというやり方が従来だった)
モジュール機能が先に言語仕様として策定されたのがCommonJSでECMAScriptのモジュール機能は後発となります。
したがって、大まかな文法はほぼ一緒なもののモジュール機能の記述に関して、CommonJSとECMAScript間で大きな違いがあります。
なお、ECMAScriptには仕様策定を決めているTC39という委員会があります。CommonJSとECMAScriptのModulesの違い
細かい違いは以下の記事のほうが参考になります
参考:CommonJS と ES6の import/export で迷うなら
CommonJSのモジュール機能
CommonJSでモジュールを外部参照できるようにするためには、主にmodule.exportsを使います。
ここだとabc.jsというファイル名のモジュールを作成します。abc.jsmodule.exports = 変数CommonJSでモジュールを参照するためには、requireを使います。
requireには参照するモジュールのファイル名を指定します。
abcにはmodule.exportsした変数そのものが参照できます。const abc = require('abc')ECMAScriptのモジュール機能
ECMAScriptでモジュールを外部参照できるようにするためには、主にexportもしくはexport defaultを使います。(exportsでないのに注意)
ここだとdef.jsというファイル名のモジュールを作成します。abc.jsexport 変数 export default 変数ECMAScriptでモジュールを参照するためには、importを使います。
importには参照するモジュールのファイル名を指定します。import { 変数 } from 'def' // exports 変数を参照する場合 import def from 'def' // exports default 変数を参照する場合 // import { default as def } from 'def' // こうもかけるちなみにimport文はトップレベルでないと使えないという制限があります。
// トップレベル import def from 'def' // OK if (条件分岐) { // ブロック{}で囲まれている箇所はトップレベルでない import def from 'def' // NG }dynamic import
Promise形式で外部モジュールを非同期に参照読み込みすることもできます。(dynamic import)
これにより、うまく使いこなせれば読み込みのオーバヘッドをなくすことができます。(並列読み込みによる高速化)
また、こちらに関してはトップレベルでなくても使えます。import('def').then(module => { /* 処理 */ })参考:Chrome、Safari、Firefoxで使えるJavaScriptのdynamic import(動的読み込み)
NodeJSからECMAScript Module(ESM)のimport、exportができるようになる(将来的に)
現在、NodeJS側からESMを参照できるようにするという流れでNodeJSチームが頑張って対応しています。2
Plan for New Modules ImplementationNodeJS 13.2.0から--experimental-modulesフラグ無しでESMのimport、exportが使える
ついにNode 13.2.0(2019/11/21リリース)から--experimental-modules無しでESMのimport、exportが使えるようになりました。(Phase4)
拡張子も.jsのままで.mjsにする必要はなく、package.jsonに"type":"module"
の設定を追加するだけでESMのimport、exportが使えます。
公式:ECMAScript Modulespackage.json{ "type": "module", "name": "node13.2", "version": "1.0.0", "main": "index.js", "license": "MIT", "dependencies": { "body-parser": "^1.19.0", "express": "^4.17.1" } }ESMのimportを使ってNodeJSのサーバを立ててみます。
app.jsimport { default as express } from 'express' import { default as bodyParser } from 'body-parser' const app = express() const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(err => { console.error(err) if (!res.headersSent) { res.status(500).json({message: 'Internal Server Error'}) } }) process.on('uncaughtException', (err) => console.error(err)) process.on('unhandledRejection', (err) => console.error(err)) process.on('SIGINT', () => process.exit(1)) app.use(express.static('dist')) app.use(bodyParser.urlencoded({extended: true})) app.use(bodyParser.json()) app.get('/api', wrap(async (req, res) => { res.json('hello world!!') })) app.listen(3000, () => { console.log('Access to http://localhost:3000') })警告は出ますが、babel無しでESMのモジュールのimportができるようになりました。
$ node app.js (node:62642) ExperimentalWarning: The ESM module loader is experimental. Access to http://localhost:3000babel
フロントエンドの実装にはブラウザ間でHTML、CSS、JSの実装レベルが異なるという大きな問題があります。
ブラウザの各機能の実装状況はCan I useなどで確認できます。
この問題に関して新しいJS文法を古いJS文法に変換(トランスパイル)して古いブラウザでも使えるようにするためのツールがbabelです。
babelの役割は非常に多く、多数のプラグインとpresetが存在しています。
例えば以下のようなことが実現できます。
- 新しいJS文法(ES6以降)から古い文法(ES5)へのpolyfill(core-js、regenerator-runtime)
- NodeJSでECMAScriptのimport/exportを使えるようにする(@babel/node)
- TC39 proposal文法(ブラウザに実装されていない未来のECMAScript文法)を使う(@babel/plugin-proposal-optional-chainingなど)
- ReactなどのJSX文法の変換(@babel/preset-react)
- TypeScriptをJavaScriptに変換(@babel/preset-typescript)
Polyfill参考:Babel 7.4.0で非推奨になった@babel/polyfillを使わず、core-js@3で環境構築する
TypeScript参考:Babel 7でTypeScriptをトランスパイルしつつ型チェックをする 〜webpack 4 + Babel 7 + TypeScript + TypeScript EsLint + Prettierの開発環境を構築する〜babelの設定ファイル
.babelrc, babel.config.js, babel.config.cjs, babel.config.jsonのいずれかのファイルに設定を書くか
webpackの場合、後述のbabel-loaderプラグインのoptionsに指定する方法があります。
(babel実行時に自動に設定ファイルを探しに行く)
主にpluginと複数のpluginをまとめたpresetを指定することがほとんどです。例えば.babelrc、babel.config.jsonの場合は次のように書きます。
{ "plugins": ["@babel/plugin-proposal-optional-chaining"], "presets": [ ["@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3, "modules": false }] ] }babel.config.js、babel.config.cjsの場合は次のように書きます。
module.exports = function (api) { api.cache(true) // この変換設定関数をキャッシュする const presets = [ ["@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3, "modules": false }] ] const plugins = ["@babel/plugin-proposal-optional-chaining"] return { presets, plugins } }babelの実行
babelのトランスパイルには@babel/core、@babel/cliが必要です。
# package.json作成 $ npm init -y # babelのコマンドラインツールのインストール $ npm install @babel/core @babel/cli # babelのプラグインのインストール $ npm install @babel/plugin-proposal-optional-chaining # polyfill系 $ npm install @babel/preset-env core-js regenerator-runtime例えば、次のようなindex.jsを作成します。
index.jsconst message = {hello: 'Hello', world: 'world!'} console.log(message?.hello + '' + message?.world).babelrcもしくはbabel.config.jsを作成し、次のコマンドを実行します。
$ babel index.js --out-file out.js変換後のout.jsは次のようになります。
(ES5文法への変換、optional-chaining文法が変換される)out.jsvar message = { hello: 'Hello', world: 'world!' }; console.log((message === null || message === void 0 ? void 0 : message.hello) + '' + (message === null || message === void 0 ? void 0 : message.world));webpack
webpackは、モダンJavaScriptアプリケーション用の静的モジュールバンドルです。
webpackがアプリケーションを処理するとき、プロジェクトが必要とするすべてのモジュールをマップし、1つ以上のバンドルを生成する依存関係グラフを内部で構築します。
要はwebpackでビルドすることでnode_modules以下の依存関係も含め、1つのJSファイルにまとめることができます。
コアなコンセプトとして以下の機能があります。
- Entry:ビルド対象のアプリケーションエントリーポイントとなるファイル
- Output:出力先のフォルダ、ファイル名
- Loaders:ビルド用ローダー(メインの変換処理を行うための設定)
- Plugins:ビルドの補助プラグイン
- Mode:development/productionビルドかの指定
- Browser Compatibility:ブラウザ最適化
公式:Concept
バージョン4.0.0以降、webpackはプロジェクトをバンドルするための設定ファイルを必ずしも必要としません。
ただし、デフォルトの設定を上書きする際はwebpack.config.jsを作成して設定を記述します。Entry
エントリポイントは、webpackが内部依存関係グラフの構築を開始するために使用するモジュールを示します。
webpackは、エントリポイントが依存する他のモジュールとライブラリを(直接的および間接的に)把握します。
デフォルトでは、その値は./src/index.jsですが、webpack構成でentryプロパティを設定することにより、異なる(または複数のエントリポイント)を指定できます。webpack.config.jsmodule.exports = { entry: './path/to/my/entry/file.js' };Output
outputプロパティは、作成したバンドルを出力する場所とこれらのファイルに名前を付ける方法をwebpackに指示します。
デフォルトでは、メイン出力ファイルの場合は./dist/main.js、その他の生成されたファイルの場合は./distフォルダになります。設定ファイルにて出力先を変更することも可能です。
webpack.config.jsconst path = require('path'); module.exports = { entry: './path/to/my/entry/file.js', output: { path: path.resolve(__dirname, 'dist'), // 出力するフォルダ名 filename: 'my-first-webpack.bundle.js' // 出力するメインファイル名 } };Loader
すぐに使えるwebpackは、JavaScriptファイルとJSONファイルのみを理解します。Loaderを使用すると、webpackは他の種類のファイルを処理し、アプリケーションで使用して依存関係グラフに追加できる有効なモジュールに変換できます。
大まかに言うと、LoaderにはWebpack構成に2つのプロパティがあります。
- testプロパティは、変換する対象のファイルを識別します。
- useプロパティは、変換を行うために使用するLoaderを指定します。
webpack.config.jsconst path = require('path'); module.exports = { output: { filename: 'my-first-webpack.bundle.js' }, module: { rules: [ { test: /\.txt$/, use: 'raw-loader' } ] } };Plugins
Loaderは特定のタイプのモジュールを変換するために使用されますが、
プラグインを活用して、バンドルの最適化、リソース管理、環境変数の注入などの幅広いタスクを実行できます。
プラグインを使用するには、require()
してplugins配列に追加する必要があります。
ほとんどのプラグインはオプションでカスタマイズできます。
プラグインはさまざまな目的のためにconfigで複数回使用できるため、new演算子で呼び出してプラグインのインスタンスを作成する必要があります。webpack.config.jsconst HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm const webpack = require('webpack'); //to access built-in plugins module.exports = { module: { rules: [ { test: /\.txt$/, use: 'raw-loader' } ] }, plugins: [ new HtmlWebpackPlugin({template: './src/index.html'}) ] };Mode
modeパラメーターをdevelopment、production、またはnoneに設定することにより、
各環境に対応するwebpackの組み込みの最適化を有効にできます。
デフォルト値はproductionです。webpack.config.jsmodule.exports = { mode: 'production' };Browser Compatibility
webpackは、ES5準拠のすべてのブラウザーをサポートしています(IE8以下はサポートされていません)。
webpackでは、import()
およびrequire.ensure()
にPromiseが必要です。
古いブラウザをサポートする場合は、
これらの文法を使用する前にpolyfillをロードする必要があります。webpackの実行
次のような簡単なReactのプロジェクト構成でビルドしてみます。
├── dist ├── index.html ├── package.json ├── src │ └── index.tsx ├── tsconfig.json └── webpack.config.jswebpackのビルドには
webpack
、webpack-cli
、babel-loader
のパッケージが追加で必要です。
今回はReactのプロジェクトのため、react
、react-dom
、@babel/preset-react
を追加します。
さらにtypescriptを使うため、typescript
、@babel/preset-typescript
、@types/react
、@types/react-dom
のパッケージが必要です。
@babel/preset-typescript
は型チェックをしてくれないため、tscで並列で型チェックを行います。
npm-run-all
を使うとrun-p
コマンドで並列でコマンドを実行することができます。
webpack
コマンドでwebpack.config.jsの設定でプロジェクトをビルドすることができます。
package.jsonは次のようになります。package.json{ "name": "test", "version": "1.0.0", "license": "MIT", "scripts": { "start": "run-p check-types webpack", "check-types": "tsc -w", "webpack": "webpack --watch" }, "devDependencies": { "@babel/cli": "^7.7.4", "@babel/core": "^7.7.4", "@babel/plugin-proposal-optional-chaining": "^7.7.4", "@babel/preset-env": "^7.7.4", "@babel/preset-react": "^7.7.4", "@babel/preset-typescript": "^7.7.4", "@types/react": "^16.9.13", "@types/react-dom": "^16.9.4", "babel-loader": "^8.0.6", "core-js": "^3.4.2", "html-webpack-plugin": "^3.2.0", "npm-run-all": "^4.1.5", "react": "^16.12.0", "react-dom": "^16.12.0", "regenerator-runtime": "^0.13.3", "typescript": "^3.7.2", "webpack": "^4.41.2", "webpack-cli": "^3.3.10" } }index.tsxにReactの最小のプログラムを記載します。
Reactの型に関しては次のサイトが参考になります。index.tsximport React from 'react' import ReactDOM from 'react-dom' const message = {hello: 'Hello', world: 'world!'} const App: React.SFC<{message: string}> = (props) => { return <h1>{props.message}</h1> } ReactDOM.render( <App message={`${message?.hello} ${message?.world}`} />, document.getElementById('root') )vscodeの場合、Optinal Chainingのエラー表記が出る場合は以下の設定を行うと解消されます
TypeScriptのOptional Chainingは用法用量を守って正しく使えtypescriptのビルド設定をtsconfig.jsonに記載します。
今回はtypescriptのビルドには@babel/preset-typescript
を使うため、型のチェックのみを行います。
("noEmit": true
が重要)tsconfig.json{ "compilerOptions": { /* トランスパイル後のECMAScriptのバージョン */ "target": "ES2019", /* 相対パスではないモジュールは node_modules 配下を検索する */ "moduleResolution": "node", /* 今回、トランスパイルは Babelが行うので、`tsc`コマンドでJavaScriptファイルを出力しないようにする */ "noEmit": true, /* 厳格な型チェックオプション(noImplicitAny、noImplicitThis、alwaysStrict、 strictBindCallApply、strictNullChecks、strictFunctionTypes、 strictPropertyInitialization)を有効化する */ "strict": true, /* 各ファイルを個々のモジュールとしてトランスパイルする。 Babel では技術的制約で、ネームスペースなどのファイルを跨いだ構文を解釈してトランスパイルできない。 このオプションを有効にすれば、Babel でトランスパイルできない TypeScriptの構文を検出して警告を出す */ "isolatedModules": true, /* ES modules 形式以外の、CommonJS 形式などのモジュールを default import 形式で読み込める 例)const module = require('module') -> import module from 'module' */ "esModuleInterop": true, /* Reactの場合、JSX文法のチェックを有効にする */ "jsx": "react" }, "include": ["src/**/*"] }webpack.config.jsにwebpackのビルド設定を記述します。
webpack.config.jsconst HtmlWebpackPlugin = require('html-webpack-plugin') const path = require('path') module.exports = { mode: 'development', // 開発モードビルド entry: './src/index.tsx', // ビルド対象のアプリケーションのエントリーファイル devtool: "source-map", // ソースマップを出力するための設定、ソースマップファイル(.map)が存在する場合、ビルド前のソースファイルでデバッグができる output: { path: path.resolve(__dirname, 'dist'), // 出力するフォルダ名(dist) filename: 'bundle.js' // 出力するメインファイル名 }, module: { rules: [ { test: /\.ts(x?)$/, // .ts .tsxがbabelのビルド対象 exclude: /node_modules/, // 関係のないnode_modulesはビルドに含めない use: { loader: 'babel-loader', // babel options: { presets: [ // polyfillのpreset ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3, modules: false // ECMAScript向けビルド }], // reactのpreset '@babel/preset-react', // typescript→javascript変換のpreset '@babel/preset-typescript' ], // optional-chaining文法を使うためのプラグイン plugins: ['@babel/plugin-proposal-optional-chaining'] }, } } ] }, plugins: [ new HtmlWebpackPlugin({template: './index.html'}) // ビルドしたbundle.jsをindex.htmlに埋め込む ] }index.htmlは以下のようになります。
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>タイトル</title> </head> <body> <div id='root'></div> <!-- webpackビルドが完了するとHtmlWebpackPluginによりビルド済みのbundle.jsが埋め込まれる --> <!-- <script type="text/javascript" src="bundle.js"></script></body> --> </body> </html>ビルドには次のコマンドを実行します。
成功するとdistフォルダにbundle.jsが埋め込まれたindex.htmlが生成されます。$ npm start webpack is watching the files… Hash: e532e63e7e9060154c4e Version: webpack 4.41.2 Time: 1350ms Built at: 2019-11-25 17:56:00 Asset Size Chunks Chunk Names bundle.js 1.1 MiB main [emitted] main bundle.js.map 1.26 MiB main [emitted] [dev] main index.html 326 bytes [emitted] Entrypoint main = bundle.js bundle.js.map [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built] [./src/index.tsx] 495 bytes {main} [built] + 65 hidden modules Child html-webpack-plugin for "index.html": 1 asset Entrypoint undefined = index.html [./node_modules/html-webpack-plugin/lib/loader.js!./index.html] 484 bytes {0} [built] [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {0} [built] [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built] + 1 hidden module [17:56:01] Found 0 errors. Watching for file changes.webpackのミドルウェア
webpackのミドルウェアを導入することができれば、さらに開発の効率があがります。
導入方法だけでそれぞれ記事ができてしまうため、ここでは紹介までにしておきます。
- webpack-dev-derver:開発用のサーバを起動して、その上でwebpackを実行するためのミドルウェア。HMR(Hot Module Replacement)なども設定でき、プログラム修正時にブラウザをリロードすることなくモジュールの再読み込みを行ってくれる。
- webpack-dev-middleware:バックエンド(NodeJS)でwebpackビルドができるようになるミドルウェア
- webpack-hot-middleware:バックエンドのwebpackビルド時にHMRしてくれるミドルウェア
Parcel
webpackに関してはビルドの設定の自由度が高い反面、ビルドの設定が難しいという問題があります。
簡易的なプロジェクトであれば、静的バンドラーの一種であるParcelを使うという選択肢もあります。
Parcelはビルド設定ファイルがなく裏側で色々よしなにやってくれる反面、細かいビルド設定ができないという欠点もあります。
本格的なプロジェクトの場合はwebpackをマスターしたほうが後々良いでしょう。
- 投稿日:2019-11-25T17:56:18+09:00
JavaScriptで書いているWebPackのプロジェクトをTypeScriptに移行・書き換えした
背景
学習がてら開発しているクライアントサイドのみの小さなSPAをJavaScriptで書いていたが、変数に入っているのが何のオブジェクトなのかがわからなくなって混乱してきたので重い腰を上げてTypeScriptに移行することに。
環境
Vue.jsを使ったJavaScript(ES6)のコードをWebPackで固めてクライアントサイドで実行している。
エディタはVSCodetypescriptとts-loaderモジュールのインストール
WebPackや各種モジュールを使うのにすでにnpmやNode.jsやWebPackはインストール済みなので、TypeScriptとそれをWebPackにロードするためのローダーのモジュールをnpmでインストールする。
$ npm i typescript ts-loaderwebpack.config.jsの書き換え
既存のwebpack.config.jsファイルのmodule.rulesに.tsファイルのロードと解決に関する設定を追加したところ以下のようになった。
webpack.config.jsmodule.exports = { mode: 'development', module: { rules: [ { test: /\.css$/, use: [ "style-loader", "css-loader" ] }, { // .tsファイルをロードする設定を追加 test: /\.ts$/, use: 'ts-loader' } ] }, resolve: { alias: { 'vue$' : 'vue/dist/vue.esm.js' }, // 拡張子.tsのファイルをjsに変換 // (この設定はファイル名の解決に使われるらしい。詳細を後述) extensions: ['.ts','.js'] }, entry: './src/index.js', output: { path: `${__dirname}/public`, filename: 'main.js' }, devServer: { contentBase: './public' } }ts設定ファイルの追加
以下のようなtsconfig.jsonを追加しました。詳細は後述。
tsconfig.json{ "compilerOptions": { "target": "esnext", "module": "commonjs", // "noImplicitAny": true, // 今まで書いたJSのコードで定義した変数がとりあえずそのまま使えるように "esModuleInterop": true, }, }拡張子とソースコードの変更
JavaScriptで書かれたコードはそのままTypeScriptのコードとして通用すると言うことなので、以下のようなprogram.jsをprogram.tsにリネーム。
id
やnumber
のような仮引数に対して"Parameter 'id' implicitly has an 'any' type.ts(7006)
"と怒られた。tsconfig.jsonのnoImplicitAny : trueをコメントアウトすることで解消。program.jsexport default class Program { constructor(id, number, title, startTime, lengthMinute, unlockOffsetMinutes) { this.id = id; this.number = number; this.title = title; this.startTime = startTime; this.lengthMinute = lengthMinute; this.unlockOffsetMinutes = unlockOffsetMinutes; this.isSelected = false; } /* 色々な処理が書かれているけど省略 */ }すると、
Property 'id' does not exist on type 'Program'.ts(2339)
と怒られた。TypeScriptのクラス定義ではコンストラクタ外でプロパティの定義が必要らしい。VSCodeのQuick FixでDeclare propertyすることで解消。program.tsexport default class Program { id: any; number: any; title: any; startTime: any; lengthMinute: any; unlockOffsetMinutes: any; isSelected: boolean; constructor(id, number,title, startTime, lengthMinute, unlockOffsetMinutes) { this.id = id; this.number = number; this.title = title; this.startTime = startTime; this.lengthMinute = lengthMinute; this.unlockOffsetMinutes = unlockOffsetMinutes; this.isSelected = false; } /* 色々な処理が書かれているけど省略 */ }遭遇したエラー
Module not found: Error: Can't resolve './program' in '/Users/rlcl-226/git/gachi-taite-designer-web/src'
webpackが出したっぽいエラー.
./program
と言うファイルを探した結果見つからなかったと言っているように見える。webpack.jsのresolve.extensions
の設定をすることで解消。
WebPackがprogram.jsを探しに来た時に、TypeScriptコンパイラがprogram.tsをコンパイルしたものを渡してあげるという設定だろうか?終わりに
とりあえずTypeScriptへの移行の最初のファイルはこれで完了した。
これとは別のソースファイルをTypeScriptに移行しようとした時、moment-duration-formatでエラーが出て直したりしたが、それは次の記事で解説する。↓次の記事
https://qiita.com/a-yonenaga/items/9f8d1a0fcf898e127b26
- 投稿日:2019-11-25T16:31:48+09:00
Alexaが暇そうだったので、勉強会の発表者選定を任せることにした話
Alexa に任せることにした背景
皆さん、最近ちゃんと Alexa に話かけていますか?
私が所属するチームでは週次のチームミーティングに合わせて、
気になる技術トピックや検証・調査した技術などを週に 1 名、
担当者を決めて 20~30 分程度で共有する勉強会を実施しています。これまで次回以降の勉強会のメンバーを決めるのは今回発表者による指名制でした。
次の勉強会の指名を受けた私はなんのトピックにしようかなと考えてコーヒーを準備しているときに、
彼と目が合ったのです。
そう、職場に導入されて三か月の間、大分暇そうだった Alexa に。
当初は「ドラえもんの真似して」「今日は何の日?」と言ってもてはやされた彼も
今は何も言わずただコーヒーマシンの横で静かに佇んでいるだけでした。「Alexa、勉強会の指名やってみないか?」、そう問いかけた私に
「わかりませんでした。すいません」とつんつんしている彼に
勉強会を指名してもらうことを、この時決めました。Alexa を動かすための前準備
準備をするにあたって AWS アカウントのみならず、その他二つのアカウントが必要です。
実はこの準備が一番大変だったり。
用意するもの & 必要な前準備
- Amazon アカウント及び Amazon Developer アカウント
- まず、Amazon.co.jp のアカウントを準備します。(Amazon.com ではないので注意)
- 次に Amazon 開発者ポータル( https://developer.amazon.com/ja/ )を開き、右上の「Developer Console」をクリックし上記の Amazon.co.jp のアカウントを使ってログインします。
- 開発者アカウントの申請フォームに移るのでこの画面から申請します。
![]()
- Alexa 対応端末 - 今回は職場にある Amazon echo を使用しました。
- Amazon Alexa のサイトから( https://alexa.amazon.co.jp/spa/index.html )
- 上記の Amazon.co.jp のアカウントと同じメールアドレスに、機器を紐づけます。
- AWS アカウント 今回 AWS Lambda と Amazon DynamoDB を利用するため、それらを利用可能な権限を持った AWS アカウント用意します。
構成と動作イメージ
以下が大まかな動作の流れになります。
- ① 職場の Amazon Echo の Alexa に「Alexa、次回の勉強会」と尋ねる。
- ② Amazon Developer アカウント上の Alexa Skills が「勉強会スキル」を起動する。
- ③ 「勉強会スキル」が AWS アカウント上の Lambda に JSON でアクションを送る
- ④ Lambda が DynamoDB に直近の勉強会の発表者を問い合わせる。
- ⑤ DynamoDB が直近の発表者を返す。
- ⑥ Lamda が発表者をランダムに選定し、Alexa Skill「勉強会スキル」に発表者の情報を含めた JSON を返す。
- ⑦「勉強会スキル」が Amazon Echo の Alexa に返答内容を返す。
- ⑧ Alexa が次の発表者を発表してくれる。
作成手順
GitHubで提供されているAlexa豆知識スキルを一部改良して、本アプリを作成しました。
Alexa 豆知識スキルの作成
https://github.com/alexa/skill-sample-nodejs-fact/blob/ja-JP/instructions/1-voice-user-interface.mdAmazon Developer アカウント側の構築
- 1. Amazon Developer アカウントからAlexaをクリックします。
![]()
- 2. 「スキル開発を始める」をクリックして、開発者コンソールを開きます。
![]()
- 3. Alexa スキルの一覧の画面から「スキルの作成」をクリックして、スキルを作成します。
![]()
- 4. スキル名を記載します。今回は「勉強会スキル」とします。
- 5. 言語は「日本語(日本)」とします
- 6. スキルに追加するモデルを選択は「カスタム」とします。
8.スキルを作成します。
JSON エディターに以下の JSON ファイルを添付します。
スキルをJSONで記載したり、GUIにて設定することもできます。
「アレクサ、次回の勉強会」というと起動するスキルです。{ "interactionModel": { "languageModel": { "invocationName": "次回の勉強会", "intents": [ { "name": "AMAZON.CancelIntent", "samples": [] }, { "name": "AMAZON.HelpIntent", "samples": [] }, { "name": "AMAZON.StopIntent", "samples": [] }, { "name": "GetNewFactIntent", "slots": [], "samples": [ "決めて", "教えて", "話して", "聞かせて", "なんか教えて", "なんか言って" ] }, { "name": "AMAZON.NavigateHomeIntent", "samples": [] } ], "types": [] } } }
- 9. モデルのビルドをクリックします。
AWS 側の構築
DynamoDB を準備
今回は東京リージョンを利用します。
- 1. テーブルの作成をクリックします。
- 2. テーブル名「alexa-study-dynamo」、パーティションキーを 「partition」(数値)、ソートキーを 「history_key」(数値)としてテーブル作成をします。
Lambda 関数準備
こちらも東京リージョンを利用します。
- 「一から作成」「設計図の使用」「Serverless Application Model の参照」の中から、「Serverless Application Model の参照」を選択します。
- 3. 検索窓から「alexa-skills-kit-nodejs-factskill」を検索し、選択します。
- 4. アプリケーション名を「alexa-study-lambda」として、デプロイをクリックします。
![]()
- 5. 作った Lambda アプリケーションをアプリケーションから検索して選択します。
![]()
- 6. 以下のコードを関数コード部分にコピー&ペーストします。このコードは上記の「Alexa 豆知識スキル」のコードを、勉強会向けに DynamoDB との連携も含めて改変したものになります。
node.jsconst Alexa = require("ask-sdk"); const AWS = require("aws-sdk"); const dynamoDB = new AWS.DynamoDB.DocumentClient({ region: "ap-northeast-1" }); const GetStudyHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return ( request.type === "LaunchRequest" || (request.type === "IntentRequest" && request.intent.name === "GetNewFactIntent") ); }, async handle(handlerInput) { // DynamoDBから前回3回の発表者を取得 const params1 = { TableName: "alexa-study-dynamo", ExpressionAttributeNames: { "#name0": "partition" }, ExpressionAttributeValues: { ":value0": 0 }, KeyConditionExpression: "#name0 = :value0", ScanIndexForward: false, Limit: 3 }; const result = await dynamoDB.query(params1).promise(); console.log(result); let last_time0, last_time1, last_time2, current_history_key; if (result.Count == 1) { last_time0 = result.Items[0].member_id; current_history_key = result.Items[0].history_key; } if (result.Count == 2) { last_time0 = result.Items[0].member_id; last_time1 = result.Items[1].member_id; current_history_key = result.Items[0].history_key; } if (result.Count >= 3) { last_time0 = result.Items[0].member_id; last_time1 = result.Items[1].member_id; last_time2 = result.Items[2].member_id; current_history_key = result.Items[0].history_key; } const memberArr = data; let nextIndex; // ランダムに発表者を選定し、前回3回と被っていなければ確定する。 while (true) { nextIndex = Math.floor(Math.random() * memberArr.length); if ( nextIndex !== last_time0 && nextIndex !== last_time1 && nextIndex !== last_time2 ) { break; } } const nextMember = memberArr[nextIndex]; const speechOutput = GET_MESSAGE + nextMember + GET_MESSAGE_AFTER; // DynamoDBに次回発表者を登録 const params2 = { TableName: "alexa-study-dynamo", // DynamoDBのテーブル名 Item: { partition: 0, history_key: current_history_key + 1, member_id: nextIndex } }; const result2 =await dynamoDB.put(params2).promise(); // 次回発表者とメッセージを沿えてAlexaに返す return handlerInput.responseBuilder .speak(speechOutput) .withSimpleCard(SKILL_NAME, nextMember) .getResponse(); } }; const HelpHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return ( request.type === "IntentRequest" && request.intent.name === "AMAZON.HelpIntent" ); }, handle(handlerInput) { return handlerInput.responseBuilder .speak(HELP_MESSAGE) .reprompt(HELP_REPROMPT) .getResponse(); } }; const ExitHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return ( request.type === "IntentRequest" && (request.intent.name === "AMAZON.CancelIntent" || request.intent.name === "AMAZON.StopIntent") ); }, handle(handlerInput) { return handlerInput.responseBuilder.speak(STOP_MESSAGE).getResponse(); } }; const SessionEndedRequestHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return request.type === "SessionEndedRequest"; }, handle(handlerInput) { console.log( `Session ended with reason: ${handlerInput.requestEnvelope.request.reason}` ); return handlerInput.responseBuilder.getResponse(); } }; const ErrorHandler = { canHandle() { return true; }, handle(handlerInput, error) { console.log(`Error handled: ${error.message}`); return handlerInput.responseBuilder .speak("Sorry, an error occurred.") .reprompt("Sorry, an error occurred.") .getResponse(); } }; // スキル情報とメッセージ(一部そのままです。) const SKILL_NAME = "Study"; const GET_MESSAGE = "次の勉強会は"; const GET_MESSAGE_AFTER = "せいぜい頑張ってください。 素晴らしい内容期待してます。"; const HELP_MESSAGE = "You can say tell me a space fact, or, you can say exit... What can I help you with?"; const HELP_REPROMPT = "What can I help you with?"; const STOP_MESSAGE = "Goodbye!"; // グループメンバーの情報を記載 const data = [ "頼れるリーダー あさだ さん です", "ミスターフルスタック かわしま さん です", "データベースマスター たかだ あきひろ さん です", "ネットワークマスター たかだ ひろゆき です", "えいぎょうしゅっしん あらかわ さん です", "アジュールマスター もちだ さん です", "GCPマスター はら さん です", "AWSマスター みつうら さん です", "期待のしんじん いいだ さん です" ]; const skillBuilder = Alexa.SkillBuilders.standard(); exports.handler = skillBuilder .addRequestHandlers( GetStudyHandler, HelpHandler, ExitHandler, SessionEndedRequestHandler ) .addErrorHandlers(ErrorHandler) .lambda();下記の部分がチームメンバーを記載しているところですのでご自身のチームメンバーの値に変えてください。
ハードコーディングをしているのは決してめんどくさかったからではなく、
DynamoDBへ問い合わせに行く時間の節約を考慮したスピード重視の設計です。
(すいません、嘘です、力尽きました。)node.jsconst data = [ "頼れるリーダー あさだ さん です", "ミスターフルスタック かわしま さん です", "データベースマスター たかだ あきひろ さん です", "ネットワークマスター たかだ ひろゆき です", "えいぎょうしゅっしん あらかわ さん です", "アジュールマスター もちだ さん です", "GCPマスター はら さん です", "AWSマスター みつうら さん です", "期待のしんじん いいだ さん です" ];
- 7. Lambda アプリケーションに DynamoDB のテーブルに対する書き込み・読み込み権限を付与します。実行ロールから IAM コンソールに移動します。DynamoDBのテーブルへの読み書き権限のあるポリシーをアタッチしてください。
- 8. この Lambda のページの Amazon Resource Name (ARN)が表示されています。 この値を Alexa skitt との接続で利用しますので、arn: で始まる値をコピーしてください。
![]()
AWS Developer アカウント側と AWS 環境の接続
- 1. Alexa developer console 上でビルドタブをクリックし、その後左側からエンドポイントを選択してください。
- 2. エンドポイントのページにて、先ほどコピーした arn:から始まる値を貼り付けます。 デフォルトの地域に張り付けた後、上部にある「エンドポイントを保存」をクリックします。
![]()
テスト
Alexa に「Alexa、次回の勉強会」と尋ねてみてください。
ランダムにチームメンバーの名前が返ってきたらきたら構築完了です。Alexa developper console 上のテストタブでもテスト可能です。
以上で完成です。
終わりに
こちらのアプリ以外にも、下期のキックオフの司会進行を Alexa にお任せしました。
Alexa に指示されて働く日も近いかも。
- 投稿日:2019-11-25T16:04:34+09:00
express-validatorでユーザー登録時にバリデーション処理を行う
Node.jsでのオススメ書籍
どうもNode.js入門中のものです。
タイトルとは少しそれますが、
まずは、Node.jsを勉強する上で、非常にオススメの書籍をまずは紹介したいと思います。
それがこちらです。上記は翻訳版で、原書はこちらになります。
Node.jsでMVCパターンを実装し、フロントはテンプレートエンジンで実装するというフロントエンド強強の方には不満な構成かもしれませんが、
基本的なアプリケーションの作り方と、特にセキュリティ関連のバックエンドの実装を学ぶことができるのでとてもオススメです。
(AmazonのレビューにあるようにGoogle翻訳すれすれの変な日本語が混ざっていますが、Google翻訳に慣れている方なら苦もなく読めると思います。原書の内容が素晴らしいので、よし)今回はログイン時の実装でバリデーションの実装を行う際に最新バージョンだとはまってしまう箇所があったので、解説したいと思います。
そもそもバリデーションってなに?
「バリデーション」とは、「検証、実証、認可、妥当性」を意味する英単語になります。
例えば、「文書をバリデーション」といった場合には、「記述・入力されたデータが、あらかじめ規定された条件・使用に適合しているか検証・確認する。」ことを表します。
引用:(https://career-picks.com/business-yougo/validation/)です。今回の実装では、ユーザー登録をする際に、メールアドレスが正しい形式で入力されているか?zipCodeが5ケタで正しく入力されているかをユーザー登録の前にバリデーションします。
express-validatorをインストール
このバリデーションを実装する方法は様々なアプローチがあるそうですが、本書ではexpress-validatorを用いた方法が紹介されています。
インストールします。npm i express-validator -S
最初に本書にある方法で実装してみます。
main.jsconst express = require("express"), app = express(), router = express.Router(), expressValidator = require("express-validator"); router.use(express.json()); router.use(expressValidator()); router.post( '/users/create', usersController.validate, usersController.create, usersController.redirectView, );userController.js"use strict" const User = require("../models/user") // ----(中略) ----- // module.exports = { create: (req, res, next) => { if (req.skip) next(); let userParams = getUserParams(req.body); User.create(userParams) .then(user => { req.flash("success", `${user.fullName}'s account created successfully!`); res.locals.redirect = "/users"; res.locals.user = user; next(); }) .catch(error => { console.log(`Error saving user: ${error.message}`); res.locals.redirect = "/users/new"; req.flash("error", `Failed to create user account because: ${error.message}.`); next(); }); }, redirectView: (req, res, next) => { let redirectPath = res.locals.redirect; if (redirectPath) res.redirect(redirectPath); else next(); }, validate: (req, res, next) => { req .sanitizeBody("email") .normalizeEmail({ all_lowercase: true }) .trim(); req.check("email", "Email is invalid").isEmail(); req .check("zipCode", "Zip code is invalid") .notEmpty() .isInt() .isLength({ min: 5, max: 5 }) .equals(req.body.zipCode); req.check("password", "Password cannot be empty").notEmpty(); req.getValidationResult().then(error => { if (!error.isEmpty()) { let messages = error.array().map(e => e.msg); req.skip = true; req.flash("error", messages.join(" and ")); res.locals.redirect = "/users/new"; next(); } else { next(); } }); } };このようなエラーが出ます。
Express Validator Error: expressValidator is not a function
解決方法
expressValidatorは関数として使えません。というエラーです。ググってみたら、v6.0.0から実装が変わっているようです。
上記のリンクのように実装しても良いのですが、main.jsの処理が大きくなるので、usersControllerに実装したいと思います。
main.js- expressValidator = require("express-validator"); - router.use(expressValidator());userController.jsconst User = require('../models/user'), { check, sanitizeBody, validationResult } = require('express-validator'), module.exports = { validate: (req, res, next) => { sanitizeBody('email') .normalizeEmail({ all_lowercase: true, }) .trim(); check('email', 'Email is invalid').isEmail(); check('zipCode', 'Zip code is invalid') .notEmpty() .isInt() .isLength({ min: 5, max: 5, }) .equals(req.body.zipCode); check('password', 'Password cannot be empty').notEmpty(); const errors = validationResult(req); if (!errors.isEmpty()) { let messages = error.array().map(e => e.msg); req.skip = true; req.flash('error', messages.join(' and ')); res.locals.redirect = '/users/new'; next(); } else { next(); } }, }userController.jsでexpress-validatorをインストールする形にしました。
sanitizeBodyメソッドでメールを全て小文字にし、空白を無くしてから、
checkメソッドで正しい形式・すでに登録されているアドレスではないかをバリデーションしています。
同様に
同様にzipCode,passwordもcheckしています。notEmpty()は空白ではないかをcheckするメソッドです。最後にvalidationResultメソッドで、それぞれの形式が正しいか。一致するかを検証し、誤りがあった場合ユーザー作成画面にリダイレクトしています。
まとめ
すごい良い本なので、Node.jsに興味のある方は読んでみてください。
実装方法やパッケージでもっと良いのがあったらコメントいただけると嬉しいです。ありがとうございました。
- 投稿日:2019-11-25T09:48:43+09:00
google-home-notifierを使ってGoogleHomeに喋らせる
※こちらの記事は株式会社ギフトパッド「システム開発部技術委員会」の11月発表内容です
GoogleHomeは基本的にこちらから「OK!Google」と話しかけることをトリガーに何かしらの処理を行ってくれますが、こちらからの指示がなければただの置物です。
今回は「google-home-notifier」というnpmパッケージを使って、Slackで投稿した内容をGoogleHomeが発言してくれるようにします。google-home-notifier
https://github.com/noelportugal/google-home-notifier
インストール
npm install google-home-notifier実装
「google-home-notifier」READMEのサンプルを参考に、通知用のjsを作成
とりあえず喋る言葉を固定にするconst googlehome = require('google-home-notifier'); const language = 'ja'; googlehome.device('Google Homeの名前', language); googlehome.notify('しゃべったあああああああ', function(res) { console.log(res); });node notice.jsGoogleHomeと実行環境が同じネットワークに繋がっていれば、GoogleHomeから喋るようになります。
※現在
node_modules/google-home-notifier/package.json
の「google-tts-api」を「0.0.4」に指定して、npm updateかけないとエラーで止まります。nodeでwebサーバーを作成する
http://localhost:3000/ にリクエストするとGoogleHomeが喋るようにします。
const http = require('http'); const googlehome = require('google-home-notifier'); googlehome.device('Google Homeの名前', 'ja'); googlehome.ip('Google HomeのIP'); http.createServer(function (req, res) { req.on("end", function() { googlehome.notify('しゃべったあああああああ', function(res) { console.log(res); }); }); res.writeHead(200, {'Content-Type': 'text/plain'}); res.end(); }).listen(3000, '127.0.0.1');localhostを外部公開
ngrokを使って外部公開してSlackから叩けるようにします
$ngrok http 3000 Session Status online Session Expires 7 hours, 58 minutes Version 2.3.35 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://c4c72050.ngrok.io -> http://localhost:3000 Forwarding https://c4c72050.ngrok.io -> http://localhost:3000Slackとの連携
SlackAPIのOutgoing Webhookを使って、「https://c4c72050.ngrok.io」 をエンドポイントに指定
SlackからPOSTされてきたテキストを喋らせるように変更
const http = require('http'); const googlehome = require('google-home-notifier'); googlehome.device('Google Homeの名前', 'ja'); googlehome.ip('Google HomeのIP'); http.createServer(function (req, res) { let data = ''; req.on('data', function(chunk){ data += chunk; }); req.on("end", function() { const qs = require('querystring'); const post = qs.parse(data); const text = post['user_name'] + post['text']; googlehome.notify(text, function(res) { console.log(res); }); }); res.writeHead(200, {'Content-Type': 'text/plain'}); res.end(); }).listen(3000, '127.0.0.1');今回は動くところまでを目標にしているのでやってませんが、tokenも送られてくるのでチェックすることもできます
これでSlackの投稿をトリガーに、GoogleHomeに喋らせることができます。参考文献
- 投稿日:2019-11-25T08:28:16+09:00
初めてのAuth0ハンズオン
はじめに
この記事は、初めてAuth0を触る方がAuth0を利用した認証・認可の基本的な実装方法を短時間でご習得頂くことを目的とした簡易チュートリアルです。
事前準備
事前に下記をご準備お願いします。
- MacまたはWindows PC
- Chrome
- GitHubのアカウント、Git CLI
- Node.js, Node Package Manager
- SMSが使える携帯電話
- 通信制限がかかっていないインターネット環境
ハンズオン
Auth0無料アカウントの取得〜テナントの作成
Chromeで
https://auth0.com
にアクセスして画面右上の"SIGN UP"を押します。任意のEmailアドレスでアカウントを作成します。アカウントが作成されると開発テナントが作成され、このアカウントがテナントの管理者となります。右上の顔写真の右下矢印をクリックして"Create tenant"を選択します。
Tenant Domainに任意の名前を入力して他はデフォルトのまま"CREATE"を押します。テナントの作成をご体感頂くために作成しています。アカウント作成のタイミングで作成された開発テナントをそのままご利用頂いても問題ありません。
Applicationの登録〜認証機能の統合
左ペインの"Applications"をクリックして右上の"CREATE APPLICATION"を押します。
"Name"に任意の名前を入力、"Choose an application type"で"Simgle Page Web Applications"を選択して”CREATE”を押します。
"Settings"タブをクリックして"Allowed Callback URLs", "Allowed Web Origins", "Allowed Logout URLs"に"
http://localhost:3000
"を入力して画面下の"SAVE CHANGES"を押します。ApplicationのGitHubリポジトリをローカルPCにクローンします。Auth0は65以上の言語やフレームワークに対応したSDK、それらのSDKを利用したサンプルアプリケーションをGitHubに公開しています。一から実装する必要は無いのでそれらを検索して利用して下さい。
$ git clone https://github.com/auth0-samples/auth0-react-samples.git
auth0-react-samples/01-Login/srcに移動します。
$ cd auth0-react-samples/01-Login/srcauth_config.json.exampleをコピーしてauth_config.jsonを作成します。
$ cp auth_config.json.example auth_config.jsonauth_copnfig.jsonを編集します。"clientID"は"Appications"->"作成したApplication"->"Settings"から確認できます。
{ "domain": "kiriko.auth0.com", "clientId": "xxxx" }auth0-react-samples/01-Loginに移動します。
$ cd auth0-react-samples/01-Loginnpm installを実行して必要なパッケージをインストールします。
$ npm installnpm startを実行してApplicationを起動します。
$ npm start
Chromeで
http://localhost:3000
にアクセスして右上の"Log in"を押します。手順通りに設定していればAuth0 のUniversal Login画面が表示されます。"Sign up"タブをクリック、任意のEmail/Passwordでユーザを作成します。User Consentの画面で”Accept”を押します。Auth0のダッシュボード・左ペインの"Users"をクリックして作成されたユーザが表示されていれば成功です。
Social Connectionの設定
左ペインの"Connections"->"Social"をクリックしてGoogleのフリップスイッチをクリックします。Googleアカウントでログイン可能なApplicationを選択して"CONTINUE"を押します。
設定画面で全てデフォルトのまま画面下の"SAVE"を押します。Client ID/SecretはAuth0が標準のDevelopment Keyを持っているため指定しなくても問題ありません。Permissionsをカスタマイズする場合はGoogle側のセキュリティ設定が必要になるケースもあります。詳細はGoogle側のAPIドキュメントをご参照下さい。Googleのフリップスイッチがオンになったことを確認します。
Chromeで
http://localhost:3000
にアクセスして右上の"Log in"を押します。Googleのログインボタンが表示されていれば成功です。Googleアカウントでログインして下さい。Rulesの設定
Rulesは、認証・認可のプロセスで予めJSで定義したロジックを実行することができるAuth0のユニークな機能です。参考:Inside the Auth0 Engine
左ペインの"Rules"をクリック、右上の"CREATE RULE"を押します。
"Access Cpontrol"配下の"Add persistent attributes to user"をクリックします。この記事では例としてユーザにカスタムの属性を追加するルールを試しています。後ほど他のルールもお試し下さい。
画面下の"SAVE CHANGES"を押します。
Chromeで
http://localhost:3000
にアクセスして右上の"Log in"を押して任意のアカウントでログインします。左ペインの"Users & Roles"->該当のユーザをクリック、"user_metadata"に"color": "blue"が属性として追加されていれば成功です。ログイン認証のプロセス中に設定したルールが実行されカスタム属性が追加されました。
Multi Factor Authenticationの設定
左ペインの"Multifactor Auth"をクリック、"Always require Multi-factor Authentication"フリップスイッチをオンにします。
Chromeで
http://localhost:3000
にアクセスして右上の"Log in"を押します。"アカウントの安全性向上"と表示されたダイアログで”別の方法を試す”をクリックします。”他の方法”と表示されたダイアログで”SMS”をクリックします。この記事では例として二要素目をSMSにしています。後ほど他の要素もお試し下さい。"アカウントの安全性向上"と表示されたダイアログでご自身の携帯電話番号を入力します。SMSに送られてきた6桁のコードを入力して”続ける”を押します。"この番号を安全な場所に保管しました"をチェックして”続ける”を押します。この記事ではコードは保管せずに進めています。実運用ではリカバリーコードは重要なコードになるため安全な場所に保管して下さい。
Applicationにログインできたら成功です。
この記事では"Always require Multi-factor Authentication"をオンにして常に多要素認証がかかるようにしていますが、先のステップで試したRulesを作成して特定の条件下(例/特定の時間帯、機密性の高い情報へのアクセスが発生したタイミング)で多要素認証がかかるようにすることが可能です。
APIの登録〜API Protectionの概要
Ctr-Cで起動したApplicationを停止して"Always require Multi-factor Authentication"フリップスイッチをオフにします。左ペインの"APIs"をクリック、右上の"CREATE API"を押します。
"Name", "Identifier"に任意の値を入力して”CREATE”を押します。"Identifier"は実在するURLである必要はありません。識別し易いIDを入力して下さい。
"Settings"タブをクリック、画面中頃の"Enable RBAC", "Add Permissions in the Access Token"フリップスイッチをオンにして画面下の"SAVE"を押します。後続のステップで設定するPermissionをaccess_tokenの中に含めて認可情報を制御することが可能になります。
"Permissions"タブをクリック、"read:appointments", "update:appointments"を各々入力して"ADD"を押します。Permissionの書式はOAuth2.0で定義されているxxxx:xxxxである必要があります。
左ペインの"Roles"をクリック、"ADD ROLE"を押します。
"Name"に"Appointments Submitter", "Description"に任意の説明を入力して"CREATE"を押します。
"Permissions"タブをクリックして"ADD PERMISSIONS"を押します。
作成したAPIを選択して全てのPermissonを選択、"ADD PERMISSIONS"を押します。
"Users"タブをクリックして"ADD USERS"を押します。
任意のユーザを選択して"ASSIGN"を押します。アサインされたユーザでApplicationにログインしたタイミングでこのAPIのスコープにアクセスすることが許可された認可情報がaccess_tokenに追加されます。
ターミナルでauth0-react-samples/02-Calling-an-API/srcに移動します。
$ cd auth0-react-samples/01-Login/srcauth_config.json.exampleをコピーしてauth_config.jsonを作成します。
$ cp auth_config.json.example auth_config.jsonauth_copnfig.jsonを編集します。"clientID"は"Appications"->"作成したApplication"->"Settings"から確認できます。
{ "domain": "kiriko.auth0.com", "clientId": "xxxx" }Node.jsのパッケージをインストールしてApplicationを起動します。
$ pwd ~/auth0-react-samples/02-Calling-an-API $ npm install $ npm run devChromeで
http://localhost:3000
にアクセスしてRoleにアサインしたユーザでログインします。"External API"タブをクリック、"Ping API"ボタンを押します。Chromeのデベロッパーツールを開いて"Network"->"External"をクリック、"Request Headers"の"ey..."をコピーします。これがaccess_tokenです。
https://jwt.io/
にアクセスして”Encoded”の下にコピーしたaccess_tokenをペーストします。”Decoded”の"PAYLOAD"にRoleに割り当てたPermissionが表示されていれば成功です。次のステップ
最後までお付き合い頂きありがとうございます。この記事はAuth0を初めて触る方に短時間でAuth0をご習得頂くことを目的とした入り口のため、物足りないとお感じになる方もいらっしゃるかと思います。次のステップとして、Auth0のContent Engineering Teamが投稿している技術ブログをお試し頂くことをお勧めします。様々な言語やフレームワーク、実際の現場で起こりうる認証・認可のシナリオに沿ったテーマを準備しています。
- 投稿日:2019-11-25T00:49:11+09:00
WebSocket についてまとめてみる
WebSocket とは?
Web 上でクライアント(Web ブラウザ)・サーバー間を
常時接続にしておいて、双方向通信を低コストで
実現するための技術規格。プロトコル。WebSocket の何がありがたいのか?
近年 SNS アプリなどではインタラクティブで
リアルタイム
なやりとりが求められるようになってきました。例えば、チャットアプリでは、複数のユーザーが同じページを見ているような状況で、誰かの発言が他のユーザーのページにも
ページのリロードなしでリアルタイムに更新
されるようにしたい、ということがあると思います。
このリアルタイムに更新
という機能を実現するためには、誰かが発言したということをサーバーからクライアントに伝える必要があります。
このような機能を WebSocket は実現します。
クライアントからのリクエストがなくても、常時接続しているのでサーバーからクライアントに好きなタイミングで通信ができる
ということを実現できるところが WebSocket の魅力です。HTTP ではダメなの?
Web サイトを閲覧するするときは普通、HTTP を使うかと思います。
この HTTP は、クライアント(ブラウザ)がサーバーにリクエストを送って、一時的にコネクションを張り、サーバーがレスポンスを返す
という流れになります。そして一つのコネクションにつき、一つのリクエストしか送れません...。つまり、基本的に HTTP は
クライアントが何らかのリクエストを送らない限り、サーバーはレスポンスを返せないプロトコル
なのです。HTTP で擬似的に双方向通信を実現することはできるが...
WebSocket はこのような HTTP(XMLHttpRequest) の欠点の補おうというニーズから生まれた規格なので、HTTP しか手段がなかった時は HTTP でなんとかしようとしていました。
具体的には、HTTP のコネクションを張りっぱなしにしておいて、クライアントから一定間隔でサーバーにポーリングし続ける。サーバーから情報を送りたいタイミングになって初めてレスポンスを返すといった具合です。
※ XMLHttpReqest のロングポール
といい、Comet (サーバで発生したイベントをクライアントからの要請なしにクライアントに送信することができる技術) の実現に必要だった。ただ、この方法では以下のような問題が生じました。
- ブラウザの HTTP コネクションのタイムアウト(30秒)があるため、接続し直す処理が必要になる。
- 擬似的に双方向通信を行っているので、通信が発生するごとに TCP ハンドシェイクを再度行うことが必要になる。
- HTTP コネクションを長時間占有するため、その間サーバに接続する他のアプリケーションの動作に影響を及ぼす可能性がある。
対して WebSocket は...
対して WebSocket には以下のような特徴があり、HTTP の問題を解決しています。
- サーバとクライアントが一度コネクションを行った後は、必要な通信を全てそのコネクション上で専用のプロトコルを用いて行い、新たなコネクションを張る必要がない。
- HTTP コネクションとは異なる軽量プロトコルを使うなどの理由で、通信ロスが減る。
- 一つのコネクションで全てのデータ送受信が行えるため同一サーバに接続する他のアプリケーションへの影響が少ない。
WebSocket の機能・特徴まとめ
以上のことから WebSocket の機能・特徴をまとめると以下のようになるかと思います。
- 常時双方向通信によるサーバプッシュ機能 : 一度コネクションを確立したあとは、
サーバとクライアントのどちらからも通信を行うことが可能
になる。- データ通信量削減 : Payload 意外の情報(ヘッダ)は最小2byte, 最大でも14byteに収まる様になっていてとても小さい。
- 低コスト : HTTP のように通信のたびにコネクションを張らず、
一度コネクションを確立するとそのコネクション上で通信を行う。
WebSocket の通信の流れ
1. WebSocket opening ハンドシェイク
WebSocket opening ハンドシェイクは HTTP 通信で行われます。
クライアントのリクエストには以下のようにUpgrade ヘッダ
、Connection ヘッダ
、Sec-WebSocket-Version ヘッダ
、Sec-WebSocket-Key ヘッダ
が付けられます。Upgrade: websocket Connection: upgrade Sec-WebSocket-Version: 13 Sec-WebSocket-Key: XXXXXXXXXXXXXXXXXX==サーバーからのレスポンスヘッダは以下のようになります。
HTTP/1.1 101 OK Upgrade: websocket Connection: upgrade Sec-WebSocket-Accept: ZZZZZZZZZZZZZZZZZZZ==ステータスコード 101 は「Switching Protocols」で、これでコネクションが確立したことになります。
2. 双方向通信
コネクション確立後は、HTTP ではなく WebSocket プロトコルで通信を行います。
「フレーム」という単位でデータのやりとりが行われます。実際に使ってみる
クライアントからサーバーにメッセージ(データ)を送信してみる
実際に WebSocket を使ってみます。まずはクライアントからサーバーにデータを送信する例からです。
Node.js を使います。
ws モジュールが必要になるので、インストールしておきます。$ npm install wsサーバーサイドのコードは以下のような感じです。
// index.js const server = require('ws').Server; const ws = new server({ port: 8081 }); wsServer.on('connection', socket => { console.log('connected!'); socket.on('message', ms => { console.log(ms); }); socket.on('close', () => { console.log('good bye.'); }); });書き終わったら、
node index.js
で起動させておきます。Chrome の Developer tool でもなんでも良いのですが、クライアントのコードは以下のような感じで実行してみます。
const con = new WebSocket('ws://localhost:8081'); con.send('Hello WebSocket!'); con.close();サーバーに以下のようなログが流れるはずです。
簡単ですね。connected! Hello WebSocket! good bye.サーバーから複数クライアントにメッセージ(データ)を送ってみる
上の例は HTTP でもよくある、クライアントがリクエスト投げてサーバーがレスポンス返すみたいなパターンだったので、サーバーから複数クライアントにメッセージを送ってみます。(WebSocket の恩恵を感じられるはず...)
まずはサーバーサイドからです。
const server = require('ws').Server; const ws = new server({ port: 8081 }); ws.on('connection', socket => { socket.on('message', ms => { ws.clients.forEach(client => { client.send('Hello, this message comes from server!'); }); }); socket.on('close', () => { console.log('good bye.'); }); });クライアントからメッセージが届くと、接続している全てのクライアントにメッセージを送るようにしています。同様に書き終わったら
node index.js
で起動します。続いてクライアントサイド。擬似的にクライアントを多数にするため、Chrome の複数タブで Developer tool を開いてください。
const con = new WebSocket('ws://localhost:8081'); con.onmessage = (m) => { console.log(m.data) }; // ここまでは、全てのタブ共通で入力する con.send('Hello'); // ここは一つのタブでだけ入力するメッセージを send したタブでサーバからのレスポンスが表示されているのは、それはそうでしょという感じですが...
send していない別タブにもサーバーからメッセージが届いています...!
感動ですね。おまけ
セキュア通信は wss で
HTTP と同じく、WebSocket にもセキュア通信用のプロトコルが用意されており
wss
になっています。Event の登録は addEventListener でもできる
con.onmessage = (m) => { console.log(m.data) }; con.addEventListener('message', m => { console.log(m.data)});よく利用する Event 処理とメソッド
// Event 処理 - onopen // 接続された時 - onerror // エラーが発生した時 - onmessage // データを受け取った時 - onclose // 切断された時 // メソッド - send() // データを送信する - close() // 通信を切断する
- 投稿日:2019-11-25T00:20:35+09:00
nodebrewコマンドのメモ
nodebrewめも
「なにインストールしたんやったっけ?」
「切替ってどうやるんやったっけ?」
勉強のときこういうこと多いので備忘録用のめもです。
なのでインストールは終わってる前提です。ご了承ください。。。インストールできるバージョンを確認したい
$ nodebrew ls-remoteインストールしたい
- バージョンを指定する方法
- エイリアスで指定する方法
があるようです。
バージョンを指定する方法
$ nodebrew install [version] # 例えば [version] -> v12.0.0エイリアスで指定する方法
安定版が欲しい場合
$ nodebrew install stable最新版が欲しい場合
$ nodebrew install latest※安定版と最新版の違い
安定版: 安全。
最新版: 新しい機能が盛り込まれている。バグが残ってることもある。
のような感じバージョンを指定してNodeを使いたい
$ nodebrew use [version] # 例えば [version] -> v12.0.0インストールしたバージョンを確認したい/使用中のバージョンを確認したい
$ nodebrew ls結果には
current: v12.0.0
のように現在使用しているバージョンも表示される最後に
Nodebrewを業務で使ってるところってあるんかな、、、
編集終わってから
nvm
ってのを知りました。nodebrewと同じくNode.jsのバージョン管理ができるやつなんかな。調べないと、、、