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

Deep Integration with Vue Router #9

Closed
gryphonmyers opened this issue Mar 16, 2021 · 33 comments
Closed

Deep Integration with Vue Router #9

gryphonmyers opened this issue Mar 16, 2021 · 33 comments

Comments

@gryphonmyers
Copy link
Contributor

The docs indicate that this plugin can be used with Vue Router for client-side routing, but does not go into any more detail. Is an example planned for this integration? Curious to see how one might get the file-based routing information into Vue Router routes.

@brillout
Copy link
Member

Is an example planned for this integration?

Yes

Curious to see how one might get the file-based routing information into Vue Router routes

I'm not that familiar with Vue Router but AFAIK you wouldn't do that. I believe with Vue Router you define a mapping between URLs and Vue components. This means that you would completely bypass vite-plugin-ssr's routing. Whether that's what you want depends on what you want to achieve.

Why do you want to use Vue Router instead of vite-plugin-ssr's routing?

@gryphonmyers
Copy link
Contributor Author

gryphonmyers commented Mar 16, 2021

Is an example planned for this integration?

Yes

Curious to see how one might get the file-based routing information into Vue Router routes

I'm not that familiar with Vue Router but AFAIK you wouldn't do that. I believe with Vue Router you define a mapping between URLs and Vue components. This means that you would completely bypass vite-plugin-ssr's routing. Whether that's what you want depends on what you want to achieve.

Why do you want to use Vue Router instead of vite-plugin-ssr's routing?

Ideally they'd work in tandem. Vue Router handling client-side navigation would prevent having to fetch the full payload for subsequent pages, allow for client-side transition effects, etc

BTW I've built similar integrations in the past. Happy to help if I understand a little more about how your plugin functions

@brillout
Copy link
Member

I don't encourage using Vue Router with SSR as it complexifies the architecture of your app.

That said: https://github.com/brillout/vite-plugin-ssr/tree/master/examples/vue-router

I've a plan for smooth transitions that is simpler than Vue Router. The idea is to use the "pjax" technique, some prior art:

The big advantage here is that we get smooth transitions without complexifying the user's app architecture. A founding principle of vite-plugin-ssr is to provide a scalable architecture.

BTW I've built similar integrations in the past. Happy to help if I understand a little more about how your plugin functions

Yes, that would be lovely. How does my plan sound to you?

@gryphonmyers
Copy link
Contributor Author

I don't encourage using Vue Router with SSR as it complexifies the architecture of your app.

That said: https://github.com/brillout/vite-plugin-ssr/tree/master/examples/vue-router

I've a plan for smooth transitions that is simpler than Vue Router. The idea is to use the "pjax" technique, some prior art:

The big advantage here is that we get smooth transitions without complexifying the user's app architecture. A founding principle of vite-plugin-ssr is to provide a scalable architecture.

BTW I've built similar integrations in the past. Happy to help if I understand a little more about how your plugin functions

Yes, that would be lovely. How does my plan sound to you?

