深入研究 Angular 和 ASP.NET Core 3.0

阅读数:1280 2019 年 11 月 25 日 08:00

深入研究Angular和ASP.NET Core 3.0

本文要点:

  • 可以把多个 Angular 应用程序集成到 ASP.NET 网站中
  • 把 Angular 代码打包成 Web 组件是引导 Angular 应用程序的好方法
  • 可以把用 Angular 编写的 Web 组件轻松地集成到 ASP.NET 视图中
  • 把 Angular 解决方案构造成 Angular 应用程序的集合以实现更好的代码重用
  • ASP.NET 结合使用 Angular 是创建 web 应用程序的强大平台

本文是我们的.NET 教育系列文章的一部分,这个系列探索了该技术的优势,设法做到不仅有助于传统的.NET 开发人员,而且有助于所有需要把健壮、高性能和经济的解决方案推向市场的技术人员。

随着.NET Core 3.0 的发布,微软拥有了通用、模块化、跨平台和开源平台的下一个重要版本,.NET Core 最初发布于 2016 年。创建.NET Core 的最初目的是为了下一代的 ASP.NET 解决方案,但是现在,其驱动并成为很多其他场景的基础,这些场景包括物联网、云和下一代移动解决方案。.NET Core 3.0 版添加了大量经常需要功能,如对 WinForms、WPF 和 Entity Framework 6 的支持。

挑战

开始使用 Angular 和 ASP.NET Core 的最简单的方法是,使用微软提供的 Visual Studio 模板。该模板可以让我们迅速启动并运行,但有一个很大的限制——Angular 接管了 UI,把 ASP.NET 保留在后台,并提供 API。如果我们希望.NET 服务某些页面而 Angular 服务其他页面,那么,我们需要在 ASP.NET Core 和 Angular 中都复制外观和菜单结构。或者,我们可以让单个 Angular 应用程序服务整个 UI,但接下来,我们必须在 Angular SPA 中实现所有的页面,包括琐碎的静态内容,如 Contact As、Licensing 等等。

我想要的设置是一个充当门户的 ASP.NET 站点,然后把 Angular 工件嵌入 ASP.NET 页面。一般来说,有两种架构设计模式。在第一种设计模式下,我有一个带路由的 Angular 应用程序,想嵌入到 ASP.NET 视图中,其中具有 Angular 提供的子菜单和提供顶层菜单的 ASP.NET 站点。在第二种设计模式下,我有 Angular 组件,这些组件不一定是成熟的 Angular 应用程序,但是,仍然需要把它们嵌入 ASP.NET 视图。例如,假设我想在一个 ASP.NET 视图中嵌入一个组件,以显示当前时间。在 Angular 中开发这么一个组件很容易,但把它嵌入 MVC 视图就比较难。最后,我希望实现尽可能多的代码重用,我希望能够在 Angular 应用程序中重用组件,并能够把相同的组件嵌入 ASP.NET 视图。本文演示了如何引导 ASP.NET 和 Angular 项目以适应这些架构设计模式。如果要看最终的代码,请参考 GitHub 上的 Multi App Demo 存储库。

总结一下,这就是我们要构建的:

  • 一个 ASP.NET Core web 网站,该网站作为门户,具有菜单结构,其中每个菜单都打开一个 MVC 视图。
  • 能够在网站中托管一个或更多 Angular SPA
  • 能够在 ASP.NET 视图中重用这些 SPA 的某些组件

实现概述

  • 我们将使用.NET Core(在撰写本文时,版本号为 3.0)创建一个 ASP.NET MVC 网站
  • 我们将使用 Angular CLI 创建一个 Angular 项目,并集成(开发工作流、产品构建、发布等)该项目到 ASP.NET 项目中。
  • 我们将把 Angular 项目作为 Web 组件(也称为自定义元素,也叫 Angular 元素)进行引导。
  • 我们将使用 Angular CLI 生成应用程序。这些应用程序将引用根 Angular 项目的可重用组件。
  • 我们将使用指向同一域的 iframes,在 ASP.NET 视图中嵌入 Angular 应用程序。
  • IFrames 将使用 JavaScript 根据内容调整大小,并且,我们将把 iframed Angular 应用程序和 ASP.NET 路由集成在一起,因此,把书签放入路由过的 Angular 视图是可行的。
  • 为了在 ASP.NET 视图中托管 Angular 组件,我们将把这些组件打包成 Web 组件。

