成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

掉了兩根頭發后,我悟了!Vue3的Scoped原來是這樣避免樣式污染

開發 前端
原來rule.selector?的值為.block?,通過重寫rule.selector?的值可以將.block?類選擇器替換為一個新的選擇器,而這個新的選擇器是在原來的.block?類選擇器后面再塞一個[data-v-c1c19b25]屬性選擇器。

前言

眾所周知,在vue中使用scoped可以避免父組件的樣式滲透到子組件中。使用了scoped后會給html增加自定義屬性data-v-x,同時會給組件內CSS選擇器添加對應的屬性選擇器[data-v-x]。這篇我們來講講vue是如何給CSS選擇器添加對應的屬性選擇器[data-v-x]。注:本文中使用的vue版本為3.4.19,@vitejs/plugin-vue的版本為5.0.4。

看個demo

我們先來看個demo,代碼如下:

<template>
  <div class="block">hello world</div>
</template>

<style scoped>
.block {
  color: red;
}
</style>

經過編譯后,上面的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值是一樣的,所以在同一組件內多個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函數

我們給上方圖片的vue函數打了一個斷點,然后在debug終端上面執行yarn dev,我們看到斷點已經停留在了vue函數這里。然后點擊step into,斷點走到了@vitejs/plugin-vue庫中的一個vuePlugin函數中。我們看到簡化后的vuePlugin函數代碼如下:

function vuePlugin(rawOptions = {}) {
  return {
    name: "vite:vue",
    // ...省略其他插件鉤子函數
    transform(code, id, opt) {
      // ..
    }
  };
}

@vitejs/plugin-vue是作為一個plugins插件在vite中使用,vuePlugin函數返回的對象中的transform方法就是對應的插件鉤子函數。vite會在對應的時候調用這些插件的鉤子函數,vite每解析一個模塊都會執行一次transform鉤子函數。更多vite鉤子相關內容查看官網。

我們這里只需要看transform鉤子函數,解析每個模塊時調用。

由于解析每個文件都會走到transform鉤子函數中,但是我們只關注index.vue文件是如何解析的,所以我們給transform鉤子函數打一個條件斷點。如下圖:

圖片圖片

然后點擊Continue(F5),vite服務啟動后就會走到transform鉤子函數中打的斷點。我們可以看到簡化后的transform鉤子函數代碼如下:

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
      );
    }
  }
}

首先調用parseVueRequest函數解析出當前要處理的文件的filename和query,在debug終端來看看此時這兩個的值。如下圖:

圖片圖片

從上圖中可以看到filename為當前處理的vue文件路徑,query的值為空數組。所以此時代碼會走到transformMain函數中。

transformMain函數

將斷點走進transformMain函數,在我們這個場景中簡化后的transformMain函數代碼如下:

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,
  };
}

我們在 通過debug搞清楚.vue文件怎么變成.js文件文章中已經深入講解過transformMain函數了,所以這篇文章我們不會深入到transformMain函數中使用到的每個函數中。

首先調用createDescriptor函數根據當前vue文件的code代碼字符串生成一個descriptor對象,簡化后的createDescriptor函數代碼如下:

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 };
}

首先調用compiler.parse方法根據當前vue文件的code代碼字符串生成一個descriptor對象,此時的descriptor對象主要有三個屬性template、scriptSetup、style,分別對應的是vue文件中的<template>模塊、<template setup>模塊、<style>模塊。

然后調用getHash函數給descriptor對象生成一個id屬性,getHash函數代碼如下:

import { createHash } from "node:crypto";
function getHash(text) {
  return createHash("sha256").update(text).digest("hex").substring(0, 8);
}

從上面的代碼可以看出id是根據vue文件的路徑調用node的createHash加密函數生成的,這里生成的id就是scoped生成的自定義屬性data-v-x中的x部分。

然后在createDescriptor函數中將生成的descriptor對象緩存起來,關于descriptor對象的處理就這么多了。

接著在transformMain函數中會分別以descriptor對象為參數執行genTemplateCode、genScriptCode、genStyleCode函數,分別得到編譯后的render函數、編譯后的js代碼、編譯后的style代碼。

