這篇文章主要介紹CSS Scoped的實(shí)現(xiàn)原理分析,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!
站在用戶的角度思考問(wèn)題,與客戶深入溝通,找到下冶網(wǎng)站設(shè)計(jì)與下冶網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個(gè)性化、用戶體驗(yàn)好的作品,建站類(lèi)型包括:成都網(wǎng)站設(shè)計(jì)、成都網(wǎng)站建設(shè)、外貿(mào)網(wǎng)站建設(shè)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、申請(qǐng)域名、網(wǎng)絡(luò)空間、企業(yè)郵箱。業(yè)務(wù)覆蓋下冶地區(qū)。CSS Scoped的實(shí)現(xiàn)原理
在Vue單文件組件中,我們只需要在style標(biāo)簽上加上scoped屬性,就可以實(shí)現(xiàn)標(biāo)簽內(nèi)的樣式在當(dāng)前模板輸出的HTML標(biāo)簽上生效,其實(shí)現(xiàn)原理如下
每個(gè)Vue文件都將對(duì)應(yīng)一個(gè)唯一的id,該id可以根據(jù)文件路徑名和內(nèi)容hash生成
編譯template標(biāo)簽時(shí)時(shí)為每個(gè)標(biāo)簽添加了當(dāng)前組件的id,如
會(huì)被編譯成編譯style標(biāo)簽時(shí),會(huì)根據(jù)當(dāng)前組件的id通過(guò)屬性選擇器和組合選擇器輸出樣式,如.demo{color: red;}會(huì)被編譯成.demo[data-v-27e4e96e]{color: red;}
了解了大致原理,可以想到css scoped應(yīng)該需要同時(shí)處理template和style的內(nèi)容,現(xiàn)在歸納需要探尋的問(wèn)題
渲染的HTML標(biāo)簽上的data-v-xxx屬性是如何生成的
CSS代碼中的添加的屬性選擇器是如何實(shí)現(xiàn)的
resourceQuery
在此之前,需要了解首一下webpack中Rules.resourceQuery的作用。在配置loader時(shí),大部分時(shí)候我們只需要通過(guò)test匹配文件類(lèi)型即可
{ test: /\.vue$/, loader: 'vue-loader' } // 當(dāng)引入vue后綴文件時(shí),將文件內(nèi)容傳輸給vue-loader進(jìn)行處理 import Foo from './source.vue'
resourceQuery提供了根據(jù)引入文件路徑參數(shù)的形式匹配路徑
{ resourceQuery: /shymean=true/, loader: path.resolve(__dirname, './test-loader.js') } // 當(dāng)引入文件路徑攜帶query參數(shù)匹配時(shí),也將加載該loader import './test.js?shymean=true' import Foo from './source.vue?shymean=true'
vue-loader中就是通過(guò)resourceQuery并拼接不同的query參數(shù),將各個(gè)標(biāo)簽分配給對(duì)應(yīng)的loader進(jìn)行處理。
loader.pitch
參考
pitching-loader官方文檔
webpack的pitching loader
webpack中l(wèi)oaders的執(zhí)行順序是從右到左執(zhí)行的,如loaders:[a, b, c],loader的執(zhí)行順序是c->b->a,且下一個(gè)loader接收到的是上一個(gè)loader的返回值,這個(gè)過(guò)程跟"事件冒泡"很像。
但是在某些場(chǎng)景下,我們可能希望在"捕獲"階段就執(zhí)行l(wèi)oader的一些方法,因此webpack提供了loader.pitch的接口。
一個(gè)文件被多個(gè)loader處理的真實(shí)執(zhí)行流程,如下所示
a.pitch -> b.pitch -> c.pitch -> request module -> c -> b -> a
loader和pitch的接口定義大概如下所示
// loader文件導(dǎo)出的真實(shí)接口,content是上一個(gè)loader或文件的原始內(nèi)容 module.exports = function loader(content){ // 可以訪問(wèn)到在pitch掛載到data上的數(shù)據(jù) console.log(this.data.value) // 100 } // remainingRequest表示剩余的請(qǐng)求,precedingRequest表示之前的請(qǐng)求 // data是一個(gè)上下文對(duì)象,在上面的loader方法中可以通過(guò)this.data訪問(wèn)到,因此可以在pitch階段提前掛載一些數(shù)據(jù) module.exports.pitch = function pitch(remainingRequest, precedingRequest, data) { data.value = 100 }}
正常情況下,一個(gè)loader在execution階段會(huì)返回經(jīng)過(guò)處理后的文件文本內(nèi)容。如果在pitch方法中直接返回了內(nèi)容,則webpack會(huì)視為后面的loader已經(jīng)執(zhí)行完畢(包括pitch和execution階段)。
在上面的例子中,如果b.pitch返回了result b,則不再執(zhí)行c,則是直接將result b傳給了a。
VueLoaderPlugin
接下來(lái)看看與vue-loader配套的插件:VueLoaderPlugin,該插件的作用是:
將在webpack.config定義過(guò)的其它規(guī)則復(fù)制并應(yīng)用到 .vue 文件里相應(yīng)語(yǔ)言的塊中。
其大致工作流程如下所示
獲取項(xiàng)目webpack配置的rules項(xiàng),然后復(fù)制rules,為攜帶了?vue&lang=xx...query參數(shù)的文件依賴(lài)配置xx后綴文件同樣的loader
為Vue文件配置一個(gè)公共的loader:pitcher
將[pitchLoder, ...clonedRules, ...rules]作為webapck新的rules
// vue-loader/lib/plugin.js const rawRules = compiler.options.module.rules // 原始的rules配置信息 const { rules } = new RuleSet(rawRules) // cloneRule會(huì)修改原始rule的resource和resourceQuery配置,攜帶特殊query的文件路徑將被應(yīng)用對(duì)應(yīng)rule const clonedRules = rules .filter(r => r !== vueRule) .map(cloneRule) // vue文件公共的loader const pitcher = { loader: require.resolve('./loaders/pitcher'), resourceQuery: query => { const parsed = qs.parse(query.slice(1)) return parsed.vue != null }, options: { cacheDirectory: vueLoaderUse.options.cacheDirectory, cacheIdentifier: vueLoaderUse.options.cacheIdentifier } } // 更新webpack的rules配置,這樣vue單文件中的各個(gè)標(biāo)簽可以應(yīng)用clonedRules相關(guān)的配置 compiler.options.module.rules = [ pitcher, ...clonedRules, ...rules ]
因此,為vue單文件組件中每個(gè)標(biāo)簽執(zhí)行的lang屬性,也可以應(yīng)用在webpack配置同樣后綴的rule。這種設(shè)計(jì)就可以保證在不侵入vue-loader的情況下,為每個(gè)標(biāo)簽配置獨(dú)立的loader,如
可以使用pug編寫(xiě)template,然后配置pug-plain-loader
可以使用scss或less編寫(xiě)style,然后配置相關(guān)預(yù)處理器loader
可見(jiàn)在VueLoaderPlugin主要做的兩件事,一個(gè)是注冊(cè)公共的pitcher,一個(gè)是復(fù)制webpack的rules。
vue-loader
接下來(lái)我們看看vue-loader做的事情。
pitcher
前面提到在VueLoaderPlugin中,該loader在pitch中會(huì)根據(jù)query.type注入處理對(duì)應(yīng)標(biāo)簽的loader
當(dāng)type為style時(shí),在css-loader后插入stylePostLoader,保證stylePostLoader在execution階段先執(zhí)行
當(dāng)type為template時(shí),插入templateLoader
// pitcher.js module.exports = code => code module.exports.pitch = function (remainingRequest) { if (query.type === `style`) { // 會(huì)查詢cssLoaderIndex并將其放在afterLoaders中 // loader在execution階段是從后向前執(zhí)行的 const request = genRequest([ ...afterLoaders, stylePostLoaderPath, // 執(zhí)行l(wèi)ib/loaders/stylePostLoader.js ...beforeLoaders ]) return `import mod from ${request}; export default mod; export * from ${request}` } // 處理模板 if (query.type === `template`) { const preLoaders = loaders.filter(isPreLoader) const postLoaders = loaders.filter(isPostLoader) const request = genRequest([ ...cacheLoader, ...postLoaders, templateLoaderPath + `??vue-loader-options`, // 執(zhí)行l(wèi)ib/loaders/templateLoader.js ...preLoaders ]) return `export * from ${request}` } // ... }
由于loader.pitch會(huì)先于loader,在捕獲階段執(zhí)行,因此主要進(jìn)行上面的準(zhǔn)備工作:檢查query.type并直接調(diào)用相關(guān)的loader
type=style,執(zhí)行stylePostLoader
type=template,執(zhí)行templateLoader
這兩個(gè)loader的具體作用我們后面再研究。
vueLoader
接下來(lái)看看vue-loader里面做的工作,當(dāng)引入一個(gè)x.vue文件時(shí)
// vue-loader/lib/index.js 下面source為Vue代碼文件原始內(nèi)容 // 將單個(gè)*.vue文件內(nèi)容解析成一個(gè)descriptor對(duì)象,也稱(chēng)為SFC(Single-File Components)對(duì)象 // descriptor包含template、script、style等標(biāo)簽的屬性和內(nèi)容,方便為每種標(biāo)簽做對(duì)應(yīng)處理 const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(loaderContext), filename, sourceRoot, needMap: sourceMap }) // 為單文件組件生成唯一哈希id const id = hash( isProduction ? (shortFilePath + '\n' + source) : shortFilePath ) // 如果某個(gè)style標(biāo)簽包含scoped屬性,則需要進(jìn)行CSS Scoped處理,這也是本章節(jié)需要研究的地方 const hasScoped = descriptor.styles.some(s => s.scoped)
處理template標(biāo)簽,拼接type=template等query參數(shù)
if (descriptor.template) { const src = descriptor.template.src || resourcePath const idQuery = `&id=${id}` // 傳入文件id和scoped=true,在為組件的每個(gè)HTML標(biāo)簽傳入組件id時(shí)需要這兩個(gè)參數(shù) const scopedQuery = hasScoped ? `&scoped=true` : `` const attrsQuery = attrsToQuery(descriptor.template.attrs) const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}` const request = templateRequest = stringifyRequest(src + query) // type=template的文件會(huì)傳給templateLoader處理 templateImport = `import { render, staticRenderFns } from ${request}` // 比如,標(biāo)簽 // 將被解析成 import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&lang=pug&" }
處理script標(biāo)簽
let scriptImport = `var script = {}` if (descriptor.script) { // vue-loader沒(méi)有對(duì)script做過(guò)多的處理 // 比如vue文件中的標(biāo)簽將被解析成 // import script from "./source.vue?vue&type=script&lang=js&" // export * from "./source.vue?vue&type=script&lang=js&" }
處理style標(biāo)簽,為每個(gè)標(biāo)簽拼接type=style等參數(shù)
// 在genStylesCode中,會(huì)處理css scoped和css moudle stylesCode = genStylesCode( loaderContext, descriptor.styles, id, resourcePath, stringifyRequest, needsHotReload, isServer || isShadow // needs explicit injection? ) // 由于一個(gè)vue文件里面可能存在多個(gè)style標(biāo)簽,對(duì)于每個(gè)標(biāo)簽,將調(diào)用genStyleRequest生成對(duì)應(yīng)文件的依賴(lài) function genStyleRequest (style, i) { const src = style.src || resourcePath const attrsQuery = attrsToQuery(style.attrs, 'css') const inheritQuery = `&${loaderContext.resourceQuery.slice(1)}` const idQuery = style.scoped ? `&id=${id}` : `` // type=style將傳給stylePostLoader進(jìn)行處理 const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${inheritQuery}` return stringifyRequest(src + query) }
可見(jiàn)在vue-loader中,主要是將整個(gè)文件按照標(biāo)簽拼接對(duì)應(yīng)的query路徑,然后交給webpack按順序調(diào)用相關(guān)的loader。
templateLoader
回到開(kāi)頭提到的第一個(gè)問(wèn)題:當(dāng)前組件中,渲染出來(lái)的每個(gè)HTML標(biāo)簽中的hash屬性是如何生成的。
我們知道,一個(gè)組件的render方法返回的VNode,描述了組件對(duì)應(yīng)的HTML標(biāo)簽和結(jié)構(gòu),HTML標(biāo)簽對(duì)應(yīng)的DOM節(jié)點(diǎn)是從虛擬DOM節(jié)點(diǎn)構(gòu)建的,一個(gè)Vnode包含了渲染DOM節(jié)點(diǎn)需要的基本屬性。
那么,我們只需要了解到vnode上組件文件的哈希id的賦值過(guò)程,后面的問(wèn)題就迎刃而解了。
// templateLoader.js const { compileTemplate } = require('@vue/component-compiler-utils') module.exports = function (source) { const { id } = query const options = loaderUtils.getOptions(loaderContext) || {} const compiler = options.compiler || require('vue-template-compiler') // 可以看見(jiàn),scopre=true的template的文件會(huì)生成一個(gè)scopeId const compilerOptions = Object.assign({ outputSourceRange: true }, options.compilerOptions, { scopeId: query.scoped ? `data-v-${id}` : null, comments: query.comments }) // 合并compileTemplate最終參數(shù),傳入compilerOptions和compiler const finalOptions = {source, filename: this.resourcePath, compiler,compilerOptions} const compiled = compileTemplate(finalOptions) const { code } = compiled // finish with ESM exports return code + `\nexport { render, staticRenderFns }` }
關(guān)于compileTemplate的實(shí)現(xiàn),我們不用去關(guān)心其細(xì)節(jié),其內(nèi)部主要是調(diào)用了配置參數(shù)compiler的編譯方法
function actuallyCompile(options) { const compile = optimizeSSR && compiler.ssrCompile ? compiler.ssrCompile : compiler.compile const { render, staticRenderFns, tips, errors } = compile(source, finalCompilerOptions); // ... }
在Vue源碼中可以了解到,template屬性會(huì)通過(guò)compileToFunctions編譯成render方法;在vue-loader中,這一步是可以通過(guò)vue-template-compiler提前在打包階段處理的。
vue-template-compiler是隨著Vue源碼一起發(fā)布的一個(gè)包,當(dāng)二者同時(shí)使用時(shí),需要保證他們的版本號(hào)一致,否則會(huì)提示錯(cuò)誤。這樣,compiler.compile實(shí)際上是Vue源碼中vue/src/compiler/index.js的baseCompile方法,追著源碼一致翻下去,可以發(fā)現(xiàn)
// elementToOpenTagSegments.js // 對(duì)于單個(gè)標(biāo)簽的屬性,將拆分成一個(gè)segments function elementToOpenTagSegments (el, state): Array{ applyModelTransform(el, state) let binding const segments = [{ type: RAW, value: `<${el.tag}` }] // ... 處理attrs、domProps、v-bind、style、等屬性 // _scopedId if (state.options.scopeId) { segments.push({ type: RAW, value: ` ${state.options.scopeId}` }) } segments.push({ type: RAW, value: `>` }) return segments }
以前面的
為例,解析得到的segments為[ { type: RAW, value: '' }, ]至此,我們知道了在templateLoader中,會(huì)根據(jù)單文件組件的id,拼接一個(gè)scopeId,并作為compilerOptions傳入編譯器中,被解析成vnode的配置屬性,然后在render函數(shù)執(zhí)行時(shí)調(diào)用createElement,作為vnode的原始屬性,渲染成到DOM節(jié)點(diǎn)上。
stylePostLoader
在stylePostLoader中,需要做的工作就是將所有選擇器都增加一個(gè)屬性選擇器的組合限制,
const { compileStyle } = require('@vue/component-compiler-utils') module.exports = function (source, inMap) { const query = qs.parse(this.resourceQuery.slice(1)) const { code, map, errors } = compileStyle({ source, filename: this.resourcePath, id: `data-v-${query.id}`, // 同一個(gè)單頁(yè)面組件中的style,與templateLoader中的scopeId保持一致 map: inMap, scoped: !!query.scoped, trim: true }) this.callback(null, code, map) }我們需要了解compileStyle的邏輯
// @vue/component-compiler-utils/compileStyle.ts import scopedPlugin from './stylePlugins/scoped' function doCompileStyle(options) { const { filename, id, scoped = true, trim = true, preprocessLang, postcssOptions, postcssPlugins } = options; if (scoped) { plugins.push(scopedPlugin(id)); } const postCSSOptions = Object.assign({}, postcssOptions, { to: filename, from: filename }); // 省略了相關(guān)判斷 let result = postcss(plugins).process(source, postCSSOptions); }最后讓我們?cè)诹私庖幌聅copedPlugin的實(shí)現(xiàn),
export default postcss.plugin('add-id', (options: any) => (root: Root) => { const id: string = options const keyframes = Object.create(null) root.each(function rewriteSelector(node: any) { node.selector = selectorParser((selectors: any) => { selectors.each((selector: any) => { let node: any = null // 處理 '>>>' 、 '/deep/'、::v-deep、pseudo等特殊選擇器時(shí),將不會(huì)執(zhí)行下面添加屬性選擇器的邏輯 // 為當(dāng)前選擇器添加一個(gè)屬性選擇器[id],id即為傳入的scopeId selector.insertAfter( node, selectorParser.attribute({ attribute: id }) ) }) }).processSync(node.selector) }) })由于我對(duì)于PostCSS的插件開(kāi)發(fā)并不是很熟悉,這里只能大致整理,翻翻文檔了,相關(guān)API可以參考Writing a PostCSS Plugin。
至此,我們就知道了第二個(gè)問(wèn)題的答案:通過(guò)selector.insertAfter為當(dāng)前styles下的每一個(gè)選擇器添加了屬性選擇器,其值即為傳入的scopeId。由于只有當(dāng)前組件渲染的DOM節(jié)點(diǎn)上上面存在相同的屬性,從而就實(shí)現(xiàn)了css scoped的效果。
小結(jié)
回過(guò)頭來(lái)整理一下vue-loader的工作流程
首先需要在webpack配置中注冊(cè)VueLoaderPlugin
在插件中,會(huì)復(fù)制當(dāng)前項(xiàng)目webpack配置中的rules項(xiàng),當(dāng)資源路徑包含query.lang時(shí)通過(guò)resourceQuery匹配相同的rules并執(zhí)行對(duì)應(yīng)loader時(shí)
插入一個(gè)公共的loader,并在pitch階段根據(jù)query.type插入對(duì)應(yīng)的自定義loader
準(zhǔn)備工作完成后,當(dāng)加載*.vue時(shí)會(huì)調(diào)用vue-loader,
一個(gè)單頁(yè)面組件文件會(huì)被解析成一個(gè)descriptor對(duì)象,包含template、script、styles等屬性對(duì)應(yīng)各個(gè)標(biāo)簽,
對(duì)于每個(gè)標(biāo)簽,會(huì)根據(jù)標(biāo)簽屬性拼接src?vue&query引用代碼,其中src為單頁(yè)面組件路徑,query為一些特性的參數(shù),比較重要的有l(wèi)ang、type和scoped
如果包含lang屬性,會(huì)匹配與該后綴相同的rules并應(yīng)用對(duì)應(yīng)的loaders
根據(jù)type執(zhí)行對(duì)應(yīng)的自定義loader,template將執(zhí)行templateLoader、style將執(zhí)行stylePostLoader
在templateLoader中,會(huì)通過(guò)vue-template-compiler將template轉(zhuǎn)換為render函數(shù),在此過(guò)程中,
會(huì)將傳入的scopeId追加到每個(gè)標(biāo)簽的segments上,最后作為vnode的配置屬性傳遞給createElemenet方法,
在render函數(shù)調(diào)用并渲染頁(yè)面時(shí),會(huì)將scopeId屬性作為原始屬性渲染到頁(yè)面上
在stylePostLoader中,通過(guò)PostCSS解析style標(biāo)簽內(nèi)容,同時(shí)通過(guò)scopedPlugin為每個(gè)選擇器追加一個(gè)[scopeId]的屬性選擇器
由于需要Vue源碼方面的支持(vue-template-compiler編譯器),CSS Scoped可以算作為Vue定制的一個(gè)處理原生CSS全局作用域的解決方案。除了 css scoped之外,vue還支持css module,我打算在下一篇整理React中編寫(xiě)CSS的博客中一并對(duì)比整理。
小結(jié)
最近一直在寫(xiě)React的項(xiàng)目,嘗試了好幾種在React中編寫(xiě)CSS的方式,包括CSS Module、Style Component等方式,感覺(jué)都比較繁瑣。相比而言,在Vue中單頁(yè)面組件中寫(xiě)CSS要方便很多。
本文主要從源碼層面分析了Vue-loader,整理了其工作原理,感覺(jué)收獲頗豐
webpack中Rules.resourceQuery和pitch loader的使用
Vue單頁(yè)面文件中css scoped的實(shí)現(xiàn)原理
PostCSS插件的作用
雖然一直在使用webpack和PostCSS,但也僅限于勉強(qiáng)會(huì)用的階段,比如我甚至從來(lái)沒(méi)有過(guò)編寫(xiě)一個(gè)PostCSS插件的想法。盡管目前大部分項(xiàng)目都使用了封裝好的腳手架,但對(duì)于這些基礎(chǔ)知識(shí),還是很有必要去了解其實(shí)現(xiàn)的。
以上是“CSS Scoped的實(shí)現(xiàn)原理分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對(duì)大家有幫助,更多相關(guān)知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)成都網(wǎng)站設(shè)計(jì)公司行業(yè)資訊頻道!
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無(wú)理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國(guó)服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡(jiǎn)單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢(shì),專(zhuān)為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場(chǎng)景需求。
分享名稱(chēng):CSSScoped的實(shí)現(xiàn)原理分析-創(chuàng)新互聯(lián)
文章位置:http://weahome.cn/article/gjped.html