我如何使用 Chevrotain、Typescript 和 Webpack 构建自己的简化 React
“知道如何解决每一个已经解决的问题。” ——理查德·费曼
在过去的 2 个月里,我一直在开发我自己的非常简化的 React 版本,称为 Syntact。我不会称它为成熟,但它已经有几个可用的功能,例如:
-
变量声明
-
函数声明
-
个组件
-
虚拟DOM
-
动态渲染
除此之外,我还构建了一个自定义编译器来替代Babel。
我为一门名为 Advanced Programming 的课程制作了这个项目,这是我的学士学位 应用计算机科学 的一部分。当我开始这个项目时,我不知道我在做什么。但是多亏了我的教练(Lars Willemsens)和万能的互联网,我设法创造了一些很酷的东西。
这不是一个关于如何制作自己的 React 的教程,但如果你想自己做这种项目,它肯定是一个很好的起点。所以让我们开始吧。
1\。编译器(我们自己的 Babel)
乐星
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--XMdVKOmy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/uploads/articles/kto7d02gch00gqpw657g.jpg)
第一步是编写“lexer”或“tokenizer”。 “Lex”代表词法分析,这基本上意味着将您的文本拆分为标记。它被用于创建编程语言,也用于文本处理和其他各种事情。
代币
令牌是代码的一个小单元。它的结构是由一个令牌名称和一个值组成的一对。示例:关键字“let”或“const”是标记。
Lexing with Chevrotain
编写词法分析器是整个过程的第一步,也是最简单的一步。我选择使用工具包Chevrotain来构建我的词法分析器。
要使用 Chevrotain 词法分析器,我们首先必须定义标记:
/// Keywords
const Import: chevrotain.ITokenConfig = createToken({ name: "Import", pattern: /import/ });
const From: chevrotain.ITokenConfig = createToken({ name: "From", pattern: /from/ });
const Return: chevrotain.ITokenConfig = createToken({ name: "Return", pattern: /return/ });
const Const: chevrotain.ITokenConfig = createToken({ name: "Const", pattern: /const/, longer_alt: Identifier });
const Let: chevrotain.ITokenConfig = createToken({ name: "Let", pattern: /let/, longer_alt: Identifier });
...
// We then add all the tokens to an array of tokens
let allTokens = [...]
进入全屏模式 退出全屏模式
好的,所以我们定义了我们的令牌并将它们捆绑在一个数组中。接下来,我们通过将标记传递给构造函数来实例化词法分析器,瞧。就这样 Syntact 词法分析器诞生了。
const syntactLexer: Lexer = new chevrotain.Lexer(allTokens);
进入全屏模式 退出全屏模式
现在我们可以使用这个词法分析器来标记我们的输入。
查看雪佛兰的文档了解更多信息:https://chevrotain.io/docs/tutorial/step1_lexing.html。
解析
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--YNDxVare--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/uploads/articles/fkki5ysc5tkvqyyg9pkr.jpg)
该过程的第二步是解析。解析器将标记列表转换为具体语法树 (CST),这是表示源代码的树数据结构的花哨术语。
为了防止歧义,解析器必须考虑括号和操作顺序。解析本身并不是很困难,但是随着更多功能的添加,解析会变得非常复杂。
用雪佛兰解析
同样,我使用 Chevrotain 为 Syntact 构建了一个解析器。 Chevrotain 解析器分析符合某些语法的标记。
语法
语法是对一组可接受的句子的描述。我们的解析器将使用这个语法来构建它的树。我用 ANTLR 语法语法编写了我的语法。
以下是我的语法文件中的一些示例:
importStatement
: import SEMICOLON
;
binaryExpression
: atomicExpression operator atomicExpression
;
进入全屏模式 退出全屏模式
在上面的示例中,我们定义了标识符的外观、转义序列是什么以及如何识别导入语句。
但老实说,在使用 Chevrotain 时,实际上并没有必要以这样的方式编写语法以使解析器正常工作。另一方面,它将帮助您更好地了解如何构建解析器。
编写解析器
一旦你的语法规划好了,就该开始构建你的解析器了。正如我们之前所说,解析器必须将词法分析器的输出转换为 CST。
首先,我们首先创建一个 Parser 类,我们将使用我们用来定义 Lexer 的标记数组来调用它。
class SyntactParser extends CstParser {
constructor() {
super(allTokens)
this.performSelfAnalysis()
}
// Later on, all grammer rules will come here...
}
进入全屏模式 退出全屏模式
接下来我们在 Parser 类中编写语法规则。两个(缩短的)示例:
public importStatement = this.RULE("importStatement", () => {
this.SUBRULE(this.import)
this.CONSUME(Semicolon)
});
});
public function = this.RULE("function", () => {
this.CONSUME(Function)
this.CONSUME(Identifier)
this.CONSUME(OpenRoundBracket)
this.SUBRULE(this.parameterDeclaration)
this.CONSUME(CloseRoundBracket)
this.CONSUME(OpenCurlyBracket)
this.MANY(() => {
this.OR([
{ ALT: () => { this.SUBRULE1(this.declareVariableStatement) } },
{ ALT: () => { this.SUBRULE(this.functionStatement) } },
{ ALT: () => { this.SUBRULE(this.functionCall) } }
])
})
this.OPTION(() => this.SUBRULE(this.returnStatement))
this.CONSUME(CloseCurlyBracket)
});
进入全屏模式 退出全屏模式
我们将根据我们之前使用 ANTLR 语法语法绘制的语法来编写语法规则。
一旦完成 - 相信我,这需要一段时间 - 我们可以开始解析令牌。输出将是雪佛兰为我们构建的 CST。
AST
一旦我们有了 CST,我们将把它转换成抽象语法树 (AST)。 AST 类似于 CST,但它包含特定于我们程序的信息,这意味着它不包含不必要的信息,如_分号_或_大括号_。为了获得 AST,我们必须使用 CST 访问者或我喜欢称之为解释器的方式“访问”CST。
口译员
解释器将遍历我们的 CST 并为我们的 AST 创建节点。感谢雪佛兰,这是一个相对可行的步骤。
下面是对 Syntact 解释器的简要介绍:
class SyntactInterpreter extends SyntactBaseCstVisitor {
constructor() {
super();
this.validateVisitor();
}
...
declareComponent(ctx: any) {
const componentName = ctx.Identifier[0].image;
const parameters = this.visit(ctx.parameterDeclaration);
const returnStatement = this.visit(ctx.returnStatement);
const variableStatements = [];
if (ctx.declareVariableStatement) {
ctx.declareVariableStatement.forEach((e: any) => {
variableStatements.push(this.visit(e))
})
}
return {
type: types.COMPONENT_DECLARATION,
id: {
type: types.IDENTIFIER,
name: componentName
},
parameters,
body: { variableStatements },
returnStatement
};
}
...
}
进入全屏模式 退出全屏模式
发电机
明白 AST 的意义了吗?凉爽的!现在我们可以继续并从生成器开始。生成器实际上将基于 AST 生成 JS 代码。
我发现这是整个解析过程中最难的部分之一。您必须遍历 AST 中的所有节点并从中生成有效的 JS 代码。
这可能是这样的:
class SyntactGenerator implements Generator {
...
private convertFunBody(body: any) {
let returnCode: any[] = [];
if (body.variableStatements) {
body.variableStatements.forEach((vS: any) => {
let datatype = vS.dataType;
let varName = vS.variableName;
let value = vS.value;
returnCode.push(`${datatype.toLowerCase()} ${varName} = ${value};\n`)
});
}
if (body.functionCalls) {
body.functionCalls.forEach((fC: any) => {
let params: string[] = [];
if (fC.params) {
fC.params.forEach((p: string) => { params.push(p) })
}
returnCode.push(`${fC.function}(${params.join(",")});`)
});
}
return returnCode.join("");
}
...
}
进入全屏模式 退出全屏模式
Err,请再来一次。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--HfjIp5JX --/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/khgr3csbrfcrnsfyn4f5.jpg)
读完这一切后筋疲力尽和有点困惑?我明白了。这是一个回顾:
-
Lexer u003d> 负责将原始文本转换为 tokens 流。
-
Parser u003d> 将标记流转换为 Concrete Syntax Tree (CST)。
-
CST 访问者/解释器 u003d> 递归访问 CST 中的每个节点,从而生成一个抽象语法树 (AST)。
-
Generator u003d > 实际上makes JS code 基于提供的AST。
一旦我们完成了上述工作,我们就可以开始制作我称之为“SyntactEngine”的东西了。
语法引擎
接下来,我创建了一个 SyntactEngine 类。这将使我们更容易协调将 JSX 转换为 JS 的不同阶段。它拥有一个名为“transpileJsxToJs”的入口点方法,我们稍后可以在我们的Webpack 加载器中使用它。
class SyntactEngine implements Engine {
private lexer: Lexer;
private parser: SyntactParser;
private interpreter: SyntactInterpreter;
private generator: Generator;
constructor() {
...
}
transpileJsxToJs(input: string): string {
...
}
tokenizeInput(input: string): ILexingResult {
...
}
parseInput(lexingResult: ILexingResult): ParseResultType {
...
}
toAst(parsedInput: ParseResultType) {
...
}
generateJsFromAst(ast: string): string {
...
}
}
进入全屏模式 退出全屏模式
2\。语法 API
我们有一个可以从 JSX 生成 JS 代码的工作编译器。现在我们需要构建一个 Syntact API,它实际上可以做像 React 这样的框架可以做的事情。创建一个虚拟 DOM,保存状态等等。
我现在只是坚持一个简单的虚拟 DOM。为此,我做了一个小的递归算法,它根据初始给定元素(例如 div)及其所有成员创建一个 DOM。
这是该方法的简化版本:
createDom(type: string, props: any, members: any, value: string | null) {
const element: any = document.createElement(type, null);
props.forEach((prop: any) => {
if (prop.type.substring(0, 2) === 'on') {
/* Check if prop type is a function handler
* Note: eval might be a security risk here. */
element[prop.type.toLowerCase()] = () => {
eval(prop.value)
}
} else if (prop.type == 'class') {
element.classList.add(prop.value)
}
});
return element;
}
进入全屏模式 退出全屏模式
3\。 Web客户端+Webpack
一旦我们有了编译器和 Syntact API,我们就可以开始使用 webpack 加载器将两者集成到我们的客户端应用程序中。
webpack 加载器将使用编译器对 Syntact JSX 进行预处理,并将其转换为 JS 代码。然后,JS 代码将使用 Syntact API 来实际使用 Syntact 的功能。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--udsLQ0bm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/uploads/articles/0dji7txtei33383yrf2m.jpg)
结束
如果你能做到这一点,感谢阅读!我希望这篇文章能帮助你理解 React 和 Babel 是如何在幕后工作的。
更多推荐

所有评论(0)