編譯后的render函數如下圖:

從上圖中可以看到template模塊已經編譯成了render函數

編譯后的js代碼如下圖:

從上圖中可以看到script模塊已經編譯成了一個名為_sfc_main的對象,因為我們這個demo中script模塊沒有代碼,所以這個對象是一個空對象。

編譯后的style代碼如下圖:

從上圖中可以看到style模塊已經編譯成了一個import語句。

最后就是使用換行符\n將templateCode、scriptCode、stylesCode拼接起來就是vue文件編譯后的js文件啦,如下圖:

想必細心的同學已經發現有地方不對啦,這里的style模塊編譯后是一條import語句,并不是真正的css代碼。這條import語句依然還是import導入的index.vue文件,只是加了一些額外的query參數。

?vue&type=style&index=0&lang.css:這個query參數表明當前import導入的是vue文件的css部分。

還記得我們前面講過的transform鉤子函數嗎?vite每解析一個模塊都會執行一次transform鉤子函數,這個import導入vue文件的css部分,當然也會觸發transform鉤子函數的執行。

第二次執行transform鉤子函數

當在瀏覽器中執行vue文件編譯后的js文件時會觸發import "/Users/xxx/index.vue?vue&type=style&index=0&lang.css"語句的執行,導致再次執行transform鉤子函數。

transform鉤子函數代碼如下:

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函數中了。在else代碼在先執行getDescriptor函數拿到descriptor對象,getDescriptor函數代碼如下:

function getDescriptor(filename) {
  const _cache = cache;
  if (_cache.has(filename)) {
    return _cache.get(filename);
  }
}

我們在第一次執行transformMain函數的時候會去執行createDescriptor函數,他的作用是根據當前vue文件的code代碼字符串生成一個descriptor對象,并且將這個descriptor對象緩存起來了。在getDescriptor函數中就是將緩存的descriptor對象取出來。

由于query中有type=style,所以代碼會走到transformStyle函數中。

transformStyle函數

接著將斷點走進transformStyle函數,代碼如下:

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函數依然不是干活的地方,而是調用的@vue/compiler-sfc包暴露出的compileStyleAsync函數。

在調用compileStyleAsync函數的時候有三個參數需要注意:source、id和scoped。

source字段的值為code,值是當前css代碼字符串。

id字段的值為data-v-${descriptor.id},是不是覺得看著很熟悉?沒錯他就是使用scoped后vue幫我們自動生成的html自定義屬性data-v-x和css選擇屬性選擇器[data-v-x]。

其中的descriptor.id就是在生成descriptor對象時根據vue文件路徑加密生成的id。

scoped字段的值為block.scoped,而block的值為descriptor.styles[index]。由于一個vue文件可以寫多個style標簽,所以descriptor對象的styles屬性是一個數組,分包對應多個style標簽。我們這里只有一個style標簽,所以此時的index值為0。block.scoped的值為style標簽上面是否有使用scoped。

直到進入compileStyleAsync函數之前代碼其實一直都還在@vitejs/plugin-vue包中執行,真正干活的地方是在@vue/compiler-sfc包中。

@vue/compiler-sfc

接著將斷點走進compileStyleAsync函數,代碼如下:

function compileStyleAsync(options) {
  return doCompileStyle({
    ...options,
    isAsync: true,
  });
}

從上面的代碼可以看到實際干活的是doCompileStyle函數。

doCompileStyle函數

接著將斷點走進doCompileStyle函數,在我們這個場景中簡化后的doCompileStyle函數代碼如下:

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函數中首先使用const定義了一堆變量,我們主要關注source和longId。

其中的source為當前css代碼字符串,longId為根據vue文件路徑加密生成的id,值的格式為data-v-x。他就是使用scoped后vue幫我們自動生成的html自定義屬性data-v-x和css選擇屬性選擇器[data-v-x]。

接著就是判斷scoped是否為true,也就是style中使用有使用scoped。如果為true,就將scopedPlugin插件push到plugins數組中。從名字你應該猜到了這個plugin插件就是用于處理css scoped的。

最后就是執行result = postcss(plugins).process(source, postCSSOptions)拿到經過postcss轉換編譯器處理后的css。

