单页应用程序 (SPA) 因其固有的丰富用户体验而成为一种常用的 Web 应用程序。 将客户端 SPA 框架或库(如角度或响应)与服务器端框架(如 ASP.NET Core)相集成可能比较困难。 开发 JavaScript 服务是为了减少集成过程中的摩擦。 使用它可以在不同的客户端和服务器技术堆栈之间进行无缝操作。
警告
本文中所述的功能从 ASP.NET Core 3.0 过时。 SpaServices NuGet 包中提供了更简单的 SPA 框架集成机制。 有关详细信息,请参阅[公告] Obsoleting AspNetCore. SpaServices 和 AspNetCore。
JavaScript 服务是用于 ASP.NET Core 的客户端技术的集合。 其目标是将 ASP.NET Core 定位为开发人员用于构建 SPA 的首选服务器端平台。
JavaScript 服务由两个不同的 NuGet 包组成:
这些包在以下情况下很有用:
在本文的重点放在使用 SpaServices 包。
创建 SpaServices 是为了将 ASP.NET Core 定位为开发人员构建 SPA 的首选服务器端平台。 SpaServices 不需要使用 ASP.NET Core 开发 Spa,它也不会将开发人员锁定到特定的客户端框架。
SpaServices 提供有用的基础结构,例如:
总体来说,这些基础结构组件增强了开发工作流和运行时体验。 组件可单独采用。
若要使用 SpaServices,安装以下组件:
Node.js (6 或更高版本) 与 npm
若要验证这些组件安装,并可找到,运行以下命令从命令行:
node -v && npm -v
如果部署到 Azure 网站,则不需要执行任何操作—在服务器环境中安装了 node.js。
Microsoft.AspNetCore.SpaServices NuGet 包
通用(也称“同构”)应用程序是在服务器和客户端上都能运行的 JavaScript 应用程序。 Angular、React 和其他常用框架提供了一个适合此应用程序开发风格的通用平台。 这其中的理念是,先通过 Node.js 在服务器上呈现框架组件,然后将下一步的执行操作委托到客户端。
ASP.NET Core标记帮助程序由 SpaServices 简化通过调用服务器上的 JavaScript 函数的服务器端预呈现的实现。
安装aspnet 预呈现的npm 包:
npm i -S aspnet-prerendering
标记帮助程序在项目的供发现通过命名空间注册 _ViewImports.cshtml文件:
@using SpaServicesSampleApp @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" @addTagHelper "*, Microsoft.AspNetCore.SpaServices"
这些标记帮助程序抽象出与低级 Api 直接通信通过利用 Razor 视图中的类似于 HTML 的语法的复杂性:
<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
asp-prerender-module
标记帮助程序,使用在前面的代码示例中,执行ClientApp/dist/main-server.js通过 Node.js 服务器上。 为清晰起见main server.js文件是一个项目中的 TypeScript JavaScript 的转译任务Webpack生成过程。 Webpack 定义入口点的别名main-server
; 并遍历此别名的依赖项关系图的开始处ClientApp/启动 server.ts文件:
entry: { 'main-server': './ClientApp/boot-server.ts' },
在下述 Angular 示例中, ClientApp/boot-server.ts文件利用aspnet-prerendering
npm 包的createServerRenderer
函数和RenderResult
类型通过 Node.js 来配置服务器呈现。 需要在服务器端呈现的 HTML 标记会传递给一个解析函数调用,该调用包装在强类型化的 JavaScript Promise
对象中。 Promise
对象的意义在于,它以异步方式将 HTML 标记提供给页面,以便该标记能够注入到 DOM 的占位符元素中。
import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; export default createServerRenderer(params => { const providers = [ { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } }, { provide: 'ORIGIN_URL', useValue: params.origin } ]; return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => { const appRef = moduleRef.injector.get(ApplicationRef); const state = moduleRef.injector.get(PlatformState); const zone = moduleRef.injector.get(NgZone); return new Promise<RenderResult>((resolve, reject) => { zone.onError.subscribe(errorInfo => reject(errorInfo)); appRef.isStable.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(); }); }); }); }); });
当结合asp-prerender-module
标记帮助程序asp-prerender-data
标记帮助程序可用于将从 Razor 视图的上下文信息传递到服务器端 JavaScript。 例如,以下标记将传递到的用户数据main-server
模块:
<app asp-prerender-module="ClientApp/dist/main-server" asp-prerender-data='new { UserName = "John Doe" }'>Loading...</app>
收到UserName
参数使用内置的 JSON 序列化程序序列化并存储在params.data
对象。 在以下 Angular 示例中,数据用于在 h1
元素中构造个性化的问候语:
import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; export default createServerRenderer(params => { const providers = [ { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } }, { provide: 'ORIGIN_URL', useValue: params.origin } ]; return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => { const appRef = moduleRef.injector.get(ApplicationRef); const state = moduleRef.injector.get(PlatformState); const zone = moduleRef.injector.get(NgZone); return new Promise<RenderResult>((resolve, reject) => { const result = `<h1>Hello, ${params.data.userName}</h1>`; zone.onError.subscribe(errorInfo => reject(errorInfo)); appRef.isStable.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: result }); moduleRef.destroy(); }); }); }); }); });
在标记帮助器中传递的属性名称用PascalCase表示法表示。 为 JavaScript,其中,相同的属性名称表示与对比驼峰式大小写。 默认 JSON 序列化配置负责这种差异。
若要展开在前面的代码示例时,数据可从服务器到视图通过传递 hydratingglobals
属性提供给resolve
函数:
import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; export default createServerRenderer(params => { const providers = [ { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } }, { provide: 'ORIGIN_URL', useValue: params.origin } ]; return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => { const appRef = moduleRef.injector.get(ApplicationRef); const state = moduleRef.injector.get(PlatformState); const zone = moduleRef.injector.get(NgZone); return new Promise<RenderResult>((resolve, reject) => { const result = `<h1>Hello, ${params.data.userName}</h1>`; zone.onError.subscribe(errorInfo => reject(errorInfo)); appRef.isStable.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: result, globals: { postList: [ 'Introduction to ASP.NET Core', 'Making apps with Angular and ASP.NET Core' ] } }); moduleRef.destroy(); }); }); }); }); });
postList
内部定义的数组globals
对象附加到浏览器的全局window
对象。 为全局作用域此变量提升可消除重复工作,特别是因为它与加载一次在服务器上,再次在客户端上的相同数据。
Webpack 开发中间件引入了 Webpack 按需生成资源的由此简化了的开发工作流。 中间件会自动编译并在浏览器中重新加载页面时提供客户端的资源。 另一种方法是手动 Webpack 调用通过项目的 npm 生成脚本的第三方依赖项或自定义代码发生更改时。 Npm 生成脚本package.json文件显示在下面的示例:
"build": "npm run build:vendor && npm run build:custom",
安装webpack npm 包:
npm i -D aspnet-webpack
到 HTTP 请求管道中的以下代码通过注册 Webpack 开发中间件Startup.cs文件的Configure
方法:
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseWebpackDevMiddleware(); } else { app.UseExceptionHandler("/Home/Error"); } // Call UseWebpackDevMiddleware before UseStaticFiles app.UseStaticFiles();
UseWebpackDevMiddleware
前必须调用扩展方法注册静态文件托管通过UseStaticFiles
扩展方法。 出于安全原因,仅当应用程序在开发模式下运行时注册该中间件。
Webpack.config.js文件的output.publicPath
属性会指示要观看的中间件dist
文件夹的更改:
module.exports = (env) => { output: { filename: '[name].js', publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix },
Webpack 的思考动态模块更换(HMR) 功能作为一种演变Webpack 开发中间件。 HMR 引入了完全相同的好处,但它进一步简化开发工作流通过自动编译所做的更改后更新页面内容。 不要混淆这与刷新浏览器中,这会干扰的当前内存中状态和 SPA 的调试会话。 没有 Webpack 开发中间件服务和浏览器中,这意味着更改推送到浏览器之间的活动链接。
安装webpack- npm 包:
npm i -D webpack-hot-middleware
HMR 组件必须注册到 MVC 的 HTTP 请求管道中Configure
方法:
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { HotModuleReplacement = true });
作为是如此Webpack 开发中间件,则UseWebpackDevMiddleware
前必须调用扩展方法UseStaticFiles
扩展方法。 出于安全原因,仅当应用程序在开发模式下运行时注册该中间件。
Webpack.config.js文件必须定义plugins
,即使它保留为空数组:
module.exports = (env) => { plugins: [new CheckerPlugin()]
加载后在浏览器中的应用,开发人员工具的控制台选项卡提供了 HMR 激活的确认:
在大多数基于 ASP.NET Core 的 Spa 中,通常还需要客户端路由,而不是服务器端路由。 SPA 和 MVC 路由系统可以不受干扰地独立工作。 没有,但是,一个边缘事例造成面临的难题: 标识 404 HTTP 响应。
请考虑在该方案中的无扩展名路由/some/page
使用。 假定该请求不模式匹配的服务器端的路由,但其模式匹配的客户端的路由。 现在,考虑的传入请求/images/user-512.png
,这通常需要查找服务器上的图像文件。 如果请求的资源路径与任何服务器端路由或静态文件都不匹配,则客户端应用程序可能会处理它,—通常返回 404 HTTP 状态代码。
安装客户端路由 npm 包。 使用 Angular 作为示例:
npm i -S @angular/router
名为的扩展方法MapSpaFallbackRoute
中使用Configure
方法:
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapSpaFallbackRoute( name: "spa-fallback", defaults: new { controller = "Home", action = "Index" }); });
路由按其配置顺序进行评估。 因此,default
进行模式匹配第一次使用在前面的代码示例中的路由。
JavaScript 服务提供预配置的应用程序模板。 在这些模板中,SpaServices 与不同的框架和库(如角度、反应和 Redux)一起使用。
可以通过.NET Core CLI 安装这些模板,通过运行以下命令:
dotnet new --install Microsoft.AspNetCore.SpaTemplates::*
显示可用的 SPA 模板的列表:
模板 | 短名称 | Language | Tags |
---|---|---|---|
带 Angular 的 MVC ASP.NET Core | angular | [C#] | Web/MVC/SPA |
带有 React.js 的 MVC ASP.NET Core | react | [C#] | Web/MVC/SPA |
含 React.js 和 Redux 的 MVC ASP.NET Core | reactredux | [C#] | Web/MVC/SPA |
若要使用 SPA 模板之一创建新项目,请在 dotnet new 命令中包括模板的短名称。 以下命令创建 Angular 应用程序,并为服务器端配置 ASP.NET Core MVC:
dotnet new angular
存在两种主要的运行时配置模式:
ASP.NET Core 使用名为的环境变量ASPNETCORE_ENVIRONMENT
来存储配置模式。 有关详细信息,请参阅设置环境。
通过在项目根目录运行以下命令还原所需的 NuGet 和 npm 包:
dotnet restore && npm i
生成并运行应用程序:
dotnet run
在应用程序根据本地主机上启动运行时配置模式。 导航到 http://localhost:5000
在浏览器中显示的登录页。
打开 .csproj生成的文件dotnet 新命令。 在项目中打开时自动还原所需的 NuGet 和 npm 包。 此还原过程可能需要几分钟时间,并在应用程序已准备好在它完成后运行。 单击绿色的运行的按钮或按Ctrl + F5
,并在浏览器打开到应用程序的登录页。 应用程序运行于 localhost 根据运行时配置模式。
SpaServices 模板是预配置为运行客户端的测试使用Karma并Jasmine。 Jasmine 是常用的单元测试框架,适用于 JavaScript,而 Karma 是这些测试的测试运行程序。 Karma 配置为使用Webpack 开发中间件这样开发人员不需要停止并运行测试,每次进行更改。 无论是针对测试用例或测试用例本身运行的代码,则将自动运行测试。
以 Angular 应用程序为例,我们在 CounterComponent
文件中为 counter.component.spec.ts提供了两个 Jasmine 测试用例:
it('should display a title', async(() => { const titleText = fixture.nativeElement.querySelector('h1').textContent; expect(titleText).toEqual('Counter'); })); it('should start with count 0, then increments by 1 when clicked', async(() => { const countElement = fixture.nativeElement.querySelector('strong'); expect(countElement.textContent).toEqual('0'); const incrementButton = fixture.nativeElement.querySelector('button'); incrementButton.click(); fixture.detectChanges(); expect(countElement.textContent).toEqual('1'); }));
打开命令提示符中ClientApp目录。 运行下面的命令:
npm test
该脚本将启动 Karma 测试运行程序,其内容中定义的设置karma.conf.js文件。 在其他设置karma.conf.js识别的测试文件,通过执行其files
数组:
module.exports = function (config) { config.set({ files: [ '../../wwwroot/dist/vendor.js', './boot-tests.ts' ],
有关发布到 Azure 的详细信息,请参阅此 GitHub 问题。
将生成的客户端的资产和已发布的 ASP.NET Core 项目合并为随时可部署的包可能会很麻烦。 幸运的是,SpaServices 协调与名为的自定义 MSBuild 目标的整个发布过程RunWebpack
:
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish"> <!-- As part of publishing, ensure the JS resources are freshly built in production mode --> <Exec Command="npm install" /> <Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" /> <Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" /> <!-- Include the newly-built files in the publish output --> <ItemGroup> <DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" /> <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> <RelativePath>%(DistFiles.Identity)</RelativePath> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> </ResolvedFileToPublish> </ItemGroup> </Target>
MSBuild 目标具有下列职责:
运行时,会调用 MSBuild 目标:
dotnet publish -c Release