什么是模板?

在 AngularJS 中,模板是一个 HTML 片段,它通过指令被插入到 DOM 中,这个模板可以包含:

angularjs 指令模板
(图片来源网络,侵删)
  • 静态 HTML:普通的 HTML 标签和内容。
  • AngularJS 指令:如 ng-repeat, ng-if, ng-bind 等。
  • 作用域绑定:通过 (插值表达式) 或 ng-model 等将模板与指令的作用域进行数据绑定。

如何定义模板?

定义模板主要有三种方式,按推荐顺序排列:

使用 template 属性(内联模板)

这是最直接的方式,直接在 JavaScript 代码中用字符串定义模板。

示例:创建一个简单的 hello-world 指令

<!DOCTYPE html>
<html ng-app="myApp">
<head>AngularJS Template Example</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
</head>
<body>
    <div ng-controller="MainController">
        <!-- 使用指令 -->
        <hello-world></hello-world>
    </div>
    <script>
        var app = angular.module('myApp', []);
        app.controller('MainController', function($scope) {
            $scope.user = { name: 'AngularJS' };
        });
        app.directive('helloWorld', function() {
            return {
                // 使用 template 属性定义模板
                template: '<h1>Hello, {{ user.name }}!</h1>',
                // 指令需要一个作用域来访问 user.name
                // 默认情况下,指令会继承其父作用域(这里是 MainController 的作用域)
                scope: true // 创建一个子作用域,这是一个好习惯
            };
        });
    </script>
</body>
</html>

优点

angularjs 指令模板
(图片来源网络,侵删)
  • 简单直接,适合非常小的、简单的模板。

缺点

  • 可读性差:当模板变大时,HTML 字符串会变得冗长且难以阅读和维护。
  • 没有语法高亮:在大多数代码编辑器中,字符串内的 HTML 不会有语法高亮,容易出错。

使用 templateUrl 属性(外部模板)

这是最常用和推荐的方式,你将 HTML 模板放在一个单独的 .html 文件中,然后在指令中通过 templateUrl 指向该文件的 URL。

步骤:

  1. 创建模板文件templates/hello-world.html:

    <!-- templates/hello-world.html -->
    <div class="hello-box">
        <h2>Greeting from a separate file!</h2>
        <p>The message is: {{ message }}</p>
    </div>
  2. 修改指令,使用 templateUrl:

    <!-- index.html -->
    <!DOCTYPE html>
    <html ng-app="myApp">
    <!-- ... head ... -->
    <body>
        <div ng-controller="MainController">
            <hello-world></hello-world>
        </div>
        <script>
            var app = angular.module('myApp', []);
            app.controller('MainController', function($scope) {
                $scope.message = 'This is a message from the controller.';
            });
            app.directive('helloWorld', function() {
                return {
                    templateUrl: 'templates/hello-world.html', // 指向外部模板文件
                    scope: true
                };
            });
        </script>
    </body>
    </html>

优点

  • 高可读性和可维护性:HTML 结构清晰,与 JavaScript 逻辑分离。
  • 语法高亮:编辑器能正确识别 HTML 文件,提供语法高亮和自动补全。
  • 缓存:浏览器可以单独缓存模板文件,提高性能。

缺点

  • 跨域问题:如果你的应用是通过 file:// 协议直接在浏览器中打开的(而不是通过 Web 服务器),由于浏览器的安全策略,加载外部模板文件会失败。必须在 Web 服务器(如 Apache, Nginx)或使用 http-server 等工具下运行。
  • 需要额外的 HTTP 请求:每个 templateUrl 都会发起一个 AJAX 请求,对于大量小模板可能会影响性能(可以通过模板缓存机制缓解)。

使用 <script> 标签(内联模板的变种)

这是一种不常用但有特定用途的技巧,你可以将模板内容放在一个 <script> 标签中,并设置其类型为 text/ng-template

示例:

