JavaScript 关于 AngularJS 的一个问题

1272729223 · 2015年06月08日 · 最后由 miclle 回复于 2015年06月11日 · 3855 次阅读

这是我个人对于 angular*模块*设计的思路,然后遇到一些问题,当然如果我的思路不理想,然后期待您给我建议,谢谢。

我把projects打包成一个模块:


// 创建我的应用下面的`project`模块
var projectModule = angular.module('myApp.project', ['myApp.config'])

// 路由
projectModule.config(function ($routeProvider) {
     var routes = {
           '/projects': {
                templateUrl: 'components/projects/list.html',
                controller: 'ProjectListCtrl',
                resolve: {
                      projects:   function (projectService) {
                              return projectService.query().$promise
                      }
                }
           },
           '/projects/:id': {
                templateUrl: 'components/projects/show.html',
                controller: 'ProjectCtrl',
                resolve:  {
                     // ...
                }
           }
     }

     angular.forEach(routes, function (route, path) {
          $routeProvider.when(path, route)
     })
})

// 服务
projectModule.factory('projectService', function ($resource) {
     var url = config.API + '/projects/:id'
     return $resource(url, { id: '@_id' })
})

// 项目列表
projectModule
.controller('ProjectListCtrl', function (projects) {
      this.projects = projects
})
.directive('projectListDirective', function () { // `project list`指令, 因为我把它关联到`ProjectListCtrl`成为一个组件单元
    return {
        replace: true,
        transclude: true,
        scope: {
               projects: '@'
        },
        template: '<div projects="{{ctrl.projects}}"><h1>项目列表</h1><div ng-transclude></div></div>',
        controller: 'ProjectListCtrl',
        controllerAs: 'ctrl',
        link: function (scope, el, attrs, ctrl) {
               console.log(scope)  
        }
    }
})


//  项目详情
projectModule
.controller('ProjectCtrl', function (project) {
     this.project = project
})
.directive('projectDiretive', function () {
   return {
        replace: true,
        template: '<h1>{{ctrl.project.title}}</h1><p>{{ctrl.project.description}}</p>',
        controller: 'ProjectCtrl',
        controllerAs: 'ctrl',
        link: function (scope, el, attrs, ctrl) {
                 console.log(scope)
        }
   }
})

components/projects/list.html

<project-list-directive>
     <ul>
         <li ng-repeat="project in $parent.projects">
                <a ng-href="/projects/{{project.id}}">{{project.title}}</a>
         </li>
    </ul>
</project-list-directive>

components/projects/show.html

<project-directive></project-directive>

不过,基于我以上的思路,在 console 里面看到报错,实际上ProjectListCtrl被 call 了两次,一次是渲染components/projects/list.html模板的时候,然后在编译<project-list-directive>指令的时候又调用了一次。而,这个错误就是在编译指令的时候调用ProjectListCtrl,然后找不到porjects这个注入依赖。

