Angular 动态创建 directive

一、原由

动态加载一个 directive,其实你会发现这种需求特别的变态,因为对于 angularjs 而言绝大部分情况下应该不会需要这样,这点很大的程度是由于angular是html标记驱动,已经极大的提供很松散、有效的、优美的加载各式各样 directive

然则,故事是发生在一个编辑器上,我们所认知的编辑器是为创建一个富HTML,而为了让用户更好的体验(总不能让客户写HTML语言吧)所产生的按表单填写数据最终生成有效的HTML。那么自然我们不可能只放一种像百度编辑器就完事,你让用创建一个轮播广告几乎是不可能;所以需要有很多组件,比如:商品列表、富文本、图片广告、轮播广告等等,而每一种组件又不可能是统一界面。

废话说了这么多,其核心就是我需要创建很多组件,用户随时可以创建某一组件在列表中。

二、白痴写法

那好,首先想到的是按比较白痴写法,比如:

<div ng-repeat="item in list">
    <ae-config ng-if="item.type=='config'"></ae-config>
    <ae-rich ng-if="item.type=='rich'"></ae-rich>
    <ae-image ng-if="item.type=='image'"></ae-image>
    <ae-nav ng-if="item.type=='nav'"></ae-nav>
    ....
</div>
<button ng-click="list.push({ type: 'rich' })">Add</button>

这的确是一个快速办法,当然这里的 ... 还有很多,但是会有一个很严重的问题,那么:

  1. 页面会有大量的 <!-- ngIf: i.type==='nav' --> 存在,是的,是的,是的,我有洁癖,这么重要的事必须说三遍。
  2. 虽然组件越来越多,维护也跟着升高,比如说:不管我组件下架或上架都需要去修改这种该死的代码,更何况不同业务线所需要组件可能也不相同,倒置哪怕我不需要的组件也要走一遍。
  3. 组件替换太麻烦,是这样,每一种业务都需要一个叫 config 配置信息,即是配置信息肯定不能当组件随意创建或删除,所以就行成另一种麻烦了。

总之,可以列的坏处还有很多。因此……

我们需要更简单、更优美的编写方式。

三、概念

1、当前作用域

理解为 controller 的 scope。

2、子作用域

理解为 directive 创建的作用域。

3、directive 隔离 scope 数据

在了解之前必须先了解 directive 隔离 scope 的一些概念。scope他可以有各种值,他们都代表者不同含义,私以为理解各种方式的使用在 Angular 极为重要,毕竟没有 directive 那 angular 就什么也不是了。

A、false

默认值,创建自己作用域,直接使用当前作用域。

B、true

创建自己作用域,并继承当前作用域。这样不但可以直接使用原作用域的数据模型,还可以拥有自己的数据模型,同时修改自己数据模型并不会污染原作用域。

C、{} 创建自己作用域,继承当前作用域。正常我们理解为 隔离 scope,但我们可以通过三种不同的传值方式,让directive可以和controller进行通信。

scope: {
    config1: '=',
    config2: '@',
    config3: '&'
}
  • =,双向绑定模式,即不管是controller还是directive自身修改 config 数据模板都会在两边同步更新。
  • @,首先值永远是一个字符串,其次只允许当前作用域修改directive的子作用域。
  • &,首先值必须是表达式(即function),由directive的子作用域访问当前作用域的一个function。
    另外注意带有参数形式的表达式:directive调用无须关心参数列表,因为对于隔离模式而言,表达式的参数是无法被改变的,即在html中指定什么参数那便是什么参数。

注意:在controller设置隔离 scope 参数时,对于像:<gen config1="{{type}}"></gen><gen config1="type"></gen>,一个是表达式结果值,一个对象本身。这一点要注意了。

四、$compile 编译HTML

我们经常使用的像 ng-ifng-repeatng-bind 等等,其实内部都是通过 $compile 先编译再将编译后的HTML插入到DOM树上,目的是为了让插入的HTML能够实现双向绑定和事件监听等工作。

