-
-
Notifications
You must be signed in to change notification settings - Fork 103
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
Add support for named routes #36
base: master
Are you sure you want to change the base?
Conversation
I can't believe my eyes! Great job! 👏 Here are few thoughts to consider though:
So thanks a lot for trying to fix things up, I'm pretty glad someone is doing the effort. And I hope this is the beginning of some discussion about how to really fix it! Thanks! |
Yes, it would be difficult to prevent this in Router as far as I can see as Routers don't know about each other.
I understood from expressjs/express#2739 (comment) that it would be problematic to add a |
index.js
Outdated
Router.prototype.route = function route(path) { | ||
var route = new Route(path) | ||
Router.prototype.route = function route(path, name) { | ||
if(arguments.length > 1 && this.findRoute(name) != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
simpler to just check if (name && ...
. IMO that is also more explicit about the intention of this code.
Ok, some brief thoughts on functionality like this (I do like it, just considering how it can be improved). Firstly, there's a big issue with nested routers here - I think a lot of people do use nested router functionality. If we're going to expose this level of functionality, I'd love to support as many people as possible. Also, it's possible to implement reverse functionality since I have support for it in So, thoughts: What if finding routes by name was just a map instead of an iteration of the stack? Now, what if we also restricted the key to a subset of characters (same as Edit: I believe this would resolve what @fourpixels is asking for. Of course, the path can only be built from the ground up (if you build it from the nested route, it won't know the path to child routes). On top of this, I think |
I was just about to say that One questions - where do you think this map should be in? Just inside the @sjanuary don't get me wrong - I just think there should be a little bit more things changed so that this starts actually working for everyone. Adding just a name and leaving people still implementing their own version of reverse routing won't solve the problem, but it's the beginning of the journey. |
I like the idea of using a map, being able to do I'm hearing that the |
I can't think of any other good reason doing all this, and having named routes, except backtracking and referring to them. |
There was actually a bit of discussion about the pull request in the Express.js community hangout last night :) Here are some of my main high-level thoughts on the pull request, as it is today:
Besides those three things, I would re-iterate the thoughts already made, adding that I think a lot more throught needs to be put into the design, especially how the feature will interact with the other current features of the router and how to a possible implementation will meet some kind of goal for users to use. All that said, nice work! |
To speak a bit to the "use cases" that this feature is good for: When writing api's it is a common practice to return a set of links, in our apps we do this:
It would be nice to be able to do something like this: res.json({
_links: {
next: url.format({
// ...
path: router.routes.streams.reverse({/* ... */})
})
}
}); That is the main use case I can think of for this feature that I would use. Obviously the actual semantics would be up for debate, if it is res.json({
_links: {
owner: url.format({
path: router.routes.owner.reverse({/* ... */})
})
}
}); I think it would also be helpful for this to expose something more like: req.route.reverse(); This probably has a ton of other issues because things can modify the route and what not, but it would be nice. Is that helpful and/or what you all where thinking of this for? |
@wesleytodd The reason I suggest a method called |
I like the sound of that api, my previous comment was more focused on the use case than the api specifically. But... The issue I see with the api you mentioned is that if you change where a router is mounted, then my examples above also have to change everywhere I use the To outline the problem more clearly, here is using the api in the PR method: var routerA = new Router();
var routerB = new Router();
var r = routerB.route('/some/:thing', 'foobar')
r.get(function (req, res) {
// The issue here is that routerB needs to know that it was mounted
// on routerA in order to jam the paths together, which a route does not know.
res.json({
link: routerB.routes.foobar.reverse({path: 'foo', thing: 'bar'}) // /some/bar ??
});
});
routerA.use('/base/:path', routerB); With the api you are proposing, correct me if I am wrong, we would need to hard code the full paths in all the response places: var routerA = new Router();
var routerB = new Router();
var r = routerB.route('/some/:thing');
r.get(function (req, res) {
// For sure knows the right path, but if we were to mount somewhere else we would
// have to change all these, and also is unfriendly to use because the whole point
// of using sub routers is to encapsulate functionality without knowledge of how it
// all might come together.
res.json({
link: getPath('/base/:path/some/:thing', {path: 'foo', thing: 'bar'}) // /base/foo/some/bar
});
});
routerA.use('/base/:path', routerB); If I understand that issue correctly from above, then it seems like we could come up with a hybrid of the two apis with a few simple things to give the correct context. The main thing would be telling the router where it was mounted in a parent app/router. We could do something similar to what express does for mounted apps. |
@wesleytodd Please re-read my initial comment. It did not suggest just replicating the routes, but instead using a traversal of the names you've used to generate the path. E.g. |
To go further, yes, you've got the right use-case. I'm don't have any other use-cases in mind. I'm only commenting on the application of the use-case. var routerA = new Router();
var routerB = new Router();
var r = routerB.route('/some/:thing', 'thing');
r.get(function (req, res) {
res.json({
link: routerA.getPath('routerB.thing', {path: 'foo', thing: 'bar'}) // Also possible: `routerB.getPath('thing')` but the path is now shorter and not complete. You'd need a reference to the "root" of where you're generating the path from.
});
});
routerA.use('/base/:path', 'routerB', routerB); // Use the name we're proposing to add here already. |
I understand your point and you're totally right about all the things. But the one I'm most afraid of is
If we think of an Express kind of app (just for a second), and the most common use case - To me it seems more like a workaround, with a bit nota bene sign: keep in mind to reverse the route from the main router, or it won't work. Unfortunately I can't think of any other possible solution, as I understand the Router should not care where it's mounted. |
@blakeembrey I miss-understand your original comment, also I made my last comment on github mobile, which makes it very hard to read the context from the rest of the conversation. Sorry :) That last code example made way more sense to me and now I get it. I still think it is not optimal to have to have access to |
@fourpixels Yeah, it's absolutely a workaround. The current model of the router doesn't allow for something else. Of course, we're allowed to imagine a world where the router is a different beast, but that's for another module 😄 @wesleytodd All good, I was on mobile too and get lazy typing, so that's why the response was super short. Should really wait to reply properly. Yes, your proposal is possible (it's actually what I mentioned in my last comment) by using an array and pushing and popping as you move along. The only issue is that you then need to use relative links to other routers to get the full path of another router. |
This is still very much a work in progress (loads more tests needed), but I did check in some changes I made today partly because I wanted to discuss the possibility of updating |
Yea, there is various discussion around this, including in the Express.js repo itself. The dependency is already at the max version before breaking changes, so it can (and will) be upgraded, but can't until Express 5.0 (version 2.0 of this module). That is definitely OK to do, though it's really a task in itself. I recently shared more information about what we need to do around getting it updated in expressjs/express#2057 (comment) |
Cool, I didn't know there was a work in progress label! The failing test when I updated path-to-regexp was this one, which I just commented out for now in this PR. |
@sjanuary Yes, that test has been noted before: pillarjs/path-to-regexp#57. This functionality would need to be 2.0 - which has a few other PRs waiting too 😄 |
index.js
Outdated
var name | ||
|
||
// If a name is used, the last argument will be a string, not a function | ||
if (callbacks.length > 1 && typeof callbacks[callbacks.length - 1] !== 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a little too liberal, due to the flatten above. For example, this would allow for router.use([[[[function(){}]],'name']])
and pick up the name, when it probably needs to be restricted to being a top-level argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also keep in mind we should be able to describe the arguments in TypeScript, since that will be coming to these modules soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd go for the route name as the second parameter, for the same reasons @dougwilson has mentioned above. The middleware arity is infinite so it's not possible to type this interface (easily).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we just shift them? I'm interested how this is going to be described in TypeScript :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shift which? For TypeScript, it'll look like:
type Middleware = (req: any, res: any, next?: (err: Error | string) => any) => any;
function use (...middleware: Middleware[]): this;
function use (path: string, ...middleware: Middleware[]): this;
function use (path: string, name: string, ...middleware: Middleware[]): this;
However, with the name at the end, it looks a little inconsistent. I did realise that since it only accepts a single middleware function, it's less of an issue, but seeing the signatures like this does make it more obvious.
function use (path: string, middleware: Middleware, name: string): this;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd go for the route name as the second parameter, for the same reasons @dougwilson has mentioned above. The middleware arity is infinite so it's not possible to type this interface (easily).
If only one string parameter is passed how do you tell if it's the path or the name? The TypeScript above looks like you insist on a path being used if you want to use a name, which I could implement if that seems like a better choice. The current implementation was designed to allow:
function use (middleware: Middleware, name: string): this;
Hi @sjanuary, I took a look for a bit and gave some initial comments, but I have to move on to something else for the time being, so will finish looking again probably tomorrow. Please add documentation for your new functionality to the README. I know it may not be finished yet, but documentation in the README and really helps me understand what it is supposed to do by being able to an English description of what to do and looking at an example of it's use in the example section. |
Thanks for all the comments @dougwilson. I've replied to a couple inline and for the rest I will make the changes you suggested. |
@blakeembrey - Is #29 a definite for 2.0? If so, I could rebase if/when it is merged although I can see it's a really big change so it might not be straightforward. I would expect that if this PR is accepted then anyone extending the router would also have to implement I'm just working on an example for this, but I think I've implemented all the other comments so far. |
It's up to @dougwilson, but I really hope so because I've had the PR open for over a year now and there's two other router implementations waiting for this. The issue is if the router subclassing gets merged, then it makes your API changes hard to do because subclassed routers can have more than one "path" argument. That's why I suggested thinking about the API a little more first, a simple solution would be |
@blakeembrey I will look into it if @dougwilson can confirm that #29 is definitely going in. Maybe it could be implemented as a subtype instead of changing the API for the base Router, which would allow it to be stricter on number of path arguments etc without compromising other use cases. |
Is there a status on this? I really would like to use named routes in my application |
Is there any news regarding this? It seems neither this nor #29 got much traction. Is there something that can be done to help? |
7e90c9e
to
8643ec6
Compare
There have been a couple of requests in the Express project for supporting named routes, expressjs/express#2746 and expressjs/express#2739. This PR enables a name to be used when a route is created via
Router.route
. Routes with names can be looked up via a new method -Router.findRoute
. Thename
argument is optional so no existing functionality should be broken. A test is also included.