Angular 7 SSR - проблемы с NgZone

Недавно я переместил веб-сайт своей компании с React на Angular, так как большинство наших проектов уже работали на Angular 7. Будучи "самым последним и величайшим" человеком, я решил внедрить рендеринг на стороне сервера, чтобы получить рейтинг скорости страницы Google близко к этому 100/100 (в настоящее время 42/100). Я возился с этим большую часть недели, но безуспешно - последний контрольно-пропускной пункт мне было особенно трудно преодолеть. Вот краткая информация о моей настройке, затем я углублюсь в детали:

  • NodeJS 8.9.1
  • Угловой 7 последний
  • Webpack 4.26.0
  • @ ngtools / webpack 7.0.5
  • Не использует angular-cli
  • Настройка AoT
  • одностраничное приложение

Это ошибка, которую я получаю, когда пытаюсь отобразить файл layout.html, настроенный для SSR:

TypeError: Cannot read property 'subscribe' of undefined
at new ApplicationRef (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:43263:37)
at _createClass (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:46296:20)
at _createProviderInstance (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:46258:26)
at initNgModule (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:46190:32)
at new NgModuleRef_ (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:46918:9)
at createNgModuleRef (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:46907:12)
at Object.debugCreateNgModuleRef [as createNgModuleRef] (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:48738:12)
at NgModuleFactory_.module.exports.NgModuleFactory_.create (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:49466:25)
at C:\code\lemmsoftWebsite\repo\node_modules\@angular\core\bundles\core.umd.js:14656:47
at ZoneDelegate.module.exports.ZoneDelegate.invoke (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:139510:26)
at Object.onInvoke (C:\code\lemmsoftWebsite\repo\node_modules\@angular\core\bundles\core.umd.js:14194:37)
at ZoneDelegate.module.exports.ZoneDelegate.invoke (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:139509:32)
at Zone.module.exports.Zone.run (C:\code\lemmsoftWebsite\repo\public\site\serverBuild\main.js:139260:43)
at NgZone.run (C:\code\lemmsoftWebsite\repo\node_modules\@angular\core\bundles\core.umd.js:14108:32)
at PlatformRef.bootstrapModuleFactory (C:\code\lemmsoftWebsite\repo\node_modules\@angular\core\bundles\core.umd.js:14654:27)
at renderModuleFactory (C:\code\lemmsoftWebsite\repo\node_modules\@angular\platform-server\bundles\platform-server.umd.js:1033:43)
at View.module.app.engine (C:\code\lemmsoftWebsite\repo\modules\clients\site\layout\index.js:60:4)
at View.render (C:\code\lemmsoftWebsite\repo\node_modules\express\lib\view.js:135:8)
at tryRender (C:\code\lemmsoftWebsite\repo\node_modules\express\lib\application.js:640:10)
at Function.render (C:\code\lemmsoftWebsite\repo\node_modules\express\lib\application.js:592:3)
at ServerResponse.render (C:\code\lemmsoftWebsite\repo\node_modules\express\lib\response.js:1008:7)
at C:\code\lemmsoftWebsite\repo\modules\clients\site\layout\index.js:83:9

После долгого прочтения файла пакета main.js я определил проблему следующим образом:

