我们提供安全,免费的手游软件下载!

安卓手机游戏下载_安卓手机软件下载_安卓手机应用免费下载-先锋下载

当前位置: 主页 > 软件教程 > 软件教程

掉了两根头发后,我悟了!vue3的scoped原来是这样避免样式污染(上)

来源:网络 更新时间:2024-06-27 09:31:41

前言

众所周知,在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
关注公众号:【前端欧阳】,给自己一个进阶vue的机会

看个demo

我们先来看个demo,代码如下:




经过编译后,上面的demo代码就会变成下面这样:




从上面的代码可以看到在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文件中的