前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >站在巨人的肩膀上--用VUE3试试搞个在线IDE吧!

站在巨人的肩膀上--用VUE3试试搞个在线IDE吧!

作者头像
用户7413032
发布2022-08-30 15:18:52
1.3K0
发布2022-08-30 15:18:52
举报
文章被收录于专栏:佛曰不可说丶佛曰不可说丶

前言

单位近日难的清闲

然,生那受苦的命,闲不住啊,领下军令状,重构单位单位的组件库使用的在线代码编辑IDE

在尝试重构之前,但是使用的是 CodeSandbox 魔改版本

说白了就是给这个开源项目改点字和接口,基本原封不动的搬过来,这样一来导致几个问题

1、拓展费劲,有新功能加入时,开源的这个编辑器晦涩难懂,无法下手

2、项目体积过大,报错较多,还不知缘由,项目体量更是巨大,启动修改困难,而且无用代码较多

3、bug更改困难,定位问题费劲,开发效率奇底

针对以上原因,就一拍脑袋,领下军令状,这一领坏了,没有提前做调研,殊不知困难重重,几乎猝死

自己说的话含着泪也要干完,这就是男人,一个吐沫一个钉\n\n\n

image.png
image.png

今天版本1.0 也算完成,写个文章记录实现思路,以慰我这累掉的几百根头发,

也为后来人提供一个实现类似需求的借鉴思路,不能说是最佳实践,但是也算是有一个能跑就行(要不我跑,要不代码跑)

更为了告诫大家,没事不要瞎折腾,躺平,摆烂把钱赚也挺好

前期调研

相信大家干一个事情之前都是雄心壮志,更是踌躇满志

me to 我也一样,在刚开始的时候,我一看这功能,这有啥难的,重写一个就完事了

于是我就开始撸codesandbox-client的源码

在这里先简单的介绍一下这个玩意

这是一个浏览器端的沙盒运行环境,支持多种流行的构建模板,例如 create-react-app、 vue-cli、parcel等等

这就是一个在浏览器实现了一个编辑器,加打包器,再加渲染器

就是vscode + webpack + 浏览器

到这,我就知道,这项目不是那么简单,直到我查到,这个项目是一群人,用时四年干出来的

我勒个去,我瞬间石化了

image.png
image.png

我的满腔热血,凉了半截,但是军令状背了,代码不跑,我就得跑啊! 干!!!!!

撸了三天的源码,梳理了一下源码中整体的脉络

1、核心代码为react开发

2、编辑器部分使用monaco-editor

3、包含独立的浏览器打包渲染包sandbox (可以抄)

4、使用lerna构建整个项目但是整体分包不是很明确,可读性差(也可能是我水平不行)

5、自己实现文件系统

6、ui组件风格自己实现

7、Packager 包管理实现自己实现

8、视图展示层使用iframe,并且和编辑器和文件系统之间使用postMessage通信,实现响应式

9、服务端CodeSandbox 自己搭建了一套,用于存储用户信息,以及模板信息

10、源码中包含了大量的编译器,比如vue3编译器等

行动方案

有了这么些,预备资料,我们就可以将真个系统的开发分为三步走策略

首先他真个在线IDE我们可以分为五大块

  • 1、文件系统
  • 2、编辑器
  • 3、渲染器
  • 4、ui呈现
  • 5、通用数据结构设计

文件系统

接下来我们一步步解决首先文件系统,所谓文件系统,在呈现方面来说,就是个树形列表,由于,源码中的react 移植,奈何代码逻辑山路十八弯,算了,准备使用 element-uitree组件代替

然,总是差点意思,干脆自己来吧! 借鉴了一个vue2的库--vue-tree-list将他移植到了vue3上

他的原理其实也很简单,主要就是递归当前组件,这里遇见一个问题,就是v-bind="$attrs" 失效问题

用过$attrs 的都知道,在vue3中 $attrs 可以很方便的做到属性以及事件的透传,如此一来,就能避免中间承上启下的组件的代码复杂度。

我们来看个例子

代码语言:javascript
复制
<div>
  <h2>这是第一个组件</h2>
  <B @changeMyData="changeMyData" :myData="myData"></B>
</div>
</template>
<script>
import B from "./B";
import { ref } from 'vue'
export default {
    components: { B },
    setup() {
        const myData = ref("100")
        const changeMyData = function (val) {
            myData = val;
        }
        return {
            myData,
            changeMyData
        }
    }
};
</script>

组件b 承上启下

代码语言:javascript
复制
<template>
 <div>
   <h3>组件B</h3>
   <C v-bind="$attrs" ></C>
 </div>
</template>
<script>
import C from "./C";
export default {
 components: { C },
};
</script>

如此,就能实现祖孙组件的通信

代码语言:javascript
复制
<template>
  <div>
    <h5>组件C</h5>
    <input v-model="myc" @input="hInput" />
  </div>
</template>
<script>
export default {
  props: ['myData'],
  emits:['changeMyData']

  setup(props,{emit}){
      const myc =props. myData;  // 在组件A中传递过来的属性
          const   hInput= function() {
          emit("changeMyData", myc); // // 在组件A中传递过来的事件
    
    return {
        hInput
    }
  }
};
</script>

但是到了递归组件,不灵了!!,就必须走老路,我也上了github 看了吗,官方未解决issues

由于我们使用的数据沿用了CodeSandbox 的数据结构

image.png
image.png

他将文件和目录分开了,分别在modulesdirectories中,于是我们终于用上了面试时候用到的算法 将数组转为tree 通过递归解决

代码语言:javascript
复制
export const setCatalogue = (currentSandbox): any[] => {

    const arr = _.cloneDeep([...currentSandbox.directories, ...currentSandbox.modules])
    function loop(parId?) {
        return arr.reduce((acc, cur) => {
            if (cur.directory_shortid == parId) {
                cur.children = loop(cur.id)
                acc.push(cur)
            }
            return acc
        }, [])
    }
    return loop()
}

文件系统就这么解决了

编辑器

codesandbox 的编辑器用的是monaco-editor 也就是vscode 的前身 但是,翻遍源码,他的调用方式跟monaco-editor

不能说是相似,简直可以说是不同,并且monaco-editor 的文档也是一塌糊涂,我猜他们魔改了这个编辑器

image.png
image.png

甚至官方都让我们直接从类型定义文件里面去猜,

巨硬爸爸,要不您就别开源了!开源咱也看不懂啊

image.png
image.png

无奈之下,另辟蹊径吧

找了个呼声高,功能相似,文档齐全的codemirror5

东西找好了,开干吧,写个通用的编辑器组件

代码语言:javascript
复制
<template>
    <Codemirror style="font-size: 16px;" ref="CodemirrorRef" v-model="code" :style="{ height: '100%' }"
        :autofocus="true" :indent-with-tab="true" :tabSize="2" :extensions="extensions" @change="change">
    </Codemirror>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Codemirror } from 'vue-codemirror'
import { oneDark } from '@codemirror/theme-one-dark'
import { setLang } from 'utils/index'
import { javascript } from '@codemirror/lang-javascript'
import { css } from "@codemirror/lang-css";
import { html } from "@codemirror/lang-html";
import { json } from "@codemirror/lang-json";
import { markdown } from "@codemirror/lang-markdown";
import _ from 'lodash'
const langType = {
    js: javascript,
    css: css,
    scss: css,
    vue: html,
    jsx: () => javascript({ jsx: true, typescript: true }),
    ts: () => javascript({ jsx: true, typescript: true }),
    html,
    json,
    md: markdown
}
const setLang = (type) => langType[type]()
const emit = defineEmits(['change'])
const props = defineProps({
    code: String,
    type: String
})
const extensions = computed(() => [setLang(props.type), oneDark])
const CodemirrorRef = ref(null)
const change = (e) => {
    emit('change', e)
}
</script>
<style>
</style>

渲染器

渲染器,其实就是整个右边的视图。你一说原理,头头是道,我看了文章也能明白,他是怎么处理的,

然而,光说不练假把式, 你一到落地,可不是这么简单,给我急的嘬牙发子

要解决渲染器的问题,除了要理解原理之外,我们还要解决几个难点

一个个来,先说原理,一句话就能概括,造个web版npm 造个web版webpack

image.png
image.png

原理如盗图

Sandbox 在一个单独的 iframe 中运行, 负责代码的转译(Transpiler)和运行

其实就是一个浏览器端的webpck

Packager类似于yarn和npm,负责拉取和缓存 npm 依赖

接下来就是难点

  • 1、web版本webpack 虽说有源码能抄,但是它是通过iframe 嵌入的,所以本质上他必须是个服务,我们怎样给他独处理成一个项目,源码中都揉一块了,我们从那入手呢
  • 2、Packager包管理,虽然开源了,但是也没提供文档,我们在移植或者,直接搬过来部署也相当困难
  • 3、这块最难,移植过来需要多少时间,工作量无法估计

好在CodeSandbox 良心啊,他们直接独立了一个渲染器将编译和npm 包拉取这一块独立出来 sandpack-client,并且开源了

他的代码非常简单,就是创建一个iframe,并且调用CodeSandbox 官方的打包服务,这样所有的渲染层的核心代码就不会在我们这边了,全部是codesandbox的服务

使用方式也非常简单

