新 AngularJS ng-ref 指令的陷阱

Pitfalls of the New AngularJS ng-ref Directive

发布AngularJSV1.7.1* introduces the new ng-ref directive。虽然这个新指令使用户能够轻松地做某些事情,但我发现滥用和问题的可能性很大。

ng-ref 属性告诉 AngularJS 在当前范围内发布组件的控制器。这对于让音频播放器等组件将其 API 公开给同级组件很有用。它的播放和停止控件可以轻松访问。

第一个问题是播放器控件 undefined 在控制器的 $onInit 函数中。

Initial vm.pl = undefined  <<<< UNDEFINED
Sample = [true,false]

对于依赖于可用数据的代码,我们如何解决这个问题?

The DEMO

angular.module("app",[])
.controller("ctrl", class ctrl {
  constructor() {
    console.log("construct")
  }
  $onInit() {
    console.log("onInit", this.pl);
    this.initPL = this.pl || 'undefined';
    this.sample = this.pl || 'undefined';
    this.getSample = () => {
      this.sample = `[${this.pl.box1},${this.pl.box2}]`;
    }
  }
})
.component("player", {
  template: `
    <fieldset>
      $ctrl.box1={{$ctrl.box1}}<br>
      $ctrl.box2={{$ctrl.box2}}<br>
      <h3>Player</h3>
    </fieldset>
  `,
  controller: class player {
    constructor() {
      console.log("player",this);
    }
    $onInit() {
      console.log("pl.init", this)
      this.box1 = true;
      this.box2 = false;
    }
  },
})
<script src="//unpkg.com/angular@1.7.1/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
    Initial vm.pl = {{vm.initPL}}<br>
    Sample = {{vm.sample}}<br>
    <button ng-click="vm.getSample()">Get Sample</button>
    <br>
    <input type="checkbox" ng-model="vm.pl.box1" />
      Box1 pl.box1={{vm.pl.box1}}<br>
    <input type="checkbox" ng-model="vm.pl.box2" />
      Box2 pl.box2={{vm.pl.box2}}<br>
    <br>
    <player ng-ref="vm.pl"></player>
</body>

父控制器初始化发生在播放器控制器初始化之前,所以这就是为什么我们在第一个 $onInit 中将 initPL 作为 undefined。 就个人而言,我更愿意在父控制器初始化时定义和加载应该传递给嵌套组件的数据,而不是从子控制器设置父控制器的初始数据。但是如果我们需要这个,我们可以使用绑定和回调在子组件初始化时完成。可能它看起来更像是一个肮脏的解决方法,但它可以在这种情况下工作,这里是代码:

angular.module("app",[])
.controller("ctrl", class ctrl {
  constructor() {
    console.log("construct")
  }
  $onInit() {
    console.log("onInit", this.pl);
    this.getSample = () => {
      this.sample = `[${this.pl.box1},${this.pl.box2}]`;
    }
    this.onPlayerInit = (pl) => {
      console.log("onPlayerInit", pl);
      this.initPL = pl || 'undefined';
      this.sample = `[${pl.box1},${pl.box2}]`;
    }
  }
})
.component("player", {
  bindings: {
      onInit: '&'
  },
  template: `
    <fieldset>
      $ctrl.box1={{$ctrl.box1}}<br>
      $ctrl.box2={{$ctrl.box2}}<br>
      <h3>Player</h3>
    </fieldset>
  `,
  controller: class player {
    constructor() {
      console.log("player",this);
    }
    $onInit() {
      console.log("pl.init", this)
      this.box1 = true;
      this.box2 = false;
      if (angular.isFunction( this.onInit() )) {
        this.onInit()(this);
      }
    }
  },
})
<script src="//unpkg.com/angular@1.7.1/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
    Initial vm.pl = {{vm.initPL}}<br>
    Sample = {{vm.sample}}<br>
    <button ng-click="vm.getSample()">Get Sample</button>
    <br>
    <input type="checkbox" ng-model="vm.pl.box1" />
      Box1 pl.box1={{vm.pl.box1}}<br>
    <input type="checkbox" ng-model="vm.pl.box2" />
      Box2 pl.box2={{vm.pl.box2}}<br>
    <br>
    <player ng-ref="vm.pl" on-init="vm.onPlayerInit"></player>
</body>

获取组件控制器的 ref 并不新鲜,当时的指令允许这样做,这根本不是问题,有必要拥有这样的功能,ng-ref 只是你的帮手从模板端执行此操作(与 angular 2+ 相同的方式)。

不过,如果您需要准备好子组件,您应该使用 $postLink() 而不是 $onInit$postLink 在组件与他的子组件链接后调用,这意味着 ng-ref 将在调用时准备就绪。

所以你所要做的就是像这样改变你的onInit

̶$̶o̶n̶I̶n̶i̶t̶(̶)̶ ̶{̶
$postLink() {
    console.log("onInit", this.pl);
    this.initPL = this.pl || 'undefined';
    this.sample = this.pl || 'undefined';
    this.getSample = () => {
      this.sample = `[${this.pl.box1},${this.pl.box2}]`;
    }
}

$postLink() - Called after this controller's element and its children have been linked. Similar to the post-link function this hook can be used to set up DOM event handlers and do direct DOM manipulation. Note that child elements that contain templateUrl directives will not have been compiled and linked since they are waiting for their template to load asynchronously and their own compilation and linking has been suspended until that occurs. This hook can be considered analogous to the ngAfterViewInit and ngAfterContentInit hooks in Angular. Since the compilation process is rather different in AngularJS there is no direct mapping and care should be taken when upgrading.

Ref.: Understanding Components

完整的工作片段可以在下面找到(我删除了所有 console.log 以使其更清楚):

angular.module("app",[])
.controller("ctrl", class ctrl {
  constructor() {
    //console.log("construct")
  }
  $postLink() {
    //console.log("onInit", this.pl);
    this.initPL = this.pl || 'undefined';
    this.sample = this.pl || 'undefined';
    this.getSample = () => {
      this.sample = `[${this.pl.box1},${this.pl.box2}]`;
    }
  }
})
.component("player", {
  template: `
    <fieldset>
      $ctrl.box1={{$ctrl.box1}}<br>
      $ctrl.box2={{$ctrl.box2}}<br>
    </fieldset>
  `,
  controller: class player {
    constructor() {
      //console.log("player",this);
    }
    $onInit() {
      //console.log("pl.init", this)
      this.box1 = true;
      this.box2 = false;
    }
  },
})
<script src="//unpkg.com/angular@1.7.1/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
    Initial vm.pl = {{vm.initPL}}<br>
    Sample = {{vm.sample}}<br>
    <button ng-click="vm.getSample()">Get Sample</button>
    <br>
    <input type="checkbox" ng-model="vm.pl.box1" />
      Box1 pl.box1={{vm.pl.box1}}<br>
    <input type="checkbox" ng-model="vm.pl.box2" />
      Box2 pl.box2={{vm.pl.box2}}<br>
    <player ng-ref="vm.pl"></player>
  </body>