sveltejs / kit

web development, streamlined
https://kit.svelte.dev
MIT License
17.88k stars 1.8k forks source link

True SPA mode #754

Closed Rich-Harris closed 3 years ago

Rich-Harris commented 3 years ago

We now have the ssr: false option which gives us something close to SPA mode: https://kit.svelte.dev/docs#ssr-and-javascript

It doesn't get us all the way there though, because in a typical SPA you would likely want to generate a single fallback page that handles all requests — for example Surge lets you add a 200.html file, while Netlify allows you to add something like this to _redirects:

/*    /200.html   200

By contrast, SvelteKit expects to generate (whether at runtime or prerender time) an HTML page that includes no content, but does include information about the route that should be hydrated, since the router isn't invoked on load. To create a true SPA, SvelteKit needs to create a content-less file that doesn't contain any route information, and the router needs to figure out what JS to load and execute.

I'm not certain how best to do this in a provider-agnostic way.

Rich-Harris commented 3 years ago

One idea: since SPA mode basically implies 'static', this becomes an adapter-static feature:

const adapter = require('@sveltejs/adapter-static');

module.exports = {
  kit: {
    adapter: adapter({
      fallback: '200.html'
    },
    prerender: {
      enabled: false
    },
    ssr: false
  }
};

It would require a new API to be exposed to the prerenderer, but that's technically straightforward.

lukasIO commented 3 years ago

I'm all for this and was thinking about a way that would allow a true SPA mode to still update dynamically on the basis of CMS provided content.

If I could dream a perfect SPA mode for SvelteKit it would be static files that could optionally still hydrate with up-to-date content (and also dynamic routes) without having to create a different content-fetching technique outside of load.

edit: A route like blog.svelte would load the required JS to both render blog.svelte (static) and the JS to hydrate the page(dynamic). Maybe the load function could just be left to run on the client instead of restricting it to running only once during the static build?

Rich-Harris commented 3 years ago

load runs on hydration (or navigation) as well as SSR, it's not just using the serialized output of calling it on the server (as was the case with Sapper's preload). This allows you to have any sort of object as a prop (e.g. you could dynamically import a component inside load and use it with <svelte:component>).

The thing that makes the client-side invocation return the same data as the server-side one is that the results of calling fetch during SSR are serialized and inlined into the page. This ensures consistency when the page hydrates, and saves network round-trips (and also means less data needs to come over the wire, since everything can get compressed using the same gzip dictionary or whatever).

It sounds like you're asking for the ability to turn that inlining behaviour off, which would certainly be possible. We'd just need to add a new option to fetch — something like this:

export async function load({ fetch }) {
  const res = await fetch('/blah.json', { inline: false });
  // ...
}

That's something that would warrant a separate issue though.

lukasIO commented 3 years ago

load runs on hydration (or navigation) as well as SSR, it's not just using the serialized output of calling it on the server (as was the case with Sapper's preload).

thanks for clarifying that for me. I was under the impression that with adapter-static load would only run during build and not at all on the client. If load already runs on the client even in an "exported" adapter-static build, then everything I wanted to express is already possible...

Rich-Harris commented 3 years ago

It is, yeah — and I guess it makes sense to note that the { inline: false } idea above is really just a more formalized way of doing this...

import { browser } from '$app/env';

export async function load({ fetch }) {
  const res = await fetch(`/blah.json?inline=${!browser}`);
  // ...
}

...since there wouldn't be any inlined data corresponding to /blah.json?inline=false, which would force SvelteKit to go back to the network.

mzaini30 commented 3 years ago

I think, SPA didn't work after build with adapter-static. This is result after I tried:

index.svelte:

<h1>index.svelte</h1>
{JSON.stringify(localStorage)}

[slug].svelte:

<h1>[slug].svelte</h1>

svelte.config.cjs:

const node = require('@sveltejs/adapter-static');
kit: {
    // ...
    adapter: node({
        fallback: 'index.html',
    }),
    ssr: false,
    // ...
}

vercel.json:

{
  "routes": [
    { "handle": "filesystem" },
    { "src": "/(.*)", "status": 200, "dest": "/" }
  ]
}

Result for index.svelte: https://great-book.vercel.app/ Result for [slug].svelte: https://great-book.vercel.app/whitecoffee-cat-is-cute

The result are same:

index.svelte
{}

Repo: https://github.com/mzaini30/great-book

Rich-Harris commented 3 years ago

that's because fallback hasn't been implemented yet and you've disabled ssr?

mzaini30 commented 3 years ago

Is this fallback: https://github.com/mzaini30/great-book/blob/master/src/routes/%24error.svelte

@Rich-Harris

llui85 commented 3 years ago

Currently in Sapper, there are 2 ways (that I know of) for generating sites:

Unfortunately there is no "middle ground" between these two options as far as I am aware. If an app is created which requires authentication, the Node app option must be used, which is not always desirable.

This would be my definition of a "true" SPA mode for SvelteKit:

A way to generate a site, which:

  1. Builds to HTML, CSS and static JS files, which do not require or use SSR in any way, so they can be deployed to static hosting such as GitHub pages.
Sample directory structure:
index.html
js/33jhd.routing.js
js/93udw.post.route.js
js/61xae.category.route.js
css/73jds.layout.css
  1. The built files are evaluated "on the fly" and use live content, such as an API (and wouldn't require any extra code beyond what's required now to contact an external server) and are not prerendered.
  2. Sveltekit features such as client-side routing and prefetching would work as expected, and server configuration and/or a service worker would be used to send all requests to index.html
  3. onMount should no longer be needed as all code will be run on the client ONLY.

For example, in index.svelte (pseudo-code, not tested), I could expect to write the following code:


<script context="module">
    export async function preload({ params }) {
        const res =  await this.fetch("https://myblog.dev/api/posts/latest")
        const data = await res.json();
        if (res.status === 200) {
            return { latestPost: data };
        } else {
            this.error(res.status, data.message);
        }
    }
</script>
<script>

export let latestPost;

</script>

<h1>My latest post on the blog:</h1>
{#if latestPost}
    <h2>{latestPost.name}</h2>
    <p>{latestPost.tagline}</p>
    <a href={"/post/" + latestPost.id}>Read more</a>
{:else}
    <p>Loading latest posts...</p>
{/if}

... and then build it to a static page with svelte-kit build --adaptor=semistatic. This static page would not contain any content, but would update from the server on page load.

The main use case which I can think of for this for is building a client for a backend service which requires authentication, without the complexity of setting up node hosting and server-side rendering (since static hosting is arguably simpler).

These are my thoughts on what a "SPA mode" in SvelteKit should have.

lukasIO commented 3 years ago

@llui85 I think what you are describing is along the lines of what I wished for in my previous comment. Dynamic routes should be possible with adapter-static once the single fallback page from this issue has been implemented

dkzlv commented 3 years ago

I mostly agree with @llui85. I have the same need. If one could argue that authentication and SSR is a match made in heaven (although it introduces some difficulties and even limitations — not always wanted), I have an e2e encrypted web app on Sapper (and a WIP migration to Kit), so SSR is nothing but meaningless for me — it just serves static files.

I also propose to add pre-built error pages, like 404.html or 500.html. I think Kit can get those from prerendering $error.svelte root component with certain status and message. Not sure if they should be statically pre-rendered or shipped like separate SPA-apps though.

mzaini30 commented 3 years ago

My problem is same 😀 :: dynamic routing ::

Rich-Harris commented 3 years ago

@llui85 that was a very long way of asking for what SvelteKit already provides!

@dkzlv

I also propose to add pre-built error pages, like 404.html or 500.html

In an SPA there's no such thing as an error page. Every request (that doesn't match a specific prerendered HTML file) is handled by the same fallback page, usually called index.html or 200.html, because there's no way to know if there was an error until the client-side router has run.

Some providers will allow you to specify a fallback 404.html page if you're prerendering rather than using SPA logic, but that's mutually exclusive with 200.html. Both pages would have the exact same content since all the work (rendering a content page or an error page) happens in the client. In other words if you were prerendering and wanted a fallback 404 page you'd do it the exact same way:

const adapter = require('@sveltejs/adapter-static');

module.exports = {
  kit: {
    adapter: adapter({
-     fallback: '200.html'
+     fallback: '404.html'
    },
    prerender: {
      enabled: false
    },
    ssr: false
  }
};
jpaquim commented 3 years ago

Not exactly related to the current discussion, but I'm having an issue when trying to build a static SPA (migrating from a "pure" svelte app, based on the rollup template, and svelte-routing for routing): some of the imported JS modules are client-only, so assume the existence of window, document, etc.

Per the troubleshooting documentation (https://kit.svelte.dev/docs#troubleshooting-server-side-rendering), I should migrate all these top-level imports to dynamic import('')s, running either conditional on the browser flag, or on the onMount callback?

Unless I'm overlooking something, that will require a rather significant refactoring of all the browser-based library imports, but given I intend to use it for a static SPA, it seems like a hassle to migrate all the imports to dynamic imports just to allow it to run in the server-side prerender.

I guess what I'm looking for is a migration path from Svelte-based SPAs, rather than Sapper projects, when including libraries that rely on a global window or document.

Rich-Harris commented 3 years ago

The fundamental issue is that the component must be imported in order to determine whether it has an ssr export. That would continue to be true if we checked each page for ssr at build time and added the information to a manifest.

There's a couple of options that spring to mind - we find some other way of establishing whether a given page should be SSR'd, or we determine it with static analysis (which has the usual caveats but should be pretty robust in this case).

Having said that, if a module crashes when you merely import it, I would view that as a bug in the module, and raise an issue.

dummdidumm commented 3 years ago

If the whole app has ssr set to false, it doesn't need to get imported and analyzed though, right? Which would prevent the issue. Or is there still some traversal due to the route manifest?

benmccann commented 3 years ago

If the whole app has ssr set to false, it doesn't need to get imported and analyzed though, right? Which would prevent the issue. Or is there still some traversal due to the route manifest?

That was exactly my thinking as well. I suggested it in https://github.com/sveltejs/kit/issues/779#issuecomment-810439808 where someone reported the same issue

dkzlv commented 3 years ago

@Rich-Harris Thanks for the clarifications. I'm not familiar with Surge, Netlify and other providers like that, I'm used to nginx though. Since SPA would still have a bunch of files to serve this thing would work fine, I would only need to adjust the config to serve index.html from all paths and serve build/assets folder from /static path or a subdomain static..

@dummdidumm I've tried this and it doesn't really work — or I'm doing something wrong.

I tried this with "@sveltejs/adapter-node": "^1.0.0-next.10" and "@sveltejs/kit": "^1.0.0-next.64". I set ssr: false under kit, but I keep getting build-step errors from this library called nanoid, it's a slim random id generator. It has a runtime check, that crypto is defined on global. Node doesn't have it, so I get an infinite loop with this error:

11:17:09 AM [vite] Error when evaluating SSR module /node_modules/nanoid/index.dev.js
Error: Your browser does not have secure random generator. If you don’t need unpredictable IDs, you can use nanoid/non-secure.
    at /node_modules/nanoid/index.dev.js:28:11
    at instantiateModule (.../vite/dist/node/chunks/dep-0776dd57.js:68919:166) (x163)  <--- notice the counter, it goes up like crazy

Should note it never behaved like this in Sapper.

The only thing that worked for me was filtering all these deps that have import problems from noExternal attribute like this:

        /**
         * Currently we have problems with 3 deps:
         * 1. emoji-regex is not ESM compatible.
         * 2. two deps of svelte-i18n: fast-memoize, deepmerge. These are not ESM compatible either.
         * 3. nanoid. It keeps throwing errors, because SSR has no secure random generator, lol.
         */
        noExternal: Object.keys(pkg.dependencies || {}).filter(
          name => !['emoji-regex', 'svelte-i18n', 'nanoid'].includes(name),
        ),

