您好,登錄后才能下訂單哦!
這篇“CSS中Scoped的實現(xiàn)原理是什么”文章的知識點大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“CSS中Scoped的實現(xiàn)原理是什么”文章吧。
CSS Scoped的實現(xiàn)原理
在Vue單文件組件中,我們只需要在style標簽上加上scoped屬性,就可以實現(xiàn)標簽內(nèi)的樣式在當(dāng)前模板輸出的HTML標簽上生效,其實現(xiàn)原理如下
每個Vue文件都將對應(yīng)一個唯一的id,該id可以根據(jù)文件路徑名和內(nèi)容hash生成
編譯template標簽時時為每個標簽添加了當(dāng)前組件的id,如<div></div>會被編譯成<div data-v-27e4e96e></div>
編譯style標簽時,會根據(jù)當(dāng)前組件的id通過屬性選擇器和組合選擇器輸出樣式,如.demo{color: red;}會被編譯成.demo[data-v-27e4e96e]{color: red;}
了解了大致原理,可以想到css scoped應(yīng)該需要同時處理template和style的內(nèi)容,現(xiàn)在歸納需要探尋的問題
渲染的HTML標簽上的data-v-xxx屬性是如何生成的
CSS代碼中的添加的屬性選擇器是如何實現(xiàn)的
resourceQuery
在此之前,需要了解首一下webpack中Rules.resourceQuery的作用。在配置loader時,大部分時候我們只需要通過test匹配文件類型即可
{ test: /\.vue$/, loader: 'vue-loader' } // 當(dāng)引入vue后綴文件時,將文件內(nèi)容傳輸給vue-loader進行處理 import Foo from './source.vue'
resourceQuery提供了根據(jù)引入文件路徑參數(shù)的形式匹配路徑
{ resourceQuery: /shymean=true/, loader: path.resolve(__dirname, './test-loader.js') } // 當(dāng)引入文件路徑攜帶query參數(shù)匹配時,也將加載該loader import './test.js?shymean=true' import Foo from './source.vue?shymean=true'
vue-loader中就是通過resourceQuery并拼接不同的query參數(shù),將各個標簽分配給對應(yīng)的loader進行處理。
loader.pitch
參考
pitching-loader官方文檔
webpack的pitching loader
webpack中l(wèi)oaders的執(zhí)行順序是從右到左執(zhí)行的,如loaders:[a, b, c],loader的執(zhí)行順序是c->b->a,且下一個loader接收到的是上一個loader的返回值,這個過程跟"事件冒泡"很像。
但是在某些場景下,我們可能希望在"捕獲"階段就執(zhí)行l(wèi)oader的一些方法,因此webpack提供了loader.pitch的接口。
一個文件被多個loader處理的真實執(zhí)行流程,如下所示
a.pitch -> b.pitch -> c.pitch -> request module -> c -> b -> a
loader和pitch的接口定義大概如下所示
// loader文件導(dǎo)出的真實接口,content是上一個loader或文件的原始內(nèi)容 module.exports = function loader(content){ // 可以訪問到在pitch掛載到data上的數(shù)據(jù) console.log(this.data.value) // 100 } // remainingRequest表示剩余的請求,precedingRequest表示之前的請求 // data是一個上下文對象,在上面的loader方法中可以通過this.data訪問到,因此可以在pitch階段提前掛載一些數(shù)據(jù) module.exports.pitch = function pitch(remainingRequest, precedingRequest, data) { data.value = 100 }}
正常情況下,一個loader在execution階段會返回經(jīng)過處理后的文件文本內(nèi)容。如果在pitch方法中直接返回了內(nèi)容,則webpack會視為后面的loader已經(jīng)執(zhí)行完畢(包括pitch和execution階段)。
在上面的例子中,如果b.pitch返回了result b,則不再執(zhí)行c,則是直接將result b傳給了a。
VueLoaderPlugin
接下來看看與vue-loader配套的插件:VueLoaderPlugin,該插件的作用是:
將在webpack.config定義過的其它規(guī)則復(fù)制并應(yīng)用到 .vue 文件里相應(yīng)語言的塊中。
其大致工作流程如下所示
獲取項目webpack配置的rules項,然后復(fù)制rules,為攜帶了?vue&lang=xx...query參數(shù)的文件依賴配置xx后綴文件同樣的loader
為Vue文件配置一個公共的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會修改原始rule的resource和resourceQuery配置,攜帶特殊query的文件路徑將被應(yīng)用對應(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單文件中的各個標簽可以應(yīng)用clonedRules相關(guān)的配置 compiler.options.module.rules = [ pitcher, ...clonedRules, ...rules ]
因此,為vue單文件組件中每個標簽執(zhí)行的lang屬性,也可以應(yīng)用在webpack配置同樣后綴的rule。這種設(shè)計就可以保證在不侵入vue-loader的情況下,為每個標簽配置獨立的loader,如
可以使用pug編寫template,然后配置pug-plain-loader
可以使用scss或less編寫style,然后配置相關(guān)預(yù)處理器loader
可見在VueLoaderPlugin主要做的兩件事,一個是注冊公共的pitcher,一個是復(fù)制webpack的rules。
vue-loader
接下來我們看看vue-loader做的事情。
pitcher
前面提到在VueLoaderPlugin中,該loader在pitch中會根據(jù)query.type注入處理對應(yīng)標簽的loader
當(dāng)type為style時,在css-loader后插入stylePostLoader,保證stylePostLoader在execution階段先執(zhí)行
當(dāng)type為template時,插入templateLoader
// pitcher.js module.exports = code => code module.exports.pitch = function (remainingRequest) { if (query.type === `style`) { // 會查詢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會先于loader,在捕獲階段執(zhí)行,因此主要進行上面的準備工作:檢查query.type并直接調(diào)用相關(guān)的loader
type=style,執(zhí)行stylePostLoader
type=template,執(zhí)行templateLoader
這兩個loader的具體作用我們后面再研究。
vueLoader
接下來看看vue-loader里面做的工作,當(dāng)引入一個x.vue文件時
// vue-loader/lib/index.js 下面source為Vue代碼文件原始內(nèi)容 // 將單個*.vue文件內(nèi)容解析成一個descriptor對象,也稱為SFC(Single-File Components)對象 // descriptor包含template、script、style等標簽的屬性和內(nèi)容,方便為每種標簽做對應(yīng)處理 const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(loaderContext), filename, sourceRoot, needMap: sourceMap }) // 為單文件組件生成唯一哈希id const id = hash( isProduction ? (shortFilePath + '\n' + source) : shortFilePath ) // 如果某個style標簽包含scoped屬性,則需要進行CSS Scoped處理,這也是本章節(jié)需要研究的地方 const hasScoped = descriptor.styles.some(s => s.scoped)
處理template標簽,拼接type=template等query參數(shù)
if (descriptor.template) { const src = descriptor.template.src || resourcePath const idQuery = `&id=${id}` // 傳入文件id和scoped=true,在為組件的每個HTML標簽傳入組件id時需要這兩個參數(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的文件會傳給templateLoader處理 templateImport = `import { render, staticRenderFns } from ${request}` // 比如,<template lang="pug"></template>標簽 // 將被解析成 import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&lang=pug&" }
處理script標簽
let scriptImport = `var script = {}` if (descriptor.script) { // vue-loader沒有對script做過多的處理 // 比如vue文件中的<script></script>標簽將被解析成 // import script from "./source.vue?vue&type=script&lang=js&" // export * from "./source.vue?vue&type=script&lang=js&" }
處理style標簽,為每個標簽拼接type=style等參數(shù)
// 在genStylesCode中,會處理css scoped和css moudle stylesCode = genStylesCode( loaderContext, descriptor.styles, id, resourcePath, stringifyRequest, needsHotReload, isServer || isShadow // needs explicit injection? ) // 由于一個vue文件里面可能存在多個style標簽,對于每個標簽,將調(diào)用genStyleRequest生成對應(yīng)文件的依賴 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進行處理 const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${inheritQuery}` return stringifyRequest(src + query) }
可見在vue-loader中,主要是將整個文件按照標簽拼接對應(yīng)的query路徑,然后交給webpack按順序調(diào)用相關(guān)的loader。
templateLoader
回到開頭提到的第一個問題:當(dāng)前組件中,渲染出來的每個HTML標簽中的hash屬性是如何生成的。
我們知道,一個組件的render方法返回的VNode,描述了組件對應(yīng)的HTML標簽和結(jié)構(gòu),HTML標簽對應(yīng)的DOM節(jié)點是從虛擬DOM節(jié)點構(gòu)建的,一個Vnode包含了渲染DOM節(jié)點需要的基本屬性。
那么,我們只需要了解到vnode上組件文件的哈希id的賦值過程,后面的問題就迎刃而解了。
// 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') // 可以看見,scopre=true的template的文件會生成一個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的實現(xiàn),我們不用去關(guān)心其細節(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屬性會通過compileToFunctions編譯成render方法;在vue-loader中,這一步是可以通過vue-template-compiler提前在打包階段處理的。
vue-template-compiler是隨著Vue源碼一起發(fā)布的一個包,當(dāng)二者同時使用時,需要保證他們的版本號一致,否則會提示錯誤。這樣,compiler.compile實際上是Vue源碼中vue/src/compiler/index.js的baseCompile方法,追著源碼一致翻下去,可以發(fā)現(xiàn)
// elementToOpenTagSegments.js // 對于單個標簽的屬性,將拆分成一個segments function elementToOpenTagSegments (el, state): Array<StringSegment> { 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 }
以前面的<div></div>為例,解析得到的segments為
[ { type: RAW, value: '<div' }, { type: RAW, value: 'class=demo' }, { type: RAW, value: 'data-v-27e4e96e' }, // 傳入的scopeId { type: RAW, value: '>' }, ]
至此,我們知道了在templateLoader中,會根據(jù)單文件組件的id,拼接一個scopeId,并作為compilerOptions傳入編譯器中,被解析成vnode的配置屬性,然后在render函數(shù)執(zhí)行時調(diào)用createElement,作為vnode的原始屬性,渲染成到DOM節(jié)點上。
stylePostLoader
在stylePostLoader中,需要做的工作就是將所有選擇器都增加一個屬性選擇器的組合限制,
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}`, // 同一個單頁面組件中的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); }
最后讓我們在了解一下scopedPlugin的實現(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等特殊選擇器時,將不會執(zhí)行下面添加屬性選擇器的邏輯 // 為當(dāng)前選擇器添加一個屬性選擇器[id],id即為傳入的scopeId selector.insertAfter( node, selectorParser.attribute({ attribute: id }) ) }) }).processSync(node.selector) }) })
由于我對于PostCSS的插件開發(fā)并不是很熟悉,這里只能大致整理,翻翻文檔了,相關(guān)API可以參考Writing a PostCSS Plugin。
至此,我們就知道了第二個問題的答案:通過selector.insertAfter為當(dāng)前styles下的每一個選擇器添加了屬性選擇器,其值即為傳入的scopeId。由于只有當(dāng)前組件渲染的DOM節(jié)點上上面存在相同的屬性,從而就實現(xiàn)了css scoped的效果。
以上就是關(guān)于“CSS中Scoped的實現(xiàn)原理是什么”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關(guān)的知識內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。