专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
黑龙江省商务厅  ·  听民意 汇民智:政府部门与代表委员的“高效联动” ·  8 小时前  
黑龙江省商务厅  ·  听民意 汇民智:政府部门与代表委员的“高效联动” ·  8 小时前  
庆阳市场监管  ·  第三届消费者权益保护微视频作品展播(二) ·  昨天  
庆阳市场监管  ·  第三届消费者权益保护微视频作品展播(二) ·  昨天  
前端早读课  ·  【第3470期】利用大型语言模型(LLMs) ... ·  昨天  
CEO品牌观察  ·  听小野主理人 讲述小野全球首店里的故事 ·  2 天前  
CEO品牌观察  ·  听小野主理人 讲述小野全球首店里的故事 ·  2 天前  
51好读  ›  专栏  ›  前端早读课

【第3396期】Monaco Editor 实现一个日志查看器

前端早读课  · 公众号  · 前端  · 2024-10-21 08:00

正文

前言

介绍了如何在 Monaco Editor 中实现一个日志查看器,包括实时日志和普通日志的展示,以及如何在 Monaco Editor 中支持类似 a 元素的链接功能。今日前端早读课文章由袋鼠云 @文长分享。

正文从这开始~~

在 Web IDE 中,控制台中展示日志是至关重要的功能。Monaco Editor 作为一个强大的代码编辑器,提供了丰富的功能和灵活的 API ,支持为内容进行 “装饰”,非常适合用来构建日志展示器。如下图:

除了实时日志外,还有一些需要查看历史日志的场景。如下图:

Monarch

Monarch 是 Monaco Editor 自带的一个语法高亮库,通过它,我们可以用类似 Json 的语法来实现自定义语言的语法高亮功能。这里不做过多的介绍,只介绍在本文中使用到的那部分内容.

一个语言定义基本上就是描述语言的各种属性的 JSON 值,部分通用属性如下:

tokenizer

(必填项,带状态的对象)这个定义了 tokenization 的规则。 Monaco Editor 中用于定义语言语法高亮和解析的一个核心组件。它的主要功能是将输入的代码文本分解成一个个的 token,以便于编辑器能够根据这些 token 进行语法高亮、错误检查和其他编辑功能。

ignoreCase

(可选项,默认值:false)语言是否大小写不敏感?tokenizer(分词器)中的正则表达式使用这个属性去进行大小写(不)敏感匹配,以及 case 场景中的测试。

brackets

