前言
单位近日难的清闲
然,生那受苦的命,闲不住啊,领下军令状,重构单位单位的组件库使用的在线代码编辑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 的数据结构
他将文件和目录分开了,分别在modules
和directories
中,于是我们终于用上了面试时候用到的算法 将数组转为tree 通过递归解决
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
项目代码
然后在项目中通过 mutations
和 actions
来通知状态以及数据变更
后来,发现,不行,太乱,到处都是commit
和 dispatch
,后期维护根本摸不着头绪
我们就追本溯源,我们说,本质整个页面上的所有数据,都围绕着currentSandbox
元数据来操作的
由于vue 的响应式特性,我们所有的数据都需要根据currentSandbox
变更而来,我们使用getters
得到即可 代码如下:
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、分析是分析是,落地是落地,高大上的技术,实现方式,虽然深奥,但是别忘了,我们是站在巨人的肩膀上,其实相当简单,鼓励大家迎难而上!