【译】单页面应用与ASP.NET Core 3.0

1,426 阅读9分钟

原文链接

随着Angular,React,Vue以及其他一些前端技术的成熟,Web开发在过去的几年中已经发生了很大的改变,从建立Web页面转向建立应用程序,同时从在服务端渲染标记,转向直接在浏览器中渲染。大部分情况下,这使得基于浏览器的使用增加。但是随着开发者逐渐过渡到前端开发,许多人都有着这样的疑问,我们还应该使用ASP.NET吗?

单页面应用(简称SPA)并不是新生事物。但是SPA现在正在以各种方式“蚕食” web开发世界。对于有些网站,例如Facebook和Twitter,转到SPA确实很有用,这就是为什么一些SPA框架直接来自于负责这些网站的公司。世界各地的开发者都意识到并为他们的网站实现了SPA,尤其是企业应用。

如果你了解我,你会知道我喜欢前端开发,但是术语“SPA”却使我不寒而栗。我更愿意将这些前端框架,像Angular,React以及Vue视为扩大网站的一个方式。建立一个巨大的SPA,我们似乎又走回了Visual Basic 5时期建立整体应用程序的老路。

我将这些框架视为建立一个个功能岛的途径,专注于前端代码最擅长的部分——用户界面。如果你想在网站中使用SPA代替所有功能,就不得不依靠其他方法来实现搜索引擎优化,并模拟登陆URL,使它看起来像一个网站。除非你要建立下一个Twitter或Facebook,不然这完全没有必要。

在APS.NET Core中使用SPA框架

如果你是个APS.NET Core的新手,你可能不知道有好几种使用SPA框架的方法:

  • 微软有针对Angular和React内建的中间件
  • 将SPA放在单独的文件夹中,并构建进ASP.NET Core项目中
  • 直接集成SPA框架到项目中

接下来我们逐一讨论这几种方式。

SPA中间件

微软提供了中间件支持使用SPA。中间件采用了不干预的方式,仅仅是让SPA位于一个子文件夹中并可以直接独立工作。下面是它工作的方式。

如何使用中间件

在Visual Studio中,可以直接选择项目模板,如下图所示:

Figure1

这种方式是将一整个SPA项目,放在一个ClientApp文件夹中,如下图所示:

Figure2

在ASP.NET Core 3中,SPA通过中间件工作,在代码中指定文件夹(名称可以任取),实现方式是在Startup配置中,将集成这个中间件作为最后一个步骤:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {

      // ...

      if (!env .IsDevelopment())
      {
        app.UseSpaStaticFiles();
      }

      app.UseRouting();

      app.UseEndpoints(endpoints =>
      {
        endpoints.MapControllerRoute(
                  name: "default",
                  pattern: "{controller}/{action=Index}/{id?}");
      });

      app.UseSpa(spa =>
      {
        // To learn more about options for serving an Angular SPA from ASP.NET Core,
        // see https://go.microsoft.com/fwlink/?linkid=864501

        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
          spa.UseAngularCliServer(npmScript: "start");
        }
      });
    }

有两个方法调用:UseSpaStaticFiles以及UseSpa。这些方法使得有能力调用Angular-CLI提供的功能来生成index.html。在上面的例子中,你会看到在开发过程中,会自动执行npm start

中间件作用于React的方式大同小异。目前,还不支持其他的框架(例如Vue,Backbone等等)。其他的限制是虽然有一个旧版的中间件可以用于ASP.NET Core 2.0,新版本仅支持ASP.NET Core 2.1及以上。

将SPA作为ASP.NET Core的子文件夹

如果不需要使用其他Web页面的话,中间件是一个好的选择。但是,我想对于大部分的开发者来说,将ASP.NET Core集成到传统MVC应用中会更常见。你可以将SPA作为一个单独的文件夹,并将它作为一个子项目,不用在这两个项目中做太多的集成。

如何使用SPA子文件夹

例如,假设你有一个Angular项目,叫ClientApp,如下图所示:

Figure3

因为这个目录有它自己的package.json,你可以使用CLI来构建。你甚至可以单独开发而不需引用ASP.NET Core项目,但是因为我经常将SPA添加到已有的Razor视图,通常我会让它们一起运行。

有个问题是SPA项目的输出目录通常需要修改。你可以使用SpaStaticFiles中间件来解决这个问题,但是我通常是修改SPA的配置文件将输出修改到wwwroot目录。例如,这是我的angular.json(使用Vue CLI)文件:

