1. HTML style scoped 屬性
今天無意中發(fā)現(xiàn)居然還有這么一個神仙屬性 HTML style scoped 屬性
他的作用主要是將 style
的作用域控制在他自身的父元素之內(nèi)冲茸。
例如:
<div>
<style type="text/css" scoped>
h1 {color:red;}
p {color:blue;}
</style>
<h1>這個標題是紅色的</h1>
<p>這個段落是藍色的斋竞。</p>
</div>
<h1>這個標題是黑色的</h1>
<p>這個段落是黑色的。</p>
以上代碼中style
中的樣式只能作用于第一個div
內(nèi)的元素适篙。
這樣一個功能應(yīng)用在目前公司老項目中非常nice(asp.net+mvc,導入部分頁就不會影響當前頁其他元素了)
但是可惜,除了火狐,其他瀏覽器都不支持V健!!
所以我決定自己實現(xiàn)一個
2. 思路
- 判斷瀏覽器是否支持
scoped
宾茂, - 找到頁面中所有帶有
scoped
屬性的style
元素瓷马,給其父元素添加一個屬性 - 修改style中的所有css選擇器,加上屬性前綴
- 監(jiān)聽dom變化跨晴,處理動態(tài)創(chuàng)建的<style scoped>
3. 編碼
1. 判斷瀏覽器是否支持scoped
if (document.createElement("STYLE").scoped !== undefined) {
return;
}
2. 找到頁面中所有帶有scoped
屬性的style
元素欧聘,給其父元素添加一個屬性
[...document.getElementsByTagName('style')].forEach(x => updateStyle(x));
/**
* 處理 <style> 標簽
* @param style dom
*/
function updateStyle(style) {
if (!style
|| style.parentElement == null
|| style.parentElement == document.head
|| style.parentElement == document.body
|| !style.hasAttribute("scoped")
|| style.scoped === true) {
return;
}
style.scoped = true; // 防止重復處理
const scope = ("_" + (Math.random() * 100000000).toString("36")).replace('.', '');
style.parentElement.setAttribute(scope, '');
// 處理css
}
3. 修改style中的所有css選擇器,加上屬性前綴
/**
* 為選擇器添加范圍前綴scope
* @param {String} selector css選擇器
* @param {String} scope css范圍前綴
* @returns 返回修改后的css選擇器
*/
function updateSelector(selector, scope) {
return selector.split(",").map(x => x.replace(/((?<=^\s*)\s|^)([\S])/, "$1" + scope + " $2")).join(",");
}
/**
* 將css代碼中所有選擇器都加上范圍前綴scope
* @param {String} css css代碼
* @param {String} scope css范圍前綴
* @returns 返回修改后的css代碼
*/
function updateCss(css, scope) {
// css = css.replace(/[\n\r\s]+/g, " "); // 去掉多余的空格和回車
css = css.replace(/("[^"]*")|('[^']*')/g, x => x.charAt(0) + encodeURIComponent(x.slice(1, -1)) + x.charAt(0)); // 去字符串干擾
css = css.replace(/[^{}]+{/g, x => updateSelector(x, scope));
css = css.replace(/("[^"]*")|('[^']*')/g, x => x.charAt(0) + decodeURIComponent(x.slice(1, -1)) + x.charAt(0)); // 還原字符串
return css;
}
用正則處理還是會有一些問題的端盆,不過勝在代碼簡潔怀骤,實際使用中只要注意避開就好了,比如下面這段css就會有問題
@keyframes show {
100% {
opacity: 1;
}
}
4. 監(jiān)聽dom變化焕妙,處理動態(tài)創(chuàng)建的<style scoped>
// 當觀察到變動時執(zhí)行的回調(diào)函數(shù)
const callback = function (mutationsList) {
if (mutationsList.length == 0) {
return;
}
for (const mutation of mutationsList) {
for (const node of mutation.addedNodes) {
if (node.nodeName === "STYLE") {
updateStyle(node);
}
}
}
};
// 創(chuàng)建一個觀察器實例并傳入回調(diào)函數(shù)
const observer = new MutationObserver(callback);
const config = { childList: true, subtree: true };
observer.observe(document, config);
// MutationObserver 是異步的蒋伦,回調(diào)時間不可控,所以需要設(shè)置一個輪詢
setInterval(() => callback(observer.takeRecords()), 100);
5. 完整代碼
// style scoped
(() => {
// 檢測原生 style是否支持 scoped
if (document.createElement("STYLE").scoped !== undefined) {
return;
}
document.createElement("STYLE").__proto__.scoped = false;
/**
* 為選擇器添加范圍前綴scope
* @param {String} selector css選擇器
* @param {String} scope css范圍前綴
* @returns 返回修改后的css選擇器
*/
function updateSelector(selector, scope) {
return selector.split(",").map(x => x.replace(/((?<=^\s*)\s|^)([\S])/, "$1" + scope + " $2")).join(",");
}
/**
* 將css代碼中所有選擇器都加上范圍前綴scope
* @param {String} css css代碼
* @param {String} scope css范圍前綴
* @returns 返回修改后的css代碼
*/
function updateCss(css, scope) {
// css = css.replace(/[\n\r\s]+/g, " "); // 去掉多余的空格和回車
css = css.replace(/("[^"]*")|('[^']*')/g, x => x.charAt(0) + encodeURIComponent(x.slice(1, -1)) + x.charAt(0)); // 去字符串干擾
css = css.replace(/[^{}]+{/g, x => updateSelector(x, scope));
css = css.replace(/("[^"]*")|('[^']*')/g, x => x.charAt(0) + decodeURIComponent(x.slice(1, -1)) + x.charAt(0)); // 還原字符串
return css;
}
/**
* 處理 <style> 標簽
* @param style dom
*/
function updateStyle(style) {
if (!style
|| style.parentElement == null
|| style.parentElement == document.head
|| style.parentElement == document.body
|| !style.hasAttribute("scoped")
|| style.scoped === true) {
return;
}
style.scoped = true; // 防止重復處理
const scope = ("_" + (Math.random() * 100000000).toString("36")).replace('.', '');
style.parentElement.setAttribute(scope, '');
style.textContent = updateCss(style.textContent, "[" + scope + "]");
}
// 當觀察到變動時執(zhí)行的回調(diào)函數(shù)
const callback = function (mutationsList) {
if (mutationsList.length == 0) {
return;
}
for (const mutation of mutationsList) {
for (const node of mutation.addedNodes) {
if (node.nodeName === "STYLE") {
updateStyle(node);
}
}
}
};
// 創(chuàng)建一個觀察器實例并傳入回調(diào)函數(shù)
const observer = new MutationObserver(callback);
const config = { childList: true, subtree: true };
observer.observe(document, config);
// MutationObserver 是異步的焚鹊,回調(diào)時間不可控痕届,所以需要設(shè)置一個輪詢
setInterval(() => callback(observer.takeRecords()), 100);
[...document.getElementsByTagName('style')].forEach(x => updateStyle(x));
})();