Vue3 CodeMirror使用快速上手

1,026 阅读6分钟

1. 快速入门

第一步:下包

codemirrorcodemirror-editor-vue3包:

yarn add codemirror-editor-vue3 codemirror@">=5.64.0 <6"

第二步:引入:

import Codemirror from "codemirror-editor-vue3";
import type { CmComponentRef } from "codemirror-editor-vue3";

第三步,创建组件

目录下@/components/codeEditor.vue,定义变量codeVal和cmOption

const codeVal = ref<string>('')
const cmOption = ref<object>({})


// 内容改变触发
function onChange (val: any) {
    // emits('Change')
    console.log(val, 'val');
}

然后再定义接受父组件的值

const props = defineProps<{
    code: string,
    option: object
}>()

使用引入进来的组件

<Codemirror 
    :value="codeVal"
    :options="cmOption"
    border
    placeholder="测试占位符"
    :height="200"
    @Change="onChange"
></Codemirror>

定义侦听器,一旦父组件传来的值codeoption变化,则更新子组件的codeValcmOption

watch(() => props.code, (newVal, oldVal) => {
    console.log(newVal, 'newVal');

    codeVal.value = newVal
}, {
    immediate: true
})
watch(() => props.option, (newVal, oldVal) => {
    cmOption.value = newVal
}, {
    immediate: true
})

第四步,在父组件里面使用这个组件

import codeEditor from '@/components/codeEditor.vue'
import { reactive, ref } from 'vue'


// 要展示的数据
const code = ref(
`const code = ref("var i = 0;\nfor (; i < 9; i++) {\n    console.log(i);\n    // more statements\n}\n    ");`
);
// 配置项
const cmOption = reactive({
    mode: "text/javascript",
})
// 代码编辑框值修改后触发
function changeValue (val) {
    console.log(val, 'val');
}

结构:

<div>
    <codeEditor
        :code="code"
        :option="cmOption"
        @change="changeValue"
    ></codeEditor>
</div>

现在的效果:

image.png

第五步,调整代码靠左边显示

父组件里面设置左对齐

<style lang="less" scoped>
.Home {
    text-align: left;
}
</style>

image.png

2. 其他功能

2.1 实现切换语言高亮

在父组件里面,首先我们增加一个下拉框,用于切换语言的选择, 这里我们用element-plus的下拉框el-select

<el-select v-model="cmOption.mode" style="width: 150px;">
    <el-option 
        v-for="lang in langOptions" 
        :key="lang.type" 
        :value="lang.value" 
        :label="lang.label"
    ></el-option>
</el-select>

声明数据

// 语言的选项
const langOptions = ref([
    {
        label: 'JavaScript',
        value: 'text/javascript'
    },
    {
        label: 'Yaml',
        value: 'text/x-yaml'
    },
    {
        label: 'HTML',
        // value: 'text/htmlembedded'
        value: 'application/x-aspx'
    },
    {
        label: 'CSS',
        value: 'text/css'
    }
])

codeEditor.vue组件里:

import "codemirror/mode/javascript/javascript.js";
+import "codemirror/mode/htmlmixed/htmlmixed.js";
+import "codemirror/mode/htmlembedded/htmlembedded.js";
+import "codemirror/mode/css/css.js";
+import "codemirror/mode/yaml/yaml.js";

子组件里面,我们修改一下之前的监听器,改为监听props的option.mode值,然后修改当前组件的cmOption.mode

watch(() => props.option.mode, (newVal, oldVal) => {
    cmOption.value.mode = newVal
}, {
    immediate: true
})

请确保你在修改下拉框的语言的时候,子组件的接受到的mode是修改后的:

image.png

这里的关键点1:

这个页面找到对应的语言点击进去:

image.png

把对应的type传给代码编辑器:

image.png

关键点2是要引入对应的js文件,你可以在node_modules/codemirror/mode找到你对应语言的文件,一般是语言/语言.js的格式:

image.png

最终实现的效果如下:

代码编辑框.gif

2.2 更新代码主题

这节要实现的效果:

代码编辑框-切换主题.gif

在父组件中,传入要使用的主题变量

