Просмотр живых журналов Blazor с подсветкой синтаксисаJavascript

Форум по Javascript
Ответить
Anonymous
 Просмотр живых журналов Blazor с подсветкой синтаксиса

Сообщение Anonymous »

Я пытаюсь создать компонент blazor (серверный), который отображает оперативные журналы. В моем случае это внешний процесс, но он должен иметь возможность отображать любой тип журналирования в реальном времени.
Что я ищу:
  • Журналирование в реальном времени, без опроса
  • Выделен синтаксис
  • Ограничено определенным количеством записей журнала
  • Прокрутка вверх останавливает представление, чтобы пользователь мог прочитать, что происходит (хотя я предполагаю, что сам текст начнет двигаться, как только буфер журнала заполнится, не знаю, как это обойти)
  • Журналы записываются сервером, поэтому при открытии журналов уже есть полная история
Мой текущий подход:
  • Использование Prism.js в качестве библиотеки подсветки
  • Пользовательская коллекция LimitedSizeObservableCollection, помогающая с пунктами 3 и 5

Код: Выделить всё

using System.Collections.ObjectModel;

namespace Commons;

/// 
///     ObservableCollection that automatically removes oldest elements when it reaches specified size
/// 
/// Type of elements
public class LimitedSizeObservableCollection : ObservableCollection
{
/// 
///     ObservableCollection that automatically removes oldest elements when it reaches specified size
/// 
/// Type of elements
/// Maximum capacity of collection
/// How many elements to remove once capacity is reached. Has to be lower than Capacity. Default: 10
public LimitedSizeObservableCollection(uint capacity, uint clearCount = 10)
{
Capacity = capacity;
ClearCount = clearCount;
if (Capacity < clearCount)
{
throw new ArgumentException($"Capacity [{Capacity}] has to be higher to clearCount [{clearCount}]", nameof(clearCount));
}
}

public uint Capacity { get; set; }

public uint ClearCount { get; set; }

protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);

if (Count > Capacity)
{
// Remove last ClearCount items
for (var i = 0; i < ClearCount && Count > 0; i++)
{
RemoveAt(Count - 1);
}
}
}
}
  • ViewModel, которая обрабатывает блокировку, чтобы предотвратить проблему с исключением при обновлении, если элементы добавляются во время рендеринга (однако это, похоже, сильно замедляет обновления, вызывая скопление ожидающих потоков)

Код: Выделить всё

using System.Diagnostics;
using System.Text;
using BusinessLogic.Services.Interfaces;
using Commons;
using CommunityToolkit.Mvvm.ComponentModel;
using Serilog.Events;

namespace Charon.Shared.UIComponents.Models;

public partial class LogWindowViewModel : ObservableObject
{
public LimitedSizeObservableCollection Logs { get; }

private readonly Lock _logsGate = new();

private readonly SynchronizationContext _uiContext;

public static IEnumerable LogLevels => Enum.GetValues();

[ObservableProperty]
private LogEventLevel _logEventLevel = LogEventLevel.Information;

private Process? ExtProcess => _ExtService.ExtProcess;
private readonly IExtService _ExtService;

public LogWindowViewModel(IExtService ExtService, uint maxLogs = 1500)
{
_uiContext = SynchronizationContext.Current
?? throw new InvalidOperationException("LogWindowViewModel must be constructed on the UI thread.");

Logs = new LimitedSizeObservableCollection(maxLogs);
_ExtService = ExtService;

_ExtService.ExtProcessChanged += RegisterExtLogListeners;

// Workaround because direct binding to Logs with a converter does not work. Element does not react to PropertyChanged notification.  See https://github.com/AvaloniaUI/Avalonia/issues/11610
}

private void AddLog(string message)
{
_uiContext.Post(_ =>
{
lock (_logsGate)
{
Logs.Add(message);
}
}, null);
}

public List GetLogsSnapshot()
{
lock (_logsGate)
{
return Logs.ToList();
}
}

public string GetLogsTextSnapshot()
{
lock (_logsGate)
{
if (Logs.Count == 0)
return string.Empty;

var sb = new StringBuilder(capacity: Logs.Count * 64);
foreach (var line in Logs)
sb.AppendLine(line);

return sb.ToString();
}
}

public void ClearLogs()
{
lock (_logsGate)
{
Logs.Clear();
}
}

private void RegisterExtLogListeners(object? sender, Process? ExtProcess)
{
if (ExtProcess == null) return;
AddLog("Ext connected");

_ExtService.ChangeLogLevel(LogEventLevel);

if (_ExtService.External)
{
AddLog("Process acquired externally. Logging not available");
return;
}

ExtProcess!.OutputDataReceived += (_, args) =>
{
if (args.Data != null) AddLog(args.Data);
};

ExtProcess!.ErrorDataReceived += (_, args) =>
{
if (args.Data != null) AddLog(args.Data);
};
}
}
  • LogsView запускает соответствующий JS. Я пробовал разные подходы, и этот сработал лучше всего, поскольку я хотел избегать JS.

