Рендеринг на стороне сервера. Веб-API и Angular 2

Я разработал веб-приложение, созданное с использованием ASP.NET Core Web API и Angular 4. Мой сборщик модулей Web Pack 2.

Я хочу, чтобы мое приложение можно было сканировать или делиться ссылками в Facebook, Twitter, Google. url должно быть таким же, когда какой-то пользователь пытается опубликовать мои новости на Facebook. Например, Джон хочет поделиться страницей с URL-адресом - http://myappl.com/#/hellopage в Facebook, затем Джон вставляет эту ссылку в Facebook: http://myappl.com/#/hellopage.

Я видел это руководство по рендерингу на стороне сервера Angular Universal без помощника по тегам и хотел бы сделать рендеринг на стороне сервера. Поскольку я использую ASP.NET Core Web API, а мое приложение Angular 4 не имеет представлений .cshtml, поэтому я не могу отправлять данные из контроллера для просмотра через ViewData["SpaHtml"] с моего контроллера:

ViewData["SpaHtml"] = prerenderResult.Html;

Кроме того, я вижу это руководство Google по Angular Universal, но они используют сервер NodeJS, а не ASP.NET Core.

Я хотел бы использовать пререндеринг на стороне сервера. Я добавляю метатеги следующим образом:

import { Meta } from '@angular/platform-browser';

constructor(
    private metaService: Meta) {
}

let newText = "Foo data. This is test data!:)";
    //metatags to publish this page at social nets
    this.metaService.addTags([
        // Open Graph data
        { property: 'og:title', content: newText },
        { property: 'og:description', content: newText },        { 
        { property: "og:url", content: window.location.href },        
        { property: 'og:image', content: "http://www.freeimageslive.co.uk/files
                                /images004/Italy_Venice_Canal_Grande.jpg" }]);

и когда я проверяю этот элемент в браузере, он выглядит так:

<head>    
    <meta property="og:title" content="Foo data. This is test data!:)">    
    <meta property="og:description" content="Foo data. This is test data!:)">
    <meta name="og:url" content="http://foourl.com">
    <meta property="og:image" content="http://www.freeimageslive.co.uk/files
/images004/Italy_Venice_Canal_Grande.jpg"">    
</head>

Я загружаю приложение обычным способом:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

и мой конфиг webpack.config.js выглядит так:

var path = require('path');

var webpack = require('webpack');

var ProvidePlugin = require('webpack/lib/ProvidePlugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var CleanWebpackPlugin = require('clean-webpack-plugin');
var WebpackNotifierPlugin = require('webpack-notifier');

var isProd = (process.env.NODE_ENV === 'production');

function getPlugins() {
    var plugins = [];

    // Always expose NODE_ENV to webpack, you can now use `process.env.NODE_ENV`
    // inside your code for any environment checks; UglifyJS will automatically
    // drop any unreachable code.
    plugins.push(new webpack.DefinePlugin({
        'process.env': {
            'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
        }
    }));

    plugins.push(new webpack.ProvidePlugin({
        jQuery: 'jquery',
        $: 'jquery',
        jquery: 'jquery'
    }));
    plugins.push(new CleanWebpackPlugin(
        [
            './wwwroot/js',
            './wwwroot/fonts',
            './wwwroot/assets'
        ]
    ));

    return plugins;
}


module.exports = {

    devtool: 'source-map',

    entry: {
        app: './persons-app/main.ts' // 
    },

    output: {
        path: "./wwwroot/",
        filename: 'js/[name]-[hash:8].bundle.js',
        publicPath: "/"
    },

    resolve: {
        extensions: ['.ts', '.js', '.json', '.css', '.scss', '.html']
    },

    devServer: {
        historyApiFallback: true,
        stats: 'minimal',
        outputPath: path.join(__dirname, 'wwwroot/')
    },

    module: {
        rules: [{
                test: /\.ts$/,
                exclude: /node_modules/,
                loader: 'tslint-loader',
                enforce: 'pre'
            },
            {
                test: /\.ts$/,
                loaders: [
                    'awesome-typescript-loader',
                    'angular2-template-loader',

                    'angular-router-loader',

                    'source-map-loader'
                ]
            },
            {
                test: /\.js/,
                loader: 'babel',
                exclude: /(node_modules|bower_components)/
            },
            {
                test: /\.(png|jpg|gif|ico)$/,
                exclude: /node_modules/,
                loader: "file?name=img/[name].[ext]"
            },
            {
                test: /\.css$/,
                exclude: /node_modules/,                
                use: ['to-string-loader', 'style-loader', 'css-loader'],
            },
            {
                test: /\.scss$/,
                exclude: /node_modules/,
                loaders: ["style", "css", "sass"]
            },
            {
                test: /\.html$/,
                loader: 'raw'
            },
            {
                test: /\.(eot|svg|ttf|woff|woff2|otf)$/,
                loader: 'file?name=fonts/[name].[ext]'
            }
        ],
        exprContextCritical: false
    },
    plugins: getPlugins()

};

Можно ли сделать рендеринг на стороне сервера без ViewData? Есть ли альтернативный способ рендеринга на стороне сервера в ASP.NET Core Web API и Angular 2?

Я загрузил пример в репозиторий github.


person StepUp    schedule 05.06.2017    source источник
comment
Вы уже пробовали github.com/aspnet/JavaScriptServices?   -  person Andrii Litvinov    schedule 09.06.2017
comment
@AndriiLitvinov да, в этом уроке необходимо использовать ViewData для отправки html в .cshtml представление, но я использую только .html представления, а не .cshtml представления.   -  person StepUp    schedule 09.06.2017
comment
Хорошо, как вы думаете, зачем вам нужно представление cshtml? Почему бы просто не вернуть prerenderResult.Html из действия?   -  person Andrii Litvinov    schedule 09.06.2017
comment
@AndriiLitvinov Я прочитал это: For example, open Views/Home/Index.cshtml, and add markup like the following: и это Now go to your Views/_ViewImports.cshtml file, and add the following line: @addTagHelper "*, Microsoft.AspNetCore.SpaServices" здесь: github.com/aspnet/JavaScriptServices/tree/dev/src/   -  person StepUp    schedule 09.06.2017
comment
жирным шрифтом вы говорите, что URL-адрес должен быть одинаковым, но затем пример URL-адреса отличается. Вы имели в виду http://myappl.com/#/hello вместо http://myappl.com/#/hellopage?   -  person quetzalcoatl    schedule 16.06.2017
comment
@quetzalcoatl, ты прав!   -  person StepUp    schedule 16.06.2017


Ответы (2)


В Angular есть возможность использовать URL-адреса в стиле HTML5 (без хэшей): Стратегия местоположения и стили URL браузера. Вы должны выбрать этот стиль URL. И для каждого URL-адреса, которым вы хотите поделиться на Facebook, вам нужно отобразить всю страницу, как показано в руководстве, на которое вы ссылаетесь. Имея полный URL-адрес на сервере, вы можете отображать соответствующий вид и возвращать HTML.

Код, предоставленный @DávidMolnár, может очень хорошо подойти для этой цели, но я еще не пробовал.

ОБНОВЛЕНИЕ:

Во-первых, чтобы предварительный рендеринг сервера работал, вы не должны использовать useHash: true, который предотвращает отправку информации о маршруте на сервер.

В демоверсии универсального приложения ASP.NET Core + Angular 2, упомянутого в выпуске GitHub. вы упомянули, ASP.NET Core MVC Controller и View используются только для сервера, предварительно отображающего HTML из Angular более удобным способом. Для остальной части приложения используется только WebAPI из мира .NET Core, все остальное — Angular и связанные с ним веб-технологии.

Удобно использовать представление Razor, но если вы категорически против этого, вы можете напрямую жестко закодировать HTML в действие контроллера:

[Produces("text/html")]
public async Task<string> Index()
{
    var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>();
    var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>();

    var applicationBasePath = hostEnv.ContentRootPath;
    var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>();
    var unencodedPathAndQuery = requestFeature.RawTarget;
    var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}";

    TransferData transferData = new TransferData();
    transferData.request = AbstractHttpContextRequestInfo(Request);
    transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)";

    var prerenderResult = await Prerenderer.RenderToString(
        "/",
        nodeServices,
        new JavaScriptModuleExport(applicationBasePath + "/Client/dist/main-server"),
        unencodedAbsoluteUrl,
        unencodedPathAndQuery,
        transferData,
        30000,
        Request.PathBase.ToString()
    );

    string html = prerenderResult.Html; // our <app> from Angular
    var title = prerenderResult.Globals["title"]; // set our <title> from Angular
    var styles = prerenderResult.Globals["styles"]; // put styles in the correct place
    var meta = prerenderResult.Globals["meta"]; // set our <meta> SEO tags
    var links = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags

    return $@"<!DOCTYPE html>
<html>
<head>
<base href=""/"" />
<title>{title}</title>

<meta charset=""utf-8"" />
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"" />
{meta}
{links}

<link rel=""stylesheet"" href=""https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/0.8.2/css/flag-icon.min.css"" />

{styles}

</head>
<body>
{html}

<!-- remove if you're not going to use SignalR -->
<script src=""https://code.jquery.com/jquery-2.2.4.min.js""
        integrity=""sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=""
        crossorigin=""anonymous""></script>

<script src=""http://ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.2.0.min.js""></script>

<script src=""/dist/main-browser.js""></script>
</body>
</html>";   
}

Обратите внимание, что резервный URL-адрес используется для обработки всех маршрутов в HomeController и отображения соответствующего углового маршрута:

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

Чтобы упростить начало работы, возьмите этот демонстрационный проект и измените его, чтобы он соответствовал вашему приложению.

ОБНОВЛЕНИЕ 2:

Если вам не нужно использовать что-либо из ASP.NET MVC, например Razor с NodeServices, мне кажется более естественным разместить приложение Universal Angular с предварительным рендерингом сервера на сервере Node.js. И размещайте ASP.NET Web Api независимо, чтобы Angular UI мог получить доступ к API на другом сервере. Я думаю, что это довольно распространенный подход к размещению статических файлов (и в случае использования предварительного рендеринга сервера) независимо от API.

Вот начальный репозиторий Universal Angular, размещенный на Node.js: https://github.com/angular/universal-starter.

А вот пример того, как пользовательский интерфейс и веб-API могут размещаться на разных серверах: https://github.com/thinktecture/nodejs-aspnetcore-webapi. Обратите внимание, как URL-адрес API настроен в urlService.ts.

Также вы можете скрыть сервер пользовательского интерфейса и API за обратным прокси-сервером, чтобы к обоим можно было получить доступ через один и тот же общедоступный домен и хост, и вам не нужно было иметь дело с CORS, чтобы заставить его работать в браузере.

person Andrii Litvinov    schedule 09.06.2017
comment
@StepUp, я обновил свой ответ, теперь он имеет больше смысла. - person Andrii Litvinov; 11.06.2017
comment
На мой взгляд, нет никакой разницы между вашим ответом и ответом Давида Молнара. - person StepUp; 11.06.2017
comment
@StepUp, достаточно честно. Во всяком случае, вы просили пример рендеринга на стороне сервера, и он есть. Каким образом это не работает для вас? - person Andrii Litvinov; 12.06.2017
comment
потому что у меня есть много html-страниц для публикации, и если я использую этот способ, было бы очень плохо отображать каждую страницу таким образом. - person StepUp; 13.06.2017
comment
@StepUp, зачем вам нужно предварительно визуализировать его из ASP.NET Core? Вы можете настроить сервер nodejs, который будет обрабатывать предварительно обработанный HTML-код и получать доступ к API-интерфейсам, предоставляемым ASP.NET Core WebAPI. Не будет ли легче? Если нет, то я считаю, что можно запустить invoke nodeService github. com/aspnet/JavaScriptServices/tree/dev/src/ напрямую для обработки запроса и возврата обработанного HTML из ASP.NET Core. Хотя кажется, что это вызывает больше накладных расходов. Как ты думаешь? - person Andrii Litvinov; 13.06.2017
comment
Как мне это сделать? Я имею в виду nodejs, которые могут предварительно отображать HTML и получать доступ к API? - person StepUp; 13.06.2017
comment
@StepUp, вот угловой универсальный стартер github.com/angular/universal-starter. Который использует узел для предварительного рендеринга. Он может получить доступ к веб-API так же, как и браузер - делать настоящие HTTP-запросы. Но запросы будут выполняться в той же сети или даже на одном сервере и будут достаточно быстрыми. Таким образом, node и веб-API будут двумя разными приложениями, которые работают в разных процессах. - person Andrii Litvinov; 13.06.2017
comment
но как я могу работать с ними (NodeJ и ASP.NET Core) вместе, используя мою конфигурацию? - person StepUp; 13.06.2017
comment
@StepUp, трудно сказать точно, потому что я не вижу вашего приложения, но я бы подумал о том, чтобы сделать угловой пользовательский интерфейс, размещенный на nodejs, одним независимым приложением (обслуживает html, js, css, выполняет предварительный рендеринг сервера) и ASP.NET Core Веб-API — еще одно независимое приложение (которое предоставляет только http API). Это немного усложнит хостинг и развертывание, но обеспечит четкое разделение задач. - person Andrii Litvinov; 13.06.2017
comment
Не могли бы вы показать несколько руководств, где можно увидеть, как можно развернуть независимое приложение Angular 2 с помощью nodejs и другого независимого приложения ASP.NET Core WebAPI, которые работают вместе? - person StepUp; 15.06.2017
comment
@StepUp, я нашел репозиторий github.com/thinktecture/nodejs-aspnetcore-webapi это показывает, как один и тот же API может быть создан как с nodejs, так и с webapi и использован угловым пользовательским интерфейсом. В urlService.ts вы просто указываете, какой URL вы хотите использовать для доступа к API. Таким образом, снова будут развернуты два независимых сервера — один для обслуживания html, js, css и т. д. с узлом и один — веб-API asp.net. Вы можете разместить оба за обратным прокси-сервером, чтобы вам не нужно было настраивать CORS. - person Andrii Litvinov; 15.06.2017
comment
Не стесняйтесь отвечать на ваши рекомендации по отдельному развертыванию со ссылкой на github, и я отмечу это как ответ. - person StepUp; 15.06.2017
comment
@StepUp, отлично, я обновил свой ответ. Надеюсь, это поможет найти подход, который лучше всего подходит для вас. - person Andrii Litvinov; 16.06.2017

Основываясь на ваших связанных учебниках, вы можете вернуть HTML непосредственно из контроллера.

Предварительно обработанная страница будет доступна по адресу http://<host>:

[Route("")]
public class PrerenderController : Controller
{
    [HttpGet]
    [Produces("text/html")]
    public async Task<string> Get()
    {
        var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>();
        var unencodedPathAndQuery = requestFeature.RawTarget;
        var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}";
        var prerenderResult = await Prerenderer.RenderToString(
            hostEnv.ContentRootPath,
            nodeServices,
            new JavaScriptModuleExport("ClientApp/dist/main-server"),
            unencodedAbsoluteUrl,
            unencodedPathAndQuery,
            /* custom data parameter */ null,
            /* timeout milliseconds */ 15 * 1000,
            Request.PathBase.ToString()
        );
        return @"<html>..." + prerenderResult.Html + @"</html>";
    }
}

Обратите внимание на атрибут Produces, который позволяет возвращать HTML-контент. См. этот вопрос.

person Dávid Molnár    schedule 09.06.2017
comment
Как я могу использовать этот URL-адрес для SEO? Например, адрес браузера моей страницы http://myappl.com/#/hello, но SEO должен искать другой адрес (http://myappl.com/api/prerenderer)? - person StepUp; 09.06.2017
comment
@StepUp, похоже, ваш вопрос не отражает того, что вы хотите. Попробуйте объяснить лучше, чего вы хотите достичь. Невозможно заставить его работать с хэшами в URL-адресе, потому что браузер не отправляет часть URL-адреса после # на сервер. Все, что сервер получит от http://myappl.com/#/hello, это http://myappl.com/. - person Andrii Litvinov; 09.06.2017
comment
@AndriiLitvinov Я хочу сделать мое приложение доступным для сканирования или обмена ссылками в Facebook, Twitter, Google. Для этого я использую метатеги constructor( private metaService: Meta) {} в mypage.component.ts, однако страница недоступна для сканирования и совместного использования. через Фейсбук. Твиттер или другие социальные сети. - person StepUp; 09.06.2017
comment
Теперь я понимаю, чего ты хочешь. Вам потребуется динамически создать HTML-страницу для http://myappl.com. Теперь вы обслуживаете файл HTML, сгенерированный Webpack/Angular. Это статический файл HTML. Вам нужно вернуть динамический файл HTML с содержимым prerenderResult.Html + ваш HTML. Обычно вы делаете это в файле .cshtml. Но у вас его нет... В качестве грязного обходного пути вы можете объединить <html>... с prerenderResult.Html, но это не очень хорошо. - person Dávid Molnár; 09.06.2017
comment
это мне не подходит. Я не могу изменить адрес страницы. url должен быть таким же, когда какой-то пользователь пытается опубликовать мои новости на Facebook. Например, Джон хочет поделиться страницей с URL-адресом - http://myappl.com/#/hellopage в Facebook, тогда Джон вставляет эту ссылку в Facebook: http://myappl.com/#/hello. - person StepUp; 09.06.2017
comment
@StepUp, я не понимаю, как этого можно будет добиться, если вы не можете изменить URL-адрес, а Facebook не может запускать скрипты. Было бы неплохо увидеть хороший ответ. Я также постараюсь исследовать немного больше в свободное время. - person Andrii Litvinov; 09.06.2017