Jinuss's blog Jinuss's blog
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

东流

前端可视化
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 概览

  • 响应式系统

  • runtime运行时

  • runtime-core

  • compiler编译

    • 深入style的scoped
      • 概述
      • scoped实践
      • scoped源码分析
        • compiler-sfc模块
        • vite与plugin-vue
        • Style 样式选择器中的处理
        • compileStyleAsync
        • scopedPlugin
        • processRule
        • rewriteSelector
        • template 元素处理
  • 《Vue3源码》笔记
  • compiler编译
东流
2024-09-19
目录

深入style的scoped

# 概述

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

# scoped实践

  • vue3组件是如下定义样式:
<template>
  <div class="header">
    <span>标绘管理</span>
  </div>
</template>
<style scoped>
.header span {
  position: relative;
  margin-left: 54px;
  font-size: 16px;
  display: inline-flex;
  height: 24px;
  line-height: 24px;
  font-family: "Microsoft YaHei";
  font-weight: bold;
  color: #303133;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 效果如下:

# scoped源码分析

# compiler-sfc模块

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

# vite与plugin-vue

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

# Style 样式选择器中的处理

在 plugin-vue中会读取.vue组件,并识别<style></style>部分,如下

if (query.type === "style") {
  return transformStyle(
    code,
    descriptor,
    Number(query.index || 0),
    options.value,
    this,
    filename
  );
}
1
2
3
4
5
6
7
8
9
10

transformStyle函数会将<style></style>的 code 传给compiler.compileStyleAsync,并返回编译的结果,其中参数包含id和scoped。

  • 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);
}
1
2
3
4
5
6
7
8
9

# compileStyleAsync

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

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

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

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

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

# scopedPlugin

scopedPlugin是vue3封装的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(",");
          }
        });
      }
    },
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

scopedPlugin就是一个对象,包含三个方法:Rule、AtRule和OnceExit,这是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`插件
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# rewriteSelector

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

selector.insertAfter(
      node as any,
      selectorParser.attribute({
        attribute: idToAdd,
        value: idToAdd,
        raws: {},
        quoteMark: `"`,
      }),
    )`
1
2
3
4
5
6
7
8
9

# template 元素处理

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

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

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

if (context.scopeId) {
  res += ` ${context.scopeId}`;
}
1
2
3
编辑 (opens new window)
上次更新: 2024/09/19, 07:55:41
directive自定义指令

← directive自定义指令

最近更新
01
GeoJSON
05-08
02
Circle
04-15
03
CircleMarker
04-15
更多文章>
Theme by Vdoing | Copyright © 2024-2025 东流 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式