怎么让自己的 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>
    ```
相关推荐
还是大剑师兰特4 分钟前
面试题:ES6模块与CommonJS模块有什么异同?
前端·es6·大剑师
胡西风_foxww19 分钟前
【ES6复习笔记】数值扩展(16)
前端·笔记·es6·扩展·数值
mosen86821 分钟前
uniapp中uni.scss如何引入页面内或生效
前端·uni-app·scss
白云~️22 分钟前
uniappX 移动端单行/多行文字隐藏显示省略号
开发语言·前端·javascript
沙尘暴炒饭24 分钟前
uniapp 前端解决精度丢失的问题 (后端返回分布式id)
前端·uni-app
昙鱼38 分钟前
springboot创建web项目
java·前端·spring boot·后端·spring·maven
天天进步201544 分钟前
Vue项目重构实践:如何构建可维护的企业级应用
前端·vue.js·重构
2402_8575834944 分钟前
“协同过滤技术实战”:网上书城系统的设计与实现
java·开发语言·vue.js·科技·mfc
小华同学ai1 小时前
vue-office:Star 4.2k,款支持多种Office文件预览的Vue组件库,一站式Office文件预览方案,真心不错
前端·javascript·vue.js·开源·github·office
APP 肖提莫1 小时前
MyBatis-Plus分页拦截器,源码的重构(重构total总数的计算逻辑)
java·前端·算法