WSC 3.0 — JavaScript

  • [align=justify]Welcome to the start of our 2.2-series “Plugin development changes”. This first part deals with the larger changes in the JavaScript part of Community Framework. The changes primarily serve two purposes:

    • Cleaner and more maintainable JavaScript
    • Better performance on low end device (such as Smartphones)


    The first goal is achieved by separating new and rewritten JavaScript into separate AMD (Asynchronous Module Definition) modules. These are being automatically loaded by the require.js AMD loader. The second goal primarily focuses on optimizing the JavaScript that is run at performance critical places. While jQuery provides powerful APIs it also tends to be rather slow. Today the DOM APIs are pretty consistent across browsers and also provide some new high level functions that previously were not available. This allows us to write concise and readable JavaScript without taking the performance hit introduced by a framework.


    But let's take a closer look at the new AMD modules:


    AMD modules basically consist of two parts:

    • Stating dependencies of the module
    • Code of the module

    More often than not you will need some APIs provided by another part of JavaScript. Without using a module loader these are usually made available by assigning them to the global scope (window) and then accessed by the name the developer gave them. This has got two major disadvantages:

    • Names must be unique and therefore it not always possible to use two libraries side by side.
    • Dependencies are implicitly hidden inside the Code.

    Using a module loader solves these problems by requiring you to explicitly state your dependencies at the top of your module. It looks like this:

    JavaScript
    define(['TimWolla/Library', 'dtdesign/Library'], function(TimWolla, Dtdesign ) {
    // `--Array of depencies------------------´ `--Local names for dependencies--´


    When looking at this code you can immediately see that my module needs the modules TimWolla/Library and dtdesign/Library to work correctly. In order to resolve the conflict between the names I choose to name them TimWolla and Dtdesign in my code. The names can be freely chosen. If I was a lazy typist I could have used T and D as well.


    require.js will take a look at the array of dependencies I gave (first parameter) and load them if they are not available yet. If the dependencies have got other dependencies it will load them as well (and their dependencies etc.). Once all the dependencies are fully loaded it will call the given function (second parameter) with the stated dependencies as the parameters. The name a module usually is implicitly derived from it's filename.


    The code of my module resides inside the function. You have to return the object that represents your module. The return value of the function is what will be passed to other modules:


    JavaScript
    function MyModule() {
    alert('Called MyModule');
    }
    return MyModule;
    });


    In this example I create a new function called MyModule and return it to make the function available to the outside world. You will notice that this return value is “trapped” in the require.js black box. There is no global name you can use to access the module. To gain access you would use the require function. It works and looks like define, but it does not create a new module. Assuming I want to access the module I just defined in the file TimWolla/MyModule.js it could look like this:


    JavaScript
    require(['TimWolla/MyModule'], function(MyModule) {
    MyModule(); // alerts "Called MyModule"
    });


    In case of Community Framework a module usually contains what previously was a single class. As an example: In WCF 2.1 you would access the PeriodicalExecutor via WCF.PeriodicalExecutor. The new and improved PeriodicalExecutor is available via the WoltLabSuite/Core/Timer/Repeating module.
    As no one likes breaking backwards compatibility the old classes are either still available or transparently mapped to the new modules (if possible). We recommend using the new modules whenever possible as they provide better performance or a better API. The old classes will be removed in a future version.


    If you want to take a look at our new modules you can find them in the WoltLab subfolder of the js folder of Community Framework. We recommend placing your modules in your own vendor named subfolder of js to avoid name clashes (I chose TimWolla in my example above).


    These are the essential things you need to know about the new JavaScript module structure. Require.js is pretty powerful, you might want to take a look at the official homepage for further information.


    I mentioned the jQuery less JavaScript above. Let's take a closer look at that as well: jQuery is powerful, often to powerful to achieve simple things. But you pay the performance cost of this flexibility. While it made sense to use jQuery in the past to bridge cross browser gaps it is not necessary any more to always use it. Here's a quick overview over a part of the native DOM API browsers provide. It should provide you with a starting point for your own research. The Mozilla Developer Network is a pretty good repository for further information on the details. All the functions mentioned below are also used inside the new module based JavaScript of Community Framework. You may want to take a look at that as well.


    • document.getElementById(): This is $('#...').
    • document.querySelectorAll(): This roughly translates to $(). The function allows you to select elements based on a CSS selector.
    • el.classList.{add|remove|contains}(): These map onto $.fn.{add|remove|has}Class.
    • el.{get,set,remove}Attribute: These map onto $.fn.attr.
    • el.appendChild: This maps onto $.fn.append.


    As always: Feel free to ask any question that I did not answer with this post and bring up your suggestions!

  • You should note that you created an anonymous module in the first example. In your last example you load the named module TimWolla/MyModule. From my understanding, this is only possible if you define this module as a named module, e.g. like this:


    JavaScript
    define('TimWolla/MyModule', ['TimWolla/Library', 'dtdesign/Library'], function (TimWolla, Dtdesign) {
    //
    }

    "A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP" — Leonard Nimoy

  • Hi


    @Netzwerg the relevant part is this:

    Assuming I want to access the module I just defined in the file TimWolla/MyModule.js it could look like this:

    and this:

    The name a module usually is implicitly derived from it's filename.


    As a little further explanation: Assume I need TimWolla/MyModule as a dependency. Require.js will look up how to resolve the filename in it's configuration. By default all modules are assumed to be in wcf/js. Therefore it loads the file wcf/js/TimWolla/MyModule.js and provides the anonymous module in this file.

  • @Netzwerg An anonymous module will be automatically named according to the full file path relative to the base path. In fact you should never define named modules because they impose a lot of issues, including potential mismatches between the file location and the name.


    Assume the base path: http://example.com/wcf/js/ and the anonymous module defined in http://example.com/wcf/js/Vendor/Product/Foo/Bar.js. As a result the module will be automatically named Vendor/Product/Foo/Bar.

    Alexander Ebert
    Senior Developer WoltLab® GmbH

  • Ah well, I didn't realize those hidden dependencies and assumed those examples were self-contained. But thanks for the explanation ;)

    "A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP" — Leonard Nimoy

  • Maybe I missed something, but how can I tell the system to search in e.g. cms/js/?

    This part is still missing and will be added later, especially since it requires additional configuration due to unpredictable paths.


    Meanwhile you can use this snippet I'm currently using for Burning Board:

    Code
    require.config({
    paths: { "WoltLab/WBB": "{@$__wcf->getPath('wbb')}js/WoltLab/WBB" }
    });

    Alexander Ebert
    Senior Developer WoltLab® GmbH