SPA^2 使用 ASP.NET Core 1.1 + Angular 2.4 - 第一部分






4.97/5 (62投票s)
"...狗和猫住在一起..." 或者如何同时构建一个应用程序框架,以充分发挥 ASP.NET Core 和 Angular 的优势。更新后的源代码现在支持 VS2015+VS2017
引言
这是本系列文章的第一篇,旨在展示如何从零开始创建一个应用程序框架,该框架同时使用 ASP.NET Core 和 Angular 2,并发挥各自的最佳特性。这种方法相对易于使用,可以很好地扩展到小型或大型团队,并且对于普通网站以及多租户网站都能获得良好的性能。
之前已有使用 ASP.NET Core 和 Angular 2 的应用程序,但它们往往要么 (a) 仅勉强使用 ASP.NET Core - 仅通过 Web API 提供数据,并将“纯粹的”HTML 文件作为 Angular 模板;要么 (b) 走向另一个极端,即使用 ASP.NET Core 和 Webpack 在服务器端预渲染 Angular 2。
我尝试过这两种方法,但发现更简单的选项 (a) 可能会诱使团队成员复制代码片段到客户端标记,而不是创建公共指令,并且代码也更脆弱,耦合性远非理想。更复杂的选项 (b) 会让许多开发人员(无论是初级还是高级)都感到头晕,而且设置和调试可能需要大量工作。在这两种情况下,ASP.NET 的许多出色功能,如标签助手、MVC 视图和 Razor,都未能得到利用。
此框架使用 ASP.NET MVC 视图、Razor 和标签助手,根据服务器端视图模型属性的类型和属性来生成客户端 HTML、CSS 标记、验证、Angular 代码和 Angular 视图。最后,一旦创建了 Web API 数据组件,我们将使用 Swagger 工具创建 Angular 2 数据服务和数据模型,从而为您留下相对简单的任务,即创建 Angular 组件将它们链接在一起。
在这些文章中,我假设您对 ASP.NET MVC、通用 C#、HTML、Bootstrap 和 Angular 有一些了解,并将重点关注如何将它们协同使用的简要细节。如果您需要深入的信息,这里有一些非常好的教程,在 Code Project 上以及像 Pluralsight 这样的培训网站上。
在过去两年中,我在许多商业项目和各种业务领域中都使用了这里的技术。这些文章将这些概念从 .NET 4.5x 和 Angular 1.x 迁移到使用 .NET Core 和 Angular 2。
TLDR - 最终源代码,请参见 Github https://github.com/RobertDyball/a2spa。
(1) 创建一个 ASP.NET Core 应用程序
要创建我们的 ASP.NET + Angular SPA,我们将从创建一个标准的 Visual Studio 2015 ASP.NET Core 模板开始,并使用 Bootstrap 3 进行样式设置。
在 Visual Studio 2015 中,点击 文件,新建,项目。
在 文件,新建,项目 之后,选择 模板,然后选择 ASP.NET Core
填写名称(我使用了 A2SPA
)、路径或位置(我使用了 c:\dev\),我喜欢勾选“为解决方案创建目录”,并且我也勾选“创建新的 GIT 存储库”。
选择 Web 应用程序,将 身份验证 保持为“无身份验证”,然后点击 OK。
您现在应该会看到解决方案的自述文件和一个新的解决方案。在键盘上按 Ctrl-F5,您应该会构建解决方案,启动 IIS Express 和您的默认浏览器,如果一切顺利,您会在浏览器中看到类似下面的内容。
(2) 将 ASP.NET Core 从版本 1.0.1 更新到 1.1.0
接下来,我们将基础 ASP.NET Core 解决方案从版本 1.0.1 更新到版本 1.1.0,尽管届时可能已经有另一个版本了,但基本方法应该类似。请查看 ASP.NET 博客 此处,了解版本 1.0.1 到 1.1.0 的详细信息。
打开 global.json,将其从
{
"projects": [ "src", "test" ],
"sdk": {
"version": "1.0.0-preview2-003131"
}
}
更改为以下内容,以使用最新的/当前的 ASP.NET Core SDK
{
"projects": [ "src", "test" ],
"sdk": {
"version": "1.0.0-preview2-1-003177"
}
}
打开 project.json 并更改文件中包含此内容的部分
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"
},
更改为以下内容,此 1.1.0 版本对应上面的 SDK 版本“1.0.0-preview2-003177
”
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.1.0",
"type": "platform"
},
最后,在同一个文件中,找到这个
"frameworks": {
"netcoreapp1.0": {
"imports": [
"dotnet5.6",
"portable-net45+win8"
]
}
},
并将其更改为此,再次以定位新的 1.1 框架而不是之前的 1.0 框架
"frameworks": {
"netcoreapp1.1": {
"imports": [
"dotnet5.6",
"portable-net45+win8"
]
}
},
接下来,我们将使用 NuGet 升级包,在项目中右键单击解决方案,然后单击“管理 NuGet 程序包”。
选择“更新”
勾选“全选程序包”,然后单击 更新 按钮开始更新
点击 同意 继续
等待下载更新,然后等待更新完成
最后,重新生成,按 ctrl-F5,您将获得一个成功的生成,并再次看到正在运行的应用。
升级前的 Package.json 是
{
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"
},
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.1",
"Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final",
"type": "build"
},
"Microsoft.AspNetCore.Routing": "1.0.1",
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
"Microsoft.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.Extensions.Logging.Debug": "1.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0"
},
"tools": {
"BundlerMinifier.Core": "2.0.238",
"Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
"Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
},
"frameworks": {
"netcoreapp1.0": {
"imports": [
"dotnet5.6",
"portable-net45+win8"
]
}
},
"buildOptions": {
"emitEntryPoint": true,
"preserveCompilationContext": true
},
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
},
"publishOptions": {
"include": [
"wwwroot",
"**/*.cshtml",
"appsettings.json",
"web.config"
]
},
"scripts": {
"prepublish": [ "bower install", "dotnet bundle" ],
"postpublish": [ "dotnet publish-iis
--publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
}
}
供参考,最终的 package.json 现在(大致)看起来应该是这样的
{
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.1.0",
"type": "platform"
},
"BundlerMinifier.Core": "2.2.306",
"Microsoft.AspNetCore.Diagnostics": "1.1.0",
"Microsoft.AspNetCore.Mvc": "1.1.0",
"Microsoft.AspNetCore.Razor.Tools": "1.1.0-preview4-final",
"Microsoft.AspNetCore.Routing": "1.1.0",
"Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
"Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.1.0-preview4-final",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.AspNetCore.StaticFiles": "1.1.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
"Microsoft.Extensions.Configuration.Json": "1.1.0",
"Microsoft.Extensions.Logging": "1.1.0",
"Microsoft.Extensions.Logging.Console": "1.1.0",
"Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.1.0"
},
"tools": {
},
"frameworks": {
"netcoreapp1.1": {
"imports": [
"dotnet5.6",
"portable-net45+win8"
]
}
},
"buildOptions": {
"emitEntryPoint": true,
"preserveCompilationContext": true
},
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
},
"publishOptions": {
"include": [
"wwwroot",
"**/*.cshtml",
"appsettings.json",
"web.config"
]
},
"scripts": {
"prepublish": [ "bower install", "dotnet bundle" ],
"postpublish": [ "dotnet publish-iis
--publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
}
}
(3) 将 Angular 2 QuickStart 添加到我们的 ASP.NET Core 应用程序中
我们将使用 Angular 2 QuickStart 应用程序,其源代码来自 Github,此处。
如果您需要一些背景信息,请浏览 Angular 2 团队的精彩教程 此处。
要将 Angular 2 QuickStart 代码合并到我们的 ASP.NET Core MVC 应用程序中,我们将首先获取源代码的副本。
有几种方法可以做到这一点;您可以从 GitHub 下载最新源代码的 ZIP 文件,也可以使用 Git Extensions 或您喜欢的 Git 工具克隆存储库的完整副本。
命令与执行位置无关,在我这里,我在 Powershell 中输入了此命令
git clone https://github.com/angular/quickstart
这是结果。
接下来导航到上面克隆的文件所在的文件夹。
将 app 文件夹(文件夹本身和文件夹中的文件)以及这些文件:index.html、systemjs.config.extras.js 和 system.config.js 复制到 VS2015 解决方案的 wwwroot 文件夹中。
此外,将文件 package.json、tsconfig.json 和 tslint.json 复制到 VS2015 项目的根目录。
复制之前,您的 VS2015 项目应该看起来像这样
如果您使用 Windows 资源管理器从 QuickStart 复制文件,可以直接在 VS2015 解决方案资源管理器中将文件粘贴到项目中。完成后您应该看到这个
注意:缺少依赖项的警告仅仅是因为存在新的 package.json 文件。另外,在 VS2015 中,systemjs.config.extras.js 文件会折叠在 system.config.js 下方 - 即使您单独复制它们。它应该在 wwwroot 文件夹中,您可以通过检查来轻松验证。
最后,在生成之前,编辑 package.json 文件以删除不再需要的包。
编辑前,package.json 文件应该如下所示,尽管确切内容可能会随着 Angular 2 版本的发展和其他变化而变化。
以前
{
"name": "angular-quickstart",
"version": "1.0.0",
"description": "QuickStart package.json from the documentation,
supplemented with testing support",
"scripts": {
"start": "tsc && concurrently \"tsc -w\" \"lite-server\" ",
"e2e": "tsc && concurrently \"http-server -s\"
\"protractor protractor.config.js\" --kill-others --success first",
"lint": "tslint ./app/**/*.ts -t verbose",
"lite": "lite-server",
"pree2e": "webdriver-manager update",
"test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"",
"test-once": "tsc && karma start karma.conf.js --single-run",
"tsc": "tsc",
"tsc:w": "tsc -w"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@angular/common": "~2.4.0",
"@angular/compiler": "~2.4.0",
"@angular/core": "~2.4.0",
"@angular/forms": "~2.4.0",
"@angular/http": "~2.4.0",
"@angular/platform-browser": "~2.4.0",
"@angular/platform-browser-dynamic": "~2.4.0",
"@angular/router": "~3.4.0",
"angular-in-memory-web-api": "~0.2.4",
"systemjs": "0.19.40",
"core-js": "^2.4.1",
"rxjs": "5.0.1",
"zone.js": "^0.7.4"
},
"devDependencies": {
"concurrently": "^3.1.0",
"lite-server": "^2.2.2",
"typescript": "~2.0.10",
"canonical-path": "0.0.2",
"http-server": "^0.9.0",
"tslint": "^3.15.1",
"lodash": "^4.16.4",
"jasmine-core": "~2.4.1",
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-cli": "^1.0.1",
"karma-jasmine": "^1.0.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~4.0.14",
"rimraf": "^2.5.4",
"@types/node": "^6.0.46",
"@types/jasmine": "^2.5.36"
},
"repository": {}
}
操作后
{
"dependencies": {
"@angular/common": "~2.4.0",
"@angular/compiler": "~2.4.0",
"@angular/core": "~2.4.0",
"@angular/forms": "~2.4.0",
"@angular/http": "~2.4.0",
"@angular/platform-browser": "~2.4.0",
"@angular/platform-browser-dynamic": "~2.4.0",
"@angular/router": "~3.4.0",
"angular-in-memory-web-api": "~0.2.4",
"systemjs": "0.19.40",
"core-js": "^2.4.1",
"rxjs": "5.0.1",
"zone.js": "^0.7.4"
},
"devDependencies": {
"typescript": "~2.0.10",
"tslint": "^3.15.1"
},
"repository": {}
}
当您点击 保存,编辑完成后,您很可能会看到 VS2015 正在恢复包
然后完成后,依赖项旁边将不再有警告
接下来我们将编辑 tsconfig.json,从这个
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
}
}
改为这样。
{
"compilerOptions": {
"diagnostics": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"listFiles": true,
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"outDir": "wwwroot",
"removeComments": false,
"rootDir": "wwwroot",
"sourceMap": true,
"suppressImplicitAnyIndexErrors": true,
"target": "es5"
},
"exclude": [
"node_modules"
]
}
最后,在生成之前,我们还有一个 TypeScript 文件需要删除。
转到 wwwroot 下的 app 文件夹并删除 app.component.spec.ts
由于我们没有将测试包含在此项目中,也没有包含依赖项,因此如果保留该文件,将会导致错误。
生成完成后,您应该会发现 TypeScript 文件已在原处自动“转译”,展开后,会为每个文件显示一个 JavaScript .js 文件和一个映射 .js.map 文件。
.js JavaScript 文件将在浏览器中执行,.js.map 文件用于在调试 JavaScript 时将问题链接回 TypeScript 源。
要测试生成,请按 Ctrl-F5 编译并启动浏览器,您仍然应该看到一个可用的 MVC 页面。
在默认 URL
以及在 /home/index
要查看 QuickStart 页面,请编辑 URL 以查看 /index.html。
您应该会看到 Angular 加载器
但目前您还看不到其他任何内容。按 F12,您应该会看到 Angular 的错误消息
展开后可能也帮助不大。
原因是所需的 JavaScript 库没有从它们当前的位置提供。查看 index.html,您会看到
<!DOCTYPE html>
<html>
<head>
<title>Angular QuickStart</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading AppComponent content here ...</my-app>
</body>
</html>
库文件本应从 wwwroot/node_modules 提供,但实际上它们在项目的根目录下。除非您打开显示所有文件选项,否则这一点并不明显。
然后您将看到该文件夹出现,灰色表示它不受版本控制或不属于解决方案,而是单独添加的。
您可以再次隐藏这些文件夹,因为它们可能会分散注意力,现在我们将解决这个问题。
我们将使用 NuGet 添加一个包 Microsoft.AspNetCore.SpaServices
。
右键单击项目根目录,单击 管理 NuGet 程序包,选择 浏览,确保勾选了预发行版
点击该程序包
那么
安装后,不要被 更新 按钮迷惑(仔细看,您会看到它是一个降级)。
重要的是,这个 aspnet core 库现在可以使用了。查看 project.json,您将看到新程序包已添加到其中
{
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.1.0",
"type": "platform"
},
"BundlerMinifier.Core": "2.2.306",
"Microsoft.AspNetCore.Diagnostics": "1.1.0",
"Microsoft.AspNetCore.Mvc": "1.1.0",
"Microsoft.AspNetCore.Razor.Tools": "1.1.0-preview4-final",
"Microsoft.AspNetCore.Routing": "1.1.0",
"Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
"Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.1.0-preview4-final",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.AspNetCore.StaticFiles": "1.1.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
"Microsoft.Extensions.Configuration.Json": "1.1.0",
"Microsoft.Extensions.Logging": "1.1.0",
"Microsoft.Extensions.Logging.Console": "1.1.0",
"Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.1.0",
"Microsoft.AspNetCore.SpaServices": "1.1.0-beta-000002"
},
"tools": {
},
"frameworks": {
"netcoreapp1.1": {
"imports": [
"dotnet5.6",
"portable-net45+win8"
]
}
},
"buildOptions": {
"emitEntryPoint": true,
"preserveCompilationContext": true
},
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
},
"publishOptions": {
"include": [
"wwwroot",
"**/*.cshtml",
"appsettings.json",
"web.config"
]
},
"scripts": {
"prepublish": [ "bower install", "dotnet bundle" ],
"postpublish": [ "dotnet publish-iis
--publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
}
}
接下来打开编辑器中的 startup.cs 文件,我们将编辑文件末尾的部分,其中包含
...
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
将其更改为
...
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider
(Path.Combine(env.ContentRootPath, "node_modules")),
RequestPath = "/node_modules"
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
// in case multiple SPAs required.
routes.MapSpaFallbackRoute("spa-fallback",
new { controller = "home", action = "index" });
});
}
}
}
为了满足 PhysicalFileProvider
和 Path.Combine
的依赖项,我们需要将此添加到 startup.cs 开头的 using
语句中
以前
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace A2SPA
...
操作后
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.FileProviders;
using System.IO;
namespace A2SPA
...
其中一些默认的依赖项条目是不需要的,因此您可以将其编辑为最终版本
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.FileProviders;
using System.IO;
namespace A2SPA
...
保存 startup.cs 并重新生成,按 Ctrl-F5,然后再次浏览到 index.html,或者根目录(因为上面的更改现在意味着当浏览 根 目录时,index.html 文件将“覆盖”/home/index 路径)。
现在应用程序应该可以正确加载了
重要的是,您仍然可以通过显式使用路径(如下所示)访问旧的 ASP.NET Core MVC 主页 /home/index,因为随着项目的继续,我们将更多地使用这些控制器/操作路径。
(4) 一起使用 ASP.NET Core 和 Angular
到目前为止,我们有两个独立的应用程序,ASP.NET Core MVC 应用程序仍然在 /home/index 运行,就像以前一样,而 Angular 应用程序则从 index.html 启动。
许多使用 ASP.NET Core 和 Angular JS 的人会在这里停止,他们使用“纯粹的”HTML 构建 Angular 网站,围绕 index.html(在这种情况下,任何 Web 服务器都可以工作),一些人更进一步,他们的集成会在服务器端预渲染 Angular 代码,从而“引导”数据和代码到客户端,但通过预渲染数据,您现在引入了另一套需要维护的代码,尽管这些代码在高流量且很少变化的网站上可能很有用,但它们可能变得相当复杂。
其他实现创建 RESTful Web 服务并使用 ASP.NET Core Web API 来提供数据。然而,这以及前面的选项仍然使 ASP.NET Core 的许多功能未得到使用。
或者,您可以使用 ASP.NET MVC 局部视图,使用常规的 MVC 控制器和操作来执行后端 C# 代码,在您的视图中使用 Razor 标记,并允许使用自定义标签助手,并利用 ASP.NET Core 服务器端缓存 - 所有这些结合起来也可以为您的 Angular 页面提供 HTML 模板,但这些模板不再是“纯粹的”,而是作为管道的一部分,提供了许多可选的接触点。
我们将使用后一种方法,首先将 Angular 主页 index.html 页面移动到 Home 控制器和 index.cshtml 视图中,将通用的共享库移至共享视图,并创建一个 MVC 控制器,该控制器将提供局部视图并替换内联和外部 Angular 模板。
下一步将是添加自定义标签助手,用于使用 HTML、样式和验证预填充这些 Angular 模板。数据不会预先加载,相反,我们将创建 ASP.NET Core 中的 RESTful 服务,以便在客户端按常规方式请求时向 Angular 页面提供数据。
Angular JS QuickStart 的 index.html 以如下方式开始
<!DOCTYPE html>
<html>
<head>
<title>Angular QuickStart</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading AppComponent content here ...</my-app>
</body>
</html>
Angular QuickStart 应用程序的模板是内联的,在 app.component.ts 中如下所示
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `<h1>Hello {{name}}</h1>`,
})
export class AppComponent { name = 'Angular'; }
然而,组件也可以引用外部 HTML 模板,如下所示,假设 appComponent.html 文件与它在同一目录下
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './appComponent.html'
})
export class AppComponent { name = 'Angular'; }
我们将使用 templateUrl
语法,类似于上面的第二个示例,指向新的控制器 PartialController.cs 并创建一个视图来提供我们需要的内容。
在 /controllers 文件夹中创建 Partial Controller,确保文件名为 PartialController.cs 并包含以下代码
using Microsoft.AspNetCore.Mvc;
namespace A2SPA.Controllers
{
public class PartialController : Controller
{
public IActionResult AboutComponent() => PartialView();
public IActionResult AppComponent() => PartialView();
public IActionResult ContactComponent() => PartialView();
public IActionResult IndexComponent() => PartialView();
}
}
这个新控制器将用于向我们的客户端 Angular 组件提供 HTML 模板或视图。
接下来,在 /Views 文件夹中创建一个名为 Partial 的文件夹,该文件夹与 /Views/Home 并列,我们的新视图将放在这里
接下来,将 /views/home 文件夹中的三个现有 ASP.NET MVC 文件 About.cshtml、Contact.cshtml 和 Index.cshtml 复制到新的 /views/partial 文件夹中。
复制完成后,将 /home/partial 中复制的文件重命名为 AboutComponent.cshtml、ContactComponent.cshtml 和 IndexComponent.cshtml。
我们现在可以清理 homecontroller.cs,删除我们不再需要的方法,以避免丢失页面或路由混乱。
所以,其中 HomeController.cs 是
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace A2SPA.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
public IActionResult Error()
{
return View();
}
}
}
将其更改为
using Microsoft.AspNetCore.Mvc;
namespace A2SPA.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
ViewData["Title"] = "Home";
return View();
}
public IActionResult Error()
{
return View();
}
}
}
通常,每个 ASP.NET MVC 视图都与 /views/shared/_layout.cshtml 共享大量通用代码,每个视图(index.cshtml、about.cstml、contacts.cshtml)都显示在共享布局中,其中内容通过 Razor 代码 @RenderBody()
标记,如下面的 layout.cshtml 摘录所示
...
<div class="container body-content">
@RenderBody()
<hr />
...
在这个新项目中,我们将继续使用 /views/home/index 和 /views/shared/_layout 来提供内容,然而,随着 Angular 2 在客户端接管,/views/partial/AppComponent.cshtml 视图将承担 _layout.cshtml 文件以前的一些任务,因为 AppComponent.cshtml 视图将用于加载其他 Angular 视图。
更新 /views/partial/AppComponent.cshtml 为以下内容,以启用使用 Angular 2 路由的菜单链接
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header" (click)="setTitle('Home - A2SPA')">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a routerLink="/home" routerLinkActive="active" class="navbar-brand">A2SPA</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>
<a class="nav-link" (click)="setTitle('Home - A2SPA')"
routerLink="/home" routerLinkActive="active">Home</a>
</li>
<li>
<a class="nav-link" (click)="setTitle('About - A2SPA')"
routerLink="/about">About</a>
</li>
<li>
<a class="nav-link" (click)="setTitle('Contact - A2SPA')"
routerLink="/contact">Contact</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container body-content">
<router-outlet></router-outlet>
@{
string razorServerSideData = "ASP.Net Core";
}
<hr />
<footer>
<p>© 2017 - A2SPA = (@razorServerSideData +
{{angularClientSideData}})<sup>2</sup></p>
</footer>
</div>
请注意新的 Angular 指令 <router-outlet>
将加载我们的 Angular 内容。
接下来更新 /views/home/index.cshtml 为此
<my-app>Loading AppComponent content here ...</my-app>
尽管我们仍将使用 /views/shared/_layout.cshtml,但需要对其进行修改,因为菜单不再在那里处理,而是在我们的新 AppComponent.cshtml 中处理,所以用此内容替换 _layout.cshtml 的当前内容
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - A2SPA</title>
<base href="~/">
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position"
asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
@RenderBody()
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<!-- Polyfill(s) for older browsers -->
<script src="/node_modules/core-js/client/shim.min.js"></script>
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="~/systemjs.config.js"></script>
<script>
System.import('app').catch(function (err) { console.error(err); });
</script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
<!-- Polyfill(s) for older browsers -->
<script src="/node_modules/core-js/client/shim.min.js"></script>
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="~/systemjs.config.js"></script>
<script>
System.import('app').catch(function (err) { console.error(err); });
</script>
</environment>
@RenderSection("scripts", required: false)
</body>
</html>
为了测试更新,我们需要更新我们的 app.component.ts 以指向由局部控制器驱动的新模板,如下所示
import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
@Component({
selector: 'my-app',
templateUrl: '/partial/appComponent'
})
export class AppComponent {
public constructor(private titleService: Title) { }
angularClientSideData = 'Angular';
public setTitle(newTitle: string) {
this.titleService.setTitle(newTitle);
}
}
Title 服务是一个特殊的服务,用于更改 Angular 2 中添加的 HTML 页面标题,因为控制器已无法位于 HTML <body>
之外。
为了确保我们的关于、联系和索引页面能够再次工作,我们需要添加路由以及每个页面的组件。
首先在 /wwwroot/app 文件夹中,创建以下三个组件
创建 about.component.ts,其中包含以下内容
import { Component } from '@angular/core';
@Component({
selector: 'my-about',
templateUrl: '/partial/aboutComponent'
})
export class AboutComponent {
}
接下来,创建 contact.component.ts,其中包含以下内容
import { Component } from '@angular/core';
@Component({
selector: 'my-contact',
templateUrl: '/partial/contactComponent'
})
export class ContactComponent {
}
最后,创建 index.component.ts,其中包含以下内容
import { Component } from '@angular/core';
@Component({
selector: 'my-index',
templateUrl: '/partial/indexComponent'
})
export class IndexComponent {
}
接下来我们添加路由逻辑,创建文件 app.routing.ts 并将以下代码添加到其中
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
import { IndexComponent } from './index.component';
import { ContactComponent } from './contact.component';
const appRoutes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: IndexComponent, data: { title: 'Home' } },
{ path: 'about', component: AboutComponent, data: { title: 'About' } },
{ path: 'contact', component: ContactComponent, data: { title: 'Contact' } }
];
export const routing = RouterModule.forRoot(appRoutes);
export const routedComponents = [AboutComponent, IndexComponent, ContactComponent];
为了包含这些新文件,我们将更新 app.module.ts 从这个
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
变为这样:
import { NgModule, enableProdMode } from '@angular/core';
import { BrowserModule, Title } from '@angular/platform-browser';
import { routing, routedComponents } from './app.routing';
import { APP_BASE_HREF, Location } from '@angular/common';
import { AppComponent } from './app.component';
// enableProdMode();
@NgModule({
imports: [BrowserModule, routing],
declarations: [AppComponent, routedComponents],
providers: [Title, { provide: APP_BASE_HREF, useValue: '/' }],
bootstrap: [AppComponent]
})
export class AppModule { }
我们将更新 main.ts 文件从这个
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
为了允许进一步的日志记录,我们使用这个代替
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.then((success: any) => console.log('App bootstrapped'))
.catch((err: any) => console.error(err));
现在,我们已经完成了第一个 ASP.NET Core + Angular 2 SPA 网站,从 /wwwroot 中删除初始的 Angular 2 index.html 文件,因为我们正在使用 ASP.NET Core /home/index 视图,它不再需要了。
最后,我们准备重新测试我们的 ASP.NET Core + Angular 2 SPA 网站,重新生成您的项目,按 Ctrl-F5 启动您的浏览器。
您应该可以导航到 根 目录,或 /home 或 /home/index 并且看到相同的内容,一个页面看起来像原始的标准 ASP.NET MVC /home/index 页面
点击“logo” A2SPA 或“Home”链接,应该再次显示与上面相同的页面。
点击菜单中的“About”,您应该看到
点击菜单中的“Contact”,我们看到
那么有什么变化呢?仔细查看底部的版权消息,应该会发现与原始消息有所不同
回顾我们的源代码,关于 /views/partial/AppComponent.cshtml,您会看到
<div class="container body-content">
<router-outlet></router-outlet>
@{
string razorServerSideData = "ASP.Net Core";
}
<hr />
<footer>
<p>© 2017 - A2SPA = (@razorServerSideData +
{{angularClientSideData}})<sup>2</sup></p>
</footer>
</div>
其中包含在服务器端执行的 Razor 标记示例、“纯粹的”HTML(原样提供),以及一个 Angular 2 指令“<router-outlet>
”,Angular 模板被推送到客户端(浏览器内部),最后是一个 Angular 2 数据绑定示例 {{angularClientSideData}
},它也在客户端执行。
在本次系列的 第二部分 中,我们将继续扩展这些概念,包括自定义标签助手、在服务器端针对数据模型执行的 C# 代码,使我们能够一次性创建包含 HTML、Angular 验证和 Bootstrap 样式的表单元素或整个表单,并在一处创建 - 您的代码是“DRY”(遵循“不要重复自己”的理想),避免了 SPA 中常见的复制粘贴。
第三部分 扩展了数据服务,添加了 EF Core / Entity Framework 和一个简单的 SQL 后端,包括一些示例数据,然后提供了用于数据录入的标签助手类。
第四部分将添加令牌验证,并开始使用 Swagger 工具动态创建 Angular 数据类型定义和服务。后续文章将涵盖在商业应用程序中使用该框架的一些实际方面以及如何发布到 IIS。
获取源代码
如果您想节省时间,此项目的最新源代码在 GitHub 此处。
请记住,这是使用 Visual Studio 2015 和最新工具在 2017 年 1 月份创建的。
注意:如果您在 VS 2015 解决方案中使用 Visual 2017 RC,它将执行一次性转换。如果有足够多的兴趣,我将创建一个涵盖 VS 2017 步骤的文章,尽管它们与此处显示的内容非常相似。
历史
- 2017年1月22日:初始版本
即将推出:自定义标签助手、数据服务和基于令牌的安全。