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

Rendering JSON responses #568

Closed
brillout opened this issue Dec 20, 2022 · 17 comments
Closed

Rendering JSON responses #568

brillout opened this issue Dec 20, 2022 · 17 comments
Labels
enhancement ✨ New feature or request

Comments

@brillout
Copy link
Member

Description

From @XiNiHa

Would it be difficult to make VPS support rendering JSON responses? I'm currently thinking about experimenting with a SolidJS-based metaframework that is focused on Islands architecture with partial navigation. SolidStart implements this by accepting a POST request on the same URL with the concrete route, and I want to try the same approach.

How would such an example POST request look like and what would the JSON contain?

@brillout brillout added the enhancement ✨ New feature or request label Dec 20, 2022
@XiNiHa
Copy link
Contributor

XiNiHa commented Dec 20, 2022

I was originally thinking about to make it possible to return any kind of JSON responses (like what API routes do) and letting the users implement the actual rendering part. Actually, this will enable users to implement API routes in somewhat easy way.

@brillout
Copy link
Member Author

Currently, with Client Routing, the initial HTML request is (and needs to be) HTML, while subsequent requests are actually JSON. So I'm not sure how this feature request differs with the current satus quo.

I'm wondering how this connects to island archtiecture. The bottom line and goal of island archtiecture is to ship less JavaScript to the client-side. What kind of JSON response do you want to make that would enable you to achieve island architecture?

@XiNiHa
Copy link
Contributor

XiNiHa commented Dec 20, 2022

Actually it's not required that the response should be in JSON. All I need is just a part(not a whole page) of HTML that can be fetched through JS.

@brillout
Copy link
Member Author

In onBeforeRender() you can check whether the request is for the initial HTML or a subsequent client-side routing with pageContext.urlOriginal.endsWiths('.json').

You can then add your HTML parts to pageContext and use passToClient as usual.

@bagr001
Copy link

bagr001 commented Jan 9, 2023

Hi, I am facing the same need.

I have SSR app with backend (fastify) integration. In the VPS app I have some modules that are used in onBeforeRender to create initial pageProps. I would also want to use those modules (that are build and bundled by vite + VSP) for serving plain JS data as API response.

ATM I found a wokraround how to do it in VPS, but it is not standard/documented way and I am aware it may break one day. I will try explain my sollution:

Goal: I want to serve some data in endpoint /api/data

  1. Create page File pages/api/data.page.server.ts that exports only onBeforeRender (export { Page } is not needed)
// pages/api/data.page.server.ts
import { getData } from 'some-data-module';

export const onBeforeRender = async (pageContext) => {

    const data = await getData();

    return {
        pageContext: {
            pageProps: { // pageProps is required by VPS
                data,
            },
        },
    };
};
  1. Create renderer File pages/api/_default.page.server.ts that does nothing.
export async function render() {
    return {}; // return empty object as no other props are needed
}
  1. in your server set up /api route
app.get('/api/*', async (req, res) => {
    const pageContextInit = {
        urlOriginal: req.raw.url
    };

    const pageContext = await renderPage(pageContextInit);
    
    res.status(200)
        .type('application/json')
        .send(pageContext.pageProps); // return pageProps that were generated in onBeforeRender

    return res;
})

The same way you can also implement POST API endpoint and passing some request data to pageContextInit

But as I mentined before, it is just a hacky workaround. I am thinking of different sollution. @brillout Would it be possible to define some file that will hold additional modules exports that will be accessible alongside import { renderPage } from 'vite-plugin-ssr';?

Example:

  1. define some /exports.ts file
// /exports.ts

import { getData } from 'some-data-module';
import { someFunction } from './utils';

export { getData, someFunction }
  1. use exported modules in server.ts
import { renderPage, exports } from 'vite-plugin-ssr';
...
app.get('/api/data', async (req, res) => {
    const data =  await exports.getData();
    
    res.status(200)
        .type('application/json')
        .send(data);

    return res;
})

Thanks!

@brillout
Copy link
Member Author

@bagr001 What's your motivation for not completely bypassing VPS and creating a normal Fastify route instead?

@bagr001
Copy link

bagr001 commented Jan 23, 2023

