mcpField
L
lit-and-monaco-editor

lit-and-monaco-editor

github.com/rodydavis开发6047 次安装2 周前更新
代码仓库

关于 lit-and-monaco-editor

面向使用 Lit、TypeScript 和 Vite 的 Web 开发者的教程,介绍如何将 Monaco Editor 封装为 Lit Web Component,并支持通过插槽或属性设置代码和语言。

主要功能

  1. 01用 Lit 封装 Monaco Editor
  2. 02通过插槽或属性提供代码
  3. 03设置代码语言与主题
  4. 04获取和设置编辑器内容
  5. 05适配系统深色模式

适用场景

  1. 01在网页中嵌入可交互代码编辑器
  2. 02将编辑器封装为可复用 Web Component

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rodydavis/skills --skill lit-and-monaco-editor

安装后在 Claude Code 中输入/lit-and-monaco-editor确认已加载。

详细介绍

Lit 和 Monaco Editor

本文将介绍如何设置一个 Lit Web 组件,并用它封装由 VSCode 提供支持的 Monaco Editor。

简而言之:最终源码见此处,在线演示见此处。

要了解如何使用 VSCode 和 Lit 构建扩展,请查看这篇博客文章。

前提条件

  • Vscode
  • Node >= 16
  • Typescript

开始使用

首先,在终端中导航到项目所在位置,然后运行以下命令:

npm init @vitejs/app --template lit-ts

接着输入项目名称 lit-code-editor,然后在 vscode 中打开项目并安装依赖项:

cd lit-code-editor
npm i lit monaco-editor
npm i -D @types/node
code .

使用以下内容更新 vite.config.ts:

import { defineConfig } from "vite";
import { resolve } from "path";

export default defineConfig({
  base: "/lit-code-editor/",
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, "index.html"),
      },
    },
  },
});

模板

打开 index.html,并使用以下内容更新它:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Lit Code Editor</title>
    <script type="module" src="/src/code-editor.ts"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100vh;
      }
    </style>
  </head>
  <body>
    <code-editor>
      <script type="text/javascript">
function x() {
  console.log("Hello world! :)");
}
      </script>
    </code-editor>
  </body>
</html>

我们将 lit-element 设置为包含一个插槽,其中放置编辑器初始代码。可以通过类型设置语言,也可以为 code-editor 组件添加属性来设置语言。

Web 组件

在更新组件之前,我们需要将 my-element.ts 重命名为 code-editor.ts

打开 code-editor.ts,并使用以下内容更新它:

import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createRef, Ref, ref } from "lit/directives/ref.js";

// -- Monaco Editor Imports --
import * as monaco from "monaco-editor";
import styles from "monaco-editor/min/vs/editor/editor.main.css";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";

// @ts-ignore
self.MonacoEnvironment = {
  getWorker(_: any, label: string) {
    if (label === "json") {
      return new jsonWorker();
    }
    if (label === "css" || label === "scss" || label === "less") {
      return new cssWorker();
    }
    if (label === "html" || label === "handlebars" || label === "razor") {
      return new htmlWorker();
    }
    if (label === "typescript" || label === "javascript") {
      return new tsWorker();
    }
    return new editorWorker();
  },
};

@customElement("code-editor")
export class CodeEditor extends LitElement {
  private container: Ref<HTMLElement> = createRef();
  editor?: monaco.editor.IStandaloneCodeEditor;
  @property() theme?: string;
  @property() language?: string;
  @property() code?: string;

  static styles = css`
    :host {
      --editor-width: 100%;
      --editor-height: 100vh;
    }
    main {
      width: var(--editor-width);
      height: var(--editor-height);
    }
  `;

  render() {
    return html`
      <style>
        ${styles}
      </style>
      <main ${ref(this.container)}></main>
    `;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "code-editor": CodeEditor;
  }
}

这里我们设置了一些样板代码,用于设置 vite 的 Web Worker,并使用 ref 指令将容器元素的引用传递给模板。

Monaco Editor 的样式也会作为样式元素加载到 shadow root 中。

现在,让我们添加一些辅助方法来访问提供的代码和语言:

private getFile() {
  if (this.children.length > 0) return this.children[0];
  return null;
}

private getCode() {
  if (this.code) return this.code;
  const file = this.getFile();
  if (!file) return;
  return file.innerHTML.trim();
}

private getLang() {
  if (this.language) return this.language;
  const file = this.getFile();
  if (!file) return;
  const type = file.getAttribute("type")!;
  return type.split("/").pop()!;
}

