今回から, Express で, バックエンドを作ります。Object Relational Mapping (ORM) としては Sequelize を使います。
[2021-03] Express v4 は Node.js のための web framework で非常にメジャーだが、機能がショボく、にもかかわらず遅い。次世代と目されているのは, Express.js 作者が開発している後継プロジェクト, Koa v2; koajs/koa: Expressive middleware for node.js using ES2017 async functions
あと有力なのは、JavaScript の割には爆速フレームワーク Fastify; Fastify, Fast and low overhead web framework, for Node.js (他の言語のそれに比べれば断然遅いことに注意。)
ほかには Restify, Hapi もあるが、上記二つだけ調べればよい。
ORM についても, Sequelize は冗長で、生産性が低い。TypeScript 前提だと TypeORM, Prisma があるが、plain JavaScript でよさそうなのは Bookshelf.js. Knex SQLクエリビルダがよい。
[/ 2021-03 ここまで]
基本的な REST API を作る。URL と API との対応については best practice がある。それに合わせるのがよい。
フロントエンドで処理が完結するアクションと、バックエンドにメッセージを投げるものがある。例えば、写真コレクションに関する URL は次のようになる。
HTTP method として URL の設計
HTTP method パス コントローラ#アクション 目的
GET
/photos
サーバ: photos#index
すべての写真の一覧を表示
GET /photos/new
フロントエンドで処理
写真を1つ作成するためのHTMLフォームを表示
POST
/photos
サーバ: photos#create
写真を1つ作成する. 201 Created
GET
/photos/:id
photos#show
特定の写真を表示する 200 OK.
見つからないとき 404 Not Found.
GET
/photos/:id/edit
フロントエンドで処理
写真編集用のHTMLフォームを表示
PUT
/photos/:id
サーバ: photos#update
特定の写真を更新する. 見つからないとき 404 Not Found. 更新に失敗 405 Method Not Allowed.
DELETE
/photos/:id
サーバ: photos#destroy
特定の写真を削除する. 見つからないとき 404 Not Found. 更新に失敗 405 Method Not Allowed.
PUT
の代わりに PATCH
を使うこともある。PATCH
method は RFC 5789 (2010年3月) で定められているが, 改訂 HTTP/1.1 (RFC 7231; 2014年6月) には取り込まれなかった。傍流と思う。
内容は前回見たので、今回は, フロントエンドからバックエンドを呼び出す部分。
簡単な API
クラスを作る。
fetch()
バックエンドを呼び出すのも HTTP プロトコルを使う (Ajax)。XMLHttpRequest
は廃れた。現代では fetch()
を使う。IDL宣言は次のとおり;
Promise<Response> fetch(RequestInfo input, optional RequestInit init = {});
2引数として input
に文字列による URL, init
にオプションを与えて呼び出すか, 1引数で使って, Request
オブジェクトを与えるか, のいずれか。どちらも同じことができるが、前者のほうが書きやすい。
フロントエンドのプログラム (JavaScript) を配布したサーバとバックエンドのサーバとで, ホスト名が違うかあるいはポート番号が違うと, cross-origin になる。Webブラウザ上で動く JavaScript からバックエンドに対して直接リクエストを発出する際, cross-origin 扱いになると、制限が多い。
Cross-origin では、原則として、HTML form
要素で送信可能な内容を超えるリソースの共有が禁止される。明示的に許可する仕組みが CORS プロトコルだ。
fetch()
の第2引数に与える mode:
は, "navigate"
, "same-origin"
, "no-cors"
, "cors"
または "websocket"
のいずれかを指定できます。デフォルト値は "no-cors"
です。
HTTP method = PUT
や content-type
ヘッダを使う場合、CORS が必要になる。
Cross-origin であることを宣言する mode:"cors"
としたうえで, サーバ側で Access-Control-Allow-Origin
, Access-Control-Allow-Headers
ヘッダなど追加対策です。
しかし、バックエンドから見ると、フロントエンドから来るリクエストはいくらでも改竄できるので、そもそも信用してはなりません。そうすると、バックエンドから見る限り、cross-origin 対策は重要ではありません。
したがい、簡単のため、次のようにしてもいいでしょう。
package.json
ファイルに次のように, バックエンドの URLを書く
"proxy": "http://localhost:3001"
これで、フロントエンドのサーバは、クライアントからのリクエストをバックエンドに転送します。
Webブラウザから見れば、フロントエンドとバックエンドが same-origin に見えるようになります。
これらのことを踏まえて, バックエンドを呼び出す API
クラス, apicall
関数を書きます。
JavaScript は、明示的に return
で戻さないと関数の戻り値が undefined
になります。最後に評価した式の値ではない。Promise
は, メソッドチェインで書き連ねることが多いですが、これと相性が悪い。
行20 の return
で戻している値は, 行28 - 33 の .then()
が返す Promise
です。念のため, .then()
に渡しているハンドラ関数の戻り値ではない。
基本的な REST API の呼出しを作ります。
this.name
でクラス名が得られるので, それで URL を作ります。
とりあえず, create()
で, CREATE と UPDATE を兼ねるようにしてみました。
React component のライフサイクルに注目。普通に公式の図が分かりやすい。
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
コンストラクタ, render()
, componentDidMount()
, componentDidUpdate()
, componentWillUnmount()
がある。副作用を持ち、DOM を更新するには, componentDidMount()
メソッドに書けばよい。
コンストラクタでは isLoaded
, error
を用意しておく。
新しく作ります。Express を使います。
Express は, minimalist のための Webアプリケーションフレームワークです。Node.js 上で動きます。まんま Ruby 用 Sinatra の JavaScript 版です。
Express は v3まで, Sencha Labs Connect 上に実装されていましたが、Express 4 (現行バージョン) から Connect に依存しなくなりました。
ミドルウェアを積み重ねて、基本的な機能を提供します。CSRF protection のための csurf, セッション管理のための express-session など。
[2021-03] Express は JavaScript でバックエンドを作る場合の de facto standard です。しかし、前述のとおり、新しくプログラムを作るなら Koa v2 や Fastify のほうがよさそう。
Sequelize は, Node.js で動く, 多くのRDBMSサーバをサポートした ORM です。DBマイグレーションもできます。まんま ActiveRecord (Ruby on Rails) です。
Promise
ベースであることも売りにしていますが、この点ははっきり失敗です。同期処理するものを Promise
ベースにしてどうする。関連して, 挙動が微妙に安定していません。ドキュメントも不十分です。
2020年5月現在, バックエンドとしては, Ruby on Rails の API モードを使った方がはるかにまともでしょう。フロントエンドと無理に開発言語を合わせる必要はありません。
あるいは, TypeScript 前提であれば, TypeORM がよかった、か。
$ mkdir myapp $ cd myapp $ npm init
対話的な質問に回答します。Entry point は src/app.js
辺りがよく使われると思います。package.json
ファイルができます。
必要なパッケージを導入します。
$ npm install express sequelize sqlite3
package.json
ファイルを修正します。--unhandled-rejections=strict
オプション付きで走らせます。
"scripts": { "start": "node --unhandled-rejections=strict ./src/app.js",
エントリポイントにした src/app.js
ファイルです。基本的な形は、どのアプリケーションでも大体同じになります。
まず require('express')
します。Express app オブジェクトを生成し、必要なミドルウェアを use()
で組み込みます。
URLパスと転送先オブジェクトとで routing します。
そして, listen()
で待ち受けます。
Express の基本は, URLパスとハンドラとの組み合わせです。
express.Router()
でルータオブジェクトを得ます。
get()
, post()
, put()
, delete()
がそれぞれ HTTPメソッドに対応します。第1引数がURLパス, 第2引数がハンドラ関数です。
URLパスは :id
のように値を取り出してハンドラ関数で使うことができます。request.params
のプロパティになります。
.sequelizerc
ファイルが出発点。中身はただの JavaScript。設定ファイルの場所を指定する。
config/database.js
ファイル。データベースの接続情報を書く。ただの JavaScript なので、パスワードは環境変数で得るか, 別ファイルにして gitリポジトリに保存しないようにする。
利用時に, 環境変数 NODE_ENV
で振り分けられるようになっている。次の例は、DBMS を替えるように書いているが、通常ではない。
Sequelize は, データベーススキーマをプログラムで変更していくことができる。正直、この機能がない ORM は使ってられない。
雛形は npx sequelize-cli model:generate コマンドで作る。
$ npx sequelize-cli model:generate --name Hoge --attributes title:string,body:string,ref:integer Sequelize CLI [Node: 12.16.3, CLI: 5.5.1, ORM: 5.21.13] New model was created at /home/hori/repos/react-by-examples/04_crud-with-nodejs/nodejs-backend/src/models/hoge.js . New migration was created at /home/hori/repos/react-by-examples/04_crud-with-nodejs/nodejs-backend/migrations/20200614094703-Hoge.js .
カラム名:型名
.
migrations/
にマイグレーションスクリプトが, src/models/
にモデルの定義が生成される。
テーブル名は複数形になる。この単数形/複数形、大文字始まり/小文字始まりがものすごく問題を難しくする。間違えるとエラーにならず、不思議な挙動になる。エラーになってくれればまだしもだが、大変。
マイグレーション実行は次のコマンド;
$ npx sequelize-cli db:migrate
とにかく、常に await
を付けること。付け忘れると上手く動かない。同期処理の関数宣言にすればよかったのに、どうしてこうなった。
全部得る
一つ得る. 見つからなかった時は null
.
新しくレコードを作る. 整合性検査が走る。try
で囲むこと。
更新. 探してから、save()
. await
を忘れずに。
削除. 探してから destroy()
.