@brillout My motivation is to share code that is transpiled by Vite and used both in VPS SSR and also directly in server code. It would be ideal to completely bypassing VPS, but I cannot find a solution how to solve this ATM :-(

In my primary use case, I aim for data-caching. Let's say I have a function that fetches some data and stores them in a module exported variable that acts like a cache. I want to access this data from VPS onBeforeRender method and also directly from server code. For example, I want to trigger a cache warmup before the server starts listening (and/or setup refresh interval in server code), but access the same data in render functions in VPS SSR.

There is a workaround to make the caching code as an external module that is built independently, but then the DX sucks for my scenario. That is why I am trying to find a way to transpile some server code by Vite and make it accessible from the server - same as VPS plugin works. Thanks.

// server/index.ts

await cacheWarmup();

server.listen();
// data/cache.ts

export let cache;
export async function cacheWarmup() {
    cache = await fetch('/api/data');
}
// pages/index.page.server.ts

import { cache} from '@/data/cache';

export const onBeforeRender = async (pageContext) => {
    return {
        pageContext: {
            pageProps: {
               cache,
               ....
            },
        },
    };
};

@brillout
Copy link
Member Author

@bagr001 Either use globalThis.cache instead of let cache or make Vite transpile your server code with vavite or HatTip, see #562. (FYI in case your company is up for it: https://github.com/sponsors/brillout.)

@leonmondria
Copy link

Maybe take a look at telefunc?

@brillout
Copy link
Member Author

brillout commented Jan 23, 2023

Maybe take a look at telefunc?

👍 Telefunc is an option as well, since .telefunc.js files are processed by Vite.

(Regardless of the solution you go with, I recommend looking into that globalThis technique – it's useful knowledge.)

@leonmondria
Copy link

I have a similar use-case where I just want a fragment, instead of the entire html/head.

Did it like this, but relies heavy on the _pageAssets:

import { renderPage } from 'vite-plugin-ssr';

async function renderFragment(requestContext) {
  const vpsRenderResult = await renderPage(requestContext);

  return {
    fragment: {
      body: vpsRenderResult.markup.body,
      assets: vpsRenderResult._pageAssets,
    }
  };
}

app.get('*', async (req, res, next) => {
  const { fragment } = await renderFragment({/* context */});
  if (!fragment) {
    return next();
  } else {
    res.send(JSON.stringify(fragment));
  }
});

Might be a nice addition to have a way to retrieve the generated manifest.

@brillout
Copy link
Member Author

@leonmondria I think your use case is quite different though, feel free to create a GitHub Discussion explaing your use case in further details.

@XiNiHa
Copy link
Contributor

XiNiHa commented Jan 25, 2023

Well actually @leonmondria 's usecase matches almost exactly with mine.

@brillout
Copy link
Member Author

@leonmondria @XiNiHa Would the following work for you?

// /renderer/_default.page.server.js

export { onBeforeRender }
export const passToClient = 'htmlFragments'

async function onBeforeRender(pageContext) {
  if (pageContext.isClientSideRouting) {
    return {
      pageContext: {
        htmlFragments: [/*...*/]
      }
    }
  }
}

The new feature here being pageContext.isClientSideRouting which is false for the very first request (the first page the user goes to, i.e. when the browser does its very first request: HTTP GET /some-page) and true for subsequent client-side routing (when the user navigates to a new page).

Note that VPS already returns JSON for subsequent client-side routing requests, so AFAICT there isn't any need to modify the usually renderPage() middleware integration.

Also note that onBeforeRender() is always called (first request + subsequent routing), whereas render() is called only for the first request (it's skipped for subsequent routing since there isn't any HTML to render). Btw., for added clarity, render() has been renamed to onRenderHtml() in the V1 design, example: /examples/react-full-v1/renderer/+onRenderHtml.tsx.

As for pageContext._pageAssets, it's getting fairly stable, so we can make it an official pageContext property. (For improved DX pageContext.pageAssets will be slightly different.)

Let me know if that doesn't work for you or if you have any questions.

@XiNiHa
Copy link
Contributor

XiNiHa commented Mar 13, 2023

Looks great for me!

@brillout
Copy link
Member Author

Released in 0.4.98. Note that it's called pageContext.isClientSideNavigation, see https://vite-plugin-ssr.com/pageContext.

@TomLadek

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
enhancement ✨ New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants