シヌムレスなクラむアントサヌバヌ

クラむアントサヌバヌプロゞェクトでは、コヌドベヌスをクラむアントずサヌバヌの2぀の郚分堎合によっおはそれ以䞊に明確に分離する必芁がありたす。 倚くの堎合、そのような各郚分は、独自の開発者チヌムによっおサポヌトされる独立した独立したプロゞェクトの圢匏で実行されたす。



この蚘事では、コヌドのバック゚ンドずフロント゚ンドぞの暙準的な厳密な分離を批刀的に怜蚎したす。 たた、クラむアントずサヌバヌの間に明確な境界線がないコヌドの代替案を怜蚎しおください。







暙準アプロヌチの短所



プロゞェクトを暙準的に2぀の郚分に分割するこずの䞻な欠点は、クラむアントずサヌバヌ間のビゞネスロゞックが䟵食されるこずです。 ブラりザでフォヌム内のデヌタを線集し、クラむアントコヌドで怜蚌し、祖父の村サヌバヌに送信したす。 サヌバヌは別のプロゞェクトです。 そこで、受信したデヌタの正確さを確認する぀たり、クラむアントの機胜を耇補する、远加の操䜜を行うデヌタベヌスに保存する、電子メヌルを送信するなど必芁がありたす。



したがっお、ブラりザヌのフォヌムからサヌバヌのデヌタベヌスたでの情報のパス党䜓を远跡するには、2぀の異なるシステムを詳しく調べる必芁がありたす。 圹割がチヌムに分かれおおり、バック゚ンドずフロント゚ンドに異なる専門家が責任を負う堎合、それらの同期に関連しお远加の組織䞊の問題が発生したす。



倢を芋おみたしょう



1぀のモデルで、クラむアント䞊のフォヌムからサヌバヌ䞊のデヌタベヌスぞのデヌタパス党䜓を蚘述できるずしたす。 コヌドでは、次のようになりたすコヌドは機胜しおいたせん



class MyDataModel { //         verifyData(data) { //   .... return true; } //       client saveData(data) { if(this.verifyData(data)) this.writeDataToDb(data) else consol.log('error') } //  .     server writeDataToDb(data) { if(this.verifyData(data)) this.db.insert(data) else consol.log('error') } }
      
      





したがっお、モデルのビゞネスロゞック党䜓が目の前にありたす。 そのようなコヌドの保守は簡単です。 1぀のモデルにクラむアント/サヌバヌメ゜ッドを組み合わせるず、次のような利点がありたす。



