Angular 9 个通用部署问题

Angular 9 universal deployment woes

老实说,我觉得这很烦人,因为它一直在变化。 我在这里用 Angular 的更简单版本解决了这个问题:

但现在这已经过时了。 不再生成 server.js,而是您必须修改 web.config 以指向 main.js 这听起来像是一个改进。 我将 yaml 更新为:

pool:
  name: Azure Pipelines
steps:
- task: gittools.gitversion.gitversion-task.GitVersion@5
  displayName: GitVersion

- task: NodeTool@0
  displayName: 'Use Node 12.x'
  inputs:
    versionSpec: 12.x

- task: Npm@1
  displayName: 'npm install angular cli'
  inputs:
    command: custom
    verbose: false
    customCommand: 'install @angular/cli -g'

- task: Npm@1
  displayName: 'npm install'
  inputs:
    verbose: false

- task: Npm@1
  displayName: 'npm build'
  inputs:
    command: custom
    verbose: false
    customCommand: 'run build:ssr'

- task: CopyFiles@2
  displayName: 'Copy dist files to staging'
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)/dist'
    TargetFolder: '$(Build.ArtifactStagingDirectory)/app/dist'

- task: AzureRmWebAppDeployment@4
  displayName: 'Azure App Service Deploy: app-name'
  inputs:
    azureSubscription: 'Pay-As-You-Go (f61dc7cf-0ca2-4982-bbe7-9b6527c2962b)'
    WebAppName: r3plica
    packageForLinux: '$(Build.ArtifactStagingDirectory)/app'
    WebConfigParameters: '-Handler iisnode -NodeStartFile dist/app-name/server/main.js -appType node'

应该就是这样,但当然,事情并没有那么简单。 现在如果我 运行 node dist/app-name/server/main.js 我会得到一个错误。它返回这个:

ReferenceError: Blob is not defined at createBase64WorkerFactory (D:\home\site\wwwroot\dist\app-name\server\main.js:1:1418371)

所以我环顾四周,有人建议我安装 npm install --save-dev blob-polyfill,然后编辑 server.ts 文件:

import { Blob } from 'blob-polyfill';

global['Blob'] = Blob;

但这似乎并没有起到任何作用。错误仍然存​​在。 有谁知道我必须做什么?


更新

我决定今天再试一次。 我 运行 npm build:ssr 并将 服务器和浏览器 文件夹复制到本地网络服务器和 运行 node server/main.js 并且它抱怨它不能'在 dist/my-project/browser/index.html 找不到 index.html 文件,这对我有帮助。 所以我将整个 dist 文件夹复制到 wwwroot 和 运行 node dist/my-project/server/main.js 并且它起作用了。

所以我更新了我的管道来做同样的事情。我确认它实际上复制了整个 dist 文件夹,然后我将 web.config 复制到根目录。 我的 web.config 文件如下所示:

<configuration>
    <system.web>
        <customErrors mode="Off" />
    </system.web>
    <system.webServer>
        <staticContent>
            <remove fileExtension=".woff2" />
            <mimeMap fileExtension=".woff2" mimeType="font/woff2" />
        </staticContent>
        <handlers>
            <!-- Indicates that the server.js file is a node.js site to be handled by the iisnode module -->
            <add name="iisnode" path="dist/my-project/server/main.js" verb="*" modules="iisnode"/>
        </handlers>
        <httpErrors errorMode="Detailed"></httpErrors>
    </system.webServer>
</configuration>

但是当我尝试加载我的网站时,它只是给我一个错误:

HTTP Error 403.14 - Forbidden

A default document is not configured for the requested URL, and directory browsing is not enabled on the server.

这真的很烦人。 我查看了 https://example.scm.azurewebsites.net 并转到调试控制台并输入 node dist/my-project/server/main.js 返回:

Node Express server listening on http://localhost:4000

