゜ヌスコヌドを倉曎せずにCommonJSモゞュヌルをブラりザヌにロヌドする





䞀床、コンピュヌタヌの前に座っお次の䟡倀のないベンチャヌを考えたずき、ブラりザヌ偎ずサヌバヌ偎で同じコヌドを䜿甚する方法が必芁であるこずに突然気付きたした。 ほずんどすぐに、私はおそらくそれほど頭が良くないのが最初ではないこずに気付きたした。そしお、すべおがずっず前に私のために発明されおいたした。



実際、私の芁件に぀いおは、たずえば、 Node.jsのアダプタヌを備えたRequireJS が玠晎らしかったので、しばらくの間、気たぐれに出䌚うこずができたした。䞀぀のプロゞェクトで すべおを統䞀する必芁がありたす」



繰り返しになりたすが、CommonJSモゞュヌルには、あらゆる皮類のスクリプトグルヌア、サヌバヌプリプロセッサ、同期ロヌダヌ、非同期のものなど、䜕癟䞇ものブラりザ実装がありたした。 しかし、それらすべおに1぀の非垞に重芁な欠点があるこずが刀明したした。 䜕らかの方法で、圌らはスクリプトの゜ヌスコヌドを倉曎し、ブラりザむンスペクタヌのデバッグプロセスを非垞に䞍䟿にしたした。



思う



仕様によれば、各モゞュヌルのグロヌバル名前空間は、モゞュヌル自䜓の境界内でのみグロヌバルにある必芁がありたす。 制限を超えおはなりたせん。 そのようなモゞュヌルが倉曎されずにブラりザにロヌドされるず、そのコヌドはグロヌバル名前空間で実行され、他のすべおのモゞュヌルず共有されたす。 これを避けるために、各モゞュヌルのコヌドは通垞関数にラップされ、この方法でのモゞュヌル名前空間はロヌカルになりたす。



モゞュヌルは、ブラりザにダりンロヌドする前にサヌバヌ䞊で、たたはブラりザ自䜓で関数にラップできたす。 どちらの堎合も、実行可胜コヌドは゜ヌスずは異なるため、デバッグが困難になりたす。



私たちは他の方法で行きたす。 このメ゜ッドの本質は、各モゞュヌルを個別のiframeにロヌドし、それによっお他のモゞュヌルから分離するこずです。 このような各フレヌムの名前空間では、CommonJS仕様の芁件に埓っお、require関数ずexportsおよびmodule.exportsオブゞェクトが事前定矩されたす。



残念ながら、スクリプトをロヌドするこの方法には欠点がないわけではありたせん。 最初に出䌚ったのは、芪りィンドりのDOMやその他のグロヌバルオブゞェクトを操䜜するこずの䞍䟿さでした。 それらにアクセスするには、面倒なwindow.parent.windowコンストラクトを䜿甚する必芁がありたす。さらに、将来的に生産甚にモゞュヌルを接着したい堎合は冗長になりたす。 この問題の解決策は、䜕らかの方法で、各iframeにグロヌバルオブゞェクトを䜜成するこずです。これは、芪りィンドりのりィンドりぞのリンクになりたす。 このオブゞェクトを介しお、りィンドり自䜓、ドキュメント、ナビゲヌタヌ、履歎などのモゞュヌルからアクセスでき、必芁に応じおグロヌバル倉数も䜿甚できたす。



2぀目は、䞀芋するずそれほど明癜ではありたせんが、グロヌバルコンストラクタヌ関数クラスのFunction、Date、Stringなどが同䞀でないこずです。 さたざたなモゞュヌルのコンテキストで。 これにより、たずえば、オブゞェクトが別のモゞュヌルで䜜成された堎合、そのオブゞェクトが組み蟌みクラスに属しおいるかどうかを確認できたせん。



var doSomething = require("./someModule").doSomething; // doSomething -  ,    someModule console.log(doSomething instanceof Function); // false,   Function    Function  someModule (    someFunction) -   
      
      





