Angular 1 深度解析:組件化編程

angular 1 也要面向組件編程

前端組件化是前端開發模式中一個不可逆轉的趨勢,三大主要前端框架 angular 2 react vue 都不約而同的把組件化編程做爲本身的一大賣點,angular 1 做爲一個歷史相對悠久的框架,在私生子 angular 2 的推進下,終於也搭上了組件化編程的末班車,公司裏那些老項目終於也有機會體驗組件化編程的滋味。css

angular 1 的組件化之路

angular 1 中相似組件化的編程思想其實很早就有,只不過那時候不叫組件,而叫指令(Directive),指定 restrict: 'E' 後這個指令就與現在組件的用法很類似了。angular 1.5 中,又將指令根據 angular 2 的相似概念加以限制,脫胎爲現在的組件(Components)。html

組件的特色

官方文檔列舉了組件和指令的不一樣點。除此以外,一個規範的組件還應符合如下幾個特色。前端

  1. 組件的標籤名稱必須包含中劃線
  2. 組件擁有良好的生命週期
  3. 組件有自包含性
  4. 組件有自封閉性
  5. 組件有可複用性
  6. 組件能夠被定製化

下面依次說明。vue

組件的名稱規範

與指令不一樣,組件必須是一個元素,HTML 對於這一點有特殊的規範。react

HTML 規範把帶有中劃線的標籤留給開發者使用,這樣造成的元素又稱做自定義元素(Custom Element)。咱們雖然沒有用到自定義元素的概念,但二者的行爲是類似的。咱們應該符合這一標準。git

這一點規範對應到 angular 1 中即爲:組件名稱必須帶有駝峯形式。angularjs

例如:github

module.component('dialog', {
	// ...
});

這是不對的。HTML 規範已經定義了 dialog 這個標準元素,重複使用標籤名可能致使咱們自定義的組件行爲和標準元素的行爲混雜到一塊兒,致使奇葩 bug;並且若是這樣作也間接致使開發者不能使用原生的 dialog 標籤。npm

另外,就算如今標準沒有定義某個元素,不表明未來不會定義。咱們的程序既然跑在瀏覽器裏,就要按規矩辦事。這是一種合法的寫法:編程

module.component('customDialog', {
	// ...
});

組件的自包含性

一個設計良好的組件必定有它本身的行爲和默認樣式。

默認行爲

默認行爲在 angular 1 中用控制器(Controller)定義。

function CustomDialogController($service) {
	this.someField = 123;
	this.someMethod = function someMethod() {

	}
}
CustomDialogController.$inject = ['$service'];

module.component('customDialog', {
	controller: CustomDialogController,
	template: require('./customDialogTemplate.html'),
});

由於組件默認啓用 controllerAs,全部變量和函數都是綁定到 this 上的,因此你也可使用 ES2015class 語法來組織代碼:

class CustomDialogController {
	constructor($service) {

	}

	someMethod() {

	}
}
CustomDialogController.$inject = ['$service'];

module.component('customDialog', {
	controller: CustomDialogController,
	template: require('./customDialogTemplate.html'),
});

這樣作有一個問題就是其餘函數不能使用 constructor 裏注入的服務(Service),只能經過 this 中轉一次。我我的的作法是這樣:

class CustomDialogController {
	constructor($service) {
		this.services = { $service };
	}

	someMethod() {
		const { $service } = this.services;
	}
}
// 下略

建議對於邏輯相對簡單的組件的控制器使用 function 定義,複雜的組件使用 class 定義,後者代碼的層次要更爲清晰易讀。

默認樣式

組件的默認樣式直接使用樣式表指定。

custom-dialog {
	display: block;
	// ...
}

對於全部瀏覽器不認識的標籤,默認都是內聯元素(display: inline),對於組件來講一般不是想要的。因此自定義的組件一般至少要有 display: (inline-)block 來改變元素的默認顯示方式。

組件的自封閉性

自封閉性包含兩個方面:數據的自封閉性和樣式的自封閉性。

數據的自封閉性

angular 1 中,組件自身的 scope 已是隔離的(isolate),即組件的 scope 不繼承自父級 scope(__proto__null)。除此以外,一個規範的組件不該該直接使用外部的數據,由於這樣會破壞組件的可複用性。舉幾個例子:

  1. $rootScope
  2. $root、$parent(模板中)
  3. 路由參數
  4. localStorage、sessionStorage

這些數據都應該經過參數綁定 binding 傳入。若是組件是路由插件生成,那麼能夠用 resolve

其次,參數綁定不該使用雙向綁定 =,規範的組件不該(直接)修改組件外部傳入的數據。官方推薦的參數綁定方式有兩種

  1. < 單向綁定,綁定可變數據。一般用於給組件傳遞數據
  2. @ 字符串綁定,綁定字符串。一般用於指定組件行爲

對於單向綁定對象的狀況,因爲是引用傳遞,也不該該修改對象內部的屬性。

遇到要向外部傳值的狀況,推薦使用 ngModel 或 事件綁定(下面會提到)

樣式的自封閉性

組件間的樣式不該該互相干擾,這一點能夠簡單的經過 scss 的樣式嵌套(Nesting)實現:

custom-dialog {
	display: block;
	// ...

	.title {
		// ...
	}

	.body {
		// ...
	}
}

這樣能夠簡單的把組件的內置樣式表限制在組件內部,從而避免樣式外溢。可是這種方法對在組件內部的其餘組件不起效果。若是這個組件的模板中還引用了別的組件,或者這個組件被定義爲可嵌入的(transclude),那麼能夠考慮加類名前綴:

custom-dialog {
	display: block;

	.custom-dialog {
		&-title {
			// ..
		}

		&-body {

		}
	}
}

組件的可複用性

組件爲複用而生,擁有良好自封閉性的組件必然是可複用的,由於這個組件不受任何外部因素干擾。組件的複用形式包括

  1. 一個頁面中使用屢次
  2. 在多個頁面中使用
  3. ng-repeat
  4. 本身套本身(遞歸樹結構)
  5. 整個源代碼拷貝到其餘項目中

等等。一個高度可複用的組件則能夠被稱爲控件,是能夠單獨投稿 npm 項目庫的。

固然,有些組件(好比單獨的頁面)可能複用需求沒那麼高,能夠視組件的複用程度不一樣,從組件的自封閉性和總體代碼量作一些取捨。

組件的定製化

一個高度可複用的組件必定能夠被定製。

行爲的定製化

經過參數綁定實現組件行爲的定製化。例如:

<custom-dialog x-title="My Dialog" x-modal="true"><!-- 與標籤名同樣,自定義屬性名也應該使用中劃線 -->
	<!--content -->
</custom-dialog>
module.component('customDialog', {
	template: require('./customDialogTemplate.html'),
	transclude: true,
	bindings: {
		title: "@",
		modal: '<',
	},
});

出於使用方便的考慮,定製用的參數都是可選的,組件內部實現應該給每一個定製參數設定默認值。

樣式的定製化

組件風格定製可使用 class 判斷。

custom-dialog {
	display: block;
	// ...

	.title {
		font-size: 16px;
		// ...
	}

	&.big {
		.title {
			font-size: 24px;
		}
	}
}

使用時

<custom-dialog x-title="My Dialog" class="mydialog big"></custom-dialog>

深度定製樣式比較好的方式是 CSS 屬性(CSS Variable,注意不是 SCSS 屬性)。

custom-dialog {
	display: block;
	// ...

	.title {
		font-size: 16px;
		color: var(--dialog-title-color, #333);
		// ...
	}

	&.big {
		.title {
			font-size: 24px;
		}
	}
}

這時只須要文檔中說明標題顏色使用 --dialog-title-color 這個 CSS 變量就好,外部使用不依賴於組件內部 DOM 實現。使用時

.mydialog {
	--dialog-title-color: red;
}

組件的生命週期

從建立至銷燬,組件有本身的生命週期(lifecycle),而不像指令那樣把 scope 做爲生命週期。經常使用的回調函數以下:

  • $onInit():組件被初始化時調用。與 constructor 不一樣,angular 1 確保 $onInit 被調用時組件的全部參數綁定都被正確賦值。
  • $onChanges(changeObj):組件參數綁定值被改變時調用。用於監聽綁定值的變化,初次綁定時也會調用這個函數。
  • $onDestroy():組件被銷燬時調用。用於清理內部資源如 $interval 等。

這些函數也是綁定在 this 上的。若是 controller 使用 ES2015class 定義方式,能夠這麼寫:

class CustomDialogController {
	constructor() {}

	onInit() {}

	onChanges({ prop1, prop2 }) {}

	onDestroy() {}
}

組件間的通訊

組件間通訊是一個讓不少人頭疼的問題,一般有這樣 3 種狀況

子 -> 父

這種狀況有標準的實現方式:事件綁定。例如

class CustomDialogController {
	close($value) {
		this.hide = true;
		this.onClose({ $value });
	}
}

module.component('customDialog', {
	controller: CustomDialogController,
	template: require('./customDialogTemplate.html'),
	bindings: {
		onClose: '&',
	},
});

使用時:

<custom-dialog on-close="$ctrl.handleClose(value)"></custom-dialog>

這種方式也能夠用於子組件向父組件傳值。

父 -> 子

用於觸發子組件的某個動做。除了改變某個在子組件內部監聽變化的綁定參數值外,行之有效的方式就只有事件廣播。

子組件先監聽某個事件

$scope.$on('custom-dialog--close', () => this.close());

父組件發送廣播

$scope.$broadcast('custom-dialog--close');

切記:事件是全局性的。當有組件複用的狀況時請使用標識指定接收對象(BUS 模型);另外最好給事件名添加組件前綴。

同級組件

請經過父級組件中轉

子 -> 某全局性組件

這個顯示 Notification 時最經常使用。遇到這種狀況時,能夠封裝服務(Service)。例如:

module.component('globalNotification', {
	controller: class GlobalNotificationController {
		constructor(notificationService) {
			notificationService.component = this;
		}

		show(props) {
			// ...
		}
	}
});

module.factory('notify', function NotifyService() {
	return {
		warn(msg) {
			this.show({ type: 'warn', text: msg });
		}
		error(msg) {
			this.show({ type: 'error', text: msg });
		}
	}
});

方案並不完美。若是有更好的建議歡迎提出。

結語

有人可能問既然三大前端框架都是組件化的,何須還要在 angular 1 上實現。卻不知 angular 1 的組件誕生的初衷就是爲了減小向 angular 2 遷移的難度。機會老是留給有準備的人,哪天老闆大發慈悲表示給你把代碼重寫的時間,你卻看着項目裏滿屏的 $scope.abc = xxx 不知所措,這豈不是悲劇。。。

本文同時發佈在:http://www.javashuo.com/article/p-tqdprqhn-kb.html

相關文章
相關標籤/搜索