I do understand where you're coming from here in trying to avoid complexity. In my view, an application should have a single source of truth for routing information, shared between server and client. Since you have your own abstraction for routing here, I can see why you'd be hesitant to integrate directly with any specific routing library - you are trying to use your routing abstraction as the source of truth and avoid having two routing solutions in play. I do worry though that by baking in any of these solutions you've linked, you'd actually be sacrificing some flexibility with regard to client side routing. A solution using these tools might be nice for some use cases, but there are features in proprietary client-side routers that you cannot recreate (nested views, dynamic routes, component navigation guards to name a few more vue router features you'd be shutting out).

I wonder if there is a solution somewhere along the lines of exposing your routing information in a way that it could be consumed and integrated with other routers for use in the client. The way Nuxt works is it generates vue routes from the page-based routing abstraction, then uses those generated routes in the client. That's obviously far too coupled to make sense given your design philosophy, but I wonder if the user (or an additional integration plugin) could take your routes and transform them to a proprietary router's expected format?

@brillout
Copy link
Member

could take your routes and transform them to a proprietary router's expected format?

If I can see a use case for it not covered by other simpler solutions, then I'll implement it.

nested views

With that pjax solution, you'll be able to do that in a simple way: you simply define a paramterized route, e.g. /product/:id/* and then discriminate what you render based on the URL. You then have smooth transitions between /product/tesla-roadster/reviews and /product/tesla-roadster/details. That's simpler than Vue Router.

The pjax solution is optional. It's actually not going to be included by default. Because server-side routing is the more classical way of doing things and it's what most web devs expect. It's easier to reason about. It doesn't suffer long-living user sessions that make deploying new versions more painful (these "Please reload your browser, a new version is available" toaster notification are a non-negligable degradation). Only a fraction of web dev demand smooth transitions. Most product managers don't care about smooth transitions.

That said, there are situations where smooth transitions are needed, hence the pjax solution I want to implement.

The pjax solution is optional so you don't have to use it if you don't want to. I do care about the premise that vite-plugin-ssr is compatible with the entire ecosystem. That's why I'm showing the integration examples React Router and Vue Router: it should become clear that vite-plugin-ssr is usable with anything. Even though I don't see Vue Router or React Router to have that much of a place (zero place?) in an SSR world.

I'm more than happy to be shown a use case where a pjax solution would be an inferior solution to Vue Router.

nested views

See answer above.

dynamic routes

The pjax solution will do that.

component navigation guards

I'm not sure what you mean here but note that you can do route guards with route functions.

// /pages/admin.page.route.js

// Route functions allow us to implement advanced routing such as route guards.
export default ({ url, contextProps }) => {
  if (url==='/admin' && contextProps.user.isAdmin) {
    return { match: true }
  }
}

more vue router features you'd be shutting out

I'd be happy to hear them out.

@gryphonmyers
Copy link
Contributor Author

gryphonmyers commented Mar 17, 2021

See answer above.

Someone who is used to working with Vue Router expects to be able to compose components within each other under nested router views. What you're describing sounds like it would, within the Vue ecosystem, constitute a workaround / hack / wheel recreation.

https://next.router.vuejs.org/guide/essentials/nested-routes.html#nested-routes

The pjax solution will do that.

Really? It will do this?

https://next.router.vuejs.org/guide/advanced/dynamic-routing.html

I'm not sure what you mean here but note that you can do route guards with route functions.

This is what an in-component guard looks like:

https://next.router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards

The ssr routing guards only run on the server, right? In some cases it is useful to have navigation guards that run on the client, and as indicated by the link above, it is standard practice to do this within the component itself.

Even though I don't see Vue Router or React Router to have that much of a place (zero place?) in an SSR world.

IMO that's not really a decision your plugin should be making for your users (nor do you seem to have, based on the fact that your docs do reference client-side routers). As you've clearly set out to do, do one thing, do it well - server side render the things. If your users want to rely entirely on SSR payloads, great, but there is a common more hybrid approach where the initial payload is SS-rendered or pre-generated, and the client takes over from there. I see no great reason why you shouldn't support both. The only piece that's missing is clear usage guidelines for making that hybrid approach work with the abstractions you provide (and potentially other things making such an approach feasible, not really sure without digging in).

If you wouldn't mind giving me some time, I'll mess around with your plugin and see if I can come up with a recommendation for a usage example with Vue Router, and whether there is additional data I'd suggest exposing to make that integration more feasible.

@brillout
Copy link
Member

IMO that's not really a decision your plugin should be making for your users

You're right.

In afterthought, no matter the arguments of server-side routing VS Vue Router, if your wish is to use Vue Router then I should accommodate for that. Even just habits is a good and valid reason to use Vue Router.

If you wouldn't mind giving me some time, I'll mess around with your plugin and see if I can come up with a recommendation for a usage example with Vue Router

Sounds good.

if the user (or an additional integration plugin) could take your routes and transform them to a proprietary router's expected format?
whether there is additional data I'd suggest exposing to make that integration more feasible.

Ok. I'll implement what you need.

@brillout brillout added documentation enhancement 🚀 New feature or request labels Mar 17, 2021
@brillout
Copy link
Member

FYI Client-side Routing landed: https://github.com/brillout/vite-plugin-ssr#import--useClientRouter--from-vite-plugin-ssrclientrouter

I'd be more than happy to offer a lower-level API to accommodate for a deep Vue Router integration. Looking forward to see how this is going to look like.

@gryphonmyers
Copy link
Contributor Author

gryphonmyers commented Mar 26, 2021

Cool! I just took a look - looks good for use cases that don't require a proprietary router.

Regarding the lower level APIs that might be needed to perform a proprietary router integration, here's what I'm thinking: expose a function usable in the client (to be called in _default.page.client.js or similar) that would return all routes in the application, pre-parsed via pathToRegexp.parse. so if I have

/pages
  about.page.vue
  news.page.vue
  news.page.route.js // export default "/news/:newsSlug/"

I should be able to call await getParsedRoutes() or similar and get back:

[
  { id: "about", tokens: ["about"] },
  { id: "news", tokens: ["news", { name: "news", prefix: "/", suffix: "", pattern: "[^\\/#\\?]+?", modifier: "" }] }
]

Why am I suggesting a parsed route rather than a routing string? With the token list, one should be able to re-compile a route specification in another router's supported path syntax, whatever that may be (in the case of Vue, it's similar to pathToRegexp but with some subtle changes) without having to load in the full pathToRegexp dependency in the client. With this information, I believe one could construct a list of routes for the client router that matches up with the routing performed by the server assuming that they come pre-sorted in order of priority.

