龙空技术网

使用 JavaScript Services 在 ASP.NET Core 中创建单页应用程序

老衲法号不行 507

前言:

现在大家对“2017aspnet跨平台”大致比较关切,朋友们都想要知道一些“2017aspnet跨平台”的相关资讯。那么小编同时在网上汇集了一些关于“2017aspnet跨平台””的相关资讯,希望兄弟们能喜欢,看官们快快来学习一下吧!

单页应用程序 (SPA) 因其固有的丰富用户体验而成为一种常用的 Web 应用程序。 将客户端 SPA 框架或库(例如 Angular 或 React)与服务器端框架(例如 ASP.NET Core)集成在一起可能会很困难。 开发 JavaScript Services 就是为了减少集成过程中的摩擦。 使用它可以在不同的客户端和服务器技术堆栈之间无缝操作。

警告

本文所述的功能自 ASP.NET Core 3.0 起被弃用。 Microsoft.AspNetCore.SpaServices.Extensions NuGet 包提供了一种更简单的 SPA 框架集成机制。 有关详细信息,请参阅 [Announcement] Obsoleting Microsoft.AspNetCore.SpaServices and Microsoft.AspNetCore.NodeServices([公告] 弃用 Microsoft.AspNetCore.SpaServices 和 Microsoft.AspNetCore.NodeServices)。

什么是 JavaScript Services

JavaScript Services 是用于 ASP.NET Core 的客户端技术集合。 其目标是将 ASP.NET Core 定位为开发人员生成 SPA 时的首选服务器端平台。

JavaScript Services 由两个不同的 NuGet 包组成:

Microsoft.AspNetCore.NodeServices (NodeServices)Microsoft.AspNetCore.SpaServices (SpaServices)

这些包在以下情况下很有用:

在服务器上运行 JavaScript使用 SPA 框架或库通过 Webpack 生成客户端资产

本文重点介绍了如何使用 SpaServices 包。

什么是 SpaServices

创建 SpaServices 的目的是将 ASP.NET Core 定位为开发人员生成 SPA 时的首选服务器端平台。 使用 ASP.NET Core 开发 SPA 时不一定要使用 SpaServices,SpaServices 也不会将开发人员束缚在特定的客户端框架中。

SpaServices 可提供有用的基础结构,例如:

服务器端预呈现Webpack 开发中间件热模块更换路由帮助程序

将这些基础结构组件结合使用时,可增强开发工作流和运行时体验。 这些组件也可单独使用。

使用 SpaServices 的先决条件

若要使用 SpaServices,请安装以下各项:

带有 npm 的 Node.js(版本 6 或更高版本)若要确保已安装并且可找到这些组件,请从命令行运行以下命令:控制台复制node -v && npm -v 如果部署到 Azure 网站,则无需执行任何操作 — 已经在服务器环境中安装 Node.js,并且 Node.js 可供使用。.NET Core SDK 2.0 或更高版本在使用 Visual Studio 2017 的 Windows 上,通过选择“.NET Core 跨平台开发”工作负载来安装该 SDK。Microsoft.AspNetCore.SpaServices NuGet 包服务器端预呈现

通用(也称为同构)应用程序是一种能够在服务器和客户端上运行的 JavaScript 应用程序。 Angular、React 和其他常用框架针对这种应用程序开发风格提供一个通用平台。 其思路是先通过 Node.js 在服务器上呈现框架组件,然后将进一步的执行任务委托给客户端。

SpaServices 提供的 ASP.NET Core 标记帮助程序通过调用服务器上的 JavaScript 函数来简化服务器端预呈现的实现。

服务器端预呈现的先决条件

安装 aspnet-prerendering npm 包:

控制台

npm i -S aspnet-prerendering
服务器端预呈现配置

可以通过在项目的 _ViewImports.cshtml 文件中注册命名空间来发现标记帮助程序:

CSHTML