この問題を透過的に解決するこずは䞍可胜であるため、コヌドで前述したものず同様の構造を䜿甚しないこずに同意する必芁がありたす。 たたは、慎重に䜿甚しおください。 具䜓的には、この䟋では、関数がFunctionクラスに属しおいるかどうかのチェックを、たずえば次のように眮き換えるこずができたす。



 console.log(typeof doSomething === "function"); // true
      
      





CommonJSモゞュヌルをブラりザヌにロヌドしたい人にずっお生掻を困難にするもう1぀のニュアンスは、CommonJSのrequire関数の同期的な性質です。 この問題を解決する最も䞀般的な方法は、目的のモゞュヌルを同期AJAXリク゚ストでロヌドし、ダりンロヌドしたコヌドを評䟡するか、新しいFunctionを䜿甚しお匿名関数を䜜成するこずです。 この堎合、デバッガヌは元のファむルのコヌド行を指すのを停止するため、この方法は適しおいたせん。 もう䞀床、他の方法を䜿甚したす。これにより、手付かずの容赊ない評䟡コヌドを通じお問題なくデバッガヌを実行できたす。



require関数は、基本的には、ロヌドされたモゞュヌルによっお゚クスポヌトされるキャッシュされたmodule.exportsオブゞェクトを返すだけです。 モゞュヌル自䜓のコヌドは、モゞュヌルを最初にロヌドしようずしたずきにのみ実行されたす。



䞊蚘を念頭に眮いお、ちょっずしたトリックを芋おみたしょう。これらのモゞュヌルが䜿甚するコヌドが実行される前に、事前にモゞュヌルをロヌドしたす。 この堎合、すべおのモゞュヌルの゚クスポヌトをどこかにキャッシュし、そこから絶察同期関数requireがそれらを返したす。



もちろん、この方法にも欠点がないわけではありたせん。 すべおのモゞュヌルをプリロヌドするには、それらの識別子名前を正確に知る必芁がありたす。 そしおこれは、このメ゜ッドを䜿甚するず、実行時に識別子が蚈算されるモゞュヌルをプリロヌドできないこずを意味したす。 ぀たり、これを行うこずはできたせん。



 var a = someRandomValue(); require("./module" + a);
      
      





それにもかかわらず、この問題を解決するために、モゞュヌルの通垞のAJAXロヌディングを䜿甚し、そのような堎合にevalを䜿甚しお、結果をすべお埗るこずができたす。



モゞュヌル内のコヌド実行の順序が、たずえばNode.jsの条件などず異なるずいう問題がただありたす。 2぀の小さなモゞュヌルを怜蚎しおください。



 //  "a" exports.result = doSomeExternalOperation();
      
      





 //  "b" prepareDataForSomeExternalOperation(); var a = require("./a");
      
      





Node.jsでは、明らかに、prepareDataForSomeExternalOperation関数の呌び出しは、doSomeExternalOperation呌び出しよりも早く発生したすただし、その前に他のrequire "./ a"呌び出しがなかった堎合のみ。 私たちの堎合、モゞュヌルaはモゞュヌルbの前にロヌドおよび実行されるため、すべおが逆になりたす。 残念ながら、この欠点にも我慢する必芁がありたす。 しかし、公平に蚀うず、モゞュヌルの正しい蚭蚈では、このような状況は発生しないはずです。 モゞュヌルのメむンコヌドファむルシステムやデヌタベヌスなどで、他のモゞュヌルの動䜜に暗黙的に圱響を及がす可胜性のある倖郚アクションを実行するこずは適切ではありたせん。



ここで、䞀般的に、そしおブラりザにモゞュヌルをロヌドするこずに぀いお私が䌝えたかったこずすべお。 説明した方法が誰かにずっお有甚であるず思われ、圌が圌の仕事でそれを䜿甚するこずに決めた堎合良い、ここではすべおが基本的であり、実装に問題はないはずです、私は非垞に幞せです。



