Angular独立组件结合Universal使用ng2-pdf-viewer启动SSR报错求助
问题场景
使用Angular独立组件搭配Angular Universal(Express Engine),引入ng2-pdf-viewer后,ng serve启动正常,但执行dev:ssr时出现canvas相关打包错误,仅在SSR环境下触发。
Angular 16.2.0 环境详情
错误信息:
./node_modules/canvas/build/Release/canvas.node:1:0 - Error: Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.
(Source code omitted for this binary file)
依赖清单:
"dependencies": { "@angular/animations": "^16.2.0", "@angular/cdk": "^16.2.9", "@angular/common": "^16.2.0", "@angular/compiler": "^16.2.0", "@angular/core": "^16.2.0", "@angular/forms": "^16.2.0", "@angular/material": "^16.2.9", "@angular/platform-browser": "^16.2.0", "@angular/platform-browser-dynamic": "^16.2.0", "@angular/platform-server": "^16.2.0", "@angular/router": "^16.2.0", "@ngneat/hot-toast": "^6.0.1", "@ngneat/overview": "^5.1.0", "@nguniversal/express-engine": "^16.2.0", "aos": "^2.3.4", "express": "^4.15.2", "ng2-pdf-viewer": "^10.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.13.0" }
Angular 17.0.0 环境详情
错误信息:
node_modules/pdfjs-dist/build/pdf.js:8671:35:
8671 │ const Canvas = require("canvas");
╵ ~~~~~~~~
提示:可将"canvas"标记为外部资源排除在打包之外,或用try/catch包裹require调用处理运行时错误
依赖清单:
"dependencies": { "@angular/animations": "^17.0.0", "@angular/common": "^17.0.0", "@angular/compiler": "^17.0.0", "@angular/core": "^17.0.0", "@angular/forms": "^17.0.0", "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", "@angular/platform-server": "^17.0.0", "@angular/router": "^17.0.0", "@angular/ssr": "^17.0.0", "express": "^4.18.2", "ng2-pdf-viewer": "^10.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.2" }
解决方案
核心思路
ng2-pdf-viewer依赖的pdfjs-dist在SSR环境下会尝试加载Node端的canvas二进制模块,而Webpack无法解析该二进制文件。解决方式是将canvas标记为外部资源,让Express在运行时直接加载,而非打包进bundle。
针对Angular 16(@nguniversal/express-engine)
- 在项目根目录创建
webpack.server.config.js文件:
module.exports = { externals: { canvas: 'commonjs canvas' } };
- 修改
angular.json中server配置的builder节点,添加Webpack配置引用:
"server": { "builder": "@angular-devkit/build-angular:server", "options": { "outputPath": "dist/server", "main": "server.ts", "tsConfig": "tsconfig.server.json", "webpackConfig": "./webpack.server.config.js" } // 保留其他原有配置 }
针对Angular 17(@angular/ssr)
方式一:直接修改server.ts,在启动应用前声明外部依赖:
import { APP_BASE_HREF } from '@angular/common'; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { provideServerRendering } from '@angular/platform-server'; import { provideRouter } from '@angular/router'; import { routes } from './app/app.routes'; // 标记canvas为外部依赖,让运行时直接加载 (global as any).canvas = require('canvas'); bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideServerRendering(), { provide: APP_BASE_HREF, useValue: '/' } ] });
方式二:同Angular 16,创建webpack.server.config.js并在angular.json中配置引用。
额外优化:仅在浏览器端渲染PDF组件
如果无需在SSR阶段渲染PDF内容,可通过平台判断仅在浏览器环境加载组件:
import { isPlatformBrowser } from '@angular/common'; import { Component, Inject, PLATFORM_ID } from '@angular/core'; @Component({ selector: 'app-pdf-wrapper', template: ` <ng-container *ngIf="isBrowser"> <pdf-viewer [src]="pdfSource"></pdf-viewer> </ng-container> ` }) export class PdfWrapperComponent { isBrowser: boolean; pdfSource = 'path/to/your.pdf'; constructor(@Inject(PLATFORM_ID) platformId: Object) { this.isBrowser = isPlatformBrowser(platformId); } }
内容的提问来源于stack exchange,提问作者arun

