[dojo-contributors] has.js: The (not so) definitive guide

Bryan Forbes bryan at reigndropsfall.net
Wed Jan 26 14:47:10 EST 2011


-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

There has been a lot of talk about using has.js (or has() or dojo/has or
whatever you want to call it) and I wanted to clear up some
misconceptions that some might have. I do not intend to offend, but
rather to inform.

History and Purpose
===================
Has was designed (as most of you probably know) as a library to
standardize feature detection. Although we may all know that we need
feature detection, we may not all know *why* we need it. I've compiled a
list of articles that everyone should read (if you're not already
familiar with them):

* http://jibbering.com/faq/notes/detect-browser/
*
http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
*
http://perfectionkills.com/detecting-event-support-without-browser-sniffing/
* http://perfectionkills.com/feature-testing-css-properties/

has.js has compiled ideas from all of these articles (and probably more)
to put together tests that work across all browsers and that won't blow
up in your face. Care has been taken to test host objects properly.

Usage
=====
Tests are functions that are registered with has.js (much like you would
register a function as a test in DOH) and those functions will be run
on-demand. The advantage that has.js has is that these functions will
only be run once and the result will be cached. This makes it so the
overhead of running the test only occurs once and only if the test is
needed. In some cases, running one test will set the result of several
others (for instance, "event-metakey", "event-preventdefault",
"event-stoppropagation", "event-srcelement", and "event-relatedtarget"
set the result of each other when run). Registering a test is simple:

has.add("dom-addeventlistener", function(g, d, el){
    return has.isHostType(d, "addEventListener");
});

has.add("dom-dataset", function(g, d, el){
    el.setAttribute("data-a-b", "c");
    return has.isHostType(el, "dataset") && el.dataset.aB == "c";
});

Test functions receive three arguments (the last two are optional and
depend on the host environment): the global object (window, in
browsers), the "document" property of the global object if it exists,
and a test element if the document supports the "createElement" method.
Test functions can return three states: true, false, and null/undefined
(test is not applicable on this platform, like the test for activex on
Firefox). When testing properties of host objects, `has.isHostType`
should be used for the reasons listed in the articles above.

Once the tests have been registered, it's a simple call to has() with
the name of the test:

if(has("bug-getelementbyid-ids-names") ||
    has("bug-getelementbyid-ignores-case")){
    dojo.byId = function(){
        // implement byId
    };
}else{
    dojo.byId = function(name, doc){
        return (doc||dojo.doc).getElementById(name);
    };
}

if(has("dom-addeventlistener")){
    // implement addEventListener connections
}else{
    // implement DOM0 connections
}

As stated before, once the tests are run the results are cached so that
they can possibly be used again. There are a good number of tests that
can be done once and the result will never be used again, however it's
not our job as general purpose library developers to decide which
results should or shouldn't be used again.

My vision of how has.js should be used is at the top-most level
possible. For instance:

if(has("dom-computedstyle")){
    dojo.getComputedStyle = function(){
    };
}else if(has("dom-currentstyle")){
    dojo.getComputedStyle = function(){
    };
}else{
    dojo.getComputedStyle = function(){
    };
}

instead of:

dojo.getComputedStyle = function(){
    if(has("dom-computedstyle")){
    }else if(has("dom-currentstyle")){
    }else{
    }
};