const cmOption = reactive({
    mode: "text/javascript",
    hintOptions: {
        completeSingle: false
    },
+    theme: '3024-day'
})

增加一个主题选择器下拉菜单

<el-select v-model="cmOption.theme" style="width: 150px;">
    <el-option 
        v-for="lang in themeOption" 
        :key="lang.type" 
        :value="lang.value" 
        :label="lang.label"
    ></el-option>
</el-select>

增加主题的选项

// 主题的选项
const themeOption = ref([
    {
        label: '3024-day',
        value: '3024-day'
    },
    {
        label: '3024-night',
        value: '3024-night'
    },
    {
        label: 'base16-dark',
        value: 'base16-light'
    },
    {
        label: 'darcula',
        value: 'darcula'
    },
     {
        label: 'dracula',
        value: 'dracula'
    }
])

在子组件里引入对应的css主题文件

// 代码主题
import "codemirror/theme/3024-day.css";
import "codemirror/theme/3024-night.css";
import "codemirror/theme/base16-dark.css";
import "codemirror/theme/base16-light.css";
import "codemirror/theme/darcula.css";
import "codemirror/theme/dracula.css";

增加主题属性并监听主题值的变化就可以啦

// 增加主题属性
const cmOption = ref<optionType>({
    mode: '',
    theme: ''
})

// 侦听主题
watch(() => props.option.theme, (newVal, oldVal) => {
    if (newVal) {
        cmOption.value.theme = newVal
    }
}, {
    immediate: true
})
const emits = defineEmits<{
    (e: 'Change'):void
}>()

2.3 语法校验

举个例子,如何实现yaml语言的语法校验呢?

tutieshi_640x373_6s.gif

父组件中传入lintgutter配置项

const cmOption = reactive({
    mode: "text/x-yaml",
    hintOptions: {
        completeSingle: false
    },
    theme: '3024-day',
+    lint: true,
+    gutters: ["CodeMirror-lint-markers"],
})

子组件里面增加该类型

type optionType = {
    mode: string,
    theme?: string,
+    lint?: boolean,
+    gutters?: string[],
}
const cmOption = ref<optionType>({
    mode: '',
    theme: '',
+    lint: false,
+    gutters: [],
})
// 是否开启校验
+watch(() => props.option.lint, (newVal, oldVal) => {
    cmOption.value.lint = newVal
}, {
    immediate: true,
})
// 侦听gutters
+watch(() => props.option.gutters, (newVal, oldVal) => {
    if (newVal) {
        cmOption.value.gutters = [...newVal]
    }
}, {
    immediate: true
})

子组件里面引入cssjs文件

import "codemirror/addon/lint/yaml-lint.js"
import "codemirror/addon/lint/lint.css"
import "codemirror/addon/lint/lint.js"

下包

yarn add js-yaml

挂载到window全局上

window.jsyaml = require('js-yaml')

如果ts报错

image.png

则创建/components/global.d.ts文件

interface Window {
    jsyaml: any
}

tsconfig.json里面增加如下,这样上面的global文件里面就能检测到了

"include": [
    "src/**/*.ts",
+    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
],

到此代码校验的功能能够实现了,但是还有缺点,比如当yaml语法报错时,切换到别的语言,报错信息应该消除才对

tutieshi_640x373_8s.gif

目前我想到的方法暂时只有,切换语言mode时,先短暂将lint关掉,然后nextTick里面再打开

// 代码语言
watch(() => props.option.mode, (newVal, oldVal) => {
    cmOption.value.mode = newVal
+    cmOption.value.lint = false
    nextTick(() => {
+        cmOption.value.lint = true
    })
}, {
    immediate: true,
})

2.4 自动换行和行号

自动换行依靠lineWrapping属性即可实现

const cmOption = reactive({
    mode: "text/x-yaml",
    hintOptions: {
        completeSingle: false
    },
    theme: '3024-day',
    lineWrapping: true,
    lint: true,
    gutters: ["CodeMirror-lint-markers"],
})

image.png

行号属性对应lineNumbers,默认是true

const cmOption = reactive({
    mode: "text/x-yaml",
    hintOptions: {
        completeSingle: false
    },
    theme: '3024-day',
    lineWrapping: true,
    lint: true,
+    lineNumbers: false,
    gutters: ["CodeMirror-lint-markers"],
})