据我所知,它应该工作正常。 有谁知道为什么不是吗?

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

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

    -处理程序 iisnode -NodeStartFile dist/server/main.js -appType 节点

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

  • [server.ts] - server.ts 中的顺序很重要。 如果您遇到浏览器API问题这是因为"import { AppServerModule } from './main.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 分钟(这是大量时间,不是吗?)请随时与我联系。

胡安

所以,我又一次设法解决了这个问题。我在里面投入了几个小时,它开始让我发疯。我决定建立一个本地网络服务器 (iss) 并尽我所能。最后,正是这个救了我,因为 iisnode 正在记录错误,我可以看到哪里出了问题。

如果我在您执行 npm run build:ssr 时保持文件夹结构不变,并更新 webconfig 以指向 main.js,如 <add name="iisnode" path="dist/example-project/server/main.js" verb="*" modules="iisnode"/>你会收到类似这样的错误:

Error: Failed to lookup view "index" in views directory "C:\inetpub\wwwroot\dist\example-project\server\dist\example-project\browser"

从错误中可以看出,它使用了 main.js 所在的相对路径。 从这里您可能可以看到我是如何解决我的问题的。

我更新了我的任务以包含一个新副本,我将 main.js 复制到根目录并更新了我的 web.config 至此:

<add name="iisnode" path="main.js" verb="*" modules="iisnode"/>.

为了完整起见,这是我的完整 web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.webServer>
        <webSocket enabled="false" />
        <handlers>
            <add name="iisnode" path="main.js" verb="*" modules="iisnode"/>
        </handlers>
        <rewrite>
            <rules>
                <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
                    <match url="^main.js\/debug[\/]?" />
                </rule>
                <rule name="StaticContent">
                    <action type="Rewrite" url="public{REQUEST_URI}"/>
                </rule>
                <rule name="DynamicContent">
                    <conditions>
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
                    </conditions>
                    <action type="Rewrite" url="main.js"/>
                </rule>
                <rule name="Angular Routes" stopProcessing="true">
                    <match url=".*" />
                    <conditions logicalGrouping="MatchAll">
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                    </conditions>
                    <action type="Rewrite" url="/index.html" />
                </rule>
            </rules>
        </rewrite>
        <security>
            <requestFiltering>
                <hiddenSegments>
                    <remove segment="bin"/>
                </hiddenSegments>
            </requestFiltering>
        </security>
        <httpErrors existingResponse="PassThrough" />
    </system.webServer>
</configuration>

这是我的 xaml 天蓝色:

pool:
  name: Azure Pipelines
steps:
- task: gittools.gitversion.gitversion-task.GitVersion@5
  displayName: GitVersion

- task: NodeTool@0
  displayName: 'Use Node 12.x'
  inputs:
    versionSpec: 12.x
    checkLatest: true

- task: Npm@1
  displayName: 'npm install angular cli'
  inputs:
    command: custom
    verbose: false
    customCommand: 'install @angular/cli -g'

- task: Npm@1
  displayName: 'npm install'
  inputs:
    verbose: false

- task: Npm@1
  displayName: 'npm build'
  inputs:
    command: custom
    verbose: false
    customCommand: 'run build:ssr'

- task: CopyFiles@2
  displayName: 'Copy dist files to staging'
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)/dist'
    TargetFolder: '$(Build.ArtifactStagingDirectory)/dist'

- task: CopyFiles@2
  displayName: 'Copy web.config'
  inputs:
    SourceFolder: '$(Build.ArtifactStagingDirectory)/dist/example-project/browser'
    Contents: web.config
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

- task: CopyFiles@2
  displayName: 'Copy main.js'
  inputs:
    SourceFolder: '$(Build.ArtifactStagingDirectory)/dist/example-project/server'
    Contents: main.js
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

- task: AzureRmWebAppDeployment@4
  displayName: 'Azure App Service Deploy: example-project'
  inputs:
    azureSubscription: 'Your Subscription'
    WebAppName: 'example-project'
    packageForLinux: '$(Build.ArtifactStagingDirectory)'
    enableCustomDeployment: true
    RemoveAdditionalFilesFlag: true