Переход с 3 на 4 через Webpack приводит к ошибке предварительного рендеринга - PullRequest
0 голосов
/ 15 мая 2018

После миграции с веб-пакета v3 на v4 при запуске приложения возникает ошибка:

NodeInvocationException: предварительная визуализация не удалась из-за ошибки: Ошибка ссылки: документ не определен в J: \ Проджетти \ Platform \ Software \ CTAgenda \ ClientApp \ расстояние \ основной-server.js: 20159: 42

В main-server.js в строке 20159:

/**
 * Factory service to easily create a `NgbFocusTrap` instance on an element
 */
var NgbFocusTrapFactory = /** @class */ (function () {
    function NgbFocusTrapFactory(_document, _ngZone) {
        this._document = _document;
        this._ngZone = _ngZone;
    }
    /**
     * Create an instance of {@link NgbFocusTrap} and return it
     * @param element HTMLElement to trap focus inside
     * @param autofocus Whether the focustrap should automatically move focus into the trapped element upon
     * initialization and return focus to the previous activeElement upon destruction.
     */
    NgbFocusTrapFactory.prototype.create = function (element, autofocus) {
        if (autofocus === void 0) { autofocus = false; }
        return new NgbFocusTrap(element, autofocus, this._document, this._ngZone);
    };
    NgbFocusTrapFactory = __decorate([
        core_1.Injectable(),
        __param(0, core_1.Inject(common_1.DOCUMENT)),
        __metadata("design:paramtypes", [Document, core_1.NgZone]) <-- ERROR
    ], NgbFocusTrapFactory);
    return NgbFocusTrapFactory;
}());
exports.NgbFocusTrapFactory = NgbFocusTrapFactory;
//# sourceMappingURL=focus-trap.js.map

Глядя в интернет, я понял, что проблема вызвана использованием клиентских компонентов, которые используют документ и окно, на стороне сервера node.js. Из сообщения об ошибке проблема возникает из-за @ ng-bootstrap / ng-bootstrap. Я также попытался переместить его из app.module.shared.ts в app.module.browser.ts, но без решения. Учитывая, что я ничего не изменил в приложении по сравнению с версией 3 веб-пакета, я не могу понять, как решить проблему. Спасибо за помощь.

Мое окружение:

  • .NET Core 2
  • Угловой 6
  • Angular-Cli 6
  • Webpack 4

packege.json

{  
  "name": "aaa",  
  "version": "0.0.0",  
  "license": "MIT",  
  "scripts": {  
    "ng": "ng",  
    "start": "ng serve",  
    "build": "ng build",  
    "test": "ng test",  
    "lint": "ng lint",  
    "e2e": "ng e2e",  
    "buildWPDev": "webpack --config webpack.dev.js",  
    "buildWPProd": "webpack --config webpack.prod.js",  
    "initConfig": "webpack-cli init"  
  },  
  "private": true,  
  "dependencies": {  
    "@angular/animations": "^6.0.0",  
    "@angular/common": "^6.0.0",  
    "@angular/compiler": "^6.0.0",  
    "@angular/core": "^6.0.0",  
    "@angular/forms": "^6.0.0",  
    "@angular/http": "^6.0.0",  
    "@angular/platform-browser": "^6.0.0",  
    "@angular/platform-browser-dynamic": "^6.0.0",  
    "@angular/platform-server": "^6.0.0",  
    "@angular/router": "^6.0.0",  
    "@ng-bootstrap/ng-bootstrap": "^2.0.0",  
    "aspnet-prerendering": "^3.0.1",  
    "aspnet-webpack": "^2.0.3",  
    "bootstrap": "^4.1.1",  
    "css": "2.2.3",  
    "es6-shim": "0.35.3",  
    "event-source-polyfill": "0.0.12",  
    "isomorphic-fetch": "2.2.1",  
    "jquery": "3.3.1",  
    "jwt-decode": "^2.2.0",  
    "material-design-icons": "^3.0.1",  
    "moment": "^2.22.1",  
    "ngx-toastr": "^8.5.0",  
    "nodemailer": "^4.6.4",  
    "popper.js": "^1.14.3",  
    "preboot": "6.0.0-beta.4",  
    "reflect-metadata": "0.1.12",  
    "rxjs": "^6.1.0",  
    "rxjs-compat": "^6.1.0",  
    "stacktrace-js": "^2.0.0",  
    "zone.js": "^0.8.26"  
  },  
  "devDependencies": {  
    "@angular/cli": "^6.0.0",  
    "@angular/compiler-cli": "^6.0.0",  
    "@angular/language-service": "^6.0.0",  
    "@ngtools/webpack": "^6.0.0",  
    "@types/chai": "4.1.3",  
    "@types/jasmine": "2.8.7",  
    "@types/jasminewd2": "~2.0.3",  
    "@types/node": "~10.0.4",  
    "@types/stacktrace-js": "0.0.32",  
    "@types/webpack-env": "^1.13.6",  
    "angular-router-loader": "^0.8.5",  
    "angular2-template-loader": "^0.6.2",  
    "chai": "4.1.2",  
    "codelyzer": "~4.3.0",  
    "css-loader": "^0.28.11",  
    "file-loader": "^1.1.11",  
    "html-loader": "^0.5.5",  
    "jasmine-core": "3.1.0",  
    "jasmine-spec-reporter": "~4.2.1",  
    "karma": "2.0.0",  
    "karma-chai": "0.1.0",  
    "karma-chrome-launcher": "2.2.0",  
    "karma-cli": "1.0.1",  
    "karma-coverage-istanbul-reporter": "^1.4.1",  
    "karma-jasmine": "1.1.1",  
    "karma-jasmine-html-reporter": "^0.2.2",  
    "karma-webpack": "^3.0.0",  
    "mini-css-extract-plugin": "^0.4.0",  
    "ng-router-loader": "^2.1.0",  
    "protractor": "~5.3.1",  
    "style-loader": "^0.21.0",  
    "to-string-loader": "^1.1.5",  
    "ts-loader": "^4.3.0",  
    "ts-node": "~6.0.3",  
    "tslint": "^5.10.0",  
    "typescript": "^2.7.2",  
    "uglifyjs-webpack-plugin": "^1.2.5",  
    "url-loader": "^1.0.1",  
    "webpack": "^4.8.1",  
    "webpack-cli": "^2.1.3",  
    "webpack-hot-middleware": "^2.22.1",  
    "webpack-merge": "^4.1.2"  
  }  
}  

webpack.config.vendor.js

const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const merge = require('webpack-merge');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const treeShakableModules = [
    '@angular/animations',
    '@angular/common',
    '@angular/compiler',
    '@angular/core',
    '@angular/forms',
    '@angular/http',
    '@angular/platform-browser',
    '@angular/platform-browser-dynamic',
    '@angular/router',
    'zone.js'
];

const nonTreeShakableModules = [
    'bootstrap',
    'bootstrap/dist/css/bootstrap.css',
    'es6-shim',
    'event-source-polyfill',
    "ngx-toastr",
    "ngx-toastr/toastr.css",
    'jquery'
];

const allModules = treeShakableModules.concat(nonTreeShakableModules);

module.exports = (env) => {
    const isDevBuild = !(env && env.prod);

    const sharedConfig = {
        mode: isDevBuild ? 'development' : 'production',
        stats: {
            modules: false
        },
        resolve: {
            extensions: ['.js']
        },
        module: {
            rules: [{
                test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/,
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 25000,
                        fallback: 'file-loader'
                    }
                }]
            }]
        },
        output: {
            publicPath: 'dist/',
            filename: '[name].js',
            library: '[name]_[hash]'
        },
        plugins: [
            new webpack.ProvidePlugin({
                $: 'jquery',
                jQuery: 'jquery'
            }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable)
            //new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/11580
            new webpack.ContextReplacementPlugin(/(.+)?angular(\\|\/)core(.+)?/, path.resolve(__dirname, './ClientApp')) //,
            //new webpack.IgnorePlugin(/^vertx$/) // Workaround for https://github.com/stefanpenner/es6-promise/issues/100
        ]
    };

    const clientBundleConfig = merge(sharedConfig, {
        entry: {
            // To keep development builds fast, include all vendor dependencies in the vendor bundle.
            // But for production builds, leave the tree-shakable ones out so the AOT compiler can produce a smaller bundle.
            vendor: isDevBuild ? allModules : nonTreeShakableModules
        },
        output: {
            path: path.join(__dirname, 'wwwroot', 'dist')
        },
        module: {
            rules: [{
                test: /\.css(\?|$)/,
                use: [{
                        loader: MiniCssExtractPlugin.loader
                    },
                    {
                        loader: 'css-loader'
                    }
                ]
            }]
        },
        plugins: [
            new MiniCssExtractPlugin({
                // Options similar to the same options in webpackOptions.output
                // both options are optional
                filename: "vendor.css"
            }),
            new webpack.DllPlugin({
                context: __dirname,
                path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'),
                name: '[name]_[hash]'
            })
        ].concat(isDevBuild ? [] : [
            new UglifyJsPlugin()
        ])
    });

    const serverBundleConfig = merge(sharedConfig, {
        target: 'node',
        resolve: {
            mainFields: ['main']
        },
        entry: {
            vendor: allModules.concat(['aspnet-prerendering'])
        },
        output: {
            path: path.join(__dirname, 'ClientApp', 'dist'),
            libraryTarget: 'commonjs2',
        },
        module: {
            rules: [{
                test: /\.css(\?|$)/,
                use: ['to-string-loader', 'css-loader']
            }]
        },
        plugins: [
            new webpack.DllPlugin({
                context: __dirname,
                path: path.join(__dirname, 'ClientApp', 'dist', '[name]-manifest.json'),
                name: '[name]_[hash]'
            })
        ]
    });

    return [clientBundleConfig, serverBundleConfig];
}  

