Skip to content

Component Rendering

In the previous chapters, we discussed how local files are transformed based on their file types before being sent to the browser. Let's observe the actual file content loaded in the browser. We can see that while we appear to be loading a .vue component, the actual file content is still a traditional .js file, and the Content-Type is application/javascript; charset=utf-8, which allows the browser to run the file directly. We can also notice that different file types have different query type parameters, such as ?type=template and ?type=import. Let's analyze how a Vue component is rendered in the browser.

Processing CSS Files

Browsers do not support directly importing .css files. If you have configured webpack to handle CSS files, you should be familiar with the solutions to this type of problem - either compiling CSS into JS files or extracting CSS from components into separate CSS files for loading via link tags. Vite uses the first approach during local development, while in production builds, it still compiles into independent CSS files for loading.

Mounting Styles

Vite uses the serverPluginCss plugin to handle requests like http://localhost:3000/src/index.css?import that end with .css suffix and contain import in the query.

ts
export function codegenCss(
  id: string,
  css: string,
  modules?: Record<string, string>
): string {
  let code =
    `import { updateStyle } from "${clientPublicPath}"\n` +
    `const css = ${JSON.stringify(css)}\n` +
    `updateStyle(${JSON.stringify(id)}, css)\n`;
  if (modules) {
    code += `export default ${JSON.stringify(modules)}`;
  } else {
    code += 'export default css';
  }
  return code;
}
// src/node/server/serverPluginCss.ts
const id = JSON.stringify(hash_sum(ctx.path));
if (isImportRequest(ctx)) {
  const { css, modules } = await processCss(root, ctx); // Here we mainly do some preprocessing operations on CSS files like less->css, postcss, etc. These operations won't be detailed here
  console.log(modules);
  ctx.type = 'js';
  ctx.body = codegenCss(id, css, modules);
}

In the code above, we intercept CSS file requests and rewrite the response. We convert CSS files into ES module format JS files. If CSS modules are enabled, we export a specific object because components need to reference it using the form :id=styles.xxx. For regular CSS files, there's no need to export a meaningful object. The core method here is updateStyle, let's see what this method actually does.

updateStyle

From the detailed response information of the http://localhost:3000/src/index.css?import request, we can see that Vite actually mounts CSS strings to specific DOM elements through the updateStyle method

js
export function updateStyle(id: string, content: string) {
  let style = sheetsMap.get(id);
  if (supportsConstructedSheet && !content.includes('@import')) {
    if (style && !(style instanceof CSSStyleSheet)) {
      removeStyle(id);
      style = undefined;
    }

    if (!style) {
      style = new CSSStyleSheet();
      style.replaceSync(content);
      document.adoptedStyleSheets = [...document.adoptedStyleSheets, style];
    } else {
      style.replaceSync(content);
    }
  } else {
    if (style && !(style instanceof HTMLStyleElement)) {
      removeStyle(id);
      style = undefined;
    }

    if (!style) {
      style = document.createElement('style');
      style.setAttribute('type', 'text/css');
      style.innerHTML = content;
      document.head.appendChild(style);
    } else {
      style.innerHTML = content;
    }
  }
  sheetsMap.set(id, style);
}

The core API used in updateStyle is CSSStyleSheet. First, we check in supportsConstructedSheet whether the current browser supports CSSStyleSheet. If not supported, we mount styles using style tags. If supported, we create a CSSStyleSheet instance. Then we pass the compiled CSS string to the CSSStyleSheet instance object. Finally, we add this object to document.adoptedStyleSheet to make our styles take effect.

Parsing Vue Files

Vite parses .vue files mainly by doing code transform to parse Vue components into JS files that call compile and render methods. The core logic of compile and render is still completed in vue3's API.

js
// src/node/server/serverPluginVue.ts
const descriptor = await parseSFC(root, filePath, ctx.body);

First, use the official library to compile single file components into descriptor. Take App.vue as an example, here we take out the important information and omit sourcemap information.

