关于 lit-and-monaco-editor
面向使用 Lit、TypeScript 和 Vite 的 Web 开发者的教程,介绍如何将 Monaco Editor 封装为 Lit Web Component,并支持通过插槽或属性设置代码和语言。
主要功能
- 01用 Lit 封装 Monaco Editor
- 02通过插槽或属性提供代码
- 03设置代码语言与主题
- 04获取和设置编辑器内容
- 05适配系统深色模式
适用场景
- 01在网页中嵌入可交互代码编辑器
- 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());
});
}
现在,编辑器应该已经运行,并且可以进行交互:
![]()
系统切换到深色模式时,编辑器也会切换!
![]()
要获取和设置编辑器中的值,我们可以添加 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 进行构建,可以阅读此处的文档。
此示例的源码见此处。