Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove jQuery dependency. #2865

Closed
wants to merge 18 commits into from
Closed

Remove jQuery dependency. #2865

wants to merge 18 commits into from

Conversation

paulmillr
Copy link
Contributor

As discussed in gh-2859, taken from Exoskeleton.

This change makes jQuery dependency 100% optional. If jQuery is included, everything will still work as-is.

New stuff:

  • View#find and View#findAll as great shortcuts for querySelector / querySelectorAll().toArray(). I'm using these names because these are native methods on Element since this spec and because they are short and concise.
  • utils.delegate and utils.undelegate for native event delegation.
  • utils.matchesSelector and utils.ajax for fast and convenient minimal selector matching / XHR shortcut.
  • Backbone.Deferred for custom deferred libraries (I think this is a big deal since Chrome now has native promises, other browsers will soon have too)
  • View#useNative property if you still want to include jQuery but also want to use native methods for some particular views (hot code)

Behaviour in no-jquery env:

  • No View#$el.
  • No custom jQuery features like selectors (:first) and event namespaces (click.ns), I think it's pretty obvious — if you want them, use jQ. Ordinary event delegation and bubbling works just like before.

Native DOM view tests are in test/no-jq.html and test/view-no-jq.js.

As discussed in gh-2859, taken from [Exoskeleton](http://exosjs.com).

This change makes jQuery dependency 100% optional.

If jQuery is included, everything will still work as-is.

New stuff:

* `View#delegate` and `View#undelegate` for native event delegation.
* `utils.matchesSelector` and `utils.ajax` for fast and convenient selector matching / XHR shortcut
* `Backbone.Deferred` for custom deferred libraries (I think this is a big deal since Chrome now has native promises)

Native DOM view tests are in test/no-jq.html and test/view-no-jq.js.
@paulmillr
Copy link
Contributor Author

There are not much comments around, but let's see whether this may be merged at first.

cc @akre54

var tag = document.createElement('div');
var name, method;
// Detect the right suffix.
['matches', 'webkit' + sfx, 'moz' + sfx, 'ms' + sfx].some(function(item) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IE8 doesn't support Array#some. Use _.some instead for fallback

@akre54
Copy link
Collaborator

akre54 commented Nov 8, 2013

How would people feel if we used addEventListener instead of Backbone.$ for event handling where namespaces &c aren't needed?

That is, instead of:

if (Backbone.$) {
  Backbone.$(window).on('hashchange', this.checkUrl);
} else {
  window.addEventListener('hashchange', this.checkUrl, false);
}

...we'd just use:

if (window.addEventListener) {
  window.addEventListener('hashchange', this.checkUrl, false);
} else {
  window.attachEvent('onhashchange', this.checkUrl);
}

var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else {
var el = _.extend(document.createElement(_.result(this, 'tagName')), attrs);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything that using $el.attr(attrs) gives us that _.extend(this.el, attrs) doesn't? Looking in the source, I'm not seeing much advantage of going the jq route.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually something related to prop vs attr?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attr uses setAttribute, prop sets using host methods (expando, like we're doing here). This line could be changed to mimic attr by doing:

_.each(attr, function(val, name) {
  el.setAttribute(name, val);
});

@wyuenho
Copy link
Contributor

wyuenho commented Nov 10, 2013

I've made a comment on the ticket.

#2859 (comment)

In general I don't like this PR at all, there should be much simpler implementation that's compatible and much less ambitious.

@paulmillr
Copy link
Contributor Author

@akre54 it's cool probably, yup

@akre54
Copy link
Collaborator

akre54 commented Nov 11, 2013

Theres a lot I like about the ideas in this pull. But at the moment it's just too confusing and the potential for frustrating upgraders is just too high. Upgrading to this hypothetical version of BB should be a no-brainer; there should be clear instructions for how to use Backbone without the jQuery dependency, and for everyone else it breaks nothing.

Normally I'd vote for offloading this stuff to a plugin that overwrites a few of the smaller methods in Backbone.View.prototype, but jQuery is just too baked into View/History at the moment. You basically have to overwrite your own View as @wyuenho did for Backgrid to get the opt-out.

Here are my suggestions:

  • Move utils.ajax to a separate file in the repository so that it sets up a native Backbone.ajax. Including it should be as simple as dropping a script tag in the page.
  • Eliminate as many BB.$ checks as possible. There's too many logic branches here. Use addEventListener in all the non-view places (namely History) where jquery features like namespacing aren't needed. Add a small shim or wrapper method for IE < 9 so upgrading won't break apps. This is not an ideal solution, so I'd welcome alternatives.
  • Use underscore methods for consistency with the rest of the codebase (see paulmillr#2)
  • Figure out what will break by switching a jQuery app to no jQuery. For most users, I suspect there shouldn't be too much trouble as Paul pointed out in earlier comments.

This pull differs from Backbone's philosophy in some respects (Backbone tends to not live on the bleeding edge wrt. vendor-prefixed features like matches) but nails it in others (modularity, minimum 3rd-party dependence, and natives -- Backbone already includes fallbacks for non-pushState browsers, for example). I'd rather the native approach gets baked into the Backbone API and not run Exoskeleton as YAJPS (Yet Another Jashkenas Project Spinoff) because of its core usefulness, but I also agree with keeping the project lightweight. What would it take to convince you, @wyuenho?

@paulmillr
Copy link
Contributor Author

ping @jashkenas

@jashkenas
Copy link
Owner

not run Exoskeleton as YAJPS (Yet Another Jashkenas Project Spinoff)

That got a big laugh.

I agree pretty much completely here with @akre54's sentiments. The additional logic branches are very displeasing, and the chances of backwards-compat destruction seem way too high. But I still think this is quite promising.

How about this approach?

Instead of the jQuery checks in this PR, remove all jQuery references from within the Backbone source. In their place, place Backbone.ajax, and Backbone.dom-style function calls, which will serve as the plug-in point going forwards. Then provide a tiny module that implements Backbone.ajax and Backbone.domWhatever via jQuery — this is the module that ships with the core backbone.js file, and leaves compatibility the same.

Then you're free to implement a Backbone.exoskeleton (or whatever) plugin that provides the same small set of functions, and does so natively, having identified the core external API, and separated it out already in Backbone proper.

This alternative dependency wouldn't ship in the main Backbone.js file, but could perhaps be provided right alongside it, and linked to at the top of the docs. Sound reasonable?

@akre54
Copy link
Collaborator

akre54 commented Nov 11, 2013

Let me give a thought to what this API would look like and I'll open a pull in the next couple days if I can think of a minimally hairy way to do it. I like the idea of decoupling jQuery but leaving it as the default.

@paulmillr
Copy link
Contributor Author

Interesting idea. So, there should be calls to:

Backbone.utils.on
Backbone.utils.off
Backbone.utils.ajax
Backbone.utils.findAll
Backbone.utils.setElement
Backbone.utils.removeElement

?

Should native and very short implementations of View#delegate and View#undelegate be removed too?

@paulmillr
Copy link
Contributor Author

Also, what about View#find and View#findAll?

@wyuenho
Copy link
Contributor

wyuenho commented Nov 11, 2013

@jashkenas 's suggestion sounds good, but I worry that this all or nothing approach may not work in practice. As I've said before, in a realistic large app, you'll very likely be using a jQuery plugin and you'll inevitably bring in jQuery as a dependency anyway.

An all-native backbone core will keep backbone light and fast on mobile, but I'd really need some kind of opt out mechanism in the View such that I can avoid jQuery in hot code on desktop. How about introducing a BaseView or NativeView or whatever, and make View extend from it? View can just override in BaseView to get backward-compat.

All of these still sounds like a lot of code changes, and I fear the final product will feel like it's polyfilling Backbone.

@braddunbar
Copy link
Collaborator

One of Backbone's best features is it's staunch commitment to it's goals and abstraction level. It is a library of data-structuring primitives. It is not a DOM library, and it is not a low level utility library. jQuery's performance or non-performance, while relevant, is orthogonal to Backbone's goals and means for achieving them. If the state of jQuery is not to our liking we should attempt to change it or create our own, separate, DOM abstraction library rather than blur the focus of this project.

jQuery's API is a great choice for abstraction because, love it or hate it, it's the most recognized javascript API in existence. In fact, my own familiarity with it is a big reason I took to Backbone so easily. Creating our own API for reasons of performance is, in my opinion, a recipe for confusion. It's the same reason we don't define a small subset of Underscore to ship with Backbone, despite the fact that Backbone does not use all of Underscore's API.

The API is already used very minimally (I count less than 7 distinct usages) and defining Backbone.$ for your own purposes is not a hardship. Don't want to use $el? No problem. Redefine Backbone.$ or View#setElement to do whatever it is you think it should.

@paulmillr
Copy link
Contributor Author

So? What's up with this? @jashkenas

@jashkenas
Copy link
Owner

So? What's up with this? @jashkenas

You tell me ;) How's your no-jquery-alt branch coming along? If you're liking it, would you like to swap out this PR for the contents of that approach?

@prust
Copy link
Contributor

prust commented Nov 21, 2013

@wyuenho:

I worry that this all or nothing approach may not work in practice
I'd really need some kind of opt out mechanism in the View such that I can avoid jQuery in hot code on desktop

If views didn't use Backbone.utils directly, but instead used a View.prototype.utils reference, then it would be easy to opt-out of jQuery for "hot code" views by overriding View.prototype.utils.

@paulmillr
Copy link
Contributor Author

I have moved View#delegate and View#undelegate to utils in this pull request. This way, user is able to exclude native utils completely.

So, from my production experience, i'm against no-jquery-alt:

  1. It is a hassle for users to include another library for this simple functionality. I mean module systems here. In AMD / Common.js envs, users will need to do

  2. It is even a bigger hassle for library developers to reason in some way about native methods. For example, Backgrid or Chaplin or Marionette or whatever.
    My solution:

    if (Backbone.$) {
      // do one thing
    } else {
      // native fallback
    }

    @jashkenas solution:

    // 1. Try to include utils replacement
    try {var Exos = require('exoskeleton')}
    // Also, AMD version. Which is a real hassle.
    // AMD will try to load utils replacement and make HTTP request.
    
    // 2. Check for exoskeleton.
    if (!Exos) {
      // do one thing
    } else {
      // native fallback
    }

    Because library authors must have backwards-compatibility with users on jQuery.

    If we go my way, it will be simple for them do implement native fallbacks. It will be done for new users without any backwards incompat.

    The other way here is something like Backbone.Exoskeleton propery that Exos will add. But this doesn't play nice with module systems — user will need to include exos before library and after backbone (how to do that with AMD when you have dependencies of dependencies of dependencies?).

  3. With ‘utils’ jeremy's solution plugin authors will need to think about other possible extensions of utils. Are you sure we want competition here? E.g. backbone-dojo-utils�. Think about second code example from point 'this' inside View.render() being set to the model? #2 and multiply it by number of possible extensions of backbone-utils.

  4. Adoption rate. It is far more likely that backbone ecosystem will adopt native methods faster if it was included with Backbone itself. This way, backbone plugins may soon become very fast by using native methods. The web will benefit sooner.

  5. Dependency analysis. There are some popular solutions currently that parse bower.json files and guess concatenation order. If marionette depends on backbone and backbone depends on jQuery, the order will be jQ -> bb -> marionette.
    With Jeremy's solution, user will need to override bower.json data in dependencies. Exoskeleton-utils will be dependent on Backbone and libraries will be dependent on exoskeleton. Hassle.

  6. There is no backwards-incompat with my solution and it can be used right away. As I have said, I can setup a super-simple Makefile that will simply do this:

    backbone:
      cat lib/{header,utils,events,model,collection,view,sync,router,history,footer}.js > backbone.js
    
    noutils:
      cat lib/{header,events,model,collection,view,sync,router,history,footer}.js > backbone.js

    And then, make noutils is all what user will need to do if he don't want to include native fallback.

@paulmillr
Copy link
Contributor Author

So I guess you won't merge this one? @jashkenas

@Yahasana
Copy link

Yes, he won't merge 💯

@wyuenho
Copy link
Contributor

wyuenho commented Dec 15, 2013

@prust Yes but the if blocks are detecting the existence of a Backbone.$ in order to decide whether to use util or $. I'd like to be able to use util in individual views even if Backbone.$ exists. All or nothing works great for mobile where you can remove jQuery completely, but it doesn't for desktop apps where Backbone and jQuery need to coexist most of the time, except for hot code.

My suggestion would be to have a View constructor flag that lets me switch on or off native/jQuery instead of having to override a prototype.

@akre54
Copy link
Collaborator

akre54 commented Dec 16, 2013

@wyuenho with utils you wouldnt need a flag:

var jqUtils = Backbone.utils;
Backbone.utils = require('native-utils');
var myView = new FastView;
Backbone.Utils = jqUtils;

@wyuenho
Copy link
Contributor

wyuenho commented Dec 16, 2013

@akre54 that's even worse than @prust 's suggestion. I can override View.prototype.utils with 0 lines of code (I'll have to make my own fast view anyway) instead of having to swap references with 3 non-obvious lines at App init. Moreover, this makes the assumption that I don't have any subview under FastView that wants to use jQuery by default.

If everyone is against having an extra flag, I'd take @prust 's View.prototype.utils. Let me try a much more minimalist PR.

@paulmillr
Copy link
Contributor Author

@wyuenho what is the problem with simply checking for if (Backbone.$)?

@wyuenho
Copy link
Contributor

wyuenho commented Dec 16, 2013

Because it's all or nothing. I can have 3 views A, B, and C, where A and B will depend on jQuery for compatibility or jQuery plugin reasons (I suppose that's how most people use Backbone anyway), but I just want C to not use jQuery at all because it's the hot zone. if (Backbone.$) will always look up the global Backbone.$ first and if it exists, it'll use the jQuery branch. But as I've said, A and B will have to depend on jQuery for the entire lifetime of the application, and I will include jQuery in the scope, and Backbone.$ will be jQuery. I just want C to not use jQuery at all. I don't even care if utils doesn't exist. All I need is the ability to not use it in 1 View superclass, so I can inherit from that View for all my hot code.

Does this make sense?

@wyuenho
Copy link
Contributor

wyuenho commented Dec 16, 2013

The only reason utils exists is to decide whether the Backbone.View methods should branch to jQuery or native DOM methods. That's all good and well but the branching shouldn't be Application-wide, it should be defined at the call sites. It simple doesn't make sense to assume the entire App with either use jQuery or native DOM methods.

The branching just needs 1 more level of granularity. That's it.

@paulmillr
Copy link
Contributor Author

A and B will depend on jQuery for compatibility or jQuery plugin reasons

How exactly? Can you provide any example plugins / behaviour of this sort?

  1. Backbone.$ is null.
  2. But you can still use generic $ jQuery in your A and B, so you can freely depend on them.

@wyuenho
Copy link
Contributor

wyuenho commented Dec 16, 2013

I have dozens of views at work that all just call jquery plugins from render like this:

this.$("selector").plugin();

Or

this.$el.plugin();

I'd rather not to have to change all of them just so I can have 1 view that doesn't use jQuery.

Aren't all existing apps suppose to be like this?

@paulmillr
Copy link
Contributor Author

OK in this case here's my proposal:

  1. Backbone.$ branching still works
  2. Add optional useNative flag to View that allows to easily override Backbone.$.

If people want to go fully no-jquery, they just don't include jQuery and everything will work by default.

If people want to use $ in some views, they include jQuery, set useNative to true in some views and leave other views as-is.

How does that sound?

@wyuenho
Copy link
Contributor

wyuenho commented Dec 16, 2013

Perfect :)

On Monday, December 16, 2013, Paul Miller wrote:

OK in this case here's my proposal:

  1. Backbone.$ branching still works
  2. Add additional useNative or some flag to View that allows to easily
    override Backbone.$.

If people want to go fully no-jquery, they just don't include jQuery and
everything will work by default.

If people want to use $ in some views, they include jQuery, set useNativeto
true in some views and leave other views as-is.

How does that sound?


Reply to this email directly or view it on GitHubhttps://github.com//pull/2865#issuecomment-30653618
.

@paulmillr
Copy link
Contributor Author

@wyuenho 1. added to this pull request. 2. added and released new Exoskeleton with useNative.

@wyuenho
Copy link
Contributor

wyuenho commented Dec 16, 2013

Thank you! Let me test this out...

@jashkenas
Copy link
Owner

Then @wyuenho came up with nicer solution.

Closing in favor of #2959.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants