4
头图

Preface

SFC Style CSS Variable Injection , that is, <style> dynamic variable injection when introducing the optimization of Single File Component (SFC) in the compilation stage. Simply put, it allows you to use the variables defined <style> in v-bind <script>

It sounds like CSS In JS? Indeed, from the perspective of use, it is very similar to CSS In JS. However, everyone knows that CSS In JS has certain performance problems in some scenarios, while <style> dynamic variable injection does not have similar problems.

So, <style> dynamic variable injection realized? I think this is a question that many students will have, so today let us thoroughly understand <style> dynamic variable injection is and what is done behind its implementation.

1 What is <style> dynamic variable injection

<style> dynamic variable injection, according to the big summary on 16141979f41ad7 SFC , it mainly has the following 5 capabilities:

  • There is no need to explicitly declare that a property is injected as a CSS variable (will be based on)
  • Responsive variables
  • Different performance in Scoped/Non-scoped mode
  • Will not pollute sub-components
  • The use of ordinary CSS variables will not be affected

Below, let's take a look at a simple example of dynamic variable injection <style>

<template>
  <p class="word">{{ msg }}</p>
  <button @click="changeColor">
    click me
  </button>
</template>

<script setup>
import { ref } from "vue"
  
const msg = 'Hello World!'
let color = ref("red")
const changeColor = () => {
  if (color.value === 'black') {
    color.value = "red"
  } else {
    color.value = "black"
  }
}
</script>

<style scoped>
  .word {
    background: v-bind(color)
  }
</style>

The corresponding rendering on the page:

From the above code snippet, it is easy to know that when we click the click me button, the background color of the text will change:

And this is <style> dynamic variable injection gives us the ability to let us very easily by <script> to manipulate the variables <template> HTML elements dynamically change the style of .

So, what happened in this process? How did it happen? It is a good thing to have questions, and then let us step by step to uncover the realization principle behind the scenes.

2 The principle of <style>

At the beginning of the article, we talked about the <style> dynamic variable injection is derived from the optimization of the single file (SFC) in the compilation stage. However, does not explain all compiled by SFC here. Students who don't know can read the article I wrote before [From the compilation process to understand the Vue3 static node promotion process]().

So, let us focus on the SFC <style> during the compilation process. First of all, there are two key points in this process.

2.1 SFC compile <style> dynamic variable injection

<style> dynamic variable injection of 06141979f41d23 during the compilation process, which is mainly based on the 2 key points . Here, we take the above example as an example analysis:

  • Bound to the corresponding DOM inline style , by CSS var() ) provided with a row in the CSS style defined above custom properties , corresponding HTML part:

    CSS part:
  • Through dynamically update color variable to realize style attribute value in line, and then change the style of HTML element that uses the CSS custom attribute

So, apparently to complete the entire process, unlike in the absence of <style> SFC compiled before the injection of dynamic variables, where the need for <style> , <script> corresponding increase in special treatment . Below, we will explain in 2 points:

SFC compilation <style> related processing

Everyone knows that the <style> part of Vue SFC is compiled mainly by postcss . This corresponds to the doCompileStyle() method packages/compiler-sfc/sfc/compileStyle.ts in the Vue source code.

Here, let's take a look at its <style> dynamic variable injection, and the corresponding code (pseudo code):

export function doCompileStyle(
  options: SFCAsyncStyleCompileOptions
): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
  const {
    ...
    id,
    ...
  } = options
  ...
  const plugins = (postcssPlugins || []).slice()
  plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
  ...
}

It can be seen in the use of postcss compile <style> will be added before cssVarsPlugin plug-ins, and to cssVarsPlugin incoming shortId (ie scopedId replace data-v results within) and isProd (whether in a production environment).

cssVarsPlugin is used postcss plug provided Declaration method , to access <style> values of all CSS properties declared, each visit to match a regular v-bind content of the command, and then use replace() the property value replacement method is var(--xxxx-xx) , Behaved like this in the above example:

cssVarsPlugin plug-in:

const cssVarRE = /\bv-bind\(\s*(?:'([^']+)'|"([^"]+)"|([^'"][^)]*))\s*\)/g
const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
  const { id, isProd } = opts!
  return {
    postcssPlugin: 'vue-sfc-vars',
    Declaration(decl) {
      // rewrite CSS variables
      if (cssVarRE.test(decl.value)) {
        decl.value = decl.value.replace(cssVarRE, (_, $1, $2, $3) => {
          return `var(--${genVarName(id, $1 || $2 || $3, isProd)})`
        })
      }
    }
  }
}

Here the CSS var() variable names, i.e. -- (after the contents) by genVarName() generation method, it will be based isProd is true or false generate different values:

function genVarName(id: string, raw: string, isProd: boolean): string {
  if (isProd) {
    return hash(id + raw)
  } else {
    return `${id}-${raw.replace(/([^\w-])/g, '_')}`
  }
}

2.SFC compilation <script> related processing

If you just stand <script> , it is obvious that 16141979f462b7 cannot perceive current SFC of <style> uses 06141979f462b1 dynamic variable injection. Therefore, it is necessary to start from the SFC to identify whether the <style> dynamic variable injection is currently used.

In packages/compiler-sfc/parse.ts in parse method will resolve SFC obtained descriptor object calls parseCssVars() method to get <style> used to v-bind all variables.

descriptor refers parsed SFC obtained comprising script , style , template object attributes, each attribute comprising an SFC in each block (Block) information, e.g. <style> properties scoped and content.

Part of the code (pseudo code) in the corresponding parse()

function parse(
  source: string,
  {
    sourceMap = true,
    filename = 'anonymous.vue',
    sourceRoot = '',
    pad = false,
    compiler = CompilerDOM
  }: SFCParseOptions = {}
): SFCParseResult {
  //...
  descriptor.cssVars = parseCssVars(descriptor)
  if (descriptor.cssVars.length) {
    warnExperimental(`v-bind() CSS variable injection`, 231)
  }
  //...
}

As you can see, the result (array) returned by the parseCssVars() descriptor.cssVars . Then, when compiling script , judge whether to inject <style> dynamic variable into related code according to descriptor.cssVars.length

<style> dynamic variable injection is used in the project, and you will see a prompt in the terminal to inform us that this feature is still in the experiment.

The compiler script by package/compile-sfc/src/compileScript.ts in compileScript completion method, where we look at it for <style> handle dynamic variable injection:

export function compileScript(
  sfc: SFCDescriptor,
  options: SFCScriptCompileOptions
): SFCScriptBlock {
  //...
  const cssVars = sfc.cssVars
  //...
  const needRewrite = cssVars.length || hasInheritAttrsFlag
  let content = script.content
  if (needRewrite) {
    //...
    if (cssVars.length) {
      content += genNormalScriptCssVarsCode(
        cssVars,
        bindings,
        scopeId,
        !!options.isProd
      )
    }
  }
  //...
}

For the example we gave earlier (using <style> dynamic variable injection), obviously cssVars.length exists, so here we will call the genNormalScriptCssVarsCode() method to generate the corresponding code.

genNormalScriptCssVarsCode() :

// package/compile-sfc/src/cssVars.ts
const CSS_VARS_HELPER = `useCssVars`
function genNormalScriptCssVarsCode(
  cssVars: string[],
  bindings: BindingMetadata,
  id: string,
  isProd: boolean
): string {
  return (
    `\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` +
    `const __injectCSSVars__ = () => {\n${genCssVarsCode(
      cssVars,
      bindings,
      id,
      isProd
    )}}\n` +
    `const __setup__ = __default__.setup\n` +
    `__default__.setup = __setup__\n` +
    `  ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }\n` +
    `  : __injectCSSVars__\n`
  )
}

genNormalScriptCssVarsCode() method mainly does these 3 things:

  • Introduce the useCssVars() method, which is mainly to monitor the variables injected dynamically by watchEffect , and then update the value of the Vars()
  • Define the __injectCSSVars__ method, which mainly calls the genCssVarsCode() method to generate the code related <style>
  • Compatible with the combined API usage in the case of <script setup> __setup__ here), if it exists, rewrite __default__.setup to (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }

So, at this point, we have roughly analyzed <style> dynamic variable injection by SFC compilation. Some of the logic has not been explained too much (to avoid falling into the situation of a doll), and interested students can understand it by themselves. Next, let's take a look at the previous example and see what will be the result of SFC compilation?

3 From the results of SFC compilation, understand the implementation details of <style>

Here, we directly use Vue's official SFC Playground to view the code output of the above example after SFC compiled

import { useCssVars as _useCssVars, unref as _unref } from 'vue'
import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from "vue"
const _withId = /*#__PURE__*/_withScopeId("data-v-f13b4d11")

import { ref } from "vue"

const __sfc__ = {
  expose: [],
  setup(__props) {

_useCssVars(_ctx => ({
  "f13b4d11-color": (_unref(color))
}))

const msg = 'Hello World!'
let color = ref("red")
const changeColor = () => {
  if (color.value === 'black') {
    color.value = "red"
  } else {
    color.value = "black"
  }
}

return (_ctx, _cache) => {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("p", { class: "word" }, _toDisplayString(msg)),
    _createVNode("button", { onClick: changeColor }, " click me ")
  ], 64 /* STABLE_FRAGMENT */))
}
}

}
__sfc__.__scopeId = "data-v-f13b4d11"
__sfc__.__file = "App.vue"
export default __sfc__

