Các Cơ Chế Nền Tảng Trong AngularJS: Dependency Injection và Directives

Giới thiệu về kiến trúc AngularJS

Mặc dù các framework hiện đại như React hay Vue đang chiếm lĩnh thị trường, nhưng AngularJS vẫn sở hữu những thiết kế kiến trúc đáng để nghiên cứu. Hai khái niệm quan trọng nhất làm nên sức mạnh của framework này là Dependency Injection (DI) và Directives. Hiểu rõ cơ chế này giúp developer nắm vững nguyên lý quản lý luồng dữ liệu và mở rộng HTML trong phát triển ứng dụng web.

1. Dependency Injection (DI)

Dependency Injection là mẫu thiết kế giúp tách biệt việc khởi tạo đối tượng khỏi việc sử dụng đối tượng đó. Trong Java, DI thường được thực hiện thông qua reflection và JVM. Trong JavaScript, cơ chế này linh hoạt hơn nhờ tính chất dynamic của ngôn ngữ, cho phép truyền phụ thuộc vào hàm khởi tạo hoặc thông qua một container quản lý.

Vấn đề耦合 (Coupling) trong code thông thường

Khi không sử dụng DI, các lớp thường tự khởi tạo phụ thuộc của mình, dẫn đến khó kiểm thử và bảo trì:

function DatabaseService() {
    this.connection = "MySQL";
}
DatabaseService.prototype.query = function(sql) {
    console.log("Executing: " + sql + " on " + this.connection);
};

function UserManager() {
    // Coupling cao: UserManager tự tạo ra DatabaseService
    this.db = new DatabaseService();
}
UserManager.prototype.getUser = function(id) {
    this.db.query("SELECT * FROM users WHERE id=" + id);
};

// Sử dụng
var manager = new UserManager();
manager.getUser(1);

Giải pháp Inject thủ công

Chúng ta có thể truyền phụ thuộc từ bên ngoài vào constructor để giảm coupling:

function UserManager(databaseService) {
    this.db = databaseService;
}
UserManager.prototype.getUser = function(id) {
    this.db.query("SELECT * FROM users WHERE id=" + id);
};

// Inject từ bên ngoài
var sqlDB = new DatabaseService();
var manager = new UserManager(sqlDB);
manager.getUser(1);

Cơ chế DI trong AngularJS

AngularJS tự động hóa quá trình này thông qua `$injector` và `$provide`. Mỗi module sẽ có một bộ chứa dịch vụ (provider cache). Khi một controller hoặc service yêu cầu một dependency, `$injector` sẽ tìm kiếm và cung cấp instance tương ứng.

Cấu trúc nội bộ của một module AngularJS bao gồm các hàng đợi cấu hình và thực thi:

var moduleDefinition = {
    _invokeQueue: [],      // Hàng đợi gọi hàm
    _configBlocks: [],     // Các khối cấu hình
    _runBlocks: [],        // Các khối chạy khi khởi động
    requires: [],          // Các module phụ thuộc
    name: 'coreModule',
    // Các phương thức đăng ký service
    provider: invokeLater('$provide', 'provider'),
    factory: invokeLater('$provide', 'factory'),
    service: invokeLater('$provide', 'service'),
    value: invokeLater('$provide', 'value'),
    // Đăng ký component
    directive: invokeLater('$compileProvider', 'directive'),
    controller: invokeLater('$controllerProvider', 'register')
};

Đối tượng `$provide` chịu trách nhiệm đăng ký service, trong khi `$injector` chịu trách nhiệm truy xuất chúng:

// Đăng ký một service thông qua provider
$provide.provider('loggingService', function() {
    this.prefix = "[LOG]";
    this.$get = function() {
        return {
            log: function(msg) {
                console.log(this.prefix + " " + msg);
            }
        };
    };
});

// Sử dụng trong controller
app.controller('DashboardCtrl', function($scope, loggingService) {
    $scope.showMessage = function() {
        loggingService.log("Dashboard loaded");
    };
});