创建 ASP.NET Core MVC 项目

我用的是 Visual Studio 2019 社区版,可以从微软那里免费下载。从 2019 版开始,用于选择模板的向导和以前的版本有所不同,但无论使用哪个版本,步骤基本上是一样的。

  • 转到创建一个新的项目。
  • 选择 ASP.NET Core Web Application。
  • 为项目选择名字和位置(我称之为我的 MultiAppDemo)。
  • 选择 ASP.NET Core(在我的情况下,是 3.0 版)。
  • 选择 ASP.NET Model-View-Controller(为了简单起见,选择 No Authentication,这样,VS 不会为这个演练生成不相关的工件)。

在 Solution Explorer 中,我们的项目视图应如下所示:

深入研究Angular和ASP.NET Core 3.0

由于我们使用了 ASP.NET MVC 模板而不是 SPA 模板,因此,我们需要添加 Microsoft.AspNetCore.SpaServices.Extensions NuGet 包。为了安装这个包,请打开软件包管理器控制台(Package Manager Console)并运行以下语句:

复制代码
Install-Package Microsoft.AspNetCore.SpaServices.Extensions

创建 Angular 项目

请确保以下软件都已安装(全部是免费的):

复制代码
npm install -g @angular/cli

我在使用 Angular v8。用更早的版本也可以,只要可以访问 createCustomElement API 就行。 Angular v7 中也有这些功能。

为了创建一个 Angular 解决方案以在我们的 ASP.NET MVC 项目中使用,请打开命令提示符,转到包含用于 MVC 项目的项目文件(扩展名.csproj)所在的文件夹。到达之后,通过命令提示符执行以下命令以创建 Angular 项目:

复制代码
ng new Apps

路由选择 N,样式选 CSS。

把目录改为 Apps,并输入以下内容(包括结尾的点):

复制代码
code .

现在,在 Visual Studio Code 中应该有我们已经打开的 Angular 项目了。

引导 Angular 元素

这里的想法是,把这个根项目做为可重用组件的存储库,其他 Angular 应用程序(我们稍后创建它们)可以把这些可重用组件作为普通 Angular 组件使用,并作为 Web 组件提供给 MVC 视图(也称为 Angular 元素)。

那么,什么是 web component?在 webcomponents.org 中,是这么定义的:

Web 组件是一组 web 平台 API,这些 API 允许我们创建新的自定义、可重用、封装的 HTML 标记以用于 web 页面和 web 应用程序。

Angular 提供一种方法,通过被称为 Angular 元素的 API 把 Angular 组件打包成 web 组件。例如,如果我们创建一个显示当前时间的 Angular 组件, 并引导该组件作为 Angular 元素当前时间,那么,我们接着可以在纯 HTML 页面中包含这个标签。在 Angular 官方网站上有更多相关信息。

在 VS Code 中打开我们的 Angular 项目。打开一个终端窗口,输入一个命令以生成时钟组件,并把该组件添加到 app.module.ts 中:

复制代码
ng g c current-time

现在,我们应该在 src/app 下有一个名为 current-time 的文件夹,包含一些组成我们的时钟组件的文件。把 app/current-time/current-time.component.html 改成具有以下标记:

复制代码
<p>{{ time }}</p>

把 app/current-time/current-time.component.ts 改成具有以下代码:

复制代码
import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-current-time',
templateUrl: './current-time.component.html',
styleUrls: ['./current-time.component.css']
})
export class CurrentTimeComponent implements OnInit, OnDestroy {
timer: any;
time: string
constructor() { }
ngOnInit() {
var setTime = () => {
var today = new Date();
this.time = ("0" + today.getHours()).slice(-2) + ":" +
("0" + today.getMinutes()).slice(-2) + ":" +
("0" + today.getSeconds()).slice(-2);
};
setTime();
this.timer = setInterval(setTime, 500);
}
ngOnDestroy(){
if (this.timer){
clearTimeout(this.timer);
}
}
}

这个实现相当简单。我们有个每半秒钟触发一次的计时器。该计时器用一个代表当前时间的字符串更新时间属性,并且 HTML 模板绑定到该字符串。

