可实时编辑预览echarts的组件

699 阅读7分钟

前言

使用monaco-editor作为代码编辑器,echarts预览采用iframe引入一个页面形式/独立的一个容器内不影响外部的操作

git仓库

演示视频

[video(video-3pHTn7yR-1727592280528)(type-csdn)(url-live.csdn.net/v/embed/427…)]

一、布局和大小设置

在这里插入图片描述

1.布局

布局分左右两侧,左侧编辑代码和运行代码;

右侧对代码的预览,和echarts版本的切换;

 <div class="container-main" ref="MainRef" :class="{ 'isDrag': isDrag }">
    <div class="container-main-code" ref="rCode">
        <div class="container-main-codeView-header">
            <div class="RunCode" @click="handleClickRun">运行</div>
        </div>
        <div class="container-main-codeView-content">
            <codeEditor @init="handleInitCode" ref="iediter"></codeEditor>
        </div>
    </div>
    <div class="container-main-move" @mousedown="mouseDown" ref="MoveRef"></div>
    <div class="container-main-view" ref="rView">
        <div class="container-main-codeView-header">
            <select class="container-main-view-select" v-model="version" style="width: 120px"  @change="versionChange">
                <option :value="item" v-for="item in versionOption">{{ item }}</option>
            </select>
        </div>
        <div class="container-main-codeView-content" ref="frameContent">
            <iframe :src="iframeSrc" id="Frame" class="container-main-view-frame"></iframe>
        </div>
    </div>
</div>

<style lang="less" scoped>
.container-main {
    width: 100%;
    height: 100%;
    display: flex;
    position: relative;

    .container-main-codeView-content {
        flex: 1;
        height: 0px;
        overflow: hidden;
    }

    .container-main-codeView-header {
        width: 100%;
        display: flex;
        box-sizing: border-box;
        padding: 6px 12px;
        height: calc(12px + 32px);
        min-height: calc(12px + 32px);
        max-height: calc(12px + 32px);
        background-color: #fff;
        box-shadow: 0px 1px 4px #1677ff8a;
        z-index: 9999;

        .RunCode {
            height: 32px;
            width: 80px;
            background-color: #1677ff;
            color: #fff;
            border-radius: 6px;
            align-items: center;
            justify-content: center;
            display: flex;
            cursor: pointer;
            font-size: 14px;
        }
    }

    .container-main-code {
        width: 45%;
        height: 100%;
        z-index: 60;
        display: flex;
        flex-direction: column;
    }

    .container-main-move {
        width: 12px;
        left: 45%;
        height: 100%;
        position: absolute;
        cursor: col-resize;
        z-index: 999;
        background-color: #f1f1f1;
    }

    .container-main-view {
        width: 55%;
        height: 100%;
        padding-left: 12px;
        box-sizing: border-box;
        z-index: 60;
        display: flex;
        flex-direction: column;
        .container-main-view-select{
            border-color: #1677ff;
            border-radius: 6px;
            height: 32px;
            outline: none;
            font-weight: 600;
            padding: 0px .5em;
            font-size: 14px;
        }

        :deep(.container-main-view-frame) {
            width: 100%;
            height: 100%;
            display: block;
            border: none;
            outline: none;
        }
    }
}

.isDrag::after {
    position: absolute;
    width: 100%;
    height: 100%;
    content: '';
    left: 0;
    top: 0;
    z-index: 9999;
    cursor: col-resize;
}
</style>

2.大小调整

可以看到<div class="container-main-move" @mousedown="mouseDown" ref="MoveRef"></div>该元素是用来,通过鼠标按下滑动设置,代码编辑和预览视图的大小;

let MoveRef = ref();
let MainRef = ref();
let startDifMove = ref(0);
let isDrag = ref(false);
let rCode = ref();
let rView = ref();
let iediter = ref();

