主旨
Experss 4.x が対象です。express app_name --view=pug として生成されたファイルを見ながら、express の仕組みを理解していく。
- この時事は、この記事の続きです。前回の記事では、
yarn startコマンドとpackage.jsonとの関係、static ファイルとpublicフォルダの関係、app.use()の評価される順序、path.joinと__dirnameについて記述している。
この記事では、router (./routes 以下の .js ファイル) と views 以下にある .pug について記述している。
前提
以下の記事は、express-generator を使って下記のコマンドで app.js 以下のファイルを生成していると想定している。
$ express appname --view=pug
生成されたファイルは以下の通り。
project_dir
├── app.js # アプリのメインファイル
├── bin
│ └── www # yarn start 時に node bin/www として実行されるファイル
├── package.json # ライブラリ等の依存関係やバージョン情報を格納したファイル
├── public # static なファイルを置くフォルダ
│ ├── images # http://localhost:8000/images
│ ├── javascripts # http://localhost:8000/javascripts
│ └── stylesheets # http://localhost:8000/stylesheets
│ └── style.css # http://localhost:8000/stylesheets/style.css
├── routes # router (ミドルウェア) 置き場
│ ├── index.js # http://localhost:8000/ (トップページ)
│ └── users.js # http://localhost:8000/users
└── views # テンプレートファイル置き場
├── error.pug # エラー時のテンプレート
├── index.pug # index.js 用のテンプレート
└── layout.pug # index.pug や error.pug に読みこまれるテンプレート
実行時には 8000 番ポートを指定して使うものとする。
$ PORT=8000 yarn start
実行後、ブラウザで http://localhost:8000 にアクセスしたときに、下記の表示になっているものとする。
routes/index.js と app.js の関係
routes/index.js は生成時は以下のような内容になっている。
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
index.js は app.js の下記の記述によって呼び出される。
var indexRouter = require('./routes/index');
app.use('/', indexRouter);
上の require の行で index.js を app.js に読みこみ、下の行 (app.use() で http://localhost:8000/ にアクセスがあったときに、index.js を実行するようにしている。いずれの行が欠けても、プログラムは正常に動作しない。
試しに app.use('/', indexRouter); をコメントアウトして PORT=8000 yarn start を実行し、http://localhost:8000/ にアクセスすると、
このように、Express ページは表示されなくなる。
require 側を消すと、indexRouter が未定義となって、アプリの実行自体がエラーになる。
$ PORT=8000 yarn start
/Users/hoge/Documents/github/express_sample/app.js:22
app.use('/', indexRouter);
^
ReferenceError: indexRouter is not defined
app.js と routes/index.js の関係
再び、app.js を下記の状態に戻してからスタート。
var indexRouter = require('./routes/index');
app.use('/', indexRouter);
今度は、app.use 側を以下のように変更してみる。
app.use('/hoge', indexRouter);
これで PORT=8000 yarn start を実行し、http://localhost:8000/ にアクセスすると、
エラーになる。これは、サーバのルート / にマッチするパスの記述を消したから。かわりに、http://localhost:8000/hoge にはアクセスできる。
このように、app.use の一つ目の引数のパスを変更することで、index.js を表示するパスを変更できる。
ちなみに、下記のようにして複数のパスを index.js に紐付けることもできる。
var indexRouter = require('./routes/index');
app.use('/', indexRouter);
app.use('/hoge', indexRouter);
app.use('/foo/bar', indexRouter);
それぞれ、下記の URL でアクセスできる。
このような、パスと実行ファイル (index.js ) を紐付けることをルーティングと呼ぶ。index.js のような、app.use の二つ目の引数で呼びだされる外部モジュールや関数のことはミドルウェアと呼ばれる。
このように、express-generator では、
-
app.jsにルートの記述をする (ルーティング) -
routes以下に、ページを表示したり http クエストを処理するミドルウェアの.jsファイルを置く
という構成になるように、ファイルが生成される。あくまで、express-generator がデフォルトでこのような構成でファイルを生成するというだけで、このような構成でしか web アプリが記述できない、というわけではない。アプリの記述に慣れてきたら、ディレクトリやファイルの構成を都合のよいようにカスタマイズしても問題はない。
routes/index.js と views/index.pug の関係
index.js の中身を見ていく。デフォルトでは下記のようになっている。
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
ブラウザにアクセスしたときに表示される "Express" ページは、
function(req, res, next) {
res.render('index', { title: 'Express' });
}
この関数で生成されている。試しに 'Express' の表記を '超Lチカ' に変更し、PORT=8000 yarn start しなおしてから http://localhost:8000 にアクセスすると、
このように、表示される文字が変更される。res.render() 関数の二つ目の引数で指定するオブジェクトの、title: のキーに対応する値によって、文字が変更されていることは分かる。しかし、title を変更することで、表示される文字の一部を変更することはできるが、ページのレイアウト自体を変更することはできない。ページのレイアウトを変更するためには、一つ目の引数を理解する必要がある。
一つ目の引数の index に対応するのは、views/index.pug ファイルである。views/index.pug ファイルの中身は以下のようになっている。
extends layout
block content
h1= title
p Welcome to #{title}
これは、HTML を簡略化した記法で書かれた「webページのテンプレート」で、pug というテンプレートエンジンによって、ページが読みこまれるときには HTML に変換されて、ブラウザに送信される。上記の記述を HTML に書き直すとすると、下記のようになる。
<!DOCTYPE html>
<html>
<head>
<title>title</title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
<h1>title</h1>
<p>Welcome to #{title}</p>
</body>
</html>
<!DOCTYPE html>
見やすいようにインデントや改行を入れているけど、実際にテンプレートエンジンで生成される出力には改行やインデントはなされない。
元になっている pug ファイルは、おおむね下記のルールで HTML を簡略化して記述できる。
- ひとつのタグをひとつの行で書く
- タグの入れ子状態をインデントの深さで表わす
- タグのパラメータは括弧内に記述する (link(rel="stylesheets") のように)
- インデントの終わりが「タグ閉じ」に相当する
詳しい説明のある記事はいくつかあるので、そちらを参考にされたい。
これらに加えて、変数も使うことができる。上の例では、プログラムの実行時に title の部分に res.render() の二番目の引数で指定されたオブジェクトの title: の値が挿入される。routes/index.js の場合、title: の値は Express となっているので、views/index.pug と routes/index.js から生成される HTML ファイルは下記のようになる。
<!DOCTYPE html>
<html>
<head>
<title>超Lチカ</title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
<h1>超Lチカ</h1>
<p>Welcome to 超Lチカ</p>
</body>
</html>
<!DOCTYPE html>
views/index.pug に含まれる #{title} という文字列は、routes/index.js で指定された 超Lチカ という文字列に置き変わる。.pug ファイルに #{name} という形式の記述は pug においては name という変数であると解釈され、res.render の引数を使って、対応する変数に埋め込む文字列を変更できる。
試しに、views/index.pug と routes/index.js を下記のように書きかえてみると…
extends layout
block content
h1= title
p Welcome to #{world}
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: '超Lチカs', world: "the World!" });
});
module.exports = router;
こうなる。
また、index.pug 内にある h1 や p といったタグの直後に = を書くと、その直後にある文字列は #{} が付いている変数であると解釈される。たとえば、上の例では、title= のタグに = が付いているので、直後にある title という文字列は変数として扱われ、title= title という記述は title 超Lチカ という記述に置きかわる。
変数名に普通の単語を使うと、ただの文字列なのか変数なのか分かりにくくなってバグが起きやすい。たとえば、変数名にはアンダースコアを使って _title のように記述するとか、すべての変数は常に #{title} のように #{} 付きで表記するというようなルールを決めて、文字列との区別が容易なようにしておくと、想定外のバグを防ぎやすい。
views/index.pug と views/layout.pug の関係
先に示したように、views/index.pug と routes/index.js から生成されるファイルには、あきらかに views/index.pug に含まれない記述がある。
<!DOCTYPE html>
<!-- ここから -->
<html>
<head>
<title>超Lチカ</title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<!-- ここからまでは index.pug には無い -->
<body>
<h1>超Lチカ</h1>
<p>Welcome to 超Lチカ</p>
</body>
</html> <!-- ここと -->
<!DOCTYPE html> <!-- ここも index.pug には無い部分 -->
index.pug に無い部分は、views/layout.pug から読みこまれている。
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
extends layout
block content
h1= title
p Welcome to #{title}
views/index.pug の冒頭にある extends layout という記述によって、views/layout.pug の内容を views/index.pug に読み込む指示をしている。これによって、views/index.pug は下記のように記述されてるかのように扱われる。
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
h1= title
p Welcome to #{title}
block の展開のされ方はやや複雑で、次のルールで extends で読みこんだ側のファイル内に展開される。
- 読み込み先に同名の block が存在しない場合は、そのまま読み込まれ。
- 読み込み先に同名の block が存在する場合は、読み込み先の block の記述で上書きされる。
たとえば、views/index.pug と views/layout.pug が下記の状態だとする。
doctype html
html
head
block scripts
script src="hoge.js"
body
block main
h1 ただのLチカ
extends layout
block main
h1 超Lチカ
views/index.pug の冒頭に extends layout という記述があるので、まず views/layout.pug が読みこまれて、出力されるファイルのベースは下記のようになる。
doctype html
html
head
block scripts
script src="hoge.js"
body
block main
h1 ただのLチカ
次に views/layout.pug の中から、views/index.pug に書かれている block main と同名の block があるかを探す。この例の場合、block main が存在しているので、views/index.pug側にある block main の記述で、出力ファイルの中にある block main の記述を置きかえる。
doctype html
html
head
block scripts
script src="hoge.js"
body
block main
h1 超Lチカ
結果的に h1 ただのLチカ が h1 超Lチカ に置きかえられる。block scripts は views/index.pug に存在しないのでそのまま残る。
このように、block と extends の記述を使うことで、「テンプレートのテンプレート」のようなものを作ることができる。
余談: include を使って pug ファイルを読み込む
pug ファイルから別のファイルを読み込む方法として、extends の他に include という記述がある。
doctype html
html
head
include scripts
body
block main
h1 ただのLチカ
script src="hoge.js"
include でファイル名を指定すると、その場所にファイルの内容がそのまま読みこまれる。上の例の場合は、出力は下記になる。
doctype html
html
head
script src="hoge.js"
body
block main
h1 ただのLチカ
extends とは違って、単純にファイルを読み込むだけ。include を使うことで、大きな pug ファイルを分割して管理することができる、
この他、if 文を使った条件分岐や、for 文を使ったループなども書ける。また pug ファイル内で変数を使うこともできる。詳しくは下記が参考になる。
- https://uetani33.net/pug-howto/#toc_id_5_1
- https://du-masa.github.io/study-frontend/pug/pug-lang.html
余談: 標準ミドルウェアとサードパーティミドルウェア
app.js の先頭のほうにある app.use 群も、ミドルウェアを使うためのコードになっている。
var cookieParser = require('cookie-parser');
var logger = require('morgan');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
これらの app.use はパスの指定がない。パスの指定がない場合は、「あらゆるアクセス(リクエスト)に対して実行する」ものと解釈される、つまりこれらのミドルウェアは、すべてのアクセスに対して自動的に適用される。ただし各ミドルウェアは、パスがマッチしていても、それが「適用される条件を満たしているときにのみ」実行されるように実装されている。
たとえば、expres.static の記述は、 ./public 以下にあるファイル名と同じファイル名(パス)が、/ 直下に指定されていた場合にのみ実行される。ファイル名に対応するファイルが存在するときは、./public 以下にあるファイルの内容をそのままブラウザに返す。./public 以下に該当するファイルがない場合は、何も処理をせずに次の行の処理をする。
また logger (morgan) のミドルウェアは、サーバのいずれのパスへのアクセスに対しても機能し、アクセスされたパス名、レスポンスコード、処理に要した時間などを、console に表示する(例えば下記のように)。
GET / 200 23.091 ms - 195
GET /stylesheets/style.css 304 0.427 ms - -
GET /users 304 0.809 ms - -
express.xxx のようにして使うことができるミドルウェアは「標準ミドルウェア」と呼ばれ、require で読みこんで使用するミドルウェアは「サードパーティミドルウェア」と呼ばれる。サードパーティのミドルウェアについては、require 行を記述した後に yarn install することで、node_module ディレクトリ以下にインストールされ、プログラム中で使えるようになる。