json
// src/App.vue
{
  filename: '/Users/yuuang/Desktop/github/vite_test/src/App.vue',
  source: '<template>\n' +
    '  <img alt="Vue logo" src="./assets/logo.png" :class="style.big"/>\n' +
    '  <div class="small">\n' +
    '    small1\n' +
    '  </div>\n' +
    '  <HelloWorld msg="Hello Vue 3.0 + Vite" />\n' +
    '</template>\n' +
    '\n' +
    '<script>\n' +
    "import HelloWorld from './components/HelloWorld.vue'\n" +
    "import style from './index.module.css'\n" +
    '\n' +
    'export default {\n' +
    "  name: 'App',\n" +
    '  components: {\n' +
    '    HelloWorld\n' +
    '  },\n' +
    '  data() {\n' +
    '    return {\n' +
    '      style: style\n' +
    '    }\n' +
    '  },\n' +
    '  mounted () {\n' +
    "    console.log('mounted')\n" +
    '  }\n' +
    '}\n' +
    '</script>\n' +
    '\n' +
    '<style>\n' +
    '.small {\n' +
    '  width:21px\n' +
    '}\n' +
    '</style>\n' +
    '\n',
  template: {
    type: 'template',
    content: '\n' +
      '  <img alt="Vue logo" src="./assets/logo.png" :class="style.big"/>\n' +
      '  <div class="small">\n' +
      '    small1\n' +
      '  </div>\n' +
      '  <HelloWorld msg="Hello Vue 3.0 + Vite" />\n',
    loc: {
      source: '\n' +
        '  <img alt="Vue logo" src="./assets/logo.png" :class="style.big"/>\n' +
        '  <div class="small">\n' +
        '    small1\n' +
        '  </div>\n' +
        '  <HelloWorld msg="Hello Vue 3.0 + Vite" />\n',
      start: [Object],
      end: [Object]
    },
    attrs: {},
    map: xxx
  },
  script: {
    type: 'script',
    content: '\n' +
      "import HelloWorld from './components/HelloWorld.vue'\n" +
      "import style from './index.module.css'\n" +
      '\n' +
      'export default {\n' +
      "  name: 'App',\n" +
      '  components: {\n' +
      '    HelloWorld\n' +
      '  },\n' +
      '  data() {\n' +
      '    return {\n' +
      '      style: style\n' +
      '    }\n' +
      '  },\n'
      '}\n',
    loc: {
      source: '\n' +
        "import HelloWorld from './components/HelloWorld.vue'\n" +
        "import style from './index.module.css'\n" +
        '\n' +
        'export default {\n' +
        "  name: 'App',\n" +
        '  components: {\n' +
        '    HelloWorld\n' +
        '  },\n' +
        '  data() {\n' +
        '    return {\n' +
        '      style: style\n' +
        '    }\n' +
        '  },\n'
        '}\n',
      start: [Object],
      end: [Object]
    },
    attrs: {},
    map: xxx
  },
  scriptSetup: null,
  styles: [
    {
      type: 'style',
      content: '\n.small {\n  width:21px\n}\n',
      loc: [Object],
      attrs: {},
      map: [Object]
    }
  ],
  customBlocks: []
}

Through the above code, we can parse a component's descriptor. After getting the parsing result, continue to judge

js
if (!query.type) {
  // watch potentially out of root vue file since we do a custom read here
  watchFileIfOutOfRoot(watcher, root, filePath);

  if (descriptor.script && descriptor.script.src) {
    filePath = await resolveSrcImport(
      root,
      descriptor.script,
      ctx,
      resolver
    );
  }
  ctx.type = 'js';
  const { code, map } = await compileSFCMain(
    descriptor,
    filePath,
    publicPath,
    root
  );
  ctx.body = code;
  ctx.map = map;
  return etagCacheCheck(ctx);
}

First, we judge whether the script tag has an src attribute. For example, <script src="./app.js"></script> is separated into a file in Vue. This method is rarely used in Vue and is not recommended. Vue advocates single file component writing. Next, it's the core compileSFCMain method

js
if (script) {
  content = script.content;
  map = script.map;
  if (script.lang === 'ts') {
    const res = await transform(content, publicPath, {
      loader: 'ts'
    });
    content = res.code;
    map = mergeSourceMap(map, JSON.parse(res.map!));
  }
}
/**
 * Utility for rewriting `export default` in a script block into a varaible
 * declaration so that we can inject things into it
 */
code += rewriteDefault(content, '__script');

First, we judge whether it's a ts file. If it is, we call esbuild to convert ts to js. Then we use rewriteDefault to rewrite the export default content to a variable. Here it's __script. After that, the code content is

js
import HelloWorld from './components/HelloWorld.vue';
import style from './index.module.css';

const __script = {
  name: 'App',
  components: {
    HelloWorld
  },
  data() {
    return {
      style
    };
  }
};

import HelloWorld from './components/HelloWorld.vue';
import style from './index.module.css';

const __script = {
  name: 'App',
  components: {
    HelloWorld
  },
  data() {
    return {
      style
    };
  }
};

