一、原由

动态加载一个 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) {}
    };
});

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