一个angular指令加载顺序异常的排除

起因

在表格展示的时候, 有些状态需要切换的, 之前都是直接使用的 checkbox. 有天老大说要优化界面, 把大部分的 checkbox 换成仿 iOS 的 SwitchButton, 开始想得很简单嘛, 新建了一个指令, 考虑了下使用场景和情况, 减少使用成本, 使用方法和原始 input 差不多, 很快就完工了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
var switchBtn = function () {
return {
restrict: 'A',
templateUrl: 'views/common/switchBtn.html',
css: { href: 'styles/common/switchBtn.css', persist: true },
replace: true,
scope: {
//传入的ng-model至少是双层结构(至少有个.)
ngModel: '=',
firstStatus: '@',
onChange: '=',
text: '=',
noText: '=',
item: '=',
},
controller: function ($scope) {},
link: function ($scope, $element, $attrs) {
$element.on('selectstart', function () {
return false;
});
var firstStatus = false;
if ($scope.firstStatus && $scope.firstStatus.toLowerCase() == 'false') {
firstStatus = false;
} else if ($scope.firstStatus && $scope.firstStatus.toLowerCase() == 'true') {
firstStatus = true;
}
if (typeof $scope.ngModel != 'undefined' && $scope.ngModel != null) {
$scope.ngModel = firstStatus || $scope.ngModel;
} else {
$scope.ngModel = firstStatus;
}
if ($scope.noText) {
$scope.text = ['', ''];
} else if (!$scope.noText && !$scope.text) {
$scope.text = ['on', 'off'];
}
$scope.changeSwitch = function () {
$scope.ngModel = !$scope.ngModel;
if ($scope.onChange) {
if ($scope.onChange($scope.ngModel, $scope.item) == false) {
$scope.ngModel = !$scope.ngModel;
}
}
};
$scope.getText = function () {
return $scope.text[$scope.ngModel ? 0 : 1];
};
},
};
};
1
2
3
4
<div class="switch-select" ng-click="changeSwitch()">
<div class="switch-bg" ng-class="{true:'on',false:'off'}[ngModel]"><span ng-bind="getText()"></span></div>
<div class="switch-btn" ng-class="{true:'on',false:'off'}[ngModel]"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/*按钮大小不符合可根据需要在相关页面css自定义大小*/
/* demo:
.[class] .switch-select {
width: 45px;
height: 16px;
}

.[class] .switch-bg {
border-radius: 18px;
font-size: 12px;
}

.[class] .switch-bg span {
top: 1px;
}

.[class] .switch-bg.on span {
left: 6px;
}

.[class] .switch-bg.off span {
right: 6px;
}

.[class] .switch-btn {
top: 1px;
height: 14px;
width: 14px;
}

.[class] .switch-btn.on {
left: 30px;
}

.[class] .switch-btn.off {
left: 1px;
}

*/
.switch-select {
position: relative;
display: inline-block;
vertical-align: top;
width: 45px;
height: 16px;
cursor: pointer;
}

.switch-bg {
height: 100%;
width: 100%;
color: #fff;
border-radius: 18px;
font-size: 12px;
text-transform: uppercase;
transition: All 0.3s ease;
-webkit-transition: All 0.3s ease;
-moz-transition: All 0.3s ease;
-o-transition: All 0.3s ease;
}

.switch-bg span {
position: absolute;
top: 1px;
line-height: 1;
}

.switch-bg.on {
background-color: #00af2c;
border: 1px solid #068506;
}

.switch-bg.on span {
left: 6px;
}

.switch-bg.off {
background-color: #ed5b49;
border: 1px solid #d2402e;
}

.switch-bg.off span {
right: 6px;
}