webpack.common.js

module.exports = {  
    output: {  
        filename: '[name].js',  
        publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix  
    },  
    resolve: {  
        extensions: ['.js', '.ts']  
    },  
    stats: {  
        modules: false  
    },  
    context: __dirname,  
    module: {  
        rules: [  
            {  
                test: /\.(png|jpg|jpeg|gif|svg)$/,  
                use: [{  
                    loader: 'url-loader',  
                    options: {  
                        limit: 25000,  
                        fallback: 'file-loader'  
                    }  
                }]  
            },  
            {  
                test: /\.html$/,  
                use: {  
                    loader: 'html-loader',  
                    options: {  
                        minimize: false  
                    }  
                }  
            }  
        ]  
    }  
};   

webpack.dev.js

const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

// Configuration in common to both client-side and server-side bundles
const clientBundleOutputDir = './wwwroot/dist';
const serverBundleOutputDir = './ClientApp/dist';

const sharedConfig = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['to-string-loader', 'css-loader']
            },
            {
                test: /\.ts$/,
                include: /ClientApp/,
                use: [{
                        loader: 'ng-router-loader'
                    },
                    {
                        loader: 'ts-loader',
                        options: {
                            silent: true
                        }
                    },
                    {
                        loader: 'angular2-template-loader'
                    }
                ]
            },
            {
                test: /\.(ts|js)$/,
                include: /ClientApp/,
                use: {
                    loader: 'angular-router-loader'
                }
            }
        ]
    },
    mode: 'development'
};

// Configuration for client-side bundle suitable for running in browsers
const clientConfig = merge(sharedConfig, {
    entry: {
        'main-client': './ClientApp/boot.browser.ts'
    },
    output: {
        path: path.join(__dirname, clientBundleOutputDir)
    },
    plugins: [
        new webpack.DllReferencePlugin({
            context: __dirname,
            manifest: require('./wwwroot/dist/vendor-manifest.json')
        })
    ].concat([
        // Plugins that apply in development builds only
        new webpack.SourceMapDevToolPlugin({
            filename: '[file].map', // Remove this line if you prefer inline source maps
            moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
        })
    ])
});

// Configuration for server-side (prerendering) bundle suitable for running in Node
const serverConfig = merge(sharedConfig, {
    entry: {
        'main-server': './ClientApp/boot.server.ts'
    },
    output: {
        libraryTarget: 'commonjs',
        path: path.join(__dirname, serverBundleOutputDir)
    },
    resolve: {
        mainFields: ['main']
    },
    target: 'node',
    devtool: 'inline-source-map',
    plugins: [
        new webpack.DllReferencePlugin({
            context: __dirname,
            manifest: require('./ClientApp/dist/vendor-manifest.json'),
            sourceType: 'commonjs2',
            name: './vendor'
        })
    ]
});

module.exports = () => {
    const clientBundleConfig = merge(commonConfig, clientConfig);
    const serverBundleConfig = merge(commonConfig, serverConfig);
    return [clientBundleConfig, serverBundleConfig];
};  

startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
            {
                HotModuleReplacement = true,
                ConfigFile = "webpack.dev.js"
            });
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();
        app.UseAuthentication();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");

            routes.MapSpaFallbackRoute(
                name: "spa-fallback",
                defaults: new { controller = "Home", action = "Index" });
        });
    } 

index.cshtml

@{
    ViewData["Title"] = "Home Page";
}

<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>

<script src="~/dist/vendor.js" asp-append-version="true"></script>
@section scripts {
    <script src="~/dist/main-client.js" asp-append-version="true"></script>
}

boot.server.ts

import { enableProdMode, ApplicationRef, NgZone } from '@angular/core';
import { APP_BASE_HREF } from '@angular/common';
import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
import { first } from 'rxjs/operators';
import 'reflect-metadata';
import 'zone.js';

import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

import { AppModule } from './app/app.module.server';

enableProdMode();

export default createServerRenderer(params => {
    const providers = [
        { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
        { provide: APP_BASE_HREF, useValue: params.baseUrl },
        { provide: 'BASE_URL', useValue: params.origin + params.baseUrl },
    ];

    return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
        const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
        const state = moduleRef.injector.get(PlatformState);
        const zone: NgZone = moduleRef.injector.get(NgZone);

        return new Promise<RenderResult>((resolve, reject) => {
            zone.onError
                .subscribe((errorInfo: any) => reject(errorInfo));
            appRef.isStable
                .pipe(
                    first(isStable => isStable)
                )
                .subscribe(() => {
                    // Because 'onStable' fires before 'onError', we have to delay slightly before
                    // completing the request in case there's an error to report
                    setImmediate(() => {
                        resolve({
                            html: state.renderToString()
                        });
                        moduleRef.destroy();
                    });
                });
        });
    });
});

app.module.shared.ts

import { CommonModule } from '@angular/common';
import { ErrorHandler } from '@angular/core';
import { NgModule } from '@angular/core';
import { NgbModule, NgbDateAdapter, NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap';
import { ToastrModule, ToastContainerModule } from 'ngx-toastr';

import { AppRoutingModule } from './app-routing.module';
import { AppErrorHandler } from './app.error-handler';
import { AppSessionStorage } from './models/shared.model';

import { AppComponent } from './components/app/app.component';

import { ClinicalTrialsModule } from './components/clinicalTrials/clinicalTrials.module';
import { HomeModule } from './components/home/home.module';
import { ErrorModule } from './components/error/error.module';
import { LoginModule } from './components/login/login.module';
import { NavMenuModule } from './components/navmenu/navmenu.module';
import { ToolbarModule } from './components/toolbar/toolbar.module';
import { TrackingsModule } from './components/trackings/trackings.module';

import { GuardsModule } from './guards/guards.module';

import { ServicesModule } from './services/services.module';

import { InterceptorsModule } from './interceptors/interceptors.module';

import { NgbDateNativeAdapter } from './extensions/ngbDateAdapter.extension';
import { NgbDateGeneralParserFormatter } from './extensions/ngbDateParserFormatter.extension';

@NgModule({
    imports: [
        CommonModule,
        GuardsModule,
        InterceptorsModule,
        ServicesModule,
        NavMenuModule,
        ToolbarModule,
        HomeModule, /* with Routing */
        ErrorModule, /* with Routing */
        LoginModule, /* with Routing */
        ClinicalTrialsModule, /* with Routing */
        TrackingsModule,
        AppRoutingModule, /* with Routing */
        ToastrModule.forRoot({
            positionClass: 'toast-top-center',
            timeOut: 7000,
            preventDuplicates: true
        }),
        ToastContainerModule,
        NgbModule.forRoot()
    ],
    declarations: [
        AppComponent
    ],
    providers: [
        { provide: ErrorHandler, useClass: AppErrorHandler },
        { provide: AppSessionStorage, useValue: { getItem() { } } },
        { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter },
        { provide: NgbDateParserFormatter, useClass: NgbDateGeneralParserFormatter }
    ]
})
export class AppModuleShared {
}

app.module.server.ts

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModuleShared } from './app.module.shared';

import { AppComponent } from './components/app/app.component';

@NgModule({
    bootstrap: [ AppComponent ],
    imports: [
        ServerModule,
        AppModuleShared
    ]
})
export class AppModule {
}

1 Ответ

0 голосов
/ 18 мая 2018

Я нашел источник проблемы: на момент написания статьи @ ng-bootstrap / ng-bootstrap не полностью совместим с последней версией Angular 6.

Как подсказывает ЗоранIvancevic (@zolakt) можно использовать обходной путь, добавив новый webpack.DefinePlugin ({'Document': null}) в файл конфигурации webpack.

Источник: https://github.com/ng-bootstrap/ng-bootstrap/issues/858

...