import { render as __render } from '/src/App.vue?type=template';
__script.render = __render;
__script.__hmrId = '/src/App.vue';
__script.__file = '/Users/yuuang/Desktop/github/vite_test/src/App.vue';
export default __script;

Next, we judge whether there's a style tag in the component

js
if (descriptor.styles) {
  descriptor.styles.forEach((s, i) => {
    const styleRequest = publicPath + `?type=style&index=${i}`;
    if (s.scoped) hasScoped = true;
    if (s.module) {
      if (!hasCSSModules) {
        code += '\nconst __cssModules = __script.__cssModules = {}';
        hasCSSModules = true;
      }
      const styleVar = `__style${i}`;
      const moduleName = typeof s.module === 'string' ? s.module : '$style';
      code += `\nimport ${styleVar} from ${JSON.stringify(
        styleRequest + '&module'
      )}`;
      code += `\n__cssModules[${JSON.stringify(moduleName)}] = ${styleVar}`;
    } else {
      code += `\nimport ${JSON.stringify(styleRequest)}`;
    }
  });
  if (hasScoped) {
    code += `\n__script.__scopeId = "data-v-${id}"`;
  }
}

First, we judge whether css-modules are used. For example, <style module> xxx </style> If used, we inject \nconst __cssModules = __script.__cssModules = {} in the code and insert import $style from './index.module.css' code in the script header. This way, we can directly use $style.xxx in the template. If it's a regular style file, we inject \nimport ${JSON.stringify(styleRequest)} directly. In App.vue, it's import "/src/App.vue?type=style&index=0"

js
if (descriptor.template) {
  const templateRequest = publicPath + '?type=template';
  code += `\nimport { render as __render } from ${JSON.stringify(
    templateRequest
  )}`;
  code += '\n__script.render = __render';
}

Next, we inject type as template import script to make request. In App.vue, it's import {render as __render} from "/src/App.vue?type=template"
Finally, inject some auxiliary information

js
code += `\n__script.__hmrId = ${JSON.stringify(publicPath)}`;
code += `\n__script.__file = ${JSON.stringify(filePath)}`;
code += '\nexport default __script';

The final complete information is

js
import HelloWorld from '/src/components/HelloWorld.vue';
import style from '/src/index.module.css?import';

const __script = {
  name: 'App',
  components: {
    HelloWorld
  },
  data() {
    return {
      style
    };
  }
};

import '/src/App.vue?type=style&index=0';
import { render as __render } from '/src/App.vue?type=template';
__script.render = __render;
__script.__hmrId = '/src/App.vue';
__script.__file = '/Users/yuuang/Desktop/github/vite_test/src/App.vue';
export default __script;

After getting the compileSFCMain return result, return to ctx.body rendering. Then, since we divided the request type into template, style, let's see how we handle various types of requests.

js
if (query.type === 'template') {
  const templateBlock = descriptor.template!;
  if (templateBlock.src) {
    filePath = await resolveSrcImport(root, templateBlock, ctx, resolver);
  }
  ctx.type = 'js';
  const cached = vueCache.get(filePath);
  const bindingMetadata = cached && cached.script && cached.script.bindings;
  const vueSpecifier = resolveBareModuleRequest(
    root,
    'vue',
    publicPath,
    resolver
  );
  const { code, map } = compileSFCTemplate(
    root,
    templateBlock,
    filePath,
    publicPath,
    descriptor.styles.some((s) => s.scoped),
    bindingMetadata,
    vueSpecifier,
    config
  );
  ctx.body = code;
  ctx.map = map;
  return etagCacheCheck(ctx);
}

When type is template, we only need to get descriptor's template field. This method is rarely used.
The core method call is compileSFCTemplate to compile template into the official h function (render function) to generate vnode for rendering.

js
if (query.type === 'style') {
  const index = Number(query.index);
  const styleBlock = descriptor.styles[index];
  if (styleBlock.src) {
    filePath = await resolveSrcImport(root, styleBlock, ctx, resolver);
  }
  const id = hash_sum(publicPath);
  const result = await compileSFCStyle(
    root,
    styleBlock,
    index,
    filePath,
    publicPath,
    config
  );
  ctx.type = 'js';
  ctx.body = codegenCss(`${id}-${index}`, result.code, result.modules);
  return etagCacheCheck(ctx);
}

Type style request is similar. We only need to use descriptor's styles field. Use compileSFCStyle for some @import keyword processing. Finally, use codegenCss to wrap it up with the client provided updateStyle method.

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (dbcbf17)