真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

CSSScoped的實(shí)現(xiàn)原理分析-創(chuàng)新互聯(lián)

這篇文章主要介紹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,如

  1. 可以使用pug編寫(xiě)template,然后配置pug-plain-loader

  2. 可以使用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

  1. 在插件中,會(huì)復(fù)制當(dāng)前項(xiàng)目webpack配置中的rules項(xiàng),當(dāng)資源路徑包含query.lang時(shí)通過(guò)resourceQuery匹配相同的rules并執(zhí)行對(duì)應(yīng)loader時(shí)

  2. 插入一個(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é)收獲頗豐

  1. webpack中Rules.resourceQuery和pitch loader的使用

  2. Vue單頁(yè)面文件中css scoped的實(shí)現(xiàn)原理

  3. 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

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部