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

2022-08-30 15:18:52 浏览数 (1)

前言

单位近日难的清闲

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

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

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

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

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

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

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

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

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

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

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

前期调研

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

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

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

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

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

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

就是vscode webpack 浏览器

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

我勒个去,我瞬间石化了

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

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

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-ui 的 tree组件代替

然,总是差点意思,干脆自己来吧! 借鉴了一个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 的数据结构

他将文件和目录分开了,分别在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 的文档也是一塌糊涂,我猜他们魔改了这个编辑器

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

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

无奈之下,另辟蹊径吧

找了个呼声高,功能相似,文档齐全的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

原理如盗图

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、分析是分析是,落地是落地,高大上的技术,实现方式,虽然深奥,但是别忘了,我们是站在巨人的肩膀上,其实相当简单,鼓励大家迎难而上!

0 人点赞