web-dev-qa-db-ja.com

サーバー側のレンダリング。 WebAPIとAngular 2

ASP.NET Core Web APIAngular 4を使用して構築されたWebアプリケーションを開発しました。私のモジュールバンドラーはWeb Pack 2です。

Facebook、Twitter、Googleでアプリケーションをクロール可能またはリンク共有可能にしたい。一部のユーザーが私のニュースをFacebookに投稿しようとする場合、urlは同じである必要があります。たとえば、JonはFacebookでURL --http://myappl.com/#/hellopageを使用してページを共有したい場合、Jonは次のリンクをFacebookに挿入します:http://myappl.com/#/hellopage

タグヘルパーなしのAngular Universalサーバーサイドレンダリング のこのチュートリアルを見てきましたが、サーバーサイドレンダリングを作成したいと思います。 ASP.NET Core Web APIを使用していて、Angular 4アプリケーションに.cshtmlビューがないため、コントローラーからViewData["SpaHtml"]を介してビューにデータを送信できません。

ViewData["SpaHtml"] = prerenderResult.Html;

さらに、 Angular Universal のこのGoogleチュートリアルですが、ASP.NET CoreではなくNodeJSサーバーを使用しています。

サーバー側の事前レンダリングを使用したいと思います。私はこの方法でメタタグを追加しています:

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 WebAPIおよびAngular 2)でサーバー側のレンダリングを行う別の方法はありますか?

githubリポジトリへの例 をアップロードしました。

11
StepUp

AngularにHTML5スタイルのURL(ハッシュなし)を使用するオプションがあります: LocationStrategyおよびブラウザーのURLスタイル 。このURLスタイルを選択する必要があります。共有したいoFacebookは、参照したチュートリアルに示すようにページ全体をレンダリングする必要があります。サーバーに完全なURLがあると、対応するビューをレンダリングしてHTMLを返すことができます。

@DávidMolnárによって提供されたコードは、この目的には非常にうまく機能する可能性がありますが、私はまだ試していません。

更新:

まず、サーバーの事前レンダリングを機能させるために、サーバーへのルート情報の送信を妨げるuseHash: trueを使用しないでください。

デモでは ASP.NET Core + Angular 2ユニバーサルアプリ 参照したGitHubの問題で言及されましたが、ASP.NET CoreMVCコントローラーとビューはサーバーにのみ使用されますAngularからより便利な方法で事前レンダリングされたHTML。アプリケーションの残りの部分では、WebAPIのみが.NET Coreの世界から使用されます。それ以外はすべてAngularおよび関連するWebテクノロジー。

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内のすべてのルートを処理し、対応するangular route:

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

開始を容易にするために、そのデモプロジェクトを取得し、アプリケーションに合うように変更することを検討してください。

更新2:

NodeServicesでRazorのようなASP.NETMVCから何も使用する必要がない場合は、Node.jsサーバーでサーバーを事前レンダリングするHost Universal AngularアプリとHostASPを使用する方が自然です。 .NET Web Apiを個別に使用して、Angular UIが異なるサーバー上のAPIにアクセスできるようにします。APIから独立して静的ファイルをホストする(場合によってはサーバーの事前レンダリングを利用する)のは非常に一般的なアプローチだと思います。

これがUniversal Angular Node.jsでホストされている)のスターターリポジトリです: https://github.com/angular/universal-starter

そして、UIとWeb APIを異なるサーバーでホストする方法の例を次に示します: https://github.com/thinktecture/nodejs-aspnetcore-webapi 。 APIURLがurlService.tsでどのように構成されているかに注目してください。

また、UIサーバーとAPIサーバーの両方をリバースプロキシの背後に隠して、両方が同じパブリックドメインとホストを介してアクセスできるようにし、ブラウザーで機能させるためにCORSを処理する必要がないようにすることも検討できます。

4
Andrii Litvinov

リンクされたチュートリアルに基づいて、コントローラーから直接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>";
    }
}

HTMLコンテンツを返すことができるProduces属性に注意してください。 this の質問を参照してください。

2
Dávid Molnár