$compile 是一个 service,因此我们可以在controller和directive中任意使用。所以这里首先需要创建一个用来动态创建各种组件的 directive。

app.directive('genCom', function($compile) {
    return {
        restrict: 'E',
        scope: true,
        require: '?ngModel',
        link: function(scope, element, attrs, ngModel) {
            if (!ngModel) return;
            ngModel.$render = function() {
                if (!ngModel.$viewValue) return;

                var data = ngModel.$viewValue,
                    comComile = $compile('<div com-' + data.type + '="i"></div>');
                element.html(comComile(scope));
            }
        }
    }
};
});

scope: true

这里为什么是 true,由于 genCom 只是一个中件间,而我们希望通过此动态创建的组件上下文依然能访问到当前 scope 的数据。

require: ‘?ngModel’

当然数据源肯定不能少。

$compile

这里要拆分来看。

首先 comComile 返回Function对象,简单来说是对 HTML 里面所涉及的其他 directive 进行数据收集工作。即将所有 directive 找出来,组成一个数组。

其次 comComile(scope) 才能够算是产生一个可被插入到DOM的HTML元素,这里将 scope 做为所需要的父作用域存在着。这也就是我们前面为什么需要将scope设置为true的原因,这样我们可以保证通过 $compile 编译后其作用域不会跟controller断背,毕竟这个directive只为创建另一个directive本身就没什么业务意义。

因此最后在 controller 中可以这么使用:

<div gen-com="rich" ng-model="item"></div>

当然我们也可以这么玩:

<div class="item" ng-repeat="item in list">
    <div gen-com="{{item.type}}" ng-model="item"></div>
</div>

当然还有组件部分。

// 配置信息组件
app.directive('comConfig', function() {
return {
    restrict: 'AE',
        template: '<p>config</p>',
        transclude: true,
        replace: true,
        scope: {},
        link: function(scope, element) {}
    };
});
// 富文本框组件
app.directive('comRich', function() {
return {
    restrict: 'AE',
        template: '<p>rich</p>',
        transclude: true,
        replace: true,
        scope: {},
        link: function(scope, element) {}
    };
});
// 导航组件
app.directive('comNav', function() {
return {
    restrict: 'AE',
        template: '<p>nav</p>',
        transclude: true,
        replace: true,
        scope: {},
        link: function(scope, element) {}
    };
});

怎么样,很酷吧!完整示例

ionic 调试

ionic 调试时如果是非调用原生插件外,都可以直接用浏览器调试,极为方便。我非常推荐使用 chrome 来开发 ionic,原因是 chrome 的不管 android 还是 ios 都是相同的内核,同样得到的是比较接近真机效果。因此这里只对真机环境的调试做描述。

Android

首先我们通过 chrome 访问: chrome://inspect/#devices

chrome_devices_not_mobile

当我们通过USB和手机相连后需要确认以下几个事:

  • 确保PC机已经安装好android驱动
  • 开启手机开发者选项
  • 开启手机USB调试
  • 选择调试应用处选择要调试的APP

当我们再回到 chrome 后,就可看到我们要调试APP信息了。

chrome_devices_inline

点击 inspect 就可以进入调试状态了。

chrome_devices_inspect

怎么样,当你看到这个页面后就不需要我多说了。

IOS

其实和android大同小异,只不过我写文章时手头没有工具,暂时先放一放,也可参考这里

Ionic View

他是 ionic 提供的一个演示工具,为什么也把他写在这里呢,是因为大部分只要在PC调试没有问题的,一般在真机上都不会有什么差别,而 Ionic View 是一个可以让我们快速构建好的APP;但未发布的;而又可以给方便客户演示真实效果的工具。

首先需要访问 https://apps.ionic.io/apps 来创建一个账号。

接着通过指令将项目上传:ionic upload 会提示你账号和密码。

最后在 Apple Store 或 Google Store 里检索: ionic view,安装后。

