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

Server-Side Rendering and Preloading with Sapper #4

Open
codediodeio opened this issue Nov 12, 2019 · 29 comments
Open

Server-Side Rendering and Preloading with Sapper #4

codediodeio opened this issue Nov 12, 2019 · 29 comments
Labels
help wanted Extra attention is needed

Comments

@codediodeio
Copy link
Owner

codediodeio commented Nov 12, 2019

Opening an issue to determine the best way to work with Firebase in Sapper. Currently, bundling with rollup leads to issues that seem related to firebase/firebase-js-sdk#1797

One possible solution is to make Firebase global with script tags.

template.html

<head>
	<script src="https://www.gstatic.com/firebasejs/7.3.0/firebase-app.js"></script>
	<script src="https://www.gstatic.com/firebasejs/7.3.0/firebase-analytics.js"></script>
	<script src="https://www.gstatic.com/firebasejs/7.3.0/firebase-performance.js"></script>
	<script src="https://www.gstatic.com/firebasejs/7.3.0/firebase-auth.js"></script>
	<script src="https://www.gstatic.com/firebasejs/7.3.0/firebase-firestore.js"></script>
</head>

_layout.svelte

<script>

	const firebaseConfig = { ... };

	import { onMount } from 'svelte';
	import { FirebaseApp, Collection } from 'sveltefire';

	let globalFirebase;

	onMount(() => {
		globalFirebase = firebase;
		if (!firebase.apps.length) {
			firebase.initializeApp(firebaseConfig);
		}
	});
</script>

