035.1 レッスン 1
Certificate: |
Web開発の要点 |
---|---|
Version: |
1.0 |
Topic: |
035 Node.jsサーバプログラミング |
Objective: |
035.1 Node.jsの基本 |
Lesson: |
1 of 1 |
はじめに
Node.jsは、PythonやRubyのような第二言語をサーバサイドプログラムに使用するのではなく、Webサーバ — いわゆるWeb バックエンド (サーバサイド) — でJavaScriptコードを実行するJavaScript実行環境です。JavaScript言語は、現代のWebアプリケーションのフロントエンド側ではすでに使用されており、ユーザーがWebブラウザで操作するインターフェースのHTMLやCSSと相互作用しています。Node.jsとブラウザ上のJavaScriptを併用することで、アプリケーション全体で1つのプログラミング言語を使用することが可能になります。
Node.jsの存在意義は、バックエンドで複数の同時接続を処理する方法にあります。Webアプリケーションサーバが接続を処理する最も一般的な方法の一つは、複数のプロセスを実行することです。コンピュータでデスクトップアプリケーションを開くと、プロセスが起動して多くのリソースを使用します。ここで、大規模なWebアプリケーションで何千人ものユーザーが同じことをしている場合を考えてみましょう。
Node.jsでは、 イベントループ と呼ばれる設計を用いてこの問題を回避しています。これは、計算されるべきタスクが入ってくるかどうかを継続的にチェックする内部ループです。JavaScriptの普及とWeb技術のユビキタス化のおかげで、Node.jsは小規模なアプリケーションから大規模なアプリケーションまで幅広く採用されました。Node.jsが広く採用されるようになった背景には、非同期処理やノンブロッキングの入出力(I/O)処理など、他の特徴もありますが、これについてはこのレッスンで後ほど説明します。
Node.js環境では、JavaScriptエンジンを使用して、サーバサイドまたはデスクトップ上でJavaScriptコードを解釈して実行します。このような状態では、プログラマが書いたJavaScriptコードは、元のJavaScriptコードが生成した機械の命令を実行するために、ジャストインタイムで解析、コンパイルされます。
Note
|
Node.jsのレッスンを進めていくと、Node.jsのJavaScriptは、ブラウザ上で動作するもの(https://www.ecma-international.org/publications-and-standards/standards/[ECMAScript仕様]に準拠)とは全く同じではないですが、非常に似ていることに気づくかもしれません。 |
スタートアップ
このセクションと以下の例では、Linux OSにNode.jsがすでにインストールされていて、ユーザーがターミナルでコマンドを実行する方法などの基本的なスキルを持っていることを前提としています。
以下の例を実行するには、 node_examples
という作業ディレクトリを作成します。ターミナルプロンプトを開き、 node
と入力します。Node.jsが正しくインストールされていれば、JavaScriptコマンドをインタラクティブにテストできる >
プロンプトが表示されます。このような環境はREPLと呼ばれ、 “read、evaluate、print、そしてloop” を意味します。以下の入力(あるいは他のJavaScriptの文)を >
プロンプトに入力してください。各行の後にEnterキーを押すと、REPL環境が実行の結果を返します。
> let array = ['a', 'b', 'c', 'd']; undefined > array.map( (element, index) => (`Element: ${element} at index: ${index}`)); [ 'Element: a at index: 0', 'Element: b at index: 1', 'Element: c at index: 2', 'Element: d at index: 3' ] >
注)「Element: ${element} at index: ${index}」を囲むのはバッククォート( ` )です。
このスニペットはES6の構文を使って書かれており、配列を反復処理し、文字列テンプレートを使って結果を表示するmap関数が用意されています。このように、有効なコマンドであれば、ほとんどのものを書くことができます。Node.jsのターミナルを終了するには、最初のピリオドを忘れずに .exit
と入力してください。
長いスクリプトやモジュールの場合は、VS Code、Emacs、Vimなどのテキストエディタを使うと便利です。先ほど示した2行のコードを(少し修正して) start.js
というファイルに保存することができます。
let array = ['a', 'b', 'c', 'd'];
array.map( (element, index) => ( console.log(`Element: ${element} at index: ${index}`)));
その後、シェルからこのスクリプトを実行すると、以前と同じ結果が得られます。
$ node ./start.js Element: a at index: 0 Element: b at index: 1 Element: c at index: 2 Element: d at index: 3
コードを書き始める前に、Node.jsのシングルスレッド実行環境とイベントループを使って、Node.jsがどのように動作するのかを概観してみましょう。
イベントループとシングルスレッド
Node.jsのプログラムがリクエストを処理するのに必要な時間を知ることは困難です。メモリ上の変数をループして返すだけの短いリクエストもあれば、システム上でファイルを開いたり、データベースにクエリを発行して結果を待ったりするような時間のかかるアクティビティを必要とするリクエストもあります。Node.jsはこの不確実性をどのように扱うのでしょうか?その答えが、イベントループです。
シェフが複数の作業をしている様子を想像してみてください。ケーキを焼くという作業は、オーブンで調理するために多くの時間を必要とします。シェフは、ケーキが焼きあがるのを待ってから、コーヒーを淹れに行くのではありません。オーブンがケーキを焼いている間に、シェフはコーヒーを作るなどの作業を並行して行います。しかし、コーヒーを作るという特定の作業に集中するタイミングなのか、それともケーキをオーブンから取り出すタイミングなのか、シェフは常に確認しています。
イベントループは、周囲の活動を常に把握しているシェフのようなものです。Node.jsでは、 “イベントチェッカー” が、JavaScriptエンジンで処理が完了した操作や処理待ちの操作を常にチェックしています。
この方法では、非同期で長い操作を行っても、後に続く他のクイック操作をブロックすることはありません。これは、イベントループのメカニズムが、I/O操作などの長いタスクがすでに完了しているかどうかを常にチェックしているからです。もしそうでなければ、Node.jsは他のタスクの処理を続けることができます。バックグラウンドタスクが完了すると、結果が返され、Node.jsの上にあるアプリケーションは、トリガー関数(コールバック)を使って出力をさらに処理することができます。
Node.jsは他の環境のように複数のスレッドを使用しないため、 シングルスレッド環境 と呼ばれ、ノンブロッキングなアプローチが最も重要となります。これが、Node.jsがイベントループを使用する理由です。しかし、計算量の多いタスクには、Node.jsは最適なツールではありません。このような問題をより効率的に解決するプログラミング言語や環境は他にもあります。
次のセクションでは、コールバック関数について詳しく見ていきます。とりあえず、コールバック関数は、あらかじめ定義された操作が完了したときに実行されるトリガーであることを理解しておいてください。
モジュール
複雑な機能や大規模なコードをより小さなパーツに分解することは、ベスト・プラクティスです。モジュール化することで、コードベースを整理し、実装を抽象化し、複雑なエンジニアリングの問題を回避することができます。これらの必要性を満たすために、プログラマはソースコードのブロックをパッケージ化して、他の内部または外部のコード部分で消費されるようにします。
球体の体積を計算するプログラムの例を考えてみましょう。テキストエディタを開いて、 volumeCalculator.js
という名前のファイルを作成し、以下のようなJavaScriptを記述します。
const sphereVol = (radius) => {
return 4 / 3 * Math.PI * radius
}
console.log(`A sphere with radius 3 has a ${sphereVol(3)} volume.`);
console.log(`A sphere with radius 6 has a ${sphereVol(6)} volume.`);
ここで、Nodeを使ってファイルを実行します。
$ node volumeCalculator.js A sphere with radius 3 has a 113.09733552923254 volume. A sphere with radius 6 has a 904.7786842338603 volume.
ここでは、球の半径に基づいて球の体積を計算するシンプルな関数を使用しました。円柱や円錐などの体積も計算する必要があることを想像してみてください。すぐに、これらの特定の関数を volumeCalculator.js
ファイルに追加しなければならないことに気づくでしょう。構造をよりよく整理するために、分離されたコードのパッケージとしてのモジュールの考え方を利用することができます。
そのためには、 polyhedrons.js
という分離したファイルを作ります。
const coneVol = (radius, height) => {
return 1 / 3 * Math.PI * Math.pow(radius, 2) * height;
}
const cylinderVol = (radius, height) => {
return Math.PI * Math.pow(radius, 2) * height;
}
const sphereVol = (radius) => {
return 4 / 3 * Math.PI * Math.pow(radius, 3);
}
module.exports = {
coneVol,
cylinderVol,
sphereVol
}
次に、 volumeCalculator.js
ファイルの中で、古いコードを削除し、以下のスニペットで置き換えます。
const polyhedrons = require('./polyhedrons.js');
console.log(`A sphere with radius 3 has a ${polyhedrons.sphereVol(3)} volume.`);
console.log(`A cylinder with radius 3 and height 5 has a ${polyhedrons.cylinderVol(3, 5)} volume.`);
console.log(`A cone with radius 3 and height 5 has a ${polyhedrons.coneVol(3, 5)} volume.`);
そして、そのファイル名をNode.jsの環境に対して実行します。
$ node volumeCalculator.js A sphere with radius 3 has a 113.09733552923254 volume. A cylinder with radius 3 and height 5 has a 141.3716694115407 volume. A cone with radius 3 and height 5 has a 47.12388980384689 volume.
Node.jsの環境では、すべてのソースコードファイルがモジュールとみなされますが、Node.jsの “モジュール” という言葉は、先ほどの例のようにコードがラップされたパッケージを示します。モジュールを使用することで、メインファイルである volumeCalculator.js
からボリューム関数を抽象化し、サイズを小さくして、実世界のアプリケーションを開発する際に良い習慣であるユニットテストを適用しやすくしました。
Node.jsでモジュールがどのように使われるかがわかったところで、最も重要なツールの一つである Node Package Manager (NPM)を使ってみましょう。
NPMの主な仕事の一つは、外部モジュールを管理し、ダウンロードして、プロジェクトやOSにインストールすることです。nodeリポジトリを初期化するには、 npm init
というコマンドを使います。
NPM はデフォルトで、リポジトリの名前、バージョン、説明などについて質問してきます。 npm init --yes
を使えば、これらの手順を省略することができます。このコマンドは、プロジェクトやモジュールのプロパティを記述した package.json
ファイルを自動的に生成します。
お好きなテキストエディターで package.json
ファイルを開くと、キーワードやNPMで使用するスクリプトコマンド、名前などのプロパティを含むJSONファイルが表示されます。
これらのプロパティの一つは、ローカルリポジトリにインストールされている依存関係です。NPMはこれらの依存関係の名前とバージョンを package.json
に追加します。また、 package.json
が失敗した場合にNPMが予備として使用する別のファイル、 package-lock.json
も追加します。
ターミナルに次のように入力します。
$ npm i dayjs
i
フラグは、引数 install
のショートカットです。インターネットに接続されている場合、NPMはNode.jsのリモートリポジトリから dayjs
という名前のモジュールを検索し、モジュールをダウンロードしてローカルにインストールします。また、NPMはこの依存関係を package.json
と package-lock.json
ファイルに追加します。これで、node_modules
というフォルダがあり、インストールされたモジュールと、必要に応じて他のモジュールが入っていることがわかります。 node_modules
ディレクトリには、ライブラリがインポートされて呼び出されたときに使用される実際のコードが入っています。しかし、 package.json
ファイルが使用されるすべての依存関係を提供するので、このフォルダはGitを使用するバージョン管理システムには保存されません。別のユーザーが package.json
ファイルを受け取り、自分のマシンで npm install
を実行するだけで、NPMは package.json
に含まれるすべての依存関係を含む node_modules
フォルダを作成し、NPMリポジトリで利用可能な何千ものファイルのバージョン管理を回避することができます。
これで、 dayjs
モジュールがローカルディレクトリにインストールされたので、Node.jsのコンソールを開き、以下の行を入力します。
const dayjs = require('dayjs');
dayjs().format('YYYY MM-DDTHH:mm:ss')
dayjs
モジュールは require
キーワードでロードされます。このモジュールのメソッドが呼ばれると、ライブラリは現在のシステムの日付を受け取り、指定されたフォーマットで出力します。
2020 11-22T11:04:36
これは、先ほどの例で使われた仕組みと同じで、Node.jsのランタイムがサードパーティの関数をコードにロードするというものです。
サーバの機能
Node.jsは、Webアプリケーションのバックエンドを制御するため、HTTPリクエストを処理することが主な仕事の一つです。
ここでは、Webサーバが受信したHTTPリクエストをどのように処理するかをまとめました。サーバの機能は、リクエストをリッスンし、それぞれが必要とするレスポンスを可能な限り迅速に判断し、そのレスポンスをリクエストの送信者に返すことです。このアプリケーションでは、ユーザーから送られてくるHTTPリクエストを受信し、リクエストを解析し、計算を行い、レスポンスを生成して送り返す必要があります。Node.jsのようなHTTPモジュールを使用することで、これらのステップを簡略化し、Webプログラマはアプリケーション自体に集中することができます。
この非常に基本的な機能を実装した次の例を考えてみましょう。
const http = require('http');
const url = require('url');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
const queryObject = url.parse(req.url,true).query;
let result = parseInt(queryObject.a) + parseInt(queryObject.b);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(`Result: ${result}\n`);
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
これらの内容を basic_server.js
というファイルに保存して、 node
コマンドで実行します。Node.jsが起動しているターミナルには、次のようなメッセージが表示されます。
Server running at http://127.0.0.1:3000/
続いて、Webブラウザで以下のURLにアクセスします。http://127.0.0.1:3000/numbers?a=2&b=17
Node.jsは、あなたのコンピュータの中でWebサーバを実行しており、2つのモジュールを使用しています。 http
と url
です。 http
モジュールは、基本的なHTTPサーバをセットアップし、受信したWebリクエストを処理して、シンプルなアプリケーションコードに渡します。URLモジュールは、URLで渡された引数を解析し、整数形式に変換して、加算演算を行います。その後、 http
モジュールはレスポンスをテキストとしてWebブラウザに送信します。
実際のWebアプリケーションでは、Node.jsは、通常はデータベースからデータを処理・取得し、処理された情報をフロントエンドに返して表示するために使用されます。しかし、このレッスンの基本的なアプリケーションでは、Node.jsがモジュールを利用してWebサーバとしてWebリクエストを処理する様子が簡潔に示されています。
演習
-
単純な関数を書くのではなく、モジュールを使う理由は何ですか?
-
なぜNode.js環境はこんなにも普及したのでしょうか?特徴を一つ挙げてください。
-
package.json
ファイルの目的は何ですか? -
node_modules
フォルダの保存や共有が推奨されないのはなぜですか?
発展演習
-
コンピュータ上でNode.jsアプリケーションを実行するにはどうすれば良いですか?
-
サーバ内部で解析するために、URLのパラメータをどのように区切ることができますか?
-
特定のタスクがNode.jsアプリケーションのボトルネックになる可能性があるシナリオを指定してください。
-
サーバの例では、2つの数値を乗算したり、合計したりするパラメータをどのように実装しますか?
まとめ
このレッスンでは、Node.js環境の概要、その特徴、および簡単なプログラムを実装するためにNode.jsをどのように使用できるかについて説明しました。このレッスンでは、以下のコンセプトを含んでいます。
-
Node.jsとは何か、そしてなぜそれが使われるのか
-
コマンドラインを使ってNode.jsプログラムを実行する方法
-
イベントループとシングルスレッド
-
モジュール
-
Node Package Manager (NPM)
-
サーバ機能
演習の回答
-
単純な関数を書くのではなく、モジュールを使う理由は何ですか?
従来の関数ではなく、モジュールを選択することで、プログラマは、読みやすく、メンテナンスしやすい、自動テストを書きやすいコードベースを作ることができます。
-
なぜNode.js環境はこんなにも普及したのでしょうか?特徴を一つ挙げてください。
その理由の一つは、Webアプリケーションのフロントエンドですでに広く使われていたJavaScript言語の柔軟性です。Node.jsを使用すると、システム全体で1つのプログラミング言語だけを使用することができます。
-
package.json
ファイルの目的は何ですか?このファイルには、プロジェクトの名前、バージョン、依存関係(ライブラリ)などのメタデータが含まれています。この
package.json
ファイルがあれば、他の人が同じライブラリをダウンロードしてインストールし、オリジナルの作成者と同じ方法でテストを実行することができます。 -
node_modules
フォルダの保存や共有が推奨されないのはなぜですか?node_modules
フォルダには、リモートリポジトリで利用可能なライブラリの実装が含まれています。そのため、これらのライブラリを共有するより良い方法は、package.json
ファイルでそれらを示し、NPMを使用してそれらのライブラリをダウンロードすることです。この方法では、ローカルでライブラリを追跡・管理する必要がないため、よりシンプルでエラーのない方法となります。
発展演習の回答
-
コンピュータ上でNode.jsアプリケーションを実行するにはどうすれば良いですか?
ターミナルのコマンドラインで
node PATH/FILE_NAME.js
と入力し、PATH
をNode.jsファイルがあるディレクトリ(フォルダ)に、FILE_NAME.js
を選択したファイル名に変更することで、これらを実行できます。 -
サーバ内部で解析するために、URLのパラメータをどのように区切ることができますか?
アンパサンド文字
(&)
が、それらのパラメータを区切り、JavaScriptのコードの中で抽出・解析できるようにするために使われます。 -
特定のタスクがNode.jsアプリケーションのボトルネックになる可能性があるシナリオを指定してください。
Node.jsは、シングルスレッドを使用するため、CPUを多用する処理を実行するのに適した環境ではありません。数値計算のシナリオを実行すると、アプリケーション全体がスローダウンしたりロックされたりする可能性があります。数値シミュレーションが必要な場合は、他のツールを使用した方が良いでしょう。
-
サーバの例では、2つの数値を乗算したり、合計したりするパラメータをどのように実装しますか?
三項演算子やif-else条件を使って、追加のパラメータをチェックします。パラメータが文字列
mult
であれば、数値の積を返し、そうでなければ、和を返します。古いコードを以下のスニペットで置き換えてください。コマンドラインで kbd:[Ctrl+C] を押し、コマンドを再実行してサーバを再起動します。ブラウザでhttp://127.0.0.1:3000/numbers?a=2&b=17&operation=mult
というURLにアクセスして、新しいアプリケーションをテストしてみてください。最後のパラメータを省略したり変更したりすると、結果は数字の合計になるはずです。let result = queryObject.operation == 'mult' ? parseInt(queryObject.a) * parseInt(queryObject.b) : parseInt(queryObject.a) + parseInt(queryObject.b);