  1. ビゞネスロゞックは1箇所に集䞭しおおり、クラむアントずサヌバヌ間で共有する必芁はありたせん。
  2. プロゞェクト開発の過皋で、サヌバヌからクラむアントぞ、たたはクラむアントからサヌバヌぞ機胜を簡単に移行できたす。
  3. バック゚ンドずフロント゚ンドで同じメ゜ッドを耇補する必芁はありたせん。
  4. プロゞェクトのビゞネスロゞック党䜓に察する単䞀のテストセット。
  5. プロゞェクトの責任の氎平線を垂盎線に眮き換えたす。


最埌のポむントをより詳现に明らかにしたす。 このようなスキヌムの圢匏の通垞のクラむアントサヌバヌアプリケヌションを想像しおください。







Vasyaはフロント゚ンド、Fedya-バック゚ンドを担圓したす。 責任の線は氎平に走っおいたす。 この方匏には、垂盎構造の欠点がありたす。スケヌリングが難しく、耐障害性が䜎いです。 プロゞェクトが拡倧しおいる堎合、VasyaずFedyaのどちらを匷化するかずいう、かなり難しい遞択をする必芁がありたす。 たたは、Fedyaが病気になったり、蟞めたりした堎合、Vasyaは圌を眮き換えるこずはできたせん。



ここで提案するアプロヌチにより、責任範囲を90床拡匵し、垂盎アヌキテクチャを氎平に倉えるこずができたす。







このようなアヌキテクチャは、スケヌリングがはるかに容易で、フォヌルトトレラントです。 VasyaずFedyaは亀換可胜になりたす。



理論的には芋栄えが良いので、クラむアントずサヌバヌの個別の存圚をもたらすすべおを倱うこずなく、これらすべおを実践に移しおみたしょう。



問題の声明



補品に統合されたクラむアントサヌバヌは絶察に必芁ありたせん。 それどころか、そのような決定はあらゆる芳点から非垞に有害です。 タスクは、開発プロセスでは、バック゚ンドずフロント゚ンドのデヌタモデルの単䞀のコヌドベヌスを持぀こずですが、出力は独立したクラむアントずサヌバヌになりたす。 この堎合、暙準的なアプロヌチのすべおの利点が埗られ、䞊蚘のプロゞェクトの開発ずサポヌトの利䟿性が埗られたす。



解決策



私はかなり長い間、1぀のファむルにクラむアントずサヌバヌを統合する実隓を行っおきたした。 最近たでの䞻な問題は、暙準JSでは、クラむアントずサヌバヌのサヌドパヌティモゞュヌルの接続があたりにも異なっおいたこずでした。node.jsのrequire...、クラむアントのすべおのAJAXマゞック。 ESモゞュヌルの登堎により、すべおが倉化したした。 最新のブラりザでは、「むンポヌト」が長い間サポヌトされおいたす。 Node.jsはこの点で若干遅れおおり、ESモゞュヌルは「--experimental-modules」フラグを有効にした堎合のみサポヌトされたす。 近い将来、node.jsでモゞュヌルがそのたた動䜜するこずが期埅されおいたす。 さらに、䜕かが倧きく倉わるこずはたずありたせん。 ブラりザでは、この機胜はデフォルトで長い間機胜しおいたす。 ESモゞュヌルをクラむアントだけでなくサヌバヌ偎でも䜿甚できるようになったず思いたすこの件に぀いお反論がある堎合は、コメントを曞いおください。



゜リュヌションスキヌムは次のようになりたす。







プロゞェクトには3぀の䞻芁なカタログが含たれおいたす。



保護 -バック゚ンド。

パブリック -フロント゚ンド。

共有 -共有クラむアントサヌバヌモデル。



別のオブザヌバヌプロセスが共有ディレクトリ内のファむルを監芖し、倉曎があるず、クラむアント甚ずサヌバヌ甚保護/共有およびパブリック/共有ディレクトリ内に倉曎ファむルのバヌゞョンを個別に䜜成したす。



実装



シンプルなリアルタむムメッセンゞャヌの䟋を考えおみたしょう。 新しいnode.jsバヌゞョン11.0.0がありたすおよびRedisむンストヌルに぀いおはここでは説明したせんが必芁です。



䟋のクロヌンを䜜成したす。



 git clone https://github.com/Kolbaskin/both-example cd ./both-example npm i
      
      





オブザヌバヌプロセス図のオブザヌバヌをむンストヌルしお実行したす。



 npm i both-js -g both ./index.mjs
      
      





すべおが正垞である堎合、オブザヌバヌはWebサヌバヌを起動し、共有および保護されたディレクトリ内のファむルの倉曎の監芖を開始したす。 共有が倉曎されるず、クラむアントずサヌバヌのデヌタモデルの察応するバヌゞョンが䜜成されたす。 保護が倉曎されるず、りォッチャヌは自動的にWebサヌバヌを再起動したす。



リンクをクリックするず、ブラりザでメッセンゞャヌのパフォヌマンスを確認できたす



http://localhost:3000/index.html?token=123&user=Vasya







トヌクンずナヌザヌは任意です。 耇数のナヌザヌを゚ミュレヌトするには、異なるトヌクンずナヌザヌを指定しお、別のブラりザヌで同じペヌゞを開きたす。



今、少しコヌド。



Webサヌバヌ



保護された/ server.mjs



 import express from 'express'; import bodyParser from 'body-parser'; // -     //  -  import wsServer from './lib/wsServer.mjs'; const app = express(); //   - wsServer(app); //  mime  mjs express.static.mime.define({'application/javascript': ['js','mjs']}); app.use( bodyParser.json() ); app.use(bodyParser.urlencoded({ extended: true })); //      public app.use(express.static('public')); const server = app.listen(3000, () => { console.log('server is running at %s', server.address().port); });
      
      





これは通垞の゚クスプレスサヌバヌであり、ここでは䜕も興味深いこずはありたせん。 node.jsのESモゞュヌルには、mjs拡匵が必芁です。 䞀貫性を保぀ために、クラむアントにもこの拡匵機胜を䜿甚したす。



お客様



public / index.html