把以下样式粘贴到我们的 app/current-time/current-time.component.css

复制代码
p {
background-color: darkslategray;
color: lightgreen;
font-weight: bold;
display: inline-block;
padding: 7px;
border: 4px solid black;
border-radius: 5px;
font-family: monospace;
}

现在,保存所有修改后的文件,让我们把这个时钟组件作为 web 组件引导:

  • 在 Visual Studio Code 中打开一个新的终端窗口
  • 添加 Angular 元素库和 polyfills。在终端窗口输入以下命令实现这个操作:
复制代码
ng add @angular/elements
  • 转到 src/app/app.module.ts
  • 如果那里还没有,那么在 app.module.ts 的顶部添加以下导入语句:
复制代码
import { createCustomElement } from '@angular/elements';
  • 从 @angular/core 给导入添加 Injector:
复制代码
import { NgModule, Injector } from '@angular/core';
  • 用 bootstrap: [AppComponent] 替换 entryComponents: [ClockComponent]
  • 最后,给 AppModule 类添加构造函数和 ngDoBootstrap。
复制代码
constructor(private injector: Injector) {
}
ngDoBootstrap(){
customElements.define('current-time', createCustomElement(CurrentTimeComponent,
{injector: this.injector}));
}

现在我们还需要做一件事情,稍后当我们在一个不同的 Angular 应用程序中导入 CurrentTimeComponents 时就会用到。我们需要从这个模块导出该组件。在 providers 上方添加导出属性就可以实现:

复制代码
exports: [
CurrentTimeComponent
],

我们的整个 app.module.ts 应如下所示:

复制代码
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { AppComponent } from './app.component';
import { createCustomElement } from '@angular/elements';
import { CurrentTimeComponent } from './current-time/current-time.component';
@NgModule({
declarations: [
AppComponent,
CurrentTimeComponent
],
imports: [
BrowserModule
],
exports: [
CurrentTimeComponent
],
providers: [],
entryComponents: [CurrentTimeComponent]
})
export class AppModule {
constructor(private injector: Injector) {
}
ngDoBootstrap(){
customElements.define('current-time', createCustomElement(CurrentTimeComponent,
{injector: this.injector}));
}
}

现在,我们来测试一下我们的解决方案是否有用。转到 src\index.html,用替换。在终端窗口输入 ng serve --open 以运行该项目。现在,我们应该在浏览器窗口看到当前时间。

深入研究Angular和ASP.NET Core 3.0

在 ASP.NET 项目中使用 Web 组件

接下来,要让我们的当前时间组件在 ASP.NET MVC Core 项目中可用。在 Visual Studio 中打开 ASP.NET 就可以了。在 Views/Shares/_Layout.cshtml 中的结束标签前粘贴以下代码:

复制代码
<environment include="Development">
<script type="text/javascript" src="http://localhost:4200/runtime.js"></script>
<script type="text/javascript" src="http://localhost:4200/polyfills.js"></script>
<script type="text/javascript" src="http://localhost:4200/styles.js"></script>
<script type="text/javascript" src="http://localhost:4200/scripts.js"></script>
<script type="text/javascript" src="http://localhost:4200/vendor.js"></script>
<script type="text/javascript" src="http://localhost:4200/main.js"></script>
</environment>
<environment exclude="Development">
<script asp-src-include="~/Apps/dist/core/runtime-es2015.*.js" type="module"></script>
<script asp-src-include="~/Apps/dist/core/polyfills-es2015.*.js" type="module"></script>
<script asp-src-include="~/Apps/dist/core/runtime-es5.*.js" nomodule></script>
<script asp-src-include="~/Apps/dist/core/polyfills-es5.*.js" nomodule></script>
<script asp-src-include="~/Apps/dist/core/scripts.*.js"></script>
<script asp-src-include="~/Apps/dist/core/main-es2015.*.js" type="module"></script>
<script asp-src-include="~/Apps/dist/core/main-es5.*.js" nomodule></script>
</environment>

