Angular 使用 Route Resolver 时,Universal 不会在查看源代码中加载我的组件

Angular Universal doesn't load my component in view-source when using Route Resolver

我正在使用 SSR 应用程序以及 Contentful CMS 和 Route Resolver 在加载组件之前获取数据。当我构建和提供应用程序时没有错误,我可以在客户端看到内容,但是当我查看视图源时,除了带有路由解析器的初始组件之外的所有内容都被渲染了。当我删除解析器并在组件内放置一些静态元素时,我会在视图源中看到它。

我已经实现了绝对 url 的 http 拦截器并正确配置了我的 server.ts,但仍然找不到它没有被渲染的原因。

路线:

import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
import { PageComponent } from './page/page.component';
import { PageResolver } from './page/page.resolver.service';


const routes: Routes = [
  { path: ':slug', component: PageComponent, resolve: { page: PageResolver } },
  { path: '', component: PageComponent, resolve: { page: PageResolver } },
  { path: '**', component: PageComponent, resolve: { page: PageResolver } }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      preloadingStrategy: PreloadAllModules,
      scrollPositionRestoration: 'top',
      enableTracing: false,
      anchorScrolling: 'enabled'
    })
  ],
  exports: [RouterModule],
  providers: [PageResolver]
})
export class AppRoutingModule { }

解析器:

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { ContentfulService} from '../core/contentful.service';
import { PageModel } from '../models/page.model';

@Injectable()
export class PageResolver implements Resolve<PageModel> {

  constructor(private contentService: ContentfulService) { }

  resolve(route: ActivatedRouteSnapshot): Promise<PageModel> {
    const slug = this.getSlug(route);
    const page = this.contentService.getContentBySlug<PageModel>(slug, 'page', 10);
    return page;
  }

  private getSlug(route: ActivatedRouteSnapshot): string {
    const routeLength = route.url.length;
    if (routeLength === 0) {
      return 'home';
    }

    if (route.data.slug === 'error') {
      return route.data.slug;
    }

    return route.url.map((urlFragment) => urlFragment.path).join('/');
  }
}

page.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';

import { ContentfulService } from '../core/contentful.service';
import { PageModel } from '../models/cms/page.model';
import { ActivatedRoute } from '@angular/router';
import { untilDestroyed } from 'ngx-take-until-destroy';

@Component({
  selector: 'app-page',
  templateUrl: './page.component.html',
  styleUrls: ['./page.component.scss']
})
export class PageComponent implements OnInit, OnDestroy {
  page: PageModel;

  constructor(private route: ActivatedRoute,
              public contentful: ContentfulService) { }

  ngOnInit() {
    console.log('HIT 1');
    this.route.data.pipe(untilDestroyed(this)).subscribe(({ page }) => {
      this.page = page;
    });
  }

  ngOnDestroy(): void {
  }
}

server.ts

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

require('source-map-support').install();

import express from 'express';
import compression from 'compression';
import {join} from 'path';
import domino from 'domino';
import fs from 'fs';
import path from 'path';

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

const template = fs.readFileSync(path.join(process.cwd(), 'dist/my-site', 'index.html')).toString();
console.log(template);
const win = domino.createWindow(template);
global['window'] = win;
// not implemented property and functions
Object.defineProperty(win.document.body.style, 'transform', {
  value: () => {
    return {
      enumerable: true,
      configurable: true,
    };
  },
});
global['document'] = win.document;
// othres mock
global['CSS'] = null;

import {enableProdMode} from '@angular/core';

import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';

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

// Express server
const app = express();
app.use(compression());

// redirects!
const redirectowww = false;
const redirectohttps = false;
const wwwredirecto = true;
app.use((req, res, next) => {
  // for domain/index.html
  if (req.url === '/index.html') {
    res.redirect(301, 'https://' + req.hostname);
  }

  // check if it is a secure (https) request
  // if not redirect to the equivalent https url
  if (
    redirectohttps &&
    req.headers['x-forwarded-proto'] !== 'https' &&
    req.hostname !== 'localhost'
  ) {
    // special for robots.txt
    if (req.url === '/robots.txt') {
      next();
      return;
    }
    res.redirect(301, 'https://' + req.hostname + req.url);
  }

  // www or not
  if (redirectowww && !req.hostname.startsWith('www.')) {
    res.redirect(301, 'https://www.' + req.hostname + req.url);
  }

  // www or not
  if (wwwredirecto && req.hostname.startsWith('www.')) {
    const host = req.hostname.slice(4, req.hostname.length);
    res.redirect(301, 'https://' + host + req.url);
  }

  next();
});

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