Should I open a new issue and provide you with a repro?

UPDATE: I saw Rich's response in here, so nevermind.

Rich-Harris commented 3 years ago

@dummdidumm @benmccann individual pages could override the app-level setting

benmccann commented 3 years ago

Ah, thanks for explaining. I'm wondering if it makes much sense as a page-level setting. I'm assuming that we don't ship it in the manifest in order to be able to decide on each navigation whether the page is server or client rendered. If it takes effect only on the first page load of a site, it seems potentially confusing and I can't think of a use case for it. I imagine that being able to disable it across the board will end up being more requested, but maybe my imagination fails me.

The other option would be to make the setting more than just a boolean. E.g. we could have true/false/per-page or something like that or always/never/true-by-default/false-by-default or whatever makes sense

dummdidumm commented 3 years ago

These more granular options would at least make it possible to skip traversal to see if invidual pages override the app-level setting, which would prevent the "does not work in node environments" library errors.

frederikhors commented 3 years ago

This is what I have been looking for for years with Sapper. And it's a game-changing for the Svelte world!

Come on guys! YOU ARE REALLY GORGEOUS!

repomaa commented 3 years ago

Hey, just ran into this when i wondered where my index.html is after running npm run build with the static adapter. I didn't quite understand from reading this thread whether it currently is possible to build SPAs with only dynamically routed pages.