{
  ...
  "projects": {
    "ng-core": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      ...
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "../wwwroot/client",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            ...

在这种情况下,我仅仅告诉Vue CLI将结果输出到wwwroot,因此我不需要配置ASP.NET Core。如果你不想使用SpaStaticFiles中间件的话,Angular和React都支持这种输出方式。

通过包含更多的子文件夹,你可以将多个SPA项目放在一个ASP.NET Core项目中。但是这也有一些限制,牵涉到依赖管理。这么做的话,每个项目都会有它自己的node_modules目录,构建时间可能会非常长。

由于配置了输出目录,在发布项目时不需要指定包含目录,但是需要告诉MSBuild (.csproj)在发布前构建SPA项目,有一个添加Target的小窍门:

 
<Target Name="client-app" BeforeTargets="ComputeFilesToPublish">
<Exec Command="npm install" WorkingDirectory="ClientApp">
</Exec>
<Exec Command="npm run build" WorkingDirectory="ClientApp ">
</Exec>
</Target>

Target指出在发布前它需要考虑一些依赖。两个Exec是在ClientApp目录下执行CLI的方式。这只在发布时执行,因此不会影响你的开发流程。

最大的缺点来自于Visual Studio而不是ASP.NET Core。由于project.json在子文件夹中,而不在项目的根目录,Task Runner Explorer找不到它。这意味着你不得不单独运行。如果你使用的是Visual Studio Code, Rider或其他IDE,你可能已经习惯于打开终端并手动构建,因此这也不会造成太大问题。

其他的限制也与上面提及的相关,多个package.json需要管理。这是困扰我最深的一点。我的大部分项目都使用NPM管理依赖,管理多个package.json文件使我觉得沮丧。这就是为什么我通常使用最后一种方式——将SPA完全集成到ASP.NET Core项目中。

将SPA完全集成到ASP.NET Core中

如果你已经使用了一个构建步骤,为什么不让它们一起工作呢?这意味着你需要在每次ASP.NET Core构建时都构建你的SPA项目,你依然可以依靠build watchers和其他的设备来加速开发。构建与发布可以合并,是的测试与产品发布尽可能简单。

如何直接集成SPA

包含以下步骤:

  • 合并NPM配置
  • 将配置移到项目的根目录

合并NPM配置

要将SPA放入APS.NET Core项目中,要么将package.json移到项目的根目录,要么将它们合并起来。例如,你已经有了一个package.json文件帮你导入前端依赖:

{
  "version": "1.0.0",
  "name": "mypackage",
  "private": true,
  "dependencies": {
    "jquery": "3.3.1",
    "bootstrap": "4.3.1"
  }
}

不幸的是,大部分SPA项目有大量的依赖,并引进了其他配置元素。因此,在引入一个Angular项目所有所需内容后,可能像这样:

{
  "version": "1.0.0",
  "name": "mypackage",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "jquery": "3.3.1",
    "bootstrap": "4.3.1",
    "@angular/animations": "^6.1.0",
    "@angular/common": "^6.1.0",
    "@angular/compiler": "^6.1.0",
    "@angular/core": "^6.1.0",
    "@angular/forms": "^6.1.0",
    "@angular/http": "^6.1.0",
    "@angular/platform-browser": "^6.1.0",
    "@angular/platform-browser-dynamic": "^6.1.0",
    "@angular/router": "^6.1.0",
    "core-js": "^2.5.4",
    "rxjs": "~6.2.0",
    "zone.js": "~0.8.26"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.8.0",
    "@angular/cli": "~6.2.9",
    "@angular/compiler-cli": "^6.1.0",
    "@angular/language-service": "^6.1.0",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "~8.9.4",
    "codelyzer": "~4.3.0",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~3.0.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~7.0.0",
    "tslint": "~5.11.0",
    "typescript": "~2.9.2"
  }
}

完成这一步以后,需要手动删除node_modules(主项目中以及客户端项目中),并重新安装所有包:

`>npm install'

现在NPM合并好了,是时候移动配置了

移动配置

要将配置文件(不是源代码)移动到项目的根目录。取决于你使用的是那种框架,你会有一系列的配置文件。例如,Angular有angular.json,tsconfig.json以及tslint.json。

因为这些文件被移动了,需要修改配置文件中的所有相对目录,例如,在angular.json中,需要修改以src/开头的路径,改为ClientApp/src/:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "ng-core": {
      "root": "",
      "sourceRoot": "ClientApp/src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {...},
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "wwwroot/client",
            "index": "ClientApp/src/index.html",
            "main": "ClientApp/src/main.ts",
            "polyfills": "ClientApp/src/polyfills.ts",
            "tsConfig": "ClientApp/src/tsconfig.app.json",
            "assets": [
              "ClientApp/src/favicon.ico",
              "ClientApp/src/assets"
            ],
            "styles": [
              "ClientApp/src/styles.css"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "ClientApp/src/environments/environment.ts",
                  "with": "ClientApp/src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true
            }
          }
        },
        "serve": { ... },
      },
    },
  },
  "defaultProject": "ng-core"
}

现在,需要构建项目,最简单的方式是:

>ng build

如果成功了,SPA应该已经在项目中起作用。如果你使用的是Visual Studio,你可以使用Task Runner Explorer帮你开始构建。

Figure 4

由于在代码发生任何改变时都会进行重新构建,你可以将它绑定到Project Open,这样项目会在你写代码的同时持续构建。

Figure 5

现在你已经完成了开发中所需的所有工作。但是,如果你想让MSBuild帮你发布项目,你还需要做一些修改。就像之前的集成一样,你还需要在.csproj中添加Target:

<Target Name="client-app" BeforeTargets="ComputeFilesToPublish">
<Exec Command="npm install" WorkingDirectory="ClientApp">
</Exec>
<Exec Command="npm run build" WorkingDirectory="ClientApp ">
</Exec>
</Target>

该选择哪种方式

许多决定取决于是否合适,而无关于技术或最佳实践。如果你现在的方式工作的很好,坚持下去就好。通常,我更喜欢完全集成的方式。我的主要理由是,当我添加一个SPA(或多于一个)时,我很少想要构建整个应用程序。我想让Web做它擅长的事情,并且在我需要添加更多控制,更好的用户体验以及更紧密的用户交互时添加新的内容。

同样的,我不仅仅使用ASP.NET Core来开发一个API,很多时候服务端生成视图是正确的做法,无论是数据传输的安全(例如发送总计数据而不是有潜在价值的数据),提高缓存支持甚至是使用布局。

这些观点都基于使用SPA框架(Angular,Vue,React等)作为建立功能模块的方式。如果你正在建立一个支配所有的SPA,并且认为那是正确的方式,那也祝你成功。虽然大部分情况下那不会是我的选择。当然,我也仅仅是另一个开发者而已。我也可能是错的。