前面的代码段显示了两个块,一个用于开发,一个用于非开发。当我们在开发时,我们托管 web 组件的 Angular 项目将运行于从 VS Code 启动的 4200 端口。当我们投入生产时,该 Angular 项目将被编译到 wwwroot/apps/core 文件夹,并带有附加哈希值命名的 javascript 文件。为了正确地引用这些文件,我们需要使用 asp-src-include 标记助手。

接下来,在 _Layout.cshtml 中,在结束标记后面直接添加

测试我们的开发配置是否有用:

  • 转到打开 Angular 项目的 VS Code,在终端提示符中输入该命令:
复制代码
ng serve --liveReload=false
  • 转到打开 ASP.NET 项目的 Visual Studio,点击 F5 键运行该项目。我们的 ASP.NET 站点应该打开,并且,我们应该看到在每个页面上显示的当前时间组件。

创建 Angular 应用程序

Web 组件很棒,也许是 web UI 的未来,但是,就今天来说,Angular 项目作为单个页面应用程序(Single Page Applications,简称 SPAs)引导仍有自己的生存空间。

Angular 是围绕着模块的概念设计的,它的一些特性,特别是路由,是与模块而不是组件保持一致的。在混合 Angular 和 ASP.NET 开发时,我的目标是在 MVC 视图中托管 Angular 应用程序。我希望 ASP.NET MVC 提供顶层菜单结构,SPA 提供它们自己的菜单和路由结构,这些都驻留在更大的 MVC 应用程序中 。此外,我希望实现代码重用,这些代码可以在解决方案中的多个 SPAs 中共享,也可以作为 web 组件包含在非 Angular 页面中。

第一步是在 Angular 中创建一个新的应用程序。最简单的实现方法是使用 Angular CLI(命令行接口)。如果还没有这个,在 VS Code 中打开 Angular 项目,并启动一个新的终端窗口。在终端窗口,执行该命令:

复制代码
ng g application App1 --routing=true

这将在配置了路由模块的 Apps\projects\App1 下生成新的 Angular 应用程序。让我们生成两个组件,并设置路由,这样我们可以路由到某处去。从终端窗口执行以下命令:

复制代码
ng g c Page1 --project=App1
ng g c Page2 --project=App1

现在,我们应该在 Apps/Projects/App1/src/app 下看到两个新的组件文件夹 page1 和 page2。

现在,让我们来为这些组件设置路由。把 Apps/Projects/App1/src/app 下的 app.component.html 改成具有这个标记:

复制代码
<h2>App1</h2>
<a routerLink="/page1" routerLinkActive="active">Page1</a>
<a routerLink="/page2" routerLinkActive="active">Page2</a>
<router-outlet></router-outlet>

并用以下代码更新 Apps/projects/App1/src/app 下的 app-routing.module.ts:

复制代码
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Page1Component } from './page1/page1.component';
import { Page2Component } from './page2/page2.component';
const routes: Routes = [
{path: '', redirectTo: 'page1', pathMatch: 'full'},
{path: 'page1', component: Page1Component},
{path: 'page2', component: Page2Component}];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

这只是标准的路由代码。关于 Angular 路由的评论,请访问该页面

现在,让我们来测试我们的新应用程序是否配置得正确。打开一个新的终端窗口,输入以下命令:

复制代码
ng serve App1 --port 4201 --open

我们的浏览器窗口应该打开,我们应该能看到类似以下的内容:

深入研究Angular和ASP.NET Core 3.0

请注意,现在我们在用端口 4201,和我们用于根 Angular 项目的不同。我们创建的每个应用程序都将需要开发环境中一个不同的端口服务于它,但是,在非开发环境中,所有的应用程序、ASP.NET 和 Angular 都将运行于同一个端口上。

现在,该演示的一个目标是实现代码重用。让我们在 App1 中重用来自基础项目的 Angular 组件。为了实现这个目标,要在 App1 的主模块中包含 CurrentTimeComponent 的导入。

转到 Apps/projects/App1/src/app 下的 app.modules.ts,添加以下导入语句:

复制代码
import { CurrentTimeComponent } from '../../../../src/app/current-time/current-time.component';

这里正在发生的事是,我们从根项目中导入 CurrentTimeComponent。或者,我们可以从根项目中导入整个 AppModule。

接下来,把 CurrentTimeComponent 添加到声明列表中:

复制代码
declarations: [
AppComponent,
Page1Component,
Page2Component,
CurrentTimeComponent
],