<!DOCTYPE html>
<html ng-app="myApp">
<head>Script Tag Template</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
</head>
<body>
    <div ng-controller="MainController">
        <hello-world-script></hello-world-script>
    </div>
    <!-- 1. 定义模板内容在 script 标签中 -->
    <script type="text/ng-template" id="hello-world-script-template.html">
        <div style="border: 1px solid blue; padding: 10px;">
            <h3>This template is inside a script tag!</h3>
            <p>Directive: {{ directiveData }}</p>
        </div>
    </script>
    <script>
        var app = angular.module('myApp', []);
        app.controller('MainController', function($scope) {
            $scope.directiveData = 'Data from controller';
        });
        app.directive('helloWorldScript', function() {
            return {
                // 2. templateUrl 指向 script 标签的 id
                templateUrl: 'hello-world-script-template.html',
                scope: true
            };
        });
    </script>
</body>
</html>

优点

  • 避免跨域问题:模板是 HTML 文档的一部分,不需要额外的 AJAX 请求。
  • 单文件应用:对于小型演示或原型,可以将所有代码(HTML, JS, 模板)放在一个文件中。

缺点

  • 可读性差:与 template 属性类似,HTML 混杂在 <script> 标签中,影响整体结构。
  • 不推荐用于生产:破坏了 HTML 的语义化,不易维护。

模板中的作用域

模板如何与数据交互,完全取决于指令的作用域配置,这是指令模板高级用法的关键。

a. 默认作用域(不创建新作用域)

如果指令定义中不设置 scope 属性,它会继承其父级(通常是控制器)的作用域,这意味着指令可以直接访问和修改父作用域中的变量。

// 在父控制器中
$scope.name = 'Parent Scope';
// 指令定义
app.directive('myDirective', function() {
    return {
        template: '<h1>{{ name }}</h1>' // 会显示 "Parent Scope"
    };
});

