Vue中CSS?scoped的原理詳細(xì)講解
前言
在日常的Vue項目開發(fā)過程中,為了讓項目更好的維護(hù)一般都會使用模塊化開發(fā)的方式進(jìn)行。也就是每個組件維護(hù)獨立的template,script,style。主要介紹一下使用<style scoped>為什么在頁面渲染完后樣式之間并不會造成污染。
示例
搭建一個簡單的Vue項目測試一下:

終端執(zhí)行npx webpack輸出dist目錄,在瀏覽器打開index.html調(diào)試一下看看現(xiàn)象:

- 每個組件都會擁有一個[data-v-hash:8]插入HTML標(biāo)簽,子組件標(biāo)簽上也具體父組件[data-v-hash:8];
- 如果style標(biāo)簽加了scoped屬性,里面的選擇器都會變成(Attribute Selector) [data-v-hash:8];
- 如果子組件選擇器跟父組件選擇器完全一樣,那么就會出現(xiàn)子組件樣式被父組件覆蓋,因為子組件會優(yōu)先于父組件mounted,有興趣可以測試一下哦。
webpack.config.js配置
先看看在webpack.config.js中的配置:

vue-loader工作流
以下就是vue-loader工作大致的處理流程:

開啟node調(diào)試模式進(jìn)行查看閱讀,package.json中配置如下:
"scripts": {
"debug": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
},VueLoaderPlugin
先從入口文件lib/index.js開始分析,因為我Webpack是4.x版本,所以VueLoaderPlugin = require('./plugin-webpack4'),重點來看看這個lib/plugin-webpack4.js文件:
const qs = require('querystring')
const RuleSet = require('webpack/lib/RuleSet')
const id = 'vue-loader-plugin'
const NS = 'vue-loader'
// 很明顯這就是一個webpack插件寫法
class VueLoaderPlugin {
apply (compiler) {
if (compiler.hooks) {
// 編譯創(chuàng)建之后,執(zhí)行插件
compiler.hooks.compilation.tap(id, compilation => {
const normalModuleLoader = compilation.hooks.normalModuleLoader
normalModuleLoader.tap(id, loaderContext => {
loaderContext[NS] = true
})
})
} else {
// webpack < 4
compiler.plugin('compilation', compilation => {
compilation.plugin('normal-module-loader', loaderContext => {
loaderContext[NS] = true
})
})
}
// webpack.config.js 中配置好的 module.rules
const rawRules = compiler.options.module.rules
// 對 rawRules 做 normlized
const { rules } = new RuleSet(rawRules)
// 從 rawRules 中檢查是否有規(guī)則去匹配 .vue 或 .vue.html
let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))
if (vueRuleIndex < 0) {
vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`))
}
const vueRule = rules[vueRuleIndex]
if (!vueRule) {
throw new Error(
`[VueLoaderPlugin Error] No matching rule for .vue files found.\n` +
`Make sure there is at least one root-level rule that matches .vue or .vue.html files.`
)
}
if (vueRule.oneOf) {
throw new Error(
`[VueLoaderPlugin Error] vue-loader 15 currently does not support vue rules with oneOf.`
)
}
// 檢查 normlized rawRules 中 .vue 規(guī)則中是否具有 vue-loader
const vueUse = vueRule.use
const vueLoaderUseIndex = vueUse.findIndex(u => {
return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader)
})
if (vueLoaderUseIndex < 0) {
throw new Error(
`[VueLoaderPlugin Error] No matching use for vue-loader is found.\n` +
`Make sure the rule matching .vue files include vue-loader in its use.`
)
}
// make sure vue-loader options has a known ident so that we can share
// options by reference in the template-loader by using a ref query like
// template-loader??vue-loader-options
const vueLoaderUse = vueUse[vueLoaderUseIndex]
vueLoaderUse.ident = 'vue-loader-options'
vueLoaderUse.options = vueLoaderUse.options || {}
// 過濾出 .vue 規(guī)則,其他規(guī)則調(diào)用 cloneRule 方法重寫了 resource 和 resourceQuery 配置
// 用于編譯vue文件后匹配依賴路徑 query 中需要的loader
const clonedRules = rules
.filter(r => r !== vueRule)
.map(cloneRule)
// 加入全局 pitcher-loader,路徑query有vue字段就給loader添加pitch方法
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
}
}
// 修改原始的 module.rules 配置
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
}
}以上大概就是VueLoaderPlugin所做的事情。也就是說VueLoaderPlugin主要就是修改module.rules的配置。總的來說就是對vue單文件編寫做的一個擴展(比如可以寫less文件,在vue style中也可以寫less)
vue-loader
vue-loader是如何操作.vue文件的,目前只關(guān)心style部分,邏輯在lib/index.js:
vue文件解析
// 很明顯這就是一個loader寫法
module.exports = function (source) {
const loaderContext = this
// ...
const {
target,
request, // 請求資源路徑
minimize,
sourceMap,
rootContext, // 根路徑
resourcePath, // vue文件的路徑
resourceQuery // vue文件的路徑 query 參數(shù)
} = loaderContext
// ...
// 解析 vue 文件,descriptor 是AST抽象語法樹的描述
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
/**
*
*/
// hash(文件路徑 + 開發(fā)環(huán)境 ?文件內(nèi)容 : "")生成 id
const id = hash(
isProduction
? (shortFilePath + '\n' + source)
: shortFilePath
)
// descriptor.styles 解析后是否具有 attrs: {scoped: true}
const hasScoped = descriptor.styles.some(s => s.scoped)
/**
*
*/
let stylesCode = ``
if (descriptor.styles.length) {
// 最終生成一個import依賴請求
stylesCode = genStylesCode(
loaderContext,
descriptor.styles,
id,
resourcePath,
stringifyRequest,
needsHotReload,
isServer || isShadow // needs explicit injection?
)
}
}可以看到解析完vue文件的結(jié)果大概就是這樣的:

依賴解析
vue文件解析完之后template,script,style等都有個依賴的路徑,后續(xù)可以通過配置的loader進(jìn)行解析了,因為我們已經(jīng)在VuePluginLoader中修改了module.rules的配置,而且依賴的路徑中query中都擁有vue字段,所以會先走到pitcher-loader,現(xiàn)在來分析lib/loaders/pitcher.js中的邏輯:
/**
*
*/
module.exports = code => code
module.exports.pitch = function (remainingRequest) {
const options = loaderUtils.getOptions(this)
const { cacheDirectory, cacheIdentifier } = options
const query = qs.parse(this.resourceQuery.slice(1))
let loaders = this.loaders
if (query.type) {
if (/\.vue$/.test(this.resourcePath)) {
// 過濾eslint-loader
loaders = loaders.filter(l => !isESLintLoader(l))
} else {
loaders = dedupeESLintLoader(loaders)
}
}
// 過濾pitcher-loader
loaders = loaders.filter(isPitcher)
const genRequest = loaders => {
const seen = new Map()
const loaderStrings = []
loaders.forEach(loader => {
const identifier = typeof loader === 'string'
? loader
: (loader.path + loader.query)
const request = typeof loader === 'string' ? loader : loader.request
if (!seen.has(identifier)) {
seen.set(identifier, true)
// loader.request contains both the resolved loader path and its options
// query (e.g. ??ref-0)
loaderStrings.push(request)
}
})
return loaderUtils.stringifyRequest(this, '-!' + [
...loaderStrings,
this.resourcePath + this.resourceQuery
].join('!'))
}
if (query.type === `style`) {
const cssLoaderIndex = loaders.findIndex(isCSSLoader)
// 調(diào)整loader執(zhí)行順序
if (cssLoaderIndex > -1) {
const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
const request = genRequest([
...afterLoaders, // [style-loader,css-loader]
stylePostLoaderPath, // style-post-loader
...beforeLoaders // [vue-loader]
])
return `import mod from ${request}; export default mod; export * from ${request}`
}
}
/**
*
*/
const request = genRequest(loaders)
return `import mod from ${request}; export default mod; export * from ${request}`
}可以看到解析帶scoped屬性的style的結(jié)果大概就是這樣的:

新的依賴解析
分析{tyep:style}的處理流程順序:
- vue-loader、style-post-loader、css-loader、style-loader。
處理資源的時候先走的是vue-loader,這時vue-loader中的處理邏輯與第一次解析vue文件不一樣了:
const incomingQuery = qs.parse(rawQuery)
// 擁有{type:style}
if (incomingQuery.type) {
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
// lib/select.js
module.exports = function selectBlock (
descriptor,
loaderContext,
query,
appendExtension
) {
// ...
if (query.type === `style` && query.index != null) {
const style = descriptor.styles[query.index]
if (appendExtension) {
loaderContext.resourcePath += '.' + (style.lang || 'css')
}
loaderContext.callback(
null,
style.content,
style.map
)
return
}可以看到vue-loader處理完后返回的就是style.content,也就是style標(biāo)簽下的內(nèi)容,然后交給后續(xù)的loader繼續(xù)處理
再來看一下style-post-loader是如何生成data-v-hash:8的,邏輯主要在lib/loaders/stylePostLoaders.js中:
const qs = require('querystring')
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}`,
map: inMap,
scoped: !!query.scoped,
trim: true
})
if (errors.length) {
this.callback(errors[0])
} else {
this.callback(null, code, map)
}
}處理最終返回的code是這樣的:

總結(jié)
到此這篇關(guān)于Vue中CSS scoped原理的文章就介紹到這了,更多相關(guān)Vue CSS scoped的原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue中監(jiān)聽input框獲取焦點及失去焦點的問題
這篇文章主要介紹了vue中監(jiān)聽input框獲取焦點,失去焦點的問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07
vue-cli3.0如何使用prerender-spa-plugin插件預(yù)渲染
這篇文章主要介紹了vue-cli3.0如何使用prerender-spa-plugin插件預(yù)渲染,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-05-05
vue3中配置文件vue.config.js不生效的解決辦法
這篇文章主要介紹了vue3中配置文件vue.config.js不生效的解決辦法,文中通過代碼示例講解的非常詳細(xì),對大家解決問題有一定的幫助,需要的朋友可以參考下2024-05-05

