写点什么

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

作者:CRMEB
  • 2022 年 3 月 17 日
  • 本文字数:3587 字

    阅读完需:约 12 分钟

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

我们知道,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-mapsconst 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

用户头像

CRMEB

关注

还未添加个人签名 2021.11.02 加入

CRMEB就是客户关系管理+营销电商系统实现公众号端、微信小程序端、H5端、APP、PC端用户账号同步,能够快速积累客户、会员数据分析、智能转化客户、有效提高销售、会员维护、网络营销的一款企业应用

评论

发布
暂无评论
在页面中直接嵌入vue-sfc的方法_CRMEB_InfoQ写作平台