メインコンテンツまでスキップ

Express によるサーバー構築

ウェブサイトが動作する仕組み

「Web プログラミングの基礎を学ぼう」 の章では、ウェブサイトを表示するために HTML ファイルと JavaScript ファイルを作成し、ブラウザから開きました。しかしながら、通常のウェブサイトではこのような手順は踏まず、URL をブラウザに入力することにより閲覧することができます。

Web では、通常インターネットを介してデータをやり取りします。インターネットを人間が直接利用することはできないので、何らかのコンピューターを使用しなければなりません。このとき、通常は

  • クライアント: サービスを利用する側のコンピューターや、その上で直接通信を担うソフトウェア
  • サーバー: サービスを提供する側のコンピューターや、その上で直接通信を担うソフトウェア

という二者の関係が発生します。また、その間で発生する通信を、その方向により

  • リクエスト: クライアントからサーバーに対する要求
  • レスポンス: リクエストに対するサーバーからクライアントへの応答

のように区別して呼びます。それでは、Node.js で Web サーバーを作ってみましょう。

Express パッケージを用いて Web サーバーを構築する

Express パッケージ を用いると、簡単に Web サーバーを構築できます。

まずは express パッケージを npm でインストールします。

npm install express

続いて、次のような main.mjs を作成しましょう。

main.mjs
import express from "express";
const app = express();

app.get("/", (request, response) => {
response.send("Hello World");
});

app.listen(3000);

ファイルを保存したら、作成したファイルを実行しましょう。

node main.mjs

ブラウザで http://localhost:3000/ にアクセスしてみましょう。ブラウザに Hello World と表示されましたか?

サーバー

Web サーバーの停止

このプログラムは、一度起動すると停止しません。サーバーにとって、クライアントからのリクエストはいつやってくるかわからないため、常に起動し続けている必要があるからです。Node.js プログラムを終了するには、ターミナル上で control + C (macOS) / Ctrl + C (Windows) を押します。

書いたコードを詳しく見てみましょう。

express 関数を呼び出すことにより、express.Application クラスのインスタンスが作成されます。

express.Application#get メソッドは、クライアントから特定のパスに対してリクエストが来た時に実行される関数を追加するメソッドです。第 1 引数にはパスの文字列を、第 2 引数には実行される関数を指定します。

例えば今回であれば第 1 引数に "/path" を渡すと http://localhost:3000/path にリクエストが来たときに関数が実行されることになります。

第 2 引数の関数を詳しく見てみましょう。この関数は 2 つの引数をとります。具体的には第 1 引数に受け取ったリクエストを表す express.Request クラス のインスタンスが、第 2 引数にこれから送るレスポンスを表す express.Response クラス のインスタンスが渡されます。

そして express.Response#send メソッドにより、クライアントが必要なデータを送信することができます。

http標準モジュール

express を使わずに Node.js 単体で Web サーバーを作成するには、http 標準モジュールを使用します。

http 標準モジュールを使って 簡単な Web サーバーを構築すると以下のようなコードになります。

main.mjs
import { Server } from "http";

const server = new Server();

server.addListener("request", (request, response) => {
response.write("Hello World");
response.end();
});

server.listen(3000);

HTTP

インターネット上には、さまざまなデータが流れています。インターネットに接続しているコンピューターが好き勝手にデータを送受信しても、意味のあるやり取りは成立しません。このため、通信を行うための手順を標準化しておく必要があります。こうしてできた手順のことを、プロトコルと呼びます。

Web の世界で用いられるプロトコルは、通常 HTTP と呼ばれるものです。ブラウザに http://example.com/path/to/index.html が入力された場合、ブラウザとサーバーの間で次の図のような通信が行われます。

HTTP

Web サーバーにアクセスするために用いた http://localhost:3000/ のうち、http はプロトコルを、localhost:3000 はサーバーの所在地を表しています (localhost は自分のコンピューターを指します)。

静的ホスティング

次の例では、//script.js/sub//sub/script.js へのリクエストについて、それぞれファイルから読み込んでレスポンスを送信しています。

main.mjs
import express from "express";
import { readFileSync } from "node:fs";
const app = express();