同样登录后可以看到已经上传的APP,先下载再查看。就可以非常方便的看到实际效果。

AngularJS优化:bindonce 不是万能药,用时需谨慎

看过一次对 Misko Hevery 的专访,有说到限制2000个对象绑定,可以让页面刷新时间减少到5ms以下。而所谓的刷新时间是指 dirty-checking,因为一次 dirty-checking 时间将会影响 DOM 更新时间。dirty-checking 核心就是对所有 $watch 进行检查。而 bindonce 是为了减少这种不必要的 $watch

什么是 Watch

$watch就是监视数据的变化。这里的数据可能是指令或$scope

比如,以下创建两个$watch,一个文本框、一个名字输出:

<p>用户名:<input type="text" ng-model="name" /></p>
<p>我的用户名:{{name}}</p>

以上,当我在文本框输入字母 a 时,会立即启动一个检查(前期也会有很多工作,比如绑定keydown事件,用于触发事件,这里不再说太多,建议看源代码),会检查到 name 发生变化;这个过程叫dirty-checking,而对所有的 $watch 都检查一遍后,才会将主动权交给浏览器做DOM上的变化(即输出部分)。

试想当我们通过一个 ng-repeat 循环N条记录,而每一行有10个绑定对象,那意味者会有 N*10 个 $watch 等着我们。往往在一个复杂的页面,这种行绑定对象会多得多。

取消 $watch

对于像 ng-repeat,都是由官方提供的,限制是无法限制,所以我们只能在绑定的时候使用传统的方式。而 bindonce 就是这样子的,比如:bo-text 相当于 binder.element.text(value);

注意:bo-text 是以一个指令出现,那么我们需要注意一个问题了,指令是在DOM被加载完全后开始执行,这里的执行顺序非常重要。当你的数据源已经加载完成,此时 bo-text 指令出现,他才能获取到数据,否则永远都是空的。因此这里得出第一个优化规则:

bindonce只能在数据存在时有效

所以对于像表单数据,正常我们都是从 $http.get 来获取,而DOM早就加载完成,所以这个时候再去使用 bindonce 已经没有意义了。

而像SPA很多时候我们会有一些共享的数据,比如版本号、APP名称、用户信息等等,这些信息如果你能够保证在 DOM 加载前就已经存在,那么就可以大胆去使用 bindonce

bindonce 更适合数据列表

$watch 最大的泛滥是数据列表,如我前所说它是N*M在增长。而我们又是通过 ng-repeat 来循环数据,ng-repeat 本身就是一个指令,所以本身会有一个 $watch 来监视数据变化,换句话说我们可以保证 规则一 原则,所以我们大可大胆在 ng-repeat 下去掉我们不需要监视的数据。

所以这里得出第二个优化规则:

ng-repeat下使用bindonce是安全的

在优化之前,我有几点建议:

  1. 绝对不要过早优化,因为NG已经够快的了。
  2. SPA应用加载顺序非常重要。
  3. bindonce适用于数据列表。
  4. 性能的限制因素是人,NG真的非常快。

AngularJS中的value和constant

Values

当我们需要一组数据,比如:当前用户登录信息或APP的一些配置,而这组数据又希望在 controller/service 使用它,那么它就是做这个事的。

value 支持string、number、date-time、array、object、function,比如:

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

app.value('user', {
    name: 'asdf',
    role: 'admin'
});

而当我们需要使用时,就像这样:

app.controller('MyCtrl', ['$scope', 'user', function($scope, user) {
    $scope.name = user.name;
}]);

Angular 会自动将 user 注入进来,同时我们可以随时修改数据,比如:

user.role = 'normal';

这样子其他再注入 user 时role的值变成normal。

Constants

valueconstant 之间的差异只有两点:

  1. 可注入类型,对于前者只允许在 servicecontroller,而后者还包括模块配置函数。
  2. constant 是常量,无法被修改。

