Apr 30, 2011

matrix.js introduction

Essentially, matrix.js is like other script loader, but it does other things such extensible resource type, dependencies registration and reload, resource locations, resource release and others. In the following, I will explain what the library can do. All these example presented here can be found at GitHub

The most used method in matrix is the "matrix" method. Here is the definition.

//returns a promise
//resourceKeys is a string or an array, e.g "a.css, b.css" 
//or ["a.css", "b.css"] or ["c.js", "a.css, b.css"]
matrix(resourceKeys[, loadByOrder])

If you have no configuration, the resource key is is treated as url of the resource. Resource key follow the format of [resourceName].[resourceType]. There are three build-in resource handler in matrix, "module", "js", and "css", resourceKeys can be a string or array. Most of the time, you should use string format resourceKeys. If it is a string, this means resource are fetched in parallel, if it is an array, this means resources are fetched in serials, most of time, we use resourceKeys as string. I will discuss more about it later. Just like any other script loader, you can load javascript and css like the following.

var promise = 
matrix("/css/reset.css, /css/base.css, /js/utilities.js, /js/app.js", 

Behind the scene, the loader will fetch two css and two javascript files using ajax call in parallel, and evaluate each source file into the page when it is ready at client side. and run fn1, fn2, fn3 after all resources are loaded. It is important to know that, the order of making ajax call is not necessary the order when ajax call is finished, so the order of evaluation is not guaranteed. If the order of evaluation is important, you can use the following code.

var promise = 
matrix("/css/reset.css, /css/base.css, /js/utilities.js, /js/app.js", 

Behind scene, the load method actually convert it into the following call.

//reset depends nothing
matrix.depend("/css/reset.css", null); 

//base.css depends on reset.css
matrix.depend("/css/base.css", "/css/reset.css"); 

//utitlies.css depends on base.css
matrix.depend("/js/utilities.css", "/css/base.css");  

//app.css depends on utilities.css
matrix.depend("js/app.css", "/js/utilities.css");  
var promise = matrix("/js/app.js", true).done(fn);

Dependencies between the module is important in resource loading, sometimes is more complicated than serialized dependencies like above case. When loading resource, matrix follows a couple steps. Firstly, matrix determines the type of resource and the correct handler for processing the resource request. After that, matrix use the handler to execute three tasks(fetch, parse and evaluation). Fetch is to get the content ready at the client side. Parsing is an optional step, matrix currently extract dependencies from the content in this step, and this is necessary only when dependencies information is not pre-configured, and parsing method is implemented by the handler. Evaluation is to integrate the resource into the page. These three tasks are implemented by a resource handler.

Let's try a more complicated case. jQuery UI library offer lots of widgets. It is a highly modularized library, some components(like ui.mouse.js) are reused over the library like. If you only want to use a certain widget,e.g dialog, you can go to their home page, deselect all, and just select dialog, and it create a a package that include just the css, js file that dialog widget use. This is a great feature. But there is still a problem. If I have two page, page one need a,b,c widget, and page two need b,c,d widget, so I need to create two js package. To save the trouble, I might better off just create a package file that include all the modules. However this might load too many modules that I don't need. We can use matrix to load the minimum set of resource to be able to use a widget. First I will show you the hard way, later I will show you easy way. But regardless of which, we need to solve two problem, resource dependencies and and resoure locations.

To understand dependencies in jQuery UI, you need to download all the source of jQury UI. Open the source file in jQuery.ui.dialog.min.js, the header says

 * jQuery UI Dialog 1.8.10
 * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 * http://docs.jquery.com/UI/Dialog
 * Depends:
 * jquery.ui.core.js
 * jquery.ui.widget.js
 *  jquery.ui.button.js
 * jquery.ui.draggable.js
 * jquery.ui.mouse.js
 * jquery.ui.position.js
 * jquery.ui.resizable.js

Now we can see what dialog depends, dialog also depends jquery.ui.dialog.css. Let's change it for now and repeat this for other module accordion, autocomplete, button, datepicker, dialog, progressbar, resizable, selectable, slider, tabs, for example, add jquery.ui.accordion.css in jQuery.ui.accordion.min.js header. We will need it later in using next method.

* Depends:
 * jquery.ui.dialog.css
 * jquery.ui.core.js
 * jquery.ui.widget.js
 *  jquery.ui.button.js
 * jquery.ui.draggable.js
 * jquery.ui.mouse.js
 * jquery.ui.position.js
 * jquery.ui.resizable.js


To convert this into code, we can write the following. Please note that I am not using the url of resource as the resource key, later I will map the reource key to url.

matrix.depend( "ui.dialog.js": "ui.dialog.css, ui.core.js, ui.widget.js, ui.button.js, ui.draggable.js, 
ui.mouse.js,ui.position.js, ui.resizable.js");

But this is not enough, because inner resources have their own dependencies as well. So we need to open all direct and indirect referenced js files and find out their dependencies. If there a file has no dependencies, we still need to use a null value. For example, ui.core.js does not have any dependencies, so its dependencies is null. Finally, we can write something like the following.

matrix.depend( {
 "ui.dialog.js": "ui.dialog.css, ui.core.js, ui.widget.js, ui.button.js, ui.draggable.js, ui.mouse.js, 
ui.position.js, ui.resizable.js",
 "ui.button.js": "ui.button.css, ui.core.js, ui.widget.js",
 "ui.draggable.js": "ui.core.js, ui.mouse.js, ui.widget.js",
 "ui.mouse.js": "ui.widget.js",
 "ui.resizable.js":"ui.resizable.css, ui.core.js, ui.mouse.js, ui.widget.js",
 //ui.redmond.css is my favorite skin for jQuery ui, :)
 "dialog.module" : "ui.core.css, ui.redmond.css, ui.dialog.js"
} );

Now we need to solve the second problem, resolving the url of resource, and we can use the following

matrix.url( {
 "ui.core.css": "js/jquery.ui/css/base/jquery.ui.core.css",
 "ui.dialog.css": "js/jquery.ui/css/base/jquery.ui.dialog.css",
 "ui.resizable.css": "js/jquery.ui/css/base/jquery.ui.resizable.css",
 "ui.button.css": "js/jquery.ui/css/base/jquery.ui.button.css",
 "ui.smoothness.css": "js/jquery.ui/css/smoothness/jquery.ui.theme.css",
 "ui.redmond.css": "js/jquery.ui/css/redmond/jquery.ui.theme.css",
} );

matrix( "dialog.module", function () {
 $( "#linkDialog" ).click( function() {
  $( "<h1>hello</h1>" ).dialog();
  return false;
 } );
} );

Let's how the resources are actually process.

So far so good, the fetching is parallel, and the evaluation is serial and in the correct order of dependencies. But the above configuration is too tedious. But if we want to use accordion widget, we need to repeat this process, obviously it violate the principle "Don't Repeat Yourself". Let's do "Convention over configuration". As I mentioned before, resource is extensible, you can extend handling of resource by adding a new resource type, which map to a new handler. For example, and x.mytype is mapped to a handler matrix.handlers.mytype. Here is methods that a handler can implement.

  1. load(resourceKey) [must implement]

    This method is to fetch and evaluate the resource and its dependencies into memory.

  2. release(resourceKey) [optional]

    This method is to release single resource (not its dependencies)

  3. url(resourceKey) [optional]

    This method is to build a url using a naming convention

  4. parseDepends(sourceCode) [optional]

    how to parse source code to build dependencies

Only the load method need to be implemented, other methods are optional. In the case here however, we want to reuse the default load method of javascript handler and css handler, all we need to do use extend url, and parseDepends. The following shows how to use the addHandler method

// it return back the new handler
//newHandlerName is also the name of new resource type
//matrix.addHandler(newHandlerName, baseHandlerName [,newBehavior])

matrix.addHandler( "uicss", "css", {
 url: function ( resourceKey ) {
  return matrix.fullUrl( matrix.baseUrl + "jquery.ui/css/base/jquery.ui." + 
matrix.resourceName( resourceKey ) + ".css" );
} );

matrix.addHandler( "uitheme", "css", {
 url: function ( resourceKey ) {
  return matrix.fullUrl( matrix.baseUrl +  "jquery.ui/css/" + 
matrix.resourceName( resourceKey ) + "/jquery.ui.theme.css" );
} );

var rdependencies = /^\s*\/\*[\w\W]*Depends:([\w\W]+)\*\//,
 ruiModule = /jquery\.ui\.(\w+?)\.(js|css)/gi;

//extend js handler
matrix.addHandler( "uijs", "js", {

 url: function ( resourceKey ) {
  return matrix.fullUrl( matrix.baseUrl +  "jquery.ui/jquery.ui." + 
matrix.resourceName( resourceKey ) + ".min.js" );

 parseDepends: function ( sourceCode ) {
  var dependencies = [],
   dependText = rdependencies.exec( sourceCode );

  if ( dependText = dependText && dependText[1] ) {
   //dependText is something like
   * jquery.ui.tabs.css
   * jquery.ui.core.js
   * jquery.ui.widget.js
   while ( module = ruiModule.exec( dependText ) ) {
    //find jquery.ui.xxx.css and convert it to xxx.uicss
    //find jquery.ui.xxx.js and convert it to xxx.uijs
    // "xx" + ".ui" + "js" == "core.uijs"
    // "xx" + ".ui" + "css" == "core.uicss"
    dependencies.push( module[1] + ".ui" + module[2] );
   return dependencies.length ? dependencies.toString() : null;

  return null;
} );

In the above code, the url method is straight forward, it is used to map a resource key to a url, since jQuery UI team follow a naming convention of their components, we can use this, and use a function to calculate the url based on resource key. What is interesting this the parseDepends method. The idea is that if we can parse it by human eye, we can parse it by code. The method basically extract the headers we modified before, and convert them into resource keys, and then build dependencies. With all this code in place, we can throw away the previous dependencies and url configuration. Let's write a bit of code, to register the dependencies and the locations of all jQuery ui files.

matrix.baseUrl = "js/";

matrix.depend( {
 "uicss.module": "core.uicss, redmond.uitheme"
} );

("accordion,autocomplete,button,datepicker,dialog," + 
"progressbar,resizable,selectable,slider,tabs," + 
"draggable,droppable,mouse,position,sortable").split( "," ),
 function (index, value) {
  var resourceKey = value + ".module";
  var depends = value + ".uijs, uicss.module";
  matrix.depend(resourceKey, depends);
 } );

I agree that I need to spend more time to do this kind of convention over configuration. But I think it is worthy, because I can dynamically load the the resources of just of my widget, any widget in jQuery UI, not just dialog. In the next post, I will explain dependencies refresh, release release, performance and other advanced topic in using matrix.

No comments:

Post a Comment