.switch-btn {
transition: All 0.3s ease;
-webkit-transition: All 0.3s ease;
-moz-transition: All 0.3s ease;
-o-transition: All 0.3s ease;
background: #eee;
background: linear-gradient(#eee, #fafafa);
border-radius: 100%;
height: 14px;
position: absolute;
top: 1px;
width: 14px;
}

.switch-btn.on {
left: 30px;
}

.switch-btn.off {
left: 1px;
}

.switch-btn .switch-btn-content {
background: #dedede;
background: linear-gradient(#dedede, #cacaca);
border-radius: 50%;
height: 9px;
width: 9px;
margin-top: 5px;
margin-left: 5px;
padding: 0;
}

加入文件, 替换 input, 简单好用, 给老大看效果也还不错, 写好文档和使用方法,
<div switch-btn ng-model="item.isTagEnum" no-text="true"></div>
交差完工.

问题

过了两天, 突然发现在表单里面刚刚加载完表单以后点击按钮没有反应, 需要等个 2-3s 才有事件. 开始以为是 ng-repate 的性能问题, 一个 repeat 里面指令太多, 试试 trackby - 无效, 试试减少其他元素(因为之前对填入的数据都加了一个 autoInputTips 的)- 无效, 最后发现只是循环这一个指令都比较慢, 而且数据量越少越快, 感觉很奇怪, 断点到指令的 link, 也发现 link 加载顺序不对, 大概有 2-3s 延迟才执行这个指令的 link, 换成其他指令, 即使很复杂的, 也没有出现加载缓慢的情况.

排查

仔细排查这个指令和其他指令的不同…一点点删改, 发现删除了
css: { href: 'styles/common/switchBtn.css', persist: true },
之后, 指令加载就正常了. 确定是 angularcss 的问题. 给指令加 compile. 去看 network, 发现 css 请求是在 controller 和 prelink 执行完了以后执行的, 于是感觉是因为 css 延迟加载导致指令加载顺序异常.

原理

去看 angularcss 源码, 在 650 行左右

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var directive = $delegate[0];
var compile = directive.compile;
if (!directive.css) {
directive.css = $directive.css;
}
directive.compile = function () {
var link = compile ? compile.apply(this, arguments) : false;
return function (scope) {
var linkArgs = arguments;
$timeout(function () {
if (link) {
link.apply(this, linkArgs);
}
});
$rootScope.$broadcast('$directiveAdd', directive, scope);
};
};
return $delegate;

果然如同猜想, 在指令有 css 的时候, angularcss 改写指令的 compile 方法, 返回的新的 postlink 在$timeout 函数里面执行原始的 link, 所以才导致了 link 函数加载顺序异常.

解决

确定原因以后解决起来就简单了, 去掉组件的独立引用 css, 合并到 common.scss 里面, 在 index 里面引入. 顺便提醒各项目组尽量少使用独立的 angularcss, 而且只在各自的顶级指令里面使用 angularcss, 会改变加载顺序而且由于使用 timeout, 会影响性能.

后记

改完以后基本使用没问题, 性能也还 ok, 因为也使用的 ng-model 传入值, 用起来感觉和原始 input 差不多, 但是有个问题, 传入的值如果不是对象而是值的时候, 会因为改变的是值而不是对象绑定不到原始值上. 比如
<div switch-btn ng-model="isDisplay" no-text="true"></div>
在 switchBtn 指令里面改变$scope.isDel 的值则不会改变原始 scope 里的值, 因为这样传递 scope 是值传递.

那怎么解决呢? 很简单, 用$scope.$parent 来取到原始节点的 scope, 然后改变值就可以了.

1
2
3
if (!/\./.test(attr['ngModel'])) {
$scope.$parent[attr['ngModel']] = value;
}

看到这里可能有人有疑惑… 如果在 ngIf 下面使用, 还是会绑定不到值啊?
是的… 可是原本的 input NgModel 就存在这个问题, 为了统一, 也不准备修复
原理是 ngIf 创建了自己的$scope:true 的原型继承作用域, 虽然可以通过原型拿到值, 但是绑定的时候还是绑定不到原型链上的.

后记的后记

后来我用的时候嫌麻烦, 改成了最简单的 css 实现.

效果:

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
.mui-switch {
width: 52px;
height: 31px;
position: relative;
border: 1px solid #dfdfdf;
background-color: #fdfdfd;
box-shadow: #dfdfdf 0 0 0 0 inset;
border-radius: 20px;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
background-clip: content-box;
display: inline-block;
-webkit-appearance: none;
-moz-appearance: none;
user-select: none;
outline: none;
}

input[type='checkbox'].mui-switch:focus {
outline: #fff none 0;
}

.mui-switch:before {
content: '';
width: 29px;
height: 29px;
position: absolute;
top: 0px;
left: 0;
border-radius: 20px;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

.mui-switch:checked {
box-shadow: #64bd63 0 0 0 16px inset;
/*border-color: #64bd63;
background-color: #64bd63;*/
border-color: #52a4ff;
background-color: #52a4ff;
}

.mui-switch:checked:before {
left: 21px;
}

.mui-switch.mui-switch-animbg {
transition: background-color ease 0.4s;
}

.mui-switch.mui-switch-animbg:before {
transition: left 0.3s;
}

.mui-switch.mui-switch-animbg:checked {
box-shadow: #dfdfdf 0 0 0 0 inset;
/*background-color: #64bd63;*/
background-color: #52a4ff;
transition: border-color 0.4s, background-color ease 0.4s;
}

.mui-switch.mui-switch-animbg:checked:before {
transition: left 0.3s;
}

.mui-switch.mui-switch-anim {
transition: border cubic-bezier(0, 0, 0, 1) 0.4s, box-shadow cubic-bezier(0, 0, 0, 1) 0.4s;
}

.mui-switch.mui-switch-anim:before {
transition: left 0.3s;
}

.mui-switch.mui-switch-anim:checked {
/*box-shadow: #64bd63 0 0 0 16px inset;
background-color: #64bd63;*/
box-shadow: #52a4ff 0 0 0 16px inset;
background-color: #52a4ff;
transition: border ease 0.4s, box-shadow ease 0.4s, background-color ease 1.2s;
}

.mui-switch.mui-switch-anim:checked:before {
transition: left 0.3s;
}

.mui-switch {
&.mui-switch-middle {
height: 26px;
width: 44px;

&:before {
width: 24px;
height: 24px;
}

&:checked {
&:before {
left: 18px;
}
}
}

&.mui-switch-small {
height: 22px;
width: 36px;

&:before {
width: 20px;
height: 20px;
}

&:checked {
&:before {
left: 14px;
}
}
}
}
1
<input class="mui-switch mui-switch-animbg mui-switch-middle" type="checkbox" ng-model="item.isDisplay" />
作者

Mosby

发布于

2016-09-04

更新于

2016-09-10

许可协议

评论