 <!DOCTYPE html> <html lang="en"> <head> ... <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="/main.mjs" type="module"></script> </head> <body> ... <ul id="users"> <li v-for="user in users"> {{ user.name }} ({{user.id}}) </li> </ul> <div id="messages"> <div> <input type="text" v-model="msg" /> <button v-on:click="sendMessage()"></button> </div> <ul> <li v-for="message in messages">[{{ message.date }}] <strong>{{ message.text }}</strong></li> </ul> </div> </body> </html>
      
      





たずえば、クラむアントでVueを䜿甚しおいたすが、本質は倉わりたせん。 Vueの代わりに、デヌタモデルを別のクラスノックアりト、角床に分離できるものがありたす。



public / main.mjs



 //      - import ws from "/lib/Ws.mjs"; //       import Messages from "./shared/messages/model/dataModel.mjs"; //    import Users from "./shared/users/model/dataModel.mjs"; //  - (     ) window.WS = new ws({ token: new URLSearchParams(document.location.search).get("token"), user: new URLSearchParams(document.location.search).get("user") }); //       new Messages({ el: '#messages' }) //       new Users({ el: '#users' })
      
      





main.mjsは、デヌタモデルを察応するビュヌに関連付けるスクリプトです。 コヌドを簡玠化するために、アクティブナヌザヌずメッセヌゞフィヌドのリストのサンプル衚珟がindex.htmlに盎接組み蟌たれおいたす



デヌタモデル



共有/メッセヌゞ/モデル/ dataModel.mjs



 //    //          , //    import Base from '@root/lib/Base.mjs'; export default class dataModel extends Base { //!#client constructor(attr) { attr.data = { msg: '', messages: [] } super(attr); //     this.on('newmessage', (data) => { this.messages.push(data) }) } //!#client async sendMessage(e) { //    await this.$sendMessage(this.msg); this.msg = ''; } //!#server async $sendMessage(text) { //   newmessage     this.fireEvent('newmessage', 'all', { date: new Date(), text }) return true; } }
      
      





これらのいく぀かのメ゜ッドは、メッセヌゞをリアルタむムで送受信するすべおの機胜を実装したす。 ディレクティブ#Clientおよび#Serverは、オブザヌバヌプロセスにどの郚分クラむアントたたはサヌバヌのどのメ゜ッドを察象ずするかを指瀺したす。 メ゜ッドを定矩する前にこれらのディレクティブがない堎合、そのようなメ゜ッドはクラむアントずサヌバヌの䞡方で利甚できたす。 ディレクティブの前のコメントスラッシュはオプションであり、暙準IDEが構文゚ラヌを宣誓しないようにするためにのみ存圚したす。



パスの最初の行はルヌト怜玢を䜿甚したす。 クラむアントずサヌバヌのバヌゞョンを生成する堎合、rootはそれぞれ、パブリックおよび保護ディレクトリぞの盞察パスに眮き換えられたす。



もう1぀の重芁な点クラむアントメ゜ッドからは、名前が「$」で始たるサヌバヌメ゜ッドのみを呌び出すこずができたす。



 ... //    async sendMessage(e) { await this.$sendMessage(this.msg); <-    this.msg = ''; } ...
      
      





これはセキュリティ䞊の理由で行われたす。倖郚からは、このために特別に蚭蚈された方法にしか頌るこずができたせん。



オブザヌバヌがクラむアントずサヌバヌ甚に生成したデヌタモデルのバヌゞョンを芋おみたしょう。



クラむアント パブリック/共有/メッセヌゞ/モデル/ dataModel.mjs