@using SpaServicesSampleApp@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"@addTagHelper "*, Microsoft.AspNetCore.SpaServices"

这些标记帮助程序通过在 Razor 视图中利用类似 HTML 的语法来抽象化与低级 API 直接通信的复杂性:

CSHTML

<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
asp-prerender-module 标记帮助程序

上面的代码示例中使用的 asp-prerender-module 标记帮助程序通过 Node.js 在服务器上执行 ClientApp/dist/main-server.js。 为清楚起见,main-server.js 文件是 Webpack 生成过程中 TypeScript 到 JavaScript 转译任务的产物。 Webpack 定义了入口点别名 main-server;此别名的依赖项关系图遍历始于 ClientApp/boot-server.ts 文件:

JavaScript

entry: { 'main-server': './ClientApp/boot-server.ts' },

在以下 Angular 示例中,ClientApp/boot-server.ts 文件利用 createServerRenderer 函数和 aspnet-prerendering npm 包的 RenderResult 类型通过 Node.js 来配置服务器呈现。 用于服务器端呈现的 HTML 标记传递到解析函数调用,该调用包装在强类型的 JavaScript Promise 对象中。 Promise 对象的意义在于,它以异步方式将 HTML 标记提供给页面,以注入到 DOM 的占位符元素中。

TypeScript

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-data 标记帮助程序

与 asp-prerender-module 标记帮助程序结合使用时,asp-prerender-data 标记帮助程序可用于将上下文信息从 Razor 视图传递到服务器端 JavaScript。 例如,以下标记将用户数据传递到 main-server 模块:

CSHTML

<app asp-prerender-module="ClientApp/dist/main-server"        asp-prerender-data='new {            UserName = "John Doe"        }'>Loading...</app>

收到的 UserName 参数使用内置的 JSON 序列化程序进行序列化,并存储在 params.data 对象中。 在以下 Angular 示例中,该数据用于在 h1 元素内构造个性化问候语:

TypeScript

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 用 camelCase 表示相同的属性名称。 默认的 JSON 序列化配置是造成这种差异的原因所在。

若要扩展上面的代码示例,可以通过解冻提供给 resolve 函数的 globals 属性,将数据从服务器传递到视图:

TypeScript

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();                });            });        });    });});

globals 对象中定义的 postList 数组附加到浏览器的全局 window 对象。 将此变量提升到全局范围可消除重复的工作,特别是在服务器上加载了一次数据,之后又在客户端上加载相同的数据时。

Webpack 开发中间件

Webpack 开发中间件引入了简化的开发工作流,Webpack 可根据该工作流按需生成资源。 在浏览器中重新加载页面时,该中间件会自动编译并提供客户端资源。 另一种方法是在第三方依赖项或自定义代码发生更改时,通过项目的 npm 生成脚本手动调用 Webpack。 以下示例显示了 package.json 文件中的 npm 生成脚本:

JSON

"build": "npm run build:vendor && npm run build:custom",
Webpack 开发中间件的先决条件

安装 aspnet-webpack npm 包:

控制台复制

npm i -D aspnet-webpack
Webpack 开发中间件配置

Webpack 开发中间件通过 Startup.cs 文件的 Configure 方法中的以下代码注册到 HTTP 请求管道中:

C#

if (env.IsDevelopment()){    app.UseDeveloperExceptionPage();    app.UseWebpackDevMiddleware();}else{    app.UseExceptionHandler("/Home/Error");}// Call UseWebpackDevMiddleware before UseStaticFilesapp.UseStaticFiles();

通过 UseStaticFiles 扩展方法注册静态文件托管之前,必须先调用 UseWebpackDevMiddleware 扩展方法。 出于安全原因,仅在应用以开发模式运行时才注册该中间件。

webpack.config.js 文件的 output.publicPath 属性指示中间件监视 dist 文件夹中的更改:

JavaScript

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-hot-middleware npm 包:

控制台

npm i -D webpack-hot-middleware
热模块更换配置

