深入理解Vue3中style的scoped

概述

scoped的作用就是样式模块化(CSS Module),即给组件每一个元素(以及非动态添加的子组件的根元素)加上一个data-v-xxxx的属性,样式选择器也会格式化成选择器[data-v-xxxx],这样就做到了样式隔离,每个组件内定义的样式只对该组件生效,避免了不同组件或页面的样式(选择器)冲突。本文将以vue3为例,深入了解scoped原理。

scoped实践

  • vue3组件是如下定义样式:


  • 效果如下:

深入理解Vue3中style的scoped_第1张图片

scoped源码分析

compiler-sfc模块

vue3中有个模块@vue/compiler-sfc,这个模块是单独拎出来,不会被打包到vue.global.jscompiler-sfc主要作用就是用来编译单文件组件,就是.vue。因为scoped的实现是在compiler-sfc模块中,所以本文的所有的讨论也是基于SFC

viteplugin-vue

vue3如果是通过vite搭建的,那么compiler-sfc会通过viteplugin-vue调用,这在script 标签的 setup 实现原理中有讲解,可以简短回顾。

Style 样式选择器中的处理

plugin-vue中会读取.vue组件,并识别部分,如下

if (query.type === "style") {
  return transformStyle(
    code,
    descriptor,
    Number(query.index || 0),
    options.value,
    this,
    filename
  );
}

transformStyle函数会将的 code 传给compiler.compileStyleAsync,并返回编译的结果,其中参数包含idscoped

  • id的生成

    id包含于descriptor中,其生成过程如下所示:

descriptor.id = getHash(normalizedPath + (isProduction ? source : "")); // normalizedPath:序列化文件路径后的字符串,如果是生产环境,还会加上源码

function getHash(text) {
  return node_crypto
    .createHash("sha256")
    .update(text)
    .digest("hex")
    .substring(0, 8);
}
compileStyleAsync

compileStyleAsync会返回一个函数doCompileStyle,该函数会加载一些css插件对样式进行编译。

上面图中的data-v-xxxx就是在这个函数中根据参数id先用正则匹配前缀data-v-替换再加上data-v-

如果参数scopedtrue,就会执行plugins.push(scopedPlugin(longId)),plugins是一个数组,后面调用post-css库对这些plugin进行处理。如下:

postcss(plugins).process(source, postCSSOptions); // source:源代码,

scoped的核心实现就是在scopedPlugin中。

scopedPlugin

scopedPluginvue3封装的post-css插件,其实现如下:

const scopedPlugin = (id = "") => {
  const keyframes = /* @__PURE__ */ Object.create(null);
  const shortId = id.replace(/^data-v-/, "");
  return {
    postcssPlugin: "vue-sfc-scoped",
    Rule(rule) {
      processRule(id, rule);
    },
    AtRule(node) {
      if (
        /-?keyframes$/.test(node.name) &&
        !node.params.endsWith(`-${shortId}`)
      ) {
        keyframes[node.params] = node.params = node.params + "-" + shortId;
      }
    },
    OnceExit(root) {
      if (Object.keys(keyframes).length) {
        root.walkDecls((decl) => {
          if (animationNameRE.test(decl.prop)) {
            decl.value = decl.value
              .split(",")
              .map((v) => keyframes[v.trim()] || v.trim())
              .join(",");
          }
          if (animationRE.test(decl.prop)) {
            decl.value = decl.value
              .split(",")
              .map((v) => {
                const vals = v.trim().split(/\s+/);
                const i = vals.findIndex((val) => keyframes[val]);
                if (i !== -1) {
                  vals.splice(i, 1, keyframes[vals[i]]);
                  return vals.join(" ");
                } else {
                  return v;
                }
              })
              .join(",");
          }
        });
      }
    },
  };
};

scopedPlugin就是一个对象,包含三个方法:RuleAtRuleOnceExit,这是post-css插件的里面的概念。

  • Rule:表示CSS里的普通规则,比如选择器和申明。
  • AtRule:表示CSS中的@规则
  • OnceExit:用于在整个CSS文件的解析完成后执行一次操作
processRule

processRule主要就是处理一般规则。定义了一个WeakSet用于避免重复操作,过滤@规则还有keyframes

下面这段代码就是遍历了选择器中的每个部分,并通过 rewriteSelector 函数进行修改,最后将修改后的选择器转换回字符串并赋值给 rule.selector

const processedRules = /* @__PURE__ */ new WeakSet();
function processRule(id, rule) {
  if (
    processedRules.has(rule) ||
    (rule.parent &&
      rule.parent.type === "atrule" &&
      /-?keyframes$/.test(rule.parent.name))
  ) {
    return;
  }
  processedRules.add(rule);
  rule.selector = selectorParser$2((selectorRoot) => {
    selectorRoot.each((selector) => {
      rewriteSelector(id, selector, selectorRoot);
    });
  }).processSync(rule.selector); // selectorParser$2就是`postcss-selector-parser`插件
}
rewriteSelector

rewriteSelector顾名思义就是重写选择器selectorscopeddata-v-xxxx是只加在选择器的最后一个,作为它的属性。而且还要考虑一些伪类选择器::after::before等等,最后就是插入属性data-v-xxxx,操作如下

selector.insertAfter(
      node as any,
      selectorParser.attribute({
        attribute: idToAdd,
        value: idToAdd,
        raws: {},
        quoteMark: `"`,
      }),
    )`

template 元素处理

scoped在元素中的处理其实就是给元素加一个属性,同一个.vue中的元素data-v-xxxx是一样的,和style的属性选择器data-v-xxxx也是一致。

plugin-vue中就生成了id,这个id不仅会给style用,元素也是用的这个相同的id

对于templatestyle部分是调用不同的解析器进行解析的。元素的属性是在@vue/compiler-dom@vue/compiler-core这两个模块中属性,本质上就是解析语法树,生成 DOM 节点时判断scopeId是否为true

if (context.scopeId) {
  res += ` ${context.scopeId}`;
}

你可能感兴趣的:(前端,源码,#,CSS,vue.js)