{#if globalFirebase}
  <FirebaseApp firebase={globalFirebase}>

	<Doc path={'hello/world'} let:data>
             {data}
	</Doc>

  </FirebaseApp>
{/if}

This works, but does not address how to preload data from Firebase.

@codediodeio codediodeio added documentation Improvements or additions to documentation help wanted Extra attention is needed and removed documentation Improvements or additions to documentation labels Nov 12, 2019
@mattpilott
Copy link

I'm also interested in this, i am currently leaking apki keys to the client, which is not ideal

@pjarnfelt
Copy link

pjarnfelt commented Jan 3, 2020

I do something similar and I don't know what is best:

any.svelte

<script>
    let client;
    onMount(async () => {
        if(process.browser){
            client = await import("../firebase/firebase.js");
            // loading firebase stuff here with client.db or client.auth 
            // which are exported in the firebase.js
        }
    });
</script>

the initialization I do in the

client.js

import * as sapper from '@sapper/app';
import firebase from 'firebase/app';
const firebaseConfig = { ... };

firebase.initializeApp(firebaseConfig);

sapper.start({
	target: document.querySelector('#sapper')
});

@evdama
Copy link

evdama commented Jan 14, 2020

I'm in the same boat here folks... having done a sapper project with tailwind which I find terrific and now I want to add the backend portion which will be firebase... I see the exact same issues with SSR that are linked above, I resorted to specifying mainFields in my rollup conf but as long as the differrent firebase packages like firestore, app or analytics are not unified, that approach just doesn't work for all the firebase packages I need (app, auth, firestore, performance, analytics, storage).

What I ended up doing for now is but this in in my sapper template.html

    <script defer src="https://www.gstatic.com/firebasejs/7.6.2/firebase-app.js"></script>
    <script defer src="https://www.gstatic.com/firebasejs/7.6.2/firebase-auth.js"></script>
    <script defer src="https://www.gstatic.com/firebasejs/7.6.2/firebase-firestore.js"></script>
    <script defer src="https://www.gstatic.com/firebasejs/7.6.2/firebase-storage.js"></script>
    <script defer src="https://www.gstatic.com/firebasejs/7.6.2/firebase-messaging.js"></script>
    <script defer src="https://www.gstatic.com/firebasejs/7.6.2/firebase-performance.js"></script>
    <script defer src="https://www.gstatic.com/firebasejs/7.6.2/firebase-analytics.js"></script>
    <script defer src="./firebase-init.js"></script>

and then have in my static/firebase-init.js

let config = {
  apiKey: "zzzzz",
  appId: "1:1xxxxxxxxx",
  authDomain: "xxxxxxxxx",
  databaseURL: "xxxxxx",
  measurementId: "G-"xxxxx,
  messagingSenderId: "xxxxxxxxxxx",
  projectId: "xxxxxxxxxxx",
  storageBucket: ""
}


firebase.initializeApp( config )
firebase.performance()
firebase.analytics()

while this works, it gives me issues related to SSR such as firebase not defined for the first paint, then when the page gets hydrated, it works because the firebase gets pulled in from the CDN. But for that intermediate second or so, until the client is fully hydrated, I see the SSR version of my Sapper page that of course shows a 500 error because there's no firebase on the server that node could render.

I really hope there's going to be a proper solution for all folks that want to use firebase with some SSR app like Sapper (Svelte toolkit for SSR), React SSR, or Angular SSR... at the moment, all I've seen are just

  • workarounds in the area of bundling with webpack and rollup. Either that, or
  • people resort to framework specific code that only loads and renders firebase packages in the client.

@evdama
Copy link

evdama commented Jan 16, 2020

The issues around SSR builds and esm vs cjs seem to bite quite a few people now as this new issue elaborates too firebase/firebase-js-sdk#1612

@evdama
Copy link

evdama commented Jan 16, 2020

I'm also interested in this, i am currently leaking apki keys to the client, which is not ideal

by API keys do you mean the public config information that you put in firebase.initializeApp()?

  • That information is just there in order to identify you project on firebase, there's no actual secrets included in that snippet
  • the Admin SDK config is different, there you'd have secret information but information is never send down to any client, only way that could leak out is when you include that config file in your github repo and share it.

@pjarnfelt
Copy link

pjarnfelt commented Jan 16, 2020

I've gotten a version of ssr and preloading up and running following this video:
Sveltecasts - Sapper & Firebase Firestore

Essentially there needs to be 2 versions of Firestore loaded. He loads clientside firebase stuff like Jeff in the scripts and then has a conditional definition of Firestore
This is my version in firestore.js

export async function firestore() {
    if (process.browser) {
        return window.db
    } else {
        const firebase = await import('firebase')
        const config = await import('./config');
        if (firebase.apps.length == 0) {
            let app = firebase.initializeApp(config.default)
            return app.firestore()
        }
        else {
            return firebase.apps[0].firestore()
        }
    }
}

on the page I need preloaded data:

<script context="module">
// load the above file
  import { firestore } from "../../firebase/firestore";
  export async function preload({ params, query }) {
    let db = await firestore();
    const data = await db
      .collection("collection")
      .doc(params.slug)
      .get();
    if (data.exists) {
      return { document: data.data() };
    } else {
      this.error("no data");
    }
  }
</script>
<script>
// this document is returned from the preload and ready for usage in html
export let document;
</script>

if the client.js

import * as sapper from '@sapper/app';

import firebase from 'firebase/app';
import 'firebase/firestore';
import {default as config} from './firebase/config'

let app = firebase.initializeApp(config)
window.db = app.firestore()

sapper.start({
	target: document.querySelector('#sapper')
});

edit:
I forgot to mention that I also updated the rollup.config.js file with names exports as mentioned here.

//--- rollup.config.js ---
commonjs({
        namedExports: {
          // left-hand side can be an absolute path, a path
          // relative to the current directory, or the name
          // of a module in node_modules
          'node_modules/idb/build/idb.js': ['openDb'],
          'node_modules/firebase/dist/index.cjs.js': ['initializeApp', 'firestore'],
        },
      }),

@evdama
Copy link

evdama commented Jan 17, 2020

@pjarnfelt that seems to be a good approach for now, I think it makes the initial app instance and firestore work nicely. Two more things I'd like to ask you

✗ client
'deleteDb' is not exported by node_modules/idb/build/idb.js
4: import { __values, __spread, __awaiter, __generator, __assign } from 'tslib';
5: import { ErrorFactory } from '@firebase/util';
6: import { deleteDb, openDb } from 'idb';
            ^

@evdama
Copy link

evdama commented Jan 18, 2020

I fixed the deleteDb issue from above by importing from firebase/app rather than just firebase.

@evdama
Copy link

evdama commented Jan 18, 2020

so there's more info on this funthing and it seems others hit this roadblock before
https://stackoverflow.com/questions/56315901/how-to-import-firebase-only-on-client-in-sapper

@pjarnfelt
Copy link

@evdama Yes, I did the named exports from your link. Forgot to mention that. Will edit my comment.

yes I'm using performance and analytics, not storage though, but I suspect that would work too. I added them to the client.js where I instantiate the app.

Now I'm just having trouble hosting it on firebase functions/hosting

@evdama
Copy link

evdama commented Jan 19, 2020

Now I'm just having trouble hosting it on firebase functions/hosting

@pjarnfelt there you go https://dev.to/eckhardtd/how-to-host-a-sapper-js-ssr-app-on-firebase-hmb :)

@pjarnfelt
Copy link

@evdama thanks for the link. I'm already doing that (hosting ssr through functions), but the troubles I'm having is that my performance is terrible. Now I got preloading and ssr on my firestore content and ssr-auth handling to avoid the front-end loading delay of the authentication (also based on a sveltecasts video). Now my first load on normal internet speed is around 10 sec and interaction around 18, which defies the whole purpose of Svelte/Sapper.
I need to debug more to find the issues.

@evdama
Copy link

evdama commented Jan 19, 2020

@pjarnfelt Interessting, my site is much quicker to first inital paint, about 0.6 seconds or so... Either way, you must be doing something odd otherwise that can't be explained. Try with lighthouse and see what it tells you to improve.

@SwiftWinds
Copy link

I've gotten a version of ssr and preloading up and running following this video:
Sveltecasts - Sapper & Firebase Firestore

Essentially there needs to be 2 versions of Firestore loaded. He loads clientside firebase stuff like Jeff in the scripts and then has a conditional definition of Firestore
This is my version in firestore.js

export async function firestore() {
    if (process.browser) {
        return window.db
    } else {
        const firebase = await import('firebase')
        const config = await import('./config');
        if (firebase.apps.length == 0) {
            let app = firebase.initializeApp(config.default)
            return app.firestore()
        }
        else {
            return firebase.apps[0].firestore()
        }
    }
}

on the page I need preloaded data:

<script context="module">
// load the above file
  import { firestore } from "../../firebase/firestore";
  export async function preload({ params, query }) {
    let db = await firestore();
    const data = await db
      .collection("collection")
      .doc(params.slug)
      .get();
    if (data.exists) {
      return { document: data.data() };
    } else {
      this.error("no data");
    }
  }
</script>
<script>
// this document is returned from the preload and ready for usage in html
export let document;
</script>

if the client.js

import * as sapper from '@sapper/app';

import firebase from 'firebase/app';
import 'firebase/firestore';
import {default as config} from './firebase/config'

let app = firebase.initializeApp(config)
window.db = app.firestore()

sapper.start({
	target: document.querySelector('#sapper')
});

edit:
I forgot to mention that I also updated the rollup.config.js file with names exports as mentioned here.

//--- rollup.config.js ---
commonjs({
        namedExports: {
          // left-hand side can be an absolute path, a path
          // relative to the current directory, or the name
          // of a module in node_modules
          'node_modules/idb/build/idb.js': ['openDb'],
          'node_modules/firebase/dist/index.cjs.js': ['initializeApp', 'firestore'],
        },
      }),

This seems to work wonders, but I can't seem to get this to work with SvelteFire; it complains that I don't have firebase/firestore imported, which I am not sure how to do because of the whole client-server incompatibility.

Anyone got any suggestions to get both this method of SSR and SvelteFire to work?

@chxru
Copy link

chxru commented May 22, 2020

I got some errors while using @pjarnfelt answer

  • svelte@3.22.2
  • firebase@7.14.4
  • node@12.16.3

firebase.js

export async function firestore() {
  if (process.browser) return window.db
  const firebase = await import('firebase/app')
  if (firebase.apps.length == 0) {
    let app = firebase.initializeApp(firebaseConfig)
    return app.firestore()
  } else {
    return firebase.apps[0].firestore()
  }
}

Client side firestore works but SSR seems not working

First issue I had was Cannot read property 'length' of undefined at if (firebase.apps.length == 0) { . So I updated if condition to !firebase.apps || firebase.apps.length == 0. Then there's a new error TypeError: firebase.initializeApp is not a function. I've no clue why is that happening :/

@sebmade
Copy link

sebmade commented Jul 22, 2020

I finally use this way which work fine :
create a firebase.js file in src/

import firebase from "firebase/app";
import 'firebase/firestore';
import 'firebase/auth';
import 'firebase/performance';
import 'firebase/analytics';

const firebaseConfig = {...}

export function initFirebase() {
  firebase.initializeApp(firebaseConfig);	
  return firebase;
}

add this in client.js

import { initFirebase } from './firebase.js';
window.firebase = initFirebase();

and then you can access firebase from window in onMount functions
no need to change rollup, just be careful to put firebase libs in packages.json devDependencies (with --save-dev)

for the firestore aspect, because I use SSR and have SEO constraints, I only use this.fetch and make firebase call with firebase-admin and put it in packages.json dependencies to not rollup them (all dependencies are interpreted as external in the rollup.config file)

hope this helps

@alfrednerstu
Copy link

@sebmade Your solution looks nice! I'm having trouble getting it to work though. Do you mind sharing an example of how you use onMount and this.fetch? Thanks!

@sebmade
Copy link

sebmade commented Aug 23, 2020

@alfrednerstu if you want to use onMount() just call window.firebase
onMount(() => { window.firebase.... })
if you want to use this.fetch in the preload function, you have to create a function on server side and use firebase-admin lib.
it don't make sense to use window.firebase with this.fetch in my opinion, this.fetch is used to make a server call wherever you are on client side or on server side during the ssr processing.
but if you really want you can by testing the env var process.browser in preload and calling window.firebase when it's true and this.fetch when it's false
if it's not working, send me error you have so I could help

@alfrednerstu
Copy link

@sebmade So you do everything twice? Both in onMount and preload? I would prefer to do everything once in preload. I have got client side working but server side is still complaining that IDBIndex is not defined ie the client side library of Firebase is loaded instead of the server side one.

@sebmade
Copy link

sebmade commented Sep 4, 2020

@alfrednerstu no, because you can't compile ssr with firebase libs, so it depends of my needs, for authentication and when user is connected I use client side firebase call, when not I sue server side firebase call

IDBIndex error is due to compilation of firebase lib, be sure to have firebase lib in devDependencies in your package.json and don't make firebase call in directly in preload function, just on the server side files.

@vc-ca
Copy link

vc-ca commented Sep 4, 2020

@sebmade would you be willing to provide some example code similar to @pjarnfelt post from Jan 16? Thanks

@Evertt
Copy link

Evertt commented Sep 18, 2020

Hey guys, I did not read the entire thread, but I just wanted to add my 2 cents. In my personal Sapper project I've written this file. https://github.com/Evertt/sapper-cms/blob/main/src/store/firebase.ts

And it makes it possible to write import { fbApp } from "./store" anywhere in your Sapper app and it will give you the correct instance of firebase every time. And it doesn't use any asynchronous loading which is nice because then you don't have to execute an async function first.

So this would work perfectly if sveltefire would just try not to use any database APIs that are different between the web version of firebase and firebase-admin.

@vhollo
Copy link

vhollo commented Sep 20, 2020

Hey Evertt,
would you mind to provide a short gist for us, non-dev savvies, how to go on with Sapper and Firestore, please?

@sebmade
Copy link

sebmade commented Sep 20, 2020

@sebmade would you be willing to provide some example code similar to @pjarnfelt post from Jan 16? Thanks

Hello @vc-ca,
The code in #4 (comment) not suits you ?

@makeitTim
Copy link

makeitTim commented Oct 12, 2020

Getting Sapper working with Firestore would be awesome. I've gotten Sapper SSR deployed to Firebase Cloud functions, which is really cool, but sapper not supporting firebase is very disappointing!

I moved Firestore to devDependencies, but I'm still getting this error:

@firebase/app: 
      Warning: This is a browser-targeted Firebase bundle but it appears it is being
      run in a Node environment.  If running in a Node environment, make sure you
      are using the bundle specified by the "main" field in package.json.
      
      If you are using Webpack, you can specify "main" as the first item in
      "resolve.mainFields":
      https://webpack.js.org/configuration/resolve/#resolvemainfields
      
      If using Rollup, use the rollup-plugin-node-resolve plugin and specify "main"
      as the first item in "mainFields", e.g. ['main', 'module'].
      https://github.com/rollup/rollup-plugin-node-resolve

I tried adding "mainFields": ['main', 'module'] but it didn't change anything, and I don't understand this.

I also don't get why I'd want firebase used from the server code at all? Isn't it correct that firebase should only run client side? Or am I misunderstanding something fundamental here?

Either way, what's the reasonable solution to using firebase with a Sapper/SSR web app? Is it really one of these methods of side-loading the dependency or sourcing it in template.html !? If so, what's the right way to do it?

@sebmade
Copy link

sebmade commented Oct 12, 2020

@makeitTim Firebase works with Sapper, I've already deploy a complete application using firestore, storage, stripe ...
The problem is during the compilation phase. I don't know why but referencing firebase during the SSR process failed.
So there is 2 ways to make it works : use firebase only on server side (in server.js or in [file].json.js) or use firebase only in functions (onMount or event functions).

@makeitTim
Copy link

makeitTim commented Oct 12, 2020

@sebmade Very cool! Are you using Firebase Hosting and Functions to host SSR?

I still don't completely understand those choices ... why would I want to use firebase Firestore db on the sapper/ssr server side at all? (And isn't the firebase library specifically preventing it?)

I think my use case is the typical and obvious one --- With a Sapper app I want to use Firestore as the backend and Firebase Auth for users to login and access their data. The app is comprised of detail screens of Firestore data and forms for updating the data.

How do I do that?

Is this library, sveltefire useful? It looks cool, but this whole lack of setup is worrying and makes me feel like I'd be more comfortable working directly with firebase app.db.

Thanks!

@Evertt
Copy link

Evertt commented Oct 12, 2020

@makeitTim I have a working project using firestore both client-side and server-side (as in during SSR). Not using sveltefire though, but using my own firestore wrapper. My project is still very buggy, but it proves the concept at least.

I'm a bit too lazy to explain though, so I'll just show you the code and the result.

This code:
https://github.com/Evertt/sapper-cms/tree/real-world-app

Produces this website:
https://mytryout-246d2.web.app/

edit

btw don't think about the fact that the repo is called sapper-cms. That was the goal when I started it, but I got distracted. It's not a cms.

@sebmade
Copy link

sebmade commented Oct 12, 2020

@makeitTim yes I use firebase hosting, I don't try to use @google-cloud/firestore api independently
I don't use sveltefire finally

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests