Modular Design Patterns in JavaScript
Last week we've discussed how to create classes and namespaces in JavaScript. For those who missed it, you can read it here. There is however something else you need to know regarding the topic, which is modules.
Modules
CommonJS
Currently, CommonJS is a de facto standard. Many third-party vendors are making modules or module-load systems according to the CommonJS module specification. Node.js is a typical project that complies with the specification. CommonJS is a voluntary working group organized to use JavaScript not only in browser, but also in server-side and desktop applications.
Definition
If you're are Node.js developer, you've already used CommonJS syntax numerous times, without knowing it. There are other CommonJS implementation, however since Node.js is the most popular and wide spread technology, I'll use it in my examples. Recall our JsDeepDive.Common.Utils class from the previous post. Let's create a module from it using CommonJS syntax:
(function () { 'use strict'; var Utils = function () { var privateMember = 1, privateFuncA = function privateFuncA() { return 'privateFuncA'; }, privateFuncB = function privateFuncB(test) { return test === privateMember; }, privateFuncC = function privateFuncC(test) { if (privateFuncB(test)) { return 'test passed'; } else { return 'test failed'; } }; return { publicFuncA: privateFuncA, privateFuncC: privateFuncC }; }(); module.exports = Utils; }());Then in another file you can load the module using require command like this:
var Utils = require('./Utils.js'); console.log(Utils.publicFuncA());Notice that we wrote the full path of our module file. This can be avoided by packing the module and creating package.json.
You can load other packages within your definition - all of them will be loaded once needed. Remember that Node.js loads modules synchronously, so if your module requires prolonged initialization running during the require call, make sure to preload it before you use it. Also in Node.js, the module location is the namespace, so there's no need to namespace in the code as you've described.
AMD
AMD has separated itself from CommonJS production group, as it failed to reach an agreement in discussions with about using JavaScript module in an asynchronous situation. CommonJS created JavaScript as part of an effort to retrieve it outside of browsers; thus, could not generate agreement with AMD, which was focused on operation within browsers. According to Require.js site, which is the leading AMD implementation:
It is an improvement over CommonJS modules because:
- It works better in the browser, it has the least amount of gotchas. Other approaches have problems with debugging, cross-domain/CDN usage, file:// usage and the need for server-specific tooling.
- Defines a way to include multiple modules in one file. In CommonJS terms, the term for this is a "transport format", and that group has not agreed on a transport format.
- Allows setting a function as the return value. This is really useful for constructor functions. In CommonJS this is more awkward, always having to set a property on the exports object. Node supports module.exports = function () {}, but that is not part of a CommonJS spec.
Definition
(function () { 'use strict'; define('Utils', ['jquery','underscore'], function($, _) { var privateMember = 1, privateFuncA = function privateFuncA() { return 'privateFuncA'; }, privateFuncB = function privateFuncB(test) { return test === privateMember; }, privateFuncC = function privateFuncC(test) { if (privateFuncB(test)) { return 'test passed'; } else { return 'test failed'; } }; return { publicFuncA: privateFuncA, privateFuncC: privateFuncC }; }); }());Here we defined the Utils module, which depends upon JQuery and Underscore modules. If your module doesn't have any dependencies, you may omit the second parameter. You may also omit the first parameter, the name, which will make the module even more portable. It allows a developer to place the module in a different path to give it a different ID/name. The AMD loader will give the module an ID based on how it is referenced by other scripts.
Firstly we need to configure our starting point. Notice that our main script was placed in data-main attribute and not as a source. This way we load Require.js first and it will take care of loading our main script.
<script data-main="src/main.js" src="src/require.js"></script>Then inside our main, we configure where each package resides. This way Require.js will know where to look for definitions of Underscore, jQuery and eventually Utils. We do this with help of requirejs.config method.
require.config({ baseUrl: 'js/lib', paths: { jquery: 'jquery-1.9.0' } });The left side is the module ID and the right side is the path to the jQuery file, relative to baseUrl. Also, the path should NOT include the '.js' file extension.This example is using jQuery 1.9.0 located at js/lib/jquery-1.9.0.js, relative to the HTML page.
Non AMD libraries
You can also configure the dependencies, exports, and custom initialization for older, traditional "browser globals" scripts that do not use define() to declare the dependencies and set a module value. You do this with shim method. You can see the example of it's usage on Require.js site in the shim section.Universal module
Someday you will want to create a module, which is both accessible in browser and server side environments. Require.js supports server side loading, providing CommonJS wrapper, however I wouldn't suggest you to follow this slippery road. Since nearly all Node.js developers don't use Require.js, they won't be able to use your beautiful module. However if you don't care about publicity and won't be sharing your work, you may try to unify your module creating and go only with Require.js.
If you'd like to make your module accessible from both Require.js and Node.js using CommonJS syntax, here is how you can do it:
(function () { 'use strict'; var Utils = function () { ..... }(); if (typeof define === 'function' && define.amd) { // Publish as AMD module define(function() {return Utils;}); } else if (typeof(module) != 'undefined' && module.exports) { // Publish as node.js module module.exports = Utils; } else { // Publish as global (in browsers) window.Utils = Utils; } }());Basically we check for appropriate methods and act accordingly.
Comments
Post a Comment