jsonex-複雑なクライアント/サーバーダイアログを簡素化





クライアントとサーバー間の相互作用は通常非常に単純であり、かなり原始的なツールに依存しています。 これはそれ自体では問題を引き起こしませんが、多くの場合、タスクの小さな複雑さでさえ通常のアプローチに適合させることが難しく、エレガントすぎないパッチソリューションを生み出します。 多くのタスクは、新しいプロジェクトごとに、無計画かつ相互に独立して解決されます。 そのようなタスクには、たとえば次のものがあります。





開発者は多くの時間を費やし、サーバー側でlotいバイクを何度も作成します。その後、クライアント側でもサポートする必要があります。



jsonexは、呼び出し可能なデータの概念に基づいたシンプルで統一されたアプローチのフレームワーク内で、上記の問題と他の多くの問題の解決策を組み合わせる試みです。



内容


jsonex

呼び出し表記

コンテキスト

通信jsonexとJS

計算可能なデータ(呼び出し可能なデータ)

クライアント側のビュー

計算可能なデータの利点

HTTPおよびWebソケットを使用する

セキュリティに関する考慮事項

JSON表現

非同期呼び出し

アーク

おわりに



jsonex



計算可能なデータの概念は単純で、さまざまなデータ形式に使用できます。 もう少し私はJSON表現のフレームワーク内でそれを使用する方法を教えます。 しかし、そのアイデアを最も純粋な形で示すために、遠くから始めましょう。



多くの場合、JSONを使用しますが、これは美しいです。 シンプルで読みやすく、階層的なデータ構造を表すことができ、広くサポートされています。 それにもかかわらず、私はいくつかのことを改善したいと思います、例えば:





JSONの拡張バージョン(jsonexと呼びましょう)は、たとえば次のようになります。



