2

I'm using indexeddb in an Angular 8 service and need window. The code builds without errors and the app creates the db objectstore flawlessly. But at runtime in production mode (with an actual node server instead of ng serve where this error does not occur), I get this error in the terminal running angular:

ERROR ReferenceError: window is not defined
    at IndexedDBService.isSupported (D:\MartijnFiles\Documents\Programming\Fenego\fenego-labs-angular\dist\server.js:71199:9)
    at IndexedDBService.openDB (D:\MartijnFiles\Documents\Programming\Fenego\fenego-labs-angular\dist\server.js:71203:18)
    at Promise (D:\MartijnFiles\Documents\Programming\Fenego\fenego-labs-angular\dist\server.js:72026:46)

Again, it all works and the isSupported() function would stop openDB() from being run if window was actually undefined. There is also no error in the browser console.

Here is the relevant part of my service.

@Injectable()
export class IndexedDBService {
  isSupported(): boolean {
    return !!window.indexedDB;
  }

  openDB(dbName: string,
         version: number,
         onUpgradeNeededCallback: OnUpgradeNeededCallback,
         onSuccessCallback: OnOpenSuccessCallback,
         onErrorCallback: OnOpenErrorCallback,
         onBlockedCallback: OnOpenBlockedCallback): Observable<IDBOpenDBRequest> {
    let openDBRequest: IDBOpenDBRequest = null;
    if (this.isSupported()) {
      openDBRequest = window.indexedDB.open(dbName, version);
      openDBRequest.onupgradeneeded = onUpgradeNeededCallback;
      openDBRequest.onsuccess = onSuccessCallback;
      openDBRequest.onerror = onErrorCallback;
      openDBRequest.onblocked = onBlockedCallback;
    }
    return of(openDBRequest);
  }

There are many suggest "solutions" out there that mostly boil down to providing it via a service or plain injection (eg. point 1 in this blog https://willtaylor.blog/angular-universal-gotchas/) but all it does is pass window from some other service via injection to mine. But my code works so it clearly has access to window...

Update:

The following line in a component's ngOnInit() has the same problem with Worker being "not defined" yet the worker is loaded and runs perfectly:

const offlineProductsWorker = new Worker('webworkers/offline-products-worker.js');

Update2:

I have found a solution (posted below) but checking for server side rendering seems more like a workaround than solving the fact that server side rendering is happening (not sure if that is supposed to be the case).

I will include my server.ts script that I use with webpack below. It is a modification of one from another project and I don't understand most of it. If anyone can point out to me what I could change to stop the server side rendering, that would be great. Or, if it is supposed to do that then why?

// tslint:disable:ish-ordered-imports no-console
import 'reflect-metadata';
import 'zone.js/dist/zone-node';
import { enableProdMode } from '@angular/core';
import * as express from 'express';
import { join } from 'path';
import * as https from 'https';
import * as fs from 'fs';

/*
 * Load config from .env file
 */
require('dotenv').config({ path: './ng-exp/.env' });
const IS_HTTPS = process.env.IS_HTTPS === 'true';
const SSL_PATH = process.env.SSL_PATH;
const ENVIRONMENT = process.env.ENVIRONMENT;

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

const logging = !!process.env.LOGGING;

// Express server
const app = express();

const PORT = process.env.PORT || 4200;
const DIST_FOLDER = process.cwd();

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');

// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine(
  'html',
  ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [provideModuleMap(LAZY_MODULE_MAP)],
  })
);

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'ng-exp'));

// Server static files from /browser
app.get(
  '*.*',
  express.static(join(DIST_FOLDER, 'ng-exp'), {
    setHeaders: (res, path) => {
      if (/\.[0-9a-f]{20,}\./.test(path)) {
        // file was output-hashed -> 1y
        res.set('Cache-Control', 'public, max-age=31557600');
      } else {
        // file should be re-checked more frequently -> 5m
        res.set('Cache-Control', 'public, max-age=300');
      }
    },
  })
);

