-
-
Notifications
You must be signed in to change notification settings - Fork 408
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
URL Manager #570
URL Manager #570
Conversation
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
Co-Authored-By: Ricardo Mendes <rokusu@gmail.com>
@service router; | ||
|
||
fromURL(url: string): RouteInfo { // /blogs/1/posts/2?foo=bar | ||
let [path, query] = url.split('?'); |
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.
Edge case, but this would fail for a URL including more than one ?
like: https://example.com/foo?param?otherParam
You could use the URL
, however it's not natively available in IE11 and may be significantly slower than plain string processing. It could also be faster though.
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.
are multiple ?
valid? wouldn't you need to escape additional ?
?
fwiw, I'm not proposing that these examples be actual implementation. They need tests, and there are def cases not covered
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.
Yes, they are valid: https://stackoverflow.com/a/2924187/420747
Not really common though.
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.
legit. so, whenever someone implements this, they should def not do a naive split on '?', but split on the first occurrence of ?
## Unresolved questions | ||
|
||
- Should the URL Manager exist as a static config or an instantiable object per-route? The above proposal is a static config / singleton, but allowing an instance per route would allow for more varied state buckets, but could also make debugging harder as there would be an URL Manager for each route segment. | ||
|
||
- `toURL` / `fromURL` or `serialize` / `deserialize`? |
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.
(Lazy-loaded) engines should be considered in this RFC as well.
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.
is engine support totally in addon space? I know nothing of engines? how would URL manipulation affect engines?
or is it more just that there could be multiple routers?
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.
Engine support is in core and addon space. Routable engines partake in the routing process. They define their routing map in routes.js
and are mounted in the parent routing map via mount(name: string, options?: MountOptions)
.
The engine routing maps are basically merged with the host routing map — it's a bit more complicated — but engines don't have their own Router
instances.
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.
ah ok, then I think everything should be compat with this.
App defines the CustomURLManager, and if needed, engines could override that per route
text/0000-url-primitives.md
Outdated
let english = | ||
path.split('/') | ||
.segments.map(segment => { | ||
return this.i18n.lookup(`routes.from.${segment}`, 'en-us') || segment; |
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.
Wouldn't this imply that the en-us
locale contains the mappings from all other languages' segments to the names used in the code? Shouldn't this mapping actually be part of every individual locale?
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.
Wouldn't this imply that the en-us locale contains the mappings from all other languages' segments to the names used in the code?
yes? maybe? depends on how you'd want to implement it
Shouldn't this mapping actually be part of every individual locale?
maybe. I don't know how molt people would want to implement this. Ember's route's are typically named in english, hence the approach here.
With this RFC, I mostly just want to enable people to play around with this, and I hope that someone does a much better job with i18n routes than I have in this example.
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 just wanted to note, that if used like this, it'd probably be wiser to remove the en-us
in favor of the current locale.
app and addon authors to customize the use of the URL— | ||
including internationalized routes, dynamic segment slugs / aliases, | ||
and alternative query params behavior—though, | ||
implementation of those specific things is outside the scope of *this* RFC. |
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.
isn't that something that the Location
API can already handle? 🤔
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.
Location api can't handle nested/array query params, which is a common ask
Co-Authored-By: Jan Buschtöns <buschtoens@gmail.com>
Co-Authored-By: Jan Buschtöns <buschtoens@gmail.com>
|
||
- Should the URL Manager exist as a static config or an instantiable object per-route? The above proposal is a static config / singleton, but allowing an instance per route would allow for more varied state buckets, but could also make debugging harder as there would be an URL Manager for each route segment. | ||
|
||
- `toURL` / `fromURL` or `serialize` / `deserialize`? |
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.
toURL
/fromURL
orserialize
/deserialize
?
definitely toURL
/ fromURL
- with (de)serialize
you are constantly question which direction is serialize? 😇
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.
There are some really interesting ideas here. I had a couple notes/suggestions/fixes along the way, but my bigger concern is: adding further complexity to the internals of the router seems extremely high risk to me. I've just spent the last quarter dealing with pain from upgrading from 3.4 → 3.8, which included a pretty significant router internals rewrite in 3.6—and while the majority of the bugs we found and fixed were the result of our app's use of private router API (🤢), the worst of the bugs we fixed were extremely subtle issues related to changes deep in the router.
In the medium term, I'd very much like to see (and I think others would as well) Ember's router become much simpler, so we should take care that designs in this space provide a way to isolate complexity and prevent the introduction of further complexity. As such, I'm concerned by the amount of new API surface around the existing router implementation.
class CustomURLManager extends URLManager { | ||
// Concrete example implementations are shown in the following sections. | ||
} |
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 just drop this entirely, given the comment here – given that all it does in this case is introduce effectively a no-op new prototype.
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.
that's what I had initially -- this was added as a clarification by another reviewer. The examples don't duplicate the router setup. imo, for learning intent purposes, it's good to keep. Would def not recommend an empty class if this were implemented.
```ts | ||
import { module, test } from 'qunit'; | ||
import { setupTest } from 'ember-qunit'; | ||
|
||
module('Unit | Route | blogs/blog/posts/post', function(hooks) { | ||
setupTest(hooks); | ||
|
||
test('it exists', function(assert) { | ||
let route = this.owner.lookup('route:blogs/blog/posts/post'); | ||
assert.ok(route); | ||
}); | ||
|
||
test('urls are resolved', function(assert) { | ||
let router = ; | ||
let sampleUrl = '/blogs/1/posts/2'; | ||
let resultUrl = toURL(fromURL(sampleURL)) | ||
|
||
assert.equal(resultUrl, sampleUrl); | ||
|
||
let mapInfo = router.mapInfoFor('blogs.blog.posts.post'); | ||
|
||
let sampleRouteInfo = { | ||
mapInfo, | ||
queryParams: {} | ||
dynamicSegments: { blog: '1', post: '2' }, | ||
} | ||
let resultRouteInfo = fromURL(toURL(sampleRouteInfo)); | ||
|
||
assert.deepEqual(resultRouteInfo, sampleRouteInfo); | ||
}); | ||
}); | ||
``` |
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.
In my experience, route unit tests are almost always a very bad thing. Granted that this might change the dynamics around that somewhat, but it also seems to me that what is being tested should not usually be for individual routes but instead for (a) the custom manager class and/or (b) the extended router class.
Note that even in your test here, you're not actually testing the route, you're testing how the router handles the definition of that route.
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.
where would a test for the url-manager live? tests/unit/url-manager.js
?
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 agree with @chriskrycho, we should write integration and acceptance tests for them
Co-Authored-By: Chris Krycho <hello@chriskrycho.com>
Co-Authored-By: Chris Krycho <hello@chriskrycho.com>
I'm all ears for other solutions that would allow people to customize query param (de)serialization as well as control how the URL is (de)serialized in general. This is RFC is entirely in response to comments from #380. ;)
should this RFC be implemented, all existing tests should be unchanged. :-
Do you have ideas for how this could be simpler?
🤷♂ it's 1 extra router config option, and 2 configs per route. :-\ |
Don't have time or mental bandwidth to do design work, alas, but—
It's not just that it's more API surface (though that's a concern) but that every one of these kinds of things introduces more internal implementation, and much of the router's current behavior—not least around URL resolutions, including especially for QPs—is most accurately described as "emergent and incredibly complex". Every new code path has surprising opportunities to introduce bugs there, and "just add more tests" isn't really a good answer there: there's already a ton of test coverage, but because the existing implementation has all that emergent complexity, there are many interactions which are difficult to cover exhaustively, and there are many places where the issues come down to subtle timing details. And of course: if we knew what needed more coverage, someone would write it; the problems that come up in this space are from unknown unknowns. To be clear, I'm not trying to come crap on this design. I'm just saying that all of those concerns should be considered very high priority constraints on this and other designs in this space, given the relatively high likelihood of accidental breakage even when extreme care is taken in the design. |
I was planning on replacing as much of the internal implementation with this stuff as makes sense.
that's exactly why I'm proposing some primitives for us to un-complexify URL/QP behavior.
of course ;) 'just also kind feels like FUD. :-\ |
Frankly, then: this will cause exactly the kind of issues I’m describing. (This is not FUD; that’s a rude and foolish way to describe someone attempting to shed light on something.) I’m happy to help further clarify why and how this is risky, but I’m not making crap up; I’m highlighting a risk area that it’s perfectly fair and reasonable for you to have been unaware of. I was unaware until a few months ago! 😄 |
Let me try to put this a different way: I think this idea or something like it is good and necessary. We can’t avoid making changes to high risk areas forever because otherwise it just gets worse and worse. However, we may need a different strategy for how we run a transition in this area because of the high risk (especially for large apps like the one I work on!) than we need for most features. The point I’m trying to make is not “this is bad; let’s not do it” but “this is much more complicated than it appears; let’s think further about what it will take to accomplish this, possibly including very different paths to getting to the desired end state proposed”. |
Love the idea, but echoing @chriskrycho, I would also like to see the existing router reduce complexity and expose existing abstractions for experimentation before expose introducing a new concept. I think a URLManager is a good end goal but agree that it’s high risk to try to get here before going through some intermediate steps first. |
@mehulkar what existing stuff would you like to see exposed? |
the apis proposed here are intended to be the low level entrypoints into anything you'd want from the router -- as is, atm, I don't know where the router URL gets pushed back in to the address bar. I don't think it'd be a good idea to make these methods public, as it would make testing the URL transforming behavior more difficult if it were customized, as you'd need to setup the entire router -- and that would take you out of the realm of unit testing. :-\ |
I wrote about some ideas here: https://mehulkar.com/blog/2019/12/post-octane-ember-routing/. I'm not particularly saying the a URLManager for serialization/deserialization is bad, but more that there's no good place to put it yet, because the routing surface area is so big. I'd like to see the |
what do you imagine would take its place? ideal world, no consequences |
@NullVoxPopuli I think that post covers it, but tldr;
|
To be honest I'm not an expert on the router (@machty is the only expert), but any backward compatible way of dealing with query params is going to add more surface area to the api until we deprecate controllers. We do need to find a way forward. |
Closing this for now due to lack of time / community interest :) |
FWIW I think modularity around URL serialization/deserialization can be useful and liked the idea of this RFC. Sorry to hear that there wasn't enough interest. For example, we've implemented our custom location type (https://api.emberjs.com/ember/release/classes/Location) to do locale-prefixing of routes. Basically, all routes have a But by putting the locale in the route we get two versions of the same website, with different languages, plus make it easier for search engines to parse. Obviously possible already today with custom Locations, but this seems like an even more flexible solution. |
rendered