我最近将我公司的网站从 React 移到了 Angular,因为我们的大多数项目已经在 Angular 7 上。作为“使用最新和最优秀”的人,我决定实现服务器端渲染以获得接近 100/100(目前为 42/100)的 google 页面速度评级。我在本周的大部分时间里一直在修补它,但没有成功——最近的障碍对我来说尤其难以克服。这是有关我的设置的简要信息,然后我将详细介绍:
- 节点JS 8.9.1
- Angular 7 最新版本
- Webpack 4.26.0
- @ngtools/webpack 7.0.5
- 不使用 angular-cli
- AOT 设置
- 单页应用
这是我在尝试渲染为 SSR 设置的 layout.html 文件时遇到的错误:
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() 时,我们得到“Cannot read property 'subscribe' of undefined”。我继续挖掘堆栈跟踪 - 这是上一步,调用 new 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)))();
}
}
这是我们对错误原因的一些了解 - 在开关的“默认”块中。如果我们在 for 循环之后 console.log(deps[0], depValues[0]),也就是 _zone,我们会得到有趣的东西:
// deps[0]
{ flags: 0,
token:
{ [Function: NgZone]
isInAngularZone: [Function],
assertInAngularZone: [Function],
assertNotInAngularZone: [Function] },
tokenKey: 'NgZone_29' }
// depValues[0]
{}
所以,这就是罪魁祸首,我想!'resolveNgModuleDep' 搞砸了!所以我继续挖掘:
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 为空。返回的对象只是 {},因此后面的所有麻烦。据我所知,我一直在来回尝试从这里解决它,但无济于事。相信我,我已经在 stackoverflow 和谷歌中搜索了一遍又一遍;我已经阅读了 1000 篇中等帖子 - 没有成功。我不使用 angular-cli 因为我喜欢自定义我的 webpack 配置等等,但我怀疑这就是原因,因为 angular-cli 本身在后台使用 webpack。我将在下面的几个片段中粘贴一些额外的东西——我的 webpack 配置,
// 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}
您会看到我已将服务器端应用程序精简为最基本的,因此我可以消除由我编写的内容引起的错误。任何帮助将非常感激。