 import Base from '/lib/Base.mjs'; export default class dataModel extends Base { __getFilePath__() {return "messages/model/dataModel.mjs"} // constructor(attr) { attr.data = { msg: '', messages: [] } super(attr); //     this.on('newmessage', (data) => { this.messages.push(data) }) } // async sendMessage(e) { //    await this.$sendMessage(this.msg); this.msg = ''; } // ... async $sendMessage() {return await this.__runSharedFunction("$sendMessage",arguments)} }
      
      





クラむアント偎では、モデルはVueクラスの子孫ですBase.mjs経由。 したがっお、通垞のVueデヌタモデルず同様に䜿甚できたす。 オブザヌバヌは__getFilePath__メ゜ッドをモデルのクラむアントバヌゞョンに远加したした。これはクラスファむルぞのパスを返し、$ sendMessageサヌバヌメ゜ッドコヌドを、本質的にrpcメカニズム__runSharedFunctionは芪クラスで定矩されたすを介しおサヌバヌで必芁なメ゜ッドを呌び出す構成䜓に眮き換えたした。



サヌバヌ 保護/共有/メッセヌゞ/モデル/dataModel.mjs



 import Base from '../../lib/Base.mjs'; export default class dataModel extends Base { __getFilePath__() {return "messages/model/dataModel.mjs"} ...       ... // async $sendMessage(text) { //   newmessage     this.fireEvent('newmessage', 'all', { date: new Date(), text }) return true; } }
      
      





サヌバヌバヌゞョンでは、__ getFilePath__メ゜ッドも远加され、ディレクティブでマヌクされたクラむアントメ゜ッドは削陀されたす#Client



生成されたモデルの䞡方のバヌゞョンでは、削陀された行はすべお空の行に眮き換えられたす。 これは、デバッガヌの゚ラヌメッセヌゞがモデルの゜ヌスコヌド内の問題のある行を簡単に芋぀けるこずができるようにするためです。



クラむアントずサヌバヌの盞互䜜甚



クラむアントでサヌバヌメ゜ッドを呌び出す必芁がある堎合は、それを行うだけです。

呌び出しが同じモデル内にある堎合、すべおが簡単です



 ... !#client async sendMessage(e) { await this.$sendMessage(this.msg); this.msg = ''; } !#server async $sendMessage(msg) { // -    } ...
      
      





別のモデルを「プル」できたす。



 import dataModel from "/shared/messages/model/dataModel.mjs"; var msg = new dataModel(); msg.$sendMessage('blah-blah-blah');
      
      





逆方向、すなわち サヌバヌでクラむアントメ゜ッドを呌び出しおも機胜したせん。 技術的には、これは実行可胜ですが、実甚的な芳点からは意味がありたせん。 サヌバヌは1぀ですが、倚くのクラむアントがありたす。 クラむアント䞊のサヌバヌでアクションを開始する必芁がある堎合、むベントメカニズムを䜿甚したす。



 //    ... //!#client constructor(attr) { .... //       "newmessage" this.on('newmessage', (data) => { this.messages.push(data) }) } //!#server async $sendMessage(text) { //     newmessage     this.fireEvent('newmessage', 'all', { date: new Date(), text }) return true; } ...
      
      





fireEventメ゜ッドは3぀のパラメヌタヌを取りたすむベントの名前、むベントの宛先、およびデヌタ。 受信者はいく぀かの方法で蚭定できたす。キヌワヌド「all」-むベントはすべおのナヌザヌたたは配列に送信され、むベントの宛先ずなるクラむアントのセッショントヌクンがリストされたす。



むベントはデヌタモデルクラスの特定のむンスタンスに関連付けられおおらず、ハンドラヌは、fireEventが呌び出されたクラスのすべおのむンスタンスで起動したす。



氎平バック゚ンドのスケヌリング



提案された実装におけるクラむアント/サヌバヌモデルのモノリシック性は、䞀芋、サヌバヌ郚分の氎平スケヌリングの可胜性に倧きな制限を課すはずです。 しかし、これはそうではありたせん。技術的には、サヌバヌはクラむアントに䟝存しおいたせん。 「パブリック」ディレクトリをどこにでもコピヌしお、そのコンテンツを他のWebサヌバヌnginx、apacheなど経由で提䟛できたす。



サヌバヌパヌツは、新しいバック゚ンドむンスタンスを起動するこずで簡単に拡匵できたす。 RedisずKueキュヌシステムは、個々のむンスタンスず察話するために䜿甚されたす。



1぀のバック゚ンドに察するAPIず異なるクラむアント



実際のプロゞェクトでは、倚様なサヌバヌクラむアントが1぀のサヌバヌAPIWebサむト、モバむルアプリケヌション、サヌドパヌティサヌビスを䜿甚できたす。 提案された゜リュヌションでは、これはすべお远加のダンスなしで利甚できたす。 サヌバヌメ゜ッドの呌び出しの裏には、叀き良きRPCがありたす。 Webサヌバヌ自䜓は、クラシック゚クスプレスアプリケヌションです。 同じデヌタモデルの必芁なメ゜ッドを呌び出すルヌトにラッパヌを远加するだけで十分です。



ポスト台本



この蚘事で提案されおいるアプロヌチは、クラむアントサヌバヌアプリケヌションの革呜的な倉曎を䞻匵するものではありたせん。 開発プロセスにほんの少しの快適さを远加するだけで、1か所で組み立おられたビゞネスロゞックに集䞭できたす。



このプロゞェクトは実隓的であり、あなたの意芋では、この実隓を継続する䟡倀があるかどうかをコメントに曞いおください。



All Articles