b. 子作用域(scope: true

创建一个继承自父作用域的新作用域,在模板中,它会先查找自己的作用域,如果没有找到,再向上查找父作用域,修改自己的变量不会影响父作用域,但修改父对象的属性会通过原型链影响到父作用域(需要注意 JavaScript 的原型继承特性)。

// 在父控制器中
$scope.user = { name: 'Parent User' };
// 指令定义
app.directive('myDirective', function() {
    return {
        scope: true, // 创建子作用域
        template: `
            <input ng-model="user.name"> 
            <p>Directive's user.name: {{ user.name }}</p>
        `
    };
});

如果指令内部修改了 user.name,父控制器中的 user.name 也会被修改,因为它们引用的是同一个对象,如果指令中定义了一个新的变量,scope.newVar = 'test',这个变量只存在于子作用域中。

c. 隔离作用域(scope: { ... }

这是创建可复用组件的最佳实践,它创建一个完全独立的作用域,不继承任何父作用域,父作用域的数据必须通过属性(Attribute)显式地传递进来。

隔离作用域通过一个对象来定义,该对象的键是模板中使用的变量名,值是定义数据绑定的方式。

三种绑定方式:

  1. (单向绑定 - 文本绑定)

    • 用途:绑定父作用域中的字符串常量。

    • 工作方式: 将父作用域的属性值作为字符串传递给指令的隔离作用域。

    • 示例:

      <!-- 在 HTML 中 -->
      <div my-dir my-title="Hello from Parent"></div>
      <!-- 指令定义 -->
      app.directive('myDir', function() {
          return {
              scope: {
                  // 模板中的 myTitle 绑定到 HTML 属性 my-title
                  myTitle: '@' 
              },
              template: '<h2>{{ myTitle }}</h2>'
          };
      });
  2. (双向绑定)

    • 用途:绑定父作用域中的模型变量,实现双向同步。

    • 工作方式: 建立一个双向数据通道,指令内部修改该变量,父作用域的变量也会被修改,反之亦然。

    • 示例:

      <!-- 在 HTML 中 -->
      <div ng-controller="ParentCtrl">
          <p>Parent: <input ng-model="parentData"></p>
          <my-dir my-model="parentData"></my-dir>
      </div>
      <!-- 指令定义 -->
      app.directive('myDir', function() {
          return {
              scope: {
                  // 模板中的 myModel 绑定到 HTML 属性 my-model
                  myModel: '=' 
              },
              template: `
                  <p>Directive: <input ng-model="myModel"></p>
                  <p>Directive sees: {{ myModel }}</p>
              `
          };
      });
  3. & (表达式绑定)

    • 用途:绑定父作用域中的一个函数,供指令内部调用。

    • 工作方式& 允许指令执行父作用域中的一个方法,可以传递参数。

    • 示例:

      <!-- 在 HTML 中 -->
      <div ng-controller="ParentCtrl">
          <my-dir on-say-hi="sayHello(name)"></my-dir>
      </div>
      <!-- 指令定义 -->
      app.directive('myDir', function() {
          return {
              scope: {
                  // 模板中的 onSayHi 绑定到 HTML 属性 on-say-hi
                  onSayHi: '&' 
              },
              template: `
                  <button ng-click="onSayHi({ name: 'Directive' })">Say Hi</button>
              `
          };
      });
      app.controller('ParentCtrl', function($scope) {
          $scope.sayHello = function(name) {
              alert('Hello, ' + name + '!');
          };
      });

高级技巧

transclude: 穿透内容

transclude 允许你将指令元素原有的 DOM 内容“提取”出来,并插入到模板的特定位置。

  • transclude: true: 创建一个“transclusion”作用域,它继承自父作用域(而不是指令的隔离作用域)。
  • ng-transclude 指令: 在模板中,这个标签标记了被提取内容应该插入的位置。

场景:创建一个可复用的对话框或标签页组件。

示例:

<!-- 定义指令 -->
<script type="text/ng-template" id="my-tabs.html">
    <div class="tabs">
        <div ng-repeat="tab in tabs">
            <button ng-click="selectTab(tab)">{{ tab.title }}</button>
        </div>
        <div class="content-area">
            <!-- 这里将被原始内容填充 -->
            <div ng-transclude></div> 
        </div>
    </div>
</script>
<!-- 使用指令 -->
<my-tabs>
    <!-- 这里的内容会被 transclude -->
    <div ng-repeat="tab in myTabs" ng-show="tab.isActive">
        {{ tab.content }}
    </div>
</my-tabs>

replace: 替换元素

  • replace: true: 指令的整个根元素(templatetemplateUrl 的最外层标签)会替换掉原始的指令标签。
  • replace: false (默认): 指令的模板会被插入到原始指令标签的内部。

示例 (replace: true):

<!-- HTML -->
<div my-dir></div>
<!-- 指令定义 -->
app.directive('myDir', function() {
    return {
        template: '<h1>Replaced Content</h1>',
        replace: true // <div> 标签会被 <h1> 完全替换
    };
});

注意:AngularJS 1.7+ 版本中,replace 选项已被废弃,因为 <ng-include><component> 等方式提供了更清晰的组件化结构,现代 AngularJS 开发中,通常不推荐使用 replace: true


总结与最佳实践

特性 推荐做法 原因
模板定义 优先使用 templateUrl 可读性、可维护性、语法高亮、可缓存。
作用域 优先使用隔离作用域 (scope: {}) 创建可复用、可预测、无副作用的组件。
数据传递 用于字符串, 用于双向模型, & 用于函数 清晰地定义组件的输入和输出接口。
指令元素 避免 replace: true 已废弃,可能导致意外的 DOM 结构问题,保持原始标签更清晰。
开发环境 始终使用 Web 服务器 避免因浏览器安全策略导致 templateUrl 加载失败。

通过遵循这些原则,你可以构建出结构清晰、易于维护和高度可复用的 AngularJS 应用。