前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >构建属于自己的 Vue SFC Playground

构建属于自己的 Vue SFC Playground

作者头像
用户6256742
发布2024-06-12 09:38:35
1580
发布2024-06-12 09:38:35
举报
文章被收录于专栏:网络日志网络日志

0. 目录

1. 前言

本文将会带您实现一个简单的 Vue SFC Playground。

2. 实现思路

2.1 基本逻辑

VueSfcEditor.vue 编辑后同步源码给VuePlayground.vue组件,VuePlayground.vue组件内部使用@vue/compiler-sfc 编译源码成浏览器可执行的脚本给 Preview.vue 执行渲染。

构建属于自己的 Vue SFC Playground
构建属于自己的 Vue SFC Playground

2.2 渲染逻辑

Preview.vue 组件首次渲染时创建一个 iframe 容器。 监听到代码变更时,通过iframe.contentWindow.postMessage 方法将编译后的代码传递给 iframe。iframe 监听 message 事件,将传递的代码包裹在 script 标签中执行。

这么做的原因在于,希望预览组件能够在 iframe 沙盒中独立运行,和主应用互不干涉。而在浏览器中想要使用 vue,需要下载 vue 浏览器包,为了避免下载 vue 导致渲染闪烁,采用消息传递的方式来局部渲染。

3. 开发

3.1 整理项目

移动 VueSfcEditor.vuesrc/components 中。并在该目录创建 Preview/index.vuePreview/preview-template.htmlVuePlayground.vue

构建属于自己的 Vue SFC Playground
构建属于自己的 Vue SFC Playground

3.2 安装 @vue/compiler-sfc

代码语言:javascript
复制
npm install @vue/compiler-sfc

3.3 修改 App.vue

代码语言:javascript
复制
<script setup>
import VuePlayground from "./components/VuePlayground.vue";
</script>

<template>
  <vue-playground></vue-playground>
</template>

3.4 VuePlayground.vue 组件

代码语言:javascript
复制
<template>
  <div class="vue-playground">
    <div class="vue-playground__editor">
      <VueSfcEditor></VueSfcEditor>
    </div>
    <div class="vue-playground__preview">
      <Preview></Preview>
    </div>
  </div>
</template>

<script setup>
import VueSfcEditor from "./VueSfcEditor.vue";
import Preview from "./Preview/index.vue";
import { provide, reactive } from "vue";
import {
  parse,
  compileScript,
  compileTemplate,
  compileStyle,
} from "@vue/compiler-sfc";

// 默认代码
const DefaultCode = `
<script setup>
import { ref } from 'vue'

const msg = ref('Hello World!')
<\/script>

<template>
  <h1>{{ msg }}</h1>
  <input v-model="msg" />
</template>
`;

const state = reactive({
  // sfc 源代码
  code: DefaultCode.trim(),
  updateCode(code) {
    state.code = code;
  },

  // 编译过程
  compile(code) {
    const { descriptor } = parse(code);
    let _code = `
    if(window.__app__){
      window.__app__.unmount();
    }
    window.__app__ = null;
    `;

    const componentName = "__AppVue__";

    // 编译脚本。
    if (descriptor.script || descriptor.scriptSetup) {
      const script = compileScript(descriptor, {
        inlineTemplate: true,
        id: descriptor.filename,
      });

      _code += script.content.replace(
        "export default",
        `window.${componentName} =`
      );
    }

    // 非 setup 模式下,需要编译 template。
    if (!descriptor.scriptSetup && descriptor.template) {
      const template = compileTemplate(descriptor.template, {
        id: descriptor.filename,
      });
      _code =
        _code +
        ";" +
        template.code.replace(
          "export function",
          `window.${componentName}.render = function`
        );
    }

    // 创建 vue app 实例并渲染。
    _code += `;
      import { createApp } from "vue";

      window.__app__ = createApp(window.${componentName});
      window.__app__.mount("#app");

      if (window.__style__) {
        window.__style__.remove();
      }
    `;

    // 编译 css 样式。
    if (descriptor.styles?.length) {
      const styles = descriptor.styles.map((style) => {
        return compileStyle({
          source: style.content,
          id: descriptor.filename,
        }).code;
      });

      _code += `
      window.__style__ = document.createElement("style");
      window.__style__.innerHTML = ${JSON.stringify(styles.join(""))};
      document.body.appendChild(window.__style__);
      `;
    }

    return _code;
  },
});

