主旨
Express 4.x が対象です。express app_name --view=pug
として生成されたファイルを見ながら、express
の仕組みを理解していく。
- この時事は、Express Generator で作成されたファイルを触って Express を理解したい2:router, pug の続きです。前回の記事では、テンプレートエンジン (pug)、ルーティングについて記述しています。
- この記事では、
app
、router
の all, get, all などのメソッドの違いと、パスの表記について記述しています。
公式ドキュメントの対応箇所は以下です:
前提
以下の記事は、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
にアクセスしたときに、下記の表示になっているものとする。
app / router の関数
よく使うのは以下の5つくらい。
- app.use
- app.get
- app.post
- app.route
- app.all
get, post, all 関数
app.get
と app.post
は、それぞれ HTTP リクエストの "GET" と "POST" に対応している。app.use
は GET も POST も拾うが、app.get
の場合は GET リクエストだけを拾う。
app.get('/', function(req, res){
res.send("GET");
});
app.use('/', function(req, res){
res.send("USE");
});
たとえば、上記のコードの場合、curl でアクセスすると、下記のような結果になる。
$ curl -X GET https://localhost:8000
GET
$ curl -X POST https://localhost:8000
USE
use と get の行の順序を逆にすると、
$ curl -X GET https://localhost:8000
USE
$ curl -X POST https://localhost:8000
USE
こうなる。use
は GET も POST も両方拾っており、get
は GET だけを拾っている。
all
はすべてのリクエストを拾う。use
と似ているが、パスにマッチしたあとの挙動が少し異なる。
var router = express.Router();
router.use('/aaa', function(req, res) {
res.send('aaa');
});
router.use('/all/bbb', function(req, res) {
res.send('bbb');
});
app.use('/use', router );
app.all('/all', router );
route 関数
route
は少し特殊で、下記のような書き方ができる。
app.route('/')
.post(function(req, res){
res.send("POST");
})
.get(function(req, res){
res.send("GET");
})
.all(function(req, res){
res.send("ALL");
});
ひとつのパスに対して、複数のリクエストを処理する関数を定義できる。上のコードの場合、下記の実行結果になる。
$ curl -X GET https://localhost:8000
GET
$ curl -X POST https://localhost:8000
POST
$ curl -X PUT https://localhost:8000
ALL
.get
, .all
などの関数の順序は、実行結果に影響する。先に書かれたリクエストの処理が優先される。下記のように記述すると、
app.route('/')
.all(function(req, res){
res.send("ALL");
})
.post(function(req, res){
res.send("POST");
})
.get(function(req, res){
res.send("GET");
});
実行結果は下記のようになり、all で定義された関数が、他の関数より優先されて実行される。
$ curl -X GET https://localhost:8000
ALL
$ curl -X POST https://localhost:8000
ALL
$ curl -X PUT https://localhost:8000
ALL
app と router の違い
get
, use
などの関数については、app
(express) と router
(experss.Router) に特に違いはない。app
と router
の大きな違いは、router
は app
の関数の引数にできるが、その逆はできないという点である。だから、app 内からミドルウェアを呼びだす時は router
を使いましょう、ということらしい。
で、app
はアプリケーションに一つしか存在できないからそうするものだと思ってたけど、下記のようなコードを書いたら問題なく動いてしまった。
var app = express();
var app2 = express();
app2.use( '/bbb', function(req, res){ res.send('app2')});
app.use( '/aaa', app2 );
実行すると、下記のようになる。
$ curl -X GET http://localhost:8000/aaa
エラー
$ curl -X GET http://localhost:8000/aaa/bbb
app2
$ curl -X GET http://localhost:8000/bbb
エラー
app2
を router
に変更しても同じ挙動になる。あれ、じゃあ router
使う意味って何だろう。そもそもapp
を二つ作ったら、何か混乱が起こるかもしれないと思っていたけど、上の結果を見ると問題なさそう。先に作った app
の use
がちゃんとルートを認識していて、app2
は app
の子になっていて、問題なく動いている。(app2
は exports されてないから当然といえば当然)
いろいろ調べてみた結果、明確な情報は見つけられなかった。おそらくだけど、app
より router
のほうがコンパクトなオブジェクトになっているので、app
を使うより router
を使うほうがリソースが節約できる、ということだと思う(完全に推測です)。
余談: app.use のパス表記
パス (ルートパス) とは、たとえば http://localhost:8000/xxxx/yyyy/zzz.html?aaa=bbb&ccc=ddd という URL でサーバにアクセスされたときに、 /xxx/yyy/zzz.html
の部分を解析するために使われる文字列のことを言う。app.use
や app.get
などのルートハンドラーの第一引数として指定される。
たとえば、下記の /
、/users
はいずれも「ルートパス」にあたる。
app.use('/', indexRouter);
app.use('/users', usersRouter);
この「(ルート)パス」の表記は、bash などで使える正規表現や、XPATH とも違うし、JSON のパス表記とも微妙に違うという、やや独特な表記法を使うようになっている(どうしてこうなったの?)。そのため、要点を押さえていないと、まったく想定していない URL にマッチしてしまったり、マッチさせたい URL にどうしてもマッチしてくれなかったりする。
ここでは、間違いやすいルートパスの表記についてのみ記載する。ルートパス表記に関する詳しいドキュメントは下記にある。
ルーティングに関する公式のドキュメント:
ルートパスのマッチングの詳細:
なお、この記事や関連記事では、ルートパスのことを単に「パス」と書いている(混乱がない限り)。
余談: app.use のパスのマッチング
以下は、PORT=8000 yarn stat
としてアプリを実行しているものとする。なお、以下は app.use
を使用する場合であって、後述するように app.all
や app.get
などは下記とは異なるマッチをする。これは結構な罠なので要注意!
-
app.use('/', ... )
はあらゆる URL にマッチする。 -
app.use( ... )
のようにパスを記述しない場合も、app.use('/', ... )
と記述されたものとして、あらゆる URL にマッチする。 -
app.use('/aaa', ... )
は "/aaa" という URL か、"/aaa/" で始まる URL にマッチする。 - http://localhost:8000/aaa
- http://localhost:8000/aaa/bbb
- http://localhost:8000/aaa/bbb/ccc.html
- 下記にはマッチしない。
- http://localhost:8000/bbb/aaa
- http://localhost:8000/aaaa
-
app.use('/aaa/', ... )
はapp.use('/aaa', ... )
と書くのと一緒。 -
app.use('/aaa*', ... )
は "/aaa" という文字列で始まるすべての URL にマッチする。"/aaa" の直後に別の文字列が続いてもいい。下記のいずれもマッチする。 - http://localhost:8000/aaa
- http://localhost:8000/aaabbb
- http://localhost:8000/aaa.html
- http://localhost:8000/aaaaa/bbb
- http://localhost:8000/aaabb/ccc.html
- 下記にはマッチしない。
- http://localhost:8000/xxxaaa
- http://localhost:8000/aa
- http://localhost:8000/bbb/aaa
-
app.use('/aaa*/bbb', ...)
は URL が "/aaa" で始まって、末尾が厳密に "/bbb" で終わる場合にのみマッチする。下記の URL にはマッチする。 - http://localhost:8000/aaa/bbb
- http://localhost:8000/aaaxxx/bbb
- http://localhost:8000/aaaxxx/ccc/bbb
- 下記にはマッチしない。
- http://localhost:8000/xxxaaa/bbb
- http://localhost:8000/aaa/bbbccc
- http://localhost:8000/aaa.html
-
app.use(/aaa/, ...)
は "aaa" を含むあらゆる URL にマッチする。下記のいずれもマッチする。 - http://localhost:8000/aaa
- http://localhost:8000/bbb/aaa
- http://localhost:8000/ccc/aaa.html
- http://localhost:8000/aaaaaaa
- http://localhost:8000/aaabbb
最後の例のように、正規表現をパスとして使う時はシングルクオート ''
は書かない。一見して、正規表現を表わす /
がパスの区切りの /
と同じなので、つい '/aaa/'
と書いてしまったりする。逆に '/aaa/'
とするべきところを /aaa/
と書いてしまってハマる(ハマった)。
正規表現まわりについては、言語によって違う部分も多いので公式のドキュメントをよく読もね(自戒)。
下記も参考までに;
余談: Router を使う場合のパス表記
app.use
と router.use
を使って、二段階のパスのマッチをするように処理を書くと、app.use
に直接 function
を書いた場合とは異なる挙動をする。
以下、例示してみる、
app.use('/aaa', function(req, res){
res.send('超Lチカ');
});
routes/app.js
に上記のような /aaa
のパスに対する処理の記述がある場合、下記の URL にアクセスしたときのみ、ブラウザに "超Lチカ" と表示される。
ここで、下記のように書きかえて実行してみる。
var router = express.Router();
router.use(`/`, function(req, res){
res.send('超Lチカ');
});
app.use('/aaa', router );
このようにしても、http://localhost:8000/aaa にアクセスしたときに "超Lチカ" と表示される。
これは、以下のように処理がなされるためである。
-
app.use('/aaa', ...)
でまず/aaa
にマッチする。 - URL がそのまま
router
に送られる、 -
router
側のrouter.use('/', ... )
の行では、送られてきた URL から、送り元(親)の app でマッチした文字列("/aaa" の部分)を取り除いた上で、パスのマッチ処理をする。 - その結果 "/" にマッチする。(URL が空の場合は "/" と見なされる)
-
function
内のres.send
が実行される。
これをさらに、下記のように書きかえてみる。
var router = express.Router();
router.use(`/bbb`, function(req, res){
res.send('超Lチカ');
});
app.use('/aaa', router );
この場合、http://localhost:8000/aaa/bbb にアクセスしたときのみ "超Lチカ" と表示される。一方で http://localhost:8000/aaa はエラーが表示される。
app.use('/aaa', ...)
で "/aaa" にマッチしているにも関わらずエラー表示が出てしまうのは、次のように処理がなされるからである。
-
app.use('/aaa', router );
で第一引数の '/aaa' が URL の "/aaa" の文字列にマッチして、router
が呼びだされる。 - URL がそのまま
router
に送られる、 -
router
側のrouter.use('/', ... )
の行では、送られてきた URL から、送り元(親)の app でマッチした文字列("/aaa" の部分)を取り除いた上で、パスのマッチ処理をする。 -
router
にはrouter.use('/', ... )
という記述がない。つまり、"/" というパスにマッチする処理がないため、何の処理も行なわれない。(空の URL は "/" と見なされる) - この結果
app.js
に処理が戻る。app.use('/aaa', router );
の行の処理は、この時点で終わる。 -
app.js
の下記のコードで "/aaa" にマッチして (パスが指定されていないため、すべてのパスにマッチする)、404 エラーが生成される。
app.use(function(req, res, next) {
next(createError(404));
});
- 最終的に、
res.render(
error)
の行まで処理が進んで、エラー画面がブラウザに表示される。
app.use
側でパスにマッチしても、処理先の router
側に書かれているいずれのパスにもマッチしない場合は、あたかも app.use
で何もマッチしなかったかのような挙動になる。
この例で、"/aaa" に対しても処理を行ないたい場合は、router
側に "/" のパスに対する処理を書くか、app.js
側にさらに "/aaa" に対する処理を書くかの、いずれかを行なう必要がある。
前者の場合は、下記のようにする。
var router = express.Router();
router.use(`/bbb`, function(req, res){
res.send('超Lチカ');
});
// 以下の行を追加
router.use(`/`, function(req, res){
res.send('ただのLチカ');
});
app.use('/aaa', router );
後者の場合は、下記のようにする。
var router = express.Router();
router.use(`/bbb`, function(req, res){
res.send('超Lチカ');
});
app.use('/aaa', router );
// 以下の行を追加
app.use(`/aaa`, function(req, res){
res.send('ただのLチカ');
});
いずれの場合でも、http://localhost:8000/aaa にアクセスしたときに「ただのLチカ」と表示されるようになる。同じ結果にはなるけど、パスの処理を階層化するという観点からは、前者のほうが良い気がする。
app.use
の第二引数で router
を指定した場合は、マッチの処理そのものが router
側に移譲される形になる。router
側でのマッチ処理の結果、いずれのパスにもマッチしないと、あたかも app.use
自体でマッチしなかったかのように、処理がスルーされるという結果になる。
このような、app
と router
のマッチの挙動を理解していないと、予想外のマッチが起こったり、どうしてもマッチしないというバグに繋りやすい。一方で、この仕組みを十分に理解していれば、app.js
の記述を減らして、パスのマッチの記述を router 側のコードに効率的に分散することができる。
余談: app.get や app.all でのパスのマッチ
app.get
や app.all
は、app.use
とマッチのパスの処理がかなり異なっている。
たとえば、下記のコードで、curl でいろいろリクエストを送ると…
app.get('/aaa', function(req, res){
res.send('getのLチカ');
});
app.use('/bbb', function(req, res){
res.send('useのLチカ');
});
$ curl -X GET http://localhost:8000/aaa
getのLチカ
$ curl -X GET http://localhost:8000/bbb
useのLチカ
$ curl -X GET http://localhost:8000/aaa/ccc
-> エラー
$ curl -X GET http://localhost:8000/bbb/ccc
useのLチカ
このようになる。これは、下記の理由による。
-
app.use('/aaa', ...)
は "/aaa", "/aaa/bbb", "/aaa/ccc/bbb" のいずれにもマッチする。 -
app.get('/aaa', ...)
は "/aaa" にしかマッチしない。
次に、下記のようなコードを書いて curl で試してみる。
var router = express.Router();
router.get('/', function(req, res) {
res.send('get:/');
});
router.use('/', function(req, res) {
res.send('use:/');
});
app.get('/get', router );
$ curl -X GET http://localhost:8000/get
use:/
router.get('/'、 ...)
がスルーされて use
でマッチしている。一方で、下記のようにすると get:/get
が表示される。
var router = express.Router();
router.get('/', function(req, res) {
res.send('get:/');
});
router.get('/get', function(req, res) {
res.send('get:/get');
});
router.use('/', function(req, res) {
res.send('use:/');
});
router.use('/get', function(req, res) {
res.send('use:/get');
});
app.get('/get', router );
$ curl -X GET http://localhost:8000/get
get:/get
$ curl -X GET http://localhost:8000/get/get
エラー
さらに、下記のように app.get
を app.use
にすると、'get:/' が表示される。
var router = express.Router();
router.get('/', function(req, res) {
res.send('get:/');
});
router.get('/get', function(req, res) {
res.send('get:/get');
});
router.use('/', function(req, res) {
res.send('use:/');
});
router.use('/get', function(req, res) {
res.send('use:/get');
});
app.use('/use', router );
$ curl -X GET http://localhost:8000/use
get:/
$ curl -X GET http://localhost:8000/use/get
get:/get
これらのことから、app.get
と app.use
のパスのマッチの処理は、以下のように行なわれていていると推測できる。
-
app.use
は、'/use' のパス記述で "/use" にも "/use/get" にもマッチする。 -
app.get
は、'/get' のパス記述で "/get" にはマッチするが "/get/get" にはマッチしない。 -
app.use
の第二引数のrouter
の中では、app
側でマッチした文字列 (が取り除かれたかのような状態で扱われる。(router
側では "/use/get" から先頭の "/use" が取り除かれて、"/get" であると見なされる) - ただし
router.use
に限り、app.get( path, router )
とされていても、app.use( path, router )
とされていても、app
側でマッチした文字列が URL から取り除かれているかのようにして、パスのマッチを行なう。 -
router.get
では、app.get( path, router )
とされたか、app.use( path, router )
とされたかでパスのマッチの挙動が変わる。
このように、複雑なことになっている。そもそも、get や all は、パスの処理を行なう router を呼びだすことは前提とされてない実装になってるんだと思う (ドキュメントの use 関数の記述にそんな雰囲気を感じる…むしろ明言してほしいところ)。
app.get
の中で、パスの処理をするような router
呼び出すと、上記のようにパスの評価で混乱がおこるので、全くオススメできない。app
内で get
を使う場合は「内部でパスの処理を行なわず、なおかつ next で処理を続けるようなミドルウェア(関数)」 (logger のような)の使用に限定しておいたほうが安全そうでだ。(下記参照)
なお、app.all
や app.post
なども app.get
と同じ挙動をする。
(Express 5.x になったらこのあたりは整理されるのだろうか…)
余談: URL 中のファイル名を変数に読みこむ
マッチしたパスの一部を、変数として取り出すこともできる。
app.use('/:val', function(req, res){
res.send(req.params.val);
});
app.js
の app.use('/', ... )
の記述を上記のように書きかえて、PORT=8000 yarn stat
としてから、http://localhost:8000/hoge にブラウザでアクセスすると、下記のように表示される。
パスの中に :val
と記述しておくと、その部分にマッチした文字列をプログラム中から req.params.val
という変数で参照できるようになる。変数名は val
でなくても良い。変数名には英数文字とアンダースコア _
が使える。このような、URL の中から文字列を取りだせる仕組みのことは、ルートパラメータと呼ばれる。
ルートパラメータは、パス表記の中に複数書くこともできる。たとえばこんな感じ。
app.use('/aaa/:val/bbb/:file', function(req, res){
res.send('val: ' + req.params.val + ', file: ' + req.params.file );
});
ルートパラメータを使うことで、URL で指定されたファイル名を読み出すようなこともできる。
詳しくは下記を参照のこと。