在Web前端開發中,我們經常會遇見需要動態的將一些來自后端或者是動態拼接的HTML字符串綁定到頁面DOM顯示,特別是在內容管理系統(CMS:是Content Management System的縮寫),這樣的需求,更是遍地皆是。
對於對angular的讀者肯定首先會想到ngBindHtml,對,angular為我們提供了這個指令來動態綁定HTML,它會將計算出來的表達式結果用innerHTML綁定到DOM。但是,問題並不是這么簡單。在Web安全中XSS(Cross-site scripting,腳本注入攻擊),它是在Web應用程序中很典型的計算機安全漏洞。XSS攻擊指的是通過對網頁注入可執行客戶端代碼且成功地被瀏覽器執行,來達到攻擊的目的,形成了一次有效XSS攻擊,一旦攻擊成功,它可能會獲取到用戶的一些敏感信息、改變用戶的體驗、誘導用戶等非法行為,有時XSS攻擊還會合其他攻擊方式同時實施比如SQL注入攻擊服務器和數據庫、Click劫持、相對鏈接劫持等實施釣魚,它帶來的危害是巨大的,也是web安全的頭號大敵。更多的Web安全問題,請參考wiki https://en.wikipedia.org/wiki/Cross-site_scripting%E3%80%82
在angular中默認是不相信添加的HTML內容,對於添加的HTML內容,首先必須利用$sce.trustAsHtml,告訴angular這是可信的HTML內容。否則你將會得到$sce:unsafe的異常錯誤。
Error: [$sce:unsafe] Attempting to use an unsafe value in a safe context.
下面是一個綁定簡單的angular鏈接的demo:
HTML:
<div ng-controller="DemoCtrl as demo">
<div ng-bind-html="demo.html"></div>
</div>
JavaScript:
angular.module("com.ngbook.demo", [])
.controller("DemoCtrl", ["$sce", function($sce) {
var vm = this;
var html = '<p>hello <a href="https://angular.io/">angular</a></p>';
vm.html = $sce.trustAsHtml(html);
return vm;
}]);
對於簡單的靜態HTML,這個問題就解決了。但對於復雜的HTML,這里的復雜是指帶有angular表達式、指令的HTML模板,對於它們來說,我們不僅希望綁定大DOM顯示,同時還希望得到angular強大的雙向綁定機制。ngBindHhtml並不會和$scope關聯雙向綁定,如果在HTML中存在ngClick、ngHref、ngSHow、ngHide等angular指令,它們並不會被compile,點擊這些按鈕,也不會發生任何反應,綁定的表達式也不會在更新。例如嘗試將上次的鏈接變為:ng-href=“demo.link”,鏈接並不會被解析,在DOM看見的仍然會是原樣的HTML字符串。
在angular中的所有指令要生效,都需要經過compile,在compile中包含了pre-link和post-link,連接上特定行為,才能工作。大部分情況下compile,是會在angular啟動時,自動compile的。但如果是對於動態添加的模板,則需要手動的compile。angular中為我們提供了$compile服務來實現這一功能。下面是一個比較通用的compile例子:
HTML:
<body ng-controller="DemoCtrl as demo">
<dy-compile html="{{demo.html}}">
</dy-compile>
<button ng-click="demo.change();">change</button>
</body>
JavaScript:
angular.module("com.ngbook.demo", [])
.directive("dyCompile", ["$compile", function($compile) {
return {
replace: true,
restrict: 'EA',
link: function(scope, elm, iAttrs) {
var DUMMY_SCOPE = {
$destroy: angular.noop
},
root = elm,
childScope,
destroyChildScope = function() {
(childScope || DUMMY_SCOPE).$destroy();
};
iAttrs.$observe("html", function(html) {
if (html) {
destroyChildScope();
childScope = scope.$new(false);
var content = $compile(html)(childScope);
root.replaceWith(content);
root = content;
}
scope.$on("$destroy", destroyChildScope);
});
}
};
}])
.controller("DemoCtrl", [function() {
var vm = this;
vm.html = '<h2>hello : <a ng-href="{{demo.link}}">angular</a></h2>';
vm.link = 'https://angular.io/';
var i = 0;
vm.change = function() {
vm.html = '<h3>change after : <a ng-href="{{demo.link}}">' + (++i) + '</a></h3>';
};
}]);
這里創建了一個叫dy-compile的指令,它首先會監聽綁定屬性html值的變化,當html內容存在的時候,它會嘗試首先創個一個子scope,然后利用$compile服務來動態連接傳入的html,並替換掉當前DOM節點;這里創建子scope的原因,是方便在每次銷毀DOM的時,也能容易的銷毀掉scope,去掉HTML compile帶來的watchers函數,並在最后的父scope銷毀的時候,也會嘗試銷毀該scope。
因為有了上邊的compile的編譯和連接,則ngHref指令就可以生效了。這里只是嘗試給出動態compile angular模塊的例子,具體的實現方式,請參照你的業務來聲明特定的directive。