欢迎转载,请支持原创,保留原文链接:blog.ilibrary.me

Babel

一直很想研究Babel,给baby写插件,改JS语法. 最近看了一篇文章,感觉非常不错.于是借此机会试了一把,添加的语法很简单,就是给JS里面的数字加上一个计量单位,比如说1k代表1000,1m代表百万, 1b代表10亿,这样子呢,就是读起来更简单,更方便易懂。

Babel是什么?Babel是一个javascript编译器.它的作用是什么呢?它的作用是把一些新的JS语法转换成老的JS语法,让老的js环境(比如ie8, ie9)也可以运行最新的js代码。

Babel的核心功能就是编译JS,然后翻译成老的js语法,让所有的环境都可以执行最新的JS代码,解决JS代码兼容性的问题。

并且Babel本身是用JS写的,改起来特别方便,部署分发也非常方便,可以规避掉很多跨平台的问题,各种环境的问题,因为JS本身依赖于JS引擎,JS引擎把这些底层的区别和依赖全隔离掉了。现在JS引擎的跨平台性非常棒!

AST

研究Babel避不开AST。 AST全称Abstract Syntax Tree, 是用来表示代码语法的一种结构。在编译前,代码就是纯文本,编译器读取代码为string以后开始分析,转换成有意义的一种语法数据结构,方便程序后续分析处理,这种数据结构就是AST. AST会贯穿编译的词法分析,语法分析,语法转换和代码生产。

这里有一篇解释AST的文章, What is an Abstract Syntax Tree.

AST Explorer是一个很强大的AST分析器,可以用来生成和分析很多语言的AST。 并且,它是开源的,github.

你可以尝试填入下面的代码,感受一下JS的AST:

var a = 1 
let b = 2
let c = a + b

自己创建新的JS语法

我以前也没改过Babel,就从比较小的一个改动入手吧,越小越好,体会一下通过Babel给JS加新语法的流程。

我加的语法是给数字后面挂个后缀,一个表示计量单位的后缀。比如1k, 代表数字1000.

编译的时候,去识别这种后缀然后转换成对应的数字。

环境搭建

先clone babel, 搭建环境.

Fork & Clone babel

因为要改babel的语法解析,所以最好自己Fork Babel, 然后clone下来, 方便修改:

git clone git clone https://github.com/your_id/babel.git

配置&编译

Clone下来以后开始准备环境。

请保证你的电脑装了最新的nodejs, 我当前用14.5会遇到编译错误,用node 14.15就能正常编译。

运行下面的命令:

cd babel
make bootstrap
make build

如果没有抛错,那环境就已经准备就绪了。

Compiler & Transpiler

编译器的一般流程是:

  1. 先做词法分析,就是把一些文字转换成一些词(token).
  2. 然后再做语法分析,然后再做代码生成。
  3. 最后一步是AST生成JS, js prettifier, 代码格式化, minify, urglify都是在这一步。
  4. 在Babel,他也是这个流程,但它多做了一步,他在于词法分析和语法分析之后会有一个语法转换的步骤,这个步骤是可以通过插件自定义的, 你可以通过自定义的转换器插件把你自己家的语法转换成老的浏览器能识别的JS语法AST,大部分babel都是干预这一步。

所以Babel和一般的编译器的差别在于,他是一个编译编译器,同事它是一个转换器,我们称之为transpiler。

