AngularJS,ui.router,根据用户角色加载模板和控制器

32
我开发了一个使用REST API的单页应用程序。用户需要登录才能访问应用程序。当用户登录后,他们将被重定向到/dashboard。在此URL /路由上,我希望根据用户的角色(例如普通用户或管理员用户)加载不同的模板和控制器。
我查看了https://github.com/angular-ui/ui-router/wiki中的模板部分,但没有任何选项支持我想要实现的内容。
通过使用templateUrl和function(stateParams),我无法注入帮助我确定用户角色以便加载模板的服务,例如views/user/dashboard.html或views/admin/dashboard.html。
通过使用templateProvider,我可以注入帮助我确定用户角色的服务,但如何加载模板呢?
任何解决方案还应根据用户角色加载不同的控制器,例如UserDashboardController或AdminDashboardController。
基本上我需要的是一个单一路由,它根据在用户登录时设置的用户角色变量加载不同的模板和控制器。
我是否想得对,或者我应该实现另一个解决方案?
任何关于此的帮助将不胜感激。

你找到任何解决方案了吗? - WelcomeTo
@MyTitle,你的目标只是为了功能上分离用户/管理员工具吗?你是否关心安全性、功能性或两者兼备?你希望管理员屏幕成为用户屏幕的超集(具有编辑、删除、创建等管理员链接和工具),还是想要创建完全不同的用户体验? - Dave Alperovich
@DaveA 是的,第一种选择:寻找管理员屏幕成为用户屏幕的超集(具有编辑、删除、创建等管理员链接和工具)。即常规用户和管理员屏幕之间没有太大区别。 - WelcomeTo
@MyTitle: 你可以尝试我的答案中的第一个解决方案。它与在页面上切换功能的想法相同。在这种情况下,您不需要配置权限,权限被假定为硬编码到每个角色中(您可以将来扩展此功能以使权限可配置或添加更多角色)。 - Khanh TO
9个回答

24

根据用户角色加载模板和控制器

虽然从技术上讲,ui-router的templateUrl函数不支持注入服务,但是您可以使用templateProvider来注入包含role变量或异步加载它的service,然后使用$templateFactory返回HTML内容。请考虑以下示例:

var app = angular.module('app', ['ui.router']);

app.service('session', function($timeout, $q){
    this.role = null;

    this.loadRole = function(){
        //load role using axax request and return promise
    };
});

app.config(function($stateProvider, $urlRouterProvider){
    $stateProvider.state('dashboard', {
        url: '/dashboard',
        templateProvider: function(session, $stateParams, $templateFactory){
          return session.loadRole().then(function(role){
              if(session.role == 'admin'){
                return $templateFactory.fromUrl('/admin/dashboard.html', $stateParams);
              } else {
                return $templateFactory.fromUrl('/user/dashboard.html', $stateParams);
              }
          });
        }
      });

    $urlRouterProvider.otherwise('/dashboard');
});

关于controller,您可以在每个模板的根元素中使用ng-controller来指定要使用的特定控制器。或者类似地,您可以使用controllerProvider选项来注入service,该service已经通过templateProvider解析了role。请看以下ui-router状态定义中controllerProvider选项的示例:
controllerProvider: function(session){
  if(session.role == 'admin'){
    return 'AdminCtrl';
  } else {
    return 'UserCtrl';  
  }
}

当然,您可以轻松地从此代码中删除重复项,并定义更易于访问的微型DSL,以使为特定角色和视图定义不同规则更加容易。

以下演示应该帮助您理解代码。

这是正确的方法吗?

通常情况下,这取决于上下文。为了帮助得出答案,让我首先提出以下问题:

  • 呈现给角色的视图有多大差异?

您是否只要隐藏几个按钮和其他操作元素,基本上使页面对普通用户只读,对超级用户可编辑?如果变化很小,我可能会选择使用相同的视图,仅隐藏特定元素,可能伪造类似于ng-if的指令,允许声明性地启用/禁用特定功能only-role='operator, admin'。另一方面,如果视图将大不相同,则采用不同的模板可以极大地简化标记。

  • 根据角色,特定页面上有多少可用的操作不同?