You can see the result of SFC compilation, and output the __sfc__ , render function, <style> dynamic variable injection. So putting aside the first two, we directly look at <style> dynamic variable injection:

_useCssVars(_ctx => ({
  "f13b4d11-color": (_unref(color))
}))

Here called _useCssVars() method in which the source of the middle finger is useCssVars() method, and then introduced to a function that returns an object { "f13b4d11-color": (_unref(color)) } . So, let's take a look at the useCssVars() method.

3.1 useCssVars() method

useCssVars() method is defined in runtime-dom/src/helpers/useCssVars.ts :

// runtime-dom/src/helpers/useCssVars.ts
function useCssVars(getter: (ctx: any) => Record<string, string>) {
  if (!__BROWSER__ && !__TEST__) return

  const instance = getCurrentInstance()
  if (!instance) {
    __DEV__ &&
      warn(`useCssVars is called without current active component instance.`)
    return
  }

  const setVars = () =>
    setVarsOnVNode(instance.subTree, getter(instance.proxy!))
  onMounted(() => watchEffect(setVars, { flush: 'post' }))
  onUpdated(setVars)
}

useCssVars mainly did these 4 things:

  • Get the current component instance instance , which is used for the subsequent operation of the VNode Tree of the component instance, that is, instance.subTree
  • Define the setVars() method, it will call the setVarsOnVNode() method, and the instance.subTree and the received getter() method will be passed in
  • onMounted() life cycle of watchEffect , the method setVars() will be called every time a component is mounted
  • In onUpdated() add life cycle setVars() method, when each component updates will call setVars() method

As you can see, whether it is onMounted() or onUpdated() life cycle, they will call the setVars() method, which is essentially the setVarsOnVNode() method. Let’s take a look at its definition first:

function setVarsOnVNode(vnode: VNode, vars: Record<string, string>) {
  if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
    const suspense = vnode.suspense!
    vnode = suspense.activeBranch!
    if (suspense.pendingBranch && !suspense.isHydrating) {
      suspense.effects.push(() => {
        setVarsOnVNode(suspense.activeBranch!, vars)
      })
    }
  }

  while (vnode.component) {
    vnode = vnode.component.subTree
  }

  if (vnode.shapeFlag & ShapeFlags.ELEMENT && vnode.el) {
    const style = vnode.el.style
    for (const key in vars) {
      style.setProperty(`--${key}`, vars[key])
    }
  } else if (vnode.type === Fragment) {
    ;(vnode.children as VNode[]).forEach(c => setVarsOnVNode(c, vars))
  }
}

For our chestnut, since the initial incoming is instance.subtree , its type is Fragment . So, in setVarsOnVNode() method will hit vnode.type === Fragment logic will traverse vnode.children , then continue to the recursive call setVarsOnVNode() .

FEATURE_SUSPENSE and vnode.component are not analyzed here. Those who are interested can find out by themselves

In the subsequent setVarsOnVNode() method, if the logic of vnode.shapeFlag & ShapeFlags.ELEMENT && vnode.el style.setProperty() method will be called to add the VNode to the DOM ( vnode.el ) style , where key is the value of the previous processing <style> when the CSS var() value Corresponds to the value of the variable defined in <script>

In this way, the entire <script> from the variable change in <style> to the style change in 06141979f46ac7 is completed. Here we use the legend to briefly review this process:

Concluding remarks

If you simply summarize the <style> dynamic variable injection, you may be able to express it in a few sentences. But how does it do it at the source code level? This is worthy of in-depth understanding, through which we can understand how to write the postcss plug-in, vars() and other technical points.

And, originally planned to leave a section to introduce how to write a Vite plug-in vite-plugin-vue2-css-vars so that Vue 2.x can also support <style> dynamic variable injection. However, considering that the length of the article is too long, it may cause reading obstacles for everyone. So, this will be introduced in the next article, but the plug-in has already been posted to NPM, and interested students can also learn about it by themselves.

Finally, if there are improper or wrong expressions in the text, you are welcome to mention Issue~

like

By reading this article, if you have any gains, you can , this will be my motivation to continue to share, thank you~

I'm Wuliu, I like to innovate and tinker with the source code, focusing on the source code (Vue 3, Vite), front-end engineering, cross-end technology learning and sharing. In addition, all my articles will be included in https://github.com/WJCHumble/Blog , welcome to Watch Or Star!

五柳
1.1k 声望1.4k 粉丝

你好,我是五柳,希望能带给大家一些别样的知识和生活感悟,春华秋实,年年长茂。