It would also be useful to be able to get a page by id. I think the function getPage_ currently does this, but perhaps should be exposed for public use? Maybe rename to getPageById? Reason this is useful: pairing each route with a dynamic import of its corresponding component. We end up with something like:

const routes = await getParsedRoutes()

app.use(createRouter({
  routes: routes.map(route => ({
    path: convertTokensToVueRouterPath(route.tokens),
    component: async () => (await getPageById(route.id)).Page //handling of contextProps omitted for now
  }))
}))

Does that all make sense? Do you see any problems with this? The only thing I haven't quite thought through yet is how features like nested routes might be supported... going to give that some more thought.

@brillout
Copy link
Member

Yes that makes sense.

One question: can path be defined as a function function isMatch(url: string): boolean instead of a pathToRegexp string? That would simply things.

Also I'd be totally ok with changing from the current pathToRegexp implementation and replace it with Vue Router's one. That would get us quicker to an MVP. Later we can implement getParsedRoutes() for other proprietary routers.

Btw the other two GitHub issues are fixed in 0.1.0-beta.21. Make sure to also pin Vite to 2.1.3.

@gryphonmyers
Copy link
Contributor Author

Yes that makes sense.

One question: can path be defined as a function function isMatch(url: string): boolean instead of a pathToRegexp string? That would simply things.

Also I'd be totally ok with changing from the current pathToRegexp implementation and replace it with Vue Router's one. That would get us quicker to an MVP. Later we can implement getParsedRoutes() for other proprietary routers.

Btw the other two GitHub issues are fixed in 0.1.0-beta.21. Make sure to also pin Vite to 2.1.3.

I have been working on a POC vue router integration - stand by for more thoughts on this topic

@brillout
Copy link
Member

Exciting. Let me know if there is anything you need from me, or if you have questions about the code (some parts are not source code reader friendly yet).

@gryphonmyers
Copy link
Contributor Author

gryphonmyers commented Mar 30, 2021

One thing I already know will be useful - exposing retrievePageProps as a public function, but with fallback to window.__vite_plugin_ssr.pageProps if it's the initial route. Use case is - being able to fetch the pageProps on navigation separate from the page component (currently the exported getPage function does both at once, and additionally it does not seem to have a retrievePageInfo implementation by default).

@brillout
Copy link
Member

One thing we could do is that you change vite-plugin-ssr directly: https://github.com/brillout/vite-plugin-ssr/blob/master/CONTRIBUTING.md#modify-vite-plugin-ssr.

We create a draft PR and take it from there. Comfy to discuss and collaborate on code changes. We can create the draft PR as soon as there is even only one LOC change.

Would that work for you?

@gryphonmyers
Copy link
Contributor Author

Yeah sounds good