可能有的小伙伴對postcss不夠熟悉,我們這里來簡單介紹一下。

postcss 是 css 的 transpiler(轉換編譯器,簡稱轉譯器),它對于 css 就像 babel 對于 js 一樣,能夠做 css 代碼的分析和轉換。同時,它也提供了插件機制來做自定義的轉換。

在我們這里主要就是用到了postcss提供的插件機制來完成css scoped的自定義轉換,調用postcss的時候我們傳入了source,他的值是style模塊中的css代碼。并且傳入的plugins插件數組中有個scopedPlugin插件,這個自定義插件就是vue寫的用于處理css scoped的插件。

在執行postcss對css代碼進行轉換之前我們在debug終端來看看此時的css代碼是什么樣的,如下圖:

從上圖可以看到此時的css代碼還是和我們源代碼是一樣的,并沒有css選擇屬性選擇器[data-v-x]

scopedPlugin插件

scopedPlugin插件在我們這個場景中簡化后的代碼如下:

const scopedPlugin = (id = "") => {
  return {
    postcssPlugin: "vue-sfc-scoped",
    Rule(rule) {
      processRule(id, rule);
    },
    // ...省略
  };
};

這里的id就是我們在doCompileStyle函數中傳過來的longId,也就是生成的css選擇屬性選擇器[data-v-x]中的data-v-x。

在我們這個場景中只需要關注Rule鉤子函數,當postcss處理到選擇器開頭的規則就會走到Rule鉤子函數。

我們這里需要在使用了scoped后給css選擇器添加對應的屬性選擇器[data-v-x],所以我們需要在插件中使用Rule鉤子函數,在處理css選擇器時手動給選擇器后面塞一個屬性選擇器[data-v-x]。

給Rule鉤子函數打個斷點,當postcss處理到我們代碼中的.block時就會走到斷點中。在debug終端看看rule的值,如下圖:

從上圖中可以看到此時rule.selector的值為.block,是一個class值為block的類選擇器。

processRule函數

將斷點走進processRule函數中,在我們這個場景中簡化后的processRule函數代碼如下:

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函數中就是使用postcss-selector-parser來解析一個選擇器,進行處理后返回一個新的選擇器。

processSync方法的作用為接收一個選擇器,然后在回調中對解析出來的選擇器進行處理,最后將處理后的選擇器以字符串的方式進行返回。

在我們這里processSync方法接收的選擇器是字符串.block,經過回調函數處理后返回的選擇器字符串就變成了.block[data-v-c1c19b25]。

我們接下來看selectorParser回調函數中的代碼,在回調函數中會使用selectorRoot.each去遍歷解析出來的選擇器。

為什么這里需要去遍歷呢?

答案是css選擇器可以這樣寫:.block.demo,如果是這樣的選擇器經過解析后,就會被解析成兩個選擇器,分別是.block和.demo。

在each遍歷中會調用rewriteSelector函數對當前選取器進行重寫。

rewriteSelector函數

將斷點走進rewriteSelector函數,在我們這個場景中簡化后的代碼如下:

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函數中each遍歷當前selector選擇器,給node賦值。將斷點走到each遍歷之后,我們在debug終端來看看selector選擇器和node變量。如下圖:

在這里selector是container容器,node才是具體要操作的選擇器節點。

比如我們這里要執行的selector.insertAfter方法就是在selector容器中在一個指定節點后面去插入一個新的節點。這個和操作瀏覽器DOM API很相似。

我們再來看看要插入的節點,selectorParser.attribute函數的作用是創建一個attribute屬性選擇器。在我們這里就是創建一個[data-v-x]的屬性選擇器,如下圖:

所以這里就是在.block類選擇器后面插入一個[data-v-c1c19b25]的屬性選擇器。

我們在debug終端來看看執行insertAfter函數后的selector選擇器,如下圖:

將斷點逐層走出,直到processRule函數中。我們在debug終端來看看此時被重寫后的rule.selector字符串的值是什么樣的,如下圖

