深入探索VueJS Scoped CSS 實現(xiàn)原理
使用VueJS進行應用開發(fā), 脫離不了對應用間的模塊進行拆分, 將大塊界面拆解為組件的過程. 我們可以很方便的在單文件中使用<template>塊維護組件的視圖, 使用<script>維護組件的邏輯部分, 使用<style>維護組件的樣式. 在我們編寫 VueJS 組件樣式時, 不得忽略的一點就是樣式污染.
樣式污染產(chǎn)生原因
提及樣式污染, 主要要追溯到Webpack對CSS文件的打包過程, 這里我們以Vue-Element-Admin中的Webpack配置項舉例:
const webpackConfig = merge(baseWebpackConfig, {
plugins: [
new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash:8].css'),
chunkFilename: utils.assetsPath('css/[name].[contenthash:8].css')
}),
]
})
Webpack 使用 MiniCssExtractPlugin 插件, 將文件(如Vue單文件組件)中的CSS代碼, 經(jīng)過處理后, 分離到形如app.hash1234.css的單獨的CSS文件:

如果沒有加入防止樣式污染的措施的同時, 項目中存在了大量的同名 ClassName, 那么可能會產(chǎn)生意想不到的CSS選擇器權重覆蓋. 這可能使后文件中某部分選擇器權重更高的類影響整個應用, 而此過程通常發(fā)生在組件的編寫中, 所以一般稱之為組件樣式污染.
Webpack & Vue SFC Object
對于 Vue 項目而言, 使用 Webpack 將極大的優(yōu)化了工作流程, 因為通過Vue Loader, Vue 單文件組件能很好的融合進 Webpack 工作流中. 通過跟蹤源碼, 可以發(fā)現(xiàn), 我們寫的單文件組件都被處理為了SFC對象, 即包含了單個HTML模塊, 單個腳本模塊, 一個或多個樣式模塊, 一個或多個自定義模塊的對象:
// vue-loader/index.js
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(),
filename,
sourceRoot,
needMap: sourceMap
})
// vuejs/component-compiler-utils/index.js
function parse(options) {
const { compiler } = options
output = compiler.parseComponent(source, compilerParseOptions)
return output
}
// vue.js
function parseComponent(content, options) {
// ...
var sfc = {
template: null,
script: null,
styles: [],
customBlocks: []
}
// ...
return sfc
}
我們可以將SFC結(jié)構融合到Webpack進行開發(fā)的過程成中, 主要有這幾點影響:
- 允許為 Vue 組件的每個部分使用其它的 webpack loader,例如在 <style>的部分使用 Sass Loader , 在 <customBlocks>的部分使用自定義 Loader
- 使用 webpack loader 將 <style>和 <template> 中引用的資源當作模塊依賴來處理
- 模擬 Scoped CSS
- 在開發(fā)過程中使用熱重載來保持狀態(tài)
以下主要介紹Scoped CSS的原理.
Scoped CSS
大白話版本之 Scoped CSS 原理
通過 Webpack 調(diào)用 VueJS 中相應 Loader , 給組件HTML模板添加自定義屬性 (Attribute) data-v-x, 以及給組件內(nèi)CSS選擇器添加對應的屬性選擇器 (Attribute Selector) [data-v-x], 達到組件內(nèi)樣式只能生效與組件內(nèi)HTML的效果, 代碼效果如下:
<div class='lionad' data-v-lionad></div>
<style>
.lionad[data-v-lionad] {
background: @tiger-orange;
}
</style>
源碼跟蹤
Webpack 使用其它 CSS Loader 處理 VueJS 中對應 CSS 代碼之前, Vue Loader 已經(jīng)替我們做了一層簡單的處理, 如果組件中 style 塊包含了 scoped 屬性:
<!-- 某個VueJS組件中 -->
<template>
<div class='lionad'></div>
</template>
<style lang="scss" scoped>
.lionad {
background: @tiger-orange;
}
</style>
下代碼即判斷當前SFC對象樣式塊中是否有scoped屬性, 并插入用于 query 中, 順帶一提, 每個單文件組件被解析后, 都會生成對應組件ID, ID主要以生產(chǎn)/開發(fā)環(huán)境做區(qū)分, 通過文件路徑+源碼或是文件路徑的值作為哈希特征值的形式生成, 如下:
// vue-loader/index.js
const id = hash(isProduction (shortFilePath + '\n' + source) : shortFilePath)
const hasScoped = descriptor.styles.some(s => s.scoped)
const query = `? vue&type=template${idQuery}${scopedQuery}`
const request = templateRequest = stringifyRequest(src + query)
templateImport = `import { render, staticRenderFns } from ${request}`
HTML模板處理
在用于處理SFC結(jié)構中HTML模板的 templateLoader 中, 我們可以得知, query 中所設置的參數(shù)將合并為 loader options 經(jīng)由 Webpack 轉(zhuǎn)交 templateLoader 再轉(zhuǎn)交 @vue/component-compiler-utils.compileTemplate 處理:
// vue-loader/templateLoader.js
const query = qs.parse(this.resourceQuery)
const { id } = query
const compilerOptions = Object.assign({}, options.compilerOptions, {
scopeId: query.scoped ? `data-v-${id}` : null
})
const compiled = compileTemplate({ compilerOptions })
實際 compileTemplate 函數(shù)在處理內(nèi)容時, 編譯函數(shù)使用的是 query 中的 compiler 或 vue-template-compiler, 后者會將模板文本轉(zhuǎn)換成為 JavaScript 渲染函數(shù), 大致如下:
- 從HTML模版轉(zhuǎn)換為AST(虛擬語法樹)
- AST優(yōu)化,處理靜態(tài)模版與動態(tài)模板
- 生成JS函數(shù),用于在運行時運行時生成純HTML
代碼分別對應:
// vue-template-compiler/build.js/createCompilerCreator var ast = parse(template.trim(), options) optimize(ast, options) var code = generate(ast, options)
先前我們的組件ID在 parse 階段解析開始標簽時就會被推入內(nèi)部儲存的數(shù)據(jù)結(jié)構中:
function elementToOpenTagSegments (el, state) {
var segments = [{ type: RAW, value: ("<" + (el.tag)) }]
// _scopedId
if (state.options.scopeId) {
segments.push({ type: RAW, value: (" " + (state.options.scopeId)) })
}
segments.push({ type: RAW, value: ">" })
return segments
}
先前我們的HTML模板 <div class='lionad'></div> 中開始標簽會被轉(zhuǎn)換成如下數(shù)據(jù)結(jié)構:
[
{ type: RAW, value: '<div' },
{ type: RAW, value: 'class=lionad' },
{ type: RAW, value: 'data-v-xxxxxx' },
{ type: RAW, value: '>' },
]
樣式模板處理
與 HTML Template 解析的過程類似, 通過 Webpack 將樣式模板轉(zhuǎn)交 stylePostLoader 進行處理, 處理邏輯主要引用了 @vue/component-compiler-utils 中的 compileStyle 部分, 后者對樣式模板進行解析的過程中, 將會對含 scoped 標記的模板引入插件 stylePlugins/scoped.js, scoped.js 將 data-v-xxxxxx 添加到選擇器末尾的過程如下:
selectors.each((selector) => {
selector.each((n) => {
if (n.value === '::v-deep' || n.value === '>>>' || n.value === '/deep/') {
return false;
}
});
selector.insertAfter(node, selectorParser.attribute({
attribute: id
}))
})
題外話, 通過以上代碼, 我們發(fā)現(xiàn)當當前處理到三種特定類型選擇器會終止循環(huán), 停止將 data-v-xxx 添加到選擇器末尾:
- 偽類 ::v-deep
- 選擇器 >>>
- 選擇器 /deep/
我們可以利用這個特征, 在組件中寫樣式穿透, 即內(nèi)部組件影響外部組件樣式 (ε=ε=ε=┏(゜ロ゜;)┛ 主動樣式污染), 當然這在特定的情境下是有用的, 比如當我們想主動覆蓋第三方UI組件框架的樣式, 卻不想引入新的CSS文件, 或不想寫非 Scoped CSS 模板的時候.
最后
本人前端菜得捉急, 文中不詳盡或有錯的地方, 歡迎各位大佬斧正. 如果本文對你有所幫助, 那是再好不過, 看到這里都是真愛啊
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
vue+springboot實現(xiàn)項目的CORS跨域請求
這篇文章主要介紹了vue+springboot實現(xiàn)項目的CORS跨域請求,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09
vue?mounted周期中document.querySelectorAll()獲取不到元素的解決
這篇文章主要介紹了vue?mounted周期中document.querySelectorAll()獲取不到元素的解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
elementUI表格多選框this.$refs.xxx.toggleRowSelection無效問題
這篇文章主要介紹了elementUI表格多選框this.$refs.xxx.toggleRowSelection無效問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11
vue elementUI select下拉框設置默認值(賦值)失敗的解決
這篇文章主要介紹了vue elementUI select下拉框設置默認值(賦值)失敗的解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10
詳解如何使用Vuex實現(xiàn)Vue后臺管理中的角色鑒權
最近參與了公司一個新的B端項目的研發(fā),從無到有搭建項目的過程中,遇到了關于項目鑒權的問題,這篇文章主要給大家介紹了關于如何使用Vuex實現(xiàn)Vue后臺管理中的角色鑒權的相關資料,需要的朋友可以參考下2022-05-05

