咱们晓得,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