代码语言:javascript
复制
import { SandpackClient } from "packages/SandpackClient";
// 数据源
const VUE_TEMPLATE_3 = {
  files: {
    "/src/App.vue": {
      code: `<template>
    <main id="app">
      <h1>{{ helloWorld }}</h1>
    </main>
  </template>
                               
  <script>
  import { ref } from "vue";
  export default {
     name: "App",
     setup() {
        const helloWorld = ref("Hello World");
        return { helloWorld };
     }
  };
  </script>
                               
  <style>
  #app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  }
  </style>   
  `,
    },
    "/src/main.js": {
      code: `import { createApp } from 'vue'
  import App from './App.vue'
              
  createApp(App).mount('#app')            
  `,
    },
  },
  dependencies: {
    "core-js": "^3.6.5",
    vue: "^3.0.0-0",
    "@vue/cli-plugin-babel": "4.5.0",
  },
  entry: "/src/main.js",
  environment: "vue-cli",
};
// 初始化
const SandpackClientStore = new SandpackClient(
                el,
               VUE_TEMPLATE_3,
                {
                    showOpenInCodeSandbox: false
                }
             );
 // 代码跟新
 
 SandpackClientStore.updatePreview(VUE_TEMPLATE_3);
            

思考再三,首先由于渲染层不涉及单位业务,并且如果自己开发不一定比官方的服务好

干脆,拿来主义,用人人家的得了

ui呈现

ui 方面,源码中使用的是他们自己封装的组件,以及自己开发的一些样式

到我们这一切从简,功能实现即可,element-ui代替在家自己开发个别样式即可

通用数据结构设计

由于,文件系统,编辑器,渲染层。三大块需要实现联动,那么你必须要上vuex了,来管理和连接这三个区块的状态以及数据

在最开始,我设想的跟开源的CodeSandbox 一样,设计很多状态,比如currentSandbox 元数据 currentCode选中数据 catalogueStructure 文件目录数据 project 项目代码

然后在项目中通过 mutationsactions 来通知状态以及数据变更

后来,发现,不行,太乱,到处都是commitdispatch,后期维护根本摸不着头绪

我们就追本溯源,我们说,本质整个页面上的所有数据,都围绕着currentSandbox 元数据来操作的

由于vue 的响应式特性,我们所有的数据都需要根据currentSandbox 变更而来,我们使用getters 得到即可 代码如下:

代码语言:javascript
复制
import { createStore } from 'vuex'
import { data } from './dome'
import { SandpackClient } from "packages/SandpackClient";
import { setCatalogue, conversionCode } from 'utils/index'
let SandpackClientStore = null
// 创建一个新的 store 实例
export default createStore({
    state() {
        return {
            currentSandbox: data,
            currentCode: {
                title: '',
                code: ''
            },
        }
    },
    mutations: {
        SETCURRENTCODE(state: any, code) {
            state.currentCode = code
        },
        SETCURRENTSANDBOX(state) {
            const modules = state.currentSandbox.modules
            modules.find((item) => {
                if (item.id == state.currentCode.id) {
                    item.code = state.currentCode.code
                    return
                }

            })
        }
    },
    actions: {
        setCurrentCode({ commit }, code) {
            commit('SETCURRENTCODE', code)
        },
        setClient({ getters }, el) {
            SandpackClientStore = new SandpackClient(
                el,
                getters.project,
                {
                    showOpenInCodeSandbox: false
                }
            );
        },
        setUpdatePreview({ commit, getters }) {
            commit('SETCURRENTSANDBOX')
            SandpackClientStore.updatePreview(getters.project);
        },

    },
    getters: {
        // 入口文件
        entryFile(state) {
            return state.currentSandbox.entry.split('/')
        },
        // 文件目录
        catalogueStructure(state) {
            return setCatalogue(state.currentSandbox)
        },
        // 项目代码
        project(state) {
            return conversionCode(state.currentSandbox)
        }
    }
})
复制代码

这样一来,整个由于响应式的特性,我们只需要修改currentSandbox的数据结构即可 ,简单了不少

源码

1.0捡漏版本实现了,目前只实现了联动,但是还没有和服务端联动,当然配置服务端的内容估计是不能开源了

源码如下: yys-Codesandbox

说点话

经历两周,算是简单的实现了破产版的Codesandbox,功能简陋,层次低廉,技术粗鄙,难登大雅(实现方式确实简单)

但是还是想给实现方式,以及实现思路,发出来:

  • 1、为了实现类似需求的朋友们一个思路上的借鉴,不能说是完全正确,但总能给点参考,毕竟花了两周呢
  • 2、将所有踩过的坑,思考的过程记录一下,这都是安身立命的财富啊
  • 3、分析是分析是,落地是落地,高大上的技术,实现方式,虽然深奥,但是别忘了,我们是站在巨人的肩膀上,其实相当简单,鼓励大家迎难而上!
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-06-09,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 前期调研
  • 行动方案
  • 文件系统
  • 编辑器
  • 渲染器
  • ui呈现
  • 通用数据结构设计
  • 源码
  • 说点话
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档