// ALl regular routes use the Universal engine
app.get('*', (req: express.Request, res: express.Response) => {
  if (logging) {
    console.log(`GET ${req.url}`);
  }
  res.render(
    'index',
    {
      req,
      res,
    },
    (err: Error, html: string) => {
      res.status(html ? res.statusCode : 500).send(html || err.message);
      if (logging) {
        console.log(`RES ${res.statusCode} ${req.url}`);
        if (err) {
          console.log(err);
        }
      }
    }
  );
});

const sslOptions = {
  key: fs.readFileSync(`${SSL_PATH}/${ENVIRONMENT}/server.key`),
  cert: fs.readFileSync(`${SSL_PATH}/${ENVIRONMENT}/server.crt`),
};

// Start up the Node server
let server;
if (IS_HTTPS) {
  server = https.createServer(sslOptions, app);
} else {
  server = app;
}
server.listen(PORT, () => {
  console.log(`Node Express server listening on http${IS_HTTPS ? 's' : ''}://localhost:${PORT}`);
  const icmBaseUrl = process.env.ICM_BASE_URL;
  if (icmBaseUrl) {
    console.log('ICM_BASE_URL is', icmBaseUrl);
  }
});

Martijn Van Loocke
  • 433
  • 2
  • 6
  • 17
  • you are doing server side rendering right? You'r actually falling in the `isSupported()` method according to the stacktrace. So I don't think this is working. – ChrisY Nov 23 '19 at 14:47
  • And yet it is working. I can see the object store in the chrome developer's console and I clear the full indexedDB each time I test. As for server side rendering, I'm not fully certain but I think for a website, the Angular code is served to the browser and the browser does all the work. – Martijn Van Loocke Nov 23 '19 at 15:02
  • I think you should also check if it is running on node environment. If it is it should return false. See [this](https://stackoverflow.com/questions/4224606/how-to-check-whether-a-script-is-running-under-node-js) if it helps. – Eldar Nov 23 '19 at 15:24
  • 1
    The `server.js` indicates that you are doing a server side render here. So these errors may come when the server is trying to render the view, cuz there is no browser and no window object on the server. Then the so called `hydrating` kicks in and the angular code is executed on the client side and then everything works. You may want to add some `if (isPlatformBrowser())` checks to avoid that this code is run on the server side, or disable server side rendering at all. – ChrisY Nov 23 '19 at 15:35

2 Answers2

1

There is a related issue here: https://github.com/hellosign/hellosign-embedded/issues/107 Basically, to avoid the error you can declare somewhere globally the window.

if (typeof window === 'undefined') {
    global.window = {}
}

I also found React JS Server side issue - window not found which explains the issue better and why it works on the client side.

Athanasios Kataras
  • 25,191
  • 4
  • 32
  • 61
1

I found the solution thanks to some input from ChrisY

I deploy my code using webpack and run it using node. It seems that node somehow renders it server side and then the browser renders it too. The server site portion has no effect on the storefront but does cause the (seemingly harmless) error. In isSupported() I added console.log(isPlatformBrowser(this.platformId))and it printed false in the server terminal but true in the browser. Thus, I changed the code as follows:

constructor(@Inject(PLATFORM_ID) private platformId: any) {}

isSupported(): boolean {
  return isPlatformBrowser(this.platformId) && !!indexedDB;
}

Now it still works in the browser as it did before but there is no server error.

Update:

I have also found the cause for the server side rendering. The server.ts file in the description has a block with res.render(. This first renders the page on the server and if it does not receive html, it returns status code 500. Otherwise it allows the client to render it. Seeing as this is a realistic scenario, I have decided to keep the extra isPlatformBrowser(this.platformId) check in my code. This should then be done for anything that can only be performed by the client (window, dom, workers, etc.).

Not not have server side rendering, an alternative to the res.render( block is

res.status(200).sendFile(`/`, {root: join(DIST_FOLDER, 'ng-exp')});
Martijn Van Loocke
  • 433
  • 2
  • 6
  • 17