Vue 3 魔法揭秘:CSS 解析与 scoped 背后的奇幻之旅

文章目录

    • 一、背景
    • 二、源码分析
      • [transformMain 返回值](#transformMain 返回值)
      • [transformStyle 方法](#transformStyle 方法)
      • [compileStyleAsync 方法](#compileStyleAsync 方法)
      • [scopedPlugin 方法](#scopedPlugin 方法)
      • [template 添加 __scopeId](#template 添加 __scopeId)
    • 三、总结

一、背景

Vue 3 文件编译流程详解与 Babel 的使用

上文分析了 vue3 的编译过程,但是在对其中样式的解析遗留了一些问题:

  • 为什么 genStyleCode 得到了 import 语句 ?我们真正的代码是怎么转化的?
  • 平时vue scoped是怎么实现样式隔离的?
  • 如下图标签/选择器上的唯一属性怎么加上去的?

带着这些疑问继续进行源码解析。

二、源码分析

书接上回我们发现了 transformMain 方法中 genStyleCode 会处理我们的为import "/Users/zcy/Desktop/毕设/smart-port/src/App.vue?vue&type=style&index=0&lang.less" 那是怎么翻译成具体的 css 的呢 ?

transformMain 返回值

我们先看一下 transformMain 方法把转码转化为了什么? 下面是转化后格式化完成后的代码:

js 复制代码
// 源码script部分
import { ref } from 'vue'; const _sfc_main = {
  setup(__props, { expose }) {
    expose(); const state = ref(1)
    const __returned__ = { state, ref }
    Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
    return __returned__
  }
}
import {
  resolveComponent as _resolveComponent, createVNode as _createVNode, toDisplayString as _toDisplayString,
  createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock,
  createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId
} from "vue"
const _withScopeId = n => (_pushScopeId("data-v-7ba5bd90"), n = n(), _popScopeId(), n)
const _hoisted_1 = { id: "nav" }
// 源码template部分
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_router_link = _resolveComponent("router-link")
  const _component_router_view = _resolveComponent("router-view")
  return (_openBlock(),
    _createElementBlock(_Fragment, null,
      [_createElementVNode("div", _hoisted_1,
        [_createVNode(_component_router_link, { to: "/login" }),
        _createVNode(_component_router_link, { to: "/" }),
        _createElementVNode("div", null, _toDisplayString($setup.state), 1 /* TEXT */)]),
      _createVNode(_component_router_view)], 64 /* STABLE_FRAGMENT */)
  )
}

// 源码样式部分
import "/Users/zcy/Desktop/毕设/smart-port/src/App.vue?vue&type=style&index=0&scoped=true&lang.less"

_sfc_main.__hmrId = "7ba5bd90"
typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)
import.meta.hot.accept(({ default: updated, _rerender_only }) => { if (_rerender_only) { __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render) } else { __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated) } })
import _export_sfc from 'plugin-vue:export-helper'
export default /*#__PURE__*/_export_sfc(_sfc_main, [['render', _sfc_render], ['__scopeId', "data-v-7ba5bd90"], ['__file', "/Users/zcy/Desktop/毕设/smart-port/src/App.vue"]])

上面这段代码核心三部分 _sfc_render、 styles,_sfc_main,可以看到这里对样式的解析其实只转化为了一个 import 方法,那是怎么会转化真正的 css 的呢 ?这个时候我们在回到 vuePlugin 入口处的 transform 方法中,如下图:

transformStyle 方法

从上图中可以看在 vue 不存在的时候会进入 transformMain ,否则会进入到 elsetransformStyle 方法,从名字就可以看出这个转化样式的过程,因此我们放开 transformMain 的断点,在 transformStyle 打上断点并进入该方法。

compileStyleAsync 方法

进入到改方法后我们可以看到内部在执行了 compileStyleAsync 方法, 之后样式就已经加上了隔离,由 #app -> #app[data-v-7ba5bd90], 因此我们深入一下 options.compiler.compileStyleAsync 这个方法,根据我们上一篇文得知,这个方法是在 vue/compiler-sfc 核心包中,我们打个断点进入该方法。

compileStyleAsync 执行了 doCompileStyle ,接下来我简单摘要一下这个方法:

js 复制代码
function doCompileStyle(options) {
    const { filename, id, scoped = false, trim = true, isProd = false, modules = false, modulesOptions = {}, preprocessLang, postcssOptions, postcssPlugins } = options;
    // scoped id 来自于 descriptor.id
    const shortId = id.replace(/^data-v-/, '');
    const longId = `data-v-${shortId}`;
    // 插件数组
    const plugins = (postcssPlugins || []).slice();
    plugins.unshift(cssVarsPlugin({ id: shortId, isProd }));
    // scoped 存在则加入改插件
    if (scoped) {
        plugins.push(scopedPlugin(longId));
    }
    let result;
    let code;
    try {
        // postcss 处理这些插件
        result = _postcss__default(plugins).process(source, postCSSOptions);
        // In async mode, return a promise.
        if (options.isAsync) {
            return result
                .then(result => ({
                code: result.css || '',
                // xxx
            }))
        }
        code = result.css;
    }
    return {
        code: code || ``,
        map: outMap && outMap.toJSON(),
        // xxxx
    };
}

scopedPlugin 方法

这边可以看到使用了 postcss 加载各种插件,其中就有 scopedPlugin ,顾名思义就是给我添加样式隔离的,接下来我们进入这个方法看一眼:


  • scopedPlugin 中的 Rule 配置会调用 processRule 方法并传入 scopedId
  • processRule 方法会遍历每个选择器进行执行 rewriteSelector 操作
  • rewriteSelector 方法会处理 v-deep、 >>>、 /deep/、等特殊操作符,最后加上 给选择器加上 scopedId 属性

如上图所示会在该方法中对 css 选择器加上隔离 __scopeId

template 添加 __scopeId

选择器的样式加上了 那我们 template 中的属性什么时候加上呢?

回到我们最开始 transformMain 中 返回的 code 当中,我们可以看到源码的 template 转化为了 render 函数,并且传入了 __scopeId 我们不难猜到肯定会在 调用 _createElementBlock 等方法的时候会转化为 vdom,最后更新到 dom 属性中, 对于 vdom 的转化过程本文就不过多深入。

三、总结

从上文可以发现 vue3css 的解析其实是分为两次:

  • 第一次先通过 transformMain 得到 import 'xxxx.vue?xxxx' 的方法
  • 第二次因为新增了import 语句插件又会重新执行,再次执行因为 vue 已经存在了,则会进入 transformStyle 方法,在里面进行具体解析,包含 scoped 等各种插件配合使用解析为最终 css 文件。
相关推荐
家里有只小肥猫21 分钟前
uniApp小程序保存canvas图片
前端·小程序·uni-app
前端大全23 分钟前
Chrome 推出全新的 DOM API,彻底革新 DOM 操作!
前端·chrome
八角丶35 分钟前
元素尺寸的获取方式及区别
前端·javascript·html
冴羽43 分钟前
Svelte 最新中文文档教程(16)—— Context(上下文)
前端·javascript·svelte
前端小臻1 小时前
关于css中bfc的理解
前端·css·bfc
前端熊猫1 小时前
CSS Grid 布局学习笔记
css·笔记·学习·grid
Ama_tor1 小时前
网页制作05-html,css,javascript初认识のhtml表格的创建
javascript·css·html
白嫖不白嫖1 小时前
网页版的俄罗斯方块
前端·javascript·css
HappyAcmen1 小时前
关于Flutter前端面试题及其答案解析
前端·flutter
饼干饿死了1 小时前
实现动态翻转时钟效果的 HTML、CSS 和 JavaScript,附源码
javascript·css·html