var ApplicationRef = /** @class */ (function () {
    /** @internal */
    function ApplicationRef(_zone, _console, _injector, _exceptionHandler, _componentFactoryResolver, _initStatus) {
        var _this = this;
        this._zone = _zone; // in this method, the _zone argument is {}, so there is no onMicrotaskEmpty method in it => when this._zone.onMicrotaskEmpty.subscribe() is attempted, we get "Cannot read property 'subscribe' of undefined"
        this._console = _console;
        this._injector = _injector;
        this._exceptionHandler = _exceptionHandler;
        this._componentFactoryResolver = _componentFactoryResolver;
        this._initStatus = _initStatus;
        this._bootstrapListeners = [];
        this._views = [];
        this._runningTick = false;
        this._enforceNoNewChanges = false;
        this._stable = true;
        // more code
    }
    // more code
}

В этом методе аргумент _zone равен {}, поэтому в нем нет метода onMicrotaskEmpty => при попытке this._zone.onMicrotaskEmpty.subscribe() мы получаем "Невозможно прочитать свойство" подписаться "из undefined". Я продолжал копаться в трассировке стека - это предыдущий шаг, где вызывается новый ApplicationRef и _zone передается как {}:

function _createClass(ngModule, ctor, deps) {
    var len = deps.length;
    switch (len) {
        case 0:
            return new ctor();
        case 1:
            return new ctor(resolveNgModuleDep(ngModule, deps[0]));
        case 2:
            return new ctor(resolveNgModuleDep(ngModule, deps[0]), resolveNgModuleDep(ngModule, deps[1]));
        case 3:
            return new ctor(resolveNgModuleDep(ngModule, deps[0]), resolveNgModuleDep(ngModule, deps[1]), resolveNgModuleDep(ngModule, deps[2]));
        default:
            // this is where we get some insight into the cause of the error
            var depValues = new Array(len);
            for (var i = 0; i < len; i++) {
                depValues[i] = resolveNgModuleDep(ngModule, deps[i]);
            }
            // if we do console.log(deps[0], depValues[0]), which is _zone, we get interesting stuff... 
            return new (ctor.bind.apply(ctor, Object(tslib__WEBPACK_IMPORTED_MODULE_0__["__spread"])([void 0], depValues)))();
    }
}

Здесь мы получаем некоторое представление о причине ошибки - в блоке "по умолчанию" коммутатора. Если мы будем console.log(deps[0], depValues ​​[0]) после цикла for, то есть _zone, мы получим интересные вещи:

// deps[0]
{ flags: 0,
  token:
   { [Function: NgZone]
     isInAngularZone: [Function],
     assertInAngularZone: [Function],
     assertNotInAngularZone: [Function] },
  tokenKey: 'NgZone_29' }
// depValues[0]
{}

Итак, вот виновник, подумал я! 'resolNgModuleDep' облажался! Поэтому я продолжал копать:

function resolveNgModuleDep(data, depDef, notFoundValue) {
    if (notFoundValue === void 0) { notFoundValue = Injector.THROW_IF_NOT_FOUND; }
    var former = setCurrentInjector(data);
    try {
        if (depDef.flags & 8 /* Value */) {
            return depDef.token;
        }
        if (depDef.flags & 2 /* Optional */) {
            notFoundValue = null;
        }
        if (depDef.flags & 1 /* SkipSelf */) {
            return data._parent.get(depDef.token, notFoundValue);
        }
        var tokenKey_1 = depDef.tokenKey;
        switch (tokenKey_1) {
            case InjectorRefTokenKey:
            case INJECTORRefTokenKey:
            case NgModuleRefTokenKey:
                return data;
        }
        var providerDef = data._def.providersByKey[tokenKey_1];
        var injectableDef = void 0;
        if (providerDef) {
            var providerInstance = data._providers[providerDef.index];
            if (providerInstance === undefined) {
                providerInstance = data._providers[providerDef.index] =
                    _createProviderInstance(data, providerDef);
            }
            return providerInstance === UNDEFINED_VALUE ? undefined : providerInstance;
        }
        else if ((injectableDef = getInjectableDef(depDef.token)) && targetsModule(data, injectableDef)) {
            var index = data._providers.length;
            data._def.providersByKey[depDef.tokenKey] = {
                flags: 1024 /* TypeFactoryProvider */ | 4096 /* LazyProvider */,
                value: injectableDef.factory,
                deps: [], index: index,
                token: depDef.token,
            };
            data._providers[index] = UNDEFINED_VALUE;
            return (data._providers[index] =
                _createProviderInstance(data, data._def.providersByKey[depDef.tokenKey]));
        }
        else if (depDef.flags & 4 /* Self */) {
            return notFoundValue;
      }
        // there it is!
        return data._parent.get(depDef.token, notFoundValue);
    }
    finally {
        setCurrentInjector(former);
    }
}

Вот оно! Прежде чем, наконец, в строке, которая возвращает data._parent.get(depDef.token, notFoundValue) - это то место, где передается depDef.token (в нашем случае это NgZone), а notFoundValue равен нулю. Возвращаемый объект - это просто {}, поэтому все проблемы возникнут позже. Это так далеко, как мне удалось получить, я ходил туда-сюда, пытаясь решить это отсюда, но безрезультатно. Поверьте мне, я искал насквозь в stackru и просто в Google; Я прочитал 1000 средних сообщений - безуспешно. Я не использую angular-cli, потому что помимо прочего мне нравится настраивать конфигурацию моего веб-пакета, но я сомневаюсь, что причина в этом, потому что сама angular-cli использует webpack под капотом. Я собираюсь вставить несколько дополнительных вещей ниже в нескольких отрывках - мой конфиг веб-пакета, метод сервера, где визуализируется html и угловой пакет, и т. Д.

// the webpack config

'use strict'

const
 AngularCompilerPlugin = require( "@ngtools/webpack" ).AngularCompilerPlugin,
 BellOnBundlerErrorPlugin = require('bell-on-bundler-error-plugin'),
 BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin,
 path = require('path'),
 ProgressBarPlugin = require('progress-bar-webpack-plugin'),
 UglifyJsPlugin = require('uglifyjs-webpack-plugin'),
 webpack = require('webpack')


module.exports = (config, name) => {
 let includePath = config.clientPath,
  publicPath = path.join(config.publicPath, 'serverBuild'),
  libPath = path.join(__dirname, '../../lib'),
  nodeModulesPath = config.nodeModulesPath,
  include = [includePath]

 return {
  target: 'node',
  mode: 'none',
  entry: [
   path.join(includePath, 'polyfills.ts'),
   path.join(includePath, 'vendor.common.ts'),
   path.join(includePath, 'vendor.server.ts'),
   path.join(includePath, 'index.server.ts')
  ],
  output: {
   path: publicPath,
   filename: '[name].js',
   chunkFilename: '[id].chunk.js',
   publicPath: '/dist/',
   libraryTarget: 'commonjs-module'
  },
  resolve: {
   extensions: ['.ts', '.js'],
   modules: ['node_modules', libPath]
  },
  module: {
   rules: [
    {
     test: /\.pug$/,
     include: [libPath, includePath],
     use: ['raw-loader', 'pug-html-loader']
    },
    {
     test: /\.css$/,
     include: [libPath, nodeModulesPath, includePath],
     exclude: [],
     use: ['to-string-loader', 'css-loader']
    },
    {
     test: /\.less$/,
     exclude: [],
     use: ['to-string-loader', 'css-loader', 'less-loader']
    },
    {
     test: /\.scss$/,
     include: [libPath, nodeModulesPath, includePath],
     exclude: [],
     use: ['raw-loader', 'sass-loader']
    },
    {
     test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
     include: [includePath, libPath],
     use: [{
      loader: '@ngtools/webpack'
     }],
     exclude: [/\.(spec|e2e)\.ts$/]
    },
    {
     test: /\.json$/,
     include,
     exclude: [],
     use: ["json2-loader"]
    }
   ]
  },
  stats: 'verbose',
  plugins: [
   // new webpack.HotModuleReplacementPlugin(),
   new BellOnBundlerErrorPlugin(),
   new ProgressBarPlugin({
    format: '  build [:bar] (:percent) - (:elapsed seconds)',
    clear: false,
    complete: '#',
    summary: 'true'
   }),
   // new webpack.NamedModulesPlugin(),
   new AngularCompilerPlugin({
    tsConfigPath: path.join(__dirname, '../../tsconfig.server.json'),
    entryModule: path.join(includePath, 'app.server.ts#AppServerModule'),
    sourceMap: true
   })
  ]
 }
}

// tsconfig.json and tsconfig.server.json
{
 "compilerOptions": {
  "baseUrl": ".",
  "target": "es6",
  "module": "es2015",
  "moduleResolution": "node",
  "emitDecoratorMetadata": true,
  "experimentalDecorators": true,
  "allowSyntheticDefaultImports": true,
  "sourceMap": true,
  "importHelpers": true,
  "strictNullChecks": false,
  "lib": [
   "es2015",
   "dom"
  ],
  "typeRoots": [
   "node_modules/@types",
   "typings"
  ],
  "types": [
   "hammerjs",
   "node"
  ],
  "paths": {
   "ramster-ui/*": ["lib/ramster-ui/*"]
  }
 },
 "include": [
  "clients/**/*",
  "lib/ramster-ui/**/*"
 ],
 "exclude": [
  "clients/**/*.spec.ts",
  "clients/**/*.e2e.ts"
 ],
 "awesomeTypescriptLoaderOptions": {
  "forkChecker": true,
  "useWebpackText": true
 },
 "angularCompilerOptions": {
  "genDir": "./compiled",
  "skipMetadataEmit": true
 },
 "compileOnSave": false,
 "buildOnSave": false,
 "atom": {
  "rewriteTsconfig": false
 }
}


{
 "extends": "./tsconfig.json",
 "include": [
  "clients/**/polyfills.ts",
  "clients/**/vendor.common.ts",
  "clients/**/vendor.server.ts",
  "clients/**/app.server.ts",
  "clients/**/index.server.ts",
 ],
 "exclude": [
  "clients/**/index.ts",
  "clients/**/vendor.browser.ts",
  "clients/**/app.ts",
  "clients/**/*.spec.ts",
  "clients/**/*.e2e.ts"
 ],
 "awesomeTypescriptLoaderOptions": {
  "forkChecker": true,
  "useWebpackText": true
 },
 "angularCompilerOptions": {
  "genDir": "./compiled",
  "skipMetadataEmit": true
 },
 "compileOnSave": false,
 "buildOnSave": false,
 "atom": {
  "rewriteTsconfig": false
 }
}

// excerpts from my server setup

// this method is called before the server is started
 setup() {
  const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require(path.join(__dirname, '../../../../public/site/serverBuild', 'main'))
  // LAZY_MODULE_MAP is undefined for now
  this.module.app.engine('html', (_, options, callback) => {
   renderModuleFactory(
    AppServerModuleNgFactory, {}
   ).then((html) => callback(null, html), (error) => callback(error))
  })
  this.module.app.set('view engine', 'html')
 }

// the method returned by "loadLayout" is mounted in expressjs
 loadLayout() {
  const {module} = this
  return function* (req, res, next) {
   try {
    res.render(path.join('../../../../public/site/layout.html'), {req, res})
   } catch (e) {
    req.locals.error = e
    next()
   }
  }
 }

// polyfills.ts
import 'core-js/es6'
import 'reflect-metadata'


// vendor.common.ts
import 'rxjs/add/operator/first'
import 'rxjs/add/operator/toPromise'
import 'popper.js'

import '@angular/common'
import '@angular/core'
import '@angular/flex-layout'
import '@angular/forms'
import '@angular/material'
import '@angular/router'


// vendor.server.ts
import 'zone.js/dist/zone-node'
import '@angular/platform-server'


// index.server.ts
import {enableProdMode} from '@angular/core'
if (process.env.NODE_ENV === 'production') {
 enableProdMode()
}
export * from './app.server.ngfactory'


// app.server.ts
import {NgModule} from '@angular/core'
import {ServerModule} from '@angular/platform-server'

@NgModule({
 imports: [
    ServerModule
  ],
  exports: [],
  declarations: [],
  providers: [],
  bootstrap: []
})
class AppServerModule {}

export {AppServerModule}

Вы увидите, что я урезал серверное приложение до самых основ, так что я могу устранить ошибку, вызванную чем-то, что я написал. Любая помощь приветствуется.

0 ответов

ZoneJS не работает должным образом как модуль AMD. Вы используете SystemJS AMD extra, а zone.js связан с форматом UMD - это приводит к тому, что Zone используется в качестве модуля AMD, что вызывает некорректное поведение

Полное объяснение и проблема на zone.js - https://github.com/angular/angular/issues/36827

Минимальная демонстрация проблемы - https://codesandbox.io/s/quirky-antonelli-2kusz?file=/index.html

Обходной путь - https://codesandbox.io/s/sweet-shape-18x7b

Веб-пакет должен быть mode: 'development', Если вы можете воспроизвести проблему с вилкой https://github.com/Angular-RU/angular-universal-starter - может помочь.

Другие вопросы по тегам