ember-fastboot / ember-cli-fastboot

Server-side rendering for Ember.js apps
http://ember-fastboot.com/
MIT License
851 stars 161 forks source link

Robust jQuery Compatibility #101

Closed tomdale closed 8 years ago

tomdale commented 8 years ago

The vast majority of Ember apps use jQuery's AJAX API in some capacity (even if it's through an intermediary library like Ember Data).

Currently we use najax, which has been great, but only supports a subset of the jQuery AJAX API. @dmethvin had the brilliant idea to instead use real jQuery, but write a Node-compatible AJAX transport. This would allow us to support effectively all of the jQuery AJAX API and allow a wider range of Ember apps to run unmodified with FastBoot.

Items to investigate:

  1. Surely this exists already? I am always hearing about the sheer volume of packages npm has and therefore what a thriving ecosystem. Given the popularity of jQuery, someone must have already written this?
  2. If not and we need to write it ourselves, is there an officially packaged and distributed version of jQuery that contains just the AJAX bits? If not, what's the best way to do a custom build with only AJAX that stays up-to-date?
  3. How do we determine/allow users to configure the origin? E.g., $.getJSON('/users/1.json') is relative to the origin in the browser; we need to make sure it goes to the right place in Node.js.

Probably the next step is to investigate getting an AJAX-only build of jQuery running reliably in Node.

danmcclain commented 8 years ago

We can extract just ajax (and the XHR transport if we wish) from the existing build tools for jQuery. It's not automatic for each release, but it's something to start with https://github.com/jquery/jquery/blob/master/README.md#modules

dmethvin commented 8 years ago

Paging in @jaubourg ...

Surely this exists already? I am always hearing about the sheer volume of packages npm has and therefore what a thriving ecosystem. Given the popularity of jQuery, someone must have already written this?

I haven't heard of anyone writing one, except @Runspired on Twitter but he didn't reply back with code. It seems like a lot of the use of jQuery in non-browser situations is still using something like jsdom to provide a browser-like environment.

If not and we need to write it ourselves, is there an officially packaged and distributed version of jQuery that contains just the AJAX bits? If not, what's the best way to do a custom build with only AJAX that stays up-to-date?

We publish the AMD modules as part of the jquery package so you should be able to get them reliably from there. As one of the most complex and flexible APIs in jQuery, Ajax depends on a lot of other functionality and in general we assume a browser environment, so there are likely to be some speed bumps in getting this to work without a jsdom-like setup.

How do we determine/allow users to configure the origin? E.g., $.getJSON('/users/1.json') is relative to the origin in the browser; we need to make sure it goes to the right place in Node.js.

jQuery makes the URL relative to whatever location.href is. In the AMD module you have the ability to define location but it's always window.location in our builds. There is a check to determine whether a request is cross domain that you want to avoid and that can be done via ajax settings, but at the moment there is still a use of createElement that needs be moved or else you'll need to shim document.createElement() as well.

As far as where is the right place to make the URL change, it depends on whether the caller and/or our ajax logic needs to know about the change. If it has to happen early in the process you should be able to use a prefilter. Inside your transport you could also do whatever node-specific mangling you want on the URL before the request and basically nobody would see it if that's desired. If you need a place for users to store some configuration data you could set it via jQuery.ajaxSetup() perhaps, which is what the other transports do.

runspired commented 8 years ago

I didn't respond with any because there isn't much code to it :P

jQuery can be consumed in small modules, it's likely you can just pull out the ajax bit from it.

runspired commented 8 years ago

An example transport from something I used to have: https://github.com/runspired/jQuery-ieXDomainShunt

runspired commented 8 years ago

The transport I mentioned on twitter was similar to the above but also client side, basically it does the same thing but uses an existing socket.io connection for the request if available.

For node, I written my own najax-like modules. which honestly could be shunted into a transport as well. I typically build a Promise based interface (using RSVP) overtop of request, and include Whitelist functionality to ensure I'm not creating a server blindly handing users permissions outside of it's desired use.

Here's a quick example.

var RSVP = require('rsvp');
var request = require('request');
var Promise = RSVP.Promise; // jshint ignore:line
var Whitelist = require('./whitelist');
var config = require('../config');
var affixUrl = require('./affix-url');

Whitelist.configure(config.whitelist);

module.export = function pajax(hash) {

  return new Promise(function (resolve, reject) {
    hash.url = affixUrl(hash.url); // appends current domain if none
    hash.method = hash.method ? hash.method.toUpperCase() : 'GET';

    var options = {
      url:     hash.url,
      method:  hash.method,
      jar:     request.jar(),
      agent:   false,
      headers: hash.headers || null
    };

    if (hash.data) {
      options.json = hash.data;
    }

    if (!Whitelist.verify(hash.url)) {
      return reject({
        status: 500,
        responseText: 'Your request was blocked because the url provided (' + hash.url + ') does not pass Whitelist Verification',
        statusText: 'Bad Request',
        headers: {}
      });
    }

    request(options, function(error, response, data) {
      var ret;

      // clean request
      if (response && !error && response.statusCode < 400) {
        ret = {
          responseText: data,
          statusText: response.statusMessage,
          status: response.statusCode,
          headers: response.headers
        };

        resolve(ret);

        // request has error
      } else {

        ret = {
          responseText: data,
          statusText: response ? response.statusMessage : 'Bad Request',
          status: response ? response.statusCode : 500,
          headers: response ? response.headers : {},
          error: error
        };

        reject(ret);
      }
    });

  }).then(
    function(success) {
      if (hash.success) {
        hash.success(success);
      }
      return success;
    },
    function(error) {
      if (hash.error) {
        hash.error(error);
      }
      throw error;
    });
};
jaubourg commented 8 years ago

It's trivial to override the default transports. Just extract the ajax source code and then override the existing transports (or don't include them) by doing something like this:

jQuery.ajaxTransport( "+* +script", nodeTransport );

The + sign indicates the transport must be used in priority for the given dataType.

Now, writing the transport itself using request shouldn't be difficult, just make sure to provide the correct information to the send completeCallback (see the doc). jQuery.ajax will take care of the rest and you'll have a 100% compatible implementation. That's what transports have been designed for.

jaubourg commented 8 years ago

Also, don't forget some logic to handle abort ;)

kedano commented 8 years ago

I have an app that use the ember-wordpress addon to get data from my WP backend.

ember serve works just fine on the project and it looks like ember fastboot almost works as well except for one issue:

najax: method jqXHR."getAllResponseHeaders" not implemented

My ember -v is at:

DEBUG: Ember      : 2.4.1
DEBUG: Ember Data : 2.4.0+9f8c40927a

Should I ping the najax project with this issue?

tomdale commented 8 years ago

@kedano It sounds like @danmcclain already beat you to it. ;) https://github.com/najaxjs/najax/commit/b42dab293469350b16ea2e7b2a5956741be9a2a7

Looks like his PR was accepted but they haven't published a release yet.

kedano commented 8 years ago

@tomdale Thanks, installing the plugin from master fixed my issue. I'll get my simple app up soonish as a proof of concept :)

danmcclain commented 8 years ago

@tomdale 0.4.0 was released a month ago, I just realized it the other day (there was no GitHub tag, only a npm release), that's when I PR'd https://github.com/ember-fastboot/ember-fastboot-server/pull/24, so master of ember-fastboot-server uses the right version

tomdale commented 8 years ago

Punting on this until after 1.0

MarkPieszak commented 8 years ago

Anyone make any other traction when it comes to handling or at the very least completely ignoring all jQuery on the server side? Running into the same issue in ng2 Universal of course.