Heute veröffentlichen wir den ersten Teil der Übersetzung des Materials, der der Erstellung eigener Syntaxkonstrukte für JavaScript mit Babel gewidmet ist.

Rückblick
Schauen wir uns zunächst an, was wir erreichen werden, wenn wir am Ende dieses Materials angelangt sind:
Wir werden die
@@
implementieren, die
Currying- Funktionen ermöglicht. Diese Syntax ähnelt der zum Erstellen von
Generatorfunktionen verwendeten . In unserem Fall wird jedoch anstelle des Zeichens
*
eine Folge von
@@
Zeichen zwischen dem
function
und dem
function
@@
. Wenn Sie Funktionen deklarieren, können Sie daher eine Konstruktion der Formularfunktion
function @@ name(arg1, arg2)
.
Im obigen Beispiel können Sie bei der Arbeit mit der Funktion
foo
deren
Teilanwendung verwenden . Wenn Sie die Funktion
foo
indem Sie so viele Parameter übergeben, die geringer sind als die Anzahl der benötigten Argumente, wird eine neue Funktion zurückgegeben, die die verbleibenden Argumente annehmen kann:
foo(1, 2, 3);
Ich habe die Reihenfolge der
@@
Zeichen gewählt, da das
@
-Symbol nicht in Variablennamen verwendet werden kann. Dies bedeutet, dass ein Konstrukt der Formularfunktion
function@@foo(){}
auch syntaktisch korrekt ist. Außerdem wird der Operator
@
für
Dekorationsfunktionen verwendet , und ich wollte etwas völlig Neues verwenden. Aus diesem Grund habe ich mich für die
@@
.
Um unser Ziel zu erreichen, müssen wir folgende Aktionen ausführen:
- Erstellen Sie eine Abzweigung des Babel-Parsers.
- Erstellen Sie Ihr eigenes Babel-Plugin für die Code-Transformation.
Sieht nach etwas Unmöglichem aus?
Tatsächlich gibt es hier nichts Schreckliches. Wir werden alles gemeinsam im Detail analysieren. Ich hoffe, wenn Sie dies lesen, werden Sie die Feinheiten von Babel meisterhaft beherrschen.
Eine Gabel erstellen Babel
Gehen Sie zum Babel-
Repository auf GitHub und klicken Sie auf die Schaltfläche
Fork
, die sich oben links auf der Seite befindet.
Erstellen einer Gabel von Babel ( Bild in voller Größe )Übrigens, wenn Sie gerade zum ersten Mal die Gabelung des beliebten Open Source-Projekts erstellt haben - herzlichen Glückwunsch!
Klonen Sie nun die Babel-Gabel auf Ihren Computer und
bereiten Sie sie für die Arbeit vor .
$ git clone https:
Lassen Sie mich nun kurz auf die Organisation des Babel-Repositorys eingehen.
Babel verwendet ein Monorepository. Alle Pakete (z. B.
@babel/core
,
@babel/parser
,
@babel/plugin-transform-react-jsx
usw.) befinden sich im Ordner
packages/
. Es sieht so aus:
- doc - packages - babel-core - babel-parser - babel-plugin-transform-react-jsx - ... - Gulpfile.js - Makefile - ...
Ich stelle fest, dass Babel ein
Makefile verwendet, um Aufgaben zu automatisieren. Beim
make build
eines Projekts mit dem
make build
wird
Gulp als Task-Manager verwendet.
Code-Konvertierung in einen AST-Kurzkurs
Wenn Sie mit Konzepten wie „Parser“ und „Abstract Syntax Tree“ (AST) nicht vertraut sind, empfehle ich Ihnen dringend, sich
dieses Material anzusehen, bevor Sie weiterlesen.
Wenn Sie sehr kurz darüber sprechen, was beim Parsen (Parsen) des Codes passiert, erhalten Sie Folgendes:
- Der als Zeichenfolge dargestellte Code (
string
) sieht aus wie eine lange Liste von Zeichen: f, u, n, c, t, i, o, n, , @, @, f, ...
- Zu Beginn führt Babel eine Code-Tokenisierung durch. In diesem Schritt scannt Babel den Code und erstellt Token. Zum Beispiel so etwas wie
function, @@, foo, (, a, ...
- Dann werden die Token zum Parsen durch den Parser geleitet. Hier erstellt Babel basierend auf der Spezifikation der JavaScript-Sprache einen abstrakten Syntaxbaum.
Hier ist eine großartige Ressource für diejenigen, die mehr über Compiler erfahren möchten.
Wenn Sie denken, dass der „Compiler“ etwas sehr Komplexes und Unverständliches ist, dann wissen Sie, dass in Wirklichkeit nicht alles so mysteriös ist. Beim Kompilieren wird einfach der Code analysiert und auf seiner Grundlage ein neuer Code erstellt, den wir XXX nennen werden. Der XXX-Code kann durch Maschinencode dargestellt werden (vielleicht taucht Maschinencode zuerst in den Köpfen der meisten von uns auf, wenn wir an den Compiler denken). Dies kann JavaScript-Code sein, der mit älteren Browsern kompatibel ist. Tatsächlich ist eine der Hauptfunktionen von Babel die Kompilierung von modernem JS-Code zu Code, der für veraltete Browser verständlich ist.
Entwickeln Sie Ihren eigenen Parser für Babel
Wir werden im Ordner
packages/babel-parser/
:
- src/ - tokenizer/ - parser/ - plugins/ - jsx/ - typescript/ - flow/ - ... - test/
Wir haben bereits über Tokenisierung und Analyse gesprochen. Sie finden den Code, der diese Prozesse implementiert, in Ordnern mit den entsprechenden Namen. Die
plugins/
Ordner enthalten Plugins (Plug-Ins), die die Funktionen des Basisparsers erweitern und dem System Unterstützung für zusätzliche Syntaxen hinzufügen. Genau so wird beispielsweise
flow
Unterstützung von
jsx
und
flow
implementiert.
Lösen wir unser Problem mithilfe der
Entwicklungstechnologie durch Testen (Test Driven Development, TDD). Meiner Meinung nach ist es am einfachsten, zuerst einen Test zu schreiben und dann nach und nach am System diesen Test fehlerfrei auszuführen. Dieser Ansatz eignet sich besonders gut, wenn Sie in einer unbekannten Codebasis arbeiten. Mit TDD können Sie leicht nachvollziehen, wo Sie Änderungen am Code vornehmen müssen, um die beabsichtigte Funktionalität zu implementieren.
packages/babel-parser/test/curry-function.js import { parse } from '../lib'; function getParser(code) { return () => parse(code, { sourceType: 'module' }); } describe('curry function syntax', function() { it('should parse', function() { expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot(); }); });
Sie können den Test für
babel-parser
folgendermaßen
TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only
:
TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only
. Auf diese Weise können Sie die Fehler anzeigen:
SyntaxError: Unexpected token (1:9) at Parser.raise (packages/babel-parser/src/parser/location.js:39:63) at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16) at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18) at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23) at Parser.parseIdentifier (packages/babel-parser/src/parser/statement.js:1096:52)
Wenn Sie feststellen, dass das Anzeigen aller Tests zu lange dauert, können Sie, um den gewünschten Test auszuführen,
jest
direkt aufrufen:
BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/curry-function.js
Unser Parser entdeckte 2
@
Token, scheinbar völlig unschuldig, wo sie nicht sein sollten.
Woher wusste ich das? Die Antwort auf diese Frage hilft uns, die Verwendung des Codeüberwachungsmodus zu finden, der mit dem
make watch
gestartet wird.
Das Anzeigen des Aufrufstapels führt uns zu
packages / babel-parser / src / this.unexpected()
/ expression.js , wo die Ausnahme
this.unexpected()
ausgelöst wird.
Fügen Sie dieser Datei einige Protokollierungsbefehle hinzu:
packages/babel-parser/src/parser/expression.js parseIdentifierName(pos: number, liberal?: boolean): string { if (this.match(tt.name)) {
Wie Sie sehen können, sind beide Token
@
:
TokenType { label: '@',
Wie habe ich herausgefunden, dass die Konstruktionen
this.state.type
und
this.lookahead().type
mir die aktuellen und nächsten Token geben?
Ich werde darüber in dem Abschnitt dieses Materials
this.eat
, der den Funktionen
this.eat
,
this.match
und
this.next
.
Bevor wir fortfahren, fassen wir zusammen:
- Wir haben einen Test für
babel-parser
. - Wir haben den Test nur mit
make test-only
. - Wir haben den Code-Überwachungsmodus mit
make watch
. - Wir haben den Status des Parsers
this.state.type
und Informationen zum Typ des aktuellen Tokens ( this.state.type
) in der Konsole this.state.type
.
Und jetzt werden wir sicherstellen, dass 2
@
-Zeichen nicht als separate Token wahrgenommen werden, sondern als neues
@
@@
, das wir für Curry-Funktionen verwendet haben.
Neues Token: "@@"
Schauen wir uns zunächst an, wo die Arten von Token bestimmt werden. Dies ist die Datei
packages / babel-parser / src / tokenizer / types.js .
Hier finden Sie eine Liste der Token. Fügen Sie hier die Definition des neuen
atat
Tokens hinzu:
packages/babel-parser/src/tokenizer/types.js export const types: { [name: string]: TokenType } = {
Suchen wir nun nach der Stelle im Code, an der beim Tokenisierungsprozess Token erstellt werden. Wenn Sie die Reihenfolge der
tt.at
Zeichen in
babel-parser/src/tokenizer
tt.at
babel-parser/src/tokenizer
, gelangen Sie zur folgenden Datei:
packages / babel-parser / src / tokenizer / index.js . Im
babel-parser
werden Tokentypen als
tt
importiert.
Wenn nun nach dem aktuellen
@
-Symbol ein weiteres
@
, erstellen
tt.atat
anstelle des
tt.at
Tokens ein neues Token
tt.atat
:
packages/babel-parser/src/tokenizer/index.js getTokenFromCode(code: number): void { switch (code) {
Wenn Sie den Test erneut ausführen, werden Sie feststellen, dass sich die Informationen zu den aktuellen und nächsten Token geändert haben:
Es sieht schon ziemlich gut aus. Wir werden die Arbeit fortsetzen.
Neuer Parser
Bevor Sie fortfahren, schauen Sie sich an, wie Generatorfunktionen in AST dargestellt werden.
AST für Generatorfunktion ( Bild in voller Größe )Wie Sie sehen können, gibt das Attribut
generator: true
der Entität
FunctionDeclaration
an, dass es sich um eine Generatorfunktion handelt.
Wir können einen ähnlichen Ansatz verfolgen, um eine Funktion zu beschreiben, die das Curry unterstützt. Wir können nämlich das Attribut
curry: true
zu
FunctionDeclaration
hinzufügen.
AST für die Curry-Funktion ( Bild in voller Größe )Eigentlich haben wir jetzt einen Plan. Beschäftigen wir uns mit seiner Implementierung.
Wenn Sie im Code nach dem Wort
FunctionDeclaration
suchen, können Sie zur Funktion
parseFunction
, die in
packages / babel-parser / src / parser / statement.js deklariert ist. Hier finden Sie die Zeile, in der das
generator
ist. Fügen Sie dem Code eine weitere Zeile hinzu:
packages/babel-parser/src/parser/statement.js export default class StatementParser extends ExpressionParser {
Wenn wir den Test erneut durchführen, erwartet uns eine angenehme Überraschung. Der Code wurde erfolgreich getestet!
PASS packages/babel-parser/test/curry-function.js curry function syntax ✓ should parse (12ms)
Und das ist alles? Was haben wir getan, um den Test auf wundersame Weise zu bestehen?
Um dies herauszufinden, lassen Sie uns darüber sprechen, wie das Parsen funktioniert. Ich hoffe, Sie werden im Verlauf dieses Gesprächs verstehen, wie die Zeile
node.curry = this.eat(tt.atat);
.
Fortsetzung folgt…
Liebe Leser! Benutzt du babel
