Vue Playground 演练场源码解读(三)
在《 Vue Playground 演练场源码解读(二)》文章中咱们介绍了以下几点:
importmap
的相关用法。- 简单介绍了
hash
和history
模式的区别。 - 介绍了
playground
分享功能利用hash
模式的实现。 - 使用
hash
模式下利用fflate
对数据进行压缩、解压。 - 介绍了核心源码
useVueImportMap 和 useStore
的实现逻辑
咱们接着对Vue Playground
源码进行拆分学习。
代码编辑器应用
在Vue Playground
源码中,代码编辑器有两种Monaco-Editor 和 Codemirror
,实际使用的是 Monaco-Editor
,默认分支中还保留了Codemirror
的相关代码。
-
codemirror
是一个开源的代码编辑器,它提供了许多功能,如语法高亮、自动补全、代码提示、代码折叠等。适合需要轻量级、高度可定制的代码编辑器的项目,特别是对性能要求较高且需要简单集成的场景。 -
Monaco-Editor
适合需要功能强大的编辑器,类似于 VS Code 的体验,支持复杂功能如 IntelliSense 和实时错误检测的项目
codemirror
在使用codemirror
代码层面也比较简单css变量 控制主题样式
引入需要的插件以及样式,下面是Vue Playground
源码中的应用,模式(modes)、插件(addons)、快捷键(keymap)
import CodeMirror from 'codemirror'
import 'codemirror/addon/dialog/dialog.css'
import './codemirror.css'
// modes
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/mode/css/css.js'
import 'codemirror/mode/htmlmixed/htmlmixed.js'
// addons
import 'codemirror/addon/edit/closebrackets.js'
import 'codemirror/addon/edit/closetag.js'
import 'codemirror/addon/comment/comment.js'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/brace-fold.js'
import 'codemirror/addon/fold/indent-fold.js'
import 'codemirror/addon/fold/comment-fold.js'
import 'codemirror/addon/search/search.js'
import 'codemirror/addon/search/searchcursor.js'
import 'codemirror/addon/dialog/dialog.js'
// keymap
import 'codemirror/keymap/sublime.js'
export default CodeMirror
用法也非常简单如下:
import CodeMirror from './codemirror'
let editor: CodeMirror.Editor
editor = CodeMirror(el.value!, {
value: '',
mode: props.mode,
readOnly: props.readonly,
tabSize: 2,
lineWrapping: true,
lineNumbers: true,
...addonOptions,
})
const cur = editor.getValue() // 获取值
editor.setOption('mode', props.mode) // 设置模式
editor.refresh() // 刷新
editor.on('change', emitChangeEvent)// 有变化时触发事件
editor.off('change', emitChangeEvent) // 解除监听
为什么Vue Playground
没有使用codemirror
呢?
仅个人观点:
-
- 现在大多用户都习惯
vscode
编辑器(Monaco-Editor
)
- 现在大多用户都习惯
-
codemirror
在某些功能不满足Vue Playground
了,比如下图的一些提示就满足不了。
Monaco-Editor
咱们先看下这个Monaco-Editor
编辑器的基础用法再一步步去分析,如下图创建一个 Hello world!
, Monaco-Editor playground 演武场地址可以查看部分demo
以下就是基础用法示例:
const value = /* set from `myEditor.getModel()`: */ `function hello() {
alert('Hello world!');
}`;
// Hover on each property to see its docs!
const myEditor = monaco.editor.create(document.getElementById("container"), {
value,
language: "javascript",
automaticLayout: true,
});
回到Vue Playground
源码中,咱们根据源码去一步步了解Monaco-Editor
, 因为Monaco-Editor
的官方文档真的不是那么好读。
下图中getOrCreateModel
方法主要就是创建视图,要么设置视图数据。这个不难理解哈,比如有文件a.js
,我点击后其实就是切换了下数据源(a.js的数据),我想创建b.js
这个时候就会走createModel
API了。
这里可以想想这个editor
会维护一个model
的MAP对象
,供用户去获取,新增、修改、删除
主题切换使用的是Shiki 式、Shiki 式中文文档 可以把它看作对常见的编辑器做了一些主题的集成。
Vue Playground
源码中应用
import * as monaco from 'monaco-editor-core'
import { createHighlighterCoreSync } from 'shiki/core'
import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs'
import { shikiToMonaco } from '@shikijs/monaco'
import langVue from 'shiki/langs/vue.mjs'
import langTsx from 'shiki/langs/tsx.mjs'
import langJsx from 'shiki/langs/jsx.mjs'
import themeDark from 'shiki/themes/dark-plus.mjs'
import themeLight from 'shiki/themes/light-plus.mjs'
let registered = false
export function registerHighlighter() {
if (!registered) {
const highlighter = createHighlighterCoreSync({
themes: [themeDark, themeLight],
langs: [langVue, langTsx, langJsx],
engine: createJavaScriptRegexEngine(),
})
monaco.languages.register({ id: 'vue' })
shikiToMonaco(highlighter, monaco)
registered = true
}
return {
light: themeLight.name!,
dark: themeDark.name!,
}
}
// update theme
watch(replTheme, (n) => {
// editorInstance updateOptions 是 Monaco-Editor 的更新字段的API
editorInstance.updateOptions({
theme: n === 'light' ? theme.light : theme.dark,
})
})
这种对Monaco-Editor
主题切换可以稍微了解下,等你去用Monaco-Editor
做项目时,你们还刚好有主题切换就能用到了。
我把对Monaco-Editor
交互的关键代码都拿出来下
import * as monaco from 'monaco-editor-core'
let editorInstance: monaco.editor.IStandaloneCodeEditor
function emitChangeEvent() {
emit('change', editorInstance.getValue()) //获取数据触发change事件
}
// 创建编辑器
editorInstance = monaco.editor.create(containerRef.value, {
...(props.readonly
? { value: props.value, language: lang.value }
: { model: null }),
fontSize: 13,
tabSize: 2,
theme: replTheme.value === 'light' ? theme.light : theme.dark,
readOnly: props.readonly,
automaticLayout: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
inlineSuggest: {
enabled: false,
},
fixedOverflowWidgets: true,
...editorOptions.value.monacoOptions,
})
watch(
() => props.value,
(value) => {
if (editorInstance.getValue() === value) return
editorInstance.setValue(value || '') // 数据有更新 设置数据
},
{ immediate: true },
)
// 切换不同的文件,加载不同的语言 js、css、vue、json ....
watch(lang, (lang) =>
monaco.editor.setModelLanguage(editorInstance.getModel()!, lang),
)
// 因为自动保存了,就不需要 手动保存了
editorInstance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
// ignore save event
})
watch(
autoSave,
(autoSave) => {
if (autoSave) {
const disposable =
editorInstance.onDidChangeModelContent(emitChangeEvent) // 有变化触发
onWatcherCleanup(() => disposable.dispose())
}
},
{ immediate: true },
)
env.ts
主要是对Monaco-Editor
的一些配置,其中用到了Web Worker
去多线程做载一些东西这里就不多说了
- 主要是处理
Monaco-Editor
的model
。Uri.parse
你可理解获取model
data的url
export function initMonaco(store: Store) {
if (initted) return
loadMonacoEnv(store)
watchEffect(() => {
// create a model for each file in the store
for (const filename in store.files) {
const file = store.files[filename]
if (editor.getModel(Uri.parse(`file:///${filename}`))) continue
getOrCreateModel(
Uri.parse(`file:///${filename}`),
file.language,
file.code,
)
}
// dispose of any models that are not in the store
for (const model of editor.getModels()) {
const uri = model.uri.toString()
if (store.files[uri.substring('file:///'.length)]) continue
if (uri.startsWith('file:///node_modules')) continue
if (uri.startsWith('inmemory://')) continue
model.dispose()
}
})
initted = true
}
reloadLanguageTools
方法主要是载@volar/monaco
-
volar.activateMarkers
用于激活 Monaco 编辑器中的诊断功能,即错误和警告标记。它会根据语言服务提供的诊断信息,在编辑器中显示错误和警告的标记 -
volar.activateAutoInsertion
用于激活 Monaco 编辑器中的自动插入功能,例如自动闭合标签 -
volar.registerProviders
用于注册 Monaco 编辑器的语言服务提供者,例如代码补全、定义跳转等功能
import * as volar from '@volar/monaco'
export async function reloadLanguageTools(store: Store) {
disposeVue?.()
let dependencies: Record<string, string> = {
...store.dependencyVersion,
}
if (store.vueVersion) {
dependencies = {
...dependencies,
vue: store.vueVersion,
'@vue/compiler-core': store.vueVersion,
'@vue/compiler-dom': store.vueVersion,
'@vue/compiler-sfc': store.vueVersion,
'@vue/compiler-ssr': store.vueVersion,
'@vue/reactivity': store.vueVersion,
'@vue/runtime-core': store.vueVersion,
'@vue/runtime-dom': store.vueVersion,
'@vue/shared': store.vueVersion,
}
}
if (store.typescriptVersion) {
dependencies = {
...dependencies,
typescript: store.typescriptVersion,
}
}
const worker = editor.createWebWorker<WorkerLanguageService>({
moduleId: 'vs/language/vue/vueWorker',
label: 'vue',
host: new WorkerHost(),
createData: {
tsconfig: store.getTsConfig?.() || {},
dependencies,
} satisfies CreateData,
})
const languageId = ['vue', 'javascript', 'typescript']
const getSyncUris = () =>
Object.keys(store.files).map((filename) => Uri.parse(`file:///${filename}`))
const { dispose: disposeMarkers } = volar.activateMarkers(
worker,
languageId,
'vue',
getSyncUris,
editor,
)
const { dispose: disposeAutoInsertion } = volar.activateAutoInsertion(
worker,
languageId,
getSyncUris,
editor,
)
const { dispose: disposeProvides } = await volar.registerProviders(
worker,
languageId,
getSyncUris,
languages,
)
disposeVue = () => {
disposeMarkers()
disposeAutoInsertion()
disposeProvides()
}
}
loadMonacoEnv
主要是对Monaco-Editor
语言的设置,以及配置(不配置应该也能用,可能体验不是那么好,这个需要去深挖Monaco-Editor
了,这里可以先记着,等需要用到了直接拿来复制就行。就跟你的eslint
一样你其实不用知道每个字段是干啥的,有需要再去具体配置),如下图
总结
本篇中咱们学习到了:
codemirror
代码编辑器的基础用法,以及Vue Playground
源码中具体实践monaco-editor
代码编辑器的基础用法,以及Vue Playground
源码中的具体实践。