// * 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
// dynamic render
app.get('*', (req, res) => {
  // mock navigator from req.
  global['navigator'] = req['headers']['user-agent'];
  const http =
    req.headers['x-forwarded-proto'] === undefined ? 'http' : req.headers['x-forwarded-proto'];

  const url = req.originalUrl;
  // tslint:disable-next-line:no-console
  console.time(`GET: ${url}`);
  res.render(
    '../my-site/index',
    {
      req: req,
      res: res,
      // provers from server
      providers: [
        // for http and cookies
        {
          provide: REQUEST,
          useValue: req,
        },
        {
          provide: RESPONSE,
          useValue: res,
        },
        // for absolute path
        {
          provide: 'ORIGIN_URL',
          useValue: `${http}://${req.headers.host}`,
        },
      ],
    },
    (err, html) => {
      if (!!err) {
        throw err;
      }

      // tslint:disable-next-line:no-console
      console.timeEnd(`GET: ${url}`);
      res.send(html);
    },
  );
});

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

感谢任何帮助或建议。

编辑:

contentful.service.ts:

import { Injectable } from '@angular/core';
import { createClient, ContentfulClientApi, EntryCollection, Entry, Asset } from 'contentful';
import { AppConfig } from './config/app-config.service';

@Injectable()
export class ContentfulService {

  private readonly contentfulClient: ContentfulClientApi;

  constructor() {
    this.contentfulClient = createClient({
      host: AppConfig.settings.contentfulHost,
      space: AppConfig.settings.contentfulSpace,
      accessToken: AppConfig.settings.contentfulAccessToken
    });
  }

  public async getContent<T>(contentId: string, include: number = 1, localeCode = AppConfig.settings.locale): Promise<T> {
    return this.contentfulClient.getEntries({ 'sys.id': contentId, include, locale: localeCode })
      .then(res => {
        return this.parseModel(res.items[0], res) as T;
      });
  }

  public async getContentBySlug<T>(slug: string, contentType: string,
                                   include: number = 1, localeCode = AppConfig.settings.locale): Promise<T> {
    console.log('CONTENTFUL STARTS.');
    return this.contentfulClient.getEntries({ content_type: contentType, 'fields.slug': slug, include, locale: localeCode })
      .then(res => {
        console.log('CONTENTFUL ENDING.');
        return this.parseModel(res.items[0], res) as T;
      });
  }

  public parseModel(model: Entry<any> | Asset, collection: EntryCollection<any>): any {
    if (!model) {
      return model;
    }
    console.log('PARSING STARTS.');
    const parsedModel = { sys: model.sys };

    for (const property in model.fields) {
      if (model.fields.hasOwnProperty(property)) {
        let value = model.fields[property];
        if (value instanceof Array) {
          const arrayValue: any[] = [];
          for (const item of value) {
            arrayValue.push(this.parseValue(item, collection));
          }
          value = arrayValue;
        } else {
          value = this.parseValue(value, collection);
        }
        parsedModel[property] = value;
      }
    }
    console.log('PARSING ENDING.');
    return parsedModel;
  }

  private parseValue(value: any, collection: EntryCollection<any>) {
    if (value && value.sys) {
      switch (value.sys.type) {
        case 'Entry':
          value = this.parseModel(value, collection);
          break;
        case 'Asset':
          value = this.parseModel(value, collection);
          break;
      }
    }
    return value;
  }

  public isContentOfType(contentItem: any, contentId: string): boolean {
    if (!contentItem || !contentItem.sys.contentType) {
      return false;
    }
    return contentItem.sys.contentType.sys.id === contentId;
  }
}

下面是我在花了几天尝试不同方法后解决这个问题的方法。 angular 的内容丰富的库在 SSR 和使用路由解析器上无法正常工作。所以我将逻辑和对 contentful 的调用移动到我们的代理 API(它已经在处理其他服务调用)。通过这种方式,我能够使用 SSR 和路由解析器通过代理调用 contentful API。代理 API 在 .NET 核心中构建并部署在 Azure 上。

我不知道为什么会发生这种情况,但我希望这能为遇到此库或其他类似库的类似问题的任何人带来启发。