frederikhors commented 3 years ago

Hey, just ran into this when i wondered where my index.html is after running npm run build with the static adapter. I didn't quite understand from reading this thread whether it currently is possible to build SPAs with only dynamically routed pages.

Yes it's possible. I'm using it right now.

The index.html is in _app dir.

repomaa commented 3 years ago

The index.html is in _app dir.

Hm, for me it's not. Note that I only have one page which looks like this: [...path].svelte

frederikhors commented 3 years ago

Naaa. Please install again. Everything works right now with latest versions.

repomaa commented 3 years ago

@frederikhors nope, no html in build anywhere. You can try yourself: just create a new sveltekit app, rename src/routes/index.svelte to src/routes/[...foo].svelte, also rm -rf build, install @sveltejs/adapter-static, configure sveltekit to use it in svelte.config.cjs and then run npm run build.

repomaa commented 3 years ago

I would imagine this issue would be closed if this would already work.

frederikhors commented 3 years ago

Sorry I'm still using prerender option... I thought you meant that.

benmccann commented 3 years ago

Trying to make libraries work with Vite is a nonstop pain point for people especially with SSR. E.g. see https://github.com/sveltejs/kit/issues/905#issuecomment-816389084 and the corresponding Discord thread. It turns out they had to do a dynamic import inside an if (browser) inside onMount. It took them a long time to figure out and while we could document it, it's rather cumbersome