原來rule.selector的值為.block,通過重寫rule.selector的值可以將.block類選擇器替換為一個新的選擇器,而這個新的選擇器是在原來的.block類選擇器后面再塞一個[data-v-c1c19b25]屬性選擇器。

總結

這篇文章我們講了當使用scoped后,vue是如何給組件內CSS選擇器添加對應的屬性選擇器[data-v-x]。主要分為兩部分,分別在兩個包里面執行。

  • 第一部分為在@vitejs/plugin-vue包內執行。

首先會根據當前vue文件的路徑進行加密算法生成一個id,這個id就是添加的屬性選擇器[data-v-x]中的x。

然后就是執行transformStyle函數,這個transformStyle并不是實際干活的地方,他調用了@vue/compiler-sfc包的compileStyleAsync函數。并且傳入了id、code(css代碼字符串)、scoped(是否在style中使用scoped)。

  • 第二部分在@vue/compiler-sfc包執行。
  • compileStyleAsync函數依然不是實際干活的地方,而是調用了doCompileStyle函數。

  • 在doCompileStyle函數中,如果scoped為true就向plugins數組中插入一個scopedPlugin插件,這個是vue寫的postcss插件,用于處理css scoped。然后使用postcss轉換編譯器對css代碼進行轉換。

  • 當postcss處理到選擇器開頭的規則就會走到scopedPlugin插件中的Rule鉤子函數中。在Rule鉤子函數中會執行processRule函數。

  • 在processRule函數中會使用postcss-selector-parser包將當前選擇器替換為一個新的選擇器,新的選擇器和原來的選擇器的區別是在后面會添加一個屬性選擇器[data-v-x]。其中的x就是根據當前vue文件的路徑進行加密算法生成的id。

責任編輯:武曉燕 來源: 前端歐陽
相關推薦

2025-02-17 09:22:16

MySQLSQL語句

2022-12-14 07:32:40

InnoDBMySQL引擎

2018-04-02 15:13:21

網絡

2023-02-15 08:17:38

2024-04-30 08:22:51

Figma圖形編輯變換矩陣

2022-05-05 08:55:12

工業物聯網IIoT

2024-02-06 09:30:25

Figma矩形矩形物理屬性

2023-05-22 15:58:11

2022-07-13 10:37:59

服務器故障優化

2020-03-23 08:30:12

程序員男友感受

2020-05-26 08:52:36

Java JVM多態

2020-11-24 06:20:02

Linux日志文件系統

2016-10-12 08:54:24

2021-02-02 09:13:11

索引SQL數據庫

2014-07-21 10:32:52

蘋果公司實習

2021-02-11 09:14:36

內存虛擬機數據

2021-08-17 07:00:00

雙重檢查鎖Nacos

2015-03-25 09:55:34

程序員程序員修補BUG真正原因

2018-10-26 10:41:19

2017-05-09 15:39:33

ensorFlow機器人機器學習
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品成人一区二区三区 | 午夜精品一区二区三区在线观看 | 欧美色综合一区二区三区 | 91就要激情 | 日韩一级在线 | 五月婷婷丁香 | 久久久久久成人 | 黄色亚洲网站 | 男女视频在线观看网站 | 成人免费视频一区 | 中文字幕爱爱视频 | 久久精品久久久久久 | 亚洲国产精品日本 | 中文字幕视频在线 | 久久婷婷麻豆国产91天堂 | 中文字幕在线一区二区三区 | 三级视频国产 | 亚洲精品天堂 | 人人澡人人射 | 免费看91 | 日韩av手机在线观看 | 午夜在线小视频 | 色888www视频在线观看 | 一级做a爰片性色毛片16 | 亚洲精品在线播放 | 欧美女优在线观看 | 一本一道久久a久久精品综合 | 久久久www成人免费无遮挡大片 | 国产精品久久久久一区二区三区 | 婷婷成人在线 | 影音先锋中文字幕在线观看 | 欧美视频1区 | 91成人免费 | 亚洲国产精品久久 | 99成人精品| 欧美综合国产精品久久丁香 | 国产精品久久久久一区二区 | 国产亚洲精品久久久久动 | 鸡毛片 | japanhdxxxx裸体 | 欧美亚洲视频在线观看 |