035.2 レッスン 2
Certificate: |
Web開発の要点 |
---|---|
Version: |
1.0 |
Topic: |
035 NodeJSサーバプログラミング |
Objective: |
035.2 NodeJS Expressの基本 |
Lesson: |
2 of 2 |
はじめに
Webサーバは、クライアントのリクエストに対するレスポンスを生成するために、非常に多彩なメカニズムを備えています。要求されたリソースがどのクライアントにとっても同じであるため、Webサーバが処理されない静的な応答を提供するだけで十分な場合もあります。例えば、誰もがアクセスできる画像をクライアントが要求した場合、サーバは画像を含むファイルを送信すれば十分です。
しかし、レスポンスが動的に生成される場合には、サーバスクリプトに書かれた単純な行よりも優れた構造が必要になることがあります。このような場合には、Webサーバが完全なドキュメントを生成し、それをクライアントが解釈して表示できるようにすると便利です。Webアプリケーションの開発では、HTMLドキュメントはテンプレートとして作成され、サーバスクリプトとは別に管理されます。サーバスクリプトは、適切なテンプレートの所定の場所に動的データを挿入し、フォーマットされたレスポンスをクライアントに送信します。
Webアプリケーションは、しばしば静的リソースと動的リソースの両方を消費します。HTMLドキュメントは、たとえそれが動的に生成されたものであっても、CSSファイルや画像などの静的リソースへの参照を持つ場合があります。Expressがこのような需要にどのように対応するかを説明するために、まず、静的ファイルを配信するサンプルサーバをセットアップし、次に、構造化されたテンプレートベースのレスポンスを生成するルートを実装します。
静的ファイル
最初のステップは、サーバとして動作するJavaScriptファイルを作成することです。以前のレッスンで取り上げた同じパターンで、シンプルなExpressアプリケーションを作ってみましょう。まず、 server
というディレクトリを作成し、 npm
コマンドで基本コンポーネントをインストールします。
$ mkdir server $ cd server/ $ npm init $ npm install express
エントリポイントには、任意のファイル名を使用できますが、ここではデフォルトのファイル名である index.js
を使用します。以下のリストは、サーバのスタートポイントとして使用する基本的な index.js
ファイルを示しています。
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
静的ファイルを送信するのに、明示的なコードを書く必要はありません。Expressには、この目的のために、 express.static
というミドルウェアが用意されています。サーバがクライアントに静的ファイルを送信する必要がある場合は、スクリプトの最初に express.static
というミドルウェアをロードするだけです。
app.use(express.static('public'))
public
パラメータは、クライアントがリクエストできるファイルを格納するディレクトリを示します。クライアントがリクエストするパスには、 public
ディレクトリを含めてはならず、ファイル名か、 public
ディレクトリからの相対パスのみを指定します。例えば、 public/layout.css
というファイルをリクエストするには、クライアントは /layout.css
にリクエストを行います。
フォーマットされた出力
静的コンテンツの送信は簡単ですが、動的に生成されたコンテンツは大きく変化します。短いメッセージで動的なレスポンスを作成することで、開発の初期段階にあるアプリケーションを簡単にテストすることができます。例えば、HTTPの POST
メソッドで送信されたメッセージをクライアントに送り返すだけのテストルートを以下に示します。レスポンスには、メッセージの内容をフォーマットなしのプレーンテキストで再現することができます。
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
このようなルートは、Expressの学習時や診断目的で使用するのに適した例で、 res.send()
で送られる生のレスポンスで十分です。しかし、便利なサーバは、より複雑なレスポンスを生成できなければなりません。ここでは、そのようなより洗練されたタイプのルートの開発に移ります。
私たちの新しいアプリケーションは、現在のリクエストの内容を単に送り返すのではなく、各クライアントが以前のリクエストで送ったメッセージの完全なリストを保持し、リクエストがあれば各クライアントのリストを送り返します。すべてのメッセージをマージした応答もオプションとして用意されていますが、特に応答がより精巧になるにつれて、他のフォーマットされた出力モードがより適切になります。
現在のセッション中に送信されたクライアントのメッセージを受信して保存するためには、まず、クッキーやHTTPの POST
メソッドで送信されたデータを処理するための追加モジュールを組み込む必要があります。以下のサンプルサーバの目的は、 POST
経由で送信されたメッセージを記録し、クライアントが GET
リクエストを発行したときに以前に送信されたメッセージを表示することだけです。そのため、 /
パスには2つのルートがあります。最初のルートはHTTPの POST
メソッドで行われたリクエストを満たし、2番目のルートはHTTPの GET
メソッドで行われたリクエストを満たします。
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.static('public'))
const cookieParser = require('cookie-parser')
app.use(cookieParser())
const { v4: uuidv4 } = require('uuid')
app.use(express.urlencoded({ extended: true }))
// Array to store messages
let messages = []
app.post('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// If there is no uuid cookie, create a new one
if ( uuid === undefined )
uuid = uuidv4()
// Add message first in the messages array
messages.unshift({uuid: uuid, message: req.body.message})
// Collect all previous messages for uuid
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.get('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// Client's own messages
let user_entries = []
// If there is no uuid cookie, create a new one
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
// Collect messages for uuid
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
静的ファイルの設定を一番上にしているのは、 layout.css
のような静的ファイルを用意するとすぐに便利だからです。前の章で紹介した cookie-parser
ミドルウェアに加えて、このサンプルでは、メッセージを送信する各クライアントにクッキーとして渡される固有の識別番号を生成する uuid
ミドルウェアも含まれています。これらのモジュールは、サンプルサーバのディレクトリにインストールされていなければ、コマンド npm install cookie-parser uuid
でインストールできます。
messages
というグローバル配列には、すべてのクライアントが送信したメッセージが格納されます。この配列の各アイテムは、プロパティ uuid
と message
を持つオブジェクトで構成されています.
このスクリプトの新機能は、2つのルートの最後に使用されている res.json()
メソッドで、クライアントから既に送信されたメッセージを含む配列を含むJSON形式のレスポンスを生成することです。
// Send back JSON response
res.json(user_entries)
JSONはプレーンテキスト形式で、一連のデータを一つの構造にまとめることができます。この構造は連想的で、内容はキーと値で表現されます。JSONは、レスポンスがクライアントで処理される場合に特に有効です。このフォーマットを使用すると、JavaScriptのオブジェクトや配列を、サーバ上の元のオブジェクトのすべてのプロパティとインデックスを使って、クライアント側で簡単に再構築することができます。
各メッセージをJSONで構成しているため、 accept
ヘッダーに application/json
が含まれていないリクエストは拒否しています。
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
curl
はデフォルトで accept
ヘッダーに application/json
を指定しないため、新しいメッセージを挿入するためにプレーンな curl
コマンドで行われたリクエストは受け入れられません。
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt Not Found
-H "accept: application/json"
オプションは、リクエストヘッダーを変更してレスポンスのフォーマットを指定するもので、今回は指定されたフォーマットで受け入れて応答します。
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt -H "accept: application/json" ["My first message"]
他のルートを使ってメッセージを取得することも同様の方法で行われますが、今回はHTTPの GET
メソッドを使用します。
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Another message","My first message"]
テンプレート
JSONのような形式のレスポンスは、プログラム間の通信には便利ですが、ほとんどのWebアプリケーションサーバの主な目的は、人間が消費するためのHTMLコンテンツを生成することです。同じファイルの中に言語が混在していると、プログラムがエラーを起こしやすくなり、コードの保守性が損なわれるため、JavaScriptのコードの中にHTMLのコードを埋め込むことは良いアイデアではありません。
Expressは、ダイナミックコンテンツ用のHTMLを分離するさまざまな テンプレートエンジン と連携することができます。その全リストは、https://expressjs.com/en/resources/template-engines.html[Express template engines site]に掲載されています。最も人気のあるテンプレート・エンジンのひとつが Embedded JavaScript (EJS)で、動的コンテンツを挿入するための特定のタグを持つHTMLファイルを作成することができます。
他のExpressコンポーネントと同様に、EJSはサーバが稼働しているディレクトリにインストールする必要があります。
$ npm install ejs
次に、サーバスクリプトでEJSエンジンをデフォルトのレンダラーとして設定する必要があります( index.js
ファイルの先頭付近、ルート定義の前)。
app.set('view engine', 'ejs')
テンプレートを使って生成されたレスポンスは、 res.render()
関数を使ってクライアントに送信されます。この関数は、テンプレートのファイル名と、テンプレート内からアクセス可能な値を含むオブジェクトをパラメータとして受け取ります。前述の例で使用したルートは、JSONだけでなく、HTMLレスポンスを生成するように書き換えることもできます。
app.post('/', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined )
uuid = uuidv4()
messages.unshift({uuid: uuid, message: req.body.message})
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
app.get('/', (req, res) => {
let uuid = req.cookies.uuid
let user_entries = []
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
レスポンスのフォーマットは、リクエストに含まれる accept
ヘッダーに依存することに注意してください。
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
JSON形式のレスポンスは、クライアントが明示的にリクエストした場合にのみ送信されます。それ以外の場合は、 index
テンプレートからレスポンスが生成されます。同じ user_entries
配列が、JSON出力とテンプレートの両方に供給されますが、後者のパラメータとして使用されるオブジェクトは、 title: "My messages"
プロパティがあり、これがテンプレート内でタイトルとして使用されます。
HTMLテンプレート
静的ファイルと同様に、HTMLテンプレートを含むファイルは、それぞれのディレクトリに存在します。デフォルトでは、EJSはテンプレートファイルが views/
ディレクトリにあると仮定します。この例では、 index
という名前のテンプレートが使われているので、EJSは views/index.ejs
というファイルを探します。以下のリストは、サンプルコードで使用できるシンプルな views/index.ejs
テンプレートの内容です。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/layout.css">
</head>
<body>
<div id="interface">
<form action="/" method="post">
<p>
<input type="text" name="message">
<input type="submit" value="Submit">
</p>
</form>
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
</div>
</body>
</html>
最初の特別なEJSタグは、 <head>
セクションの <title>
要素です。
<%= title %>
レンダリングの際には、この特別なタグは、 res.render()
関数のパラメータとして渡されたオブジェクトの title
プロパティの値で置き換えられます。
テンプレートの大部分は従来のHTMLコードで構成されていますので、テンプレートには新しいメッセージを送信するためのHTMLフォームが含まれています。テストサーバはHTTPの GET
メソッドと POST
メソッドに対して同じパス /
で応答しますので、formタグには action="/"
と method="post"
属性が付いています。
テンプレートの他の部分は、HTMLコードとEJSのタグが混在しています。EJSは、テンプレート内の特定の目的のためのタグを持っています。
<% … %>
-
フロー制御を挿入します。このタグによって直接コンテンツが挿入されることはありませんが、JavaScriptの構造体とともに使用することで、HTMLのセクションを選択したり、繰り返したり、抑制したりすることができます。ループを開始する例:
<% messages.forEach( (message) => { %>
<%# … %>
-
コメントを定義します。その内容はパーサーによって無視されます。HTMLで書かれたコメントとは異なり、これらのコメントはクライアントからは見えません。
<%= … %>
-
変数のエスケープされた内容を挿入します。クロスサイトスクリプティング(XSS)攻撃の抜け道となるJavaScriptコードの実行を避けるために、未知のコンテンツをエスケープすることが重要です。例:
<%= title %>
<%- … %>
-
エスケープせずに変数の内容を挿入します。
HTMLコードとEJSタグが混在していることは、クライアントのメッセージがHTMLリストとしてレンダリングされているスニペットを見れば明らかです。
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
このスニペットでは、最初の <% … %>
タグが forEach
文を開始し、 message
配列のすべての要素をループしています。 <%
および %>
の区切り文字は、HTMLのスニペットを制御することができます。messages
の各要素に対して、 <li><%= message %></li>
という新しいHTMLリストアイテムが生成されます。これらの変更により、以下のようなリクエストを受信した場合、サーバはHTMLでレスポンスを送信するようになります。
$ curl http://myserver:8080/ --data message="This time" -c cookies.txt -b cookies.txt <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>My messages</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/layout.css"> </head> <body> <div id="interface"> <form action="/" method="post"> <p> <input type="text" name="message"> <input type="submit" value="Submit"> </p> </form> <ul> <li>This time</li> <li>in HTML</li> </ul> </div> </body> </html>
リクエストを処理するコードとレスポンスを表示するコードを分離することで、コードがすっきりし、チーム内で専門性の異なる人がアプリケーションの開発を分担できるようになります。例えば、Webデザイナーは、 views/
にあるテンプレートファイルと、関連するスタイルシートに注目することができます。これらは、サンプルサーバの public/
ディレクトリに格納された静的ファイルとして提供されます。
演習
-
クライアントが
assets
ディレクトリ内のファイルをリクエストできるようにするには、express.static
をどのように設定すればよいですか? -
リクエストのヘッダーに指定されているレスポンスのタイプは、Expressルートの中でどのように識別できますか?
-
res
(レスポンス)ルートパラメーターのどのメソッドが、content
というJavaScriptの配列からJSON形式のレスポンスを生成しますか?
発展演習
-
デフォルトでは、Expressのテンプレートファイルは
views
ディレクトリにあります。テンプレートファイルがtemplates
に格納されるように、この設定を変更するにはどうすればよいでしょうか。 -
クライアントがタイトルのないHTMLレスポンスを受け取ったとします(つまり、
<title></title>
)。EJS テンプレートを検証した後、開発者はファイルのhead
セクションに<title><% title %></title>
タグを見つけました。この問題の原因は何だと思われますか? -
EJSのテンプレートタグを使って、JavaScriptの変数
h2
の内容を持つ<h2></h2>
のHTMLタグを書いてください。このタグは、変数h2
が空でない場合にのみレンダリングされる必要があります。
まとめ
このレッスンでは、Express.jsが提供する、静的なレスポンスや整形された動的なレスポンスを生成するための基本的な方法について説明しました。静的なファイルのためのHTTPサーバをセットアップするための労力はほとんど必要ありません。また、EJSのテンプレート・システムは、HTMLファイルから動的コンテンツを生成するための簡単な方法を提供します。このレッスンでは、以下の概念と手順を説明しました。
-
静的ファイルのレスポンスに
express.static
を使用する方法 -
リクエストヘッダーのコンテンツタイプフィールドにマッチするレスポンスを作成する方法
-
JSON 構造のレスポンス
-
HTMLベースのテンプレートでEJSタグを使用する方法
演習の回答
-
クライアントが
assets
ディレクトリ内のファイルをリクエストできるようにするには、express.static
をどのように設定すればよいですか?サーバスクリプトには、
app.use(express.static('assets'))
の呼び出しを追加する必要があります。 -
リクエストのヘッダーに指定されているレスポンスのタイプは、Expressルートの中でどのように識別できますか?
クライアントは、
req.headers.accept
プロパティに対応するaccept
ヘッダーフィールドで、受け入れ可能なタイプを設定します。 -
res
(レスポンス)ルートパラメーターのどのメソッドが、content
というJavaScriptの配列からJSON形式のレスポンスを生成しますか?res.json()
メソッド:res.json(content)
。
発展演習の回答
-
デフォルトでは、Expressのテンプレートファイルは
views
ディレクトリにあります。テンプレートファイルがtemplates
に格納されるように、この設定を変更するにはどうすればよいでしょうか。このディレクトリは、スクリプトの初期設定で
app.set('views', './templates')
で定義できます。 -
クライアントがタイトルのないHTMLレスポンスを受け取ったとします(つまり、
<title></title>
)。EJS テンプレートを検証した後、開発者はファイルのhead
セクションに<title><% title %></title>
タグを見つけました。この問題の原因は何だと思われますか?<%= %>
タグは、<%= title %>
のように、変数の内容を囲むために使用します。 -
EJSのテンプレートタグを使って、JavaScriptの変数
h2
の内容を持つ<h2></h2>
のHTMLタグを書いてください。このタグは、変数h2
が空でない場合にのみレンダリングされる必要があります。<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>