過去3年間、私はNode.jsでサーバーを開発しており、作業の過程でいくつかのコードベースを蓄積しました。これをフレームワークとして設計し、オープンソースに入れることにしました。
フレームワークの主な機能は次のとおりです。
- JSマジックのないシンプルなアーキテクチャ
- モデルの自動シリアル化/逆シリアル化(たとえば、フィールドがクライアントから来たかどうかをチェックする必要はありません。すべてが自動的にチェックされます)
- APIスキーマを生成する機能
- スキーマベースのドキュメント生成
- クライアント用に完全に型指定されたSDKを生成する(現時点ではJSフロントエンドについて話している)
この記事では、Node.jsについて少し説明し、このフレームワークを検討します。
興味のある方-カットをお願いします
問題は何ですか?なぜ別の自転車を書くのですか?
Node.jsでのサーバー開発の主な問題は次の2つです。
- 開発されたコミュニティとインフラストラクチャの欠如
- 十分に低いエントリー
インフラストラクチャがないため、すべてが単純です。Node.jsはかなり新しいプラットフォームであるため、Node.jsの古いプラットフォーム(PHPや.NETなど)で便利かつ迅速に記述できるものは困難です。
低エントリーはどうですか? ここでの主な問題は、誇大宣伝の輪を回し、新しいライブラリ/フレームワークが登場するたびに、誰もが単純化しようとしていることです。 それはシンプルに見えるでしょう-より良いですが、 その結果、常に経験豊富な開発者がこれらのソリューションを使用するとは限りません-ライブラリとフレームワークの崇拝のカルトが発生します。 おそらく最も明白な例は明白です。
エクスプレスの何が問題になっていますか? 何もない! いいえ、もちろん、あなたはそれに欠陥を見つけることができますが、エクスプレスはあなたが使用できるようにする必要があるツールです、エクスプレスから問題が始まるか、他のフレームワークは、あなたがそれと結びついているときにプロジェクトで主要な役割を果たします。
上記に基づいて、新しいプロジェクトの開発を開始し、「コア」サーバーを作成することから始めました。その後、他のプロジェクトに転送され、完成し、もちろんAirshipフレームワークになりました。
基本コンセプト
アーキテクチャについて考え始めると、「サーバーは何をしているのですか?」という質問を自問しました。 最高レベルの抽象化では、サーバーは次の3つのことを行います。
- リクエストを受け取る
- リクエストを処理します
- 答えを与える
それでも、なぜあなたの人生を複雑にしますか? 北でサポートされる各リクエストにはモデルがあり、リクエストモデルはリクエストハンドラに移動し、ハンドラは応答モデルを提供します。
私たちのコードはネットワークについて何も知らず、通常のリクエストとレスポンスのクラスのインスタンスでのみ機能することに注意することが重要です。 この事実により、より柔軟なアーキテクチャを実現できるだけでなく、トランスポート層を変更することもできます。 たとえば、コードを変更せずにHTTPからTCPに転送できます。 もちろん、これは非常にまれなケースですが、このような機会は、アーキテクチャの柔軟性を示しています。
モデル
モデルから始めましょう、それらから何が必要ですか? まず、モデルをシリアル化およびシリアル化解除する簡単な方法が必要です。 ユーザークエリもモデルです。
以下の説明では、記事を膨らまさないように、ドキュメントで読むことができるいくつかの詳細を省略します。
仕組みは次のとおりです。
class Point { @serializable() readonly x: number @serializable() readonly y: number constructor(x: number, y: number) { this.x = x this.y = y } }
ご覧のとおり、モデルは通常のクラスです。唯一の違いは、 serializable
デコレーターの使用です。 このデコレータを使用して、シリアライザにフィールドを指定します。
これで、モデルの初期化と逆シリアル化ができます。
JSONSerializer.serialize(new Point(1,2)) // { "x": 1, "y": 2 } JSONSerializer.deserialize(Point, { x: 1, y: 2 }) // Point { x: 1, y: 2 }
間違った型のデータを渡すと、シリアライザーは例外をスローします。
JSONSerializer.deserialize(Point, { x: 1, y: "2" }) // Error: y must be number instead of string
クエリモデル
クエリは同じモデルです。違いは、すべてのクエリがASRequest
から継承され、 @queryPath
デコレータを使用して@queryPath
を指定することです。
@queryPath('/getUser') class GetUserRequest extends ASRequest { @serializable() private userId: number constructor( userId: number ) { super() this.userId = userId } }
回答モデル
応答モデルも通常どおりに記述されますが、 ASResponse
から継承されASResponse
。
class GetUserResponse extends ASResponse { @serializable() private user: User constructor(user: User) { super() this.user = user } }
リクエストハンドラー
要求ハンドラーはBaseRequestHandler
を継承し、2つのメソッドを実装します。
export class GetUserHandler extends BaseRequestHandler { // public async handle(request: GetUserRequest): Promise<GetUserResponse> { return new GetUserResponse(new User(....)) } // public supports(request: Request): boolean { return request instanceof GetUserRequest } }
なぜなら このアプローチでは、1つのハンドラーで複数のリクエストの処理を実装することはあまり便利ではありませんBaseRequestHandler
と呼ばれるMultiRequestHandler
子孫があり、複数のリクエストを処理できます。
class UsersHandler extends MultiRequestHandler { // @handles(GetUserRequest) // GetUserRequest public async handleGetUser(request: GetUserRequest): Promise<ASResponse> { } @handles(SaveUserRequest) public async handleSaveUser(request: SaveUserRequest): Promise<ASResponse> { } }
クエリの受信
システムへのリクエストのプロバイダーを記述する基本クラスRequestsProvider
があります。
abstract class RequestsProvider { public abstract getRequests( callback: ( request: ASRequest, answerRequest: (response: ASResponse) => void ) => void ): void }
システムはgetRequests
メソッドを呼び出し、要求を待機して処理し、応答をanswerRequest
ます。
HTTP経由でリクエストを受信するために、 HttpRequestsProvider
実装されています。これは非常に簡単に機能します。すべてのリクエストはPOSTを経由し、データはjsonを経由します。 使用方法も簡単です。ポートとサポートされているリクエストのリストを渡すだけです。
new HttpRequestsProvider( logger, 7000, // GetUserRequest, SaveUserRequest )
すべてをまとめる
メインサーバークラスはAirshipAPIServer
で、リクエストハンドラーとリクエストプロバイダーを渡します。 なぜなら AirshipAPIServer
は1つのハンドラーのみを受け入れます。ハンドラーのリストを受け入れ、必要なハンドラーを呼び出すハンドラーマネージャーが実装されています。 その結果、サーバーは次のようになります。
let logger = new ConsoleLogger() const server = new AirshipAPIServer({ requestsProvider: new HttpRequestsProvider( logger, 7000, GetUserRequest, SaveUserRequest ), requestsHandler: new RequestHandlersManager([ new GetUserHandler(), new SaveUserRequest() ]) }) server.start()
APIスキーマ生成
APIスキームは、サーバーのすべてのモデル、リクエスト、レスポンスを記述する特別なJSONです。特別なaschemegen
ユーティリティを使用して生成できます。
まず、すべてのリクエストと回答を示す設定を作成する必要があります。
import {AirshipAPIServerConfig} from "airship-server" const config: ApiServerConfig = { endpoints: [ [TestRequest, TestResponse], [GetUserRequest, GetUserResponse] ] } export default config
その後、設定へのパスとスキームが書き込まれるフォルダーへのパスを指定して、ユーティリティを実行できます。
node_modules/.bin/aschemegen --o=/Users/altox/Desktop/test-server/scheme --c=/Users/altox/Desktop/test-server/build/config.js
クライアントSDKの生成
なぜ回路が必要なのですか? たとえば、TypeScriptフロントエンド用に完全に型指定されたSDKを生成できます。 SDKは4つのファイルで構成されています。
- API.ts-すべてのメソッドとネットワークを操作するメインファイル
- Models.ts-すべてのモデルはこちら
- Responses.ts-ここにすべての応答モデルがあります
- MethodsProps.ts-リクエストを説明するインターフェースがあります
作業ドラフトのAPI.tsの一部を次に示します。
/** * This is an automatically generated code (and probably compiled with TSC) * Generated at Sat Aug 19 2017 16:30:55 GMT+0300 (MSK) * Scheme version: 1 */ const API_PATH = '/api/' import * as Responses from './Responses' import * as MethodsProps from './MethodsProps' export default class AirshipApi { public async call(method: string, params: Object, responseType?: Function): Promise<any> {...} /** * * * @param {{ * appParams: (string), * groupId: (number), * name: (string), * description: (string), * startDate: (number), * endDate: (number), * type: (number), * postId: (number), * enableNotifications: (boolean), * notificationCustomMessage: (string), * prizes: (Prize[]) * }} params * * @returns {Promise<SuccessResponse>} */ public async addContest(params: MethodsProps.AddContestParams): Promise<Responses.SuccessResponse> { return this.call( 'addContest', { appParams: params.appParams, groupId: params.groupId, name: params.name, description: params.description, startDate: params.startDate, endDate: params.endDate, type: params.type, postId: params.postId, enableNotifications: params.enableNotifications, notificationCustomMessage: params.notificationCustomMessage, prizes: params.prizes ? params.prizes.map((v: any) => v ? v.serialize() : undefined) : undefined }, Responses.SuccessResponse ) } ...
このコードはすべて自動的に記述されます。これにより、IDEがこれを行うことができる場合、クライアントを記述してフィールド名とそのタイプに関する無料のヒントを得ることに気を取られることがなくなります。
SDKの生成も簡単ですasdkgen
ユーティリティを実行し、スキームへのパスとSDKが存在するパスを渡す必要があります。
node_modules/.bin/asdkgen --s=/Users/altox/Desktop/test-server/scheme --o=/Users/altox/Desktop/test-server/sdk
ドキュメント生成
私はSDK生成で停止せず、ドキュメント生成を書きました。 ドキュメントは非常にシンプルで、リクエスト、モデル、レスポンスの説明を含むプレーンHTMLです。 おもしろいことから:各モデルに対して、JS、TS、Swiftの生成コードがあります。
おわりに
このソリューションは本番環境で長い間使用されており、古いコードを維持し、新しいコードを作成し、クライアント用のコードを作成するのに役立ちます。
多くの人にとって、記事とフレームワーク自体の両方は非常に明白に見えるかもしれませんが、他の人はそのようなソリューションがすでに存在すると言い、そのようなプロジェクトへのリンクに私の鼻を突っ込むことさえあるかもしれません。 私の弁護では、2つのことしか言えません。
- 一度にそのような解決策を見つけられなかった、または私に合わなかった
- バイクを書くのは楽しいです!
上記のすべてが気に入ったら、 GitHubへようこそ。