如果想让自己的组件库可以做到在线编辑的效果,要怎么实现呢?文档库建设踩坑实记录 - 掘金 文章中记录了使用 vuepress 和 vitepress 文档库搭建的原理和存在的问题。本文讨论的是不使用 vuepress 和 vitepress 实现vue 组件在线编辑功能。
要实现 vue 组件库的在线编辑功能,有3个关键实现点:
- 代码编辑器。能够让用户输入代码,进行编辑。
- 代码运行器。就是将 vue 代码进行实时编译可以在 HTML 运行,显示出代码的运行结果。
- 代码结合,将代码编辑器的代码传递给代码运行器。将代码编辑器的代码通过 vue 的双向数据绑定到一个变量上,同时将该变量传入给代码运行器,实现代码的一面编辑一面显示效果的功能。
代码编辑器
monaco-editor 是微软提供的开源Web端代码编辑器,vscode 就是基于它研发出来的。在 vue 项目中使用它。安装 monaco-editor 和 monaco-editor-webpack-plugin。
npm install monaco-editor monaco-editor-webpack-plugin -D
webpack 配置:
arduino
// webpack.config.js
new MonacoWebpackPlugin({ languages: ['javascript', 'typescript', 'html', 'css', 'json'] }),
封装成 vue 组件 Editor:
html
<template>
<div ref="editor" class="repl-editor" :style="{ height: `300px` }" />
</template>
<script setup>
import {
onMounted, ref, defineProps, defineEmits,
} from 'vue'
import * as monaco from "monaco-editor"
import { debounce } from 'lodash-es'
const props = defineProps({
modelValue: String
})
const emits = defineEmits(['update:modelValue'])
const editor = ref()
let monacoEditor = null
onMounted(
() => {
const initCode = props.modelValue
// 初始化 monacoEditor,注意 monacoEditor 使用 ref 类型会导致页面卡死,需要使用 toRaw 解决
monacoEditor = monaco.editor.create(editor.value, {
value: initCode,
language: 'html',
tabSize: 2,
scrollBeyondLastLine: false,
lineNumbers: 'off',
theme: 'vs-dark',
folding: true,
colorDecorators: true,
scrollbar: {
alwaysConsumeMouseWheel: false,
},
})
// 编辑内容变化,触发 update:modelValue 修改绑定的数据
monacoEditor.onDidChangeModelContent(
debounce(() => {
const newValue = monacoEditor.getValue();
emits('update:modelValue', newValue)
}, 200)
)
}
)
</script>
在webpack中使用monaco-editor_can't resolve 'monaco-editor-CSDN博客
vue 使用 monaco-editor 实现在线编辑器 - 三勺 - 博客园
代码运行器
文档库建设踩坑实记录 - 掘金 文章中讲到 vuepress 和 vitepress 文档库搭建的原理利用的是 vue/compiler-sfc 库进行 vue 代码的编译。本文采用了另外一种编译 vue 的方法,即采用 vue3-sfc-loader。
html使用vue.js作为组件引入,vue3-sfc-loader用法!亲测有效!_ChanJcy的博客-CSDN博客 文章介绍了利用 vue3-sfc-loader 在 HTML 中直接使用 vue.js 的方法。
在 vue 中使用 vue3-sfc-loader 的方式如下:
html
// src/Repl/Repl.vue
<template>
<div ref="editorRef"></div>
<Editor v-model="vueCode"/>
</template>
<script setup>
import { ref, createApp, onMounted, onBeforeUnmount, toRaw, watch, } from 'vue'
import Editor from '../Editor/Editor.vue'
import * as vue from 'vue/dist/vue.esm-bundler.js'
import { loadModule } from 'vue3-sfc-loader'
// 通过 code 变量初始化编辑器 Editor 的值,和要编译运行的代码
const props = defineProps({
code: String
})
const editorRef = ref()
const vueCode = ref(props.code)
// app 用来作为编译运行代码的容器
let app = null
// 将代码传进去进行编译
const compileVue = (codeStr) => {
const options = {
// 缓存模块
moduleCache:{
vue,
},
getFile(){
return Promise.resolve(codeStr)
},
addStyle(textContent) {
document.head.appendChild(
Object.assign(document.createElement('style'), {
textContent,
}),
)
},
}
loadModule('repl.vue', options).then(component => {
app = createApp(component)
app.mount(editorRef.value)
})
}
watch(()=> vueCode.value, ()=>{
if (app) {
app.unmount()
}
compileVue(toRaw(vueCode.value))
})
onMounted(()=>{
compileVue(props.code)
})
onBeforeUnmount(()=>{
if (app) {
app.unmount()
}
})
</script>
代码结合
自定义 MdToVue loader
在代码运行器部分已经讲了,通过 vueCode 变量绑定了 Editor 用户输入的代码内容,同时代码运行器会监控 vueCode 变量的变化,当 vueCode 变化的时候,把最新 vueCode 的值传给代码运行器进行编译。从而可以实现一边写代码,一边实时编译查看效果的功能。
使用 md 文件作为原始的代码。如下:
md
```vue
<script setup>
import { ref } from 'vue';
const msg = ref('ComponentA');
</script>
<template>
<h1>{{ msg }}</h1>
</template>
```
这也比较符合使用习惯,文档库中组件使用说明开始的默认代码一般使用 md 文件编写。在 vue 中 md 格式的代码是不能被识别和编译的,需要自定义实现 loader 对 md 文件进行转义。将 md 转为 vue 的 loader 如下:
js
// src/MdToVue/index.js
const { getRepl } = require('./vue-template.js')
const { encodeQuotes } = require('./utils.js')
const marked = require('marked')
module.exports = function markdownToVueLoader(source) {
let code = ''
// 解析 Markdown
const tokens = marked.lexer(source)
for (const token of tokens) {
if (token.type === 'code') {
code = encodeQuotes(token.text)
}
}
return getRepl(code)
}
js
// src/MdToVue/vue-template.js
const { decodeQuotes } = require('./utils.js')
const getRepl = (code) => `
<template><Repl code="${decodeQuotes(code)}" /></template>
`
module.exports = {
getRepl,
}
根据 token.type 标识得到 md 文件中的源码 code,然后将这个 code 传递给封装的 Repl 组件。由于代码中 ' 和 " 会自动闭合,所以需要对其进行转义:
js
// src/MdToVue/utils.js
const encodeQuotes = str => {
const encodedStr = encodeURIComponent(str)
.replace(/'/g, '%27')
.replace(/"/g, '%22')
return encodedStr
}
const decodeQuotes = encodedStr => {
const decodedStr = decodeURIComponent(encodedStr
.replace(/%27/g, "'")
.replace(/%22/g, '"'))
return decodedStr
}
module.exports = {
encodeQuotes,
decodeQuotes
}
修改 webpack.config.js 文件中 loader 使用规则:
js
{
test: /\.md$/,
use: [
'vue-loader',
path.resolve(__dirname, "./src/MdToVue/index.js")
]
},
这样 md 格式的文件会先试用自定义的loader MdToVue 进行编译,然后再使用 vue 文件进行编译。
自定义组件引入
通过上面的一顿操作,md 的文件已经可以被正常编译显示出来。
通常我们还需要将自己封装的组件库也能在编辑器中正常引入和使用,需要怎么做呢?需要在 vue3-sfc-loader 中 loadModule 方法的 moduleCache 配置相应的对应关系,之前我们已经配置了 import * as vue from 'vue/dist/vue.esm-bundler.js'... moduleCache:{ vue, }
vue 模块,表示代码中 vue 模块实际从哪里引入进来的。同样,我们需要配置一下自己的组件库来源:
html
// src/Repl/Repl.vue
<template>
<div ref="editorRef"></div>
<Editor v-model="vueCode"/>
</template>
<script setup>
...
import * as vue from 'vue/dist/vue.esm-bundler.js'
import * as pk from '../Component/index.js'
const compileVue = (codeStr) => {
const options = {
moduleCache:{
vue,
pk, // 添加 pk
},
...
}
}
...
</script>
这样在 md 中引入就可以正常使用自己封装的组件库 pk 了:
md
```vue
<script setup>
import { ref } from 'vue';
import { HelloWorld } from 'pk';
const msg = ref('ComponentA');
</script>
<template>
<h1>{{ msg }}</h1>
<HelloWorld />
</template>
```