As we know, Vue recommends the use of Single File Component (SFC), which is arguably a feature of the Vue framework.

However, it is not easy to use THE SFC mode of Vue in a very simple way in a regular HTML file or in a simple Playground (such as JSBin or CodePen) when we are learning and practicing.

Therefore, Vue officially provides special SFC Playground for everyone to learn Vue.

However, is there a way to use VUE-SFC without SFC Playground in a local single HTML file or on platforms like CodePen and JSBin?

There are ways to do this. Let me show you an example:

This is a Vue component written in CodePen

How does this work?

It’s really a three-step process.

The first step is to embed SFC content

The first step is to embed vuE-SFC components inline in plain HTML files. The trouble here is that the SFC contains HTML tags, as well as

Let’s see if we can put the contents of the SFC into an element that does not parse the HTML content, such as the

In fact, there is a less obvious, simpler tag, which is

Step 2 Compile the SFC component

Next, we compile the SFC component. This can be done with the official vue/ compile-SFC module.

How compile- SFC is used is very simple in the official documentation, but this does not prevent us from studying @vitejs/plugin-vue and the webpack plugin vue-loader to find out how it is used. Parse the source code into descriptor objects, and compile script, template, and styles one by one. Finally, these modules are put together again. If compatibility is not considered, the easiest way is to directly use ES-Module to put together.

The following is the core code for compiling SFC.

import * as compiler from '@vue/compiler-sfc';

function generateID() {
  return Math.random().toString(36).slice(2.12);
}

function transformVueSFC(source, filename) {
  const {descriptor, errors} = compiler.parse(source, {filename});
  if(errors.length) throw new Error(errors.toString());
  const id = generateID();
  const hasScoped = descriptor.styles.some(e= > e.scoped);
  const scopeId = hasScoped ? `data-v-${id}` : undefined;
  const templateOptions = {
    id,
    source: descriptor.template.content,
    filename: descriptor.filename,
    scoped: hasScoped,
    slotted: descriptor.slotted,
    compilerOptions: {
      scopeId: hasScoped ? scopeId : undefined.mode: 'module',}};const script = compiler.compileScript(descriptor, {id, templateOptions, sourceMap:true});
  if(script.map) {
    script.content = `${script.content}\n//# sourceMappingURL=data:application/json; base64,${btoa(JSON.stringify(script.map))}`;
  }
  consttemplate = compiler.compileTemplate({... templateOptions,sourceMap: true});
  if(template.map) {
    template.map.sources[0] = `${template.map.sources[0]}? template`;
    template.code = `${template.code}\n//# sourceMappingURL=data:application/json; base64,${btoa(JSON.stringify(template.map))}`;
  }
  let cssInJS = ' ';
  if(descriptor.styles) {
    const styled = descriptor.styles.map((style) = > {
      return compiler.compileStyle({
        id,
        source: style.content,
        scoped: style.scoped,
        preprocessLang: style.lang,
      });
    });
    if(styled.length) {
      const cssCode = styled.map(s= > s.code).join('\n');
      cssInJS = `(function(){const el = document.createElement('style');
el.innerHTML = \`${cssCode}\ `; document.body.appendChild(el); } ()); `; }}const moduleCode = `
  import script from '${getBlobURL(script.content)}';
  import {render} from '${getBlobURL(template.code)}';
  script.render = render;
  ${filename ? `script.__file = '${filename}'` : ' '};
  ${scopeId ? `script.__scopeId = '${scopeId}'` : ' '};
  ${cssInJS}
  export default script;
  `;
  return moduleCode;
}
Copy the code

So finally, the code compiles.

There’s a little bit of detail here. We can use BlobURL to import modules, which was explained in detail in my previous article “Sharing tips: Implementing import inline JS modules in browsers”, so I won’t go into details here. Alternatively, we can use dataURL to set soureMap to compiled code for easy debugging.

The third step is to apply the compiled code to the page

There are many ways to do this, but one of the more convenient and elegant ways to do this is again to use BlobURL, just as in my last article. Let’s look at the code.

function getBlobURL(jsCode) {
  const blob = new Blob([jsCode], {type: 'text/javascript'});
  const blobURL = URL.createObjectURL(blob);
  return blobURL;
}

// https://github.com/WICG/import-maps
const map = {
  imports: {
    vue: 'https://unpkg.com/vue@3/dist/vue.esm-browser.js',},scopes: {}};function makeComponent(component) {
  const module = component.getAttribute('component');
  let moduleName = module;
  if(!/\.vue$/.test(module)) {
    moduleName += '.vue';
  }
  component.setAttribute('module', moduleName);
  if(module) {
    return [getBlobURL(transformVueSFC(component.innerHTML, moduleName)), module];
  }
  return [];
}

const currentScript = document.currentScript || document.querySelector('script');

function setup() {
  const components = document.querySelectorAll('noscript[type="vue-sfc"]');
  const importMap = {};
  let mount = null;

  [...components].forEach((component) = > {
    const [url, module] = makeComponent(component);
    if(component.hasAttribute('mount')) {
      if(mount) throw new Error('Not support multiple app entrances.');
      mount = [module, component.getAttribute('mount')];
    }
    if(url) {
      importMap[module] = url; }});const importMapEl = document.querySelector('script[type="importmap"]');
  if(importMapEl) {
    // map = JSON.parse(mapEl.innerHTML);
    throw new Error('Cannot setup after importmap is set. Use <script type="sfc-importmap"> instead.');
  }

  const externalMapEl = document.querySelector('script[type="sfc-importmap"]');

  if(externalMapEl) {
    const externalMap = JSON.parse(externalMapEl.textContent);
    Object.assign(map.imports, externalMap.imports);
    Object.assign(map.scopes, externalMap.scopes);
  }

  Object.assign(map.imports, importMap);

  const mapEl = document.createElement('script');
  mapEl.setAttribute('type'.'importmap');
  mapEl.textContent = JSON.stringify(map);
  currentScript.after(mapEl);

  if(mount) {
    const script = document.createElement('script');
    script.setAttribute('type'.'module');
    script.innerHTML = `
      import {createApp} from 'vue';
      import App from '${mount[0]}';
      createApp(App).mount('${mount[1]}');    
    `;
    document.body.appendChild(script);
  }
}

setup();
Copy the code

I won’t elaborate on it here, and the code is not very complicated. If you are interested, you can study it. If you have any questions, please feel free to discuss in the comments section.

As a result, we can easily write vuE-sFC inline directly in a separate HTML page, as shown in the following example code:

<noscript type="vue-sfc" component="MyComponent" mount="#app">
  <script>
    export default {
      data() {
        return {
          count: 0}}}</script>

  <template>
    <button @click="count++">Count is: {{ count }}</button>
  </template>

  <style scoped>
    button {
      font-weight: bold;
    }
  </style>
</noscript>
<div id="app"></div>
<script src="https://unpkg.com/noscript-sfc/index.js"></script>
Copy the code

See the GitHub repository for the full project code.