0

My angular application has universal set up and running, but server side rendering wasn't taking place. I got a suggestion and step by step tutorial on what to do, because apparently ssr wasn't working since I was using "window" and "localstorage". I had to replace them with the ones provided by @ng-toolkit/universal This is the service that I created

import { Injectable, Inject } from '@angular/core';
import { LOCAL_STORAGE } from '@ng-toolkit/universal';

@Injectable({
  providedIn: 'root'
})
export class LocalService implements Storage {
  constructor(@Inject(LOCAL_STORAGE) private localStorage: any) {}

  get length(): number {
    return this.localStorage.length;
  }

  clear(): void {
    this.localStorage.clear();
  }

  getItem(key: string): any {
    return this.localStorage.getItem(key);
  }

  key(index: number): string | null {
    return this.localStorage.key(index);
  }

  removeItem(key: string): void {
    this.localStorage.removeItem(key);
  }

  setItem(key: string, data: string): void {
    this.localStorage.setItem(key, data);
  }

  [key: string]: any;

  [index: number]: string;
}

After it got errors, I found that I needed to include it in app.module.ts as a provider like this

providers: [
    { provide: LOCAL_STORAGE, useFactory: () => window.localStorage }
]

Afterwards, in every component where I had localStorage, I imported the service and implemented it instead

import { LocalService } from '../services/local.service';

constructor(private localStorage: LocalService)

this.localStorage.setItem('lang', 'en');
this.localStorage.getItem('lang');

The application runs fine when using ng serve/npm start to run it client side, but when building the app with "npm run build:ssr" and serving it locally, I get the following error

Node Express server listening on http://localhost:4000
ERROR TypeError: Cannot read properties of undefined (reading 'getItem')

This is what my server.ts file looks like

import 'zone.js/dist/zone-node';

import * as express from 'express';
import { join } from 'path';

// Fix for non SSR modulеs
const domino = require('domino');
const fs = require('fs');
const path = require('path');
const compression = require('compression');
const template = fs.readFileSync(path.join('.', 'dist/browser', 'index.html')).toString();
const win = domino.createWindow(template);

// tslint:disable-next-line:no-string-literal
global['window'] = win;

// tslint:disable-next-line:no-string-literal
global['document'] = win.document;

// tslint:disable-next-line:no-string-literal
global['DOMTokenList'] = win.DOMTokenList;

// tslint:disable-next-line:no-string-literal
global['Node'] = win.Node;

// tslint:disable-next-line:no-string-literal
global['Text'] = win.Text;

// tslint:disable-next-line:no-string-literal
global['HTMLElement'] = win.HTMLElement;

// tslint:disable-next-line:no-string-literal
global['navigator'] = win.navigator;

// tslint:disable-next-line:no-string-literal
global['MutationObserver'] = getMockMutationObserver();

function getMockMutationObserver() {

  return class {

    observe(node, options) { }

    disconnect() { }

    takeRecords() {
      return [];
    }

  };

}
// End of Fix for non SSR modulеs

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist/browser');

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

// 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', DIST_FOLDER);

// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });
// Serve static files from /browser
app.get('*.*', express.static(DIST_FOLDER, {
  maxAge: '1y'
}));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render('index', { req });
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node Express server listening on http://localhost:${PORT}`);
});

I looked for an answer but couldn't find it anywhere. If you can answer or know the issues I would highly appreciate it.

2 Answers2

0

Upgrade "webpack-cli": "^2.1.4" to "webpack-cli": "3.1.1" in package.json file.

Pranam Bhat
  • 42
  • 10
  • Remember that Stack Overflow isn't just intended to solve the immediate problem, but also to help future readers find solutions to similar problems, which requires understanding the underlying code. This is especially important for members of our community who are beginners, and not familiar with the syntax. Given that, **can you [edit] your answer to include an explanation of what you're doing** and why you believe it is the best approach? – Jeremy Caney Jul 25 '22 at 00:14
0

Instead of having the provider in your AppModule, you're going to have to put the provider in the main.ts:

const providers: StaticProvider[] = [
  { provide: LOCAL_STORAGE, useFactory: () => window.localStorage }
];

document.addEventListener('DOMContentLoaded', () => {
  platformBrowserDynamic(providers).bootstrapModule(AppBrowserModule)
  .catch(err => console.error(err));
});

Then you'll also need to provide it during SSR (main.server.ts):

export default createServerRenderer(params => {
  const { AppServerModule, AppServerModuleNgFactory, LAZY_MODULE_MAP } = (module as any).exports;

  const providers: StaticProvider[] = [
    { provide: APP_BASE_HREF, useValue: params.baseUrl },
    { provide: LOCAL_STORAGE, useValue: null },
  ];

  const options = {
    document: params.data.originalHtml,
    url: params.url,
    extraProviders: providers
  };

  // Bypass ssr api call cert warnings in development
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

  const renderPromise = AppServerModuleNgFactory
    ? /* AoT */ renderModuleFactory(AppServerModuleNgFactory, options)
    : /* dev */ renderModule(AppServerModule, options);

  return renderPromise.then(html => ({ html }));
});

Don't forget to null-check the injectiontoken in your components. Anywhere you're injecting this LOCAL_STORAGE provider:

class xxx {
  constructor(@Inject(LOCAL_STORAGE) private localStorageService: LocalStorage | null) { }

  ...
}

and use it, you need to check if this field is not null

if (this.localStorageService) {
  // Here you can use the service safely
  ...
}
Pieterjan
  • 2,738
  • 4
  • 28
  • 55
  • Hey! Thank you for your reply, I am going to attempt that right now, only one question, what did you mean with null-check token? – networkdavit Jul 24 '22 at 12:12