怎么让自己的 vue 组件库可以在线编辑?

如果想让自己的组件库可以做到在线编辑的效果,要怎么实现呢?文档库建设踩坑实记录 - 掘金 文章中记录了使用 vuepress 和 vitepress 文档库搭建的原理和存在的问题。本文讨论的是不使用 vuepress 和 vitepress 实现vue 组件在线编辑功能。

要实现 vue 组件库的在线编辑功能,有3个关键实现点:

  1. 代码编辑器。能够让用户输入代码,进行编辑。
  2. 代码运行器。就是将 vue 代码进行实时编译可以在 HTML 运行,显示出代码的运行结果。
  3. 代码结合,将代码编辑器的代码传递给代码运行器。将代码编辑器的代码通过 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>
    ```
相关推荐
布瑞泽的童话22 分钟前
无需切换平台?TuneFree如何搜罗所有你爱的音乐
前端·vue.js·后端·开源
白鹭凡34 分钟前
react 甘特图之旅
前端·react.js·甘特图
2401_8628867838 分钟前
蓝禾,汤臣倍健,三七互娱,得物,顺丰,快手,游卡,oppo,康冠科技,途游游戏,埃科光电25秋招内推
前端·c++·python·算法·游戏
书中自有妍如玉1 小时前
layui时间选择器选择周 日月季度年
前端·javascript·layui
Riesenzahn1 小时前
canvas生成图片有没有跨域问题?如果有如何解决?
前端·javascript
f8979070701 小时前
layui 可以使点击图片放大
前端·javascript·layui
小贵子的博客1 小时前
ElementUI 用span-method实现循环el-table组件的合并行功能
javascript·vue.js·elementui
忘不了情1 小时前
左键选择v-html绑定的文本内容,松开鼠标后出现复制弹窗
前端·javascript·html
码上飞扬1 小时前
前端框架对比选择:如何在众多技术中找到最适合你的
vue.js·前端框架·react·angular·svelte