pillarjs / router

Simple middleware-style router
MIT License
409 stars 101 forks source link

Advertise Browser Support #14

Closed wesleytodd closed 7 months ago

wesleytodd commented 9 years ago

I am interested to know if you all think it is worth mentioning that this module is compatible with browsers. I threw together an example of using this module in front-end code, and it works perfectly fine.

It is a big deal to say that the module is "Browser Compatible" because of the support issues, but the current implementation works in Chrome & FF (which is all I tested). And after reading most of the code I don't see anything that will clearly break it in other browsers (at least with the required polyfills).

dougwilson commented 9 years ago

We could do it if someone is willing to help me setup automated browser testing (because otherwise it's just too easy to accidentally break browser support). I just have never setup browser testing before.

wesleytodd commented 9 years ago

I believe we had something setup internally for this, probably with Sauce Labs, I will ask our QA lead if he has any suggestions and post back.

Really I am just glad to hear that you are interested in this being a "supported feature" of the module. I am going to write a push-state router implementation wrapping this module, and didn't want to base my stuff on something that might not be supported in the long run.

dougwilson commented 9 years ago

No problem. There are modules that are definitively Node.js-specific, and others (like this) that are basically neutral (that don't intend to not work in browsers, but it's not the author's primary concern). If I can set it up so I get automated browser tests, then I'll know when a change breaks and can just make it not break, thus providing support :)

rlidwka commented 9 years ago

I'm curious, what it can be used for in browser environment? Some kind of a single-page app?

wesleytodd commented 9 years ago

@rlidwka Yeah, we are doing an isomorphic app. Basically I am making a routes.js file that looks like this:

var index = require('./handlers/index'),
    foo = require('./handlers/foo');

module.exports = function(router) {
    router.get('/', index);
    router.get('/foo/:bar', foo);
};

Where router is an express app on the server and a wrapper around this router on the client. Then I make each middleware a module and use browserify's browser field in the package.json to load the client or server side version of the middleware.

We are using React for rendering and a wrapper around XHR/Request or data loading. It enables almost all of our modules to work on both the server and the client.

wesleytodd commented 9 years ago

For backstory:

Our last application was an Angular app with a Go backend. One of the most common bugs we had was weird behavior because we forgot to define all the proper routes in both Go and javascript. This was both a consequence of our architecture (multiple angular apps on a single domain) and having the routes duplicated. So this time around we really want to avoid the duplicated route issue.

wesleytodd commented 9 years ago

Ok, so going more deeply into the browser testing I found that __proto__ is not supported before IE11. This small change fixes it:

// Was:
router.__proto__ = this

// Now:
for (var i in this) {
  router[i] = this[i];
}

Also, it looks like you could get IE9 support with two polyfills, Function.bind and Array.isArray. bind is only used once and `isArray is used twice (not counting dependencies). So they might also easily be replaced with older style implementations.

Anyway, I can look at putting together a PR for that if the change seem fine to you all.

As for the automation, Sauce Labs has free accounts for OS projects, so if you, @dougwilson, want to set one up I could also do a PR for that integration.

dougwilson commented 9 years ago

Anyway, I can look at putting together a PR for that if the change seem fine to you all.

I would rather find a Object.setPrototypeOf polyfill to use, rather that bloating the memory for Node.js users from all the property copying. It would also break people who add stuff to Router.prototype after they constructed router instances.

Sauce Labs has free accounts for OS projects, so if you, @dougwilson, want to set one up I could also do a PR for that integration

Sounds good to me.

dougwilson commented 9 years ago

Anyway, I can look at putting together a PR for that if the change seem fine to you all.

I would rather find a Object.setPrototypeOf polyfill to use, rather that bloating the memory for Node.js users from all the property copying. It would also break people who add stuff to Router.prototype after they constructed router instances.

To expand on this, what I mean is we can just replace that with a call to Object.setPrototypeOf and the browser polyfill can decide to just do a property copy, if it desires, but the Node.js people can still have their __proto__ goodness.

wesleytodd commented 9 years ago

Yeah, great point. I was unsure of that because of the exact thing you pointed out. I will look at doing that first thing tomorrow morning.

Also, if you are interested, here is the progress I made today. I still have quite a bit of work to do for the popstate handler and coming up with a good req/res abstraction instead of my plain objects. But it functions as it should.

So thanks for making my dreams come true! I really always wanted to be able to do isomorphic apps, this is the first time I am actually able to make it happen.

wesleytodd commented 9 years ago

Does anyone happen to know of a good polyfill for Object.setPrototypeOf? I have looked for a stand alone version that would support back to IE8 and could not find one. The best looking one I found is in core-js. But it is heavily integrated into that ecosystem. If anyone has any better suggestions than trying to pull that out into a standalone version let me know.

dougwilson commented 9 years ago

Polyfill can be the following for our purposes:

Object.setPrototypeOf = { __proto__: [] } instanceof Array
  ? function setPrototypeOf(obj, prototype) { obj.__proto__ = prototype }
  : function mixinProperties(obj, prototype) { for (var prop in prototype) { obj[prop] = prototype[prop] } }
dougwilson commented 9 years ago

Apparently __proto__ is part of the ES6 specification.

wesleytodd commented 9 years ago

MDN certainly has some conflicting information on this subject. Last thing to consider here is avoiding the pollution of the global namespace. I threw together a little npm module to avoid that polution, if it looks good to you I will put together a PR using this module.

dougwilson commented 9 years ago

Link to module :)?

wesleytodd commented 9 years ago

HAHA, yeah that would help! https://github.com/wesleytodd/setprototypeof

nateps commented 9 years ago

Hey! I'm planning on using this module for the new recommended routing library for DerbyJS (http://derbyjs.com/). Everything else in the framework (including the current router based on older Express code) supports IE 9+. @wesleytodd's recommendation for falling back to property mixins looks like the best solution given the current API. Could we get it merged in?

Looks like .bind is only used in one specific case. Could make the call support the specific arguments only. Using a simple function closure is faster than calling bind and apply anyway.

var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }

could simply be:

var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(done, layerError) {
      process.nextTick(function() {
        done(layerError)
      })
    }

Agree?

dougwilson commented 9 years ago

Looks like .bind is only used in one specific case. Could make the call support the specific arguments only. Using a simple function closure is faster than calling bind and apply anyway. Agree?

I don't agree; the setImmediate polyfill needs to support n-arguments, otherwise it's not a polyfill at all and it'll randomly break because we're not testing in an environment that uses it. Please suggest a different fallback that support n-arguments :)

As far as this whole thing, I can merge in, but not until I can actually test it (i.e. I need tests that fail if the changes are missing). Without testing, nothing is guaranteed and everything will just randomly break in the future. If someone can provide god instructions on how I can setup a browser CI for this project, that's all I'm waiting for.

nateps commented 9 years ago

It isn't really a polyfill in the current implementation, since it is a function that called defer. Could use a more specific name like deferDone to make it more clear that it is single purpose. I figure might as well make it simple given that it is the only use anywhere in this module.

Could of course write a true polyfill pretty easily, but it would be more code and slower.

dougwilson commented 9 years ago

That's irrelevent; it's a simple setImmediate polyfill; it's simply called defer so I don't have to overwrite the setImmediate name, otherwise I would have just called it setImmediate.

wesleytodd commented 9 years ago

Whatever the solutions we end on are, the real problem is that it is not supported, officially supported, until we have automated tests. Do you have a good setup in derby? If so can you setup something similar here?

nateps commented 9 years ago

We set something up with testling, but that has broken and haven't heard anything about James fixing it. Will probably switch to Sauce Labs, which is the only other good option of which I'm aware. For now, we've been manually running the tests in VMs. Microsoft makes it free and relatively easy to download different versions of Windows for testing at least: https://www.modern.ie/en-us/virtualization-tools#downloads

Re: setImmediate, you could make it generic:

var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(fn) {
      process.nextTick(function() {
        fn.apply(undefined, arguments)
      })
    }

Or https://github.com/YuzuJS/setImmediate if you want to actually polyfill it.

Alternatively, how about removing this altogether, since it is only needed in Node <= 0.8, which is pretty old at this point. The Express version of this is calling setImmediately directly at the moment: https://github.com/strongloop/express/blob/master/lib/router/index.js#L188 If people want to polyfill it, they can always include the module above and polyfill it themselves.

dougwilson commented 9 years ago

Alternatively, how about removing this altogether, since it is only needed in Node <= 0.8

Um, telling me to drop support for Node.js 0.8 is the same as me saying I won't support IE < 9. You're not really starting off great asking me to support old browsers and then saying I shouldn't support old Node.js versions...

dougwilson commented 9 years ago

If people want to polyfill it, they can always include the module above and polyfill it themselves.

Sure, then people can include .bind polyfills themselves as well. Your arguments seem contradictorily to me...

blakeembrey commented 9 years ago

Just commenting to say the generic code looks more like:

var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(fn) {
      var args = Array.prototype.slice.call(arguments, 1)

      process.nextTick(function() {
        fn.apply(null, args)
      })
    }

But since it's only used in a single place, I don't see why it couldn't be a function that just accepts two arguments? It's not a proper polyfill, but we don't need one for the only use of it.

dougwilson commented 9 years ago

Regardless of what is here: my offer is still out. I just need to figure out (or someone can help me, which would get it done way faster) testing modules on web browsers in a CI and then I'll pick away and casting the widest browser support I can. I just need help setting up web testing on a CI.

wesleytodd commented 9 years ago

I would personally wait until automated test are in place to claim support. An important project like this one cannot just rely on manual testing, it is just not a good practice.

And for the setImmediate issue, I am with @dougwilson on not dropping support. But it seems like @blakeembrey's solution would do just fine. I think we should just tell people they need bind and Array.isArray polyfills for ie9 support.

nateps commented 9 years ago

I hear you on the CI testing, and I'll definitely let you know if I get a chance to work on it.

I think using standard APIs and notifying people that they have to include a polyfill is a very good approach for modern libraries. Less code and simpler to maintain. The only obvious issue is things that can't be polyfilled like __proto__.

I'd vote for simply using setImmediate directly, because it is consistent with the implementation in Express, which @dougwilson wrote as well.

wesleytodd commented 9 years ago

Ill put together the PR to fix the proto issue first thing in the morning. Then at least we can all be happy to use it at our own risk (Edit: with polyfills) until we get CI. Sound alright?

rlidwka commented 9 years ago

I would personally wait until automated test are in place to claim support. An important project like this one cannot just rely on manual testing, it is just not a good practice.

As long as there is no node.js-specific code and native modules, it should work in browser. Manual testing is enough to check this.

wesleytodd commented 9 years ago

PR submitted.

dougwilson commented 9 years ago

Hey everyone, the __proto__ stuff from @wesleytodd was just merged. From looking back through this issue we still have the following things:

Should we ban .bind or require a polyfill? We can ban bind, though it may be hard to keep it out of our dependencies in the long run.

wesleytodd commented 9 years ago

Bind is supported to IE9. Personally I say just leave it be, and people can do a polyfill for IE8.

The reason I am using it is because we are attempting to support IE8 on my company's site, so we are still using the router to determine what controller to run even in IE8. But for most people IE10 will be the cut off because that is the History api support.

eth0lo commented 9 years ago

Hi, probably I'm coming to late to this discussion, but why you don't use the browser field in the package.json as other libraries that support browser and node at the same time?

The drawback here is that you need to abstract the inconsistencies in another file/module and do the mapping in the browser field of package.json, this way you abstract/proxy that.

felixfbecker commented 8 years ago

This is awesome. Will allow many people to have a reliable router for their SPAs without the big frameworks like Angular.

wesleytodd commented 8 years ago

@felixfbecker If you are interested I have been using https://github.com/wesleytodd/nighthawk in multiple apps for a few months now and it's working GREAT. The company I work for is using it for our single page isomorphic apps with React with great success.

felixfbecker commented 8 years ago

One thing though - what are req and especially res set to?

wesleytodd commented 8 years ago

@felixfbecker Since this is unrelated to this repo, if you want to open an issue on that repo I can help you out over there :) just dont want to bother people over here for no reason

wesleytodd commented 8 years ago

This might be the only situation where simply waiting fixes an issue. Since Microsoft has dropped support for anything under IE11, the issues with bind and pretty much anything else like this is resolved. We don't need to support it.

We have been using this in a few of our production apps for a while now with no browser support problems, and we have primarily a windows user base. I know we still haven't setup a test environment, but for anyone stumbling upon this issue I can safely say that this module works in all major browsers. :)

dougwilson commented 8 years ago

@wesleytodd, yea, I was actually surprised Microsoft would end so many IEs at once. AFAIK, Function.prototype.bind could have just been shimmed in the browser before loading this code, anyhow.

Regardless, I do one day think it would be nice to figure out a good way to run these modules in at least one web browser during CI, to truly declare web browser support. Just recently I added as part of the CI running a browserified version of the module against the test suite (though running under Node.js), but it's a good first step for me. I was recently looking into the feasibility of running against JXcore in the CI as well, but that's a different issue from web browser support ;)

wesleytodd commented 8 years ago

haha, yeah it was a bold move by them. But a long time coming. At one point we were shimming bind, but now we only do for tests that run in phantomjs (apparently it doesn't implement that....) because we stopped supporting old IE's.

FWIW, I have found, pretty reliably, that unless you actually run against a bunch of specific browsers you really are not going to catch the types of errors you think.. So just testing in one doesn't REALLY help, which is what makes this so annoying and hard.

I think I mentioned it before, but sauce labs should work for this, and do a bunch of browsers. I found this page on setting it up and if you create the account and post the creds I can submit a PR for it adding it. Let me know!

Edit: or if you want to encrypt them I can just add the PR with test ones and you can fill them in with the encrypted values

mikermcneil commented 8 years ago

@wesleytodd re req/res abstraction, we're mocking these in sails for the purpose of socket.io requests. That code is definitely not browser compatible but might be useful if you need a list of properties on req/res you need to mock

wesleytodd commented 8 years ago

https://github.com/wesleytodd/nighthawk/blob/master/lib/request.js

That is what im doing right now. But link up yours so I can see what I am mssing!

wesleytodd commented 8 years ago

Actually, maybe we can see about abstracting it out so we have something to share. I can take a look at that this weekend if you think its possiable.

nateps commented 8 years ago

Separate from the issue of old browser support, my understanding is that .bind() is not as performant as a simple closure, and it is easy to avoid using it. This of course is the kind of thing that micro benchmarks are going to exaggerate, but I've seen this mentioned repeatedly (https://jsperf.com/bind-vs-self-closure for example).

Personally, we just avoid use of .bind() in all of our open source projects and in our internal style guide. I don't find the sugar compelling in this case vs. the performance tradeoff. This isn't probably performance critical in this case, but I find it simpler just to avoid using it than to use it only in non-hot code.

dougwilson commented 8 years ago

Hi @nateps, the only place we use .bind is https://github.com/pillarjs/router/blob/6bae01f1e9de52445b0f353b720214ca798b3089/index.js#L34 which is just a shim for when setImmediate is not in the global. I would recommend just shimming that.

nateps commented 8 years ago

Yes, you are correct, and shimming the global method does solve any concerns about IE 8 compatibility. Just wanted to note the performance consideration since you mentioned the idea of banning .bind().

Personally, I've found this method good to avoid altogether, primarily because it is easier to ask people to avoid things wholesale than to ask for contributors to have good judgement about using certain things only when they are not performance critical.

dougwilson commented 8 years ago

I agree. I avoid .bind in code because of performance reasons with current engines, and I don't find it that much clearer than just stacking functions anyway... :)

mikermcneil commented 8 years ago

@wesleytodd definitely-- although I think @dougwilson should be involved in this too, esp. re http2

Here's the function we use to build the virtual request object in Sails core: https://github.com/balderdashy/sails/blob/master/lib/router/req.js

Here's an example of building the virtual request then squirting it through some middleware (Basically incoming socket.io messages do this): https://github.com/balderdashy/sails/blob/master/lib/router/index.js#L168

And finally the usage that kicks the whole thing off (btw this is nothing special, and I have no particular attachment to this usage; I'm just linking to it for context-- basically this method ends up triggerring the code that builds the virtual request which I linked to above.): https://github.com/balderdashy/sails/blob/master/test/unit/virtual-request-interpreter.test.js#L12

and the implementation of sails.request(), just to show how it fakes the client streams: https://github.com/balderdashy/sails/blob/master/lib/app/request.js#L27

mikermcneil commented 8 years ago

@wesleytodd Hopefully we can simplify all that ^^ craziness! As is, it works with a lot of middleware out of the box (everything I've tried including the middleware being used there in core). I'm sure there is probably a significant subset of middleware that won't work with it though.

Ideally we get to where we can replace these mocks with some kind of abstractions; i.e. "request emitter" and "response receiver".