怎么让自己的 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>
    ```
相关推荐
y先森6 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy6 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189119 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
天天进步20153 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js