2
\$\begingroup\$

In my ASP.NET MVC project I simply inserted <script></script> into my views - it caused code duplications and spaghetti code.

Now, I have decided to switch to modular JS and have implemented the following mechanism:

  • ALL actions are only executed through the framework and its modules - whether it is a view or object behavior, date-to-string converter or websocket connection
  • every module exposes exports functions, which can be called by anybody, including other modules
  • all functions including exports and init are called within a sandboxed context, which includes both objects specific for the module and objects, injected by a framework (currently, I am implementing the mechanism to "hide" / disallow non-context methods within a module)

Framework object with the only register function:

window.kibfw = {
    register: function(name, module) {
        if (!name || !module.init)
            throw new Error('Could not load a module ' + name);

        if (name === 'register')
            throw new Error('Module cannot have name "register" which is reserved');

        if (this[name])
            throw new Error('Module ' + name + ' is already registered.');

        module.context = module.context || {};

        /* Here, inject the required context objects (jQuery, libraries etc.) */
        module.context.$ = window.jQuery;
        module.context.kibfw = window.kibfw;

        try {
            module.init.apply(module.context, arguments);    
        } catch(e) {
            throw new Error('Error during module initialization: ' + e.message + '\n' + e.stackTrace);
        }

        var moduleExports = (this[name] = {});
        for (var exportFnName in module.exports) {
            if (!module.exports.hasOwnProperty(exportFnName))
                continue;

            moduleExports[exportFnName] = (function (exportFn) {
                return function() {
                    try {
                        exportFn.apply(module.context, arguments);
                    } catch (e) {
                        throw new Error('Module: ' + name + '. Error during method invocation: ' + e.message + '\n' + e.stackTrace);
                    }
                };
            })(module.exports[exportFnName]);
        }
    }
};

Example module:

kibfw.register('decorator', {
  init: function () {
    this.$decorateItems = this.$('input');
  },
  context: {
    $decorateItems: undefined
  },
  exports: {
    setColor: function (color) {
      this.$decorateItems.css('color', color);
    }
  }
});

// Usage:
$("button").click(function() {
    kibfw.decorator.setColor($(this).attr('data-color'));
});

JSFiddle demonstration

How do you think, would it be useful and scalable?
Which problems can I stumble upon while using this approach?

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Question - why not use AMD or a preprocessor like Webpack to achieve this? You're reinventing CommonJS here. \$\endgroup\$
    – Dan
    Jul 29, 2016 at 12:16

2 Answers 2

2
\$\begingroup\$

Very readable code, some minor comments:

  • Some folks dislike the lacking curly braces for one liners, I dont mind it here myself
  • JsHint will complain about you creating a function in a for loop, I think you should indeed refactor that
  • Using this[name] to register modules would be to fickle for me (shenanigans with bind, apply, and call come to mind), I would prefer a self executing function with a var statement.
  • You only have 1 line of comment, for a very straightforward piece of code, I would have commented the moduleExports[exportFnName] = (function (exportFn) { line
  • I would split this out in to 2 different error messages: if (!name || !module.init) throw new Error('Could not load a module ' + name);
  • Actually, in your code I see that init is a bit contrived, I would not make that mandatory personally

Now, when I take a step back, I think you might be suffering from Second System Syndrome. Consider that in effect you are replacing

$("button").click(function() {
    $('input').attr('data-color'));
});

with

kibfw.register('decorator', {
  init: function () {
    this.$decorateItems = this.$('input');
  },
  context: {
    $decorateItems: undefined
  },
  exports: {
    setColor: function (color) {
      this.$decorateItems.css('color', color);
    }
  }
});

// Usage:
$("button").click(function() {
    kibfw.decorator.setColor($(this).attr('data-color'));
});

I like window.kibfw, it is pretty beautiful. But I would hate to have to write a website with it.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your response! Very useful answer, I will accept this one. I have come up with a simple solution, which I will show in another answer. \$\endgroup\$ Aug 1, 2016 at 4:53
0
\$\begingroup\$

After konijn's feedback, I have come up with a simpler solution

Module framework:

var Module = (function() {
  function module(name, constructor) {
    if (module[name]) {
      throw new Error('Module ' + name + ' already exists.');
    }

    var exports = (module[name] = {});
    constructor.call(null, exports);
  } 

  return module;
})();

New module declaration (new keyword doesn't have any practical usage, just for readbility):

new Module('decorator', function(exports) {
  var $decorateItems = $("p"); 

  exports.setColor = function(color) { 
    $decorateItems.css('color', color);
  };
});

Calling an exported function:

Module.decorator.setColor('blue');

After some code analysis, I have understood that all my modules are independent, and only a couple modules will be called from others - I will simply load them first using ASP.NET MVC Bundling.
Modules dependency and requirements features really seem like an overkill here.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.