有家世界500强,用ECMAScript(js)做基语言,然后做了一个transpiler, 把JS写的代码翻译成各种语言。我所在的项目就有用这个工具, 叫xscript,它把js写的代码翻译成各种语言(js, java, C#, swift, OC).

Babel强大也就在这里,它可以很好很方便制定一些语言特性,然后把它翻译成一个比较老的版本的JS, 老的JS就可以跑在新老浏览器上了。

动手改

回到我们的新JS语法。

我们做这个东西很简单,给数字尾部添加计量单位,它不涉及太多的转换的工作,主要集中在词法分析这块,因为语法分析都可以跳过了,后面的也不用管。

整个动手改的流程我参考了新加坡一位小哥的博客

  1. 第一步先写一个测试,这个测试呢,这个测试肯定会失败,就用新的语法写测试,肯定会失败.
     // filename: packages/babel-parser/test/number-metric.js
    
     import { parse } from '../lib';
    
     function getParser(code) {
     return () => parse(code, { sourceType: 'module' });
     }
    
    
     describe('digital metric k syntax', function() {
         it('should parse', function() {
         expect(getParser(`1k`)()).toMatchSnapshot();
         });
     });
    
    
  2. 运行测试, 可以通过下面任意方法运行测试:
    1. 方法一: BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/number-metric.js
    2. 方法二: TEST_ONLY=babel-parser TEST_GREP="digital metric" make test-only
  3. 失败以后呢,看报错报错报什么地方然后去找到这个,报错的那一行,你就会发现你就可快速定位到他的词法分析代码,数字词法分析所在的地方。
     BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/number-metric.js
     FAIL  packages/babel-parser/test/number-metric.js
     digital metric k syntax
         ✕ should parse (3 ms)
    
     ● digital metric k syntax › should parse
    
         SyntaxError: Identifier directly after number (1:1)
    
         774 |
         775 |   _raise(errorContext, message) {
         > 776 |     const err = new SyntaxError(message);
             |                 ^
         777 |     Object.assign(err, errorContext);
         778 |
         779 |     if (this.options.errorRecovery) {
    
         at Parser._raise (packages/babel-parser/lib/index.js:776:17)
         at Parser.raiseWithData (packages/babel-parser/lib/index.js:769:17)
         at Parser.raise (packages/babel-parser/lib/index.js:737:17)
         at Parser.readNumber (packages/babel-parser/lib/index.js:8845:18)
         at Parser.getTokenFromCode (packages/babel-parser/lib/index.js:8541:14)
         at Parser.nextToken (packages/babel-parser/lib/index.js:8073:12)
         at Parser.parse (packages/babel-parser/lib/index.js:13621:10)
         at parse (packages/babel-parser/lib/index.js:13676:38)
         at packages/babel-parser/test/number-metric.js:6:16
         at Object.<anonymous> (packages/babel-parser/test/number-metric.js:38:21)
    
     Test Suites: 1 failed, 1 total
     Tests:       1 failed, 1 total
     Snapshots:   0 total
     Time:        1.702 s
     Ran all test suites matching /packages\/babel-parser\/test\/number-metric.js/i.
    

    我们可以看到 Parser.readNumber (packages/babel-parser/lib/index.js:8845 这地方,readNumber出错了。

  4. 定位词法分析的地方以后就可以直接去改了。添加自己的词法分析逻辑。 注意: packages/babel-parse/lib下的文件是编译后的文件,我们不应该改那个文件,应该改packages/babel-parser/src/tokenizer/index.js->readNumber,

    添加如下代码(注意总共有两处修改,有标注):

        if (next === charCodes.lowercaseM) {
       this.expectPlugin("decimal", this.state.pos);
       if (hasExponent || hasLeadingZero) {
         this.raise(start, Errors.InvalidDecimal);
       }
       ++this.state.pos;
       isDecimal = true;
     }
    
     // 第一处修改: 下面三行是加的第一段代码
     if (next === charCodes.lowercaseK) {
       ++this.state.pos;
     }
    
     if (isIdentifierStart(this.input.codePointAt(this.state.pos))) {
       throw this.raise(this.state.pos, Errors.NumberIdentifier);
     }
    
     // remove "_" for numeric literal separator, and trailing `m` or `n`
     // 这是第二处修改: 把字母k替换成'000'
     const str = this.input.slice(start, this.state.pos).replace(/[_mn]/g, "").replace(/[_k]/, '000');
    
     if (isBigInt) {
       this.finishToken(tt.bigint, str);
       return;
     }
    
  5. 添加新的词法分析之后,你再运行这个测试就发现这个测试能过了。

到这里,新的语法算是成功加上去了。看着是不是很简单?

确实不难,因为这个只修改了parser里的词法分析器,没有涉及语法分析和代码转换的部分,这也是为何我们没有接触到AST的原因。

我的代码提交在support new syntax 1k

问题

这个改动只是入个门,找个感觉。并且它是有缺陷的,整数它可以正常工作。浮点数就会不工作。

请思考:

  1. 0.1k == 0.1000? 0.1k == 100, 不等于0.1000
  2. 0xb111k这个怎么解释?
  3. 当前只支持了’k’, 还有’m’,’b’, ‘g’, ‘t’没有支持。’m’已经被另外一个特性给占了,@babel/plugin-syntax-decimal plugin,

我希望把这个问题放后面改进。改babel不容易,不要把调起得太高。

下一步

  1. 处理浮点数代单位的问题
  2. 搞中文js编程,冲刺核高基
  3. 写一个xscript, 搞多语言转译.

参考

  1. Babel 英文官方文档
  2. Babel 中文文档
  3. 如何创建babel插件
  4. 写一个算术表达式预先计算的babel插件
  5. 预计算代码地址
  6. 倒转函数名
  7. 自定义js syntax, curry function, example
  8. 写一个简单的内嵌的babel plugin