codediodeio / sveltefire

Cybernetically enhanced Firebase apps
MIT License
1.68k stars 131 forks source link

Server-Side Rendering and Preloading with Sapper #4

Open codediodeio opened 5 years ago

codediodeio commented 5 years ago

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 https://github.com/firebase/firebase-js-sdk/issues/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.

mattpilott commented 4 years ago

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

pjarnfelt commented 4 years ago

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 commented 4 years ago

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

evdama commented 4 years ago

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

evdama commented 4 years ago

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()?

pjarnfelt commented 4 years ago

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 commented 4 years ago

@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

evdama commented 4 years ago

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

evdama commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

I got some errors while using @pjarnfelt answer

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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

Evertt commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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

Hello @vc-ca, The code in https://github.com/codediodeio/sveltefire/issues/4#issuecomment-662580999 not suits you ?

makeitTim commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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