在不同的角色中,表面上看起来相似的操作内部工作方式是否不同?例如,如果您的编辑操作对于用户管理员角色都可用,但在某种情况下它会启动向导式UI,在另一种情况下则会启动一个复杂的面向高级用户的表单,则有一个单独的控制器更有意义。另一方面,如果管理员操作是用户操作的超集,则使用单个控制器更容易跟进。请注意,在这两种情况下,保留控制器事项很划算 - 它们应该只将视图粘合到封装在服务/视图模型/模型/选取名称中的行为中

  • 您是否会从应用程序的不同位置有许多上下文分离的链接引导到特定的页面
例如,只需编写ui-sref="dashboard",即可为特定的页面提供导航,而不考虑当前用户role,如果在各种情境下都有用处,则将它们定义在单个路由/状态下似乎更易于维护,而不是使用条件逻辑来构建基于角色的不同ui-sref/ng-href。但是,您还可以根据用户角色动态地定义路由/状态-动态加载或不加载。

  • 不同角色在特定页面上可用的视图和操作是否分别演变,还是一起演变?

有时我们首先为常规用户构建功能,然后为高级用户和最终用户构建功能。如果可以轻松划分明确的边界,将useradmin页面的工作分配给团队成员并不罕见。在这种情况下,拥有单独的viewscontrollers可以简化开发人员的工作,避免冲突。当然,这并非全是美好的事物-团队必须非常有纪律性地消除最可能发生的重复

希望我的建议能帮助您做出决策。


谢谢。但在你的情况下,admin是硬编码的角色。但我将使用异步AJAX请求接收用户角色。所以我不确定这会起作用:role = injector.get('session').role, - WelcomeTo
@MyTitle 我已经更新了我的答案,以支持异步解析role - miensol
1
哇!太棒了!+100 - AndiDev

15
我是否想到了正确的方案,或者我应该实施另一种解决方案?
依我之见,你不应该这样做。这里,我提出了另外两个解决方案,具体取决于你的应用程序如何实现。
1) 如果角色权限可以配置(您可以有一个单独的页面来配置您的角色,为您的角色分配权限等)。然后仅使用一个模板和一个控制器来处理您的角色(普通用户,管理员用户等),并使用ng-show、ng-class等来相应地显示您的HTML。
在这种情况下,我们不太关心用户是普通用户还是管理员用户,那只是我们角色的名称。我们关心的是权限及其动态性 => 因此,我们应该根据配置的权限动态地显示html(当然,在用户执行操作时也会在服务器端进行检查,以防止用户创建恶意http请求并将其发布到服务器)。如果我们要为此使用单独的模板,则有无数个案例。
这个解决方案的重点是页面功能对于您的角色是相同的,您只需基于用户的身份显示/隐藏页面的功能。
2)如果角色的权限是固定的(无法配置),并且常规用户和管理员用户的视图功能不同。最好为这些视图使用单独的状态,并根据已登录用户授权访问这些视图(当然,在用户执行操作时还会在服务器端进行授权)。
原因是:管理员用户视图和普通用户视图具有不同的功能(应该相互分离)。

我猜一半的赏金总比没有好。本来应该是全部的。再说,很难满足那些不知道自己想要什么的人。 - Dave Alperovich

7

如果您使用的Angular版本大于1.2,您可以使用templateUrl函数制作指令。

基本想法是,在仪表板视图中放置一个自定义指令,根据用户级别确定模板。类似这样:

(function () {
  'use strict';
  angular.module('App.Directives')
    .directive('appDashboard', ['UserManager', function (UserManager) {
      return {
        restrict: 'EA',
        templateUrl: function(ele, attr){
            if (UserManager.currentUser.isAdmin){
                return 'admin.html';
            }else{
                return 'user.html';
            }
        }
      };
    }]);
})(); 

1
这只运行一次,如果用户注销并使用另一个角色重新登录,则会出现问题。 - Khanh TO

4

I. 我的建议是不要使用“...单一路由加载不同模板...”,这是我的回答。

如果可能的话:

试着退后一步,重新考虑整个设计,
尝试削弱应用程序用户对url的兴趣

他们并不是。如果他们真的理解了什么是url地址栏...他们会用它来复制发送粘贴...而不是去研究它的组成部分...

II. 建议:强制使用ui-router 状态

... UI-Router 是围绕状态组织的,这些状态可以选择性地具有路由以及其他行为...

That means,让我们将应用程序重新考虑为一组/层次结构明确定义的状态。它们可以定义url,但不需要(例如:error state,没有url) III.我们如何从围绕状态构建应用程序中受益?
分离关注点——这应该是我们的目标。 状态是一个聚集了一些视图/控制器解析器自定义数据...的单元。
这意味着,可能会有更多的状态重用视图控制器等。这样的状态确实可能不同(相同的视图,不同的控制器)。但它们是单一目的——它们存在的目的是处理某些场景:
  • 用户/员工记录的管理
  • 用户/员工列表——电话簿式的信息(只有电子邮件、电话...)
  • 安全管理——用户的权限是什么?...
再次强调,可能会有许多许多“状态”。即使有一百个状态也不会影响性能。这些只是定义,一组对其他部分的引用,在需要时才应该使用。
一旦我们在“状态”的层面上定义了“用例”、“用户故事”,我们就可以将它们分组为集合/层次结构。这些组可以以不同的格式(不同的菜单项)呈现给不同的用户角色。
但最终,我们获得了很多自由和简化了可维护性。
IV. 保持应用程序运行并增长
如果只有少量状态,维护似乎不是问题。但是,应用程序可能会成功。成功并在其设计内增长。
拆分状态定义(作为工作单位)和它们的层次结构(哪个用户角色可以访问哪个状态)将简化其管理。

在状态之外应用安全措施(通过事件监听器,比如'$stateChangeStart'比无休止地重构模板提供程序要容易得多。此外,安全的主要部分仍应该在服务器上应用,无论 UI 允许什么。

V. 摘要:

虽然有一个很棒的功能templateProvider,可以为我们做一些有趣的事情(例如这里:使用 AngularJs 中的 UI-Router 更改导航菜单)...

...但我们不应该将其用于安全性。可以将其实现为从现有状态构建的某个菜单/层次结构,基于当前角色。事件监听器应检查用户是否进入了授权状态,但主要检查必须在服务器上进行...


谢谢。听起来不错,但是你能提供一个例子吗? - WelcomeTo
不确定是否能够提供一个足够简单的示例来说明这个设计建议...但我会在今天晚些时候考虑一下...或者之后再考虑。我的观点的核心是:尽可能简单地定义状态。可能会有很多状态。一旦为用户创建了导航 - 使其与角色相关(每个角色有更多的导航设置)。如果需要,在事件上引入一些检查...但真正的安全应用于服务器(仅当用户具有所需角色时才获取数据)。因此,这更多是一个设计/架构原则,而不是简单的用例答案...如果这能帮助到你,我会很高兴...稍后再联系 ;) - Radim Köhler
我认为这个答案提供的方法存在问题。用户打开ww.someapp.com/,由angular重定向到#!/,假设此时用户可以登录或未登录。显然,已注册用户不需要看到“营销”主页,他们更喜欢在访问“/#!/”或“/”路径时被有效地重定向到仪表板。 - Konstantin Isaev

3
我曾经采用以下解决方案(可能不是最理想的,但在这种情况下对我起作用):
  1. 在模板中使用ngController指定控制器。

  2. 使用通用视图名称(例如views/dashboard.html)加载模板。

  3. 每当登录用户角色发生更改时,使用$templateCache.put(...)更改views/dashboard.html引用的内容。


下面是一个简化示例:
app.controller('loginCtrl', function ($location, $scope, User) {
    ...
    $scope.loginAs = function (role) {
        // First set the user role
        User.setRole(role);

        // Then navigate to Dashboard
        $location.path('/dashboard');
    };
});