const mouseMove = (event: MouseEvent) => {
    let movex = event.clientX - startDifMove.value;
    let codePie: number;
    let viewPie: number;
    let maxWidth = MainRef.value.clientWidth;
    codePie = ((movex / maxWidth) * 100)
    viewPie = (100 - codePie)
    if (codePie < 10) {
        codePie = 10;
        viewPie = 90;
    }
    if (viewPie < 10) {
        codePie = 90
        viewPie = 10;
    }
    MoveRef.value.style.left = codePie + '%';
    rCode.value.style.width = codePie + '%';
    rView.value.style.width = viewPie + '%';
    iediter.value.resize();
}
const mouseUp = () => {
    window.removeEventListener('mousemove', mouseMove);
    window.removeEventListener('mouseup', mouseUp);
    isDrag.value = false;
}
const mouseDown = (event: MouseEvent) => {
    startDifMove.value = event.clientX - MoveRef.value.offsetLeft;
    isDrag.value = true;
    window.addEventListener('mousemove', mouseMove);
    window.addEventListener('mouseup', mouseUp);
}

onUnmounted(() => {
    mouseUp();
    document.removeEventListener('visibilitychange', visibilitychangeFun)
})
const visibilitychangeFun = () => {
    let visibilityState = document.visibilityState
    if (visibilityState == 'visible') {
        mouseUp();
    }
}
document.addEventListener('visibilitychange', visibilitychangeFun)

二、代码编辑组件

左侧用来编辑代码的组件;暴露出,getValue resize方法,用来获取输入内容和调整大小

1.组件代码

<template>
    <div class="monaco-editor" ref="editor"></div>
</template>
<script setup lang="ts">
import {code} from './defaultCode';
import * as monaco from 'monaco-editor';
import { ref, onMounted,toRaw,defineExpose,defineEmits } from 'vue';

import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'

let editor = ref();
let instance = ref();
const emits = defineEmits(['init']);
let defaultCode = code;
const initEditor = () => {
    instance.value = monaco.editor.create(editor.value, {
        value: defaultCode,
        language: 'javascript',
    });
    emits('init',getValue())
}
onMounted(()=>{
    initEditor();
})

const getValue = () =>{
    return toRaw(instance.value).getValue();
}
const resize = () =>{
    toRaw(instance.value).layout();
}

// 解决 codeEditor 输入报错的
// https://github.com/microsoft/monaco-editor/issues/2122
window.MonacoEnvironment = {
  getWorker (_: string, label: string) {
    if (label === 'typescript' || label === 'javascript') return new TsWorker()
    if (label === 'json') return new JsonWorker()
    if (label === 'css') return new CssWorker()
    if (label === 'html') return new HtmlWorker()
    return new EditorWorker()
  }
}

defineExpose({
    getValue,
    resize
})
window.onresize = () =>{
    requestAnimationFrame(()=>{
        resize();
    })
}
</script>

<style lang="less" scoped>
.monaco-editor{
    width: 100%;
    height: 100%;
}
</style>

2.默认代码和base64

