使用VueJS進行應用開發, 脫離不了對應用間的模塊進行拆分, 將大塊界面拆解爲組件的過程. 咱們能夠很方便的在單文件中使用<template>
塊維護組件的視圖, 使用<script>
維護組件的邏輯部分, 使用<style>
維護組件的樣式. 在咱們編寫 VueJS 組件樣式時, 不得忽略的一點就是樣式污染.css
說起樣式污染, 主要要追溯到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代碼, 通過處理後, 分離到形如app.hash1234.css
的單獨的CSS文件:vue
若是沒有加入防止樣式污染的措施的同時, 項目中存在了大量的同名 ClassName, 那麼可能會產生意想不到的CSS選擇器權重覆蓋. 這可能使後文件中某部分選擇器權重更高的類影響整個應用, 而此過程一般發生在組件的編寫中, 因此通常稱之爲組件樣式污染.node
對於 Vue 項目而言, 使用 Webpack 將極大的優化了工做流程, 由於經過Vue Loader
, Vue 單文件組件能很好的融合進 Webpack 工做流中. 經過跟蹤源碼, 能夠發現, 咱們寫的單文件組件都被處理爲了SFC對象
, 即包含了單個HTML模塊, 單個腳本模塊, 一個或多個樣式模塊, 一個或多個自定義模塊的對象:webpack
// 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結構融合到Webpack
進行開發的過程成中, 主要有這幾點影響:git
<style>
的部分使用 Sass Loader , 在 <customBlocks>
的部分使用自定義 Loader<style>
和 <template>
中引用的資源看成模塊依賴來處理如下主要介紹Scoped CSS
的原理.github
經過 Webpack 調用 VueJS 中相應 Loader , 給組件HTML模板添加自定義屬性 (Attribute) data-v-x
, 以及給組件內CSS選擇器添加對應的屬性選擇器 (Attribute Selector) [data-v-x]
, 達到組件內樣式只能生效與組件內HTML的效果, 代碼效果以下:web
<div class='lionad' data-v-lionad></div>
<style> .lionad[data-v-lionad] { background: @tiger-orange; } </style>
複製代碼
Webpack 使用其它 CSS Loader 處理 VueJS 中對應 CSS 代碼以前, Vue Loader
已經替咱們作了一層簡單的處理, 若是組件中 style
塊包含了 scoped
屬性:數據結構
<!-- 某個VueJS組件中 -->
<template>
<div class='lionad'></div>
</template>
<style lang="scss" scoped> .lionad { background: @tiger-orange; } </style>
複製代碼
下代碼即判斷當前SFC
對象樣式塊中是否有scoped
屬性, 並插入用於 query 中, 順帶一提, 每一個單文件組件被解析後, 都會生成對應組件ID, ID主要以生產/開發環境作區分, 經過文件路徑+源碼或是文件路徑的值做爲哈希特徵值的形式生成, 以下:app
// 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}`
複製代碼
在用於處理SFC結構中HTML模板的 templateLoader
中, 咱們能夠得知, query 中所設置的參數將合併爲 loader options 經由 Webpack 轉交 templateLoader
再轉交 @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
函數在處理內容時, 編譯函數使用的是 query 中的 compiler 或 vue-template-compiler
, 後者會將模板文本轉換成爲 JavaScript 渲染函數, 大體以下:
代碼分別對應:
// vue-template-compiler/build.js/createCompilerCreator
var ast = parse(template.trim(), options)
optimize(ast, options)
var code = generate(ast, options)
複製代碼
先前咱們的組件ID在 parse 階段解析開始標籤時就會被推入內部儲存的數據結構中:
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>
中開始標籤會被轉換成以下數據結構:
[
{ type: RAW, value: '<div' },
{ type: RAW, value: 'class=lionad' },
{ type: RAW, value: 'data-v-xxxxxx' },
{ type: RAW, value: '>' },
]
複製代碼
與 HTML Template 解析的過程相似, 經過 Webpack 將樣式模板轉交 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
}))
})
複製代碼
題外話, 經過以上代碼, 咱們發現噹噹前處理到三種特定類型選擇器會終止循環, 中止將 data-v-xxx
添加到選擇器末尾:
::v-deep
>>>
/deep/
咱們能夠利用這個特徵, 在組件中寫樣式穿透, 即內部組件影響外部組件樣式 (ε=ε=ε=┏(゜ロ゜;)┛ 主動樣式污染), 固然這在特定的情境下是有用的, 好比當咱們想主動覆蓋第三方UI組件框架的樣式, 卻不想引入新的CSS文件, 或不想寫非 Scoped CSS 模板的時候.
本人前端菜得捉急, 文中不詳盡或有錯的地方, 歡迎各位大佬斧正. 若是本文對你有所幫助, 那是再好不過, 看到這裏都是真愛啊(笑.jpg) 勞煩點贊 收藏 關注 三連擊
!!!
爲了方便理解, 或是防止陷入使人頭疼的細節, 文中源碼片斷有部分刪減