今回から, 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().