Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8926

Express Generator で作成されたファイルを触って Express を理解したい3:use,get,post,all,パスの表記

$
0
0

主旨

Express 4.x が対象です。express app_name --view=pugとして生成されたファイルを見ながら、expressの仕組みを理解していく。

公式ドキュメントの対応箇所は以下です:
- Express アプリケーションで使用するミドルウェアの作成

前提

以下の記事は、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.getapp.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と似ているが、パスにマッチしたあとの挙動が少し異なる。

varrouter=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) に特に違いはない。approuterの大きな違いは、routerappの関数の引数にできるが、その逆はできないという点である。だから、app 内からミドルウェアを呼びだす時は routerを使いましょう、ということらしい。

で、appはアプリケーションに一つしか存在できないからそうするものだと思ってたけど、下記のようなコードを書いたら問題なく動いてしまった。

app.js
varapp=express();varapp2=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
エラー

app2routerに変更しても同じ挙動になる。あれ、じゃあ router使う意味って何だろう。そもそもappを二つ作ったら、何か混乱が起こるかもしれないと思っていたけど、上の結果を見ると問題なさそう。先に作った appuseがちゃんとルートを認識していて、app2appの子になっていて、問題なく動いている。(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.useapp.getなどのルートハンドラーの第一引数として指定される。

たとえば、下記の //usersはいずれも「ルートパス」にあたる。

app.js
app.use('/',indexRouter);app.use('/users',usersRouter);

この「(ルート)パス」の表記は、bash などで使える正規表現や、XPATH とも違うし、JSON のパス表記とも微妙に違うという、やや独特な表記法を使うようになっている(どうしてこうなったの?)。そのため、要点を押さえていないと、まったく想定していない URL にマッチしてしまったり、マッチさせたい URL にどうしてもマッチしてくれなかったりする。

ここでは、間違いやすいルートパスの表記についてのみ記載する。ルートパス表記に関する詳しいドキュメントは下記にある。

ルーティングに関する公式のドキュメント:
- https://expressjs.com/ja/guide/routing.html

ルートパスのマッチングの詳細:
- https://www.npmjs.com/package/path-to-regexp

なお、この記事や関連記事では、ルートパスのことを単に「パス」と書いている(混乱がない限り)。

余談: app.use のパスのマッチング

以下は、PORT=8000 yarn statとしてアプリを実行しているものとする。なお、以下は app.useを使用する場合であって、後述するように app.allapp.getなどは下記とは異なるマッチをする。これは結構な罠なので要注意!

最後の例のように、正規表現をパスとして使う時はシングルクオート ''は書かない。一見して、正規表現を表わす /がパスの区切りの /と同じなので、つい '/aaa/'と書いてしまったりする。逆に '/aaa/'とするべきところを /aaa/と書いてしまってハマる(ハマった)。

正規表現まわりについては、言語によって違う部分も多いので公式のドキュメントをよく読もね(自戒)。

下記も参考までに;
- 基本的なルーティング

余談: Router を使う場合のパス表記

app.userouter.useを使って、二段階のパスのマッチをするように処理を書くと、app.useに直接 functionを書いた場合とは異なる挙動をする。

以下、例示してみる、

app.js
app.use('/aaa',function(req,res){res.send('超Lチカ');});

routes/app.jsに上記のような /aaaのパスに対する処理の記述がある場合、下記の URL にアクセスしたときのみ、ブラウザに "超Lチカ" と表示される。

ここで、下記のように書きかえて実行してみる。

app.js
varrouter=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が実行される。

これをさらに、下記のように書きかえてみる。

app.js
varrouter=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" に対する処理を書くかの、いずれかを行なう必要がある。

前者の場合は、下記のようにする。

app.js
varrouter=express.Router();router.use(`/bbb`,function(req,res){res.send('超Lチカ');});// 以下の行を追加router.use(`/`,function(req,res){res.send('ただのLチカ');});app.use('/aaa',router);

後者の場合は、下記のようにする。

app.js
varrouter=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自体でマッチしなかったかのように、処理がスルーされるという結果になる。

このような、approuterのマッチの挙動を理解していないと、予想外のマッチが起こったり、どうしてもマッチしないというバグに繋りやすい。一方で、この仕組みを十分に理解していれば、app.jsの記述を減らして、パスのマッチの記述を router 側のコードに効率的に分散することができる。

余談: app.get や app.all でのパスのマッチ

app.getapp.allは、app.useとマッチのパスの処理がかなり異なっている。

たとえば、下記のコードで、curl でいろいろリクエストを送ると…

app.js
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 で試してみる。

varrouter=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が表示される。

varrouter=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.getapp.useにすると、'get:/' が表示される。

varrouter=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.getapp.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.allapp.postなども app.getと同じ挙動をする。

(Express 5.x になったらこのあたりは整理されるのだろうか…)

余談: URL 中のファイル名を変数に読みこむ

マッチしたパスの一部を、変数として取り出すこともできる。

app.js
app.use('/:val',function(req,res){res.send(req.params.val);});

app.jsapp.use('/', ... )の記述を上記のように書きかえて、PORT=8000 yarn statとしてから、http://localhost:8000/hogeにブラウザでアクセスすると、下記のように表示される。

パスの中に :valと記述しておくと、その部分にマッチした文字列をプログラム中から req.params.valという変数で参照できるようになる。変数名は valでなくても良い。変数名には英数文字とアンダースコア _が使える。このような、URL の中から文字列を取りだせる仕組みのことは、ルートパラメータと呼ばれる。

ルートパラメータは、パス表記の中に複数書くこともできる。たとえばこんな感じ。

app.js
app.use('/aaa/:val/bbb/:file',function(req,res){res.send('val: '+req.params.val+', file: '+req.params.file);});

image.png

ルートパラメータを使うことで、URL で指定されたファイル名を読み出すようなこともできる。

詳しくは下記を参照のこと。
- https://expressjs.com/ja/guide/routing.html

つづく?


Viewing all articles
Browse latest Browse all 8926

Trending Articles