现在,转到 App1 中的 app.component.html,并为当前时间添加标签,就添加在路由器出口的正下方。

复制代码
<h2>App1</h2>
<a routerLink="/page1" routerLinkActive="active">Page1</a>
<a routerLink="/page2" routerLinkActive="active">Page2</a>
<router-outlet></router-outlet>
<app-current-time></app-current-time>

请注意,我们为这个组件使用了 Angular 标签(app-current-time),而不是 web 组件标签名(current-time)。原因是,我们把该组件作为 Angular 组件包含在内了。App1 完全不知道这个 Angular 组件在其他地方用作 web 组件。

保存所有的文件并检查浏览器。我们的 App1 页面现在应该显示当前时间组件。

深入研究Angular和ASP.NET Core 3.0

把 App1 作为 SPA 集成到 ASP.NET MVC

在这个演练中,我们要做的最后一件事是,把 App1 作为单页面应用程序合并到 ASP.NET MVC 应用程序 。我们希望有以下特性:

  • 该 SPA 应该嵌入 MVC 视图之一。
  • 其应该可以深度链接到一个 SPA 中的页面。
  • 应该支持实时重新加载。

首先,让我们在主控制器上(Home Controller)上设置一个名为 App1 的常规 MVC 视图。

在我们的 MVC 项目中,转到 Controllers/HomeController.cs,并添加以下代码:

复制代码
[Route("app1/{*url}")]
public IActionResult App1(string url)
{
return View("App1", url);
}

这个在路由(Route)属性中的{*url}构造告诉 ASP.NET 捕获在 url 变量中 /app1/ 段右侧的一切内容。然后,将其传到 Angular 应用程序。
现在,右键单击 View() 令牌,然后选择添加视图。调用视图 App1,并点击 Add 按钮。这应该在 Views/Home 中创建一个名为 App1.cshtml 的文件。确保该文件有以下标记:

复制代码
@{
ViewData["Title"] = "App1";
}
This is the view for App1.

转到 Shared/_Layout.cshtml,并给该视图添加一个链接,就添加在到隐私(Privacy)视图链接的下方。最简单的方法是,复制这个隐私链接标记,并用“App1”这个词替换“Privacy”这个词。

复制代码
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home"
asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home"
asp-action="Privacy">Privacy</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home"
asp-action="App1">App1</a>
</li>
</ul>

在 _Layout.cshtml 中时,让我们多做一个更改。让我们web 组件周围添加一些标记,以直观地指明这是一个 web 组件而不是 Angular 组件。添加


和注释就可以做到:

复制代码
<div class="container">
<partial name="_CookieConsentPartial" />
<main role="main" class="pb-3">
@RenderBody()
</main>
<hr />
This is a web component<br />
<current-time></current-time>
</div>

接下来,我们来测试一下这个应用程序。点击 F5 键,确保可以通过 App1 链接跳转到 App1 视图。

深入研究Angular和ASP.NET Core 3.0

下一步是把 App1 应用程序嵌入 App1 MVC 视图。我们准备使用一个 iframe,它指向在同一个域的 URL。使用 iframe 的好处是可以把 App1 封装在其自身的容器中,但也带来两个挑战:

  • iframe 需要动态地随其内容的变化而调整其大小。
  • 在用户在 Angular 应用程序中跳转时,顶部窗口的地址栏必须改变。

我们将使用 JavaScript 来解决这两个挑战。因为 iframe 指向同一个域,所以,这是唯一可行的方法,从而避免了跨域限制。

但是,在我们这么做之前,我们仍然需要在.NET 代码中做更多的修改。

首先,我们在 Startup 中配置 App1。打开 Startup.cs,并把以下代码添加到配置(Configure)方法中:

复制代码
app.Map("/apps/app1", builder => {
builder.UseSpa(spa =>
{
if (env.IsDevelopment())
{
spa.UseProxyToSpaDevelopmentServer($"http://localhost:4201/");
}
else
{
var staticPath = Path.Combine(
Directory.GetCurrentDirectory(), $"wwwroot/Apps/dist/app1");
var fileOptions = new StaticFileOptions
{ FileProvider = new PhysicalFileProvider(staticPath) };
builder.UseSpaStaticFiles(options: fileOptions);
spa.Options.DefaultPageStaticFileOptions = fileOptions;
}
});
});

