4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

主旨

Express 4.x が対象です。express app_name --view=pug として生成されたファイルを見ながら、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 リクエストだけを拾う。

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

.js
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 は少し特殊で、下記のような書き方ができる。

.js
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 などの関数の順序は、実行結果に影響する。先に書かれたリクエストの処理が優先される。下記のように記述すると、

.js
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
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
エラー

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 にどうしてもマッチしてくれなかったりする。

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

ルーティングに関する公式のドキュメント:

ルートパスのマッチングの詳細:

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

余談: 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
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 が実行される。

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

app.js
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 エラーが生成される。
.js
app.use(function(req, res, next) {
  next(createError(404));
});
  • 最終的に、res.render(error) の行まで処理が進んで、エラー画面がブラウザに表示される。

app.use 側でパスにマッチしても、処理先の router 側に書かれているいずれのパスにもマッチしない場合は、あたかも app.use で何もマッチしなかったかのような挙動になる。

この例で、"/aaa" に対しても処理を行ないたい場合は、router 側に "/" のパスに対する処理を書くか、app.js 側にさらに "/aaa" に対する処理を書くかの、いずれかを行なう必要がある。

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

app.js
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 );

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

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

このような、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 で試してみる。

.js
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 が表示される。

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

.js
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.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 で指定されたファイル名を読み出すようなこともできる。

詳しくは下記を参照のこと。

つづく?

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?