今天,我们发布了材料翻译的第一部分,该部分致力于使用Babel为JavaScript创建自己的语法构造。

复习
首先,让我们看一看本材料的结尾将实现的目标:
我们将实现
@@
语法,该语法允许
currying函数。 该语法类似于用于创建
生成器函数的语法,但是在我们的示例中,在
function
关键字和
function
名称之间放置了一系列
@@
字符,而不是
*
符号。 结果,在声明函数时,可以使用形式
function @@ name(arg1, arg2)
的构造。
在上面的示例中,使用
foo
函数时,可以使用其
部分应用程序 。 通过向函数
foo
传递太多的参数(小于所需数量的参数)来调用函数
foo
,将返回一个可以接收其余参数的新函数:
foo(1, 2, 3);
我选择
@@
字符的顺序,因为
@
符号不能在变量名中使用。 这意味着形式
function@@foo(){}
的构造在语法上也是正确的。 另外,“ operator”
@
用于
装饰器功能 ,我想使用全新的功能。 结果,我选择了
@@
结构。
为了实现我们的目标,我们需要执行以下操作:
- 创建Babel解析器的fork。
- 创建自己的Babel插件进行代码转换。
看起来像不可能的事情?
实际上,这里没有什么可怕的,我们将一起详细分析所有内容。 我希望当您阅读本文时,您将精通Babel的复杂性。
创建一个叉子Babel
转到GitHub上
的 Babel
存储库 ,然后单击页面左上方的
Fork
按钮。
创建Babel的叉子( 全图 )顺便说一句,如果您是第一次创建流行的开源项目的分支-恭喜!
现在,在计算机上克隆Babel前叉并
准备工作 。
$ git clone https:
现在,让我简要地谈谈Babel存储库的组织。
Babel使用单一存储库。 所有软件包(例如
@babel/core
,
@babel/parser
,
@babel/plugin-transform-react-jsx
等)都位于
packages/
文件夹中。 看起来像这样:
- doc - packages - babel-core - babel-parser - babel-plugin-transform-react-jsx - ... - Gulpfile.js - Makefile - ...
我注意到Babel使用
Makefile来自动执行任务。 当通过
make build
项目时,
Gulp用作任务管理器。
代码转换为AST短期课程
如果您不熟悉诸如“解析器”和“抽象语法树”(AST)之类的概念,那么在继续阅读之前,我强烈建议您先阅读
本材料。
如果您非常简短地讨论解析(解析)代码时发生的情况,则会得到以下信息:
- 呈现为字符串(类型
string
)的代码看起来像一长串字符: f, u, n, c, t, i, o, n, , @, @, f, ...
- 从一开始,Babel就执行代码标记化。 在此步骤中,Babel扫描代码并创建令牌。 例如,类似
function, @@, foo, (, a, ...
- 然后,令牌将通过解析器进行解析。 在这里,Babel基于JavaScript语言的规范 ,创建了一个抽象语法树。
对于那些想了解更多有关编译器的人来说,
这是一个很好的资源。
如果您认为“编译器”是非常复杂且难以理解的东西,那么请知道实际上并不是所有事物都那么神秘。 编译只是解析代码并在其基础上创建新代码,我们将其称为XXX。 XXX代码可以用机器代码表示(也许,当我们考虑编译器时,机器代码是我们大多数人首先想到的)。 这可能是与旧版浏览器兼容的JavaScript代码。 实际上,Babel的主要功能之一就是将现代JS代码编译为过时的浏览器可以理解的代码。
为Babel开发自己的解析器
我们将在
packages/babel-parser/
文件夹中工作:
- src/ - tokenizer/ - parser/ - plugins/ - jsx/ - typescript/ - flow/ - ... - test/
我们已经讨论了令牌化和解析。 您可以在具有相应名称的文件夹中找到实现这些过程的代码。
plugins/
文件夹包含用于扩展基本解析器功能并向系统添加对其他语法的支持的插件(插件)。 例如,这正是实现
jsx
和
flow
支持的方式。
让我们使用
开发技术
通过测试 (测试驱动的开发,TDD)解决问题。 我认为,最简单的方法是先编写一个测试,然后逐步在系统上工作,使该测试运行无错误。 在不熟悉的代码库中工作时,此方法特别好。 TDD使您易于理解在何处需要更改代码以实现预期的功能。
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(); }); });
您可以像这样对
babel-parser
运行测试:
TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only
。 这将使您看到错误:
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)
如果发现查看所有测试花费太多时间,则可以运行
jest
直接运行
jest
:
BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/curry-function.js
我们的解析器发现了2个
@
令牌,这些令牌似乎完全是无辜的,它们不应该在哪里。
我怎么知道的 这个问题的答案将帮助我们找到使用
make watch
启动的代码监视模式的方法。
查看调用堆栈会导致我们进入
包/ babel-parser / src / parser / expression.js ,其中抛出
this.unexpected()
异常。
向该文件添加几个日志记录命令:
packages/babel-parser/src/parser/expression.js parseIdentifierName(pos: number, liberal?: boolean): string { if (this.match(tt.name)) {
如您所见,两个标记都是
@
:
TokenType { label: '@',
我如何发现构造
this.state.type
和
this.lookahead().type
将给我当前标记和下一个标记?
我将在本材料的有关
this.eat
,
this.match
和
this.next
函数的部分中讨论这
this.next
。
在继续之前,让我们总结一下:
- 我们为
babel-parser
编写了一个测试。 - 我们使用
make test-only
运行测试。 - 我们通过
make watch
使用了代码监视模式。 - 我们了解了解析器的状态,并在控制台中
this.state.type
有关当前令牌类型( this.state.type
)的信息。
现在,我们将确保不会将2个
@
字符视为单独的令牌,而将其作为新的
@@
令牌,我们决定将其用作currying函数。
新令牌:“ @@”
首先,让我们看一下确定令牌类型的位置。 这是文件
包/ babel-parser / src / tokenizer / types.js 。
在这里您可以找到令牌列表。 在此处添加新
atat
令牌的定义:
packages/babel-parser/src/tokenizer/types.js export const types: { [name: string]: TokenType } = {
现在让我们在代码中寻找在令牌化过程中创建令牌的位置。 在
babel-parser/src/tokenizer
找到
tt.at
字符的序列,将我们引到文件:
packages / babel-parser / src / tokenizer / index.js 。 在
babel-parser
令牌类型作为
tt
导入。
现在,如果在当前
@
符号之后出现另一个
@
,则创建一个新令牌
tt.atat
而不是
tt.at
令牌:
packages/babel-parser/src/tokenizer/index.js getTokenFromCode(code: number): void { switch (code) {
如果再次运行测试,您将注意到有关当前令牌和下一个令牌的信息已更改:
它看起来已经不错了。 我们将继续努力。
新解析器
在继续之前,请看一下AST中发电机功能的表示方式。
AST用于生成器功能( 全尺寸图片 )如您所见,
FunctionDeclaration
实体的
generator: true
属性表示这是一个generator
FunctionDeclaration
。
我们可以采用类似的方法来描述支持currying的函数。 即,我们可以将
curry: true
属性添加到
FunctionDeclaration
。
AST的currying功能( 全尺寸图片 )实际上,现在我们有一个计划。 让我们来处理它的实现。
如果您在代码中查找单词
FunctionDeclaration
,则可以转到
parseFunction
函数,该函数在
包/ babel-parser / src / parser / statement.js中声明 。 在这里,您可以找到设置了
generator
属性的行。 在代码中添加另一行:
packages/babel-parser/src/parser/statement.js export default class StatementParser extends ExpressionParser {
如果我们再次运行测试,一个令人惊喜的惊喜将等待着我们。 该代码已成功测试!
PASS packages/babel-parser/test/curry-function.js curry function syntax ✓ should parse (12ms)
这就是全部吗? 我们做了什么才能使测试奇迹般地通过?
为了找出答案,让我们谈谈解析的工作原理。 在这次对话中,希望您能理解
node.curry = this.eat(tt.atat);
。
待续...
亲爱的读者们! 你用巴别塔吗?