该段代码告诉.NET 核心运行时,把应用程序映射到 /apps/app1 路径,以代理到开发中的端口 4201,并期望在非开发环境中的 wwwroot/apps/app1 可用编译后的文件。

但是,我们不希望 /apps/app1 的用户使用我们的应用程序。我们希望我们的应用程序在用户转到 App1 视图时可用,App1 视图可以是 /home/app1 或只是 /app1 URL。

这里是我们打算使用 iframe 的地方。打开 App1.cshtml,并添加以下标记:

复制代码
<iframe src="/apps/app1/@Model" class="app-container" frameborder="0" scrolling="no"></iframe>

请注意 @Model 构造。它被映射到组件中的{*url},我们把路径的一部分从顶部窗口传到 App1 右侧的 iframe,因此,路由在 Angular 应用程序内部进行。

现在,我们可以测试这个应用程序了。转到 VS Code,并从一个可用的终端窗口执行以下 serve 命令:

ng serve App1 --port 4201 --servePath / --baseHref /apps/app1/ --publicHost http://localhost:4201

该命令在 4201 端口启动 App1。由于我们知道准备从 apps/app1 给它提供服务,因此,它设置了基础 HREF,并且,它指示 Angular 使用 localhost:4201 而不是使用相对的 URL 进行实时重载。

转到 Visual Studio,并点击 F5 键。在 ASP.NET 站点出现在浏览器窗口后,转到 App1 菜单。如果看到和下面类似的屏幕,那就意味着该应用程序已经正确地连接上了。

深入研究Angular和ASP.NET Core 3.0

尽管 App1 Angular 应用程序确实出现在 App1 视图中,但是没有内容。如果点击 Page 1 和 Page 2 的链接,可以看到在 Angluar 组件中跳转是正常工作的,但是,在浏览器顶部的地址栏没有反映出跳转的当前状态。让我们来解决这两个问题。

为了在启动时以及 iframe 的内容有变化时调整 iframe 的大小,我们将使用名为 iFrame Resizer 的 JavaScript 组件,iFrame Resizer 是由 David Bradshaw 创建的。

为了让该组件工作,我们需要执行这三个步骤。

在 _Layout.cshtml 中,把以下脚本标签粘贴到指向 site.js 的脚本标签的正上方

复制代码
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.1.1/iframeResizer.min.js">
</script>

给位于 wwwroot/js 的 site.js 添加以下代码行。

复制代码
$('.app-container').iFrameResize({ heightCalculationMethod: 'documentElementOffset' });

接着,转到 VS Code,并在结束标签的上方给位于 Apps/projects/App1/src 的 Index.html 添加以下脚本标签:

复制代码
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.1.1/iframeResizer.contentWindow.min.js">
</script>

保存所有的文件,我们来重新测试一下这个应用程序。App1 现在应该如下所示:

深入研究Angular和ASP.NET Core 3.0

请注意,内容不再消失了。iFrame Resizer 的这一点做得很不错,在 iframe 初始加载后,它将不断调整 iframe 的大小以适合内容。

现在,我们来解决这个问题:当点击 Angular 路由器链接时,地址栏没有更新。因为 App1 在 iframe 中运行,因此,地址栏没有更新。iframe 的地址在改变,但是,我们看不到,原因是,我们看到的地址栏是用于顶部浏览器窗口的。

请记住,我们已经有代码可以捕捉 /app1 URL 段右侧的路径,并存入{*ulr}变量,再把它传给 iframe。我们需要添加的代码是另一个方式,当路由在 Angular 应用程序中进行时,我们希望把变化传播到顶层地址栏。

我们需要把代码添加到 App1 应用程序中的路由模块中来实现。

打开 Apps/projects/App1/src/app 中的 app-routing.module.ts。在 AppRouting Module 的构造函数中添加以下代码:

复制代码
constructor(private route:Router){
var topHref = window.top.location.href != window.location.href ?
window.top.location.href.substring(0,
window.top.location.href.indexOf('/app1') + 5) :
null;
this.route.events.subscribe(e => {
if(e instanceof NavigationEnd){
if (topHref){
window.top.history.replaceState(window.top.history.state,
window.top.document.title, topHref + e.url);
}
}
});
}

