百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

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

yuyutoo 2024-12-03 19:37 1 浏览 0 评论

单页应用程序 (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 UseStaticFiles
app.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 上启动。 在浏览器中导航到 http://localhost:5000 会显示登陆页面。

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

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

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

.NET CLI

dotnet publish -c Release

相关推荐

史上最全的浏览器兼容性问题和解决方案

微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(页底留言开放,欢迎来吐槽)●●●...

平面设计基础知识_平面设计基础知识实验收获与总结
平面设计基础知识_平面设计基础知识实验收获与总结

CSS构造颜色,背景与图像1.使用span更好的控制文本中局部区域的文本:文本;2.使用display属性提供区块转变:display:inline(是内联的...

2025-02-21 16:01 yuyutoo

写作排版简单三步就行-工具篇_作文排版模板

和我们工作中日常word排版内部交流不同,这篇教程介绍的写作排版主要是用于“微信公众号、头条号”网络展示。写作展现的是我的思考,排版是让写作在网格上更好地展现。在写作上花费时间是有累积复利优势的,在排...

写一个2048的游戏_2048小游戏功能实现

1.创建HTML文件1.打开一个文本编辑器,例如Notepad++、SublimeText、VisualStudioCode等。2.将以下HTML代码复制并粘贴到文本编辑器中:html...

今天你穿“短袖”了吗?青岛最高23℃!接下来几天气温更刺激……

  最近的天气暖和得让很多小伙伴们喊“热”!!!  昨天的气温到底升得有多高呢?你家有没有榜上有名?...

CSS不规则卡片,纯CSS制作优惠券样式,CSS实现锯齿样式

之前也有写过CSS优惠券样式《CSS3径向渐变实现优惠券波浪造型》,这次再来温习一遍,并且将更为详细的讲解,从布局到具体样式说明,最后定义CSS变量,自定义主题颜色。布局...

柠檬科技肖勃飞:大数据风控助力信用社会建设

...

你的自我界限够强大吗?_你的自我界限够强大吗英文

我的结果:A、该设立新的界限...

行内元素与块级元素,以及区别_行内元素和块级元素有什么区别?

行内元素与块级元素首先,CSS规范规定,每个元素都有display属性,确定该元素的类型,每个元素都有默认的display值,分别为块级(block)、行内(inline)。块级元素:(以下列举比较常...

让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华
让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华

去年的两会期间,习近平总书记在参加人大会议四川代表团审议时,对治蜀兴川提出了明确要求,指明了前行方向,并带来了“祝四川人民的生活越来越安逸”的美好祝福。又是一年...

2025-02-21 16:00 yuyutoo

今年国家综合性消防救援队伍计划招录消防员15000名

记者24日从应急管理部获悉,国家综合性消防救援队伍2023年消防员招录工作已正式启动。今年共计划招录消防员15000名,其中高校应届毕业生5000名、退役士兵5000名、社会青年5000名。本次招录的...

一起盘点最新 Chrome v133 的5大主流特性 ?

1.CSS的高级attr()方法CSSattr()函数是CSSLevel5中用于检索DOM元素的属性值并将其用于CSS属性值,类似于var()函数替换自定义属性值的方式。...

竞走团体世锦赛5月太仓举行 世界冠军杨家玉担任形象大使

style="text-align:center;"data-mce-style="text-align:...

学物理能做什么?_学物理能做什么 卢昌海

作者:曹则贤中国科学院物理研究所原标题:《物理学:ASourceofPowerforMan》在2006年中央电视台《对话》栏目的某期节目中,主持人问过我一个的问题:“学物理的人,如果日后不...

你不知道的关于这只眯眼兔的6个小秘密
你不知道的关于这只眯眼兔的6个小秘密

在你们忙着给熊本君做表情包的时候,要知道,最先在网络上引起轰动的可是这只脸上只有两条缝的兔子——兔斯基。今年,它更是迎来了自己的10岁生日。①关于德艺双馨“老艺...

2025-02-21 16:00 yuyutoo

取消回复欢迎 发表评论: