0

I'm having a weird issue in Angular Universal where all routes work properly when they are being accessed with a full-page entry except one! Here is my current configuration:

App Routes:

const routes: Routes = [{
  path: '',
  pathMatch: 'full',
  redirectTo: '/home'
}, {
  path: 'admin',
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}, {
  path: 'home',
  loadChildren: () => import('./home/home.module').then(m => m.HomeModule)
}, {
  path: 'foo',
  loadChildren: () => import('./foo/foo.module').then(m => m.FooModule)
}, {
  path: '**',
  loadChildren: () => import('./not-found/not-found.module').then(m => m.NotFoundModule)
}];

server.ts:

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

import * as express from 'express';
import {join} from 'path';
import * as proxy from 'http-proxy-middleware';
// 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', (_, options: { req, res }, callback) => {
  const engine = ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [
      provideModuleMap(LAZY_MODULE_MAP),
      {provide: 'headers', useFactory: () => options.req.headers, deps: []}
    ]
  });

  engine(_, options, callback);
});

app.set('view engine', 'html');
app.set('views', DIST_FOLDER);

app.use(`/app/`, createRoutes());

function createRoutes() {
  const router = express.Router();
  router.get('*.*', express.static(DIST_FOLDER, {
    maxAge: '1y'
  }));

  router.get('*', (req, res) => {
    res.render('index', {req, res}, (error, html) => {
      return res.send(html);
    });
  });

  return router;
}

const apiProxy = proxy('/api', { target: 'http://localhost:8000'});
app.use('/api', apiProxy);

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

HomeRoutingModule:

import { HomeComponent } from './home.component';

const routes: Routes = [{ path: '', component: HomeComponent }];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class HomeRoutingModule { }

HomeComponent has two dependencies to some HTTP handling services and also to primeNg's MessageService.

So when I visit http://localhost:4200/app/query, everything works fine and I can then navigate through the app properly. But when I visit http://localhost:4200/app or http://localhost:4200/app/home nothing happens, I don't even get a response back and even the callback I tried adding to the index route rendering in express is not being called.

I did some digging in dist/server/main.js and narrowed down the issue to the following function:

function _render(platform, moduleRefPromise) {
    return moduleRefPromise.then((/**
     * @param {?} moduleRef
     * @return {?}
     */
    (moduleRef) => {
        /** @type {?} */
        const transitionId = moduleRef.injector.get(platform_browser["ɵTRANSITION_ID"], null);
        if (!transitionId) {
            throw new Error(`renderModule[Factory]() requires the use of BrowserModule.withServerTransition() to ensure
the server-rendered app can be properly bootstrapped into a client app.`);
        }
        /** @type {?} */
        const applicationRef = moduleRef.injector.get(core["ApplicationRef"]);
        return applicationRef.isStable.pipe((Object(first["a" /* first */])((/**
         * @param {?} isStable
         * @return {?}
         */
        (isStable) => isStable))))
            .toPromise()
            .then((/**
         * @return {?}
         */
        () => {
            /** @type {?} */
            const platformState = platform.injector.get(PlatformState);
            /** @type {?} */
            const asyncPromises = [];
            // Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
            /** @type {?} */
            const callbacks = moduleRef.injector.get(BEFORE_APP_SERIALIZED, null);
            if (callbacks) {
                for (const callback of callbacks) {
                    try {
                        /** @type {?} */
                        const callbackResult = callback();
                        if (Object(core["ɵisPromise"])(callbackResult)) {
                            asyncPromises.push(callbackResult);
                        }
                    }
                    catch (e) {
                        // Ignore exceptions.
                        console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
                    }
                }
            }
            /** @type {?} */
            const complete = (/**
             * @return {?}
             */
            () => {
                /** @type {?} */
                const output = platformState.renderToString();
                platform.destroy();
                return output;
            });
            if (asyncPromises.length === 0) {
                return complete();
            }
            return Promise
                .all(asyncPromises.map((/**
             * @param {?} asyncPromise
             * @return {?}
             */
            asyncPromise => {
                return asyncPromise.catch((/**
                 * @param {?} e
                 * @return {?}
                 */
                e => { console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e); }));
            })))
                .then(complete);
        }));
    }));
}

What happens here is that for /home route, I never get to enter the callback that starts with const platformState = platform.injector.get(PlatformState);. I'm not sure why this happens thought.

Anyone has any ideas and could help?


Update #1:

I managed to narrow down the problem to an RxJS operator I'm using in my ngOnInit of HomeComponent. For this component I'm doing some polling, so I have something like:

timer(0, 15000).pipe(
  mergeMap(() => this.dataService.fetchData())
).subscribe(...)

Trying to provide a minimum non-working component I removed everything from my component and added the following to ngOnInit:

timer(0, 20000).subscribe(
  (...args) => console.log(args), 
  (...args) => console.error(args)
);

Now, when I try to visit the route, it still doesn't load (as expected) but I see in the console of the server the values from the subscription.

George Karanikas
  • 1,244
  • 1
  • 12
  • 38

1 Answers1

2

You can trigger your polling once application is stable injecting ApplicationRef in the component, and subscribing to isStable

joaqcid
  • 133
  • 5