0
点赞
收藏
分享

微信扫一扫

在页面中直接嵌入vue-sfc的方法

非衣所思 2022-03-17 阅读 114

我们知道,Vue推荐使用单文件组件(Single File Component,简称SFC),可以说SFC是Vue框架的特色。

但是,我们在学习和练习的时候,如果想要用非常简单的方式在一个常规的HTML文件,或者简单的Playground(比如JSBin或者CodePen)里面使用Vue的SFC方式,是不太容易的。

因此Vue官方提供了专门的​​SFC Playground​​来方便大家学习Vue。

不过,有没有办法不用SFC Playground,在本地单个HTML文件或者CodePen和JSBin这样的平台使用Vue-SFC呢?

办法是有的,我先放一个例子:

​​这是一个在CodePen中写的Vue组件​​

这是怎么做到的呢?

其实要分成三个步骤。

第一步 嵌入SFC内容

首先是要在普通的HTML文件中内联嵌入Vue-SFC组件。这里的麻烦之处在于,SFC中包含有HTML标签,而且还有​​<script>​​标签,因此,将它放在页面中内联,浏览器就会解析这些标签。尽管我们可以通过给​​<script>​​设置type来避免浏览器执行脚本,但是依然不能阻止浏览器解析这些标签本身。

那有同学就动脑筋想了,我们是否可以把SFC的内容放到一个不解析HTML内容的元素中,比如​​<textarea>​​标签。这样当然是可以的,但是也有些许麻烦,就是我们要给这个textarea元素设置样式将它隐藏起来。

实际上,有个不容易想到的,更简单的标签,那就是​​<noscript>​​。正常情况下,浏览器不会解析​​<noscript>​​标签中的元素,而且又可以和其他标签一样,通过​​textConent​​获取其中的内容,所以我们用​​<noscript>​​标签来放置SFC的内容是再合适不过的了。

第二步 编译SFC组件

接着,我们要编译SFC组件。这个可以通过官方提供的 ​​vue/compile-sfc​​ 模块来实现。

compile-sfc 如何使用,​​官方文档​​里写得非常简单,但这不妨碍我们通过研究​​@vitejs/plugin-vue​​和webpack插件​​vue-loader​​来找到它的用法。用法其实也不复杂,核心就是先parse源代码,成为descriptor对象,然后再一一编译script、template和styles。最终,再把这些模块拼起来,拼接的时候,如果不考虑兼容性,最简单的方式是直接使用ES-Module来拼接。

以下是编译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))}`;
}
const template = 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;
}
复制代码

那么最终,代码就编译出来了。

我们可以用BlobURL来import模块,我们还可以使用dataURL,来给编译好的代码设置soureMap,这样就方便了调试。

第三步 将编译好的代码应用于页面

这一步,有很多方法,其中一个比较方便和优雅的方法仍然是使用BlobURL,原理和我上一篇文章一样,我们看一下代码。

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();
复制代码

这里不详细展开说了,代码也不是很复杂,有兴趣的同学可以研究一下,有问题欢迎在评论区讨论。

最终,实现的效果就是,我们可以以下面示例代码的样子来直接内联的方式在一个独立的HTML页面中很方便地书写Vue-SFC了:

<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>

最后

如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163相互学习,我们会有专业的技术答疑解惑

如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:​​http://github.crmeb.net/u/defu​​不胜感激 !

PHP学习手册:https://doc.crmeb.com

技术交流论坛:​​https://q.crmeb.com​​

举报

相关推荐

0 条评论