It seems like it will be impossibly hard to get users that want to run SPA apps to go to all the library authors that wrote client-side only libraries and tell them that they need to add server-side support because their web framework is imposing SSR upon them as they try to build a client-side app.

Anyway, I thought I'd share this user report because I've seen a lot of stuff like this on Discord. The solution is probably to change the way the setting works in one of the ways mentioned in https://github.com/sveltejs/kit/issues/754#issuecomment-811176398, but I'm not sure which is most preferable yet.

benmccann commented 3 years ago

Needing to import the templates to resolve the options is also biting a user in https://github.com/sveltejs/kit/issues/933#issuecomment-816820251 where they're trying to build a site with no-prerendered pages but the build process is trying to connect to the production database because we don't know that there are no prerendered pages until we import the site's modules and templates. We would either need to expose these options in another way or add an option to force disablement of prerendering along with adding an option to force disabling of SPA mode. Hopefully there aren't too many of these. It seems like something that could trip up users by default

urbainy commented 3 years ago

Yes, TRUE SPA mode is really what I want. I'm using Svelte in a Restful architecture with non-NodeJS back-end. I wish SvelteKit routing function can display an error page when absent path inputted by user without back-end involvement. Another method I can think is SvelteKit expose an error path so that back-end redirect to it. Hope this feature can be implemented soon~

plunkettscott commented 3 years ago

I know maintainers hate comments like, "I have this problem too", but I couldn't resist explaining why this is a very important use case for us.

We build self-contained apps using Go and when releasing versions of these apps we embed the files to be served from the binary's embedded file system. Lacking a true SPA mode is what's keeping us from exploring SvelteKit for these sort of deployment scenarios. When we embed the frontend app files, the client has to handle all routing because there is no SSR component.

As an alternative to having an SPA mode we've experimented with SSR using things like v8 bindings in Go but have found that the overhead isn't worth the gain for the purpose of the application, especially since SEO isn't a concern.

repomaa commented 3 years ago

@plunkettscott Exactly this! I stumbled upon this issue when i realized i couldn't embed a bundled sveltekit SPA into my crystal binary.

Rich-Harris commented 3 years ago

Implemented — docs here: https://github.com/sveltejs/kit/tree/master/packages/adapter-static