@gryphonmyers
Copy link
Contributor Author

#24

@brillout brillout changed the title Vue Router example Deep Integration with Vue Router Apr 3, 2021
@gryphonmyers
Copy link
Contributor Author

gryphonmyers commented Apr 4, 2021

Also I'd be totally ok with changing from the current pathToRegexp implementation and replace it with Vue Router's one.

So on this topic, I'm wondering if there is a way we can structure this such that the route resolution is overridable / not necessarily coupled to any routing solution.

Default (pathToRegExp-based) Routing

my.page.route.js

export default "/:myPathToRegexParam?"

(location unknown)

/**
* @param {(string|Function)[]} routes The accumulation of all my.page.route.js files and / or static page identifiers in an unsorted state. 
* @returns {(string|Function)[]} Sorted routes
*/

export function sortRoutes(routes) {
  /* apply default, common-sense sort */
  return routes.sort(defaultRouteSort);
}

/**
* @param {(string|Function)[]} routes Pre-sorted routes to be matched
* @returns {string|Function|null} A single matching route
*/

export function matchRoutes(routes, url) {
  return routes
     .sort(defaultRouteSort)
     .find(route => matchRouteByPathToRegexp(route, url));
} 

Vue Router integration

my.page.route.js

export default { path: "/:myVueRouterParam?" } /* "component" property is assumed to be the current page component 
and will be auto-populated. Though, presumably "children" property could be filled in here...? But then how do 
child components get auto-populated? */

(location unknown)

export function sortRoutes(routes) {
  /* Apply common-sense sort on Vue Router routes. Perhaps this in turn should expose application-specific overrides as the default sort might not always work as needed... */
  return routes.sort(sortRoutesByVueRouterPriority);
}

/**
* @param {(string|Function)[]} routes Pre-sorted routes to be matched
* @returns {string|Function|null} A single matching route
*/

export async function matchRoutes(routes, url) {
  const router = createRouter({
    routes,
    history: createMemoryHistory()
  });
  
  await router.push(url);

  return router.currentRoute; // Not 100% accurate, basically pseudo-code.
}

Something like this could allow for overriding the default routing mechanism with something more specific to an application's routing needs. As indicated above, I don't really know where this code goes since it seems like it would be application-specific, not page-specific, and currently there is not a place where the plugin stores application-specific overrides. I wonder if the plugin could expose some sort of plugin interface for overriding behavior like this (while also making those overrides portable)? E.g. a "@vite-plugin-ssr/vue-router" plugin could package this integration up for easy consumption.