// A simplified `User` service that takes care of swapping templates,
// based on the role. ("User" is probably not the best name...)
app.service('User', function ($http, $templateCache) {
    var guestRole = 'guest';
    var facadeUrl = 'views/dashboard.html';
    var emptyTmpl = '';
    var errorTmpl = 'Failed to load template !';
    var tempTmpl  = 'Loading template...';

    ...

    // Upon logout, put an empty template into `$templateCache`
    this.logout = function () {
        this.role = guestRole;
        $templateCache.put(facadeUrl, emptyTmpl);
    };

    // When the role changes (e.g. upon login), set the role as well as the template
    // (remember that the template itself will specify the appropriate controller) 
    this.setRole = function (role) {
        this.role = role;

        // The actual template URL    
        var url = 'views/' + role + '/dashboard.html';

        // Put a temporary template into `$templateCache`
        $templateCache.put(facadeUrl, tempTmpl);

        // Fetch the actual template (from the `$templateCahce` if available)
        // and store it under the "generic" URL (`views/dashboard.html`)
        $http.get(url, {cache: $templateCache}).
              success(function (tmpl) {
                  $templateCache.put(facadeUrl, tmpl);
              }).
              error(function () {
                  // Handle errors...
                  $templateCache.put(facadeUrl, errorTmpl);
              });
    };

    // Initialize role and template        
    this.logout();
});

// When the user navigates to '/dashboard', load the `views/dashboard.html` template.
// In a real app, you should of course verify that the user is logged in etc...
// (Here I use `ngRoute` for simplicity, but you can use any routing module.)
app.config(function ($routeProvider) {
    $routeProvider.
        when('/dashboard', {
            templateUrl: 'views/dashboard.html'
        }).
        ...
});

请参见这个简短演示
(我使用ngRoute来简化,但这并不影响,因为所有的工作都是由User服务完成的。)

3

您实际上不需要使用路由器来完成它。

最简单的方法是为所有角色使用一个单一的模板,并在其中使用动态ng-include。假设您在$scope中有注入器:

<div ng-include="injector.get('session').role+'_dashboard.html'"></div>

所以您应该有user_dashboard.htmladmin_dashboard.html视图。在每个视图中,您可以应用单独的控制器,例如user_dashboard.html:
<div id="user_dashboard" ng-controller="UserDashboardCtrl">
    User markup
</div>

1
不需要长篇解释。
使用 resolve 并更改 $route.$$route.templateUrl,或通过将新路由或相关参数传递给 promise 使用 routeChangeError。
var md = angular.module('mymodule', ['ngRoute']);
md.config(function($routeProvider, $locationProvider) {
    $routeProvider.when('/common_route/:someparam', {
        resolve: {
            nextRoute: function($q, $route, userService) {
                defer = $q.defer()
                userService.currentRole(function(data) { defer.reject({nextRoute: 'user_based_route/'+data) });
                return defer.promise;
            }
        }
    });
    $rootScope.$on("$routeChangeError", function(evt, current, previous, rejection) {
      if (rejection.route) {
        return $location.path(rejection.route).replace();
      }
    });
});

1

我知道这个问题发布已经有一段时间了,但是我还是想加入我的答案,因为我使用的方法与其他答案不同。

在这种方法中,我完全基于用户角色将路由和模板URL分开,并在用户试图查看未经授权的路由时将其重定向到首页。

使用UI Router,我基本上像这样向状态添加一个数据属性:

.state('admin', {
            url: "/admin",
            templateUrl: "views/admin.html",
            data: {  requireRole: 'admin' }
        })

当用户通过身份验证后,我会像这样从控制器中将他们的角色数据存储到localstorage$rootscope中。
var role = JSON.stringify(response.data); // response from api with role details

// Set the stringified user data into local storage
localStorage.setItem('role', role);

// Putting the user's role on $rootScope for access by other controllers
$rootScope.role = response.data;

最后,我使用$stateChangeStart来检查角色并在用户不应查看页面时重定向用户:
.run(['$rootScope', '$state', function($rootScope, $state) {

        // $stateChangeStart is fired whenever the state changes. We can use some parameters
        // such as toState to hook into details about the state as it is changing
        $rootScope.$on('$stateChangeStart', function(event, toState) {

                var role = JSON.parse(localStorage.getItem('role'));
                $rootScope.role = role;

                // Redirect user is NOT authenticated and accesing private pages
                var requireRole = toState.data !== undefined
                                  && toState.data.requireRole;

                 if( (requireRole == 'admin' && role != 'admin')) )
                 {
                   $state.go('index');
                   event.preventDefault();
                   return;
                 }
     }

});

除上述内容外,您仍需进行服务器端授权检查,然后再向用户显示任何数据。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接