AngularJS在controller延迟加载自定义指令

有个几百KB的地址库,原则上我可以使用 http 按需加载数据来减少文件大小。可我就是太变态,因为地址的修改频率非常非常低,所以要一开始就把玩意儿加载放在那不使用,就不是一个滋味。这便是变态之原因一。

我的需求里面有一个订单详情页,但详情页可以直接修改地址,问题来了,详情页打开频率非常高,而修改频率极低【注意是极低】,所以我希望当我对其进行修改操作时,再延迟加载指令。其变态之原因二。

依赖

ocLazyLoad

简单点说就是将所需要的文件加载完毕后,并返回一个 promise,这样我们可以跟路由的 resolve一起,达到一个完成的延迟加载 css/js etc.

同时,ocLazyLoad 提供一个指令,可以直接在 view 中延迟加载某个指令,也会重新编译该指令内的HTML元素 $compile(content)($scope)

<div oc-lazy-load="{name: 'TestModule', files: ['js/testModule.js', 'partials/lazyLoadTemplate.html']}">
    // Use a directive from TestModule
    <test-directive></test-directive>
</div>

看上去问题已经可以解决了,可总是那么不如意,ngIf 会打乱由于 DOM 被预编译且被缓存,那就蛋疼了。

即使重新编译也没用。

好,既然是变态做法,那自然不能够依赖 ocLazyLoad 提供的最方便免费午餐,需要一点点改进。

一个解决办法

大概分几步:

  1. 包裹一个 div,方便于只对该元素内进行重新编译,否则会出现循环编译。
  2. 调用 $ocLazyLoad.load 延迟加载所需要的文件。
  3. 已经加载过的,还需要重新编译,因为对于 ocLazyLoad 而言会保留自己的 module 信息。

那么,以下就是大概的一个方案了:

<div ng-init="cityLoad('#citySelect')">
    <city-select id="citySelect" ng-model="city.dis"></city-select>
</div>
var citySelectParam = {
    name: 'angular.city.select',
    files: ['angular-city-select/angular-city-select.min.js', 'angular-city-select/angular-city-select.min.css']
};
$scope.cityLoad = function (eleId) {
    try {
        angular.module(citySelectParam .name);
        $compile(angular.element(eleId))($scope);
    } catch (err) {
        $ocLazyLoad.load(citySelectParam ).then(function () {
        $compile(angular.element(eleId))($scope);
        });
    }
}

好了,大概就是这样子吧。希望您也有这么个变态做法。

ng-if和ng-click组合导致ng-click无效

我们来先看以下一个示例,这里我有一个jsfiddle副本。

Code 1

<div class="container-fluid">
    <p ng-if="!changing">
        {{title}}
        <button type="button" ng-click="changing = true" class="btn btn-primary">修改</button>
    </p>
    <p ng-if="changing">
        <input type="text" class="form-control" ng-model="title" />
        <button type="button" ng-click="changing = false" class="btn btn-primary">保存</button>
    </p>
</div>

Code 2

<div class="container-fluid">
    <p ng-show="!changing">
        {{title}}
        <button type="button" ng-click="changing = true" class="btn btn-primary">修改</button>
    </p>
    <p ng-show="changing">
        <input type="text" class="form-control" ng-model="title" />
        <button type="button" ng-click="changing = false" class="btn btn-primary">保存</button>
    </p>
</div>

以上,会发现Code 1无法点击修改,那么问题的根源在哪?

1、优先级。

ng-if的优化级是600,这高于绝大部分其他原始指令,比如:ng-click

2、ng-if有自己的$scope

因为每一个ng-if都会有自己的$scope,所以当去个性changing值时,只会针对ng-if的作用域。那么相对于我们的父作用域依然还是false

ng-ifng-show

ng-showng-if 有两点比较核心的区别。

  • ng-if 自己创建域,每次都会重新创建或移除DOM。
  • ng-show 只是单纯的设置 display: none !important;,无独立域。

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top