起因
在表格展示的时候, 有些状态需要切换的, 之前都是直接使用的 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: {              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
   | 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  .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: #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: #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: #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" />
   |