文章目录
一、背景
上文分析了 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
,否则会进入到 else
中 transformStyle
方法,从名字就可以看出这个转化样式的过程,因此我们放开 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
的转化过程本文就不过多深入。
三、总结
从上文可以发现 vue3
对 css
的解析其实是分为两次:
- 第一次先通过 transformMain 得到
import 'xxxx.vue?xxxx'
的方法 - 第二次因为新增了
import
语句插件又会重新执行,再次执行因为vue
已经存在了,则会进入 transformStyle 方法,在里面进行具体解析,包含scoped
等各种插件配合使用解析为最终css
文件。