let _str = `bGV0IGRhdGEgPSBbCiAgIHsKICAgICAgbmFtZTogJ+WSqOivouexu+aVsOmHjycsCiAgICAgIHZhbHVlOiA1MDAwLAogICAgICBwcm86IDUwCiAgIH0sCiAgIHsKICAgICAgbmFtZTogJ+aKleivieexu+aVsOmHjycsCiAgICAgIHZhbHVlOiAzMDAwLAogICAgICBwcm86IDMwCiAgIH0sCiAgIHsKICAgICAgbmFtZTogJ+ivhOS7t+exu+aVsOmHjycsCiAgICAgIHZhbHVlOiAyMDAwLAogICAgICBwcm86IDIwCiAgIH0KXQpjb25zdCBwdXNoWmVybyA9IChuKSA9PiB7CiAgIGlmIChuICUgMTAgPT0gMCkgewogICAgICByZXR1cm4gbiArICcuMDAnCiAgIH0gZWxzZSB7CiAgICAgIHJldHVybiBuCiAgIH0KfQpsZXQgY29sb3JzID0gWycjM0I4Q0U5JywgJyM1QkMzNzQnLCAnI0VBNjY3MiddOwpsZXQgZGF0YU5hbWVNYXAgPSB7CiAgICflkqjor6LnsbvmlbDph48nOiAnenhsJywKICAgJ+aKleivieexu+aVsOmHjyc6ICd0c2wnLAogICAn6K+E5Lu357G75pWw6YePJzogJ3BqbCcKfQpsZXQgbGVnZW5kUmljaHMgPSB7CiAgIHRpdGxlMTogewogICAgICBjb2xvcjogJyM5OTk5OTknLAogICAgICB3aWR0aDogMTIwLAogICAgICBhbGlnbjogJ2xlZnQnLAogICAgICBoZWlnaHQ6IDE0LAogICAgICBmb250U2l6ZTogMTQsCiAgICAgIHBhZGRpbmc6IFsyLCAwLCAwLCA0XQogICB9LAogICB0aXRsZTI6IHsKICAgICAgaGVpZ2h0OiAxNCwKICAgICAgY29sb3I6ICcjOTk5OTk5JywKICAgICAgZm9udFNpemU6IDE0LAogICAgICBwYWRkaW5nOiBbMiwgMCwgMCwgMF0KICAgfSwKICAgemhhbjogewogICAgICBoZWlnaHQ6IDE0CiAgIH0sCiAgIG5zZWw6IHsKICAgICAgd2lkdGg6IDE0LAogICAgICBoZWlnaHQ6IDE0LAogICAgICBiYWNrZ3JvdW5kQ29sb3I6ICcjY2NjY2NjOGEnLAogICAgICBib3JkZXJSYWRpdXM6IDE0LAogICB9LAogICBudmFsdWUxOiB7CiAgICAgIHdpZHRoOiAxMjAsCiAgICAgIGZvbnRTaXplOiAxOCwKICAgICAgY29sb3I6ICcjOTk5OTk5JywKICAgICAgcGFkZGluZzogWzAsIDAsIDAsIDE0ICsgNF0KICAgfSwKICAgbnZhbHVlMjogewogICAgICBmb250U2l6ZTogMTgsCiAgICAgIGNvbG9yOiAnIzk5OTk5OScsCiAgIH0KfTsKbGV0IHNlbGVjdHMgPSB7fTsKbGV0IGxlZ2VuZCA9IFtdOwpjb25zdCBjb21wdXRlZExlZ2VuZFJpY2hzID0gKERBVEEpID0+IHsKICAgREFUQS5tYXAoKGl0ZW0sIGluZGV4KSA9PiB7CiAgICAgIGxlZ2VuZC5wdXNoKGl0ZW0ubmFtZSkKICAgICAgbGV0IG1hcEsgPSBkYXRhTmFtZU1hcFtpdGVtLm5hbWVdOwogICAgICBzZWxlY3RzW2l0ZW0ubmFtZV0gPSB0cnVlOwogICAgICBsZXQgX2NvbG9yID0gY29sb3JzW2luZGV4XQoKICAgICAgbGV0IGljb24gPSBgaWNvbl9gICsgbWFwSwogICAgICBsZWdlbmRSaWNoc1tpY29uXSA9IHsKICAgICAgICAgd2lkdGg6IDE0LAogICAgICAgICBoZWlnaHQ6IDE0LAogICAgICAgICBiYWNrZ3JvdW5kQ29sb3I6IF9jb2xvciwKICAgICAgICAgYm9yZGVyUmFkaXVzOiAxNCwKICAgICAgfQoKICAgICAgbGV0IHZhbHVlMSA9ICd2YWx1ZTFfJyArIG1hcEs7CiAgICAgIGxldCB2YWx1ZTIgPSAndmFsdWUyXycgKyBtYXBLOwogICAgICBsZWdlbmRSaWNoc1t2YWx1ZTFdID0gewogICAgICAgICB3aWR0aDogMTIwLAogICAgICAgICBmb250U2l6ZTogMTgsCiAgICAgICAgIGNvbG9yOiBfY29sb3IsCiAgICAgICAgIHBhZGRpbmc6IFswLCAwLCAwLCAxNCArIDRdCiAgICAgIH0KICAgICAgbGVnZW5kUmljaHNbdmFsdWUyXSA9IHsKICAgICAgICAgZm9udFNpemU6IDE4LAogICAgICAgICBjb2xvcjogX2NvbG9yLAogICAgICB9CiAgIH0pCn0KCmNvbXB1dGVkTGVnZW5kUmljaHMoZGF0YSk7CgpvcHRpb24gPSB7CiAgIC8v5L2g55qE5Luj56CBCiAgIGNvbG9yOiBjb2xvcnMsCiAgIGJhY2tncm91bmRDb2xvcjogIiNmZmYiLAogICBzZXJpZXM6IFsKICAgICAgewogICAgICAgICB0eXBlOiAncGllJywKICAgICAgICAgcmFkaXVzOiBbJzMwJScsICczNSUnXSwKICAgICAgICAgY2VudGVyOiBbJzYwJScsICc1MCUnXSwKICAgICAgICAgbGFiZWw6IHsKICAgICAgICAgICAgc2hvdzogZmFsc2UsCiAgICAgICAgIH0sCiAgICAgICAgIGRhdGE6IGRhdGEsCiAgICAgICAgIGl0ZW1TdHlsZTogewogICAgICAgICAgICBib3JkZXJXaWR0aDogMiwKICAgICAgICAgICAgYm9yZGVyQ29sb3I6ICcjZmZmJwogICAgICAgICB9LAogICAgICAgICBtaW5BbmdsZTogMwogICAgICB9CiAgIF0sCiAgIGxlZ2VuZDogWwogICAgICB7CiAgICAgICAgIG9yaWVudDogJ3ZlcnRpY2FsJywKICAgICAgICAgbGVmdDogJzE1JScsCiAgICAgICAgIHRvcDogJ2NlbnRlcicsCiAgICAgICAgIGljb246ICdjaXJjbGUnLAogICAgICAgICBmb3JtYXR0ZXI6IChsZWcpID0+IHsKICAgICAgICAgICAgbGV0IGl0ZW0gPSBkYXRhLmZpbHRlcigoVikgPT4gVi5uYW1lID09IGxlZylbMF07CiAgICAgICAgICAgIGxldCBtYXBLID0gZGF0YU5hbWVNYXBbbGVnXTsKICAgICAgICAgICAgbGV0IGljb24gPSAnaWNvbl8nICsgbWFwSzsKICAgICAgICAgICAgbGV0IHZhbHVlMSA9ICd2YWx1ZTFfJyArIG1hcEs7CiAgICAgICAgICAgIGxldCB2YWx1ZTIgPSAndmFsdWUyXycgKyBtYXBLOwogICAgICAgICAgICBpZiAoc2VsZWN0c1tsZWddKSB7CiAgICAgICAgICAgICAgIHJldHVybiBgeyR7aWNvbn18fXt0aXRsZTF8JHtsZWd9fXt0aXRsZTJ85Y2g5q+UfVxue3poYW58fVxueyR7dmFsdWUxfXwke2l0ZW0udmFsdWV9fXske3ZhbHVlMn18JHtwdXNoWmVybyhpdGVtLnBybyl9JX1gCiAgICAgICAgICAgIH0gZWxzZSB7CiAgICAgICAgICAgICAgIC8vIGljb27ngbDoibIKICAgICAgICAgICAgICAgcmV0dXJuIGB7bnNlbHx9e3RpdGxlMXwke2xlZ319e3RpdGxlMnzljaDmr5R9XG57emhhbnx9XG57JHt2YWx1ZTF9fCR7aXRlbS52YWx1ZX19eyR7dmFsdWUyfXwke3B1c2haZXJvKGl0ZW0ucHJvKX0lfWAKCiAgICAgICAgICAgICAgIC8vIOWPlua2iOmAieaLqeeahOWFqOeBsOiJsgogICAgICAgICAgICAgICAvLyByZXR1cm4gYHtuc2VsfH17dGl0bGUxfCR7bGVnfX17dGl0bGUyfOWNoOavlH1cbnt6aGFufH1cbntudmFsdWUxfCR7aXRlbS52YWx1ZX19e252YWx1ZTJ8JHtwdXNoWmVybyhpdGVtLnBybyl9JX1gCiAgICAgICAgICAgIH0KICAgICAgICAgfSwgIC8vMTQgKyAxNCArIDE4CiAgICAgICAgIGl0ZW1XaWR0aDogMCwKICAgICAgICAgaXRlbUhlaWdodDogMCwKICAgICAgICAgcGFkZGluZzogMCwKICAgICAgICAgaXRlbUdhcDogMzAsCiAgICAgICAgIGRhdGE6IGxlZ2VuZCwKICAgICAgICAgdGV4dFN0eWxlOiB7CiAgICAgICAgICAgIHJpY2g6IGxlZ2VuZFJpY2hzCiAgICAgICAgIH0KICAgICAgfQogICBdCn07CgpteUNoYXJ0Lm9uKCdsZWdlbmRzZWxlY3RjaGFuZ2VkJywgKGV2ZW50KSA9PiB7CiAgIHNlbGVjdHMgPSBldmVudC5zZWxlY3RlZDsKICAgbXlDaGFydC5zZXRPcHRpb24oewogICAgICBsZWdlbmQ6IHt9CiAgIH0pCn0p`
let Base64 = {
   _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",

   encode: function (e: string) {
      let t = "";
      let n, r, i, s, o, u, a;
      let f = 0;
      e = Base64._utf8_encode(e);
      while (f < e.length) {
         n = e.charCodeAt(f++);
         r = e.charCodeAt(f++);
         i = e.charCodeAt(f++);
         s = n >> 2;
         o = (n & 3) << 4 | r >> 4;
         u = (r & 15) << 2 | i >> 6;
         a = i & 63;
         if (isNaN(r)) {
            u = a = 64
         } else if (isNaN(i)) {
            a = 64
         }
         t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a)
      }
      return t
   },

   decode: function (e: string) {
      let t = "";
      let n, r, i;
      let s, o, u, a;
      let f = 0;
      e = e.replace(/[^A-Za-z0-9+/=]/g, "");
      while (f < e.length) {
         s = this._keyStr.indexOf(e.charAt(f++));
         o = this._keyStr.indexOf(e.charAt(f++));
         u = this._keyStr.indexOf(e.charAt(f++));
         a = this._keyStr.indexOf(e.charAt(f++));
         n = s << 2 | o >> 4;
         r = (o & 15) << 4 | u >> 2;
         i = (u & 3) << 6 | a;
         t = t + String.fromCharCode(n);
         if (u != 64) {
            t = t + String.fromCharCode(r)
         }
         if (a != 64) {
            t = t + String.fromCharCode(i)
         }
      }
      t = Base64._utf8_decode(t);
      return t
   },

   _utf8_encode: function (e: string) {
      e = e.replace(/rn/g, "n");
      let t = "";
      for (let n = 0; n < e.length; n++) {
         let r = e.charCodeAt(n);
         if (r < 128) {
            t += String.fromCharCode(r)
         } else if (r > 127 && r < 2048) {
            t += String.fromCharCode(r >> 6 | 192);
            t += String.fromCharCode(r & 63 | 128)
         } else {
            t += String.fromCharCode(r >> 12 | 224);
            t += String.fromCharCode(r >> 6 & 63 | 128);
            t += String.fromCharCode(r & 63 | 128)
         }
      }
      return t
   },

   _utf8_decode: function (e: string) {
      let t = "";
      let n = 0;
      let r: number = 0;
      let c1: number = 0;
      let c2: number = 0;
      let c3 = null;
      while (n < e.length) {
         r = e.charCodeAt(n);
         if (r < 128) {
            t += String.fromCharCode(r);
            n++
         } else if (r > 191 && r < 224) {
            c2 = e.charCodeAt(n + 1);
            t += String.fromCharCode((r & 31) << 6 | c2 & 63);
            n += 2
         } else {
            c2 = e.charCodeAt(n + 1);
            c3 = e.charCodeAt(n + 2);
            t += String.fromCharCode((r & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
            n += 3
         }
      }
      return t
   }
};
export const code: string =  Base64.decode(_str);

三、预览容器

在项目根目录public文件中创建,EchartsView文件夹 在这里插入图片描述

1.frame.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        *,
        *::before,
        *::after {
            box-sizing: border-box;
        }

        * {
            margin: 0;
        }

        html,
        body {
            height: 100%;
            margin: 0px;
            padding: 0px;
        }

        body {
            line-height: 1.5;
            -webkit-font-smoothing: antialiased;
        }

        img,
        picture,
        video,
        canvas,
        svg {
            display: block;
            max-width: 100%;
        }

        input,
        button,
        textarea,
        select {
            font: inherit;
        }

        p,
        h1,
        h2,
        h3,
        h4,
        h5,
        h6 {
            overflow-wrap: break-word;
        }

        .container{
            width: 100%;
            height: 100%;
            overflow: hidden;
        }
    </style>
</head>

<body>
    <div class="container"></div>
</body>
    <script id="echartsjs" src=""></script>
    <script src="./main.js"></script>
    <script id="codes"></script>
</html>

2.main.js

  1. messageData存放postMessage传递的数据;
  2. 通过获取iframe地址栏参数version创建不同版本的cdn script标签
  3. 动态将创建的echarts cdn标签插入,监听message事件拿到编辑器代码
  4. 创建script标签将代码写入后,插入iframe
let messageData = {};
const handleQuery = (query) => {
    let queryObj = {}
    if (query) {
        query.split('&').forEach((item) => {
            let arr = item.split('=')
            queryObj[arr[0]] = arr[1]
        })
    }
    return queryObj
}
const URLDATA = window.location.href.split('?')[1]
const AllParams = handleQuery(URLDATA);

const setVersionEchart = () => {
    let version = AllParams.version;
    if (!version) return;
    let echartsjs = document.querySelector("#echartsjs");
    document.body.removeChild(echartsjs);
    let script = document.createElement('script');
    script.setAttribute('id', 'echartsjs');
    let cdn = `https://cdn.staticfile.org/echarts/${version}/echarts.min.js`
    script.src = cdn;
    document.body.appendChild(script);
    script.onload = () => {
        window.addEventListener('message', res => {
            console.log(res);
            messageData = res.data;
            initEchart()
        }, false)
    }
}
setVersionEchart();

const initEchart = () => {
    let codes = messageData.codes;
    let runCodes = `
    let container = document.querySelector('.container');
    myChart = echarts.init(container);
    myChart.clear();
    ${codes}
    myChart.setOption(option);
    myChart.resize();
    window.onresize = () =>{
        myChart.resize();
    }
    `
    const codesDom = document.querySelector('#codes');
    codesDom.textContent = runCodes;
}

四、运行和版本切换

let frameContent = ref();
const handleClickRun = () => {
    let Frame_:HTMLElement | null = document.querySelector("#Frame");
    frameContent.value.removeChild(Frame_);
    let newFrame:HTMLIFrameElement | any = document.createElement('iframe');
    newFrame.src = iframeSrc.value;
    newFrame.setAttribute('class', 'container-main-view-frame')
    newFrame.setAttribute('id', 'Frame')
    frameContent.value.appendChild(newFrame);
    newFrame.onload = () => {
        let codes = iediter.value.getValue();
        if (codes.trim()) {
            newFrame.contentWindow.postMessage({ codes }, '*')
        }
    }
}
const handleInitCode = async () => {
    await getVersionOption()
    handleClickRun();
}

let baseSrc = `/EchartsView/frame.html`
let iframeSrc = ref(baseSrc)

let version = ref();
let versionOption = ref([]);
const getVersionOption = () => {
    return getEchartsLibraries().then(res => {
        const data = res.data;
        versionOption.value = data.versions.filter((v: string) => {
            return parseInt(v) >= 3
        }).reverse();
        version.value = '5.5.0';
        versionChange()
    })
}
const versionChange = () => {
    iframeSrc.value = baseSrc + '?version=' + version.value;
    handleClickRun();
}