在有些項(xiàng)目中有時(shí)需要讓用戶輸入一些代碼,使用戶能自主處理一些數(shù)據(jù)邏輯,但如果讓用戶直接注入代碼,會(huì)有很高的風(fēng)險(xiǎn)(惡意代碼).
因此我們可以將用戶輸入的代碼以字符串的形式傳給后端,讓后端根據(jù)所傳的代碼字符串處理數(shù)據(jù).
為了使用戶能有一個(gè)較好的代碼輸入體驗(yàn),我們可以使用monaco-editor
npm install --save monaco-editor
使用的版本
"monaco-editor": "^0.20.0",
組件MonacoEditor.vue文件
<template>
<div ref="editor" class="main"></div>
</template>
<script>
import * as monaco from 'monaco-editor'
import createSqlCompleter from './util/sql-completion'
import createJavascriptCompleter from './util/javascript-completion'
import registerLanguage from './util/log-language'
const global = {}
const getHints = model => {
let id = model.id.substring(6)
return (global[id] && global[id].hints) || []
}
monaco.languages.registerCompletionItemProvider(
'sql',
createSqlCompleter(getHints)
)
monaco.languages.registerCompletionItemProvider(
'javascript',
createJavascriptCompleter(getHints)
)
registerLanguage(monaco)
/**
* monaco options
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html
*/
export default {
props: {
options: {
type: Object,
default () {
return {}
}
},
value: {
type: String,
required: false
},
language: {
type: String
},
hints: {
type: Array,
default () {
return []
}
}
},
name: 'MonacoEditor',
data () {
return {
editorInstance: null,
defaultOptions: {
theme: 'vs-dark',
fontSize: 14
}
}
},
watch: {
value () {
if (this.value !== this.editorInstance.getValue()) {
this.editorInstance.setValue(this.value)
}
}
},
mounted () {
this.initEditor()
global[this.editorInstance._id] = this
window.addEventListener('resize', this.layout)
},
destroyed () {
this.editorInstance.dispose()
global[this.editorInstance._id] = null
window.removeEventListener('resize', this.layout)
},
methods: {
layout () {
this.editorInstance.layout()
},
undo () {
this.editorInstance.trigger('anyString', 'undo')
this.onValueChange()
},
redo () {
this.editorInstance.trigger('anyString', 'redo')
this.onValueChange()
},
getOptions () {
let props = { value: this.value }
this.language !== undefined && (props.language = this.language)
let options = Object.assign({}, this.defaultOptions, this.options, props)
return options
},
onValueChange () {
this.$emit('input', this.editorInstance.getValue())
this.$emit('change', this.editorInstance.getValue())
},
initEditor () {
this.MonacoEnvironment = {
getWorkerUrl: function () {
return './editor.worker.bundle.js'
}
}
this.editorInstance = monaco.editor.create(
this.$refs.editor,
this.getOptions()
)
this.editorInstance.onContextMenu(e => {
this.$emit('contextmenu', e)
})
this.editorInstance.onDidChangeModelContent(() => {
this.onValueChange()
})
this.editorInstance.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S,
() => {
this.$emit('save', this.editorInstance.getValue())
}
)
}
}
}
</script>
<style scoped>
.main /deep/ .view-lines * {
font-family: Consolas, "Courier New", monospace !important;
}
</style>
Util/javascript-completion.js
/* eslint-disable */
import * as monaco from 'monaco-editor'
// js 有內(nèi)置提示
function createCompleter (getExtraHints) {
const createSuggestions = function (model, textUntilPosition) {
let text = model.getValue()
textUntilPosition = textUntilPosition.replace(/[\*\[\]@\$\(\)]/g, '').replace(/(\s+|\.)/g, ' ')
let arr = textUntilPosition.split(/[\s;]/)
let activeStr = arr[arr.length - 1]
let len = activeStr.length
let rexp = new RegExp('([^\\w]|^)' + activeStr + '\\w*', 'gim')
let match = text.match(rexp)
let mergeHints = Array.from(new Set([...getExtraHints(model)]))
.sort()
.filter(ele => {
let rexp = new RegExp(ele.substr(0, len), 'gim')
return (match && match.length === 1 && ele === activeStr) ||
ele.length === 1 ? false : activeStr.match(rexp)
})
return mergeHints.map(ele => ({
label: ele,
kind: monaco.languages.CompletionItemKind.Text,
documentation: ele,
insertText: ele
}))
}
return {
provideCompletionItems (model, position) {
let textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
})
return { suggestions: createSuggestions(model, textUntilPosition) }
}
}
}
export default createCompleter
Util/log-language.js
function registerLanguage (monaco) {
monaco.languages.register({
id: 'log'
})
monaco.languages.setMonarchTokensProvider('log', {
tokenizer: {
root: [
[/(^[=a-zA-Z].*|\d\s.*)/, 'log-normal'],
[/\sERROR\s.*/, 'log-error'],
[/\sWARN\s.*/, 'log-warn'],
[/\sINFO\s.*/, 'log-info'],
[
/^([0-9]{4}||[0-9]{2})-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]{3})?/,
'log-date'
],
[
/^[0-9]{2}\/[0-9]{2}\/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]{3})?/,
'log-date'
],
[/(^\*\*Waiting queue:.*)/, 'log-info'],
[/(^\*\*result tips:.*)/, 'log-info']
]
}
})
monaco.editor.defineTheme('log', {
base: 'vs',
inherit: true,
rules: [{
token: 'log-info',
foreground: '4b71ca'
},
{
token: 'log-error',
foreground: 'ff0000',
fontStyle: 'bold'
},
{
token: 'log-warn',
foreground: 'FFA500'
},
{
token: 'log-date',
foreground: '008800'
},
{
token: 'log-normal',
foreground: '808080'
}
],
colors: {
'editor.lineHighlightBackground': '#ffffff',
'editorGutter.background': '#f7f7f7'
}
})
}
export default registerLanguage
Util/sql-completion.js
/* eslint-disable */
import * as monaco from 'monaco-editor'
const hints = [
'SELECT',
'INSERT',
'DELETE',
'UPDATE',
'CREATE TABLE',
'DROP TABLE',
'ALTER TABLE',
'CREATE VIEW',
'DROP VIEW',
'CREATE INDEX',
'DROP INDEX',
'CREATE PROCEDURE',
'DROP PROCEDURE',
'CREATE TRIGGER',
'DROP TRIGGER',
'CREATE SCHEMA',
'DROP SCHEMA',
'CREATE DOMAIN',
'ALTER DOMAIN',
'DROP DOMAIN',
'GRANT',
'DENY',
'REVOKE',
'COMMIT',
'ROLLBACK',
'SET TRANSACTION',
'DECLARE',
'EXPLAN',
'OPEN',
'FETCH',
'CLOSE',
'PREPARE',
'EXECUTE',
'DESCRIBE',
'FROM',
'ORDER BY']
function createCompleter (getExtraHints) {
const createSuggestions = function (model, textUntilPosition) {
let text = model.getValue()
textUntilPosition = textUntilPosition.replace(/[\*\[\]@\$\(\)]/g, '').replace(/(\s+|\.)/g, ' ')
let arr = textUntilPosition.split(/[\s;]/)
let activeStr = arr[arr.length - 1]
let len = activeStr.length
let rexp = new RegExp('([^\\w]|^)' + activeStr + '\\w*', 'gim')
let match = text.match(rexp)
let textHints = !match ? []
: match.map(ele => {
let rexp = new RegExp(activeStr, 'gim')
let search = ele.search(rexp)
return ele.substr(search)
})
let mergeHints = Array.from(new Set([...hints, ...textHints, ...getExtraHints(model)]))
.sort()
.filter(ele => {
let rexp = new RegExp(ele.substr(0, len), 'gim')
return (match && match.length === 1 && ele === activeStr) ||
ele.length === 1 ? false : activeStr.match(rexp)
})
return mergeHints.map(ele => ({
label: ele,
kind: hints.indexOf(ele) > -1
? monaco.languages.CompletionItemKind.Keyword
: monaco.languages.CompletionItemKind.Text,
documentation: ele,
insertText: ele
}))
}
return {
provideCompletionItems (model, position) {
let textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
})
return { suggestions: createSuggestions(model, textUntilPosition) }
}
}
}
export default createCompleter
在使用的頁面
<monaco-editor
v-model.trim="transformScript"
language="javascript"
style="height: 380px"
/>
import MonacoEditor from '@/module/components/MonacoEditor/MonacoEditor'
transformScript: '', // 執(zhí)行代碼文本