如何在 Angular 17 SSR 中将浏览器静态内容移动到 CDN

问题描述 投票:0回答:1

我们正在使用 Angular 17 SSR 构建 SEO 优化的网站。为了使我们的网站加载速度更快,我们决定将静态文件移动到 S3 并添加 s3 的 cdn 路径而不是浏览器路径来提供静态文件,如 html 、 css 、 javascript 等。

为此,我们构建项目并将文件从浏览器文件夹移动到 s3 存储桶并获取其 CDN url。 下面是我们的 server.ts 文件。

import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import AppServerModule from './src/main.server';


// The Express app is exported so that it can be used by serverless Functions.
export async function app(): Promise<express.Express> {
  const server = express();

  const serverDistFolder = dirname(fileURLToPath(import.meta.url));
  // const browserDistFolder = resolve(serverDistFolder, '../browser');
  const browserDistFolder: string = "https://custom-cdn.something.co";
  const indexHtml = join(serverDistFolder, 'index.server.html');

  const commonEngine = new CommonEngine();

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

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

  // All regular routes use the Angular engine
  server.get('*', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;

    commonEngine
      .render({
        bootstrap: AppServerModule,
        inlineCriticalCss: false,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
      })
      .then((html) => res.send(html))
      .catch((err) => next(err));
  });


  return server;
}

async function run(): Promise<void> {
  const port = process.env['PORT'] || 4000;

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

run();

现在,当我们运行它时,我们的页面损坏了,并且 JavaScript 无法工作。 每个 js 块都会多次出现此错误, 无法加载模块脚本:需要 JavaScript 模块脚本,但服务器以 MIME 类型“text/html”进行响应。根据 HTML 规范对模块脚本强制执行严格的 MIME 类型检查。

我尝试添加这个

// Serve static files from /browser
  server.get('*.*', express.static(browserDistFolder, {
    maxAge: '1y'
  }));

但结果是一样的。 我猜想,每个文件都被用作文本/html,因此,CSS、图像和 JS 不起作用。 这是 S3 问题还是代码问题? 我们在这里缺少什么? 任何帮助或反馈都会有帮助。

angular amazon-s3 server-side-rendering cdn angular-ssr
1个回答
0
投票

我也遇到了同样的问题,我意识到需要通过http请求从cdn请求静态文件。对我有用的代码如下:

import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import bootstrap from './src/main.server';

export function app(): express.Express {
  const server = express();
  const serverDistFolder = dirname(fileURLToPath(import.meta.url));
  const browserDistFolder = 'https://custom-cdn.something.co'
  const indexHtml = join(serverDistFolder, 'index.server.html');

  const commonEngine = new CommonEngine();

  server.set('view engine', 'html');
  server.set('views', browserDistFolder); 
  
  const hasExtension = (filePath: string): boolean => {
    return path.extname(filePath) !== '';
  }

  server.get('**', async (req, res) => {
    try {        
        let requestPath = req.path;

        if (!hasExtension(requestPath)) {
            requestPath += '/index.html';
        }
        
        const path = `${browserDistFolder}${requestPath}`
        const response = await fetch(path);
             
        if (!response.ok) {
            res.status(response.status).send('Error message');
            return;
        }

        const contentType = response.headers.get('content-type') ?? '';

        res.set('Content-Type', contentType);

        if (contentType.includes('text') || contentType?.includes('json')) {            
            const body = await response.text();
            res.send(body);
        } 
        else {            
            const arrayBuffer = await response.arrayBuffer();
            const buffer = Buffer.from(arrayBuffer);
            res.send(buffer);
        }

    } catch (error) {
        console.error('Error fetching:', error);
        res.status(500).send('Internal Server Error');
    }
  });

  server.get('**', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req
    
    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
        providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
      })
      .then((html) => res.send(html))
      .catch((err) => next(err));
  });

  return server;
}

function run(): void {
  const port = process.env['PORT'] || 4000;  
  const server = app();
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

run();

代码小解释:

创建项目时,默认情况下会配置一个快速中间件来提供 browserDistFolder 中的静态文件。

  server.set('view engine', 'html');
  server.set('views', browserDistFolder);  
  server.get('*.*', express.static(browserDistFolder, {
     maxAge: '1y'
  }));

但是使用 CDN 时,您必须配置自己的中间件以从 CDN 获取文件并将其提供给应用程序。为此,您可以使用 fetch 发出 http 请求并获取文件

const path = `${browserDistFolder}${requestPath}`
const response = await fetch(path);
        
if (!response.ok) {
    res.status(response.status).send('Error message');
    return;
}

const contentType = response.headers.get('content-type') ?? '';

res.set('Content-Type', contentType);

您必须检查内容类型,对于 html、js 或 css 文件,您可以将响应正文转换为文本格式。但是对于img、video、fonts等类型的文件,你必须将响应体转换为buffer,这样浏览器才能正确读取。

if (contentType.includes('text') || contentType.includes('json')) {            
    const body = await response.text();
    res.send(body);
} 
else {            
    const arrayBuffer = await response.arrayBuffer();
    const buffer = Buffer.from(arrayBuffer);
    res.send(buffer);
}

如果你在 Angular 中启用了预渲染路由,则需要检查请求的资源是否有扩展名,因为当应用程序请求这些预渲染路由时,请求路径只是预渲染的文件夹名称没有index.html的路线。

   let requestPath = req.path; // example: my-pre-rendered-route

    if (!hasExtension(requestPath)) { // true
        requestPath += '/index.html'; // my-pre-rendered-route/index.html
    }

如果不添加

/index.html
,则不会加载预渲染路线。

© www.soinside.com 2019 - 2024. All rights reserved.