{ //  name: 'John', familyName: 'Smith', dateOfBirth: '1901-01-01', friendIds: [ 124124, 283746, /*   */ ], num: 123, }
      
      





もう悪くない。 JSON configsでコメントを書く機能をたくさん提供します。 しかし、この例には非常に疑わしい行が1つあります。



  dateOfBirth: '1901-01-01',
      
      





これは何ですか ひも? 日付? 人は文脈から推測できますが、パーサーはそれほど賢くはありません。 形式で提供されない日付タイプ。 それを認識するために、2つのアプローチを使用できます。データスキームを記述するか、パーサーを正しい方向に導くことができる何らかの種類のヒントアノテーションを使用します。



JSONは最初はスキームを必要としません。日付がどのフィールドにあるかをパーサーに伝えることができるようにするためだけに、その存在を要求するのは奇妙です。 したがって、2番目の方法に進みましょう。 日付に注釈を追加する多くの方法を考えることができますが、任意のデータ型を示す簡単で同時に普遍的な方法があると便利です。 これには呼び出し表記が最適です。



呼び出し表記



次のようにフィールドを記述します。



  dateOfBirth: Date('1901-01-01'),
      
      





これで、データ型が明らかになりました。 しかし、パーサーはそのようなレコードに遭遇したときに正確に何をすべきでしょうか? アプローチはかなり簡単です。 SomeName(args...)



という形式の構築に遭遇したSomeName(args...)



パーサーは次のことを行う必要があります。





したがって、分析の結果は、ハンドラー関数の実装に完全に依存します。 Date('1901-01-01')



呼び出しDate('1901-01-01')



は、JavaScriptではDate



型、Pythonではdate



またはdatetime



などのオブジェクトに変わります。



パーサーが指定された名前のハンドラーを見つけられない場合、デフォルトのハンドラーを呼び出します。このハンドラーは、妥当なものを返すか、例外をスローします。



呼び出し表記の処理中に、データに含まれる任意のコードを実行するように見えるかもしれませんが、そうではありません。



さらに、これらのポイントをより詳細に検討します。 それまでの間、呼び出し表記法は非常に柔軟性が高いことに注意してください。 新しいハンドラーを追加することにより、システムを簡単に拡張できます。



 var handlers = { Date: function (ctx, v) { return new Date(v); }, Complex: function (ctx, real, imag) { return new Complex(real, imag); }, ArrayBuffer: function (ctx, v) { return base64DecToArr(v).buffer; }, Person: function (ctx, personDataDict) { return new Person(personDataDict); } }; //    ,        var person = new JsonexParser(handlers).parse( "Person({"+ " name: 'John',"+ " dateOfBirth: Date('1901-01-01'),"+ " i: Complex(0, 1),"+ " song: ArrayBuffer('Q2FsbCBub3RhdGlvbiBpcyBjb29sIQ=='),"+ "})" );
      
      





ここでは、base64表現の日付、複素数、バイナリデータを解析する機能を追加しました。これらすべては文字通り数行のコードであり、仮想パーサーの構造を完全に変更することはありません。



コンテキスト



ご覧のとおり、各ハンドラーは、独自の引数に加えて、 ctx



パラメーターを受け入れます。 このパラメーターは処理コンテキストを渡します。 この段階では、 ctx



は最初は空の辞書であると想定しています。 コンテキストにより、次のことができます。





たとえば、コンテキストを使用すると、以前に計算されたオブジェクトを使用できるget



およびset



ハンドラーを簡単に作成get



ます。



 var handlers = { //         set: function (ctx, key, data) { ctx.box = ctx.box || {}; ctx.box[key] = data; return data; }, //  ,    get: function (ctx, key) { return ctx.box ? ctx.box[key] : undefined; } }; var data = new JsonexParser(handlers).parse( "[ set('x', { a: 'a' }), get('x') ]" ); data[0].a = 5; //  data[0]  data[1]         console.log(JSON.stringify(data)); // [{"a":5},{"a":5}]
      
      





通信jsonexとJS



JSONと同様に、jsonexはJS構文と密接に関連しています。 これは構文的に正しいJS式であり、最も単純なケースでは、各呼び出し表記が計算のコンテキストで定義されていれば、JS式として評価することさえできます。 例えば



 [ foo(), bar.baz() // , jsonex      ]
      
      





foo



bar



正しく定義されていれば、有効で計算可能なJS式です。



もちろん、これは、対応するクロージャーで必要な変数を定義することにより、 eval()



を使用してjsonexを取得および評価する価値があるという意味ではありません。 潜在的なセキュリティの問題に加えて、このアプローチは、実行中の何かとしてではなく、データとしてデータを正確に分析することを可能にする柔軟性の一部を失います。 ただし、場合によっては、jsonexは実際にはJSの限定されたサブセットと見なされ、jsonexデータはJS式と見なされます。



計算可能なデータ(呼び出し可能なデータ)



jsonexのデータは計算可能な式で、計算前または計算プロセスで簡単に解析できます。 そのような式をサーバー要求として使用しないのはなぜですか? たとえば、クエリは次のようになります。



 getUsers([1, 15, 7])
      
      





サーバーは、適切なハンドラーを使用して計算できます。



 var handlers = { getUsers: function (ctx, userIds) { var listOfUsers = getUsersFromDbOrWhatever(userIds); return listOfUsers; } };
      
      





次に、結果をjsonexでシリアル化し、応答をクライアントに送信します。



 [ User({id: 1, name: 'John', ...}), ...]
      
      







クライアントは、使用できる状態の正直なオブジェクトを含むデータを受け取ります。 同時に、サーバーはjsonex式の単純な計算機になります。 APIを拡張するには、新しいハンドラーを追加するだけで十分です。URLを台無しにしたり、引数を解析したり、適切な型にキャストしたり、GETとPOSTを区別したりする必要はありません。



クライアント側のビュー



クライアントからの通話を整理する方法について考えてみましょう。 "getUsers([1, 15, 7])"



1、15、7 "getUsers([1, 15, 7])"



ような文字列の形式でjsonex表現を手動で収集するのは不便です。 したがって、呼び出し表記を記述し、シリアライザーが理解するヘルパーオブジェクトが必要です。 これは、その使用方法の例です。



 var getUsers = function (userIds) { return new jsonex.Call('getUsers', userIds); //   }; //     jsonex.stringify(getUsers([1, 15, 7])); // 'getUsers([1,15,7])'
      
      





この場合、サーバーリクエストは次のようになります。



 server.ask( getUsers([1, 15, 7]), //  function (err, result) { //   ... } );
      
      





server.ask()



は次を実行する必要があります。





この例では、最初の引数はgetUsers()



が返す値、つまり、文字列'getUsers([1,15,7])'



シリアル化されたjsonex.Call



型のオブジェクトになります。



シンプルできれいに見えます。 コードを書くという観点から見ると、すべてのアクションはすぐに使用できるオブジェクトで実行され、変換はすべて内部に隠されています。 この例ではコールバックを使用していますが、Promiseを使用すると、すべてがさらに見やすくなります。



サーバーから受け取った結果がErrorクラスの子孫である場合、 server.ask()



はサーバーがエラーを返したと見なし、対応する引数でコールバックを引き起こします。 解析の結果は、目的のクラスのすぐに使用できるオブジェクトであるため、このアプローチが可能です。



したがって、エラーについてクライアントに通知するには、サーバーが適切な呼び出し表記を使用して必要なクラスのオブジェクトを返すだけで十分です。 この場合、クライアントは、この表記法をErrorから継承したクラスのオブジェクトに置き換えるハンドラーを実装する必要があります。



エラーメッセージを含むサーバー応答の例:



 UnexpectedError('Error details message')
      
      





ハンドラーの例:



 handlers.UnexpectedError = function(ctx, msg) { return new ServerError(msg); // ServerError    Error };
      
      





計算可能なデータの利点



計算可能なデータから得られるもの:





たとえば、バッチリクエストは次のようになります。



 [ getFoo(), getBar(1, 2, 3), ]
      
      





応答として、 getFoo()



およびgetBar()



の呼び出しの結果を含む配列が来ます。



ある計算の結果を別の計算で使用します。



 [ set('x', getUserBooks(17)), //    17 getAuthors( //    getProps( //   'authorId'     get('x') get('x'), 'authorId' ) ), ]
      
      





答えは、本のリストとこれらの本の著者のリストを持つ配列になります。

注:この例では、 getProps()



呼び出しは潜在的に危険な可能性があり、おそらく公開したくないプロパティに手を差し伸べる能力を示します-そのようなハンドラーの実装には注意してください。



往復データ転送:



 [ 137, // ,  , , id  someRequest(...) ]
      
      





応答は、番号137の配列とsomeRequest()



を呼び出した結果になります。

注:実際には、 someRequest()



処理中に例外がスローされた場合でも、より複雑な構成を使用して往復データが返されるようにする必要があります。



追加のデータ転送:



 last( //    metaInfo('   ', 1, 3, 4), someRequest(...) )
      
      





ここで、 metaInfo()



呼び出しはコンテキスト内の何かを変更したり、追加のアクションを引き起こしたり、何らかの形で処理に影響を与えたりしますが、 last()



は最後の引数のみを返すため、その戻り値は応答しません。



HTTPおよびWebソケットを使用する



HTTP要求には、メインデータ(要求本文)に加えて、パス、メソッド、およびヘッダーが含まれます。 HTTP応答には戻りコードが含まれています。 jsonexを使用する場合、すべてのリクエストに単一のパスを使用すると便利です。これは、バッチリクエストやWebソケットとやり取りする場合に通常行われるのと同じ方法です。 APIはさまざまな方法で分散できますが、これはほとんど意味がありません。



HTTPメソッドは必要ありません。各リクエストには、データの受信または変更の呼び出しを含めることができるためです。 それでも、さまざまなHTTPメソッドのサポートは、ブラウザ、プロキシ、および他のHTTPの世界で適切に動作するために役立つか、必要な場合さえあります。 実装は簡単です-リクエストおよびレスポンスオブジェクトを計算コンテキストに追加するだけで、ハンドラーはHTTPプロトコルの複雑さを考慮することができます。 戻りコードについても同じことが言えます。 jsonexコンピューティングの一部としては必要ありませんが、HTTP環境との適切な対話のために、正しく公開する価値があります。



データ自体の転送に関しては、HTTPリクエストの本文で送信される場合、すべてが素晴らしいです。 ほとんどの場合、jsonexリクエストにPOSTメソッドを使用するのが妥当と思われるため、そうなります。 ただし、何らかの目的でGET、HEAD、またはDELETEを使用する場合は、URLの一部としてデータを転送する必要があります。これは、標準に従ってこれらの要求の本文を無視する必要があるためです。 これを行う簡単で安価な方法があります-jsonexをqueryなどの単一のクエリ文字列パラメーターに渡しquery



。 したがって、リクエストgetUsers([1,2,3])



はアドレスへの呼び出しに変わります example.com/api?query=getUsers%28%5B1%2C2%2C3%5D%29







ひどいように見えますが、これは内部API呼び出しであるため、デバッグプロセスで見るのはプログラマだけです。 このアプローチは、クライアント側のパッケージ化とサーバー側のアンパックの両方を非常に容易にするため、JSON形式でデータを転送するためによく使用されます。 怖いアドレスがまだ恥ずかしい場合は、フードの下にそれらを隠すツールを書くのは簡単です。



HTTPヘッダーとjsonex機能の両方を使用して、メタデータを転送できます。



 last( authToken('myAuthToken'), //   ,   someOtherHeader('blah blah'), getUsers([1, 15]) )
      
      





Webソケットが各メッセージとともにヘッダーを送信する通常の方法がないため、そのようなデータを要求自体に統合する機能は非常に便利です。 Webソケットの場合、往復データを渡す機能と、各リクエストのパスとメソッドを指定する必要がないことも重要です。



HTTPの場合、リクエストのべき等性も重要です。 このプロパティはHTTPメソッドによって決定されます。一部のメソッドはmust等である必要があり、他のメソッドはそうではありません。 jsonexリクエストは、べき等呼び出しと非べき等呼び出しの混合である可能性があるため、それについて何かを行うことができるメカニズムが必要です。 たとえば、べき等性を必要とするフラグを設定して、呼び出しでチェックできます。



 var handlers = { idempotent: function (ctx) { ctx.mustBeIdempotent = true; }, updateUser(ctx, userData) { if (ctx.mustBeIdempotent) { throw new NonIdempotentCallError('updateUser'); } ... } };
      
      





リクエストの例:



 last( idempotent(), [ getUsers([1,2]), updateUser({id:1, ...}) //   ] )
      
      





どの呼び出しがi等性であるかは、APIのドキュメントから明確である必要があります。また、 idempotent()



を呼び出すと、複雑なリクエストで危険なものが使用されていないという確信が得られます。



セキュリティに関する考慮事項



ハンドラーを作成する場合、リクエストでは、引数を任意の組み合わせで呼び出すことができることを覚えておく必要があります。 これによりセキュリティ上の問題が発生しないようにするには、次の制限に従う必要があります。





最後の段落では、要求全体に制限を課しており、別の考慮に値します。 要求の複雑さを制御する最も簡単な方法は、計算されたとおりに要求を計算し、特定のしきい値を超えた場合に停止することです。



 handlers.expesiveCall = function (ctx, args...) { ctx.cost += calcCost(args...); if (ctx.cost > ctx.costThreshold) { throw new TooExpensiveError(); } ... } };
      
      





コストを計算するときは、引数を転送するデータの性質と量が計算の複雑さに大きく影響する可能性があるため、引数を考慮する必要があります。



別のオプションは、リクエスト全体の予備分析です。 これは、リクエストが単なるデータであるため可能です。 しかし、複雑なクエリを分析すること自体が困難なタスクになる可能性があり、その計算の複雑さも監視する必要があります。



別のアプローチは、リソースへの人為的に制限されたアクセスでサンドボックスで計算を実行することです。 環境でこれを簡単かつ安価に行うことができる場合、これは良い選択肢です。



同じハンドラーを個別に使用すると、ハンドラーの組み合わせが達成できない効果を潜在的に持つ可能性があることを覚えておくことが重要です。 この事実は新しい脅威を隠すかもしれませんが、それらは簡単に回避できます。 ハンドラーのサブセットが他のハンドラーとの予期しない組み合わせの点で心配な場合は、いつでも別個の分離されたセットに入れることができます。 このセットは、別のアドレスで使用可能にするか、必要な制限を導入するリクエストに特別なメタデータを追加することでアクティブ化できます。



極端な場合、多数のハンドラーを個別にのみ、またはDate()



非常にプリミティブな呼び出しと組み合わせて使用​​することを許可できます。 これらのハンドラーは基本的に、リクエストで複数の呼び出しを許可しない使い慣れたモデルに戻ります。 計算可能なデータは大きな機会を提供しますが、必要に応じて制限するのは簡単です。



JSON表現



今すぐjsonexを使用したい場合、1つの問題が発生します-JSONとは異なり、高性能jsonex解析およびシリアル化ライブラリーは、控えめに言っても、まだそれほど広く利用できません)しかし、抜け道があります-JSON表現に基づいてjsonexを使用できます。 jsonexは、3つの単純なルールを適用することでJSONに変わります。



 //   f(...) => {"?": ["f", ...]} //   '?' {'?': value, ...} => {"?": ["?", value], ...} //  ,    f({...}) => {"?": "f", ...}
      
      





最初のルールは、JSON表現で呼び出し表記を記述する方法を示しています。 さらに、プロパティ「?」を持つ辞書 特別な意味を獲得します。 2番目のルールは、プロパティ「?」を使用して通常の辞書を作成する方法の質問に答えます。 呼び出し表記と混同しないように。 3番目は構文糖です。これは、単一の引数があり、この引数が辞書である場合の特別な形式の表記法です。 jsonexのデータとJSON表現のデータの例を次に示します。



 Person({ //  Person name: 'John', dateOfBirth: Date('1901-01-01'), i: Complex(0, 1), d: { '?': 123 } })
      
      





JSON表現:



 { "?": "Person", "name": "John", "dateOfBirth": {"?": ["Date", "1901-01-01"]}, "i": {"?": ["Complex", 0, 1]}, "d": {"?": ["?", 123]} }
      
      







JSON表現はより複雑に見えますが、同じことを示しています。 標準のJSON.parse()



JSON.parse()



し、2回目のパスで追加できます。 または、いくつかの単純なケースでは、 JSON.parse()に渡されるreviver関数を使用して、解析中に直接計算できます。 JSON表現でのシリアル化についても同じことが言えます。JSON.stringify()に渡されたreplacer関数を使用すると簡単です。



一般的に、計算可能なデータの概念は、ほとんどすべてのデータ形式の拡張機能として追加できます。



非同期呼び出し



前に示した例では、すべての呼び出しが同期的であると暗黙的に考えられていました。 それらの1つをより密接に検討してください。



 [ set('x', getUserBooks(17)), getAuthors(getProps(get('x'), 'authorId')), ]
      
      





getUserBooks()



およびgetAuthors()



は、おそらくある種のデータストアに移動し、I / Oを使用し、それに応じて非同期にする必要があります。 そのため、その場でそれらを計算することはできません。 また、(たとえば、 ファイバーを使用して)できる場合でも、次々にではなく、独立した非同期呼び出しを並行して行うことができます。



ソリューションは、実行のために非同期呼び出しをキューに入れ、適切な場所で結果を置き換えるコンピューティングエンジンにすることができます。次に、すべての同期部分を計算した後、非同期部分が完了するのを待ち、その後計算の準備ができたと考えました。実行のキューとして、async.queue()のようなものを使用して、指定されたレベルの並列処理でタスクを実行できます



しかし、実際にはタスクはより複雑です。この例では、一部の呼び出しの計算は他の呼び出しに依存しており、計算するset()



まで計算できませんgetUserBooks()



。したがって、を呼び出そうとするとset()



、この計算を延期し、それが依存するすべての計算に対して、準備ができたらすぐにを追加する必要があることを示す必要がありset()



ます。ここでは、1つの遅延計算にgetUserBooks()



依存していますが、より複雑な依存関係も可​​能です。



しかし、それだけではありません。呼び出しは、get()



完了するまで有用なものを返すことができませんset()



したがって、get()



遅延計算にもなり、今回はからの信号を待機しset()



ます。次に、にgetProps()



依存しますがget()



、にgetAuthors()



依存しgetProps()



ます。



コンピューティングエンジンは、これらすべての依存関係を考慮し、すべての保留中の計算を正しい順序で実行する必要があります。これは非常に単純なタスクではないようですので、このアプローチが機能することを証明する実装が必要です。



アーク



arcプロジェクトは、非同期ハンドラーと依存呼び出しを使用する場合を含め、計算可能なデータの概念が実行可能であることを示すために作成されました。エンジン自体は非常にシンプルで、数百行のコードで構成されており、現時点ではトリッキーな最適化が含まれていないため、非常に理解しやすくなっています。



エンジンのソースデータはトークンのストリームです。現在、トークン化はJSON表現でのみ使用できますが、jsonexを直接操作することは間近です。arcは、node.jsとbrowserifyを使用するブラウザーの両方で使用できます。 arcの非同期ハンドラーの作成は非常に簡単に見えます。非同期呼び出しを適切なディレクティブでラップするだけで、エンジンは内部ですべてのハードワークを実行します。



 handlers.getUserBooks = function (ctx, userId) { return ctx.async(function (cb) { doSomethingAsync(...args, cb); }); };
      
      





コールバックのみがサポートされていますが、Promiseサポートが計画されています。エンジンの使用例、およびjsonexでのシリアル化とJSON表現の例は、対応するセクションにあります。



補足

驚いたことに、文字通り、記事が掲載されてから数時間後に、アークには代替手段がありました。 Habrauser mayorovpは、jsonexをJSON表現で解析するためのライブラリのバージョンを提示しました。このバージョンのライブラリは単一のファイルで構成され、依存関係はなく、標準のファイルを使用し、A + promiseJSON.parse()



をサポートしています。残念ながら制限のためJSON.parse()



、この実装では、非同期呼び出しの処理はそれほど効率的ではないため、切断されます。jQueryで代替ライブラリを使用した例を次に示します。



 var parser = new JSONEX.parser({ allow_async: true, functions: { Foo: function() { var result = $.Deferred(); setTimeout(function() { result.resolve("Hello, world!"); }, 1000); return result.promise(); } }}); $.when(parser.parse('[{"?": ["Foo"]}]')) .done(function(result) { console.log(result); });
      
      





おわりに



jsonexとarcは現在開発中です。この記事で言及したjsonexの機能はおそらく変更されませんが、名前空間、バイナリチャンク、ストリーム、スコープなどの新しい機能が追加されます。アークはかなり変化する可能性があります。



提示されたアイデアの目新しさと非ローリングの性質のため、ある程度の注意を払ってそれらを使用することをお勧めします。それでも、あなたがそれらのいくつかを好きで、あなたのプロジェクトと実験で役に立つならば、私は非常にうれしいです。また、感想、経験、発見を共有していただければ幸いです。



All Articles