掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
我們知道,Javascript發(fā)展到現(xiàn)在出現(xiàn)了眾多模塊化規(guī)范,比如AMD、CMD、 Common JS、ESModule等,這些模塊化規(guī)范能夠讓我們的JS實(shí)現(xiàn)作用域隔離。但CSS卻并沒有這么幸運(yùn),發(fā)展到現(xiàn)在卻一直沒有模塊化規(guī)范,由于CSS是 根據(jù)選擇器去全局匹配元素的,所以入鍋你在頁(yè)面的兩個(gè)不同的地方定義了一個(gè)相同的類名,先定義的樣式就會(huì)被后定義的覆蓋掉。由于這個(gè)原因,CSS的命名沖突一直困擾著前端人員。

成都創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供蘭考網(wǎng)站建設(shè)、蘭考做網(wǎng)站、蘭考網(wǎng)站設(shè)計(jì)、蘭考網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)與制作、蘭考企業(yè)網(wǎng)站模板建站服務(wù),10多年蘭考做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。
這種現(xiàn)狀是前端開發(fā)者不能接受的,所以CSS社區(qū)也誕生了各種各樣的CSS模塊化解決方案(這并不是規(guī)范),比如:
現(xiàn)在來(lái)看CSS Module是目前最為流行的一種解決方案,它能夠與CSS預(yù)處理器搭配使用在各種框架中。
CSS Module的流行源于React社區(qū),它獲得了社區(qū)的迅速采用,后面由于Vue-cli對(duì)其集成后開箱即用的支持,將其推到了一個(gè)新高度。
在w3c 規(guī)范中,CSS 始終是「全局生效的」。在傳統(tǒng)的 web 開發(fā)中,最為頭痛的莫過于處理 CSS 問題。因?yàn)槿中裕髅鞫x了樣式,但就是不生效,原因可能是被其他樣式定義所強(qiáng)制覆蓋。
產(chǎn)生局部作用域的唯一方法就是為樣式取一個(gè)獨(dú)一無(wú)二的名字,CSS Module也就是用這個(gè)方法來(lái)實(shí)現(xiàn)作用域隔離的。
在CSS Module中可以使用:local(className)來(lái)聲明一個(gè)局部作用域的CSS規(guī)則。
:local(.qd_btn) {
border-radius: 8px;
color: #fff;
}
:local(.qd_btn):nth(1) {
color: pink;
}
:local(.qd_title) {
font-size: 20px;
}CSS Module會(huì)對(duì):local()包含的選擇器做localIdentName規(guī)則處理,也就是為其生成一個(gè)唯一的選擇器名稱,以達(dá)到作用域隔離的效果。
以上css經(jīng)過編譯后會(huì)生成這樣的代碼:
這里的:export是CSS Module為解決導(dǎo)出而新增的偽類,后面再進(jìn)行介紹。
當(dāng)然CSS Module也允許使用:global(className)來(lái)聲明一個(gè)全局作用域的規(guī)則。
:global(.qd_text) {
color: chocolate;
}而對(duì)于:global()包含的選擇器CSS Module則不會(huì)做任何處理,因?yàn)镃SS規(guī)則默認(rèn)就是全局的。
或許很多了會(huì)好奇我們?cè)陂_發(fā)過程好像很少使用到:local(),比如在vue中,我們只要在style標(biāo)簽上加上module就能自動(dòng)達(dá)到作用域隔離的效果。
是的,為了我們開發(fā)過程方便,postcss-modules-local-by-default插件已經(jīng)默認(rèn)幫我們處理了這一步,只要我們開啟了CSS模塊化,里面的CSS在編譯過程會(huì)默認(rèn)加上:local()。
組合的意思就是一個(gè)選擇器可以繼承另一個(gè)選擇器的規(guī)則。
:local(.qd_btn) {
border-radius: 8px;
color: #fff;
}
:local(.qd_title) {
font-size: 20px;
composes: qd_btn;
}
Composes 還可以繼承外部文件中的樣式
/* a.css */
:local(.a_btn) {
border: 1px solid salmon;
}
/** default.css **/
.qd_box {
border: 1px solid #ccc;
composes: a_btn from 'a.css'
}
編譯后會(huì)生成如下代碼:
從上面的這些編譯結(jié)果我們會(huì)發(fā)現(xiàn)有兩個(gè)我們平時(shí)沒用過的偽類::import、:export。
CSS Module 內(nèi)部通過ICSS來(lái)解決CSS的導(dǎo)入導(dǎo)出問題,對(duì)應(yīng)的就是上面兩個(gè)新增的偽類。
Interoperable CSS (ICSS) 是標(biāo)準(zhǔn) CSS 的超集。
語(yǔ)句:import允許從其他 CSS 文件導(dǎo)入變量。它執(zhí)行以下操作:
一個(gè):export塊定義了將要導(dǎo)出給消費(fèi)者的符號(hào)??梢哉J(rèn)為它在功能上等同于以下 JS:
module.exports = {
"exportedKey": "exportedValue"
}語(yǔ)法上有以下限制:export:
以下是輸出可讀性所需要的,但不是強(qiáng)制的:
應(yīng)該只有一個(gè):export塊
它應(yīng)該位于文件的頂部,但在任何:import塊之后
大概了解完CSS Module語(yǔ)法后,我們可以再來(lái)看看它的內(nèi)部實(shí)現(xiàn),以及它的核心原理 —— 作用域隔離。
一般來(lái)講,我們平時(shí)在開發(fā)中使用起來(lái)沒有這么麻煩,比如我們?cè)趘ue項(xiàng)目中能夠做到開箱即用,最主要的插件就是css-loader,我們可以從這里入手一探究竟。
「這里大家可以思考下,?css-loader主要會(huì)依賴哪些庫(kù)來(lái)進(jìn)行處理?」
我們要知道,CSS Module新增的這些語(yǔ)法其實(shí)并不是CSS 內(nèi)置語(yǔ)法,那么它就一定需要進(jìn)行編譯處理
那么編譯CSS我們最先想到的是哪個(gè)庫(kù)?
postcss對(duì)吧?它對(duì)于CSS就像Babel對(duì)于javascript
可以安裝css-loader來(lái)驗(yàn)證一下:
跟我們預(yù)期的一致,這里我們能看到幾個(gè)以postcss-module開頭的插件,這些應(yīng)該就是實(shí)現(xiàn)CSS Module的核心插件。
從上面這些插件應(yīng)該能看出哪個(gè)才是實(shí)現(xiàn)作用域隔離的吧
整個(gè)流程大體上跟Babel編譯javascript類似:parse ——> transform ——> stringier
與Babel不同的是,PostCSS自身只包括css分析器,css節(jié)點(diǎn)樹API,source map生成器以及css節(jié)點(diǎn)樹拼接器。
css的組成單元是一條一條的樣式規(guī)則(rule),每一條樣式規(guī)則又包含一個(gè)或多個(gè)屬性&值的定義。所以,PostCSS的執(zhí)行過程是,先css分析器讀取css字符內(nèi)容,得到一個(gè)完整的節(jié)點(diǎn)樹,接下來(lái),對(duì)該節(jié)點(diǎn)樹進(jìn)行一系列轉(zhuǎn)換操作(基于節(jié)點(diǎn)樹API的插件),最后,由css節(jié)點(diǎn)樹拼接器將轉(zhuǎn)換后的節(jié)點(diǎn)樹重新組成css字符。期間可生成source map表明轉(zhuǎn)換前后的字符對(duì)應(yīng)關(guān)系。
CSS在編譯期間也是需要生成AST得,這點(diǎn)與Babel處理JS一樣。
PostCSS的AST主要有以下這四種:
#main {
border: 1px solid black;
}@media screen and (min-width: 480px) {
body {
background-color: lightgreen;
}
}border: 1px solid black;
/* 注釋*/
與Babel類似,這些我們同樣可以使用工具來(lái)更清晰地了解CSS 的 AST:
npm i postcss postcss-modules-extract-imports postcss-modules-local-by-default postcss-modules-scope postcss-selector-parser
這些插件的功能我們都可以自己一一去體驗(yàn),我們先將這些主要的插件串聯(lián)起來(lái)試一試效果,再來(lái)自行實(shí)現(xiàn)一個(gè)Postcss-modules-scope插件
(async () => {
const css = await getCode('./css/default.css')
const pipeline = postcss([
postcssModulesLocalByDefault(),
postcssModulesExtractImports(),
postcssModulesScope()
])
const res = pipeline.process(css)
console.log('【output】', res.css)
})()把這幾個(gè)核心插件集成進(jìn)來(lái),我們會(huì)發(fā)現(xiàn),我們的css中的樣式不用再寫:local也能生成唯一hash名稱了,并且也能夠?qū)肫渌募臉邮搅?。這主要是依靠postcss-modules-local-by-default、postcss-modules-extract-imports兩個(gè)插件。
/* default.css */
.qd_box {
border: 1px solid #ccc;
composes: a_btn from 'a.css'
}
.qd_header {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
composes: qd_box;
}
.qd_box {
background: coral;
}
現(xiàn)在我們就自己來(lái)實(shí)現(xiàn)一下類似postcss-modules-scope的插件吧,其實(shí)原理很簡(jiǎn)單,就是遍歷AST,為選擇器生成一個(gè)唯一的名字,并將其與選擇器的名稱維護(hù)在exports里面。
說(shuō)到遍歷AST,與Babel相似Post CSS也同樣提供了很多API用于操作AST:
(更多內(nèi)容可在postcss文檔上查看)
有了這些API我們處理AST就非常方便了
編寫PostCSS插件與Babel類似,我們只需要按照它的規(guī)范進(jìn)行處理AST就行,至于它的編譯以及目標(biāo)代碼生成我們都不需要關(guān)心。
const plugin = (options = {}) => {
return {
postcssPlugin: 'plugin name',
Once(root) {
// 每個(gè)文件都會(huì)調(diào)用一次,類似Babel的visitor
}
}
}
plugin.postcss = true
module.exports = pluginconst selectorParser = require("postcss-selector-parser");
// 隨機(jī)生成一個(gè)選擇器名稱
const createScopedName = (name) => {
const randomStr = Math.random().toString(16).slice(2);
return `_${randomStr}__${name}`;
}
const plugin = (options = {}) => {
return {
postcssPlugin: 'css-module-plugin',
Once(root, helpers) {
const exports = {};
// 導(dǎo)出 scopedName
function exportScopedName(name) {
// css名稱與其對(duì)應(yīng)的作用域名城的映射
const scopedName = createScopedName(name);
exports[name] = exports[name] || [];
if (exports[name].indexOf(scopedName) < 0) {
exports[name].push(scopedName);
}
return scopedName;
}
// 本地節(jié)點(diǎn),也就是需要作用域隔離的節(jié)點(diǎn):local()
function localizeNode(node) {
switch (node.type) {
case "selector":
node.nodes = node.map(localizeNode);
return node;
case "class":
return selectorParser.className({
value: exportScopedName(
node.value,
node.raws && node.raws.value ? node.raws.value : null
),
});
case "id": {
return selectorParser.id({
value: exportScopedName(
node.value,
node.raws && node.raws.value ? node.raws.value : null
),
});
}
}
}
// 遍歷節(jié)點(diǎn)
function traverseNode(node) {
// console.log('【node】', node)
if(options.module) {
const selector = localizeNode(node.first, node.spaces);
node.replaceWith(selector);
return node
}
switch (node.type) {
case "root":
case "selector": {
node.each(traverseNode);
break;
}
// 選擇器
case "id":
case "class":
exports[node.value] = [node.value];
break;
// 偽元素
case "pseudo":
if (node.value === ":local") {
const selector = localizeNode(node.first, node.spaces);
node.replaceWith(selector);
return;
}else if(node.value === ":global") {
}
}
return node;
}
// 遍歷所有rule類型節(jié)點(diǎn)
root.walkRules((rule) => {
const parsedSelector = selectorParser().astSync(rule);
rule.selector = traverseNode(parsedSelector.clone()).toString();
// 遍歷所有decl類型節(jié)點(diǎn) 處理 composes
rule.walkDecls(/composes|compose-with/i, (decl) => {
const localNames = parsedSelector.nodes.map((node) => {
return node.nodes[0].first.first.value;
})
const classes = decl.value.split(/\s+/);
classes.forEach((className) => {
const global = /^global\(([^)]+)\)$/.exec(className);
// console.log(exports, className, '-----')
if (global) {
localNames.forEach((exportedName) => {
exports[exportedName].push(global[1]);
});
} else if (Object.prototype.hasOwnProperty.call(exports, className)) {
localNames.forEach((exportedName) => {
exports[className].forEach((item) => {
exports[exportedName].push(item);
});
});
} else {
console.log('error')
}
});
decl.remove();
});
});
// 處理 @keyframes
root.walkAtRules(/keyframes$/i, (atRule) => {
const localMatch = /^:local\((.*)\)$/.exec(atRule.params);
if (localMatch) {
atRule.params = exportScopedName(localMatch[1]);
}
});
// 生成 :export rule
const exportedNames = Object.keys(exports);
if (exportedNames.length > 0) {
const exportRule = helpers.rule({ selector: ":export" });
exportedNames.forEach((exportedName) =>
exportRule.append({
prop: exportedName,
value: exports[exportedName].join(" "),
raws: { before: "\n " },
})
);
root.append(exportRule);
}
},
}
}
plugin.postcss = true
module.exports = plugin(async () => {
const css = await getCode('./css/index.css')
const pipeline = postcss([
postcssModulesLocalByDefault(),
postcssModulesExtractImports(),
require('./plugins/css-module-plugin')()
])
const res = pipeline.process(css)
console.log('【output】', res.css)
})() 
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流