Vue中CSS?scoped的原理詳細(xì)講解
前言
在日常的Vue項(xiàng)目開發(fā)過程中,為了讓項(xiàng)目更好的維護(hù)一般都會(huì)使用模塊化開發(fā)的方式進(jìn)行。也就是每個(gè)組件維護(hù)獨(dú)立的template
,script
,style
。主要介紹一下使用<style scoped>
為什么在頁面渲染完后樣式之間并不會(huì)造成污染。
示例
搭建一個(gè)簡(jiǎn)單的Vue項(xiàng)目測(cè)試一下:
終端執(zhí)行npx webpack
輸出dist目錄,在瀏覽器打開index.html調(diào)試一下看看現(xiàn)象:
- 每個(gè)組件都會(huì)擁有一個(gè)[data-v-hash:8]插入HTML標(biāo)簽,子組件標(biāo)簽上也具體父組件[data-v-hash:8];
- 如果style標(biāo)簽加了scoped屬性,里面的選擇器都會(huì)變成(Attribute Selector) [data-v-hash:8];
- 如果子組件選擇器跟父組件選擇器完全一樣,那么就會(huì)出現(xiàn)子組件樣式被父組件覆蓋,因?yàn)樽咏M件會(huì)優(yōu)先于父組件mounted,有興趣可以測(cè)試一下哦。
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
開始分析,因?yàn)槲襑ebpack是4.x版本,所以VueLoaderPlugin = require('./plugin-webpack4')
,重點(diǎn)來看看這個(gè)lib/plugin-webpack4.js
文件:
const qs = require('querystring') const RuleSet = require('webpack/lib/RuleSet') const id = 'vue-loader-plugin' const NS = 'vue-loader' // 很明顯這就是一個(gè)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 // 對(duì) 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的配置。總的來說就是對(duì)vue單文件編寫做的一個(gè)擴(kuò)展(比如可以寫less文件,在vue style中也可以寫less)
vue-loader
vue-loader
是如何操作.vue文件的,目前只關(guān)心style
部分,邏輯在lib/index.js
:
vue文件解析
// 很明顯這就是一個(gè)loader寫法 module.exports = function (source) { const loaderContext = this // ... const { target, request, // 請(qǐng)求資源路徑 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) { // 最終生成一個(gè)import依賴請(qǐng)求 stylesCode = genStylesCode( loaderContext, descriptor.styles, id, resourcePath, stringifyRequest, needsHotReload, isServer || isShadow // needs explicit injection? ) } }
可以看到解析完vue文件的結(jié)果大概就是這樣的:
依賴解析
vue文件解析完之后template,script,style等都有個(gè)依賴的路徑,后續(xù)可以通過配置的loader進(jìn)行解析了,因?yàn)槲覀円呀?jīng)在VuePluginLoader
中修改了module.rules的配置,而且依賴的路徑中query中都擁有vue字段,所以會(huì)先走到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。
處理資源的時(shí)候先走的是vue-loader
,這時(shí)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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Vue路由鉤子及應(yīng)用場(chǎng)景(小結(jié))
本篇文章主要介紹了詳解Vue路由鉤子及應(yīng)用場(chǎng)景(小結(jié)),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11vue中監(jiān)聽input框獲取焦點(diǎn)及失去焦點(diǎn)的問題
這篇文章主要介紹了vue中監(jiān)聽input框獲取焦點(diǎn),失去焦點(diǎn)的問題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07vue-cli3.0如何使用prerender-spa-plugin插件預(yù)渲染
這篇文章主要介紹了vue-cli3.0如何使用prerender-spa-plugin插件預(yù)渲染,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05淺談Vue.js中如何實(shí)現(xiàn)自定義下拉菜單指令
這篇文章主要介紹了淺談Vue.js中如何實(shí)現(xiàn)自定義下拉菜單指令,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-01-01vue3中配置文件vue.config.js不生效的解決辦法
這篇文章主要介紹了vue3中配置文件vue.config.js不生效的解決辦法,文中通過代碼示例講解的非常詳細(xì),對(duì)大家解決問題有一定的幫助,需要的朋友可以參考下2024-05-05詳解Vue 動(dòng)態(tài)組件與全局事件綁定總結(jié)
這篇文章主要介紹了詳解Vue 動(dòng)態(tài)組件與全局事件綁定總結(jié),從示例中發(fā)現(xiàn)并解決問題,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11