随着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中,可以直接选择项目模板,如下图所示:
这种方式是将一整个SPA项目,放在一个ClientApp文件夹中,如下图所示:
在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,如下图所示:
因为这个目录有它自己的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帮你开始构建。
由于在代码发生任何改变时都会进行重新构建,你可以将它绑定到Project Open,这样项目会在你写代码的同时持续构建。
现在你已经完成了开发中所需的所有工作。但是,如果你想让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,并且认为那是正确的方式,那也祝你成功。虽然大部分情况下那不会是我的选择。当然,我也仅仅是另一个开发者而已。我也可能是错的。