【问题标题】:Questions on .NET Core 3.1 & Angular SSR spa.UseSpaPrerendering alternative有关 .NET Core 3.1 和 Angular SSR spa.UseSpaPrerendering 替代方案的问题
【发布时间】:2020-05-02 00:17:37
【问题描述】:

我创建这篇文章是为了从社区中获得一些见解。 不久前,随着 .NET Core 3.0 的发布,众所周知且广泛使用的 spa.UseSpaPrerendering 已被标记为已过时。

大约在 2019 年初,我在一个使用 Angular 但需要 SEO 和更好的加载性能的项目中使用 .NET Core 实现了 SSR。

1 年后(现在,即 2020 年初),他们希望在不同的项目中使用相同的方法。但它已经使用了 Core 3.1。我们立即注意到了 Depriated 标志,所以我自己去寻找一种方法。

根据过去的经验,SSR 问题分为两部分,第一部分是让您的 Angular 应用程序真正能够在服务器端运行。因此,摆脱或解决所有无法在服务器端执行的东西(通过在 Angular 中使用 isPlatform 东西来解决窗口 API 的使用)。第二部分是让 .NET Core 启动 Angular CLI 以开始实际的预渲染。这是使用UseSpaPrerendering 完成的。

分析告诉我们自己弄清楚的文档并检查我过去的代码,事情实际上开始变得有意义了。

我查看了 package.json 文件中的命令,执行的 2 个主要命令是 build:ssr 以实际预编译浏览器/...文件旁边的整个 server/main.js。第二个命令是serve:ssr,它将由UseSpaPrerendering 代码执行(至少我假设是这样)。

实际上,我们的 CI/CD 会执行 build:ssr 并将所有文件发布到运行 .NET Core 运行时的应用服务器。然后使用UseSpaPrerendering 代码执行serve:ssr

现在向前迈进,我需要找到解决方案。我想我也可以自己运行必要的命令。因此,在我的 Angular 中排除了一些非 SSR 兼容代码后,我自己运行了build:ssr 命令,然后运行了serve:ssr 命令。这有效,我的 Angular 应用程序是由节点本身提供的,而不是由 .NET Core 提供的 SSR 渲染。

下一步是我在我的 .NET Core 启动文件中尝试这样做。现在我自己做了build:ssr(因为在生产中它将由CI/CD完成),我在我的package.json中重新编写了start脚本来运行命令npm run serve:ssr。我使用 startup.cs 中的 spa.UseAngularCliServer(npmScript: "start"); 代码启动了该命令,然后我的 .NET Core 运行时启动了我的 API 和我的 SSR Angular 应用程序。

到目前为止一切顺利,但现在只有 1 个问题。我的 SSR Angular 默认托管在 4000 端口上,并且还监听该端口(我可以在输出中看到),我的 API 监听端口 5000(http) 和 5001(https)。

所以现在我有几个问题:

  1. 现在这样做是否正确?
  2. 如何确保在生产环境中,当有人进入我的应用程序时,节点侦听器会启动?
  3. 如果我完全分离我的 .NET Core API 和 Angular SSR 应用程序会更好吗?因此也单独部署它们?

【问题讨论】:

  • 看不到 ASP.NET 团队为此提供的任何指导有点令人沮丧。特别是因为 Angular SSR 的官方文档仍然说要使用已弃用的库

标签: c# angular .net-core server-side-rendering


【解决方案1】:

对于遇到此问题的任何人,我刚刚解决了它,这是我们的解决方案,但事实很少:

  • [Web.config] Node Context,我的意思是进程工作目录,在iisnode中工作方式不同,PWD是目标文件路径,这意味着如果你的main.js在dist/server/main中。 js 那么相对于浏览器的路径不会是 dist/browser/ 而是 ../browser/
  • 考虑到在部署期间,您必须根据这个新结构生成 Web.config

    -Handler iisnode -NodeStartFile dist/server/main.js -appType 节点

  • [server.ts] - 考虑到这一点,还要考虑根据您的运行时环境设置浏览器路径,这样如果您在生产环境中,它应该是 ../browser

  • [server.ts] - server.ts 中的订单事项。 如果您遇到浏览器 API 问题,这是因为“import { AppServerModule } from './ma​​in.server';” 必须放置在 多米诺声明之后。

这是一个 server.ts 上的工作示例,它也根据带有区域设置字符串的 url 请求使用 i18n 重定向(现在我也解决了这个 i18n 问题,我可以告诉你阅读文档是值得的)。

/***************************************************************************************************
 * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
 */
import { APP_BASE_HREF } from '@angular/common';
import '@angular/localize/init';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import 'zone.js/dist/zone-node';
import { environment } from './environments/environment';

// THIS FIX MOST OF THE COMMON ISSUES WITH SSR:
// FIRST SET THE BROWSER PATH ACCORDING TO RUNTIME ENVIRONMENT
let browserPath;
if (environment.production) {
  browserPath = '../browser';
} else {
  browserPath = 'dist/browser';
}
const enDistFolder = join(process.cwd(), browserPath + '/en');

// Emulate browser APIs
const domino = require('domino');
const fs = require('fs');
const templateA = fs.readFileSync(join(enDistFolder, 'index.html')).toString();

const win = domino.createWindow(templateA);
console.log('win');
win.Object = Object;
console.log('Object');
win.Math = Math;
console.log('Math');

global['window'] = win;
global['document'] = win.document;
global['Event'] = win.Event;
console.log('declared Global Vars....');

/****************************************************/   
/** NOTE THIS: I need to avoid sorting this line */
// USE CTRL+P -> SAVE WITHOUT FORMATTING
import { AppServerModule } from './main.server';
/****************************************************/

// The Express app is exported so that it can be used by serverless Functions.
export function app() {
  const server = express();
  const indexHtml = existsSync(join(browserPath, 'index.original.html')) ? 'index.original.html' : 'index.html';

  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

  server.set('view engine', 'html');
  server.set('views', browserPath);

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

  server.use('/robots.txt', express.static('/en/robots.txt'));
  server.use('/ads.txt', express.static('/en/ads.txt'));

  // THE ORIGINAL Universal Requests handler
  // // // All regular routes use the Universal engine
  // // server.get('*', (req, res) => {
  // //   res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  // // });

  // OUR i18n REQUESTS HANDLER
  // All regular routes use the Universal engine
  server.get('*', (req, res) => {
    // this is for i18n
    const supportedLocales = ['en', 'es'];
    const defaultLocale = 'es';
    const matches = req.url.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//);

    // check if the requested url has a correct format '/locale' and matches any of the supportedLocales
    const locale = (matches && supportedLocales.indexOf(matches[1]) !== -1) ? matches[1] : defaultLocale;

    res.render(`${locale}/index.html`, { req });
  });

  return server;
}

function run() {
  const port = process.env.PORT || 4000;

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

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
  run();
}

export * from './main.server';

我仍然需要处理这段代码和我们的应用程序(SSR 和 oauth 问题,另一个有趣的话题),但我想分享它,因为我们花了将近 20 次部署来解决这些问题。

最后的话:如果您在 angular 8 迁移后来到这里,我很乐意为您提供帮助并给您很好的提示,但老实说,请按照指南并仔细阅读文档。此外,如果您使用的是 Azure DevOps 管道,则应考虑使用 npm 缓存。我们的 as 很大,现在我们在每个构建过程中节省了超过 12 分钟(这是一个巨大的时间,不是吗?)请随时与我联系。

胡安

【讨论】:

  • 您知道在您使用 Linux 应用服务时这是如何工作的吗?
【解决方案2】:

在将项目部署在 azure 或其他任何东西上时,我也在为 netcore 3.1 和 angular 苦苦挣扎。你有什么发现吗?你能提供你的启动文件吗? 当我使用 dotnet publish 时,package.json 不会复制到 publish/ClientApp 目录,因此spa.UseAngularCliServer() 使用的命令失败或者它只是找不到 /index.html。

现在,我像这样在本地运行我的项目:

app.UseSpa(spa =>
  {
    spa.Options.SourcePath = "ClientApp";
    if (env.IsDevelopment())
    {
      spa.UseAngularCliServer(npmScript: "dev:ssr");
    }
  });

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-03-21
    • 2020-10-07
    • 1970-01-01
    • 2017-04-16
    • 1970-01-01
    • 2010-11-21
    • 2020-07-30
    • 2020-04-06
    相关资源
    最近更新 更多