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.