在线代码编辑器
一. 需求分析
以 https://play.vueuse.org/ 为例,实现一个以下功能vue代码编辑器
- [x] 在线编辑, 实时预览代码渲染
- [x] 多vue文件模拟
- [ ] 添加unocss提示和处理
- [ ] 导入npm包
- [ ] 导出代码
- [ ] 可分享页面
- [x] 多窗格布局
- [ ] 错误处理
- [ ] 使用electron完善成类代码编辑器
接下来继续拆解其功能
二. 技术选型
打包器
vite
,vue3 + ts- 组件按需导入
vite-plugin-components
- 组件按需导入
代码编辑器
monaco-editor
- html 高亮
vscode-html-languageservice
- VS Code 主题
theme-vitesse
- 自定义html worker 线程封装
- html 高亮
多窗格布局
splitpanes
文件处理
- 保存到本地
file-saver
- 压缩
jszip
- 保存到本地
ui
- css框架
windicss
- 组件库
@headlessui/vue
- 图标包
@iconify/json
- css框架
工具库
@vueuse
@antfu/utils
字符串压缩
lz-string
, 压缩代码为最小格式按比例缩放窗格
vue-hako
三. TODO: monaco 自定义插件与渲染
参考资料
monaco是从vscode中直接剥离出来的编辑器,以便可供web使用。
以下是源码仓库
- monaco-editor:仓库代码打包后的版本,不包含源码,一般用于用户使用
- monaco-editor-core:构建
monaco-editor
的基础库,可以用这个库来添加自定义的语言服务
粗粗看了下, 好像两个库区别是 core 没有自带的language service
微软提出的高级语言服务协议,通信使用JSON-RPC传输,规范了编辑器与服务间的交互,避免了每个编辑器都用不同的代码实现同样的功能。
只需要实现对应的接口,就可以完美接入vscode;
现在需要为其添加vue和windicss的支持,这需要非常了解vscode、vue、windicss的运行机制
四. 加载npm包
import map仅仅只能用于学习,生产环境还是得用stackbilz、codesandbox的方法
从skypack 获取esm 包
原理
es-modules
通过 import-maps 特性加载es-moudle模块,在未支持这一特性的浏览器上,可使用 es-module-shims 做兼容性处理
- 「将 Vue SFC 编译为 ESM 」探索之路
- vue 无build加载
- import maps
- (重要) es-module-shims原理及综合描述
- inline-module
- 只是需要注意的是,
<script src="https://unpkg.com/inline-module/index.js" setup></script>
这段必须出现在所有的type="inline-module"
的script标签之后,所有type="module"
的script标签之前。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script async src="https://unpkg.com/es-module-shims@1.5.15/dist/es-module-shims.js"></script>
</head>
<body>
<div id="app">
template: {{ message }}
</div>
</body>
<script>
console.log('currentScript', document.currentScript)
</script>
<script type="inline-module" id="foo">
const foo = 'i am foo';
export default {foo};
</script>
<!-- https://generator.jspm.io/#U2NhYGBkDM0rySzJSU1hKEpNTC5xMLTQM9Az0C1K1jMAAKFS5w0gAA -->
<script type="inline-module-importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script setup>
const currentScript = document.currentScript || document.querySelector('script')
const map = {
imports: {
vue: 'https://unpkg.com/vue@3/dist/vue.esm-browser.js',
},
scopes: {},
}
function getBlobURL(module) {
const jsCode = module.innerHTML
const blob = new Blob([jsCode], {type: 'text/javascript'})
return URL.createObjectURL(blob)
}
function setup() {
const modules = document.querySelectorAll('script[type="inline-module"]')
const importMap = {};
[...modules].forEach((module) => {
const {id} = module
if (id)
importMap[`#${id}`] = getBlobURL(module)
})
console.log('modules', {modules, importMap})
const importMapEl = document.querySelector('script[type="importmap"]')
if (importMapEl) {
// map = JSON.parse(mapEl.innerHTML);
throw new Error('Cannot setup after importmap is set. Use <script type="inline-module-importmap"> instead.')
}
const externalMapEl = document.querySelector('script[type="inline-module-importmap"]')
if (externalMapEl) {
const externalMap = JSON.parse(externalMapEl.textContent)
Object.assign(map.imports, externalMap.imports)
Object.assign(map.scopes, externalMap.scopes)
}
Object.assign(map.imports, importMap)
const mapEl = document.createElement('script')
mapEl.setAttribute('type', 'importmap')
mapEl.textContent = JSON.stringify(map)
currentScript.after(mapEl)
}
if (currentScript.hasAttribute('setup'))
setup()
</script>
<script type="module">
import {createApp} from 'vue'
import * as vue from 'vue'
import foo from '#foo';
console.log('vue', vue)
console.log(import.meta, document.currentScript)
console.log('foo', foo)
createApp({
data() {
return {
message: foo,
}
},
}).mount('#app')
</script>
</html>
system.js 在import-maps特性上,支持umd模块和更多的功能
stackblitz
五. 编译vue文件
1. 编译过程
正常情况下,使用importmap特性,已经可以在浏览器上直接运行es-module模块。但是,vue3的单文件,是无法在浏览器上直接使用的,所以,我们需要把其转换为js代码。
这时候需要使用vue/compiler-sfc 转换单文件代码成原生js、css、html
TODO: css 待处理
2. 多文件
以下为思路,选择了第二种方式
- inline-module: 每个文件都创建成blob, 挂在import-map上,自然就可以通过浏览器互相import。这样每个文件的名字、导入方式需要一致
- vue-playground:原来的使用方法,有点像webpack, 挂载一个全局变量module, 嵌入iframe前提前做好挂载,从一个入口往下递归解析成ast, 替换导入导出地址,组装成script module。 最后通过postMessage发送到iframe, 动态替换,每次不重新创建iframe, 只有在importmap或vue版本变化时才重新创建
六. 错误处理 TODO
运行时错误和