Solid ist eine reaktive JavaScript-Bibliothek zur Erstellung von Benutzeroberflächen ohne virtuelles DOM. Es kompiliert Vorlagen einmal zu echten DOM-Knoten und verpackt Aktualisierungen in feingranulare Reaktionen, sodass bei Zustandsaktualisierungen nur der zugehörige Code ausgeführt wird.
Auf diese Weise kann der Compiler das anfängliche Rendering optimieren und die Laufzeit die Updates. Dieser Fokus auf Leistung macht es zu einem der am besten bewerteten JavaScript-Frameworks.
Ich wurde neugierig darauf und wollte es ausprobieren, also habe ich einige Zeit damit verbracht, eine kleine To-Do-App zu erstellen, um zu untersuchen, wie dieses Framework Komponenten rendert, den Zustand aktualisiert, Stores einrichtet und vieles mehr.
Hier ist die endgültige Demo, wenn Sie es einfach nicht erwarten können, den endgültigen Code und das Ergebnis zu sehen
Erste Schritte
Wie die meisten Frameworks können wir mit der Installation des npm-Pakets beginnen. Um das Framework mit JSX zu verwenden, führen Sie Folgendes aus:
npm install solid-js babel-preset-solid
Anschließend müssen wir babel-preset-solid zu unserer Babel-, Webpack- oder Rollup-Konfigurationsdatei hinzufügen mit
"presets": ["solid"]
Oder wenn Sie eine kleine App aufbauen möchten, können Sie auch eine ihrer Vorlagen verwenden
# Create a small app from a Solid template
npx degit solidjs/templates/js my-app
# Change directory to the project created
cd my-app
# Install dependencies
npm i # or yarn or pnpm
# Start the dev server
npm run dev
Es gibt TypeScript-Unterstützung. Wenn Sie also ein TypeScript-Projekt starten möchten, ändern Sie den ersten Befehl in npx degit solidjs/templates/ts my-app.
Erstellen und Rendern von Komponenten
Zum Rendern von Komponenten ist die Syntax ähnlich wie bei React.js, sodass sie Ihnen vertraut vorkommen mag
import { render } from "solid-js/web";
const HelloMessage = props => <div>Hello {props.name}</div>;
render(
() => <HelloMessage name="Taylor" />,
document.getElementById("hello-example")
);
Wir müssen damit beginnen, die Funktion render zu importieren, dann erstellen wir ein Div mit etwas Text und einer Prop, und wir rufen render auf und übergeben die Komponente und das Container-Element.
Dieser Code wird dann zu echten DOM-Ausdrücken kompiliert. Zum Beispiel sieht der obige Code-Schnipsel, sobald er von Solid kompiliert wurde, ungefähr so aus
import { render, template, insert, createComponent } from "solid-js/web";
const _tmpl$ = template(`<div>Hello </div>`);
const HelloMessage = props => {
const _el$ = _tmpl$.cloneNode(true);
insert(_el$, () => props.name);
return _el$;
};
render(
() => createComponent(HelloMessage, { name: "Taylor" }),
document.getElementById("hello-example")
);
Der Solid Playground ist ziemlich cool und zeigt, dass Solid verschiedene Möglichkeiten zum Rendern bietet, darunter Client-seitig, Server-seitig und Client-seitig mit Hydration.
Ändernde Werte mit Signalen verfolgen
Solid verwendet einen Hook namens createSignal, der zwei Funktionen zurückgibt: einen Getter und einen Setter. Wenn Sie es gewohnt sind, ein Framework wie React.js zu verwenden, mag Ihnen das etwas seltsam vorkommen. Normalerweise würden Sie erwarten, dass das erste Element der Wert selbst ist; in Solid müssen wir jedoch explizit den Getter aufrufen, um abzufangen, wo der Wert gelesen wird, um seine Änderungen zu verfolgen.
Wenn wir zum Beispiel den folgenden Code schreiben
const [todos, addTodos] = createSignal([]);
Das Protokollieren von todos gibt nicht den Wert zurück, sondern eine Funktion. Wenn wir den Wert verwenden möchten, müssen wir die Funktion aufrufen, wie in todos().
Für eine kleine To-Do-Liste wäre dies
import { createSignal } from "solid-js";
const TodoList = () => {
let input;
const [todos, addTodos] = createSignal([]);
const addTodo = value => {
return addTodos([...todos(), value]);
};
return (
<section>
<h1>To do list:</h1>
<label for="todo-item">Todo item</label>
<input type="text" ref={input} name="todo-item" id="todo-item" />
<button onClick={() => addTodo(input.value)}>Add item</button>
<ul>
{todos().map(item => (
<li>{item}</li>
))}
</ul>
</section>
);
};
Der obige Code-Schnipsel würde ein Textfeld anzeigen und nach dem Klicken auf die Schaltfläche „Element hinzufügen“ die Todos mit dem neuen Element aktualisieren und es in einer Liste anzeigen.
Das kann sehr ähnlich wie die Verwendung von useState erscheinen, wie unterscheidet sich die Verwendung eines Getters? Betrachten Sie den folgenden Code-Schnipsel
console.log("Create Signals");
const [firstName, setFirstName] = createSignal("Whitney");
const [lastName, setLastName] = createSignal("Houston");
const [displayFullName, setDisplayFullName] = createSignal(true);
const displayName = createMemo(() => {
if (!displayFullName()) return firstName();
return `${firstName()} ${lastName()}`;
});
createEffect(() => console.log("My name is", displayName()));
console.log("Set showFullName: false ");
setDisplayFullName(false);
console.log("Change lastName ");
setLastName("Boop");
console.log("Set showFullName: true ");
setDisplayFullName(true);
Das Ausführen des obigen Codes würde zu folgendem Ergebnis führen:
Create Signals
My name is Whitney Houston
Set showFullName: false
My name is Whitney
Change lastName
Set showFullName: true
My name is Whitney Boop
Das Wichtigste, was Sie bemerken sollten, ist, dass My name is ... nach dem Festlegen eines neuen Nachnamens nicht protokolliert wird. Dies liegt daran, dass zu diesem Zeitpunkt niemand auf Änderungen an lastName() hört. Der neue Wert von displayName() wird erst gesetzt, wenn sich der Wert von displayFullName() ändert. Deshalb sehen wir den neuen Nachnamen, wenn setShowFullName wieder auf true gesetzt wird.
Dies gibt uns eine sicherere Möglichkeit, Wertaktualisierungen zu verfolgen.
Reaktivitätsprimitiven
Im letzten Code-Schnipsel habe ich createSignal eingeführt, aber auch ein paar andere Primitiven: createEffect und createMemo.
createEffect verfolgt Abhängigkeiten und wird nach jedem Rendering ausgeführt, bei dem sich eine Abhängigkeit geändert hat.
// Don't forget to import it first with 'import { createEffect } from "solid-js";'
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log("Count is at", count());
});
Count is at... wird jedes Mal protokolliert, wenn sich der Wert von count() ändert.
createMemo erstellt ein schreibgeschütztes Signal, das seinen Wert neu berechnet, wann immer sich die Abhängigkeiten des ausgeführten Codes ändern. Sie würden es verwenden, wenn Sie einige Werte zwischenspeichern und darauf zugreifen möchten, ohne sie neu zu bewerten, bis sich eine Abhängigkeit ändert.
Wenn wir zum Beispiel einen Zähler 100 Mal anzeigen und den Wert beim Klicken auf eine Schaltfläche aktualisieren möchten, würde die Verwendung von createMemo die Neuberechnung nur einmal pro Klick ermöglichen
function Counter() {
const [count, setCount] = createSignal(0);
// Calling `counter` without wrapping it in `createMemo` would result in calling it 100 times.
// const counter = () => {
// return count();
// }
// Calling `counter` wrapped in `createMemo` results in calling it once per update.
// Don't forget to import it first with 'import { createMemo } from "solid-js";'
const counter = createMemo(() => {
return count()
})
return (
<>
<button onClick={() => setCount(count() + 1)}>Count: {count()}</button>
<div>1. {counter()}</div>
<div>2. {counter()}</div>
<div>3. {counter()}</div>
<div>4. {counter()}</div>
<!-- 96 more times -->
</>
);
}
Lifecycle-Methoden
Solid stellt einige Lifecycle-Methoden zur Verfügung, wie z. B. onMount, onCleanup und onError. Wenn wir möchten, dass Code nach dem anfänglichen Rendering ausgeführt wird, müssen wir onMount verwenden
// Don't forget to import it first with 'import { onMount } from "solid-js";'
onMount(() => {
console.log("I mounted!");
});
onCleanup ähnelt componentDidUnmount in React – es wird ausgeführt, wenn eine Neuberechnung des reaktiven Geltungsbereichs erfolgt.
onError wird ausgeführt, wenn ein Fehler im nächstgelegenen Gültigkeitsbereich eines Kindes auftritt. Wir könnten es zum Beispiel beim Abrufen von Daten verwenden, wenn dies fehlschlägt.
Stores
Um Stores für Daten zu erstellen, stellt Solid createStore zur Verfügung, dessen Rückgabewert ein schreibgeschütztes Proxy-Objekt und eine Setter-Funktion sind.
Wenn wir zum Beispiel unser Todo-Beispiel ändern würden, um einen Store anstelle von Zustand zu verwenden, würde es etwa so aussehen
const [todos, addTodos] = createStore({ list: [] });
createEffect(() => {
console.log(todos.list);
});
onMount(() => {
addTodos("list", [
...todos.list,
{ item: "a new todo item", completed: false }
]);
});
Der obige Code-Schnipsel würde zunächst ein Proxy-Objekt mit einem leeren Array protokollieren, gefolgt von einem Proxy-Objekt mit einem Array, das das Objekt {item: "a new todo item", completed: false} enthält.
Eine Sache, die man beachten sollte, ist, dass das Top-Level-State-Objekt nicht verfolgt werden kann, ohne auf eine Eigenschaft darin zuzugreifen – deshalb protokollieren wir todos.list und nicht todos.
Wenn wir in createEffect nur todo protokollieren würden, würden wir den anfänglichen Wert der Liste sehen, aber nicht den nach der Aktualisierung, die in onMount vorgenommen wurde.
Um Werte in Stores zu ändern, können wir sie mit der Setter-Funktion aktualisieren, die wir beim Verwenden von createStore definieren. Wenn wir zum Beispiel einen Todo-Listeneintrag zu „abgeschlossen“ aktualisieren möchten, könnten wir den Store auf diese Weise aktualisieren
const [todos, setTodos] = createStore({
list: [{ item: "new item", completed: false }]
});
const markAsComplete = text => {
setTodos(
"list",
i => i.item === text,
"completed",
c => !c
);
};
return (
<button onClick={() => markAsComplete("new item")}>Mark as complete</button>
);
Kontrollfluss
Um die Verschwendung durch die Neuerstellung aller DOM-Knoten bei jeder Aktualisierung mit Methoden wie .map() zu vermeiden, bietet Solid Vorlagenhelfer.
Einige davon sind verfügbar, wie z. B. For zum Durchlaufen von Elementen, Show zum bedingten Ein- und Ausblenden von Elementen, Switch und Match zum Anzeigen von Elementen, die einer bestimmten Bedingung entsprechen, und mehr!
Hier sind einige Beispiele, wie man sie verwendet
<For each={todos.list} fallback={<div>Loading...</div>}>
{(item) => <div>{item}</div>}
</For>
<Show when={todos.list[0].completed} fallback={<div>Loading...</div>}>
<div>1st item completed</div>
</Show>
<Switch fallback={<div>No items</div>}>
<Match when={todos.list[0].completed}>
<CompletedList />
</Match>
<Match when={!todos.list[0].completed}>
<TodosList />
</Match>
</Switch>
Demo-Projekt
Dies war eine kurze Einführung in die Grundlagen von Solid. Wenn Sie damit experimentieren möchten, habe ich ein Starter-Projekt erstellt, das Sie automatisch auf Netlify bereitstellen und auf Ihr GitHub klonen können, indem Sie auf die Schaltfläche unten klicken!
Dieses Projekt enthält die Standardkonfiguration für ein Solid-Projekt sowie eine Beispiel-Todo-App mit den grundlegenden Konzepten, die ich in diesem Beitrag erwähnt habe, um Ihnen den Einstieg zu erleichtern!
Dieses Framework hat noch viel mehr zu bieten, als ich hier behandelt habe. Schauen Sie sich also gerne die Dokumentation für weitere Informationen an!
Warum hört displayFullName() statt des Nachnamens? Was löst den Effekt aus, auf onwe und nicht auf einen anderen zu hören?
Die Anweisung
createEffecthört auf Änderungen am Wert der FunktiondisplayName.Der von
displayNamezurückgegebene Wert ändert sich nur, wenn sichdisplayFullNamevontrueauffalseoder umgekehrt ändert.Daher wird die Variable
lastNamegeändert, wenn wirsetLastNamemit einem neuen Wert aufrufen, aber dies aktualisiert nicht, wasdisplayNameist, da zu diesem ZeitpunktdisplayFullNameimmer nochfalseist, sodassdisplayNamenur denfirstNamezurückgibt, der sich nicht geändert hat.Infolgedessen wird
createEffectnicht ausgelöst.Ich hoffe, es macht Sinn!
Das createEffect-Beispiel ist abgekürzt
Entschuldigung, ich werde versuchen, das bald zu aktualisieren, danke!!
Großartiger Artikel!
Tolle Einführungsartikel. Gut organisierter Inhalt und auf den Punkt gebracht, während alle wesentlichen Merkmale und Syntax abgedeckt werden. Danke.
Schöner Artikel, da er die Kernkonzepte klar demonstriert. Es ist jedoch schwierig, die Vorteile oder Nachteile gegenüber React zu erkennen – warum wurde diese Bibliothek erstellt, welches Problem löst sie? Gedanken?
Einfacheres Ausführungsmodell. In vielerlei Hinsicht hat es die Elemente von Svelte, um weniger Code zu schreiben, aber angewendet auf explizite Laufzeit-APIs. Keine Konzepte wie
useRefoderuseCallback. Solid ist in erster Linie eine Zustandsbibliothek, die den DOM rendert.Low-Level-Abstraktion. Sehr wenig zwischen dem, was Sie schreiben, und der Hardware. JSX-Elemente sind DOM-Elemente und Komponenten sind buchstäblich nur Funktionen. Dies entfernt den Abstraktionskosten und die Komplexität von der Ausführung.
Leistung. Die Leistung von Solid liegt weit vor anderen Lösungen, sowohl im Browser als auch auf dem Server. Sie ist näher an Vanilla JS als an der Konkurrenz. Sie erzeugt auch einige der kleinsten Bundle-Größen.
Weitere Informationen zu den Vorteilen
Einführung in die SolidJS UI-Bibliothek
Und sehen Sie sich diesen Vortrag von React Finland 2021 an
Ehrlich gesagt, ich weiß, dass technische Gründe allein nicht ausreichen, um Meinungen zu ändern, aber wir können nur dafür sorgen, dass dieser Teil unbestreitbar ist und hoffen, dass der Rest sich ergibt.
Danke für den Artikel, Charlie! Eine kleine Anmerkung: Es gibt kein componentDidUnmount in React, der korrekte Name ist componentWillUnmount, bitte bearbeiten Sie ihn.