1. 快速入门
第一步:下包
codemirror和codemirror-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>
定义侦听器,一旦父组件传来的值code和option变化,则更新子组件的codeVal和cmOption
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>
现在的效果:
第五步,调整代码靠左边显示
父组件里面设置左对齐
<style lang="less" scoped>
.Home {
text-align: left;
}
</style>
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是修改后的:
这里的关键点1:
在这个页面找到对应的语言点击进去:
把对应的type传给代码编辑器:
关键点2是要引入对应的js文件,你可以在node_modules/codemirror/mode找到你对应语言的文件,一般是语言/语言.js的格式:
最终实现的效果如下:
2.2 更新代码主题
这节要实现的效果:
在父组件中,传入要使用的主题变量
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语言的语法校验呢?
父组件中传入lint和gutter配置项
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
})
子组件里面引入css和js文件
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报错
则创建/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语法报错时,切换到别的语言,报错信息应该消除才对
目前我想到的方法暂时只有,切换语言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"],
})
行号属性对应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"],
})
2.5 上传文件
增加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 下载文件
下载文件对应的代码
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就是什么值:
这里涉及到两步,第一把yaml文件转化为js对象,第二,如何渲染为动态表单呢?
第一步,在jsyaml里面有提供load方法,可以直接转化:
let obj = window.jsyaml.load(code.value)
转换后的对象:
第二步,拿到里面所有的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>