Node.jsを学ぶ-The definitive Node.js handbook-6
【所要時間】
2時間20分(2018年 12月10,19日)
【概要】
- The definitive Node.js handbookを参考にNode.jsを学ぶ。
- Node.jsを学ぶ-The definitive Node.js handbook- 5の続き。
【要約・学んだこと】
Async and Await
JSの非同期関数の近代的なアプローチについて。
JSはES2015のpromisesのコールバックから急速に発展し、ES2017のasynchronous JSはasync/awaitシンタックスでさらにシンプルになった。
async関数はpromisesとgeneratorsの組み合わせで、それらはpromisesよりも高いレベルの抽象化と基本的になる。改めて、async/awaitはpromises上でビルとされる。
Why were async/await introduced?
promise周辺の定型文を減らすことができ、”シェーンを壊さない”というチェーンprimiseの制限を減らすことができる。
ES2015でpromisesが導入された時、非同期コードの問題を解決したが、2年後にpromiseが最終的な解決策ではないことがわかった。
promiseは有名なコールバッック地獄を解決するために導入されたが、複雑さをもたらした。
それらはより良いシンタックスが開発者に公開されるよいプリミティブだったので、時が経つとasync functionsを得ることができた。
同期のように見えるが非同期で、裏側ではノンブロッキングなのである。
How it works
async関数は下記の例のようにpromiseを返す。
const doSomethingAsync = () => {
return new Promise((resolve) => {
setTimeout(() => resolve('I did something'), 3000)
})
}
awaitで待機してこの関数を呼び出したい時、コードをコールすることはpromiseがreslovedかrejectedされるまで止まる。クライアント関数はasyncとして定義されなければいけないので注意が必要。
const doSomething = async () => {
console.log(await doSomethingAsync())
}
A quick example
非同期的に関数を実行するのに使われるasync/awaitのシンプルな例だ。
const doSomethingAsync = () => {
return new Promise((resolve) => {
setTimeout(() => resolve('I did something'), 3000)
})
}const doSomething = async () => {
console.log(await doSomethingAsync())
}console.log('Before')
doSomething()
console.log('After')
このコードは下記のように出力する。
Before
After
I did something //after 3s
Promise all the things
asyncキーワードを関数の前につけることは、関数がpromiseを返すことを意味する。
例え明示的でなくとも、内部でpromiseを返している。
そのため下記のコードは有効だ。
const aFunction = async () => {
return 'test'
}aFunction().then(alert) // This will alert 'test'
そしてこれは下記のコードと同じだ。
const aFunction = async () => {
return Promise.resolve('test')
}aFunction().then(alert) // This will alert 'test'
The code is much simpler to read
上記のコードを見てわかるように、コードはシンプルだ。asyncをプレーンなpromiseとチェーン、コールバック関数を使って描くときと比べる。
簡単な例として、主な利点はコードがより複雑なときに発生する。
例えば、これはpromiseを使ってJSONリソースを取得し、解析する。
const getFirstUserData = () => {
return fetch('/users.json') // get users list
.then(response => response.json()) // parse JSON
.then(users => users[0]) // pick first user
.then(user => fetch(`/users/${user.name}`)) // get user data
.then(userResponse => response.json()) // parse JSON
}getFirstUserData()
そして同じ機能をawait/asyncを使うと下記のようになる。
const getFirstUserData = async () => {
const response = await fetch('/users.json') // get users list
const users = await response.json() // parse JSON
const user = users[0] // pick first user
const userResponse = await fetch(`/users/${user.name}`) // get user data
const userData = await user.json() // parse JSON
return userData
}getFirstUserData()
Multiple async functions in series
async関数は簡単にチェーンになり、シンタックスはプレーンなpromisesを使うよりはるかに読みやすい。
const promiseToDoSomething = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 10000)
})
}const watchOverSomeoneDoingSomething = async () => {
const something = await promiseToDoSomething()
return something + ' and I watched'
}const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
const something = await watchOverSomeoneDoingSomething()
return something + ' and I watched as well'
}watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => {
console.log(res)
})
出力は
I did something and I watched and I watched as well
Easier debugging
promisesのデバッグは、デバッガーが非同期コードをステップオーバーしないため難しい。
async/awaitはコンパイラーが非同期コードのようなものなので、とても簡単だ。
The Node.js Event Emitter
Node.jsでカスタムイベントを動作させることができる。
ブラウザ上でJSで動作させるなら、ユーザーとのやりとりがイベントを通してどれくらい処理されるかを知っている:mouse cllicks, keyboard button pressesなど
バック側では、Node.jsはevents moduleを使用した同様のシステムの構築というオプションを提供している。
このモジュールはEventEmitterクラスというイベントを扱うために使うものを提供してくれる。
これを使い初期化する。
const eventEmitter = require('events').EventEmitter()
このオブジェクトはon, emitメソッドなどを公開している。
- emitはイベントのトリガーに使われる。
- onはイベントがトリガーされるときに実行されるコールバック関数の追加に使われる。
- 例えば、start eventを作る。サンプルを提供するために、コンソールにロギングするだけで反応する。
eventEmitter.on('start', () => {
console.log('started')
})
下記を実行する。
eventEmitter.emit('start')
イベントハンドラー関数はトリガーされ、console logを取得する。
emit()への追加の引数として渡すことで、イベントハンドラに引数を渡すことができる。
eventEmitter.on('start', (number) => {
console.log(`started ${number}`)
})eventEmitter.emit('start', 23)
複数の引数の場合は
eventEmitter.on('start', (start, end) => {
console.log(`started from ${start} to ${end}`)
})eventEmitter.emit('start', 1, 100)
EventEmitterオブジェクトはイベントと作用するために他にも複数のメソッドを公開している。
- once(): one-timeリスナーを加える。
- removeListener()/off(): イベントからリスナーを取り除く。
- removeAllListeners(): イベントの全てのリスナーを取り除く。
How HTTP requests work
ブラウザでURLをタイプすると何が起きるのか。
ここではブラウザがHTTP/1.1プロトコルを使ったページリクエストをどのように行うのかを説明する。
The HTTP protocol
URLリクエストだけを解析する。
モダンなブラウザはアドレスバーに記入したことが実際のURLか検索ワードかを知る能力があり、もし有効なURLでなければデフォルトのサーチエンジンを使い検索する。
実際のURLをタイプしたとする。
URLを打ち込みエンターを押す時、ブラウザはまずフルURLをビルドする。
もしflaviocopes.comといったドメインだけを打ち込むと、デフォルトブラウザはHTTP://を前に置いてくれ、HTTPプロトコルを初期化する。
Things relate to macOS / Linux
windowsでは少し異なる動きをすると思われる。
DNS Lookup phase
ブラウザはDNSルックアップを開始し、サーバーIPアドレスを取得する。
ドメイン名は人間には扱いやすいが、インターネットは222.324.3.1(IPv4)といった数字の塊のIPアドレスを通じてサーバーの正確な場所をさがすコンピュータによって構成されている。
まず、DNSローカルキャッシュをチェックし、ドメインが最近解明されたかどうかを調べる。
Chrome has a handy DNS cache visualizer you can see at this URL: chrome://net-internals/#dns (copy and paste it in the Chrome browser address bar)
もし見つからなければ、ブラウザはDNSリゾルバを使う。gethostbyname POSIXシステムコールを使い、ホスト情報を取得する。
gethostbyname
gethostbynameはまずローカルホストファイル内を見る。それはmacOSかLinux上が/etc/hostsにあり、システムがローカルに情報を提供しているかをみる。
ドメインについて何も情報を与えていなければ、システムはDNSサーバーにrequestを作らせる。
DNSサーバーのアドレスはsystem preferancesに保存されている。
- 8.8.8.8: GoogleパブリックDNSサーバー
- 1.1.1.1: CloudFlare DNSサーバー
大抵の人はインターネットプロバイダに与えられたDNSサーバーを使う。
ブラウザはUDPプロトコルを使ってDNSリクエストを行う。
TCPとUDPはコンピュータネットワーキングの基本的なプロトコルの2つだ。これらは同じコンセプトレベルにいるが、TCPはコネクション型で、UDPはコネクションレスプロトコルでより軽く、オーバーヘッドの少ないメッセージを送信するのに使われる。
UDPリクエストがどの用意使われるかはここでは扱わない。
DNSサーバーはキャッシュ内にドメインIPを持つ。そうでなければroot DNSサーバーに尋ねる。それがインターネット全体を動かすシステムだ。(13のサーバーで構成され、世界中に分散されている。)
DNSサーバーはそれぞれのドメイン名を知っているわけではない。
知っていることは、どこにtop-level DNS resolversがあるかだ。
トップレベルドメインはドメインの拡張子だ。: .com, .it, .pizzaなど。
ルートDNSサーバーがrequestを受け取るとすぐに、requestをtop-level domain(TLD) DNSサーバーに転送する。
flaviocopes.comを探してるとすると、ルートドメインDNSサーバーは.com TLDサーバーのIPを返す。
我々のDNSリゾルバがTLDサーバーのIPをキャッシュするので、ルートDNSサーバーに再びこれを尋ねる必要がなくなる。
TLD DNSサーバーは、我々が探しているドメインの正式なName ServersのIPアドレスを持っている。
どのように?それはドメインを購入する時、ドメイン登録者が適切なTDLをname serversに送る。name serversを更新すると(例えばホスティングプロバイダをへんこうすると)この情報は自動的にドメイン登録者によって更新される。
これらはホスティングプロバイダのDNSサーバーだ。通常バックアップのために1つ以上ある。
例:
ns1.dreamhost.com
ns2.dreamhost.com
ns3.dreamhost.com
DNSレゾルバは最初に探しているドメイン(サブドメインを含む)のIPを尋ねようとする。
それがIPアドレスの真実の究極の源である。
TCP request handshaking
サーバーIPアドレスが利用可能になったので、ブラウザはTCPコネクションを開始できる。
TCPコネクションは完全に開始する前にいくらかのハンドシェーキングが必要とし、データの送信を開始できる。
コネクションが設立したらリクエストを送ることができる。
Sending the request
リクエストはコミュニケーションプロトコルによって決められた正確な方法で、構造化されたプレーンテキストドキュメントである。
それは3つのパートで構成される。
- リクエストline
- リクエストheader
- リクエストbody
The request line
リクエストラインは一行:
- HTTPメソッド
- リソースロケーション
- プロトコルバージョン
例:
GET / HTTP/1.1
The request header
リクエストヘッダーはいくつかの値を設定するfield: valueといったペアのセットである。
2つの必須フィールドがあり、HostとConnectionだ。他のフィールドは全てオプションとなる。
Host: flaviocopes.com
Connection: close
Hostはターゲットにしたいドメイン名を示し、Connectionはコネクションがopenにし続けなくては行けない場合でなければ常にcloseである。
よく使われるヘッダーフィールドは下記となる。:
Origin
Accept
Accept-Encoding
Cookie
Cache-Control
Dnt
しかしこれ以外にもたくさんある。ヘッダーパートは空行によって決められる。
The request body
リクエストボディはオプションであり、GETリクエストで使われるのではなく、POSTリクエストでよく使われ、時に他の同士でも使われる。そしてJSONフォーマットにデータを保有する。
The response
リクエストが送られるとすぐにサーバーはそれを処理し、レスポンスを送り返す。
レスポンスはステータスコードとステータスメッセージで始まる。リクエストが成功すると200を返す。
200 OK
リクエストは下記のようなステータスコードを返すことがある。
404 Not Found
403 Forbidden
301 Moved Permanently
500 Internal Server Error
304 Not Modified
401 Unauthorized
レスポンスはHTTPヘッダーリストとレスポンスボディを保持する。(ブラウザでリクエストを作り、HTMLとなるため)
Parse the HTML
ブラウザはHTMLを受け取ると、解析を始める。このページに必要とされる全てのリソースに対して実行したのと全く同じプロセスを繰り返す。
- CSS files
- images
- the favicon
- JavaScript files
- …
ブラウザがどのようにレダリングするかはここでは扱わないが、これはただのHTMLページではなく、HTTPに提供される全てのアイテムに共通する。
【わからなかったこと】
なし
【感想】
間が空いてしまった。早く終わらせたい。残り3分の1くらい。