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

通过Scott AddieFiyaz Hasan

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

警告

本文中所述的功能从 ASP.NET Core 3.0 过时。 SpaServices NuGet 包中提供了更简单的 SPA 框架集成机制。 有关详细信息,请参阅[公告] Obsoleting AspNetCore. SpaServices 和 AspNetCore。

什么是 JavaScript 服务

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

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

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

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

在本文的重点放在使用 SpaServices 包。

什么是 SpaServices

创建 SpaServices 是为了将 ASP.NET Core 定位为开发人员构建 SPA 的首选服务器端平台。 SpaServices 不需要使用 ASP.NET Core 开发 Spa,它也不会将开发人员锁定到特定的客户端框架。

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

总体来说,这些基础结构组件增强了开发工作流和运行时体验。 组件可单独采用。

使用 SpaServices 的先决条件

若要使用 SpaServices,安装以下组件:

  • Node.js (6 或更高版本) 与 npm

    • 若要验证这些组件安装,并可找到,运行以下命令从命令行:

      node -v && npm -v
      
    • 如果部署到 Azure 网站,则不需要执行任何操作—在服务器环境中安装了 node.js。

  • .NET Core SDK 2.0 或更高版本

    • 在使用 Visual Studio 2017 的 Windows 上,通过选择 .Net Core 跨平台开发工作负载来安装 SDK。
  • 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 预呈现模块标记帮助程序

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-prerenderingnpm 包的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 预呈现-数据标记帮助程序

当结合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对象。 为全局作用域此变量提升可消除重复工作,特别是因为它与加载一次在服务器上,再次在客户端上的相同数据。

附加到窗口对象的全局 postList 变量

Webpack 开发中间件

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

"build": "npm run build:vendor && npm run build:custom",

Webpack Dev 中间件先决条件

安装webpack npm 包:

npm i -D aspnet-webpack

Webpack Dev 中间件配置

到 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来存储配置模式。 有关详细信息,请参阅设置环境

运行 .NET Core CLI

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

dotnet restore && npm i

生成并运行应用程序:

dotnet run

在应用程序根据本地主机上启动运行时配置模式 导航到 http://localhost:5000 在浏览器中显示的登录页。

运行 Visual Studio 2017

打开 .csproj生成的文件dotnet 新命令。 在项目中打开时自动还原所需的 NuGet 和 npm 包。 此还原过程可能需要几分钟时间,并在应用程序已准备好在它完成后运行。 单击绿色的运行的按钮或按Ctrl + F5,并在浏览器打开到应用程序的登录页。 应用程序运行于 localhost 根据运行时配置模式

测试应用

SpaServices 模板是预配置为运行客户端的测试使用KarmaJasmine 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 目标具有下列职责:

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

运行时,会调用 MSBuild 目标:

dotnet publish -c Release

其他资源

上一篇:通过 ASP.NET Core 使用带 Redux 的 React 项目模板

下一篇:通过 LibMan 在 ASP.NET Core 中获取客户端库

关注微信小程序
程序员编程王-随时随地学编程

扫描二维码
程序员编程王

扫一扫关注最新编程教程