Để truy xuất service thủ công bên ngoài Angular context, ta có thể sử dụng `angular.injector`:

var injector = angular.injector(['ng', 'myAppModule']);
var logger = injector.get('loggingService');
logger.log("Manual injection test");

2. Directives (Chỉ thị)

Directives cho phép开发者 tạo ra các thẻ HTML mới hoặc mở rộng hành vi của các thẻ现有. Đây là cách AngularJS thực hiện việc绑定 dữ liệu và tương tác DOM.

Cấu hình cơ bản của Directive

Khi định nghĩa một directive, chúng ta cần quan tâm đến các thuộc tính cấu hình sau:

  • restrict: Quy định cách sử dụng (E: Element, A: Attribute, C: Class, M: Comment).
  • template/templateUrl: HTML sẽ được render vào vùng chứa của directive.
  • replace: Quyết định whether directive thay thế hoàn toàn element gốc hay chỉ chèn nội dung vào bên trong.
  • priority: Thứ tự thực thi khi nhiều directive cùng tồn tại trên một element.

Quản lý Scope trong Directive

Scope là cầu nối giữa directive và controller cha. Có 3 trạng thái chính:

  • false: Chia sẻ scope chung với cha (thay đổi ở con sẽ ảnh hưởng cha).
  • true: Tạo scope mới kế thừa từ cha (prototypal inheritance).
  • {}: Tạo isolated scope hoàn toàn. Đây là cách phổ biến nhất để tạo component tái sử dụng.

Trong isolated scope, việc binding dữ liệu được quy định qua các ký tự:

scope: {
    modelValue: '=',        // Two-way binding (Đồng bộ 2 chiều)
    titleText: '@',         // One-way binding (Chuỗi ký tự, dùng {{}})
    callbackFunc: '&'       // Method binding (Gọi hàm từ cha)
}

Giao tiếp giữa các Directive (Require)

Thuộc tính `require` cho phép một directive truy cập vào controller của một directive khác. Ký tự `^` cho phép tìm kiếm controller ở phần tử cha.

require: '^parentDirective'  // Tìm controller của parentDirective ở cha

Vòng đời Link và Controller

  • controller: Chạy trước, dùng để expose API cho các directive khác thông qua `require`.
  • link: Chạy sau, dùng để đăng ký event listener và thao tác DOM trực tiếp.

Ví dụ thực tế: Progress Bar Component

Dưới đây là ví dụ xây dựng một component thanh tiến trình tùy chỉnh, minh họa cho việc sử dụng isolated scope và template:

<!DOCTYPE html>
<html ng-app="demoApp">
<head>
    <script src="angular.js"></script>
    <style>
        .bar-container { border: 1px solid #ccc; width: 200px; height: 20px; }
        .bar-fill { height: 100%; background-color: green; }
    </style>
</head>
<body ng-controller="MainCtrl">
    <div>
        <h3>Tiến độ xử lý</h3>
        <progress-bar current-value="progress" max-value="100"></progress-bar>
    </div>

    <script>
        var app = angular.module('demoApp', []);
        
        app.controller('MainCtrl', function($scope) {
            $scope.progress = 75;
        });

        app.directive('progressBar', function() {
            return {
                restrict: 'E',
                scope: {
                    current: '=currentValue',
                    maximum: '=maxValue'
                },
                template: '<div class="bar-container"><div class="bar-fill" ng-style="{width: percent + '%'}"></div></div>',
                link: function(scope, element, attrs) {
                    scope.$watch('current', function(newVal) {
                        scope.percent = (newVal / scope.maximum) * 100;
                    });
                }
            };
        });
    </script>
</body>
</html>

Trong ví dụ trên, directive `progress-bar` nhận dữ liệu từ controller cha thông qua binding hai chiều. Hàm `link` sẽ tính toán tỷ lệ phần trăm và cập nhật style CSS cho thanh hiển thị dựa trên giá trị nhận được.

Thẻ: angularjs dependency-injection custom-directives javascript-architecture frontend-development

Đăng vào ngày 10 tháng 9 lúc 23:19