(可选项,括号定义的数组)tokenizer 使用这个来轻松的定义大括号匹配,更多信息详见 @brackets 和 bracket 部分。每个方括号定义都是一个由 3 个元素或对象组成的数组,描述了 open 左大括号、close 右大括号和 token 令牌类。默认定义如下:

 [ ['{','}','delimiter.curly'],
['[',']','delimiter.square'],
['(',')','delimiter.parenthesis'],
[','>','delimiter.angle'] ]
tokenizer

tokenizer 属性描述了如何进行词法分析,以及如何将输入转换成 token ,每个 token 都会被赋予一个 css 类名,用于在编辑器中渲染,内置的 css token 包括:

 identifier         entity           constructor
operators tag namespace
keyword info-token type
string warn-token predefined
string.escape error-token invalid
comment debug-token
comment.doc regexp
constant attribute

delimiter .[curly,square,parenthesis,angle,array,bracket]
number .[hex,octal,binary,float]
variable .[name,value]
meta .[content]

当然也可以自定义 css token,通过以下方式将自定义的 css token 注入。

 editor.defineTheme("vs", {
base: "vs",
inherit: true,
rules: [
{
token: "token-name",
foreground: "#117700",
}
],
colors: {},
});

一个 tokenizer 由一个描述状态的对象组成。tokenizer 的初始状态由 tokenizer 定义的第一个状态决定。这句话什么意思呢?查看下方例子,root 就是 tokenizer 定义的第一个状态,就是初始状态。同理,如果把 afterIf 和 root 两个状态调换位置,那么 afterIf 就是初始状态。

 monaco.languages.setMonarchTokensProvider('myLanguage', {
tokenizer: {
root: [
// 初始状态的规则
[/\d+/, 'number'], // 识别数字
[/\w+/, 'keyword'], // 识别关键字
// 转移到下一个状态
[/^if$/, { token: 'keyword', next: 'afterIf' }],
],
afterIf: [
// 处理 if 语句后的内容
[/\s+/, ''], // 忽略空白
[/[\w]+/, 'identifier'], // 识别标识符
// 返回初始状态
[/;$/, { token: '', next: 'root' }],
]
}
});

如何获取 tokenizer 定义的第一个状态呢?

 class MonarchTokenizer {
...
public getInitialState(): languages.IState {
const rootState = MonarchStackElementFactory.create(null, this._lexer.start!);
return MonarchLineStateFactory.create(rootState, null);
}
...
}

通过 getInitialState 获取初始的一个状态,通过代码可以看到 确认哪个是初始状态是通过 this._lexer.start 这个属性。这个属性又是怎么被赋值的呢?

 function compile() {
...
for (const key in json.tokenizer) {
if (json.tokenizer.hasOwnProperty(key)) {
if (!lexer.start) {
lexer.start = key;
}

const rules = json.tokenizer[key];
lexer.tokenizer[key] = new Array();
addRules('tokenizer.' + key, lexer.tokenizer[key], rules);
}
}
...
}

在 compile 解析 setMonarchTokensProvider 传入的语言定义对象时,会将读取出来的第一个 key 作为初始状态。可能会有疑问,就一定能保证在定义对象时,写入的第一个属性,在读取时一定第一个被读出吗?
在 JavaScript 中,对象属性的顺序有一些特定的规则:

  • 整数键:如果属性名是一个整数(如 "1"、"2" 等),这些属性会按照数值的升序排列。

  • 字符串键:对于非整数的字符串键,属性的顺序是按照它们被添加到对象中的顺序。

  • Symbol 键:如果属性的键是 Symbol 类型,这些属性会按照它们被添加到对象中的顺序。

因此,当使用 for...in 循环遍历对象的属性时,属性的顺序如下:

  • 首先是所有整数键,按升序排列。

  • 然后是所有字符串键,按添加顺序排列。

  • 最后是所有 Symbol 键,按添加顺序排列。

看个例子:

上述例子可以看出,“1”、“2” 虽然被写在了后面,但仍然会被排序优先输出,其后才是字符串键根据添加顺序输出。所以,尽可能不要使用整数键去定义状态名。

当 tokenizer 处于某种状态时,只有那个状态的规则才能匹配。所有规则是按顺序进行匹配的,当匹配到第一个规则时,它的 action 将被用来确定 token 的类型。不会再使用后面的规则进行尝试,因此,以一种最有效的方式排列规则是很重要的。比如空格和标识符优先。

如何定义一个状态?

每个状态定义为一个用于匹配输入的规则数组,规则可以有如下形式:

  • [regex, action] {regex: regex, action: action} 形式的简写。

  • [regex, action, next] { regex: regex, action: action{ next: next} } 形式的简写。

 monaco.languages.setMonarchTokensProvider('myLanguage', {
tokenizer: {
root: [
// [regex, action]
[/\d+/, 'number'],
/**
* [regex, action, next]
* [/\w+/, { token: 'keyword', next: '@pop' }] 的简写
*/

[/\w+/, 'keyword', '@pop'],
]
}
});

regex 是正则表达式,action 分为以下几种:

  • string { token: string } 的简写

  • [action, ..., actionN] :多个 action 组成的数组。这仅在正则表达式恰好由 N 个组(即括号部分)组成时才允许。举个例子:

 [/(\d)(\d)(\d)/, ['string', 'string', 'string']
  • { token: tokenClass } :这个 tokenClass 可以是内置的 css token,也可以是自定义的 token。同时,还规定了一些特殊的 token 类:

    • @rematch 备份输入并重新调用 tokenizer 。这只在状态发生变化时才有效(或者我们进入了无限的递归),所以这个通常和 next 属性一起使用。例如,当你处于特定的 tokenizer 状态,并想要在看到某些结束标记时退出,但是不想在处于该状态时使用它们,就可以使用这个。例如:

 monaco.languages.setMonarchTokensProvider('myLanguage'




    
, {
tokenizer: {
root: [
[/\d+/, 'number', 'word'],
],
word: [
[/\d/, '@rematch', '@pop'],
[/[^\d]+/, 'string']
]
}
});

这个 language 的状态流转图是怎么样的呢?

可以看出,在定义一个状态时,应保证状态存在出口即没有定义转移到其他状态的规则),否则可能会导致死循环,不断的使用状态内的规则去匹配。

  • @pop 弹出 tokenizer 栈以返回到之前的状态。

  • @push 推入当前状态,并在当前状态中继续。

 monaco.languages.setMonarchTokensProvider('myLanguage', {
tokenizer: {
root: [
// 当匹配到开始标记时,推送新的状态
[/^\s*function\b/, { token: 'keyword', next: '@function' }],
],
function: [
// 在 function 状态下的匹配规则
[/^\s*{/, { token: 'delimiter.bracket', next: '@push' }],
[/[^}]+/, 'statement'],
[/^\s*}/, { token: 'delimiter.bracket', next: '@pop' }],
],
}
});
 - $n

匹配输入的第 n 组,或者是 $0 代表这个匹配的输入。

$Sn 状态的第 n 个部分,比如,状态 @tag.foo ,用 $S0 代表整个状态名(即 tag.foo ), $S1 返回 tag, $S2 返回 foo 。

实时日志

在本篇文章中,Monaco Editor 的使用就不再提及,不是本文的重点。利用 Monaco Editor 实现日志查看器主要是为了让不同的类型的日志有不同的高亮主题。

实时日志中,存在不同的日志类型,如:info、error、warning 等。

 /**
* 日志构造器
* @param {string} log 日志内容
* @param {string} type 日志类型
*/

export function createLog(log: string, type = '') {
let now = moment().format('HH:mm:ss');
if (process.env.NODE_ENV == 'test') {
now = 'test';
}
return `[${now}] <${type}> ${log}`;
}

根据日志可以看出,每条日志都是 [xx:xx:xx] 开头,紧跟着 ,后面的是日志内容。(日志类型:info 、error、warning。)

注册一个自定义语言 realTimeLog 作为实时日志的一个 language 。

这里规则也很简单,在 root 中设置了两条解析规则,分别是匹配日志日期和日志类型。在匹配到对应的日志类型后,给匹配到的内容打上 token ,然后通过 next 携带匹配的引用标识( $1 表示正则分组中的第 1 组)进入下一个状态 consoleLog,在状态 consoleLog 中,匹配日志内容,并打上 token ,直到遇见终止条件(日志日期)。

 import { languages } from "monaco-editor/esm/vs/editor/editor.api";
import { LanguageIdEnum } from "./constants";

languages.register({ id: LanguageIdEnum.REALTIMELOG });

languages.setMonarchTokensProvider(LanguageIdEnum.REALTIMELOG, {
keywords: ["error", "warning", "info", "success"],
date: /\[[0-9]{2}:[0-9]{2}:[0-9]{2}\]/,
tokenizer: {
root: [
[/@date/, "date-token"],
[
//,
{
cases: {
"$1@keywords": { token: "$1-token", next: "@log.$1" },
"@default": "string",
},
},
],
],
log: [
[/@date/, { token: "@rematch", next: "@pop" }],
[/.*/, { token: "$S2-token" }],
],
},
});

// ===== 日志样式 =====
export const realTimeLogTokenThemeRules = [
{
token: "date-token"






请到「今天看啥」查看全文