angular - Angular 2+ Universal 页面未在服务器上呈现
问题描述
我已经阅读了 angular.io 文档中提供的指南,以在我的 Angular 8 应用程序中实现 SSR(Angular Universal),一切正常,我的服务器控制台或浏览器控制台中没有错误,构建运行并成功托管以下线
npm run build:ssr && npm run
问题是我在查看源代码中的 app-root 标记内没有看到服务器端呈现的代码
我已经尝试过 stackoverflow 的问题,但没有发现类似的问题
以下是文件:
服务器.ts
import 'zone.js/dist/zone-node';
import * as express from 'express';
import { join } from 'path';
// Express server
const app = express();
const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist/browser');
// require('jsdom-global')();
const domino = require('domino');
const fs = require('fs');
const path = require('path');
// const template = fs.readFileSync(path.join(__dirname, '.', 'dist', 'index.html')).toString();
const template = fs.readFileSync(path.join(__dirname, join(DIST_FOLDER, 'index.html'))).toString();
const win = domino.createWindow(template);
global['window'] = win;
global['document'] = win.document;
global["branch"] = null;
global["object"] = win.object;
global["HTMLElement"] = win.HTMLElement;
global["navigator"] = win.navigator;
// * 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
app.get('*', (req, res) => {
res.render('./index', { req });
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`);
});
app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';
import { ServiceWorkerModule, SwUpdate } from '@angular/service-worker';
import { httpInterceptorProviders } from './common/http-interceptor';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { GiftRegistryInfoComponent } from './modals/gift-registry-info/gift-registry-info.component';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { ModalModule } from 'ngx-bootstrap/modal';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { GooglePlaceModule } from 'ngx-google-places-autocomplete';
import { AgmCoreModule } from '@agm/core';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { NgxSpinnerModule } from 'ngx-spinner';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { NgReduxModule, NgRedux } from '@angular-redux/store';
import { RootState, rootReducer, INITIAL_STATE } from './redux/app.store';
import { Actions } from './redux/app.actions';
import { CookieService } from 'ngx-cookie-service';
import { EventImageUploadComponent } from './modals/event-image-upload/event-image-upload.component';
import { ImageCropperModule } from 'ngx-image-cropper';
import { ShareComponent } from './components/share/share.component';
import { ShareButtonsModule } from '@ngx-share/buttons';
import { ImagesViewerComponent } from './components/images-viewer/images-viewer.component';
import { SlickModule } from 'ngx-slick';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { LoginComponent } from './modals/login/login.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ConfirmationComponent } from './modals/confirmation/confirmation.component';
import { PopoverModule } from 'ngx-bootstrap/popover';
import { CreateScheduleItemComponent } from './components/schedule/create-schedule-item/create-schedule-item.component';
import { TimepickerModule } from 'ngx-bootstrap/timepicker';
import { AddChecklistItemComponent } from './modals/add-checklist-item/add-checklist-item.component';
import { UpdateBudgetComponent } from './modals/budget/update-budget/update-budget.component';
import { AddExpenceComponent } from './modals/budget/add-expence/add-expence.component';
import { GuestlistItemComponent } from './components/guestlist/guestlist-item/guestlist-item.component';
import { GuestlistInviteComponent } from './modals/guestlist/guestlist-invite/guestlist-invite.component';
@NgModule({
declarations: [
AppComponent,
GiftRegistryInfoComponent,
EventImageUploadComponent,
ImagesViewerComponent,
LoginComponent,
ConfirmationComponent,
AddChecklistItemComponent,
UpdateBudgetComponent,
AddExpenceComponent,
GuestlistInviteComponent,
],
entryComponents: [
EventImageUploadComponent,
ImagesViewerComponent,
LoginComponent,
AddChecklistItemComponent,
UpdateBudgetComponent,
AddExpenceComponent,
GuestlistInviteComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
AppRoutingModule,
HttpClientModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: true }),
BrowserAnimationsModule,
TabsModule.forRoot(),
ModalModule.forRoot(),
BsDatepickerModule.forRoot(),
GooglePlaceModule,
AgmCoreModule.forRoot({
apiKey: 'AIzaSyCvKiudJ7QySLw32JNijbiic2HzrXrVZPE'
}),
NgxSpinnerModule,
InfiniteScrollModule,
NgReduxModule,
// Always keep all the angular material imports after BrowserModule
MatSnackBarModule,
ImageCropperModule,
HttpClientModule, // (Required) for share counts
HttpClientJsonpModule, // (Optional) For linkedIn & Tumblr counts
ShareButtonsModule,
SlickModule.forRoot(),
TooltipModule.forRoot(),
FormsModule,
ReactiveFormsModule,
PopoverModule.forRoot(),
TimepickerModule.forRoot(),
],
providers: [
httpInterceptorProviders,
MatSnackBarModule,
Actions,
CookieService,
],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(
updates: SwUpdate,
private snackBar: MatSnackBar,
ngRedux: NgRedux<RootState>
) {
updates.available.subscribe(event => {
const snackBarRef =
this.snackBar.open('A new version is available, please reload to update', 'Reload', { duration: 0, verticalPosition: 'top' });
snackBarRef.onAction().subscribe(() => {
window.location.reload();
});
});
ngRedux.configureStore(rootReducer, INITIAL_STATE);
}
}
角.json
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"habari-events-pwa": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/browser",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": false,
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
"./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css",
"src/styles.scss",
"src/assets/css/slick.css"
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"src/assets/js/slick.js"
]
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
],
"serviceWorker": true
},
"dev": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.dev.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
],
"serviceWorker": true
},
"stage": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.stage.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
],
"serviceWorker": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "habari-events-pwa:build"
},
"configurations": {
"production": {
"browserTarget": "habari-events-pwa:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "habari-events-pwa:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
"./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css",
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "habari-events-pwa:serve"
},
"configurations": {
"production": {
"devServerTarget": "habari-events-pwa:serve:production"
}
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/server",
"main": "src/main.server.ts",
"tsConfig": "tsconfig.server.json"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"sourceMap": false,
"optimization": {
"scripts": false,
"styles": true
}
}
}
}
}
}
},
"defaultProject": "habari-events-pwa"
}
包.json
{
"name": "habari-events-pwa",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"build:dev": "ng build --prod --configuration=dev",
"build:stage": "ng build --prod --configuration=stage",
"build:prod": "ng build --prod --configuration=production",
"compile:server": "webpack --config webpack.server.config.js --progress --colors",
"serve:ssr": "node dist/server",
"build:ssr": "npm run build:client-and-server-bundles && npm run compile:server",
"build:client-and-server-bundles": "ng build --prod && ng run habari-events-pwa:server:production --bundleDependencies all"
},
"private": true,
"dependencies": {
"@agm/core": "^1.0.0-beta.6",
"@angular-redux/store": "^9.0.0",
"@angular/animations": "^8.1.0",
"@angular/cdk": "^8.1.1",
"@angular/common": "~8.1.0",
"@angular/compiler": "~8.1.0",
"@angular/core": "~8.1.0",
"@angular/forms": "~8.1.0",
"@angular/material": "^8.0.2",
"@angular/platform-browser": "~8.1.0",
"@angular/platform-browser-dynamic": "~8.1.0",
"@angular/platform-server": "~8.1.0",
"@angular/pwa": "^0.801.0",
"@angular/router": "~8.1.0",
"@angular/service-worker": "~8.1.0",
"@fortawesome/angular-fontawesome": "^0.4.0",
"@fortawesome/fontawesome-svg-core": "^1.2.19",
"@fortawesome/free-brands-svg-icons": "^5.9.0",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@nguniversal/express-engine": "^8.1.1",
"@nguniversal/module-map-ngfactory-loader": "8.1.1",
"@ngx-share/button": "^7.1.2",
"@ngx-share/buttons": "^7.1.2",
"@ngx-share/core": "^7.1.2",
"bootstrap": "4.1.1",
"express": "^4.15.2",
"font-awesome": "^4.7.0",
"jquery": "^3.3.1",
"jsdom-global": "^3.0.2",
"jsencrypt": "^3.0.0-rc.1",
"mock-browser": "^0.92.14",
"ngx-bootstrap": "^5.0.0",
"ngx-cookie-service": "^2.2.0",
"ngx-google-places-autocomplete": "^2.0.4",
"ngx-image-cropper": "^1.4.1",
"ngx-infinite-scroll": "^7.2.0",
"ngx-sharebuttons": "^4.1.4",
"ngx-slick": "^0.2.1",
"ngx-spinner": "^8.0.1",
"redux": "^4.0.1",
"rxjs": "~6.4.0",
"tslib": "^1.9.0",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.801.0",
"@angular/cli": "~8.1.0",
"@angular/compiler-cli": "~8.1.0",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "^5.0.0",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"ngx-validators": "^5.0.0",
"protractor": "~5.4.0",
"ts-loader": "^5.2.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.4.3",
"webpack-cli": "^3.1.0"
}
}
我希望 app-root 标记内有一些服务器端呈现的代码
解决方案
推荐阅读
- android - 如何提高将重图像上传到服务器的性能
- python-3.x - “uvloop”没有属性“EventLoopPolicy”
- docker - 无法在节点 docker 映像中安装 npm
- python - 从默认模型 gpt-2-simple python 上的输入生成文本
- javascript - Laravel 框架中控制器功能的 ajax url 不正确
- r - 未找到对象“趋势”
- java - 创建java接口的实例有什么好处?
- web3js - 如何使用 web3.js 购买或出售 erc20 代币?
- node.js - {....} 类型的参数不能分配给 'string | 网址'
- android - 我不明白为什么会收到此错误: android.view.InflateException: Binary XML file line #23 in com.whootis.hmmonitor:layout/activity_main