private getTheme() {
  if (this.theme) return this.theme;
  if (this.isDark()) return "vs-dark";
  return "vs-light";
}

private isDark() {
  return (
    window.matchMedia &&
    window.matchMedia("(prefers-color-scheme: dark)").matches
  );
}

这些方法会检查插槽中是否有包含所提供语言的 script 标签,或者查找在 code-editor 上设置的属性,然后返回其值。

现在,让我们将编辑器附加到容器引用:

firstUpdated() {
  this.editor = monaco.editor.create(this.container.value!, {
    value: this.getCode(),
    language: this.getLang(),
    theme: this.getTheme(),
    automaticLayout: true,
  });
   window
    .matchMedia("(prefers-color-scheme: dark)")
    .addEventListener("change", () => {
      monaco.editor.setTheme(this.getTheme());
    });
}

现在,编辑器应该已经运行,并且可以进行交互:

README 配图

系统切换到深色模式时,编辑器也会切换!

README 配图

要获取和设置编辑器中的值,我们可以添加 2 个辅助方法:

setValue(value: string) {
  this.editor!.setValue(value);
}

getValue() {
  const value = this.editor!.getValue();
  return value;
}

现在所有功能都应该符合预期,最终代码如下所示:

import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createRef, Ref, ref } from "lit/directives/ref.js";

// -- Monaco Editor Imports --
import * as monaco from "monaco-editor";
import styles from "monaco-editor/min/vs/editor/editor.main.css";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";

// @ts-ignore
self.MonacoEnvironment = {
  getWorker(_: any, label: string) {
    if (label === "json") {
      return new jsonWorker();
    }
    if (label === "css" || label === "scss" || label === "less") {
      return new cssWorker();
    }
    if (label === "html" || label === "handlebars" || label === "razor") {
      return new htmlWorker();
    }
    if (label === "typescript" || label === "javascript") {
      return new tsWorker();
    }
    return new editorWorker();
  },
};

@customElement("code-editor")
export class CodeEditor extends LitElement {
  private container: Ref<HTMLElement> = createRef();
  editor?: monaco.editor.IStandaloneCodeEditor;
  @property() theme?: string;
  @property() language?: string;
  @property() code?: string;

  static styles = css`
    :host {
      --editor-width: 100%;
      --editor-height: 100vh;
    }
    main {
      width: var(--editor-width);
      height: var(--editor-height);
    }
  `;

  render() {
    return html`
      <style>
        ${styles}
      </style>
      <main ${ref(this.container)}></main>
    `;
  }

  private getFile() {
    if (this.children.length > 0) return this.children[0];
    return null;
  }

  private getCode() {
    if (this.code) return this.code;
    const file = this.getFile();
    if (!file) return;
    return file.innerHTML.trim();
  }

  private getLang() {
    if (this.language) return this.language;
    const file = this.getFile();
    if (!file) return;
    const type = file.getAttribute("type")!;
    return type.split("/").pop()!;
  }

  private getTheme() {
    if (this.theme) return this.theme;
    if (this.isDark()) return "vs-dark";
    return "vs-light";
  }

  private isDark() {
    return (
      window.matchMedia &&
      window.matchMedia("(prefers-color-scheme: dark)").matches
    );
  }

  setValue(value: string) {
    this.editor!.setValue(value);
  }

  getValue() {
    const value = this.editor!.getValue();
    return value;
  }

  firstUpdated() {
    this.editor = monaco.editor.create(this.container.value!, {
      value: this.getCode(),
      language: this.getLang(),
      theme: this.getTheme(),
      automaticLayout: true,
    });
    window
      .matchMedia("(prefers-color-scheme: dark)")
      .addEventListener("change", () => {
        monaco.editor.setTheme(this.getTheme());
      });
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "code-editor": CodeEditor;
  }
}

用法

使用此组件时,可以通过插槽提供代码:

<code-editor>
  <script type="text/javascript">
function x() {
  console.log("Hello world! :)");
}
  </script>
</code-editor>

也可以通过属性提供:

<code-editor 
  code="console.log('Hello World');" 
  language="javascript"
  >
</code-editor>

也可以同时使用两者:

<code-editor language="typescript">
  <script>
function x() {
  console.log("Hello world! :)");
}
  </script>
</code-editor>

也可以手动设置主题:

<code-editor theme="vs-light"> </code-editor>

总结

如果你想进一步了解如何使用 Lit 进行构建,可以阅读此处的文档。

此示例的源码见此处。

评论

支持 Markdown