必须在 Configure 方法中将 HMR 组件注册到 MVC 的 HTTP 请求管道:

C#

app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {    HotModuleReplacement = true});

与 Webpack 开发中间件一样,调用 UseStaticFiles 扩展方法之前,必须先调用 UseWebpackDevMiddleware 扩展方法。 出于安全原因,仅在应用以开发模式运行时才注册该中间件。

webpack.config.js 文件必须定义一个 plugins 数组,即便将其留空亦可:

JavaScript

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
路由帮助程序配置

在 Configure 方法中使用名为 MapSpaFallbackRoute 的扩展方法:

C#

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 Services 提供预配置的应用程序模板。 在这些模板中,SpaServices 与各种框架和库(例如 Angular、React 和 Redux)结合使用。

可以通过使用 .NET Core CLI 运行以下命令来安装这些模板:

.NET CLI

dotnet new --install Microsoft.AspNetCore.SpaTemplates::*

系统会显示可用 SPA 模板的列表:

若要使用其中一个 SPA 模板创建新项目,请在 dotnet new 命令中包含该模板的 短名称。 以下命令将使用为服务器端配置的 ASP.NET Core MVC 创建 Angular 应用程序:

.NET CLI

dotnet new angular
设置运行时配置模式

存在两种主要运行时配置模式:

开发:包含源映射以简化调试。不优化客户端代码的性能。生产:不包含源映射。通过捆绑和缩小来优化客户端代码。

ASP.NET Core 使用名为 ASPNETCORE_ENVIRONMENT 的环境变量来存储配置模式。 有关详细信息,请参阅设置环境。

使用 .NET Core CLI 运行

通过在项目根目录下运行以下命令来还原所需的 NuGet 和 npm 包:

.NET CLI

dotnet restore && npm i

生成并运行应用程序:

.NET CLI

dotnet run

应用程序根据运行时配置模式在 localhost 上启动。 在浏览器中导航到 会显示登陆页面。

使用 Visual Studio 2017 运行

打开由 dotnet new 命令生成的 .csproj 文件。 所需的 NuGet 和 npm 包在项目打开时会自动还原。 此还原过程可能需要几分钟的时间,应用程序在此过程完成后即可运行。 单击绿色的运行按钮或按 Ctrl + F5,浏览器将打开到应用程序的登陆页面。 应用程序根据运行时配置模式在 localhost 上运行。

测试应用

SpaServices 模板已预先配置为使用 Karma 和 Jasmine 运行客户端测试。 Jasmine 是适用于 JavaScript 的常用单元测试框架,而 Karma 是这些测试的测试运行程序。 Karma 配置为使用 Webpack 开发中间件,使开发人员无需在每次进行更改时都停止并运行测试。 无论是针对测试用例运行的代码还是测试用例本身,测试都会自动运行。

以 Angular 应用程序为例,系统已经为 counter.component.spec.ts 文件中的 CounterComponent 提供了两个 Jasmine 测试用例:

TypeScript

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 数组标识要执行的测试文件:

JavaScript复制

module.exports = function (config) {    config.set({        files: [            '../../wwwroot/dist/vendor.js',            './boot-tests.ts'        ],
发布应用

有关发布到 Azure 的详细信息,请参阅此 GitHub 问题。

将生成的客户端资产和已发布的 ASP.NET Core 项目组合成一个可即时部署的包的过程可能会很繁琐。 值得庆幸的是,SpaServices 可使用名为 RunWebpack 的自定义 MSBuild 目标来协调整个发布过程:

XML

<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 目标具有以下职责:

还原 npm 包。创建第三方客户端资产的生产级生成。创建自定义客户端资产的生产级生成。将 Webpack 生成的资产复制到发布文件夹。

运行以下命令时将调用该 MSBuild 目标:

.NET CLI

dotnet publish -c Release

标签: #2017aspnet跨平台 #aspnet调用js函数