gevera commented 3 years ago

Implemented — docs here: https://github.com/sveltejs/kit/tree/master/packages/adapter-static

Thank you! This is was very needed indeed!

Smilefounder commented 3 years ago

@frederikhors nope, no html in build anywhere. You can try yourself: just create a new sveltekit app, rename src/routes/index.svelte to src/routes/[...foo].svelte, also rm -rf build, install @sveltejs/adapter-static, configure sveltekit to use it in svelte.config.cjs and then run npm run build.

@repomaa have you found a way to fix this? I'm facing the same issue with my config like below:

import preprocess from 'svelte-preprocess';
import adapter from '@sveltejs/adapter-static';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

/** @type {import('@sveltejs/kit').Config} */
const config = {
    // Consult https://github.com/sveltejs/svelte-preprocess
    // for more information about preprocessors
    preprocess: preprocess(),

    kit: {
        // hydrate the <div id="svelte"> element in src/app.html
        target: '#svelte',
        adapter: adapter({
            // default options are shown
            // pages: 'build',
            // assets: 'build',
            fallback: 'index.html'
        }),
        prerender: {
            enabled: false
        },
        ssr: false,
        vite: {
            // root: "./src",
            optimizeDeps: {
                include: ['clipboard-copy']
            },
            resolve: {
                alias: {
                    // $components: resolve(__dirname, "./src/components"),
                    // $stores: resolve(__dirname, "./src/stores"),
                    $mixlib: resolve(__dirname, "./src/mix.lib.ts"),
                },
            },
            // esbuild: {
            //  include: /\.(tsx?|jsx?)$/,
            //  exclude: [],
            //  loader: 'tsx'
            // }
        }
    }
};

export default config;
frederikhors commented 3 years ago

@Smilefounder what is the problem. True SPA is working today.

ivanjeremic commented 2 years ago

This is something I always wanted, Why have both Svelte and SvelteKit? Why not have just Svelte which can be both at the same time, just Svelte like it is barebones and if you want it to be an App Framework just use the App Framework features, this would make Svelte even more easier to use and userstand because users can go deeper and deeper as they need to. It is the same idea with the library mode which I like you have one framework which can be an app or an component library just apply the same to all around building apps with Svelte.

frederikhors commented 2 years ago

@ivanjeremic if you use SvelteKit and adapter-static you can have that. And it's wonderful!

laoshaw commented 1 year ago

@ivanjeremic if you use SvelteKit and adapter-static you can have that. And it's wonderful!

I do need supply a client-side route though? I hope sveltekit can have an option for SPA-mode at npm create time that will toggle SSR off totally and add a client-side route package, that way, users can choose either SSR or SPA from the start easily.

mattiash commented 1 year ago

@laoshaw the client side router is already included in svelte-kit. You can look at my sample in https://github.com/mattiash/svelte-kit-admin-ui It can be built and served by a static http server and access an http api to fetch data.

laoshaw commented 1 year ago

@mattiash Thanks. It seems you removed a few files from src/route, yes I need do this too, was asking sveltekit to provide an option to have this SPA-mode out-of-box. I'm glad to know client-side-router is built-in, still new to svelte, just came over from vue.

ollyde commented 8 months ago

I'm trying to deploy a SvelteKit app for capacitor, since I understood this would work under the hood.

Following the current config

I get errors such as Error: Unexpected option config.kit.ssr

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://kit.svelte.dev/docs/integrations#preprocessors
  // for more information about preprocessors
  preprocess: [
    vitePreprocess(),
    preprocess({
      postcss: true,
    }),
  ],

  kit: {
    // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
    // If your environment is not supported or you settled on a specific environment, switch out the adapter.
    // See https://kit.svelte.dev/docs/adapters for more information about adapters.
    adapter: adapter({
      pages: "build",
      assets: "build",
      fallback: "index.html",
    }),
    prerender: { entries: [] },
    prerender: {
      enabled: false,
    },
    ssr: false,
  },
};

export default config;

Has this been removed, if so, that would really suck, since we want one code base for all platforms 🙈

ollyde commented 8 months ago

My bad was using the wrong adapter, sorry

// import adapter from "@sveltejs/adapter-node";
import adapter from "@sveltejs/adapter-static";