精神:Node.js MVCフレームワーク



やあみんな! これからは、名前がSpiritとなるnode.js



用の独自のMVCフレームワークの作成に関する詳細を含む一連の記事を始めたいと思います。



最初の記事は4つのパートで構成されます。

1.フレームワークのアイデアと使命

2.サーバーのセットアップ

3.フレームワークフレームワークの作成

4.高度で便利なルーターの作成



この記事は巨大であり、大量のテキストと大きなコードブロックがあることをすぐに警告します。







フレームワークのアイデアと使命



インスピレーション、気分、時間(今では十分ではありません)が現れると、スピリットはゆっくりと発達します。 批判や提案を歓迎しますが、私は自分のビジョンに従って開発し、最初の記事がコミュニティによって温かく受け入れられた場合、各論理セクションの詳細を記事の形でレイアウトします。



主な目標は、プラットフォームのトレーニングと関心の育成です。 誰かがプロジェクトを分岐し、それに基づいてフレームワークを作成した場合、私は非常に幸せになります。あなたは私に道徳的な支援を求めることさえできます。 それでも、あなたがあなたの行動の詳細な説明、自分自身のポジティブな(本当の)カルマを獲得する方法でスピリットの開発に参加したいなら、shocksilien @ gmail.comに書いてください、私たちは議論します)。



すべてのソースコードはGithubのgithub.com/theshock/spiritで入手できます。



詳細を説明しますが、プレゼンテーションスタイルは、読者が少なくとも管理、特にjavascriptとnode.jsのプログラミングの基本を知っており、CMFについてのアイデアがあることを意味します。 いくつかの場所では、記事をアンロードするために、コメントのみを残してコードの一部を切り取り、自分でそれらを思い付くよう願っています。 いずれにせよ、完全で実用的な例がGitHubにあります。



例では、Debianシステムを使用しており、ユーザーのホームディレクトリは「/ home / spirit」であると想定しています。 特に指定がない限り、サイトはspirit.example.comでホストされます。



サーバーのセットアップ



nginxやapacheとは異なり、node.jsはそのままではサーバー上で自動的に起動せず、いくつかの設定が必要です。



最初にinit



を使用してデーモンを作成し、次にmonit



使用してサーバーがクラッシュしたかどうかmonit



確認します。 このトピックは長い間描かれてきました。情報はGoogleの nodejs.ruなどにあります。 このアプローチは、node.jsリンクレデューサーの例で個人的にテストされ、承認されました。数時間で15,000以上のリンクが削減され、まだ揺れさえしませんでした(約20日間)。



2番目のステップは、フロントエンドとしてのNginxです。

 server { listen 80; server_name spirit.example.com; access_log /home/spirit/example-app/logs/nginx.access.log; location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8124/; } location ~* \.(html|js|ico|gif|jpg|png|css|doc|xml|rar|zip|swf|avi|mpg|flv|mpeg|wmv|ogv|oga|ogg)$ { root /home/spirit/example-app/public; } }
      
      





非常に単純なコードが表示されます。nginxは、静的を除くすべてをnode.jsにリダイレクトします。node.jsはポート8124(または指定したもの)に配置されます。 静的変数は、node.jsをNginxに直接関与させることなく提供されます。



フレームワークスケルトンを作成する



そのため、このようなディレクトリ構造に努めます。

/home/spirit/

lib/

│ ├ mysql-libmysqlclient/

│ ├ spirit/

│ └ MooTools.js

example-app/

│ ├ engine/

│ │ ├ classes/

│ │ └ init.js

│ ├ logs/

│ └ public/

another-example-app/

engine/

│ ├ classes/

│ └ init.js

logs/

public/







この図には、2つの重要なアイデアがあります。

1.すべてのライブラリを含む。 また、Spiritはアプリケーション自体とは別のディレクトリに配置されます。これにより、すべてのライブラリのリストを複製することなく、複数のアプリケーションを同じサーバーに保持できます。

2.各アプリケーションには2つのキーディレクトリがあります。エンジンには、サーバーロジックとパブリックを格納し、そのすべてのコンテンツをクライアントに提供します。 これにより、外部からの侵入からサーバーコードを保護し、異なるロジックを区別します。



lib



ディレクトリに、必要なすべてのライブラリをダンプします。



Spiritは、変更されたMooToolsに基づいており、 リンク短縮に関するトピックでどのように接続しかを示しています。 接続を簡単にするために、 MooTools.More Class.Bindslib/MooTools.js



