preface

I think everyone is familiar with Vue’s Scope CSS, but when it comes to the principle of Vue’s Scope CSS implementation, many people might say that it is not to add properties to HTML, CSS 🙃.

That’s true, but that’s just the result of the final Scope CSS rendering. How does this process work? I don’t think there are many people who can answer one or two.

So, back to today’s article, I will focus on the following three points, from the final rendering results of Vue’s Scope CSS, to explore the underlying principles of its implementation:

  • What is Scope CSS
  • Vue-loader handles components (.vue files)
  • ScopeID is applied in Patch phase to generate HTML attributes

1 What is Scope CSS

Scope CSS, or scoped CSS, is an integral part of componentization. Scope CSS allows us to define CSS in components without polluting them. For example, let’s define a component in Vue:

<! -- App.vue --> <template> <div class="box">scoped css</div> </template> <script> export default {}; </script> <style scoped> .box { width: 200px; height: 200px; background: #aff; } </style>

Usually, in the development environment, our components will be processed by the Vue-loader and then rendered to the page with the framework code at runtime. Correspondingly, their HTML and CSS counterparts would look like this:

HTML part:

<div data-v-992092a6>scoped css</div>

The CSS part:

.box[data-v-992092a6] {
  width: 200px;
  height: 200px;
  background: #aff;
}

As you can see, the essence of Scope CSS is based on the properties of HTML and CSS selectors, by adding data-v-xxxx attributes to the HTML tag and CSS selector, respectively.

2 Vue-loader processing components (.vue file)

Earlier, we also mentioned that in the development environment a component (.vue file) will be processed by the vue-loader first. So, for Scope CSS, Vue-Loader does these three things:

  • Parse the component and extracttemplate,script,styleThe corresponding code block
  • Construct and exportexportComponent instance, binding ScopId to the option of the component instance
  • rightstyleThe CSS code is compiled and converted, and the properties of the selector are generated by applying Scopid

Note, here is the vue – loader for the vue file processing part, does not involve HMR, cooperate Devtool logic, interested students can understand ~

However, the main reason why the vue-loader has so much power is that the underlying vue-loader uses the package @vue/component-compiler-utils that is officially provided by Vue. It provides parsing components (.vue file), compilation template, compilation style and other three capabilities.

So, let’s take a look at how the Vue-loader uses @Vue/Component-Compiler-Utils to parse components to extract template, script, and style.

2.1 Extract template, script, style

The Vue-loader extracts template, script, and style using the @Vue /component-compiler-utils package parse method. The corresponding code (pseudocode) will look like this:

// vue-loader/lib/index.js
const { parse } = require("@vue/component-compiler-utils");

module.exports = function (source) {
  const loaderContext = this;
  const { sourceMap, rootContext, resourcePath } = loaderContext;
  const sourceRoot = path.dirname(path.relative(context, resourcePath));
  const descriptor = parse({
    source,
    compiler: require("vue-template-compiler"),
    filename,
    sourceRoot,
    needMap: sourceMap,
  });
};

Let’s examine this code point by point. First, we get the current context LoaderContext, which contains the core Webpack packaging objects Compiler, Compilation, and so on.

Next, build the source entry sourceRoot, which normally refers to the SRC file directory and is mainly used to build the source-map.

Finally, the parse method provided by @Vue /component-compiler-utils is used to parse the source (component code). Here, let’s look at some of the parameters of the parse method:

  • soruceA block of source code, where the component’s corresponding code is containedtemplate,style,script
  • compilerCompile the core object, which is a CommonJS module (vue-template-compiler),parseIt is used internally by the methodparseComponentMethod to parse the component
  • filenameThe file name of the current component, for exampleApp.vue
  • sourceRootFile resource entry, used for buildsource-mapuse
  • needMapIf you needsource-map.parseThe internal method is based onneedMapThe value of (truefalseBy default,true) to determine whether it is generatedscript,styleThe correspondingsource-map

Execution of the parse method returns an object to desciptor that contains the template, style, and script blocks.

What you can see, then, is that the Vue-loader resolves the components, almost outsourced to the Vue-supplied package. And, I think at this time there will certainly be students asked: these and Vue’s Scope CSS has a few cents relationship 🙃?

It matters a lot! Because Vue’s Scope CSS is not made without bricks, it is implemented only if the component is parsed and the template and style sections are handled separately!

So, at this point we’ve obviously finished parsing the component. Next, you need to construct and export the component instance ~

2.2 Construct and export component instances

After parsing the component, the Vue-Loader processes and generates template, script, and style import import statements, and then calls the Normalizer method to normalize the component, and finally splits them into a code string:

let templateImport = `var render, staticRenderFns`; If (descriptor. Template) {let scriptPort = 'var script = {}'; If (descriptor. Script) {let stylesCode = ' '; If (descriptor.styles.length) {let code = '${templateImport} ${scriptImport} ${stylesCode} import normalizer from ${stringifyRequest(`! ${componentNormalizerPath}`)} var component = normalizer( script, render, staticRenderFns, ${hasFunctional ? `true` : `false`}, ${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`}, ${hasScoped ? JSON.stringify(id) : `null`}, ${isServer ? JSON.stringify(hash(request)) : `null`} ${isShadow ? `,true` : ``} ) `.trim() + `\n`;

The template, script, style sections of the TemplateImport, ScriptImport, StylesCode, etc. import import statements would look like this:

import { render, staticRenderFns, } from "./App.vue? vue&type=template&id=7ba5bd90&scoped=true&"; import script from "./App.vue? vue&type=script&lang=js&"; // export * from "./ app.vue? vue&type=script&lang=js&"; import style0 from "./App.vue? vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css&";

(id=7ba5bd90&scoped=true); (id=7ba5bd90&scoped=true) This means that the component’s template and style require Scope CSS at this time, and the scopeId is 7ba5bd90.

Of course, this is just the first step in telling subsequent template and style compilations that you need to pay attention to generating Scope CSS! Then, the normalizer method is called to normalize the component:

import normalizer from "! . /node_modules/vue-loader/lib/runtime/componentNormalizer.js"; var component = normalizer( script, render, staticRenderFns, false, null, "7ba5bd90", null ); export default component.exports;

Pay attention to,
normalizerRename the original method
normalizeComponent, hereinafter referred to as
normalizeComponent~

I think you’ve all noticed that the scopeId is passed as a parameter to the NormalizeComponent method, and the purpose of passing it is to bind the scopeId on the options of the component instance. So, let’s look at the normalizeComponent method (pseudocode) :

function normalizeComponent (
  scriptExports,
  render,
  staticRenderFns,
  functionalTemplate,
  injectStyles,
  scopeId,
  moduleIdentifier, /* server only */
  shadowMode /* vue-cli only */
) {
  ...
  var options = typeof scriptExports === 'function'
    ? scriptExports.options
    : scriptExports
  // scopedId
  if (scopeId) {
    options._scopeId = 'data-v-' + scopeId
  }
  ...
}

As you can see, options._scopeid is equal to data-v-7ba5bd90, which is used to add data-v-7ba5bd90 to the HTML tag of the current component at patch time. Therefore, this is why the template forms the real place with scopeId!

2.3 Compile Style and apply ScopId to generate selector properties

After constructing the import statement corresponding to Style, since the query in the import statement contains VUE at this time, it will be processed by the Pitching loader inside the VUE-loader. The Pitching Loader will override the import statement and concatenate the inline Loader, which would look something like this:

export * from ' "-! . /node_modules/vue-style-loader/index.js?? ref--6-oneOf-1-0 ! . /node_modules/css-loader/dist/cjs.js?? ref--6-oneOf-1-1 ! . /node_modules/vue-loader/lib/loaders/stylePostLoader.js ! . /node_modules/postcss-loader/src/index.js?? ref--6-oneOf-1-2 ! . /node_modules/cache-loader/dist/cjs.js?? ref--0-0 ! . /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./App.vue? vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css&" '

Webpack will then parse out the required loaders for the module, and obviously there will be 6 loaders resolved here:

[
  { loader: "vue-style-loader", options: "?ref--6-oneOf-1-0" },
  { loader: "css-loader", options: "?ref--6-oneOf-1-1" },
  { loader: "stylePostLoader", options: undefined },
  { loader: "postcss-loader", options: "?ref--6-oneOf-1-2" },
  { loader: "cache-loader", options: "?ref--0-0" },
  { loader: "vue-loader", options: "?vue-loader-options" }
]

At this point, Webpack will execute the six loaders (and the parsing module itself, of course). Also, the Normal Loader in webpack.config.js that conforms to the rule is ignored (vue-style-loader also ignores the front-loader).

For those of you who don’t know about inline Loader, you can take a look at this article
Have you really mastered the loader? 10 q – loader

For Scope CSS, the core is StylePostLoader. Now let’s look at the definition of StylePostLoader:

const { compileStyle } = require("@vue/component-compiler-utils");
module.exports = function (source, inMap) {
  const query = qs.parse(this.resourceQuery.slice(1));
  const { code, map, errors } = compileStyle({
    source,
    filename: this.resourcePath,
    id: `data-v-${query.id}`,
    map: inMap,
    scoped: !!query.scoped,
    trim: true,
  });

  if (errors.length) {
    this.callback(errors[0]);
  } else {
    this.callback(null, code, map);
  }
};

From the definition of StylePostLoader, we know that it uses the compileStyle method provided by @vue/component-compiler-utils to complete the compilation of the component style. Also, the parameter id is passed as data-v-${query.id}, which is data-v-7ba5bd90, and this is the key point where the selector property declared in the style is scopeId!

Inside the compileStyle function, the familiar postCSS is used to compile the style code and construct the scopeId property of the selector. How to use PostCSS to accomplish this process is not explained in detail here

3. HTML attributes are generated by using ScopeID in Patch stage

If you remember from 3.2, binding _scopeId to the options of the instance is the key to implementing a template Scope! However, what was not explained at the time was how exactly this _scopeId is applied to 😲 of the element on the template?

If you want to find the answer in the vue-loader or @vue/component-compiler-utils code, I can tell you that you will not find it in 10,000 years! Because the actual application of _scopeId occurs in the framework code at the Vue runtime (surprise 😵).

For those of you who know how to compile a Vue template, I think you all know that the template will be compiled into a render function, and then the corresponding VNode will be created according to the render function. Finally, the VNode will render the DOM into the real DOM on the page:

However, the process of VNode to real DOM is completed by patch method. Assuming that we are rendering the DOM for the first time, the patch method will hit isUndef(oldvNode) as true:

function patch (oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } 
}

Because the DOM is rendered for the first time, there’s nothing there, right
oldVnode 😶

As you can see, the createElm method is executed. The createElm method, on the other hand, creates the actual DOM for the VNode, and it does one very important thing: it calls the setScope method and applies _scopeId to generate the data-v-xxx property on the DOM! Corresponding code (pseudocode) :

// packages/src/core/vdom/patch.js function createElm( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { ... setScope(vnode); . }

In the setScope method, the options._scopeid of the component instance is added to the DOM as an attribute, resulting in a property named data-v-xxx on the HTML tag in the template. Also, this process is done by the VUE wrapped utility function nodeops.setStyleScope, which essentially calls the DOM object’s setAttribute method:

// src/platforms/web/runtime/node-ops.js
export function setStyleScope (node: Element, scopeId: string) {
  node.setAttribute(scopeId, '')
}

conclusion

For those of you who have looked up online articles about Vue-Loader and Scope CSS, Many articles have written about applying scopeId to the compilerTemplate method of the @vue/component-compiler-utils package to generate the properties of the HTML tag in the template. However, as you can see from reading this article, there is absolutely no relationship between the two (except in the case of SSR)!

Also, as you may have noticed, the code for the Vue runtime framework mentioned in this article is Vue 2.x (not Vue3). So, if you are interested, you can use the route provided in this article to review the process of Scope CSS in VUE3, I believe you will be very fruitful 😎. Finally, if there is something wrong in this paper, please mention it

Thumb up 👍

If you get anything from reading this article, you can like it. It will be my motivation to keep sharing. Thank you

I am Wu Liu, like innovation, tinkering with source code, focus on source code (VUE 3, VET), front-end engineering, cross-end technology learning and sharing. In addition, all my articles will be included in
https://github.com/WJCHumble/Blog, welcome Watch Or Star!