Even though the results of has tests are cached, they should be checked
as little as possible to increase efficiency. And really, once you know
the bugs and features of a host environment do they really need checked
every time a function is invoked? (The build argument is moot here
because in the second example we're adding a penalty to our users who
don't want to make a build or who want to make a general purpose build.)

On Loading has.js
=================
has.js is going to need to be loaded. Call it a penalty or call it an
advantage: it will be needed if we want to use has.js. Whether it's a
global (why are globals considered "magic"?) or an object on `dojo`, the
registry will need to be there. Can you imagine releasing Dojo, saying
we are using has.js, and we've completely stripped it out or made it
completely private? Our users would not be able to write their own tests
using the has.js API which would be a disservice.

We also want to keep the results of tests that are used in "core" and
"base" available to our users. As Rawld and Bill pointed out with the
`addEventListener` test: even though tests seemingly could be thrown
away or inlined, others might like or need to use them.

Names must also be kept consistent. Imagine a user coming from jQuery,
FuseJS, or MooTools that has been using has.js in their framework and
having to learn completely new test names. Also think about a user
having to load duplicate tests with a different name for use with
another library. We won't be winning any users by changing test names.

My suggestion for loading has.js is this:
* Check for `has` global.
* If it exists, use that and register it with the module loader as "has"
and make sure any "has/*" requirements are skipped and the global `has`
is returned as the user is now responsible for the has.js core.
* If not, load a "Dojo approved" has.js with the module name "has" and
refer to it with the `has` variable.

The "Dojo approved" has.js would be wrapped in the AMD module format
like what Kris has (no pun intended) done[1]. For compatibility's sake
and for the sanity of maintenance (merging fixes and new tests from
upstream), the has.js modules should be kept as-is when we distribute
them. We would also need to add a list of has.js modules that a user
using their own has.js would need for Dojo core, Dojo base, Dijit, etc.

[1] https://github.com/kriszyp/has.js

Any tests that don't exist in has.js that are needed should be
maintained in new modules under their respective project's namespace
(and most likely submitted back to has.js) so tests can be used by
modules other than the originally intended module without having to
require the originally intended module. It would be inefficient to have
something like dojo.gfx define a test for a feature of canvas and the
only way to use that test would be to require dojo.gfx.

As for the has.js plugin for RequireJS, I'm not a fan for *core and
base* because I don't see a solid enough argument for its use that can't
be done by using `if(has("some-test")){}else{}` within a module. There
is also a common thought that Dojo's core is far too broken up and is
hard to browse to find what you want (is dojo.connect defined in
event.js or connect.js?); using the has.js plugin would only further
that misconception by breaking up modules based on features rather than
functionality.

Builds and Optimization
=======================
For a general purpose library (read this as normal Dojo releases),
has.js tests should not be stripped out. A small penalty (~100ms) will
be incurred from running tests at the startup of Dojo, but that small
penalty is an acceptable hit for the assurance that Dojo will work
consistently and as fast as possible across browsers.

That being said, I think we should modify the build system to strip out
blocks of code based on a provided profile of test results. I'm not
convinced that test result profiles are needed, but I think we'd be
doing the community a disservice by not providing that functionality.

Mobile
======
This should probably be a different thread, but I will state it here and
we can move it to a new thread if need be: I'm not convinced that test
profiles are right for mobile. How many different browsers are out
there? The best list I can find[2] is from 2009 and is UA string based.
iOS is probably fairly simple since we know the version of Safari for
each release and they include the device in the UA string. Android is a
completely different beast: how many Android devices are out there, how
many versions of WebKit for Android are out there, and how do we know if
a vendor has modified WebKit to remove or add certain features or bugs
(they can obviously modify the UA string). Are we realistically
suggesting that a test profile be made for each mobile browser on every
device out there based on UA strings? And what happens if a browser has
the same UA string as another, but provides different features/bugs as
the one we built a profile for? To me, there are just too many unknowns
to reliably do test profile builds for mobile devices.

[2] http://www.zytrax.com/tech/web/mobile_ids.html

Conclusion
==========
Hopefully I haven't offended too many people but I wanted to clear up
some misconceptions about has.js and its intended use and use within a
general purpose library. If you knew all of this already, please ignore
it ;).

- -- 
Bryan Forbes
http://www.reigndropsfall.net

GPG Fingerprint
3D7D B728 713A BB7B B8B1  5B61 3888 17E0 70CA 0F3D
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (Darwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk1Aej4ACgkQOIgX4HDKDz01rwCgp3/fu8sHsMsW7LdeJIZWUsMV
Eg8AoIDf1KCWUUZIlmw9AkXabY7n3/8a
=FdYZ
-----END PGP SIGNATURE-----


More information about the dojo-contributors mailing list