ファイルに直接接続しました。これにより、 コンテキストを失うことなく 、引数としてオブジェクトのメソッドを渡すことができlcfirst







アプリケーションの初期化



コードでライブラリを接続(必須)するには、次のいずれかの方法を使用します。

1.名前で。 たとえば、 require( 'fs' )



。 したがって、カーネルに接続され、ドキュメントで説明されているライブラリの1つを接続します

2.現在のファイルからの相対パスに従って。 たとえば、 require( './classes/foo' )



またはrequire( '../lib/mootools.js' )





3.絶対パス。 たとえば、 require( '/home/spirit/lib/mootools' )







実際には、2番目と3番目の方法はjavascriptファイルへのリンクですが、最後に.jsがありません(必須です。それ以外の場合はエラーをキャッチする可能性があります)。 相対パスは一貫性なく動作し、環境に依存し、各ファイルには異なるルートがあるため、可能な限り相対パスを使用しないようにしてください。

 // __dirname -  ,   //  ,      var libPath = __dirname + '/../../lib'; //  : /home/spirit/lib //            // fs.realpathSync(libPath),     //    MooTools   , //      require(libPath + '/MooTools').apply(GLOBAL); //       var spirit = require(libPath + '/spirit/spirit') .createSpirit(__dirname, libPath); //     -   //    ,   require('http').listen spirit.listen("127.0.0.1:8124");
      
      







メインクラスを書きます



エクスポートを可能にするために渡したいものすべて-exportsオブジェクトに追加する必要があります。 つまり、 var foo = new (require( './bar' ). bar );



bar.js



ファイルでこれを行う必要があります: exports . bar = new Class({ /* ... */ });



exports . bar = new Class({ /* ... */ });



。 まず、このような変数をドットではなく角括弧で宣言し、エディターでは文字列として強調表示されます(通常は最小です)。これにより、操作するクラスの名前が強調表示されます。 第二に、最初はこのアプローチが好きではなく、不便でさえありました(上記の例では、 bar



2回繰り返す必要があることがわかりbar



)が、最後にそれを回して手に入れるようにします-それは便利で美しいでしょう。 したがって、フレームワークのメインクラスを作成する関数:

 exports['createSpirit'] = function (projectPath, libPath) { return new Spirit(projectPath, libPath); };
      
      







クラス自体に、次のアイデアを実装します。

1.すべてのクラスはapp/classes



およびlib/spirit/classes





2.クラスを名前でロードする場合、最初にアプリケーションディレクトリをチェックし、次にフレームワークをチェックします(特に明記しない限り)。 これにより、必要に応じてフレームワーククラスをオーバーロードできます(この部分については後で説明します)

3.ロードされたクラス-キャッシュされます。これにより、FSへの不必要なアクセスが回避されます(またはnode.jsにキャッシュされますか?)

4.クラスは、他のすべてのアクションの開始点になります。



クラスコードをpastbinに投稿しますが、ここではインターフェイスのみを説明します



