Node/Expressでエンタープライズアプリを構築する
質問
Node/Express/Mongo(実際にはMEANスタックを使用)を使用してエンタープライズアプリケーションを構成する方法を理解しようとしています。
2 冊の本を読み、(StackOverflow の同様の質問を含め)いくつかググった後、Express を使用して大規模なアプリケーションを構造化する良い例を見つけることができませんでした。私が読んだすべてのソースは、次のエンティティによってアプリケーションを分割することを示唆しています。
- ルート
- コントローラ
- モデル
しかし、この構造の主な問題点は、コントローラが神のオブジェクトのようなものであり、コントローラ自身が
req
,
res
オブジェクトがあり、バリデーションを担当し
ビジネスロジックを持つ
に含まれています。
一方、ルートはエンドポイント(パス)をコントローラのメソッドにマッピングするだけなので、私には過剰なエンジニアリングのように思えます。
私はScala/Javaのバックグラウンドを持っているので、すべてのロジックをコントローラ/サービス/Daoの3層に分離する習慣があります。
私にとっては、以下の記述が理想的です。
-
コントローラは、WEB部分とのやりとり、すなわちマーシャル/アンマーシャル、いくつかの簡単な検証(required, min, max, email regexなど)だけを担当します。
-
サービス層は(実はNodeJS/Expressアプリでは見逃していました)、ビジネスロジックといくつかのビジネス検証のみを担当します。サービス層はWEB部分について何も知りません(つまり、WEBコンテキストからだけでなく、アプリケーションの他の場所からも呼び出すことができます)。
-
DAOレイヤーについては、私にとってはすべてクリアです。マングースのモデルは実際にDAOなので、ここが一番わかりやすいですね。
私が見た例はとてもシンプルで、Node/Expressの概念しか示していないように思いますが、ビジネスロジックや検証の多くを含む、実際の例を見てみたいです。
EDITです。
もうひとつはっきりしないのは、DTOオブジェクトが存在しないことです。この例を考えてみましょう。
const mongoose = require('mongoose');
const Article = mongoose.model('Article');
exports.create = function(req, res) {
// Create a new article object
const article = new Article(req.body);
// saving article and other code
}
このJSONオブジェクトは
req.body
の JON オブジェクトが Mongo ドキュメントを作成するためのパラメータとして渡されています。これは私にとっては嫌な感じがします。私は生の JSON ではなく、具象クラスで作業したいのです。
ありがとうございます。
どのように解決するのですか?
コントローラは、あなたがそうであって欲しくないと思うまで、神オブジェクトです...
- ズルズルって言わないんだね(╯°□°)╯︵┻━━━━━━━━━━━!!!!
解答に興味があるだけ? 飛び乗る 最新のセクション 結果発表です。 .
┬──┬─◡ノ(° -°ノ)ノ
回答に入る前に、この回答が通常のSOの長さよりずっと長くなってしまったことをお詫びします。コントローラだけでは何もできず、MVCパターン全体が重要なのです。そのため、最小限の責任で適切な分離されたコントローラを実現する方法を示すために、ルータ <-> コントローラ <-> サービス <-> モデルに関するすべての重要な詳細を調べることが適切であると感じました。
想定されるケース
まずは小さな仮想的なケースから。
- AJAXでユーザー検索を提供するAPIを持ちたい。
- Socket.ioを通じて同じユーザー検索を提供するAPIを持ちたい。
まずはExpressから。簡単でしょう?
routes.js
import * as userControllers from 'controllers/users';
router.get('/users/:username', userControllers.getUser);
コントローラ/ユーザ.js
import User from '../models/User';
function getUser(req, res, next) {
const username = req.params.username;
if (username === '') {
return res.status(500).json({ error: 'Username can\'t be blank' });
}
try {
const user = await User.find({ username }).exec();
return res.status(200).json(user);
} catch (error) {
return res.status(500).json(error);
}
}
では、Socket.ioの部分をやってみましょう。
これは socket.io の質問ではないので、定型文は省きます。
import User from '../models/User';
socket.on('RequestUser', (data, ack) => {
const username = data.username;
if (username === '') {
ack ({ error: 'Username can\'t be blank' });
}
try {
const user = User.find({ username }).exec();
return ack(user);
} catch (error) {
return ack(error);
}
});
うーん、何か臭うなぁ...。
-
if (username === '')
. コントローラバリデータを2回書かなければなりませんでした。もしn
コントローラバリデータがあったらどうでしょう?それぞれを2つ(またはそれ以上)コピーして最新の状態に保つ必要があるでしょうか? -
User.find({ username })
が2回繰り返されています。それはもしかしたら、サービスかもしれません。
今、ExpressとSocket.ioの正確な定義にそれぞれ接続された2つのコントローラを書きました。Express と Socket.io の両方が後方互換性を持つ傾向があるため、これらはその寿命が尽きるまで壊れない可能性が高いです。 しかし の場合、再利用はできません。エクスプレスを Hapi ? コントローラを全て作り直す必要があります。
もう一つ、あまり目立たないかもしれませんが、悪い匂いがします...。
コントローラのレスポンスは手作りです。
.json({ error: whatever })
RLのAPIは常に変化しています。将来的には、あなたのレスポンスが
{ err: whatever }
のような、あるいはもっと複雑な(そして有用な)ものを望むかもしれません。
{ error: whatever, status: 500 }
始めよう (可能な解決策)
呼び出すことができない は なぜなら、世の中には無限のソリューションが存在するからです。それは、あなたの創造性とニーズ次第です。私は比較的大きなプロジェクトでこの方法を使用していますが、うまく機能しているように見えますし、私が前に指摘したことすべてを修正しています。
最後まで面白くするために、Model -> Service -> Controller -> Routerとします。
モデル
質問の主題ではないので、モデルについての詳細には触れません。
以下のようなMongooseのモデル構造を持っているはずです。
models/User/validate.js
export function validateUsername(username) {
return true;
}
mongoose 4.x のバリデータに適した構造については、以下の記事を参照してください。 はこちら .
models/User/index.js
import { validateUsername } from './validate';
const userSchema = new Schema({
username: {
type: String,
unique: true,
validate: [{ validator: validateUsername, msg: 'Invalid username' }],
},
}, { timestamps: true });
const User = mongoose.model('User', userSchema);
export default User;
username フィールドを持つ基本的な User Schema と
created
updated
mongoose-controlledフィールドがあります。
を入れた理由は
validate
フィールドを含む理由は、ほとんどのモデルのバリデーションをコントローラではなく、ここで行うべきであることに気づいてもらうためです。
Mongoose Schema はデータベースに到達する前の最後のステップです。誰かが MongoDB に直接問い合わせない限り、モデルの検証は必ず行われるので、コントローラに置くよりも安全です。先ほどの例のようにバリデータをユニットテストするのが簡単だと言っているわけではありません。
サービス
このサービスはプロセッサとして動作します。与えられたパラメータを処理し、値を返します。
ほとんどの場合(これを含む)、これは マングースモデル を返し プロミス (またはコールバック; ただし 私なら間違いなく はES6とプロミスを使います。)
services/user.js
function getUser(username) {
return User.find({ username}).exec(); // Just as a mongoose reminder, .exec() on find
// returns a Promise instead of the standard callback.
}
この時点で、あなたは不思議に思うかもしれません、いいえ
catch
ブロックはないのでしょうか?そうではありません。
クールなトリック
を行うので、この場合はカスタムブロックは必要ありません。
他の場合、些細な同期サービスで十分です。同期サービスが決してI/Oを含まないことを確認してください。 Node.jsのスレッド全体が .
services/user.js
function isChucknorris(username) {
return ['Chuck Norris', 'Jon Skeet'].indexOf(username) !== -1;
}
コントローラ
コントローラの重複を避けたいので、コントローラには a コントローラが必要です。
controllers/user.js
export function getUser(username) {
}
この署名は今どのように見えますか?きれいでしょう?usernameパラメータにしか興味がないので、以下のような無駄なものを取る必要はありません。
req, res, next
.
不足しているバリデータとサービスを追加してみましょう。
コントローラ/user.js
import { getUser as getUserService } from '../services/user.js'
function getUser(username) {
if (username === '') {
throw new Error('Username can\'t be blank');
}
return getUserService(username);
}
まだきちんと見えますが、...どうでしょう?
throw new Error
はどうでしょう、アプリケーションがクラッシュしませんか?- しーっ、待てよ。まだ終わってませんよ。
この時点で、私たちのコントローラのドキュメントは以下のようなものになります。
/**
* Get a user by username.
* @param username a string value that represents user's username.
* @returns A Promise, an exception or a value.
*/
に記載されている "値" とは何でしょうか?
@returns
? 先ほど、サービスは同期でも非同期でもいいと言ったことを思い出してください (
Promise
)?
getUserService
はこの場合、非同期ですが
isChucknorris
サービスはそうではないので、Promiseの代わりに単に値を返すことになります。
みんながドキュメントを読んでくれることを願っています。なぜなら、あるコントローラは他のコントローラとは異なる扱いが必要であり、その中には
try-catch
ブロックが必要だからです。
開発者 (私を含む) が最初に試す前にドキュメントを読むことを信用できないので、この時点で決断を下さなければなりません。
-
コントローラを強制的に
Promise
戻る - 常にPromiseを返すサービス
⬑これでコントローラーの戻り値が一貫しないことが解決します(try-catchブロックを省略できることではありません)。
私は、最初の選択肢を好みます。なぜなら、コントローラはほとんどの場合、最も多くのPromiseを連鎖させるものだからです。
return findUserByUsername
.then((user) => getChat(user))
.then((chat) => doSomethingElse(chat))
ES6 Promise を使っている場合は、代わりに
Promise
の素晴らしいプロパティを利用することができます。
Promise
はその寿命の間、非プロミスを処理することができ、 それでもなお
Promise
:
return promise
.then(() => nonPromise)
.then(() => // I can keep on with a Promise.
もし私たちが呼び出す唯一のサービスが
Promise
を使わないのであれば、自分で作ればいいのです。
return Promise.resolve() // Initialize Promise for the first time.
.then(() => isChucknorris('someone'));
例に戻ると、次のようになります。
...
return Promise.resolve()
.then(() => getUserService(username));
実際に必要なのは
Promise.resolve()
は必要ありません。
getUserService
はすでにPromiseを返していますが、一貫性を持たせたいのです。
について疑問に思っているのであれば
catch
ブロックについて疑問に思っている方: カスタムな処理を行いたい場合を除き、コントローラ内でこれを使用することはありません。こうすることで、すでに組み込まれている 2 つの通信チャネル (エラーの場合は例外、成功の場合は return メッセージ) を利用して、個別のチャネルでメッセージを配信することができるようになります。
ES6 Promiseの代わりに
.then
の代わりに、より新しい ES2017 の
async / await
(
現在では正式な
) をコントローラに追加します。
async function myController() {
const user = await findUserByUsername();
const chat = await getChat(user);
const somethingElse = doSomethingElse(chat);
return somethingElse;
}
お知らせ
async
の前にある
function
.
ルーター
ついにルーターが登場!やったー
つまり、まだユーザーに対して何も応答しておらず、あるのは常に
Promise
(を返すことがわかっているコントローラだけです。) そして、以下のような場合に例外が発生する可能性があります。
throw new Error is called
または何らかのサービス
Promise
が壊れる。
ルーターは、統一された方法で、ペンションをコントロールし、クライアントにデータを返すもので、それは何らかの既存のデータであるでしょう。
null
または
undefined
data
またはエラー。
Router は複数の定義を持つ唯一のものになります。その数はインターセプターに依存します。この例では、API(Expressを使用)とSocket(Socket.ioを使用)です。
何をしなければならないかを確認しましょう。
ルータは
(req, res, next)
を
(username)
. 素朴なバージョンでは、次のような感じです。
router.get('users/:username', (req, res, next) => {
try {
const result = await getUser(req.params.username); // Remember: getUser is the controller.
return res.status(200).json(result);
} catch (error) {
return res.status(500).json(error);
}
});
これはうまく機能しますが、このスニペットをすべてのルートにコピーペーストすると、膨大な量のコードが重複することになります。そこで、より良い抽象化を行う必要があります。
この場合、ある種の偽ルータークライアントを作成し、プロミスと
n
パラメータを受け取り、ルーティングを行い
return
タスクを実行します。
/**
* Handles controller execution and responds to user (API Express version).
* Web socket has a similar handler implementation.
* @param promise Controller Promise. I.e. getUser.
* @param params A function (req, res, next), all of which are optional
* that maps our desired controller parameters. I.e. (req) => [req.params.username, ...].
*/
const controllerHandler = (promise, params) => async (req, res, next) => {
const boundParams = params ? params(req, res, next) : [];
try {
const result = await promise(...boundParams);
return res.json(result || { message: 'OK' });
} catch (error) {
return res.status(500).json(error);
}
};
const c = controllerHandler; // Just a name shortener.
もっと詳しく知りたい方は、この トリック の他の返信で、この完全版について読むことができます。 React-Reduxとsocket.ioを使ったWebsocketについて ("SocketClient.js"セクション)を参照してください。
を使うと、ルートはどのようになりますか?
controllerHandler
?
router.get('users/:username', c(getUser, (req, res, next) => [req.params.username]));
冒頭と同じように、きれいな1行で。
さらなるオプションの手順
コントローラの約束
ES6 Promisesを使用している方のみ適用されます。ES2017
async / await
のバージョンは、すでに私には良いように見えます。
なぜか、私はこのバージョンで
Promise.resolve()
という名前を付けて初期化Promiseを構築するのが嫌いです。そこで何が起こっているのかが明確でないからです。
むしろもっとわかりやすいものに置き換えてほしい。
const chain = Promise.resolve(); // Write this as an external imported variable or a global.
chain
.then(() => ...)
.then(() => ...)
これで、あなたは
chain
がPromiseの連鎖の始まりであることがわかりました。あなたのコードを読む人は皆、そうでなくても、少なくともそれがサービス関数の連鎖であると仮定しています。
Expressエラーハンドラ
Express にはデフォルトのエラーハンドラがあり、少なくとも予期しないエラーを捕捉するために使用する必要があります。
router.use((err, req, res, next) => {
// Expected errors always throw Error.
// Unexpected errors will either throw unexpected stuff or crash the application.
if (Object.prototype.isPrototypeOf.call(Error.prototype, err)) {
return res.status(err.status || 500).json({ error: err.message });
}
console.error('~~~ Unexpected error exception start ~~~');
console.error(req);
console.error(err);
console.error('~~~ Unexpected error exception end ~~~');
return res.status(500).json({ error: '⁽ƈ ͡ (ुŏ̥̥̥̥םŏ̥̥̥̥) ु' });
});
さらに言えば、おそらく次のようなものを使うべきでしょう。
デバッグ
または
ウィンストン
の代わりに
console.error
のように、より専門的な方法でログを処理することができます。
そしてこれが、これを
controllerHandler
:
...
} catch (error) {
return res.status(500) && next(error);
}
捕捉されたエラーをExpressのエラーハンドラにリダイレクトしているだけです。
ApiErrorとしてのエラー
Error
は、Javascriptで例外を投げるときに、エラーをカプセル化するためのデフォルトのクラスと考えられています。もし本当に自分自身の制御されたエラーだけを追跡したいのであれば、私ならおそらく
throw Error
から Express エラーハンドラ
Error
から
ApiError
に変更し、さらにそれをステータス・フィールドに追加することで、よりニーズに合ったものにすることができます。
export class ApiError {
constructor(message, status = 500) {
this.message = message;
this.status = status;
}
}
追加情報
カスタム例外
任意の時点で任意のカスタム例外を投げることができるのは
throw new Error('whatever')
を使うか、あるいは
new Promise((resolve, reject) => reject('whatever'))
. を使って遊ぶだけです。
Promise
.
ES6 ES2017
それは非常に意見が分かれるところですね。IMO ES6 (あるいは ES2017 は、Nodeをベースにした大きなプロジェクトに取り組むのに適切な方法です。
まだ使用していない場合は ES6 の機能や ES2017 と バベル トランスパイラです。
結果
これは、コメントや注釈のない完全なコードです(すでに前に示されています)。このコードに関するすべてのことは、該当するセクションまでスクロールして確認することができます。
ルーター.js
const controllerHandler = (promise, params) => async (req, res, next) => {
const boundParams = params ? params(req, res, next) : [];
try {
const result = await promise(...boundParams);
return res.json(result || { message: 'OK' });
} catch (error) {
return res.status(500) && next(error);
}
};
const c = controllerHandler;
router.get('/users/:username', c(getUser, (req, res, next) => [req.params.username]));
コントローラ/ユーザ.js
import { serviceFunction } from service/user.js
export async function getUser(username) {
const user = await findUserByUsername();
const chat = await getChat(user);
const somethingElse = doSomethingElse(chat);
return somethingElse;
}
services/user.js
import User from '../models/User';
export function getUser(username) {
return User.find({}).exec();
}
models/User/index.js
import { validateUsername } from './validate';
const userSchema = new Schema({
username: {
type: String,
unique: true,
validate: [{ validator: validateUsername, msg: 'Invalid username' }],
},
}, { timestamps: true });
const User = mongoose.model('User', userSchema);
export default User;
models/User/validate.js
export function validateUsername(username) {
return true;
}
関連
-
[解決済み】Nodejs: Errorを解決する方法。ENOENT: そのようなファイルまたはディレクトリがありません
-
[解決済み】nodemon - app crashed - waiting for file changes before start
-
[解決済み] bodyParser は非推奨です express 4
-
[解決済み] NPMが同じエラーで固まる EISDIR: ディレクトリに対する不正な操作、エラーで読み込み (ネイティブ)
-
[解決済み] ノード / エクスプレス EADDRINUSE、アドレスはすでに使用中です - サーバーを停止してください。
-
[解決済み] forEachループでasync/awaitを使用する
-
[解決済み] Expressで"? "の後にあるGETパラメータにアクセスするにはどうすればよいですか?
-
[解決済み] nodeやExpressを使用してJSONを返す正しい方法
-
[解決済み】Expressで完全なURLを取得する方法は?
-
[解決済み】サービスは常にDTOを返すべきですか、それともドメインモデルも返すことができますか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】AWS lambda function エラー - モジュール 'index' をインポートできません。エラー
-
[解決済み] create-react-app、インストールエラー("コマンドが見つからない")。
-
[解決済み] TypeError: コールバックはnodejsの関数ではありません。
-
[解決済み] Npmエラー - Windows NT - 解決方法
-
[解決済み] S3 Bucket に何かを送信しようとすると、AWS Missing credentials が表示される (Node.js)
-
[解決済み] ReferenceError: describe は定義されていません NodeJs
-
[解決済み] エラーです。Cannot find module 'ejs'
-
[解決済み] Macでポート3000をロックしているプロセスを見つける(そして殺す)【終了
-
[解決済み] Node.jsプロジェクトのフォルダ構造
-
[解決済み】Node.jsはなぜシングルスレッドなのですか?[クローズド]