Код: Выделить всё

@using Charon.Shared.UIComponents.Models
@using System.Collections.Specialized
@inject LogWindowViewModel LogWindowViewModel
@inject IJSRuntime JSRuntime;
@inject ILogger Logger;
@implements IDisposable


@if (_cleaning)
{



}
else
{


}


@code {
private bool _cleaning;
private NotifyCollectionChangedEventHandler? _logsChangedHandler;

private bool _jsReady;

public void Dispose()
{
if (_logsChangedHandler is not null)
LogWindowViewModel.Logs.CollectionChanged -= _logsChangedHandler;
}

protected override Task OnParametersSetAsync()
{
_logsChangedHandler ??= async void (_, args) =>
{
try
{
if (_cleaning || !_jsReady)
return;

if (args is { Action: NotifyCollectionChangedAction.Add, NewItems: not null })
{
var lines = args.NewItems.Cast().Where(s => s is not null).Cast().ToArray();
if (lines.Length > 0)
await JSRuntime.InvokeVoidAsync("logsView.appendLines", "logsCode", lines, 250);
}
else
{
await JSRuntime.InvokeVoidAsync("logsView.setLogs", "logsCode", LogWindowViewModel.GetLogsTextSnapshot());
}
}
catch (Exception e)
{
Logger.LogError(e, "Error updating logs");
}
};
LogWindowViewModel.Logs.CollectionChanged += _logsChangedHandler;

return base.OnParametersSetAsync();
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsReady = true;
await JSRuntime.InvokeVoidAsync("logsView.setLogs", "logsCode", LogWindowViewModel.GetLogsTextSnapshot());
}
await base.OnAfterRenderAsync(firstRender);
}

///
/// Clears logs and the related view.
/// Uses a workaround since PrismJS modifies the DOM directly which means Blazor cannot remove the logs by itself.
///
public async Task Clear()
{
_cleaning = true;
await InvokeAsync(StateHasChanged);

LogWindowViewModel.ClearLogs();
await Task.Delay(500);

_cleaning = false;

if (_jsReady)
await JSRuntime.InvokeVoidAsync("logsView.setLogs", "logsCode", string.Empty);

await InvokeAsync(StateHasChanged);
}
}
  • Сопровождающий JS, который добавляет элемент пользовательского интерфейса и запускает подсветку синтаксиса на нем.

    Displaimer. Для создания этой части я использовал искусственный интеллект, так как я плохо разбираюсь в JS.

Код: Выделить всё

// AI Generated and reviewed.  Unfortunately, there were too many issues with adding new entries to logs via just Blazor's DOM manipulation, so this JS helper is used instead

(function () {
const ensureElement = (id) => {
if (!id) return null;
return document.getElementById(id);
};

const highlightNow = (el) => {
if (!el) return;
if (window.Prism && window.Prism.highlightElement) {
window.Prism.highlightElement(el);
}
};

window.logsView = {
setLogs: (codeId, text) => {
const el = ensureElement(codeId);
if (!el) return;

el.textContent = text ?? "";
highlightNow(el);
},

appendLines: (codeId, lines, highlightDelayMs) => {
const el = ensureElement(codeId);
if (!el) return;

if (!Array.isArray(lines) || lines.length === 0) return;

// Use a fragment to minimize layout work.
const frag = document.createDocumentFragment();
for (const line of lines) {
frag.appendChild(document.createTextNode((line ?? "") + "\n"));
}
el.appendChild(frag);

// Highlight when new lines are appended (no timer/debounce).
highlightNow(el);
}
};
})();
Я хотел бы внести полученный компонент в репозиторий, чтобы каждый мог использовать его, как только я получу его надежную работу.
Проблемы:
  • Плохая производительность и нагрузка на память, задачи по добавлению записей накапливаются.
  • Вновь добавленные записи «мигают», поскольку для завершения выделения требуется некоторое время.


Подробнее здесь: https://stackoverflow.com/questions/798 ... ghlighting
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Javascript»