One other topic / thought here that is a little bit of a tangent, but just throwing it out there as something to think about: Vue Router has a concept of nested routes, which correspond to composed router views. E.g. /user/preferences might render a "user" view with some preference configuration tab selected, while /user/history might render the same view with a history tab selected. The real question here (and I don't have an answer yet) is do the current APIs accommodate or hinder this concept? I feel like more complex routing needs like this should be considered, even if they are off the beaten path, and we should understand how one might achieve such an application structure within this system. A cautionary tale: Nuxt attempts to support this feature through the "nuxt child" feature but in my opinion, it is wildly unintuitive / confusing. I believe a more elegant solution is possible... going to think on it.

EDIT: Forgot to mention that the solution I'm offering above makes it trivial to integrate with Vue Router in the client. As long as we have a way of fetching those routes (pre-sorted by the server / prerender process), it can instantiate a vue router instance similar to the above example. Route transformation (as I had previously suggested) would no longer be needed as they are authored in the Vue Router format within the application both on server and client.

@brillout
Copy link
Member

brillout commented Apr 4, 2021

I'd suggest we proceed like the following:

  • You fork vite-plugin-ssr and implement the changes you need.
  • Unlike the previous PR: 1. we don't publish your changes instead you use npm run link (allowing you to modify the vite-plugin-ssr source code while testing the changes on the examples, see CONTRIBUTING.md), 2. you try your changes on /examples/vue-router and improve the example to progressively have a more and more deep Vue Router integration.
  • We do things iteratively one step at a time. If you are stuck on the next step, we can figure out the next step together.
  • I'm not too familiar with Vue Router, so I guess it's more your expertise. I'm not sure I'm the best person to help about Vue Router related questions.
  • Since I'm not too familiar with Vue Router I don't see to be much of a help about the grand plan visions of things.
  • But I'm happy to help out for implementing modifications on vite-plugin-ssr; ideally by asking questions directly about the source code here or at your vite-plugin-ssr fork.

Allowing to replace the Filesystem Routing with a custom one should AFAICT fairly easy to achieve.

export { useCustomFilesytemRouter }

let customFilesystemRouter;
function useCustomFilesytemRouter(_customRouter) {
  customFilesystemRouter = _customRouter
}

function routeWithFileSystem() {
  if( customFilesystemRouter ) {
     // ...
  } else {
    useDefaultFilesystemRouter()
  }
}

As for accessing all routes of all pages: check out the loadPageRoutes() in route.shared.ts. I believe this is what you want.

Any other questions about the vite-plugin-ssr source code?

We can discuss things like plugin architecture later. (I've a couple of ideas here but let's first do quick & dirty hacks to improve /examples/vue-router/.)

@gryphonmyers
Copy link
Contributor Author

OK sounds good. I'll start putting a PR together.

@Dema
Copy link

Dema commented Jun 12, 2021

One big disadvantage of ssr-only routes is that an application "hangs" while data is being fetched by ssr engine. I'm seeing queries to /index.pageContext.json files returning data from my graphql enpoints. And I have no "loading" indicators while it is being loaded. I'd prefer this behavior only on the first page load, after page is loaded, it's better to use client-only routing without hitting the server at all. This will allow much smoother UX and it greatly reduces load of SSR server.

@brillout
Copy link
Member

@Dema see #95

@gryphonmyers
Copy link
Contributor Author

One big disadvantage of ssr-only routes is that an application "hangs" while data is being fetched by ssr engine. I'm seeing queries to /index.pageContext.json files returning data from my graphql enpoints. And I have no "loading" indicators while it is being loaded. I'd prefer this behavior only on the first page load, after page is loaded, it's better to use client-only routing without hitting the server at all. This will allow much smoother UX and it greatly reduces load of SSR server.

This is why pre-rendering is great!

@brillout
Copy link
Member

We will tentatively merge the PR in 1-2 weeks.

@brillout
Copy link
Member

brillout commented Oct 7, 2021

Gryphon was busy but we should be able to finish up the PR fairly easy (thanks to the new onBeforeRoute() hook), and as soon as time allows on Gryphon's side.

@brillout
Copy link
Member

@gryphonmyers I guess you got caught up with high prio things :-) If you'd be interested in working on this, I'd be more than happy to do it together 😊 Warm regards from the Vike team. Removing the being-worked-on label in the meantime.

@gryphonmyers
Copy link
Contributor Author

gryphonmyers commented Oct 20, 2021 via email

@brillout
Copy link
Member

No worries; re-labeling as being-worked-on 😀.

@awacode21
Copy link

What is the state now?

@brillout
Copy link
Member

I'm not aware of any updates. That said, I believe vite-plugin-ssr is ready for it, see https://vite-plugin-ssr.com/onBeforeRoute.

@sebasptsch
Copy link

In the react-router example there's mention of this issue potentially allowing deep integration with react router and the docs state that it should be possible now using the onBeforeRoute function. Would it be possible to see the example updated to use it or is there an existing demo?

@brillout
Copy link
Member

brillout commented Aug 7, 2022

@sebasptsch To my knowledge, no one has tried to deeply integrate React Router before. So you're entering uncharted territory here. On vite-plugin-ssr's side onBeforeRoute() should do the trick, but I don't know how it would work on React Router's side.

@brillout
Copy link
Member

Closing, see https://vite-plugin-ssr.com/vue-router-and-react-router. Bottom line being:

If you want a feature provided by Vue Router or React Router that vite-plugin-ssr is missing, then open a new feature request.

@brillout brillout closed this as not planned Won't fix, can't repro, duplicate, stale May 31, 2023
@stskavishkannanm

This comment was marked as off-topic.

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

No branches or pull requests

6 participants