image.png

2.5 上传文件

tutieshi_640x373_5s.gif

增加el-upload上传框

<el-upload
    v-model:file-list="fileList"
    class="upload-demo"
    action="#"
    :auto-upload="false"
    multiple
    :on-remove="handleRemove"
    :limit="3"
    :on-exceed="handleExceed"
    :on-success="handleSuccess"
    :on-change="handleChange"
>
    <el-button type="primary">上传yaml文件</el-button>
</el-upload>

增加数据和上传逻辑,在on-change方法里面进行上传操作

// 上传文件
const fileList = ref([])

// 移除
function handleRemove () {

}

// 文件数量超过了
function handleExceed () {

}

// 上传成功
function handleSuccess () {

}

// 上传文件成功
function handleChange (uploadFile, uploadFiles) {
    console.log(uploadFiles, 'uploadFiles');
    if (uploadFiles.length > 0) {
        const reader = new FileReader()
        reader.onload = function (e) {
            console.log(e, 'e');
            let val = e.currentTarget.result
            if (val) {
                code.value = val
            }
        }
        reader.readAsText(uploadFiles[0].raw)
    }
}

其中利用FileReader这个API来进行上传操作,readAsText读取文件里面的内容,读取完毕在onload事件里面赋值给code.value

2.6 下载文件

tutieshi_640x373_5s.gif

下载文件对应的代码

function downloadYaml () {
    try {
        let blob = new Blob([code.value], { type: 'text/plain' });
        let objectUrl = URL.createObjectURL(blob);
        let a = document.createElement('a');
        document.body.appendChild(a);
        a.setAttribute('style', 'display:none');
        a.setAttribute('href', objectUrl);
        a.setAttribute('download', 'myYaml' + '.yaml');
        try {
            a.click();
        } catch (e) {
            navigator.msSaveBlob(blob, 'myYaml');
        }
        URL.revokeObjectURL(objectUrl);
    } catch (error) {
        console.log(error, 'error');
    }
}

核心逻辑,构建Blob数据,类型是text/plain,并利用URL.createObjectURL创建一个URL,然后把URL传递给a标签,触发a标签的点击事件

下载文件的按钮

<el-button
    type="default"
    class="btn-dl"
    @click="downloadYaml"
>下载yaml文件</el-button>

2.7 利用yaml生成动态表单

假设我有这样一段yaml文件:

# 示例 YAML 文件
version: '1.0'
services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html
  database:
    image: mysql:5.7
    cpu: 1.1
    mem: 3.0
    age: 
    environment:
      MYSQL_ROOT_PASSWORD: example
      MYSQL_DATABASE: mydb

用户希望根据services下面的database里面的字段,生成对应的表单项,里面填的属性名是什么,表单的label就是什么值:

image.png

这里涉及到两步,第一把yaml文件转化为js对象,第二,如何渲染为动态表单呢?

第一步,在jsyaml里面有提供load方法,可以直接转化:

let obj = window.jsyaml.load(code.value)

转换后的对象:

image.png

第二步,拿到里面所有的keys,遍历生成数组,再v-for循环生成表单

// 点击进入下一步
const formList = ref([])
function handleNext () {
    try {
        let obj = window.jsyaml.load(code.value)
        console.log(obj, 'obj');

        let database = obj.services.database
        for (const key in database) {
            formList.value.push({
                type: 'input',
                label: key,
                value: database[key]
            })
        }
    } catch (error) {
        console.log(error, 'error');
    }
}

遍历生成动态表单

<!-- 生成动态表单 -->
<div class="form-list">
    <el-form v-if="formList.length" :model="form" style="width: 600px" label-width="100px">
        <el-form-item 
            v-for="formItem in formList" 
            :key="formItem.label" 
            :label="formItem.label"
        >
            <el-input 
                :type="formItem.type" 
                style="width: 200px" 
                v-model="formItem.label"
            />
        </el-form-item>
        <el-form-item>
            <el-button type="primary" @click="onSubmit">Create</el-button>
            <el-button>Cancel</el-button>
        </el-form-item>
    </el-form>
</div>