pastebin.com/0b14MEbe

 var Spirit = new Class({ Binds: ['hit', 'load'], initialize : function (String projectPath, String libPath) { }, load : function (String className, Boolean fromSpirit) { //   load    , : // spirit.load('Controllers.User'); //     ,  ,    -   //  fromSpirit    //  ,    }, loadLib : function (libName) { //     , : // spirit.loadLib('mysql-libmysqlclient') //     libPath //           }, //       hit : function (req, res) { }, listen : function (port, url) { //     //  .listen(8124, '127.0.0.1')  //  .listen('127.0.0.1:8124') } }); var load = function (path, className) { // some code is here //         //    Spirit.      ? //  ! return data[className](this); };
      
      







したがって、このアプローチは、クラスを作成する次のスタイルにつながります。

 exports['Name.Space.ClassName'] = function (spirit) { return new Class({ method : function (msg) { this.response.writeHead(200, {'Content-Type': 'text/plain'}); this.response.end(msg); } }); };
      
      







これがなぜ必要なのかはまだ明らかではありません。

 exports['Helper.AnotherClass'] = function (spirit) { var mysql = spirit.loadLib('mysql-libmysqlclient'); return new Class({ Extends : spirit.load('Name.Space.ClassName'), start : function (msg) { this.method('Started'); }, query : function () { // using mysql } }); };
      
      







そして、 Name.Space.ClassName



どのようにName.Space.ClassName



ますか? require( '../Name/Space/ClassName' );



? クラスを別の場所に移した場合、すべての方法を書き直しますか? 完全なパスを入力してライブラリをロードしますか?

別の例を見てみましょう。 フレームワークにすべてのヒットを処理するRouterクラスがあるとしましょう:

 exports['Router'] = function (spirit) { return new Class({ // .. hit : function (request, response) { // some code }, // .. }); };
      
      







ヒットロギングを導入したいと思います。 アプリケーションのディレクトリでRouterクラスを宣言します。



 exports['Router'] = function (spirit) { var Logger = spirit.load('Logger'); var logger = new Logger; return new Class({ Extends : spirit.load('Router', true), hit : function (request, response) { logger.log('hit!'); this.parent(request, response); }, // .. }); };
      
      







したがって、クラスを拡張して、フレームワークの動作を変更し、ソースコードにアクセスすることはできません。



ルーターを作成する



ルーターはリクエストを解析し、実行のために目的のコントローラーに渡します。

ルーティングは次のように行われます。

1.最初に、正規表現を使用して、手動で追加されたルートのアドレスの1つとの一致がチェックされます。 これにより、デフォルトルーターの原則に該当しない特別なURLを入力できます。

2.段落2でルートが見つからない場合、アドレスはスラッシュで区切られ、最も近いアドレスがコントローラー間で検索されます。 アドレスurl / AA / BB / CC / DD / EEで、コントローラAA.BB.CC.DD.EEが最初に検索され、次にAA.BB.CC.DDなどが、目的のコントローラが見つかるまで検索されます。 そのようなコントローラーがない場合、Indexコントローラーが代用されます。 AA.BB.CC.DDコントローラーはないが、AA.BB.CC.DD.Indexがあれば、それが選択されます。 次に、メソッドが選択され、他のすべてが引数として渡されます。



すべてのコントローラークラスは、アプリケーションの初期化中に読み込まれます。これにより、各リクエストでの不必要な誤計算が回避されます。



アプリケーションのディレクトリは次のようになります。

/home/spirit/example-app/

engine/

│ ├ classes/

│ │ ├ controllers/

│ │ │ ├ Admin/

│ │ │ │ ├ Articles.js

│ │ │ │ └ Index.js

│ │ │ ├ Man/

│ │ │ │ ├ Index.js

│ │ │ │ └ Route.js

│ │ │ ├ Index.js

│ │ │ └ Users.js

│ │ └ Controller.js

│ └ init.js

logs/

public/









次のコードをアプリケーションの初期化ファイルに追加します。これは、手動ルートへのアプローチを示します。 :A 、: D 、: H 、: Wは、それぞれ[az]、[0-9]、[0-9a-f]、[0-9a-z]のパターンに対応しています。 <and>は、この式がアドレスの最初と最後にそれぞれ現れるようにする責任があります。 したがって、 </test-:A>



などの式はURL " /test-abc123



"と一致しませんが、式/test-:A



/test-abc123



/test-:A



このURLと一致します。 すべてのテンプレートは配列に追加され、コントローラーメソッドが呼び出されたときに引数として渡されます。 argsMapがある場合、ハッシュが送信されます。 たとえば、アドレスが「 /articles-15/page-3



」の場合、最初の引数は配列[15, 3]



argsMap : ['id', 'page']



[15, 3]



になりますが、 argsMap : ['id', 'page']



を渡すと、ハッシュ{id:15, page:3}



がメソッドに渡されます{id:15, page:3}





 spirit.createRouter() .addRoutes( { route : "</article-:D/page-:D" , contr : 'Man.Route:articleWithPage' , argsMap : ['id', 'page'] }, { route : "</article-:D" , contr : 'Man.Route:article' , argsMap : ['id'] }, { route : "</~:W>" , contr : 'Man.Route:user' }, { route : "</hash-:H>" , contr : 'Man.Route:hash' } );
      
      







そして、このコードで動作するコントローラーは次のとおりです(上の図では太字で強調表示されています)。

 exports['Controllers.Man.Route'] = function (spirit) { return new Class({ Extends : spirit.load('Controller'), indexAction : function () { this.exit('(Man.Route) index action'); }, testAction : function () { this.exit('(Man.Route) test action'); }, articleWithPageAction : function (args) { this.exit('(Man.Route) article #' + args.id + ', page #' + args.page); }, articleAction : function (args) { this.exit('(Man.Route) article #' + args.id); }, hashAction : function (args) { this.exit('(Man.Route) hash: ' + args[0]); }, userAction : function (args) { this.exit('(Man.Route) user: ' + args[0]); } }); };
      
      





そして、すべての子がexit



メソッドを設定するために使用する親コントローラー:

 exports['Controller'] = function (spirit) { return new Class({ exit : function (msg) { this.response.writeHead(200, {'Content-Type': 'text/plain'}); this.response.end(msg); } }); };
      
      







まず、Spiritクラスを拡張する必要があります。リクエストの分析を完全にルーターの肩に転送します。

  createRouter : function () { var Router = this.load('Router'); var router = new Router(); router.spirit = this; this.router = router; router.init(); return router; }, hit : function (req, res) { this.router.hit(req, res); },
      
      







また、ルーター自体も特に積み上げられることはなく、主な仕事をその代理に与えます。

 exports['Router'] = function (spirit) { var RouterHelper = spirit.load('Router.Helper'); return new Class({ init : function () { var path = this.spirit.requirePath + 'Controllers'; this.routerHelper = new RouterHelper(this); this.routerHelper.requireAll(path); }, hit : function (request, response) { var contrData = this.routerHelper.route(request); var contr = contrData.contr; contr.spirit = this.spirit; contr.request = request; contr.response = response; if (typeof contr.before == 'function') contr.before(); contr[contrData.method](contrData.args); if (typeof contr.after == 'function') contr.after(); }, addRoutes : function () { var rh = this.routerHelper; rh.addRoutes.apply(rh, arguments); } }); };
      
      







また、代理人には2つの部下がいます-手動で追加されたルートを担当する部下(RouterRegexp)と、デフォルトの方法でルーティングする部下(RouterPlain)。 requireAllメソッドに注意してください-同期スタイルでは、コントローラーのディレクトリが再帰的に再帰され、すべてのクラスが接続されます。 この場合、このメソッドはプロジェクトの初期化時にのみ呼び出されるため、非同期は必要ありませんが、実際のコードではそのようなことを非同期スタイルで記述することをお勧めします-ファイルシステムにアクセスするのにかかる時間はコード実行プロセスを遅くしません。 Node.jsが好きなのは、他の言語とは異なり、すべてのメソッド名が明確で、美しく、短く、同じスタイルに準拠しているためです。



 var fs = require('fs'); exports['Router.Helper'] = function (spirit) { var RouterPlain = spirit.load('Router.Plain'); var RouterRegexp = spirit.load('Router.Regexp'); return new Class({ initialize : function (router) { this.router = router; this.plain = new RouterPlain(this); this.regexp = new RouterRegexp(this); }, route : function (request) { var url = request.url; return this.regexp.route(url) || this.plain.route(url); }, requireAll : function (path) { var files = fs.readdirSync(path); for (var i = 0; i < files.length; i++) { var file = path + '/' + files[i]; var stat = fs.statSync(file); if (stat.isFile()) { this.addController(file); } else if (stat.isDirectory()) { this.requireAll(file); } } this.checkAllIndexActions(); }, //       regexp  addRoutes : function (routes) {}, //  ".js"       removeExt : function (file) {}, //      indexAction checkAllIndexActions : function () {}, //       addController : function (file) {}, //     false,   createController : function (name) {} }); };
      
      







まず、 addRoutes



を使用して渡されるレギュラーを解析する必要があります

 exports['Router.Regexp'] = function (spirit) { return new Class({ initialize : function (routerHelper) { this.routerHelper = routerHelper; }, route : function (url) { //            for (var i = 0; i < this.routes.length; i++) { var route = this.routes[i]; //           //      lastIndex,  //        , //   route.regexp.lastIndex = 0; var result = route.regexp.exec(url); if (result) { return { contr : this.routerHelper .createController(route.contr.name), method : route.contr.method, args : this.regexpRouteArgs(result, route.argsMap) }; } } return false; }, routes : [], addRoute : function (route, controller, argsMap) { }, addRoutes : function (hash) { }, //     ,   addRoutes //         , //  ( )) -    regexpContr : function (string) { var parts = string.split(':'); var method = parts.length > 0 ? parts[1] + 'Action' : 'indexAction'; var contr = 'Controllers.' + parts[0]; // ... }, //         //      // ,        regexpRoute : function (route) { var re = new RegExp(); re.compile(this.prepareRegexp(route), 'ig'); return re; }, replaces : { A : '([az]+)', D : '([0-9]+)', H : '([0-9a-f]+)', W : '([0-9a-z]+)', }, prepareRegexp : function (route) { return route .escapeRegExp() .replace(/>$/, '$') .replace(/^</, '^') .replace(/:([ADHW])/g, function ($0, $1) { return this.replaces[$1]; }.bind(this)); }, //  ,   regexp.exec  //    input  lastIndex  //  ,   argsMap regexpRouteArgs : function (result, argsMap) { }, }); };
      
      







正規表現のルーターが機能しなかった場合、デフォルトでルーターを使用します。

 var url = require('url'); exports['Router.Plain'] = function (spirit) { return new Class({ initialize : function (routerHelper) { this.routerHelper = routerHelper; }, route : function (url) { var parts = this.getControllerName(url); var controller = this.routerHelper.createController(parts.name); var method = 'indexAction'; if (parts.args.length) { var action = parts.args[0].lcfirst(); //    -      //     -  ,  -  indexAction if (typeof controller[action + 'Action'] == 'function') { method = action + 'Action'; parts.args.shift(); } } return { contr : controller, method : method, args : parts.args }; }, getControllerName : function (url) { var controllers = this.routerHelper.controllers; //  pathname       var path = this.splitUrl(url); var name, args = []; do { if (!path.length) { //     - name = 'Controllers.Index'; break; } //         name = 'Controllers.' + path.join('.') + '.Index'; if (controllers[name]) break; //  -      name = 'Controllers.' + path.join('.'); if (controllers[name]) break; //    -    args.unshift(path.pop()); } while (true); return { name : name, args : args }; }, splitUrl : function (urlForSplit) { return url .parse(urlForSplit, true) .pathname.split('/') .filter(function (item) { return !!item; }) .map(function (item) { return item.ucfirst(); }); }, }); };
      
      







クラス拡張



UPD:リポジトリ内のリビジョンを比較する場合など、URLで2つの異なるファイルへのリンクを指定する必要がある場合、拡張ルーターを表示するように求められたコメントで。 ただし、リンクは識別子ではなく、パス(たとえば、「 shock/spirit/init.js/f81e45



」)である必要があります。 このリンクテンプレートを使用することをお勧めします。

http://example.com/compare/( shock/spirit/init.js/f81e45 )/( tenshi/spirit/src/init.js/d5d218 )



、各ファイルは括弧内に示されています。 しかし、フレームワークツールはこれを許可しません。 関係ありません プロジェクトでは(フレームワークに触れることなく)Router.Regexpクラスを作成します。



 exports['Router.Regexp'] = function (spirit) { return new Class({ Extends : spirit.load('Router.Regexp', true), prepareRegexp : function (route) { return this.parent(route) .replace(/:P/g, '([0-9a-z._\\/-]+)'); } }); };
      
      





新しい修飾子-":P"が導入されました。 Router.Regexp.replacesオブジェクトを単純に拡張する方がおそらく正しいでしょうが、メソッドのオーバーロードの可能性を示したかったのです。 さて、init.jsに新しいルートを追加します。



 spirit.createRouter() .addRoutes( // ... { route : "</compare/(:P)/(:P)>" , contr : 'Man.Route:compare' } );
      
      





そして、メソッドをMan.Routeに追加します。

  compareAction : function (args) { this.exit('Compare "' + args[0] + '" and "' + args[1] + '"'); }
      
      





リンクhttp://example.com/compare/( shock/spirit/init.js/f81e45 )/( tenshi/spirit/src/init.js/d5d218 )



http://example.com/compare/( shock/spirit/init.js/f81e45 )/( tenshi/spirit/src/init.js/d5d218 )



て答えを得る:

Compare " shock/spirit/init.js/f81e45 " and " tenshi/spirit/src/init.js/d5d218 "







おわりに



そこで、プロジェクトを作成し、サーバーにWeb上で表示させ、クラスを含める方法を学び、アドレスをソートして、必要なコントローラーにアクションの実行を送信しました。 次の記事では、ある種のテンプレートエンジンの形式でViewと、オブジェクトに関する情報を格納するModelを接続します。 いくつかの記事では、フレームワークについてブログを書きます。 面白い?



All Articles