Vue Playground 演练场源码解读(三)

114 阅读5分钟

Vue Playground 演练场源码解读(三)

在《 Vue Playground 演练场源码解读(二)》文章中咱们介绍了以下几点:

  1. importmap 的相关用法。
  2. 简单介绍了hashhistory模式的区别。
  3. 介绍了playground分享功能利用hash模式的实现。
  4. 使用hash模式下利用fflate对数据进行压缩、解压。
  5. 介绍了核心源码useVueImportMap 和 useStore的实现逻辑

咱们接着对Vue Playground源码进行拆分学习。

代码编辑器应用

Vue Playground源码中,代码编辑器有两种Monaco-Editor 和 Codemirror,实际使用的是 Monaco-Editor,默认分支中还保留了Codemirror的相关代码。

  • codemirror是一个开源的代码编辑器,它提供了许多功能,如语法高亮、自动补全、代码提示、代码折叠等。适合需要轻量级、高度可定制的代码编辑器的项目,特别是对性能要求较高且需要简单集成的场景。

  • Monaco-Editor适合需要功能强大的编辑器,类似于 VS Code 的体验,支持复杂功能如 IntelliSense 和实时错误检测的项目

codemirror

在使用codemirror代码层面也比较简单css变量 控制主题样式

12.png

引入需要的插件以及样式,下面是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呢?

仅个人观点:

    1. 现在大多用户都习惯vscode编辑器(Monaco-Editor
    1. codemirror在某些功能不满足Vue Playground了,比如下图的一些提示就满足不了。

13.png

Monaco-Editor

咱们先看下这个Monaco-Editor编辑器的基础用法再一步步去分析,如下图创建一个 Hello world!, Monaco-Editor playground 演武场地址可以查看部分demo

14.png

以下就是基础用法示例:

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会维护一个modelMAP对象,供用户去获取,新增、修改、删除

15.png

主题切换使用的是Shiki 式Shiki 式中文文档 可以把它看作对常见的编辑器做了一些主题的集成。

16.png

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去多线程做载一些东西这里就不多说了

  1. 主要是处理 Monaco-EditormodelUri.parse你可理解获取modeldata的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
}
  1. 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()
  }
}
  1. loadMonacoEnv 主要是对Monaco-Editor语言的设置,以及配置(不配置应该也能用,可能体验不是那么好,这个需要去深挖Monaco-Editor了,这里可以先记着,等需要用到了直接拿来复制就行。就跟你的eslint一样你其实不用知道每个字段是干啥的,有需要再去具体配置),如下图

17.png

总结

本篇中咱们学习到了:

  1. codemirror 代码编辑器的基础用法,以及Vue Playground源码中具体实践
  2. monaco-editor 代码编辑器的基础用法,以及 Vue Playground源码中的具体实践。