這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 追憶Scoped 偶然想起了一次面試,二面整體都聊完了,該做的演算法題都做出來了,該背的八股文也背的差不多了,面試官頻頻點頭,似乎對我的基礎和項目經驗都很是滿意。嗯,我內心os本次面試應該十拿九穩了。 突然,面試官說:「我的主技術棧是Rea ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
追憶Scoped
偶然想起了一次面試,二面整體都聊完了,該做的演算法題都做出來了,該背的八股文也背的差不多了,面試官頻頻點頭,似乎對我的基礎和項目經驗都很是滿意。嗯,我內心os本次面試應該十拿九穩了。
突然,面試官說:「我的主技術棧是React,Vue寫的很少,對Vue中style樣式中的scoped有點興趣,你知道vue中為什麼有這個麽?」
我不假思索:「哦, 這個主要是為了做樣式隔離,避免組件間和父子組件間的樣式覆蓋問題。有點類似React中使用的StyleModule,也是可以避免不同組件間樣式覆蓋問題。」
回答完之後我又開始暗自得意,回答的多麼巧妙,既回答了問題,又表明自己對React也是有一點瞭解的。
可能面試官看出了我的得意之色,點點頭之後又問出了一個問題:「知道是怎麼實現的麽?」
我先茫然的盯著面試官的臉看了兩秒鐘,然後在我已有的知識庫中搜索,搜索一遍又一遍,發現這可能是我的一個盲區,我確實不太清楚實現原理啊!!
面試官可能看出了我對於此的知識匱乏,很和善的說「我就是感興趣,隨便問問」。
啊,事已至此,我只能對面試官露出一個尷尬卻不失禮貌的微笑說「這塊我確實沒有仔細思考過,我下來會詳細研究一下這款,具體是如何現在scoped的。」
「好,那本次面試就到這裡吧,回去等通知吧!」面試官和藹的說。
雖然最後面試順利通過,但是這個問題我覺得還是有必要記錄下:”Vue中Style中的Scoped屬性是如何實現樣式隔離的?“
初見Scoped
我們初次見到scoped
應該是在Vue Loader中的Scoped Css
文檔中。
子組件的根元素
使用 scoped
後,父組件的樣式將不會滲透到子組件中。
深度作用選擇器
如果你希望 scoped
樣式中的一個選擇器能夠作用得“更深”,例如影響子組件,你可以使用 >>> 操作符:
<style scoped> .a >>> .b { /* ... */ } </style>上述代碼會編譯成:
.a[data-v-f3f3eg9] .b { /* ... */ }
註意:像Sass之類的預處理器無法正確解析>>>
。這種情況下可以使用/deep/
或::v-deep
操作符取而代之,兩者都是>>>
的別名,同樣可以正常工作。
實戰Scoped
style
標簽無
scoped標識
<style lang="less" > .demo { a { color: red; } } </style>
編譯之後
.demo a { color: red; }
style
表現中有
scoped標識
<style lang="less" scoped> .demo { a { color: red; } } </style>
編譯之後
.demo a[data-v-219e4e87] { color: red; }
父子組件中同時修改a
標簽樣式
// 子組件 <style scoped> a { color: green; } </style> // 父組件 <style lang="less" scoped> .demo { a { color: red; } } </style>
編譯完之後,父組件樣式對子組件樣式沒有影響
/* 子組件 a 標簽樣式 */ a[data-v-458323f2] { color: green; } /* 父組件 a 標簽樣式 */ .demo a[data-v-219e4e87] { color: red; }
如果想父組件對子組件的樣式產生影響,就需要使用更深級的選擇器 >>>
或 /deep/
或 ::v-deep
使父組件的樣式對子組件產生影響。
<style lang="less" scoped> .demo { /deep/ a { color: red; } } </style>
編譯完之後
a[data-v-458323f2] { color: green; } .demo[data-v-ca3944e4] a { color: red; }
我們可以看到 編譯後的 /deep/ a
被替換成了 a
標簽,實現了父組件對子組件樣式的修改。
解密Scoped實現
回顧初見Scoped
,我們是在vue-loader
的說明文檔中瞭解到的scoped
的用法,所以我們從vue-loader
包入手,發現compiler.ts
中:
try { // Vue 3.2.13+ ships the SFC compiler directly under the `vue` package // making it no longer necessary to have @vue/compiler-sfc separately installed. compiler = require('vue/compiler-sfc') } catch (e) { try { compiler = require('@vue/compiler-sfc') } catch (e) { } }可以看到compiler的引用在
@vue/compiler-sfc
包中,@vue/compiler-sfc
包的compileStyle.ts
文件中有一個doCompileStyle()
函數,然後我們大致看下這個函數的作用:export function doCompileStyle( options: SFCAsyncStyleCompileOptions ): SFCStyleCompileResults { // 只保留了部分主要流程代碼 const plugins = (postcssPlugins || []).slice() plugins.unshift(cssVarsPlugin({ id: id.replace(/^data-v-/, ''), isProd })) if (trim) { plugins.push(trimPlugin()) } if (scoped) { // 引入了scoped插件 plugins.push(scopedPlugin(id)) } try { // 調用postcss result = postcss(plugins).process(source, postCSSOptions) } catch (e) { } }
doCompileStyle()
主要做了一件事,就是按需引入postcss需要的插件,其中就有scoped
的插件。這個scoped
插件應該就是Scoped Css
的核心了。
我們看下scopedPlugin
插件都做了什麼
const scopedPlugin = () => { return { postcssPlugin: 'vue-sfc-scoped', Rule(rule) { processRule(id, rule) } } function processRule(id: string, rule: Rule) { /* import selectorParser from 'postcss-selector-parser' * 通過 postcss-selector-parser 獲取css AST */ rule.selector = selectorParser(selectorRoot => { selectorRoot.each(selector => { rewriteSelector(id, selector, selectorRoot) }) }).processSync(rule.selector) } function rewriteSelector( id: string, selector: selectorParser.Selector, selectorRoot: selectorParser.Root ) { let node: selectorParser.Node | null = null let shouldInject = true // find the last child node to insert attribute selector selector.each(n => { // DEPRECATED ">>>" and "/deep/" combinator if ( n.type === 'combinator' && (n.value === '>>>' || n.value === '/deep/') ) { n.value = ' ' n.spaces.before = n.spaces.after = '' // warn( // `the >>> and /deep/ combinators have been deprecated. ` + // `Use :deep() instead.` // ) // 可以結束本次迴圈 return false } if (n.type === 'pseudo') { const { value } = n // deep: inject [id] attribute at the node before the ::v-deep // combinator. if (value === ':deep' || value === '::v-deep') { if (n.nodes.length) { // .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar // replace the current node with ::v-deep's inner selector let last: selectorParser.Selector['nodes'][0] = n n.nodes[0].each(ss => { selector.insertAfter(last, ss) last = ss }) // insert a space combinator before if it doesn't already have one const prev = selector.at(selector.index(n) - 1) if (!prev || !isSpaceCombinator(prev)) { selector.insertAfter( n, selectorParser.combinator({ value: ' ' }) ) } selector.removeChild(n) } else { // DEPRECATED usage in v3 // .foo ::v-deep .bar -> .foo[xxxxxxx] .bar // warn( // `::v-deep usage as a combinator has ` + // `been deprecated. Use :deep(<inner-selector>) instead.` // ) const prev = selector.at(selector.index(n) - 1) if (prev && isSpaceCombinator(prev)) { selector.removeChild(prev) } selector.removeChild(n) } return false } } if (n.type !== 'pseudo' && n.type !== 'combinator') { node = n } }) if (node) { ;(node as selectorParser.Node).spaces.after = '' } else { // For deep selectors & standalone pseudo selectors, // the attribute selectors are prepended rather than appended. // So all leading spaces must be eliminated to avoid problems. selector.first.spaces.before = '' } if (shouldInject) { // 給seletor的node節點添加屬性 id selector.insertAfter( // If node is null it means we need to inject [id] at the start // insertAfter can handle `null` here node as any, selectorParser.attribute({ attribute: id, value: id, raws: {}, quoteMark: `"` }) ) } }
上述是保留了主要流程的插件代碼,至此,我們可以得出scoped
的實現方案就是通過postcss
插件這種形式實現。
大家如果沒有理解上述插件的原理,下麵我提供個簡單的插件代碼,方便大家在node平臺上運行理解。
簡易流程:
const postcss = require('postcss'); // 解析Css AST const selectorParser = require('postcss-selector-parser'); postcss([ { postcssPlugin: 'post-test-plugin', Rule(rule) { console.log(rule.selector, 'rule.selector'); rule.selector = selectorParser(selectorRoot => { selectorRoot.each(selector => { let node = null; selector.each(n => { if(n.type === 'combinator' && n.value === '/deep/') { n.value = ' '; return false; } if(n.type !=='pseudo' && n.type !=='combinator') { node= n; } }) selector.insertAfter( node, selectorParser.attribute({ attribute: '123456', }) ) }) }).processSync(rule.selector) console.log(rule.selector, 'after ruleSelector'); } } ]).process(`/deep/ a { color: red }; b:hover{ color: blue }`).then(res =>{ console.log(res.css); // [123456] a { color: red }; b[123456]:hover{ color: blue } });
關於Debug的一個小技巧
上述解密部分有的朋友可能會疑惑,怎麼就能剛好定位到這些文件呢?這裡給大家分享一個debug的小技巧,主要適用於vscode
編輯器。以本次scoped分析
為例:
通過源碼我們大概分析出可能
是compiler-sfc
包中的插件進行的scoped操作,那麼我們直接在猜測位置打下斷點如圖所示:
然後打開package.json
文件,在scripts
命令行上有調試按鈕,點擊調試選擇build命令:
然後自動開始執行npm run build
,定位到我們剛纔打的斷點那裡:
左側有調用堆棧和當前變數以及調試按鈕,然後就可以一步步進行調試啦。
至此,Vue的Scoped Css
對你來說應該不再陌生了吧,如果還是有疑惑,可以按照上述步驟自行調試解惑哦~