使用 ASP.Net Core - AngularJs 构建单页应用程序






4.83/5 (8投票s)
在本篇文章中,我们将使用 AngularJS 和 ASP.Net Core 来创建一个单页应用程序(SPA)。我们将使用 Angular-UI-Router 来处理我们的应用程序路由,而不是 MVC 路由。
如果您是 ASP.Net Core 新手,请在此处获取完整的入门概述:http://shashangka.com/2016/11/26/net-core-startup/
先决条件: 在开始之前,请确保开发环境已正确准备。以下是开发示例应用程序所需的先决条件列表。
要点: 为了开发示例应用程序,将特别关注以下几点,其中列出了关键机制。
- 包管理
- 通过 Gulp 传输库
- 使用 AngularJs 应用
- 添加中间件
- 脚手架 MSSQL 数据库
- Web-API
- 启用 CORS
- 使用 Gulp 构建 AngularJS 应用
- 测试应用程序(CRUD)
- 发布到 IIS
开始: 让我们开始吧,打开 Visual Studio 创建新项目,转到 > 文件 > 新建 > 项目,然后选择创建 ASP.Net Core 应用程序的项目,使用 ASP.Net Core 2.0 MVC Web 应用程序,如下图所示。

最初,应用程序的文件夹结构可能看起来像这样,我们将根据我们的需求对其进行修改。
按照我们的计划,我们将使用 AngularJS 路由通过 ASP.NET 静态文件服务来创建单页应用程序。
我们需要指向一个静态页面来接收所有请求,然后 Angular 将处理请求并决定提供哪个页面。让我们在 *wwwroot* 文件夹中创建一个 html 页面。这将是我们加载到客户端请求上的主页面。
提供默认页面
现在我们需要修改 *Startup.cs* 文件以添加用于提供静态 html 页面的中间件。以下是用于提供页面的代码片段。在此应用中,我已将其创建为“*index.html*”。
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("/index.html");
app.UseDefaultFiles(options);
处理客户端路由回退的中间件: 为了在 AngularJS SPA 应用中重新加载页面时避免 404 错误,我们需要添加中间件来处理客户端路由回退。下面的代码片段将负责这一点。
app.Use(async (context, next) =>
{
    await next();
    if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value))
    {
        context.Request.Path = "/index.html";
        context.Response.StatusCode = 200;
        await next();
    }
});
在此处获取有关中间件的更多详细信息:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?tabs=aspnetcore2x
如果我们运行应用程序进行测试,将默认提供静态页面。现在让我们开始客户端包管理。
客户端
包管理: 我们需要添加一些前端包,如 AngularJS,正如我们所见,我们的初始模板没有任何类似的东西。
要添加新包,请右键单击项目,然后单击“*管理 Bower 包*”  
  通过逐个搜索来安装所需的包。最后,已安装的包将列出,如下面的屏幕截图所示。
 通过逐个搜索来安装所需的包。最后,已安装的包将列出,如下面的屏幕截图所示。
让我们向我们的应用程序添加一些 Node 包,首先我们需要在项目中添加一个 npm 配置文件。右键单击项目,转到 > 添加 > 新项目。在新建项目窗口中,选择 npm 配置文件,如下图所示。  将所有这些包添加到 devDependencies,它将自动安装到我们的项目中。
 将所有这些包添加到 devDependencies,它将自动安装到我们的项目中。
"gulp": "^3.9.1", "gulp-concat": "^2.6.1", "gulp-rename": "^1.2.2", "gulp-cssmin": "^0.2.0", "gulp-uglify": "^3.0.0", "gulp-htmlclean": "^2.7.20", "rimraf": "^2.6.2"
最后,我们的包依赖项已列出。
让我们将所需的库从 bower_componen 文件夹传输到“wwwroot”,以便在主 html 页面中调用它们。添加一个 gulp 文件,然后复制下面的代码片段并粘贴到新添加的文件中。
/// <binding AfterBuild='build-all' />
var gulp = require("gulp"),
    rimraf = require("rimraf"),
    concat = require("gulp-concat"),
    cssmin = require("gulp-cssmin"),
    uglify = require("gulp-uglify"),
    rename = require("gulp-rename");
var root_path = {
    webroot: "./wwwroot/"
};
//library source
root_path.nmSrc = "./bower_components/";
//library destination
root_path.package_lib = root_path.webroot + "lib/";
gulp.task('copy-lib-css', function () {
    gulp.src('./bower_components/bootstrap/dist/css/bootstrap.min.css')
        .pipe(gulp.dest(root_path.webroot + '/css/'));
    gulp.src('./bower_components/toastr/toastr.min.css')
        .pipe(gulp.dest(root_path.webroot + '/css/'));
});
gulp.task('copy-lib-js', function () {
    gulp.src('./bower_components/jquery/dist/jquery.min.js')
        .pipe(gulp.dest(root_path.package_lib + '/jquery/'));
    gulp.src('./bower_components/bootstrap/dist/js/bootstrap.min.js')
        .pipe(gulp.dest(root_path.package_lib + '/bootstrap/'));
    gulp.src('./bower_components/toastr/toastr.min.js')
        .pipe(gulp.dest(root_path.package_lib + '/toastr/'));
    gulp.src('./bower_components/angular/angular.min.js')
        .pipe(gulp.dest(root_path.package_lib + 'angular/'));
    gulp.src('./bower_components/angular-ui-router/release/angular-ui-router.min.js')
        .pipe(gulp.dest(root_path.package_lib + 'angular/'));
});
gulp.task("copy-all", ["copy-lib-css", "copy-lib-js"]);
//Copy End
现在,从 Visual Studio 的顶部菜单中,转到 > 视图 > 其他窗口 > 任务运行程序资源管理器。让我们通过刷新获取任务列表,然后像下面的屏幕截图一样运行任务。 
正如我们从图中看到的,库已传输到根文件夹。我们使用了一个管理模板,因此其他库列在“*js*”文件夹中。
我们现在将在主布局页面中添加 AngularJs 库引用。如下所示。
<!DOCTYPE html> <html lang="en" ng-app="templating_app"> <head></head> <body> <!-- Core JS Files --> <script src="/lib/jquery/jquery-1.10.2.js" type="text/javascript"></script> <script src="/lib/bootstrap/tether.min.js"></script> <script src="/lib/bootstrap/bootstrap.min.js" type="text/javascript"></script> <!-- App JS Files --> <script src="/lib/angular/angular.min.js"></script> <script src="/lib/angular/angular-ui-router.min.js"></script> </body> </html>
AngularJS 应用程序: 让我们开始使用 AngularJS 应用程序,创建一个名为“*app*”的新文件夹。 “app”文件夹将保存我们所有的前端开发文件。让我们创建所有必需的文件和文件夹。在 shared 文件夹中,我们添加了侧边栏/顶部栏菜单之类的部分视图,这些视图将由 angular 指令调用。 
模块: 这将定义我们的应用程序。
var templatingApp;
(
    function () {
        'use strict';
        templatingApp = angular.module('templating_app', ['ui.router']);
    }
)();
路由: 此文件将处理来自 URL 的应用程序路由。
templatingApp.config(['$locationProvider', '$stateProvider', '$urlRouterProvider', '$urlMatcherFactoryProvider', '$compileProvider',
    function ($locationProvider, $stateProvider, $urlRouterProvider, $urlMatcherFactoryProvider, $compileProvider) {
        //console.log('Appt.Main is now running')
        if (window.history && window.history.pushState) {
            $locationProvider.html5Mode({
                enabled: true,
                requireBase: true
            }).hashPrefix('!');
        };
        $urlMatcherFactoryProvider.strictMode(false);
        $compileProvider.debugInfoEnabled(false);
        $stateProvider
            .state('home', {
                url: '/',
                templateUrl: './views/home/home.html',
                controller: 'HomeController'
            })
            .state('dashboard', {
                url: '/dashboard',
                templateUrl: './views/home/home.html',
                controller: 'HomeController'
            })
            .state('user', {
                url: '/user',
                templateUrl: './views/user/user.html',
                controller: 'UserController'
            })
            .state('about', {
                url: '/about',
                templateUrl: './views/about/about.html',
                controller: 'AboutController'
            });
        $urlRouterProvider.otherwise('/home');
    }]); 
路由问题: 此文件将处理来自 URL 的应用程序路由。在 AngularJs 中,我们需要启用 HTML5 模式以删除 URL 中的 /#!/ 符号。下面的代码片段用于启用此模式。
$locationProvider.html5Mode({
    enabled: true,
    requireBase: true
}).hashPrefix('!');
我们还需要在主页面中指定 base。
<base href="/">
指令:用于顶部导航栏:
templatingApp.directive("navbarMenu", function () {
    return {
        restrict: 'E',
        templateUrl: 'views/shared/navbar/nav.html'
    };
});
用于顶部侧边栏
templatingApp.directive("sidebarMenu", function () {
    return {
        restrict: 'E',
        templateUrl: 'views/shared/sidebar/menu.html'
    };
});
Angular Controller: 这是 Angular Controller,它将管理视图并执行从客户端到服务器的所有 http 调用。
templatingApp.controller('UserController', ['$scope', '$http', function ($scope, $http) {
    $scope.title = "All User";
    $scope.ListUser = null;
    $scope.userModel = {};
    $scope.userModel.id = 0;
    getallData();
    //******=========Get All User=========******
    function getallData() {
        $http({
            method: 'GET',
            url: '/api/Values/GetUser/'
        }).then(function (response) {
            $scope.ListUser = response.data;
        }, function (error) {
            console.log(error);
        });
    };
    //******=========Get Single User=========******
    $scope.getUser = function (user) {
        $http({
            method: 'GET',
            url: '/api/Values/GetUserByID/' + parseInt(user.id)
        }).then(function (response) {
            $scope.userModel = response.data;
        }, function (error) {
            console.log(error);
        });
    };
    //******=========Save User=========******
    $scope.saveUser = function () {
        $http({
            method: 'POST',
            url: '/api/Values/PostUser/',
            data: $scope.userModel
        }).then(function (response) {
            $scope.reset();
            getallData();
        }, function (error) {
            console.log(error);
        });
    };
    //******=========Update User=========******
    $scope.updateUser = function () {
        $http({
            method: 'PUT',
            url: '/api/Values/PutUser/' + parseInt($scope.userModel.id),
            data: $scope.userModel
        }).then(function (response) {
            $scope.reset();
            getallData();
        }, function (error) {
            console.log(error);
        });
    };
    //******=========Delete User=========******
    $scope.deleteUser = function (user) {
        var IsConf = confirm('You are about to delete ' + user.Name + '. Are you sure?');
        if (IsConf) {
            $http({
                method: 'DELETE',
                url: '/api/Values/DeleteUserByID/' + parseInt(user.id)
            }).then(function (response) {
                $scope.reset();
                getallData();
            }, function (error) {
                console.log(error);
            });
        }
    };
    //******=========Clear Form=========******
    $scope.reset = function () {
        var msg = "Form Cleared";
        $scope.userModel = {};
        $scope.userModel.id = 0;
    };
}]);
服务器端
数据库: 让我们在 MSSQL Server 中创建一个数据库。这是我们存储数据的表。
CREATE TABLE [dbo].[User]( [Id] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](250) NULL, [Email] [nvarchar](250) NULL, [Phone] [nvarchar](50) NULL ) ON [PRIMARY] GO
在后端,我们将使用反向工程从现有数据库生成 EF 模型。
Entity Framework: 在我们将 Entity Framework 用于我们的应用程序之前,我们需要安装包。让我们右键单击项目,然后转到 > 工具 > NuGet 包管理器 > 程序包管理器控制台,逐个安装以下包。
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
- Install-Package Microsoft.EntityFrameworkCore.Tools.DotNet
安装后,.csproj 文件将如下所示。
NUGET 包
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.5" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.2" /> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.3" /> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" /> </ItemGroup>
EF 模型: 让我们使用程序包管理器控制台运行以下命令。
dotnet ef dbcontext scaffold "Server=DESKTOP-80DEJMQ;Database=dbCore;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer –-output-dir Models
现在打开 DbContext 文件,然后添加一个构造函数以将配置(如连接字符串)传递到 DbContext。
public dbCoreContext(DbContextOptions<dbCoreContext> options) :
             base(options){
        }
注册 DbContext: 在 Startup.cs 中,让我们将 DbContext 添加为服务以启用数据库连接。
//Database Connection var connection = @"Server=DESKTOP-80DEJMQ;Database=dbCore;Trusted_Connection=True;"; services.AddDbContext<dbCoreContext>(options => options.UseSqlServer(connection));
启用 CORS: 为了从其他域访问 API,我们启用了 CORS。我们在 Startup.cs 的 ConfigureServices 方法中添加了服务。
services.AddCors(o => o.AddPolicy("AppPolicy", builder =>
            {
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader();
            }));
CORS 机制
图片来源:https://mdn.org.cn/en-US/docs/Web/HTTP/CORS
API: 这是我们的 MVC API Controller,它全局使用了特定的 RoutePrefix 属性。通过这个 api Controller 类,我们使用 Entity Framework DbContext 执行数据库操作。
[Route("api/Values"), Produces("application/json"), EnableCors("AppPolicy")]
public class ValuesController : Controller
{
    private dbCoreContext _ctx = null;
    public ValuesController(dbCoreContext context)
    {
        _ctx = context;
    }
    // GET: api/Values/GetUser
    [HttpGet, Route("GetUser")]
    public async Task<object> GetUser()
    {
        List<User> users = null;
        object result = null;
        try
        {
            using (_ctx)
            {
                users = await _ctx.User.ToListAsync();
                result = new
                {
                    User
                };
            }
        }
        catch (Exception ex)
        {
            ex.ToString();
        }
        return users;
    }
    // GET api/Values/GetUserByID/5
    [HttpGet, Route("GetUserByID/{id}")]
    public async Task<User> GetUserByID(int id)
    {
        User user = null;
        try
        {
            using (_ctx)
            {
                user = await _ctx.User.FirstOrDefaultAsync(x => x.Id == id);
            }
        }
        catch (Exception ex)
        {
            ex.ToString();
        }
        return user;
    }
    // POST api/Values/PostUser
    [HttpPost, Route("PostUser")]
    public async Task<object> PostUser([FromBody]User model)
    {
        object result = null; string message = "";
        if (model == null)
        {
            return BadRequest();
        }
        using (_ctx)
        {
            using (var _ctxTransaction = _ctx.Database.BeginTransaction())
            {
                try
                {
                    _ctx.User.Add(model);
                    await _ctx.SaveChangesAsync();
                    _ctxTransaction.Commit();
                    message = "Saved Successfully";
                }
                catch (Exception e)
                {
                    _ctxTransaction.Rollback();
                    e.ToString();
                    message = "Saved Error";
                }
                result = new
                {
                    message
                };
            }
        }
        return result;
    }
    // PUT api/Values/PutUser/5
    [HttpPut, Route("PutUser/{id}")]
    public async Task<object> PutUser(int id, [FromBody]User model)
    {
        object result = null; string message = "";
        if (model == null)
        {
            return BadRequest();
        }
        using (_ctx)
        {
            using (var _ctxTransaction = _ctx.Database.BeginTransaction())
            {
                try
                {
                    var entityUpdate = _ctx.User.FirstOrDefault(x => x.Id == id);
                    if (entityUpdate != null)
                    {
                        entityUpdate.Name = model.Name;
                        entityUpdate.Phone = model.Phone;
                        entityUpdate.Email = model.Email;
                        await _ctx.SaveChangesAsync();
                    }
                    _ctxTransaction.Commit();
                    message = "Entry Updated";
                }
                catch (Exception e)
                {
                    _ctxTransaction.Rollback(); e.ToString();
                    message = "Entry Update Failed!!";
                }
                result = new
                {
                    message
                };
            }
        }
        return result;
    }
    // DELETE api/Values/DeleteUserByID/5
    [HttpDelete, Route("DeleteUserByID/{id}")]
    public async Task<object> DeleteUserByID(int id)
    {
        object result = null; string message = "";
        using (_ctx)
        {
            using (var _ctxTransaction = _ctx.Database.BeginTransaction())
            {
                try
                {
                    var idToRemove = _ctx.User.SingleOrDefault(x => x.Id == id);
                    if (idToRemove != null)
                    {
                        _ctx.User.Remove(idToRemove);
                        await _ctx.SaveChangesAsync();
                    }
                    _ctxTransaction.Commit();
                    message = "Deleted Successfully";
                }
                catch (Exception e)
                {
                    _ctxTransaction.Rollback(); e.ToString();
                    message = "Error on Deleting!!";
                }
                result = new
                {
                    message
                };
            }
        }
        return result;
    }
}
我们已经完成了应用程序步骤,现在是时候将我们的应用程序缩小到一个 js 文件,然后将其引用到主 html 页面了。
修改 Gulp: 将下面的代码片段添加到现有的 gulp 文件,转到 > TaskRunnerExplorer 然后刷新窗口,它将在我们构建应用程序时起作用,因为它已绑定。
gulp.task('min-js', function () {
    gulp.src(['./app/**/*.js'])
        .pipe(concat('app.js'))
        .pipe(uglify())
        .pipe(gulp.dest(root_path.webroot))
});
gulp.task('copy-folder-html', function () {
    gulp.src('app/**/*.html')
        .pipe(gulp.dest(root_path.webroot + 'views'));
});
gulp.task("build-all", ["min-js", "copy-folder-html"]);
//Build End
构建应用程序后,“*wwwroot*”文件夹将如下所示。
最终主 HTML: 让我们将 app.js 文件添加到主 html 页面。
<!DOCTYPE html> <html lang="en" ng-app="templating_app"> <head> </head> <body> <!-- Core JS Files --> <script src="/lib/jquery/jquery-1.10.2.js" type="text/javascript"></script> <script src="/lib/bootstrap/tether.min.js"></script> <script src="/lib/bootstrap/bootstrap.min.js" type="text/javascript"></script> <!-- App JS Files --> <script src="/lib/angular/angular.min.js"></script> <script src="/lib/angular/angular-ui-router.min.js"></script> <script src="/app.js"></script> </body> </html>
输出: 在这里,我们可以看到用户数据已在网格中列出,这些数据也可以从其他域或设备访问。

发布到 IIS: 右键单击项目文件,转到 > 发布窗口,选择要发布的类型。我选择了文件夹发布。
转到应用程序池,确保 .Net CLR 版本为 2.0,如下图所示。

解决 IIS 中的底层提供程序问题:转到 URL:http://shashangka.com/2015/12/15/the-underlying-provider-failed-on-open-only-under-iis
让我们从 IIS 浏览网站。

希望这会有所帮助 🙂
参考
- http://shashangka.com/2016/08/12/crud-using-net-core-angularjs2-webapi
- http://shashangka.com/2016/11/26/net-core-startup
- https://benjii.me/2016/01/angular2-routing-with-asp-net-core-1
- https://docs.microsoft.com/en-us/aspnet/core/security/cors
- https://docs.microsoft.com/en-us/ef/core/get-started/aspnetcore/existing-db
- https://c-sharpcorner.com/article/enable-cross-origin-resource-sharing-cors-in-asp-net-core