Error: [$injector:unpr] Unknown provider: projectsProvider <- projects <- ProjectListCtrl
http://errors.angularjs.org/1.3.15/$injector/unpr?p0=projectsProvider%20%3C-%20projects%20%3C-%20ProjectListCtrl
    at REGEX_STRING_REGEXP (http://localhost:3000/bower_components/angular/angular.js:63:12)
    at http://localhost:3000/bower_components/angular/angular.js:4015:19
    at Object.getService [as get] (http://localhost:3000/bower_components/angular/angular.js:4162:39)
    at http://localhost:3000/bower_components/angular/angular.js:4020:45
    at getService (http://localhost:3000/bower_components/angular/angular.js:4162:39)
    at Object.invoke (http://localhost:3000/bower_components/angular/angular.js:4194:13)
    at $get.extend.instance (http://localhost:3000/bower_components/angular/angular.js:8493:21)
    at http://localhost:3000/bower_components/angular/angular.js:7739:13
    at forEach (http://localhost:3000/bower_components/angular/angular.js:331:20)
    at nodeLinkFn (http://localhost:3000/bower_components/angular/angular.js:7738:11) <div ng-view="" class="ng-scope">

resolve 中的 property 是通过 router 而注入的。所以当你再定义路由的时候,resolve property 中的依赖可以正常注入到其相应的 controller 中。而 directive 就不行了。当你在 directive 中使用 controller 属性的时候,只有以下 objects 可以被注入: $scope, $element, $attrs, $transclude, 以及你定义的所有 service,例如 projectService

你可以尝试用$controller service 手动构建 controller。$controller service 的第二个参数可以传入一个 object,定义你所有想 inject 的依赖。比如,根据你的情况,projectDirective 你可以这么写:

.directive('projectDiretive', function ($controller) {
   return {
        replace: true,
        template: '<h1>{{ctrl.project.title}}</h1><p>{{ctrl.project.description}}</p>',
        controller:  $controller('ProjectCtrl', {project: ....//你想要注入的object} ),
        controllerAs: 'ctrl',
        link: function (scope, el, attrs, ctrl) {
                 console.log(scope)
        }
   }
})

详情请参看 angular 文档:https://docs.angularjs.org/api/ng/service/$controller

#2 楼 @simonykq 哇,刚先问你怎么解决,谢谢!我看看!

#2 楼 @simonykq

还是无法解决,我就我能想到的关键字 google 了个遍,似乎这个问题比较偏,所以是不是我的思路本身就错了?现在卡的问题就是你上问说的,不过我有两点不太明白:

  1. 在渲染projectListDirective指令的时候,ProjectListCtrl扮演什么角色?为什么这里会再次构造一次ProjectListCtrl
  2. 我看你发给我的那个$controller的文档,不太明白locale是什么,以及它和通过$controller(‘ProjectListCtrl’)找到的ProjectListCtrl之间是什么关系?

不过我还是有点盲目的尝试了 stackoverflow 上面我觉得貌似有点思路的方式。

.directive('projectListDirective', function ($controller) {
    return {
        ...
        controller: $controller('ProjectListCtrl', { projects: $injector.get(projects) }),  // 说没有`$injector`
        ...
    }
})


.directive('projectListDirective', function ($controller) {
    return {
        ...
        controller: $controller('ProjectListCtrl', { projects:  ctrl.projects }),  // ctrl 未定义
        ...
    }
})

...   ...

假如您方便的话,希望能帮我解答一下,这个问题困扰我好几天了。谢谢。

先回答你的问题: 1 在创建 Directive 的时候,Directive 支持一个叫 controller 的属性。这个 controller 在 link 函数之前调用,它就跟你在 router 上声明相应 template 所对应的 controller 一样,只不过这里的 controller 对应的是 directive 的 template,而不是 router 上声明的 template。如果你有兴趣读过 ngRoute 代码的话,它们的底层实现做的无非就是在 ng-view 这个 directive(注意,ng-view 也是 directive)上加一个 controller 属性,然后把你在之前 router 中声明的 controller 绑定到这个 ng-view directive 上。Angular 中除了 Controller 之外其余的 components 都是 singleton(单件)。也就是说 controller 可以有很多 instances。因为你在 router 和 directive 中都用到了 ProjectListCtrl,所有这个 controller 会被创建两次。 2 $controller 是一个 Angular 内置的 service,它专门用来创建 controller 实例(之前也提到 controller 可以被多次实例化,这个 service 有点像 Java 中的工厂模式)。他的第一个参数如果是 String 的话他会通过这个 String 来寻找之前你在 app 中注册过的 Controller,如果找得到的话他会用这个 controller 对应的 constructor 来实例化此 controller。第二个参数是一个可选的 object。此 object 上所有声明的属性都可被此 controller 实例(注意,仅此 controller 实例)作为依赖注入(Dependency injection)例如:

 function MyController(myDependency){
   console.log(myDependency) 
}
//执行完以下代码应该会在console打印 “hello”
 $controller('MyController', {myDependency: 'hello'});
//执行完以下代码应该会报错,因为你没有注入myDependency依赖,angular的DI系统找不到myDependency代码依赖 
$controller('MyController');

#5 楼 @simonykq 非常感谢,您的回答我很满意。 :) 我想我应该能解决了,您帮我理清了几个重要的概念。

我查过国外的 stack over flow 上的一些。好像目前的 Angular 不支持在 Directive 中输入 resolve 属性,但有很多国外 developer 已经请求加入此功能,详情请看: https://github.com/angular/angular.js/issues/2095 不过如果你就是想达到类似像 router 中 resolve 属性那样效果的话(在后端没有返回数据之前,不加载此 directive),你可以尝试用 jQuery 中的 Ajax api 请求后台 JSON。在发送 AJax 请求的过程中,有一个 async 属性,把它设置为 false(默认情况是 true)。这样你的 directive 代码从异步变成了同步。在你后台没返回之前,此方法将一直堵塞。虽然这个办法不太好,但是目前恐怕只能这么做

#7 楼 @simonykq 嗯,那个 issue 我之前看过,我拆分出两个 controller 了。当然似乎,我也看到过 stack overflow 上别人用ng-if这个指令先把projectListDirective包起来,只是我不想再用$scope. 我还没试过,这个问题困了我几天,终于算是到头了!非常感谢。哈哈。

  1. 你命名 directive 的时候加一个 Diretive 后缀太多余了的感觉。
  2. 你 controller, controllerAs 无故地就增加了代码的阅读复杂度,其实剥离出 controller 的原因是要跟别的 directive 共享 controller,或者要被外部调用才做的,你分离也没有什么意义,放在 link 里就可以了,简单明了。
  3. 你以为你没用 $scope, 其实 <h1>{{ctrl.project.title}}</h1> 的代码就是 $scope.ctrl 用 bindonce 才解决你的问题。

factory 的命名我会首字母大写 (砣峰式),比如:

projectModule.factory('User', function ($resource) {
     var url = config.API + '/users/:id'
     return $resource(url, { id: '@_id' })
})
需要 登录 后方可回复, 如果你还没有账号请 注册新账号