provide("store", state);
</script>

<style scoped>
.vue-playground {
  display: flex;
  height: 100%;
}
.vue-playground > * {
  flex: 1;
}
</style>

3.5 修改 VueSfcEditor.vue

代码语言:javascript
复制
<template>
    <!-- ... -->
</template>

<script setup>
import {
    // ...
    inject
} from "vue";
// ...

// 注入store
const store = inject("store");

onMounted(() => {
  if (!vueSfcEditor.value) return;

  const view = new EditorView({
    doc: store.code,
    extensions: [
      // ...
      EditorView.updateListener.of((view) => {
        if (view.docChanged) {
          store.updateCode(view.state.doc.toString());
        }
      }),
    ],
    parent: vueSfcEditor.value,
  });
});
</script>

3.6 Preview/preview-template.html

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body>
    <div id="app"></div>
    <script type="importmap">
      {
        "imports": {
          "vue": "https://unpkg.com/vue@3.4.21/dist/vue.esm-browser.js"
        }
      }
    </script>
    <script>
      // 监听 message,preview/index.vue 通过 postmessage 传递需要执行的代码
      window.addEventListener("message", ({ data }) => {
        const { type, code } = data;

        if(type === "eval") {
          handleEval(code);
        }
      });

      const evalScriptElements = [];

      // 处理需要执行的代码
      function handleEval(code) {
        // 移除历史脚本
        if(evalScriptElements.length) {
          evalScriptElements.forEach(el => el.remove());
        }
        
        // 创建新的脚本元素
        const script = document.createElement("script");
        script.setAttribute("type", "module");
        script.innerHTML = code;
        evalScriptElements.push(script);

        // 插入到 body 中。
        document.body.appendChild(script);
      }
    </script>
  </body>
</html>

3.7 Preview/index.vue

代码语言:javascript
复制
<template>
  <div class="preview" ref="preview"></div>
</template>

<script setup>
import { ref, onMounted, watch, inject, onUnmounted } from "vue";
import PreviewTemplate from "./preview-template.html?raw";

const preview = ref();

let proxy = null;

// 注入store
const store = inject("store");

// 创建沙盒
function createSandbox() {
  const template = document.createElement("iframe");
  template.setAttribute("frameborder", "0");
  template.style = "width: 100%; height:100%";
  template.srcdoc = PreviewTemplate;
  preview.value.appendChild(template);

  template.onload = () => {
    proxy = createProxy(template);
  };
}

// 创建代理,用于监听code 变化,告诉沙盒重新渲染
function createProxy(iframe) {
  let _iframe = iframe;

  const stopWatch = watch(() => store?.code, compile, { immediate: true });

  function compile(code) {
    if (!code?.trim()) return;

    const compiledCode = store?.compile(code);

    _iframe.contentWindow.postMessage(
      { type: "eval", code: compiledCode },
      "*"
    );
  }

  // 销毁沙盒
  function destory() {
    _iframe?.remove();
    _iframe = null;
    stopWatch?.();
  }

  return {
    compile,
    destory,
  };
}

onMounted(createSandbox);
onUnmounted(() => proxy?.destory());
</script>

<style scoped>
.preview {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>

4. 效果/预览

构建属于自己的 Vue SFC Playground
构建属于自己的 Vue SFC Playground

预告

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-04-21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. 目录
  • 1. 前言
  • 2. 实现思路
    • 2.1 基本逻辑
      • 2.2 渲染逻辑
      • 3. 开发
        • 3.1 整理项目
          • 3.2 安装 @vue/compiler-sfc
            • 3.3 修改 App.vue
              • 3.4 VuePlayground.vue 组件
                • 3.5 修改 VueSfcEditor.vue
                  • 3.6 Preview/preview-template.html
                    • 3.7 Preview/index.vue
                    • 4. 效果/预览
                    • 预告
                    相关产品与服务
                    容器服务
                    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档