该代码通过比较顶部窗口的 HREF 和当前窗口的 HREF 来确定应用程序是否在 iframe 中运行。如果应用程序在 iframe 中运行,那么,代码把顶部窗口的 HREF 保存在一个局部变量中,但是去掉了指向 /app1 段右侧的 HREF 部分。然后,代码进入 NavigationEnd 事件,并把路由过的 URL 追加到顶部窗口的 HREF 的后面。

我们还将需要给导入添加 Router 和 NavigationEnd。整个 app-routing.module.ts 应该如下所示:

复制代码
import { NgModule } from '@angular/core';
import { Routes, RouterModule, Router, NavigationEnd } from '@angular/router';
import { Page1Component } from './page1/page1.component';
import { Page2Component } from './page2/page2.component';
const routes: Routes = [
{path: '', redirectTo: 'page1', pathMatch: 'full'},
{path: 'page1', component: Page1Component},
{path: 'page2', component: Page2Component}];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
constructor(private route:Router){
var topHref = window.top.location.href != window.location.href ?
window.top.location.href.substring(0,
window.top.location.href.indexOf('/app1') + 5) :
null;
this.route.events.subscribe(e => {
if(e instanceof NavigationEnd){
if (topHref){
window.top.history.replaceState(window.top.history.state,
window.top.document.title, topHref + e.url);
}
}
});
}
}

为了测试该应用程序,请从 Visual Studio 启动它。点击 Page 1 或 Page 2 的链接。观察到顶部 URL 现在在变化。我们还可以复制修改过的 URL,并把它粘贴到一个独立的窗口,App1 将路由到顶部 URL 中指定的组件。

调整发布(Publish)设置

还有最后一件事要做。我们需要修改项目文件,以将 Angular 构建任务纳入发布过程。为此,转到 ASP.NET 项目,右键单击项目文件,选择 Edit .csproj。项目文件应该与如下所示的类似:

复制代码
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TypeScriptToolsVersion>3.3</TypeScriptToolsVersion>
<SpaRoot>Apps\</SpaRoot>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Content Remove="$(SpaRoot)**" />
<None Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.0.0" />
</ItemGroup>
<Target Name="PublishApps" AfterTargets="ComputeFilesToPublish">
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build -- --prod --outputPath=./dist/core" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build App1 -- --prod --base-href=/apps/app1/ --outputPath=./dist/app1" />
<ItemGroup>
<DistFiles Include="$(SpaRoot)dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>wwwroot\%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

这里有个有趣的部分,就是 Target 标签。我们指示构建过程运行 npm 安装,然后构建两个 Angular 项目,接着,复制 dist 文件夹输出到 ASP.NET 站点的 wwwroot 文件夹。

为了测试我们的发布配置是否有用:

  • 在 Visual Studio 中右键单击 ASP.NET 项目名称。
  • 转到 Publish。
  • 在选择(Pick)一个发布目标下,选择文件夹(Folder)。
  • 点击发布(Publish)按钮。

在这个过程的最后,我们应该看到在 Output 窗口中发布的新文件的文件夹的整个路径。为了测试发布的站点:

  • 在命令窗口打开发布文件夹。
  • 输入:dotnet .dll
  • 转到到我们的浏览器,打开 http://localhost:5000

结论

我们创建了一个 ASP.NET 站点,把两个 Angular 项目与它集成在一起,并把 Angular 工件嵌入 MVC 视图。如果我们想试用这个解决方案,建议从 GitHub 中克隆项目。尝试添加 App2,并从不同的 MVC 视图中为它提供服务,或者尝试创建更多的 web 组件。

作者介绍

30 年来,Evgueni Tsygankov 一直在编写软件,从 80 年代的 Commodore 64 一直到如今的云计算。目前,他在 Effita 领导其中的一支开发团队,Effita 是总部在密苏里州圣路易斯的一家软件公司。在空闲的时候,Evgueni 把时间用于陪伴他的两个孩子以及打冰球和踢足球。

原文链接:

Angular & ASP.NET Core 3.0 - Deep Dive

评论

发布
用户头像
期待vue版的出来!
2019 年 11 月 29 日 07:42
回复
没有更多了