vue3如何避免樣式污染的方法示例
眾所周知,在vue中使用scoped可以避免父組件的樣式滲透到子組件中。使用了scoped后會給html增加自定義屬性data-v-x
,同時會給組件內(nèi)CSS選擇器添加對應(yīng)的屬性選擇器[data-v-x]
。本文講一下vue是如何給CSS選擇器添加對應(yīng)的屬性選擇器[data-v-x]
。注:本文中使用的vue版本為3.4.19
,@vitejs/plugin-vue
的版本為5.0.4
。
先看個demo
代碼如下:
<template> <div class="block">hello world</div> </template> <style scoped> .block { color: red; } </style>
經(jīng)過編譯后,上面的demo代碼就會變成下面這樣:
<template> <div data-v-c1c19b25 class="block">hello world</div> </template> <style> .block[data-v-c1c19b25] { color: red; } </style>
從上面的代碼可以看到在div上多了一個data-v-c1c19b25
自定義屬性,并且css的屬性選擇器上面也多了一個[data-v-c1c19b25]
。
那有人就會好奇,為什么生成這樣的代碼就可以避免樣式污染呢?
.block[data-v-c1c19b25]
:這里面包含兩個選擇器。.block
是一個類選擇器,表示class的值包含block
。[data-v-c1c19b25]
是一個屬性選擇器,表示存在data-v-c1c19b25
自定義屬性的元素。
所以只有class包含block
,并且存在data-v-c1c19b25
自定義屬性的元素才能命中這個樣式,這樣就能避免樣式污染。并且由于在同一個組件里面生成的data-v-x
值是一樣的,所以在同一組件內(nèi)多個html元素只要class的值包含block
,就可以命中color: red
的樣式。
接下來我將通過debug的方式帶你了解,vue是如何在css中生成.block[data-v-c1c19b25]這樣的屬性選擇器。
@vitejs/plugin-vue
還是一樣的套路啟動一個debug終端。這里以vscode
舉例,打開終端然后點擊終端中的+
號旁邊的下拉箭頭,下拉中點擊Javascript Debug Terminal
就可以啟動一個debug
終端。
假如vue
文件編譯為js
文件是一個毛線團,那么他的線頭一定是vite.config.ts
文件中使用@vitejs/plugin-vue
的地方。通過這個線頭開始debug
我們就能夠梳理清楚完整的工作流程。
vuePlugin函數(shù)
我們給上方圖片的vue
函數(shù)打了一個斷點,然后在debug
終端上面執(zhí)行yarn dev
,我們看到斷點已經(jīng)停留在了vue
函數(shù)這里。然后點擊step into
,斷點走到了@vitejs/plugin-vue
庫中的一個vuePlugin
函數(shù)中。我們看到簡化后的vuePlugin
函數(shù)代碼如下:
function vuePlugin(rawOptions = {}) { return { name: "vite:vue", // ...省略其他插件鉤子函數(shù) transform(code, id, opt) { // .. } }; }
@vitejs/plugin-vue
是作為一個plugins
插件在vite中使用,vuePlugin
函數(shù)返回的對象中的transform
方法就是對應(yīng)的插件鉤子函數(shù)。vite會在對應(yīng)的時候調(diào)用這些插件的鉤子函數(shù),vite每解析一個模塊都會執(zhí)行一次transform
鉤子函數(shù)。更多vite鉤子相關(guān)內(nèi)容查看官網(wǎng)。
我們這里只需要看transform
鉤子函數(shù),解析每個模塊時調(diào)用。
由于解析每個文件都會走到transform
鉤子函數(shù)中,但是我們只關(guān)注index.vue
文件是如何解析的,所以我們給transform
鉤子函數(shù)打一個條件斷點。如下圖:
然后點擊Continue(F5),vite
服務(wù)啟動后就會走到transform
鉤子函數(shù)中打的斷點。我們可以看到簡化后的transform
鉤子函數(shù)代碼如下:
function transform(code, id, opt) { const { filename, query } = parseVueRequest(id); if (!query.vue) { return transformMain( code, filename, options.value, this, ssr, customElementFilter.value(filename) ); } else { const descriptor = getDescriptor(filename); if (query.type === "style") { return transformStyle( code, descriptor, Number(query.index || 0), options.value ); } } }
首先調(diào)用parseVueRequest
函數(shù)解析出當前要處理的文件的filename
和query
,在debug終端來看看此時這兩個的值。如下圖:
從上圖中可以看到filename
為當前處理的vue文件路徑,query
的值為空數(shù)組。所以此時代碼會走到transformMain
函數(shù)中。
transformMain函數(shù)
將斷點走進transformMain
函數(shù),在我們這個場景中簡化后的transformMain
函數(shù)代碼如下:
async function transformMain(code, filename, options) { const { descriptor } = createDescriptor(filename, code, options); const { code: templateCode } = await genTemplateCode( descriptor // ...省略 ); const { code: scriptCode } = await genScriptCode( descriptor // ...省略 ); const stylesCode = await genStyleCode( descriptor // ...省略 ); const output = [scriptCode, templateCode, stylesCode]; let resolvedCode = output.join("\n"); return { code: resolvedCode, }; }
首先調(diào)用createDescriptor
函數(shù)根據(jù)當前vue文件的code代碼字符串生成一個descriptor
對象,簡化后的createDescriptor
函數(shù)代碼如下:
const cache = new Map(); function createDescriptor( filename, source, { root, isProduction, sourceMap, compiler, template } ) { const { descriptor, errors } = compiler.parse(source, { filename, sourceMap, templateParseOptions: template?.compilerOptions, }); const normalizedPath = slash(path.normalize(path.relative(root, filename))); descriptor.id = getHash(normalizedPath + (isProduction ? source : "")); cache.set(filename, descriptor); return { descriptor, errors }; }
首先調(diào)用compiler.parse
方法根據(jù)當前vue文件的code代碼字符串生成一個descriptor
對象,此時的descriptor
對象主要有三個屬性template
、scriptSetup
、style
,分別對應(yīng)的是vue文件中的 <template>
模塊、<template setup>
模塊、 <style>
模塊。
然后調(diào)用getHash
函數(shù)給descriptor
對象生成一個id
屬性,getHash
函數(shù)代碼如下:
import { createHash } from "node:crypto"; function getHash(text) { return createHash("sha256").update(text).digest("hex").substring(0, 8); }
從上面的代碼可以看出id是根據(jù)vue文件的路徑調(diào)用node的createHash
加密函數(shù)生成的,這里生成的id就是scoped生成的自定義屬性data-v-x
中的x
部分。
然后在createDescriptor
函數(shù)中將生成的descriptor
對象緩存起來,關(guān)于descriptor
對象的處理就這么多了。
接著在transformMain
函數(shù)中會分別以descriptor
對象為參數(shù)執(zhí)行genTemplateCode
、genScriptCode
、genStyleCode
函數(shù),分別得到編譯后的render函數(shù)、編譯后的js代碼、編譯后的style代碼。
編譯后的render函數(shù)如下圖:
從上圖中可以看到template模塊已經(jīng)編譯成了render函數(shù)
編譯后的js代碼如下圖:
從上圖中可以看到script模塊已經(jīng)編譯成了一個名為_sfc_main
的對象,因為我們這個demo中script模塊沒有代碼,所以這個對象是一個空對象。
編譯后的style代碼如下圖:
從上圖中可以看到style模塊已經(jīng)編譯成了一個import語句。
最后就是使用換行符\n
將templateCode
、scriptCode
、stylesCode
拼接起來就是vue文件編譯后的js文件啦,如下圖:
想必細心的同學已經(jīng)發(fā)現(xiàn)有地方不對啦,這里的style模塊編譯后是一條import語句,并不是真正的css代碼。這條import語句依然還是import導入的index.vue
文件,只是加了一些額外的query參數(shù)。
?vue&type=style&index=0&lang.css
:這個query參數(shù)表明當前import導入的是vue文件的css部分。
還記得前面講過的transform
鉤子函數(shù)嗎?vite每解析一個模塊都會執(zhí)行一次transform
鉤子函數(shù),這個import導入vue文件的css部分,當然也會觸發(fā)transform
鉤子函數(shù)的執(zhí)行。
第二次執(zhí)行transform鉤子函數(shù)
當在瀏覽器中執(zhí)行vue文件編譯后的js文件時會觸發(fā)import "/Users/xxx/index.vue?vue&type=style&index=0&lang.css"
語句的執(zhí)行,導致再次執(zhí)行transform
鉤子函數(shù)。
transform
鉤子函數(shù)代碼如下:
function transform(code, id, opt) { const { filename, query } = parseVueRequest(id); if (!query.vue) { return transformMain( code, filename, options.value, this, ssr, customElementFilter.value(filename) ); } else { const descriptor = getDescriptor(filename); if (query.type === "style") { return transformStyle( code, descriptor, Number(query.index || 0), options.value ); } } }
由于此時的query
中是有vue
字段,所以!query.vue
的值為false,這次代碼就不會走進transformMain
函數(shù)中了。在else
代碼在先執(zhí)行getDescriptor
函數(shù)拿到descriptor
對象,getDescriptor
函數(shù)代碼如下:
function getDescriptor(filename) { const _cache = cache; if (_cache.has(filename)) { return _cache.get(filename); } }
我們在第一次執(zhí)行transformMain
函數(shù)的時候會去執(zhí)行createDescriptor
函數(shù),他的作用是根據(jù)當前vue文件的code代碼字符串生成一個descriptor
對象,并且將這個descriptor
對象緩存起來了。在getDescriptor
函數(shù)中就是將緩存的descriptor
對象取出來。
由于query
中有type=style
,所以代碼會走到transformStyle
函數(shù)中。
transformStyle函數(shù)
接著將斷點走進transformStyle
函數(shù),代碼如下:
async function transformStyle(code, descriptor, index, options) { const block = descriptor.styles[index]; const result = await options.compiler.compileStyleAsync({ ...options.style, filename: descriptor.filename, id: `data-v-${descriptor.id}`, source: code, scoped: block.scoped, }); return { code: result.code, }; }
從上面的代碼可以看到transformStyle
函數(shù)依然不是干活的地方,而是調(diào)用的@vue/compiler-sfc
包暴露出的compileStyleAsync
函數(shù)。
在調(diào)用compileStyleAsync
函數(shù)的時候有三個參數(shù)需要注意:source
、id
和scoped
。
source
字段的值為code
,值是當前css代碼字符串。
id
字段的值為data-v-${descriptor.id}
,是不是覺得看著很熟悉?沒錯他就是使用scoped
后vue幫我們自動生成的html自定義屬性data-v-x
和css選擇屬性選擇器[data-v-x]
。
其中的descriptor.id
就是在生成descriptor
對象時根據(jù)vue文件路徑加密生成的id。
scoped
字段的值為block.scoped
,而block
的值為descriptor.styles[index]
。由于一個vue文件可以寫多個style標簽,所以descriptor
對象的styles
屬性是一個數(shù)組,分包對應(yīng)多個style標簽。我們這里只有一個style
標簽,所以此時的index
值為0。block.scoped
的值為style標簽上面是否有使用scoped
。
直到進入compileStyleAsync
函數(shù)之前代碼其實一直都還在@vitejs/plugin-vue
包中執(zhí)行,真正干活的地方是在@vue/compiler-sfc
包中。
@vue/compiler-sfc
接著將斷點走進compileStyleAsync
函數(shù),代碼如下:
function compileStyleAsync(options) { return doCompileStyle({ ...options, isAsync: true, }); }
從上面的代碼可以看到實際干活的是doCompileStyle
函數(shù)。
doCompileStyle函數(shù)
接著將斷點走進doCompileStyle
函數(shù),在我們這個場景中簡化后的doCompileStyle
函數(shù)代碼如下:
import postcss from "postcss"; function doCompileStyle(options) { const { filename, id, scoped = false, postcssOptions, postcssPlugins, } = options; const source = options.source; const shortId = id.replace(/^data-v-/, ""); const longId = `data-v-${shortId}`; const plugins = (postcssPlugins || []).slice(); if (scoped) { plugins.push(scopedPlugin(longId)); } const postCSSOptions = { ...postcssOptions, to: filename, from: filename, }; let result; try { result = postcss(plugins).process(source, postCSSOptions); return result.then((result) => ({ code: result.css || "", // ...省略 })); } catch (e: any) { errors.push(e); } }
在doCompileStyle
函數(shù)中首先使用const
定義了一堆變量,我們主要關(guān)注source
和longId
。
其中的source
為當前css代碼字符串,longId
為根據(jù)vue文件路徑加密生成的id,值的格式為data-v-x
。他就是使用scoped
后vue幫我們自動生成的html自定義屬性data-v-x
和css選擇屬性選擇器[data-v-x]
。
接著就是判斷scoped
是否為true,也就是style中使用有使用scoped。如果為true,就將scopedPlugin
插件push到plugins
數(shù)組中。從名字你應(yīng)該猜到了這個plugin插件就是用于處理css scoped的。
最后就是執(zhí)行result = postcss(plugins).process(source, postCSSOptions)
拿到經(jīng)過postcss
轉(zhuǎn)換編譯器處理后的css。
可能有的小伙伴對postcss
不夠熟悉,我們這里來簡單介紹一下。
postcss
是 css 的 transpiler(轉(zhuǎn)換編譯器,簡稱轉(zhuǎn)譯器),它對于 css 就像 babel 對于 js 一樣,能夠做 css 代碼的分析和轉(zhuǎn)換。同時,它也提供了插件機制來做自定義的轉(zhuǎn)換。
在我們這里主要就是用到了postcss
提供的插件機制來完成css scoped的自定義轉(zhuǎn)換,調(diào)用postcss
的時候我們傳入了source
,他的值是style模塊中的css代碼。并且傳入的plugins
插件數(shù)組中有個scopedPlugin
插件,這個自定義插件就是vue寫的用于處理css scoped的插件。
在執(zhí)行postcss
對css代碼進行轉(zhuǎn)換之前我們在debug終端來看看此時的css代碼是什么樣的,如下圖:
從上圖可以看到此時的css代碼還是和我們源代碼是一樣的,并沒有css選擇屬性選擇器[data-v-x]
scopedPlugin插件
scopedPlugin
插件在我們這個場景中簡化后的代碼如下:
const scopedPlugin = (id = "") => { return { postcssPlugin: "vue-sfc-scoped", Rule(rule) { processRule(id, rule); }, // ...省略 }; };
這里的id就是我們在doCompileStyle
函數(shù)中傳過來的longId
,也就是生成的css選擇屬性選擇器[data-v-x]
中的data-v-x
。
在我們這個場景中只需要關(guān)注Rule
鉤子函數(shù),當postcss
處理到選擇器開頭的規(guī)則就會走到Rule
鉤子函數(shù)。
我們這里需要在使用了scoped后給css選擇器添加對應(yīng)的屬性選擇器[data-v-x]
,所以我們需要在插件中使用Rule
鉤子函數(shù),在處理css選擇器時手動給選擇器后面塞一個屬性選擇器[data-v-x]
。
給Rule
鉤子函數(shù)打個斷點,當postcss
處理到我們代碼中的.block
時就會走到斷點中。在debug終端看看rule
的值,如下圖:
從上圖中可以看到此時rule.selector
的值為.block
,是一個class值為block
的類選擇器。
processRule函數(shù)
將斷點走進processRule
函數(shù)中,在我們這個場景中簡化后的processRule
函數(shù)代碼如下:
import selectorParser from "postcss-selector-parser"; function processRule(id: string, rule: Rule) { rule.selector = selectorParser((selectorRoot) => { selectorRoot.each((selector) => { rewriteSelector(id, selector, selectorRoot); }); }).processSync(rule.selector); }
前面我們講過rule.selector
的值為.block
,通過重寫rule.selector
的值可以將當前css選擇器替換為一個新的選擇器。在processRule
函數(shù)中就是使用postcss-selector-parser
來解析一個選擇器,進行處理后返回一個新的選擇器。
processSync
方法的作用為接收一個選擇器,然后在回調(diào)中對解析出來的選擇器進行處理,最后將處理后的選擇器以字符串的方式進行返回。
在我們這里processSync
方法接收的選擇器是字符串.block
,經(jīng)過回調(diào)函數(shù)處理后返回的選擇器字符串就變成了.block[data-v-c1c19b25]
。
我們接下來看selectorParser
回調(diào)函數(shù)中的代碼,在回調(diào)函數(shù)中會使用selectorRoot.each
去遍歷解析出來的選擇器。
為什么這里需要去遍歷呢?
答案是css選擇器可以這樣寫:.block.demo
,如果是這樣的選擇器經(jīng)過解析后,就會被解析成兩個選擇器,分別是.block
和.demo
。
在each遍歷中會調(diào)用rewriteSelector
函數(shù)對當前選取器進行重寫。
rewriteSelector函數(shù)
將斷點走進rewriteSelector
函數(shù),在我們這個場景中簡化后的代碼如下:
function rewriteSelector(id, selector) { let node; const idToAdd = id; selector.each((n) => { node = n; }); selector.insertAfter( node, selectorParser.attribute({ attribute: idToAdd, value: idToAdd, raws: {}, quoteMark: `"`, }) ); }
在rewriteSelector
函數(shù)中each遍歷當前selector
選擇器,給node
賦值。將斷點走到each遍歷之后,我們在debug終端來看看selector
選擇器和node
變量。如下圖:
在這里selector
是container容器,node
才是具體要操作的選擇器節(jié)點。
比如我們這里要執(zhí)行的selector.insertAfter
方法就是在selector
容器中在一個指定節(jié)點后面去插入一個新的節(jié)點。這個和操作瀏覽器DOM API很相似。
我們再來看看要插入的節(jié)點,selectorParser.attribute
函數(shù)的作用是創(chuàng)建一個attribute屬性選擇器。在我們這里就是創(chuàng)建一個[data-v-x]
的屬性選擇器,如下圖:
所以這里就是在.block
類選擇器后面插入一個[data-v-c1c19b25]
的屬性選擇器。
我們在debug終端來看看執(zhí)行insertAfter
函數(shù)后的selector
選擇器,如下圖:
將斷點逐層走出,直到processRule
函數(shù)中。我們在debug終端來看看此時被重寫后的rule.selector
字符串的值是什么樣的,如下圖
原來rule.selector
的值為.block
,通過重寫rule.selector
的值可以將.block
類選擇器替換為一個新的選擇器,而這個新的選擇器是在原來的.block
類選擇器后面再塞一個[data-v-c1c19b25]
屬性選擇器。
總結(jié)
這篇文章我們講了當使用scoped后,vue是如何給組件內(nèi)CSS選擇器添加對應(yīng)的屬性選擇器[data-v-x]
。主要分為兩部分,分別在兩個包里面執(zhí)行。
第一部分為在
@vitejs/plugin-vue
包內(nèi)執(zhí)行。- 首先會根據(jù)當前vue文件的路徑進行加密算法生成一個id,這個id就是添加的屬性選擇器
[data-v-x]
中的x
。 - 然后就是執(zhí)行
transformStyle
函數(shù),這個transformStyle
并不是實際干活的地方,他調(diào)用了@vue/compiler-sfc
包的compileStyleAsync
函數(shù)。并且傳入了id
、code
(css代碼字符串)、scoped
(是否在style中使用scoped
)。
- 首先會根據(jù)當前vue文件的路徑進行加密算法生成一個id,這個id就是添加的屬性選擇器
第二部分在
@vue/compiler-sfc
包執(zhí)行。compileStyleAsync
函數(shù)依然不是實際干活的地方,而是調(diào)用了doCompileStyle
函數(shù)。- 在
doCompileStyle
函數(shù)中,如果scoped
為true就向plugins
數(shù)組中插入一個scopedPlugin
插件,這個是vue寫的postcss
插件,用于處理css scoped。然后使用postcss
轉(zhuǎn)換編譯器對css代碼進行轉(zhuǎn)換。 - 當
postcss
處理到選擇器開頭的規(guī)則就會走到scopedPlugin
插件中的Rule
鉤子函數(shù)中。在Rule
鉤子函數(shù)中會執(zhí)行processRule
函數(shù)。
ata-v-x]中的
x`。 - 然后就是執(zhí)行
transformStyle
函數(shù),這個transformStyle
并不是實際干活的地方,他調(diào)用了@vue/compiler-sfc
包的compileStyleAsync
函數(shù)。并且傳入了id
、code
(css代碼字符串)、scoped
(是否在style中使用scoped
)。
到此這篇關(guān)于vue3是如何避免樣式污染的的文章就介紹到這了,更多相關(guān)vue3 樣式污染內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!