しかし、あなたが突然今䜕もするこずがない堎合、たたは単に興味がある堎合-私の実装の詳现の説明ぞようこそ



コディム



゜ヌスはGithubで無料で入手できたす。



このナヌティリティをクラスの圢匏で蚭蚈するこずにしたした。各むンスタンスは独自の構成を持぀個別のアプリケヌションになりたすただし、珟時点では、構成党䜓は1行すべおのスクリプトが配眮されおいるパスずモゞュヌルキャッシュで構成されたす。



 function Comeon(path) { var self = this; self.path = path; self.modules = {}; }
      
      





クラスには、単䞀のパブリック非同期関数、requireがあり、すべおの䟝存関係でモゞュヌルの実行を開始し、オプションでメむンモゞュヌルが終了しお゚クスポヌトパラメヌタヌを受け取った埌に呌び出されるコヌルバック関数を受け入れたす。



 Comeon.prototype.require = function require(moduleRequest, callback) { var self = this; loadNextModule.bind(self)(enqueueModule.bind(self)(getModuleId("", moduleRequest)), callback); }
      
      





2぀の䞻芁で最も興味深い関数enqueueModuleずloadNextModuleを怜蚎する前に、いく぀かのヘルパヌ機胜を怜蚎したす。



searchRequires関数は、パラメヌタヌずしおモゞュヌルファむルのURLを取埗し、同期XHR芁求でそれをロヌドし、その䞭のrequire関数ぞの呌び出しの発生を探したす。 ダりンロヌドしたコヌドを実行するのではなく、この関数を䜿甚しおモゞュヌルの䟝存関係を探しおいるだけであるずいう事実に泚意を喚起したいず思いたす。 モゞュヌルファむルは、このダりンロヌド䞭にブラりザによっおキャッシュされたす。これは、埌でこのモゞュヌルを接続するずきに圹立ちたす。



 var requirePattern = /(?:^|\s|=|;)require\(("|')([\w-\/\.]*)\1\)/g; function searchRequires(url) { var requires = []; var xhr = new XMLHttpRequest(); xhr.open("GET", url, false); xhr.onreadystatechange = function () { if ((xhr.readyState === 4) && (xhr.status === 200)) { var match; while ((match = requirePattern.exec(xhr.responseText)) !== null) { requires.push(match[2]); } } }; xhr.send(); return requires; }
      
      





getModuleIdおよびgetModuleContext関数は、それぞれモゞュヌルぞの識別子ずパスを取埗するために䜿甚されたす。



 function getModuleId(moduleContext, moduleRequest) { var moduleId = []; (/^\.\.?\//.test(moduleRequest) ? (moduleContext + moduleRequest) : moduleRequest).replace(/\.(?:js|node)$/, "").split("/").forEach(function (value) { if (value === ".") { } else if (value === "..") { moduleId.pop(); } else if (/[\w\-\.]+/.test(value)) { moduleId.push(value); } }); return moduleId.join("/"); }
      
      





 function getModuleContext(moduleId) { return moduleId.slice(0, moduleId.lastIndexOf("/") + 1); }
      
      





require関数は、モゞュヌルのコンテキストで、芁求されたキャッシュされた゚クスポヌトを返す関数です。 以前にアプリケヌションむンスタンスのコンテキストに固定し、珟圚のモゞュヌルのパスを最初のパラメヌタヌずしお枡しおから、各iframeをりィンドりに配眮したす。



 function require(moduleContext, moduleRequest) { var self = this; var moduleId = getModuleId(moduleContext, moduleRequest); if (self.modules[moduleId] && self.modules[moduleId].exports) { return self.modules[moduleId].exports; } else { throw Error("Module not found."); } }
      
      





最埌に、すべおの䞻芁な䜜業を実行する2぀の機胜を怜蚎したす。



再垰関数enqueueModuleは、パラメヌタヌずしお枡されたモゞュヌルをキュヌに远加したす。たた、䟝存関係ごずに自分自身を呌び出しお远加したす。 その結果、モゞュヌルロヌドキュヌを取埗したす。キュヌの最埌には、メむンモゞュヌルアプリケヌションぞの゚ントリポむントがありたす。 このキュヌのおかげで、ダりンロヌド可胜な各モゞュヌルは、䟝存するすべおのキャッシュされたモゞュヌルをすでに自由に䜿甚できたす。



 function enqueueModule(moduleId) { var self = this; var moduleQueue = []; if (!self.modules[moduleId]) { self.modules[moduleId] = { url: self.path + moduleId + ".js?ts=" + (new Date()).valueOf() }; moduleQueue.push(moduleId); searchRequires(self.modules[moduleId].url).forEach(function (value) { Array.prototype.push.apply(moduleQueue, enqueueModule.bind(self)(getModuleId(getModuleContext(moduleId), value))); }); } return moduleQueue; }
      
      





loadNextModule関数は、enqueueModule関数によっお返されたキュヌを通過し、モゞュヌルを順にブラりザヌにロヌドしたす䟝存関係を怜玢するために既にダりンロヌドしたため、ブラりザヌはキャッシュからファむルを取埗したす。 䞊蚘で合意したように、各モゞュヌルを接続するために、グロヌバル倉数、exportsおよびmodule.exports倉数、およびrequire関数を䜜成する個別のiframeが䜿甚されたす。 次の各iframeは、前のスクリプトが完党にロヌドされた埌にのみロヌドされたす。 ダりンロヌドキュヌが終了するず、最初に枡されたコヌルバック関数ある堎合を呌び出し、最埌のモゞュヌルの゚クスポヌトをそこに転送したす。



 function loadNextModule(moduleQueue, callback) { var self = this; if (moduleQueue.length) { var iframe = document.createElement("iframe"); iframe.src = "about:blank"; iframe.style.display = "none"; iframe.onload = function () { var moduleId = moduleQueue.pop(); var iframeWindow = this.contentWindow; var iframeDocument = this.contentDocument; iframeWindow.global = window; iframeWindow.require = require.bind(self, getModuleContext(moduleId)); iframeWindow.module = { exports: {} } iframeWindow.exports = iframeWindow.module.exports; var script = iframeDocument.createElement("script"); script.src = self.modules[moduleId].url; script.onload = function () { self.modules[moduleId].exports = iframeWindow.module.exports; if (moduleQueue.length) { loadNextModule.bind(self)(moduleQueue, callback); } else if (typeof callback === "function") { callback(self.modules[moduleId].exports); } }; iframeDocument.head.appendChild(script); }; document.body.appendChild(iframe); } else if (typeof callback === "function") { callback(); } }
      
      





おたけずしお、comeon.jsスクリプトを読み蟌んだ盎埌にアプリケヌションを起動できる機胜を远加したす。



 var script = Array.prototype.slice.call(document.getElementsByTagName("script"), -1)[0]; var main = script.getAttribute("data-main"); if (main) { window.addEventListener("load", function () { var comeon = new Comeon(script.getAttribute("data-path") || "/"); comeon.require(main); }); }
      
      





以䞊です。 これで、ブラりザ偎でCommonJS圢匏で蚘述されたモゞュヌルを䜿甚し、喜んでデビュヌするこずができたす。 これを行うには、comene.jsをスクリプトぞのパスずデヌタ属性のメむンモゞュヌルの名前に接続するだけです。



 <script src="http://rawgithub.com/avgaltsev/comeon/master/comeon.js" data-path="scripts/" data-main="main"></script>
      
      





たたは、実行プロセス䞭に互いに独立した耇数のモゞュヌルを接続する必芁がある堎合、たたはアプリケヌションに耇数の゚ントリポむントがある堎合、より詳现な構文を䜿甚できたす。



 <script src="http://rawgithub.com/avgaltsev/comeon/master/comeon.js"></script> <script> window.onload = function () { var comeon = new Comeon("scripts/"); //   comeon.require("main"); //    comeon.require("another_main", function (exports) { console.log(exports); }); }; </script>
      
      






All Articles