app.get("/", (request, response) => {
response.send(readFileSync("static/index.html", "utf-8"));
});
app.get("/script.js", (request, response) => {
response.send(readFileSync("static/script.js", "utf-8"));
});
app.get("/sub/", (request, response) => {
response.send(readFileSync("static/sub/index.html", "utf-8"));
});
app.get("/sub/script.js", (request, response) => {
response.send(readFileSync("static/sub/script.js", "utf-8"));
});

app.listen(3000);

express.static 関数を用いると、このような「リクエストを受け取ったら、そのパスに応じて適切なファイルを読み込んでレスポンスとして返す」という一連の動作を簡単に記述できます。

main.mjs
import express from "express";

const app = express();
app.use(express.static("static"));
app.listen(3000);

これにより、リクエストのパスをもとに、static フォルダ内の適切なファイルが自動的に配信されます。

index.html の省略

express.static を用いる場合、index.html は省略可能になります。つまり、/ へのリクエストで static/index.html が、/sub へのリクエストで static/sub/index.html にアクセスできるようになります。これは、Express や JavaScript に限ったことではなく、多くの Web サーバーの実装において、こういったルールが成り立ちます。

複雑なウェブページ

前項のプログラムを書き換えて、複雑な HTML を出力できるようにしてみましょう。

main.mjs
import express from "express";
const app = express();

const names = ["田中", "鈴木", "佐藤"];
app.get("/", (request, response) => {
response.send(`
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Title</title>
</head>
<body>
<ul>
${names.map((name) => `<li>${name}</li>`).join("")}
</ul>
</body>
</html>
`);
});

app.listen(3000);
Array#join

Array#join メソッドは、配列を指定した区切り文字で結合した文字列を返すメソッドです。

console.log(["Apple", "Banana", "Orange"].join("/")); // Apple/Banana/Orange

このようにテンプレートリテラルを用いることで、複雑なウェブページの内容を表すことができます。

String#replace メソッド

上記のようにテンプレートリテラルを使って HTML を作成することもできますが、HTML がさらに長くなったり、複雑なプログラムが必要になってきたりしたらこのまま続けていくのは難しそうです。

String#replace メソッドは、第一引数に与えられた文字列に一致する文字列を第二引数の文字列に置き換えた新しい文字列を返します。 String#replace メソッドは、最初に一致した場所のみを置き換えます。 一致するすべての場所を置き換えたい場合は、代わりに String#replaceAll メソッドを使います。

const base = "ABCBCC";
document.write(base.replace("BC", "EFG")); // AEFGBCC
document.write(base.replaceAll("C", "T")); // ABTBTT

String#replace メソッドを用いることで、長い HTML を見通しよく記述できるようになります。

index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Title</title>
</head>
<body>
<ul>
<!-- users -->
</ul>
</body>
</html>
main.mjs
import express from "express";
import { readFileSync } from "node:fs";

const app = express();

const names = ["田中", "鈴木", "佐藤"];
app.get("/", (request, response) => {
const template = readFileSync("./index.html", "utf-8");
const html = template.replace(
"<!-- users -->",
names.map((name) => `<li>${name}</li>`).join(""),
);
response.send(html);
});

app.listen(3000);

リクエストを受け取ったとき、プログラムではまず readFileSync 関数を用いて index.html ファイルの内容を文字列として読み込んでいます。

次の行で String#replace メソッドを用いて、読み込んだ HTML ファイルの中の "<!-- users -->" を、 names を箇条書きにした文字列に置換します。

課題

訪問者カウンター

Express を用いて、あなたは n 人目のお客様です。 とレスポンスする Web サーバーを作成してください。n はアクセスされるたびに 1 ずつ増えるようにしてください。

解答例: 訪問者カウンター
let count = 0;
app.get("/", (request, response) => {
count += 1;
response.send(`あなたは${count}人目のお客様です。`);
});

サーバー側とクライアント側

(重要) アクセスされた時刻をウェブサーバー側で求めて表示するウェブサーバーと、ブラウザに求めさせるウェブサーバーをそれぞれ作成してください。また、この 2 つの違いは何でしょうか。どういった場合にどちらの手法を使うのが適切でしょうか。

解答例: サーバー側とクライアント側
app.get("/", (request, response) => {
response.send(`
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Title</title>
</head>
<body>
${
// この部分はサーバーサイドで実行されます
new Date().toString()
}
<script>
// この部分はクライアントサイドで実行されます
document.write(new Date().toString());
</script>
</body>
</html>
`);
});