Repository navigation
Architecture de
🌐 Diese Seite auf: English · Português · Español · Français · Deutsch
eQuantic.UI ist ein Komponenten-Framework für .NET, in dem eine Komponente einmal in C# geschrieben und auf zwei Arten von Ziel realisiert wird: im Web, als DOM und CSS, von ASP.NET Core ausgeliefert und im Browser von JavaScript getrieben, das der Build aus genau diesem C# kompiliert hat; und in nativen Fenstern und auf Telefonen, als Pixel, die die GPU-Engine Photon malt. Es gibt keine zweite Sprache zu lernen und keine externe Toolchain zu installieren: Der Compiler, der Bundler und die Engine kommen in den eigenen Paketen des SDK mit.
graph LR
A[C#-Komponenten, ein Vokabular] --> B{eqc}
B -->|TypeScript| C{Eingebettetes Bun}
C --> D[wwwroot/_equantic]
A --> E[ASP.NET Core SSR]
E --> F((Browser))
D --> F
A --> G[.NET-Build, AOT]
G --> H[Photon-Shell]
H --> I((Fenster / Telefon))
Das Diagramm oben ist ein ```mermaid-Zaun: GitHub zeichnet es hier, und die Dokumentationsseite zeichnet DENSELBEN Zaun über die Mermaid-Komponente des SDK selbst.
Diese Seite ist die Landkarte. Write-once-Komponenten erklärt das
gemeinsame Vokabular und die Realisierer in der Tiefe, Photon die native Engine,
Compiler und Build-Ablauf die Web-Pipeline Schritt für Schritt, und
Paketarchitektur, was jedes Paket mitbringt. Das Repository hält die
gemessenen Audits neben dem Code:
docs/README.md ist ihr Index.
- ✅ 100% .NET: kein Node.js, kein npm und kein Vite auf der Maschine der Entwicklerin; die Toolchain ist eingebettet.
- ✅ Einmal schreiben: eine Komponentenklasse, realisiert auf dem DOM und auf einer GPU-Display-List.
- ✅ In sich geschlossen: ASP.NET Core liefert, rendert und kompiliert alles für das Web.
- ✅ Vertraut: Routen sind Attribute, Dienste kommen aus
IServiceCollection, Einstellungen ausappsettings.json; wer .NET entwickelt, kennt die Idiome schon. - ✅ Kein Plattformartefakt: kein Swift, kein Manifest, kein plist, kein CSS. Das SDK leitet aus C#-Deklarationen ab, was jedes Ziel braucht.
| Schicht | Paket | Was sie hält | Darf referenzieren |
|---|---|---|---|
| Das Vokabular | eQuantic.UI.Primitives |
die abstrakten visuellen Knoten (Box, Row, Text, Pressable, …), typisierte Styles und Design-Tokens, das Komponentenmodell, die Kontraktattribute |
nichts |
| Die Bibliothek |
eQuantic.UI.Components, eQuantic.UI.Charts
|
die Write-once-Komponenten (Button, Card, DataTable, BarChart, …), nur aus dem Vokabular zusammengesetzt |
Primitives |
| Der Web-Realisierer | eQuantic.UI.Web |
senkt einen Knotenbaum auf dem Server zu HtmlElement/DOM und atomarem CSS ab; dazu die DOM-Notluke (HtmlElement, DynamicElement, ClassBuilder) |
Primitives |
| Der Web-Host | eQuantic.UI.Server |
die ASP.NET-Core-Integration: SSR, Server Actions, Routing, Metadaten, Assets, Hot Reload | Web |
| Die Browser-Runtime |
@equantic/runtime, von eQuantic.UI.Server aus dem TypeScript in eQuantic.UI.Runtime gebündelt |
der TypeScript-Zwilling des Web-Realisierers, der Reconciler, der Zustand, der Router und die transpilierten Module der Bibliothek selbst | nichts |
| Der native Zweig |
eQuantic.UI.Native.Engine (+ Metal, Vulkan, Reference), Native.Framework, Native.Components, Native.Hosting, die Shells |
die GPU-Engine und ihre Backends, die Layout-Engine, der Photon-Realisierer und -Host, der Anwendungs-Builder und je eine Shell pro Plattform | Primitives, und einander nach unten |
| Der Compiler |
eQuantic.UI.Compiler (eqc) |
der Roslyn-gestützte Transpiler von C# nach TypeScript, den das Web-SDK ausführt | Codegen |
| Die SDKs |
eQuantic.UI.Sdk, eQuantic.UI.Sdk.Native
|
MSBuild-SDKs: Ein Sdk="…"-Attribut bringt jedes Paket oben mit und verdrahtet den Build |
nichts |
Zwei Regeln halten die Tabelle zusammen. Das Vokabular referenziert nichts, also kann es zu jedem
Ziel ausgeliefert werden, Browser-Bundle und Native AOT eingeschlossen. Ein Realisierer
referenziert nie die Bibliothek, die er realisiert: Eine Komponente erreicht einen Realisierer als
den Baum, den ihr Build erzeugt hat, also kennen Web, Email und Native.Components das
Vokabular und nicht Components. Beide Regeln sind Tests im Repository
(AssemblyLayeringTests), keine Konventionen.
Die Projektdatei einer Web-Anwendung nennt das SDK und nichts weiter, ohne jede
PackageReference:
<Project Sdk="eQuantic.UI.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>Die Version kommt aus global.json, so wie jedes MSBuild-SDK versioniert wird:
{
"msbuild-sdks": {
"eQuantic.UI.Sdk": "0.2.0-preview.52"
}
}Sdk.props importiert Microsoft.NET.Sdk.Web, fügt die Framework-Pakete hinzu (Primitives, Code,
Components, Charts, Web, Server, den Source-Generator und das eingebettete Bun-Paket für
Betriebssystem und Architektur der bauenden Maschine), bringt die deklarative Oberfläche in jeder
Datei in Reichweite (eQuantic.UI.Components.UI und eQuantic.UI.Charts.ChartsUI als statische
Usings) und nimmt Pages/**/*.cs als die Komponenten auf, die der Build für den Browser kompiliert.
Neben einem Quell-Checkout des Frameworks tauscht es jedes Paket gegen das entsprechende Projekt, so
dass die Beispiele gegen den Baum bauen. Siehe Build-Ablauf.
Auf das Wesentliche gekürzt liest sich diese Datei so:
<!-- eQuantic.UI.Sdk/Sdk/Sdk.props -->
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<EnableEQuanticUICompilation>true</EnableEQuanticUICompilation>
<EQuanticOutputPath>wwwroot/_equantic/</EQuanticOutputPath>
</PropertyGroup>
<ItemGroup>
<!-- die deklarative Oberfläche, in jeder Datei in Reichweite -->
<Using Include="eQuantic.UI.Components.UI" Static="true" />
<Using Include="eQuantic.UI.Charts.ChartsUI" Static="true" />
<!-- was der Build für den Browser kompiliert -->
<UIComponent Include="Pages/**/*.cs" Exclude="obj/**;bin/**" />
</ItemGroup>
</Project>dotnet build
↓
Microsoft.NET.Sdk.Web, die gewöhnliche Pipeline, durch CoreCompile (das C# der Anwendung)
↓
ResolveBunPath das eingebettete Bun für diese Maschine, einmal aus seinem Paket entpackt
↓
CompileEQuanticUI nach CoreCompile, und bevor die Static Web Assets definiert werden
1. eqc liest die Quellen des Projekts und die mitgelieferten Quellen der Bibliothek
2. Roslyn bindet sie. Das semantische Modell ist maßgeblich; ein nicht bindbarer Aufruf ist ein Build-Fehler
3. ein TypeScript-Modul je Seite oder Komponente
4. Bun bündelt jedes Modul; in Debug mit einer Source Map zurück zum C# (EQuanticSourceMaps)
↓
CopyEQuanticRuntime die ausgelieferte runtime.js, aus dem Server-Paket
↓
Ausgabe: wwwroot/_equantic/, als Static Web Assets aus dem definiert, was auf der Platte liegt,
damit Komprimierung, Manifeste und der Publish die Dateien dieses Builds sehen
Das TypeScript-Zwischenformat ist Absicht: Der ausgegebene Code ist lesbar, wird vom
TypeScript-Compiler ein zweites Mal typgeprüft und für den Debugger des Browsers auf die C#-Zeile
zurückgemappt. Der Bundler ist Bun, weil er eine einzige ausführbare Datei ist, die TypeScript ohne
Konfiguration bündelt, und weil er als ein Paket je Betriebssystem und Architektur ausgeliefert wird
(eQuantic.UI.Runtime.OsxArm64, .Win64, .Linux64, …), das das SDK auswählt. Niemand installiert
ihn.
Das hier gibt eqc für eine der Komponenten der Bibliothek selbst aus, Divider, genau so, wie sie in
runtime.js ausgeliefert wird. Das C#, aus dem sie kam, ist Divider.cs in
eQuantic.UI.Components: dieselbe Klasse, dasselbe Build, die Enums auf ihre camelCase-Namen
abgesenkt, der Objektinitialisierer als abschließendes props angekommen:
import { Box, BoxStyle, BuildContext, EdgeInsets, SizeValue, StatelessComponent } from "../runtime-exports";
export class Divider extends StatelessComponent {
declare inset: string;
declare axis: string;
declare leadingInset: any;
constructor(inset: any = 'none', axis: any = 'horizontal', props?: any) {
super();
if (inset !== undefined) this.inset = inset;
if (axis !== undefined) this.axis = axis;
if (this.inset === undefined) this.inset = 'none';
if (this.axis === undefined) this.axis = 'horizontal';
this.inset = inset;
this.axis = axis;
if (props && typeof props === 'object') Object.assign(this, props);
}
build(context: BuildContext) {
let line = new Box(new BoxStyle({ width: this.axis === 'vertical' ? SizeValue.fixed(1) : SizeValue.fill, height: this.axis === 'vertical' ? SizeValue.fill : SizeValue.fixed(1), background: context.theme.border }));
if (this.inset === 'none') return line;
let padding = this.inset === 'leading' ? new EdgeInsets(this.leadingInset ?? 16, 0, 0, 0) : EdgeInsets.symmetric(16, 0);
return new Box(new BoxStyle({ width: SizeValue.fill, padding: padding }), line);
}
}Größen werden hier nicht genannt, weil sie sich mit jeder Änderung bewegen: Die gzip-Größe der
ausgelieferten Runtime ist ein Budget, das die Suite erzwingt (ServedRuntimeBudgetTests, gegen den
Eintrag in tests/eQuantic.UI.Server.Tests/Budgets/served-runtime.json), und die CI berichtet die
Größe jedes Seitenmoduls bei jedem Pull Request.
| Datei | Was sie ist |
|---|---|
runtime.js |
die Browser-Runtime: der Reconciler, die Events, der Zustand, die Server-Actions-Brücke, der Router UND die gemeinsame Komponentenbibliothek, deren transpilierte Module darin mitreisen. Die Seite erreicht sie über die Import Map der Shell als nacktes Modul @equantic/runtime, und der Server antwortet auf diese URL mit dem Bundle, das er einbettet; diese Datei sind dieselben Bytes, für Werkzeuge, die die Module außerhalb eines Servers laden (die Vorschau in VS Code liest sie) |
<Component>.js, und .js.map in Debug |
ein Modul je Seite oder Komponente, flach. Der Boot importiert das Modul der Seite bei der Navigation, also fällt das Lazy Loading je Route aus dem Modulgraph selbst heraus; es gibt keinen pages/-Ordner. eqc führt bun mit --splitting aus, also ist das, was zwei oder mehr Module teilen, ein Chunk, der nach einem von ihnen mit einem Inhaltshash benannt ist: NotFoundScreen-<hash>.js im Dashboard-Beispiel ist seine Konsolen-Shell, kein zweiter Typ dieses Namens. Die Map trägt das C#, aus dem das Modul kam, also ist EQuanticSourceMaps in Debug full und überall sonst none (external schreibt eine ohne das C#, für einen Fehlerberichter), und ein Publish nimmt nie eine mit |
strings/<culture>.json |
die Kulturkataloge, wenn die Anwendung .resx hat |
icons/ |
die Icon-Größen der Anwendung und das Web-Manifest, wenn ein AppIcon deklariert ist |
Die Styles sind keine Datei: Der Server schreibt sie in die Seite, das generierte Token-Sheet und die
atomaren Klassen (<style id="eq-atomic">) darunter.
Der Server rendert das erste HTML aus DEMSELBEN C#: WebRealizer senkt den Baum der Komponente zu
HtmlElements ab, und die atomaren Style-Klassen, die er schreibt, hashen identisch zu denen, die
das lowering.ts des Browsers schreibt, also vergleicht die Hydration Klassennamen und malt nichts
neu. Eine Seite, die IServerPrefetch implementiert, bekommt ihre Daten auf dem Server VOR dem
ersten Build geladen, und die Antwort reist mit der Seite zum Browser: Das erste Paint hat die Daten
schon, und es gibt keinen Ladezustand zu entwerfen. Siehe
Write-once-Komponenten für den Paritätskontrakt und
Server-Integration für die Optionen der Pipeline.
Dieselbe Komponentenklasse, von dotnet build zu einer gewöhnlichen .NET-Anwendung gebaut (Native
AOT beim Publish), von Photon gehostet:
PhotonApplication.CreateBuilder(args) … .Build().UseRoot<Shell>()
↓
LayoutEngine Constraints gehen hinunter, Größen kommen herauf, Eltern setzen Positionen, alles in einem Durchgang
↓
PhotonRealizer der ausgelegte Baum wird eine Display List: gerundete Boxen, Schatten, Text, Bilder, Icons
↓
RHI ein Renderer über IRhiDevice; Metal bei Apple, Vulkan auf Windows und Android, überall ein CPU-Reference-Backend
↓
Shell macOS, iOS, Android oder Windows: das Fenster, die Eingabe, die Textdienste, die Accessibility-Brücke, die Fähigkeiten
Text wird von der Plattform geformt (CoreText, DirectWrite, das Textlayout von Android) hinter einem
einzigen ITextMeasurer-Kontrakt; Fähigkeiten (Kamera, Standort, Speicher, Biometrie, …) sind
typisierte Schnittstellen, die die Shell implementiert und die eine Komponente über ihren Konstruktor
oder GetService<T>() nimmt. Hot Reload ist dotnet watch: Die Runtime wendet die Änderung im
Prozess an, und das Fenster zeichnet neu.
Eine Seite, die Daten braucht, sollte keinen Controller, kein DTO, kein fetch und keinen
JSON-Kontrakt brauchen, die von Hand im Takt gehalten werden. Sie braucht eine Methode.
Ohne das verteilt sich dasselbe Feature über vier Stellen, die sich nur durch Disziplin einig sind:
[ApiController]
public class TodoController : ControllerBase
{
[HttpPost("/api/todos")]
public Task<Todo> AddTodo([FromBody] AddTodoRequest request) { /* … */ }
}async function addTodo(title) {
const response = await fetch("/api/todos", { method: "POST", body: JSON.stringify({ title }) });
return await response.json();
}// Pages/TodoList.cs
[Page("/todos")]
public class TodoList : StatefulComponent
{
private List<Todo> _todos = [];
// Läuft auf dem SERVER. Der Client bekommt einen Aufruf, nicht den Körper.
[ServerAction]
public async Task<List<Todo>> LoadTodos()
{
using var db = new AppDbContext();
return await db.Todos.ToListAsync();
}
[ServerAction]
public async Task<Todo> AddTodo(string title)
{
using var db = new AppDbContext();
var todo = new Todo { Title = title };
db.Todos.Add(todo);
await db.SaveChangesAsync();
return todo;
}
protected override async void OnMount() => await Load();
private async Task Load()
{
var loaded = await LoadTodos();
SetState(() => _todos = loaded);
}
private async Task Add(string title)
{
var added = await AddTodo(title); // typsicher, für Sie serialisiert
SetState(() => _todos.Add(added));
}
public override VisualNode Build(ComponentContext context) =>
Column(gap: Space.S2, children: [.. _todos.Select(todo => TodoItem(todo))]);
}Der Compiler ersetzt den Körper jeder [ServerAction] durch einen Aufruf an die Brücke:
async loadTodos() {
return await this._serverActions.invoke("TodoList/LoadTodos", []);
}app.UseServerActions() antwortet auf POST /api/_equantic/actions. Aufrufbar sind nur Methoden mit
[ServerAction] (eine Whitelist, keine Konvention), und [Authorize] wird erzwungen, bevor die
Methode läuft. Payload-Größe und Argumenttypen werden validiert; siehe Sicherheit.
Eine Server Action ist Anfrage/Antwort; was der Server veröffentlicht, während eine Seite offen ist,
erreicht sie über Server-Events.
Auf der Leitung ist es ein POST je Aufruf, was zu wissen lohnt, wenn man den Netzwerk-Tab liest:
{ "actionId": "TodoList/LoadTodos", "arguments": [] }{ "success": true, "result": [ { "id": 1, "title": "Buy milk" } ] }Eine Komponente, die ein externes Skript oder Stylesheet braucht, deklariert es, und der Server injiziert die Tags einmal je Seite, dedupliziert:
public class CodeBlock : StatelessComponent, IRequireAssets
{
public void ConfigureAssets(AssetBuilder assets)
{
assets.AddStylesheet("https://cdn.example.com/prism.css", id: "prism-theme");
assets.AddScript("https://cdn.example.com/prism.js");
}
}IComponentAssetProvider<T> tut dasselbe für eine Komponente, die Sie nicht geschrieben haben,
registriert in der DI oder vom Assembly-Scan von AddUI() gefunden:
public class PrismAssetProvider : IComponentAssetProvider<CodeBlock>
{
public void ConfigureAssets(AssetBuilder assets) =>
assets.AddScript("https://cdn.example.com/prism.js");
}
// ausdrückliche Registrierung, wenn nicht der Scan sie finden soll
options.WithAssetProvider<PrismAssetProvider>();Der Ablauf: ServerRenderingService geht den gerenderten Baum ab und sammelt beide Arten,
AssetCollection dedupliziert nach Schlüssel (Stylesheets zuerst, dann Skripte), und die Tags landen
im <head>. Eine Seite ohne jede Komponente, die Assets verlangt, bekommt keine. Siehe
Assets.
Eine Route ist ein Attribut auf der Seite, zur Build-Zeit entdeckt, also gibt es keine Routentabelle, die man im Takt mit den Seiten halten müsste:
[Page("/counter")]
[Page("/count")] // mehrere Routen, eine Seite
public class Counter : StatefulComponent { }
[Page("/user/{id:int}")] // ASP.NET-Routensyntax, Parameter eingeschlossen
public class UserProfile : StatefulComponent
{
public override VisualNode Build(ComponentContext context)
{
var id = context.Route.Param("id"); // was die Route sagte, ohne ein Ziel darin
var page = context.Route.Query("page"); // und der Query-String
…
}
}
[Page("/admin/dashboard")]
[Authorize(Roles = "Admin")]
public class AdminDashboard : StatefulComponent { }// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddUI(options => options.ScanAssembly(typeof(Program).Assembly));
var app = builder.Build();
app.UseStaticFiles();
app.UseServerActions();
app.MapUI(); // jede [Page], dazu der SPA-Fallback
app.Run();Im Browser übernimmt der Router der Runtime die Navigation ohne Reload: Ein Link ist ein echter
Anker, und Navigator.Go(destination) ist der programmatische Zwilling für die Routen, die eine
Komponente selbst zu nehmen beschließt. Auf Photon erreichen derselbe Link und derselbe Navigator
die Navigationsnaht des Hosts.
MeineApp/
├── MeineApp.csproj # <Project Sdk="eQuantic.UI.Sdk">
├── global.json # die SDK-Version
├── Program.cs # der ASP.NET-Core-Host
├── Pages/ # Komponenten mit [Page]
│ ├── Home.cs
│ └── Counter.cs
├── Components/ # Ihre eigenen Write-once-Komponenten
├── Assets/ # AppIcon.cs, *.svg → Vektoren zur Build-Zeit
├── Resources/ # .resx → strings/<culture>.json
└── wwwroot/
└── _equantic/ # vom Build erzeugt; die .map-Dateien in Debug
├── runtime.js
├── Counter.js Counter.js.map
└── Home.js Home.js.map
dotnet new install eQuantic.UI.Templates # einmal
dotnet new equantic-app -n MeineApp # eine Web-Anwendung
dotnet new equantic-native -n MeineApp # eine Photon-Anwendung
dotnet watch run # Hot Reload, Web oder Native
dotnet build # C# → TypeScript → JavaScript, SSR-fertig
dotnet publish -c Release # eine veröffentlichbare App; Native AOT für Photon1. Sie bearbeiten Counter.cs
2. der Server, der die .cs der Anwendung beobachtet, führt eqc erneut aus, und Counter.js wird an
seiner Stelle neu geschrieben; dotnet watch wendet dieselbe Änderung unterdessen auf den
laufenden Server an
3. der Server schiebt ein Event auf /_equantic/hmr, Server-sent Events mit Herzschlag und Reconnect
4. der Browser holt das neue Modul und MOUNTET die Seite erneut, mit den Feldern, die die Seite
hielt; das alte Server-Markup wird nicht neu hydriert
5. auf Photon gibt es keinen Schritt 3: Die Runtime wendet die Änderung im laufenden Prozess an,
und der Host zeichnet beim nächsten Frame neu
Siehe Debugging für das Fehler-Overlay und die Source Maps, die auf die C#-Zeile zurückzeigen.
| Aspekt | WebForms | eQuantic.UI |
|---|---|---|
| Zustand | ViewState in einem versteckten Feld | Zustand auf der Komponenteninstanz, vom Reconciler gehalten; der Server hält Daten, keinen Ansichtszustand |
| Rendering | servergeneriertes HTML bei jedem Postback | serverseitig gerendertes erstes Paint, dann ein Reconciler im Client |
| Aktualisierungen | Postback der ganzen Seite | an ihrem Platz, im Browser oder im Fenster |
| Serverlogik | Postback-Handler |
[ServerAction]-Methoden, wie Methoden aufgerufen |
| Ziele | der Browser | der Browser, und native Fenster und Telefone aus derselben Klasse |
Was es von WebForms behält, ist der Teil, der richtig war: Eine Komponente ist eine C#-Klasse mit Zustand und Handlern, und das Framework, nicht die Entwicklerin, entscheidet, wie sie auf den Bildschirm kommt.
public class Counter : StatefulComponent
{
private int _count;
// liest sich wie ein WebForms-Handler, und läuft im Browser, ohne Postback
private void Increment() => SetState(() => _count++);
public override VisualNode Build(ComponentContext context) =>
Row(gap: Space.S2, children: [
Text($"{_count}", TypeRole.Title),
Button("+", onPressed: Increment),
]);
}-
TypeScript als Zwischenformat: lesbare Ausgabe, eine zweite Typprüfung und Source Maps zurück zum C#.
-
Bun, je Plattform eingebettet: eine Bundler-Binary je Betriebssystem und Architektur, als Paket geliefert, das das SDK auswählt; keine
package.json, nichts installiert.eQuantic.UI.Runtime.OsxArm64/ ein Paket je Betriebssystem und Architektur ├── tools/bun/bun-darwin.zip die Binary, beim ersten Build einmal entpackt └── build/ die Props, die das SDK liest, um sie zu finden -
Eine Style-Engine: typisierte C#-Styles senken sich im Web zu dedupliziertem atomarem CSS und auf Photon zu GPU-Malen ab; es gibt keine CSS-Ebene zu schreiben. Siehe Design System.
-
Einmal schreiben, je Ziel realisieren: Das Vokabular ist geschlossen und liegt unter jedem Realisierer; die Realisierer sind der Erweiterungspunkt, nicht die Knoten. Die Abwägungen, die das kostet, gemessen, stehen im Architektur-Audit des Repositorys und in seinem Gegenstück, wie Flutter jede davon löst.
🏁 Hier starten
🏗️ Architektur
- Architekturüberblick
- Write-once-Komponenten
- Deklarative Oberfläche
- Paketarchitektur
- Komponenten
- Styling
- Lokalisierung
- Analytics und GTM
📱 Write-once
- Photon-Engine
- Design System
- Fähigkeiten
- Speicher
- Formulare
- Code-Editor
- Markdown
- Mermaid
- E-Mail-Rendering
⚙️ Kompilierung
⚡ Runtime
🔌 Server
🎨 Ökosystem
🚀 Entwicklung