Transcrypt是一个很有意思的工具: 它让你告别手写繁复的JavaScript代码,使用相对简明清晰的Python代替这一工作。 之后使用这个工具,可以把Python编写的代码转换成JavaScript。

1. 为什么不直接写JavsScript?

JavaScript本身不算是很难的编程语言,但还是有很多不便之处。这里只能举几个例子:

1.1 js的模块化问题。

想要实现一个很复杂的js程序,一般要考虑将不同的功能拆分成模块,然后各自完成各自的任务。 然而,js本身没有什么方式可以做到这一点: 要么在浏览器或者NodeJS中,使用require这样的方案(”AMD”—-模块异步加载)(浏览器还需要额外加载require.js), 要么使用各种打包工具(CommonJS—-规定了通用的模块定义方式),根据模块各部分代码相互关系,将所有的代码打包进一个巨大的文件。

Transcrypt支持Python的模块机制(import语法),效果上最后还是生成一个打包的代码文件,但使用起来,比CommonJS要清晰一些。

1.2 缺乏对class这样的关键字的支持

JavaScript虽然算是一种基于对象的语言—-JavaScript中包括数字、字符串等都是对象, 但又没有办法通过class来自己声明一个对象。

这就导致不同的程序员,会采用不同的方案来构建对象。比如有使用Object的:

var owl = {};
owl.color = "white";
owl.category = "Bubo bubo";
owl.eat = function(){ ... };

或者改写一个函数,增加各种attributes:

function Owl(){
    var self = this;
    this.color = "white";
    this.category = "Bubo bubo";
    this.eat = function(){ ... };
    return this;
}

var owl = new Owl();

1.3 缺乏语法糖,代码复杂

Python简洁的语法,很多得益于丰富的语法糖: 很简洁的几句话就可以实现复杂的功能,而JavaScript则可能要从头开始写一系列代码。

举几个例子:

a) 使用给定的值生成一个字符串

output = """My name is {name}, I'm {age} years old.
My favourite fruit is {favourite}.""".format({
    "favourite": "banana",
    "name": "Alice",
    "age": 18,
})
console.log(output)

这也是一个简单的例子,如何套用模板,将数据转换成供显示的文本。js的解决方案就复杂许多:

  1. 首先,js是不支持带有换行的字符串变量的。所以我们不能用Python"""..."""这种语法,指定一个带有换行的字符串。
  2. 其次,也不能在js的字符串中定义占位符,然后用数据填充,所以要自行断开字符串,将数据与字符串合并。

看起来结果就是:

var data = {
    "favourite": "banana",
    "name": "Alice",
    "age": 18,
};
var output = "My name is " + data.name + ", I'm " + data.age + " years old.\n";
output += "My favourite fruit is " + data.favourite + ".";
console.log(output);

即使看上去代码量差不多,这样做还是有一个缺点: 如果需要修改模板,使用Python,只要将带有占位符的字符串换掉就可以了(比如字符串在单独的模板文件中予以定义), 但使用js,就需要具体修改带有逻辑运算符的代码。这样就很不直观,容易出错。

b) 根据已有数据生成一个新的数组

假设我们有一个数组[-2, -1, 0, 1, 2, 3, 4],想列出这个数组中大于0的各个数字的平方。

使用js的思路,是新建一个数组,然后遍历原数组,检查各项是否大于0, 如果是,将结果记录到新的数组:

var original = [-2, -1, 0, 1, 2, 3, 4];
var result = [];
for(var i=0; i<original.length; i++){
    if(original[i] > 0){
        result.push(Math.pow(original[i], 2));
    }
}

Python则简单很多。Python支持在构建list的时候,指定条件,并使用表达式指定要放入list的值:

original = [-2, -1, 0, 1, 2, 3, 4]
result = [each ** 2 for each in original if each > 0]

1.4 复杂的异步编程

JavaScript的另一个令人诟病的问题,是在调用一系列异步调用时难以组织代码。

实际应用中,无论NodeJS还是浏览器,都要遇到各种异步调用的代码。

假设我们有3个函数:

  1. readFile用于读取一个文件;
  2. encrypt用于加密数据;
  3. upload用于将数据上传到服务器。

这三个函数都是异步调用的。

传统的方式,一般是在实际完成功能需要的参数之外,这些函数还接受一个callback(回调函数)作为输入。 这样调用上述函数之后,他们会立刻返回,并不耽误时间。然后当函数对应的后台任务完成时,再呼叫callback,传入结果。

比如这样看,可以假设readFile的用法是readFile(filename, callback)。 这个函数接受一个filename作为要读取的文件名,然后在读取完毕时呼叫callback函数,传入读取结果。

实际调用时,要写成:

readFile("/path/to/somefile", function(err, data){
    if(err){
        ...
        return; // 如果读取文件出错
    }
    // 否则读取文件成功,继续做下一步的事情
});

假如我们想在读取文件后,将文件内容加密,就要在上述代码的读取成功处, 加上对encrypt=encrypt(data, callback)函数的调用。于是代码就成了这个样子:

readFile("/path/to/somefile", function(err, data){
    if(err){
        ...
        return; // 如果读取文件出错
    }
    // 读取文件成功,加密data
    encrypt(data, function(err, ciphertext){
        if(err){
            ...
            return; // 加密失败
        }
        // 加密文件成功,继续做别的事情
    });
});

如果进一步,想在加密之后,将密文上传到服务器,整个代码就……

readFile("/path/to/somefile", function(err, data){
    if(err){
        ...
        return; // 如果读取文件出错
    }
    // 读取文件成功,加密data
    encrypt(data, function(err, ciphertext){
        if(err){
            ...
            return; // 加密失败
        }
        // 加密文件成功,密文是ciphertext,将它上传
        upload(ciphertext, function(err, result){
            if(err){
                ...
                return; // 上传失败
            }
            // 上传成功
        });
    });
});

很多时候,要完成的工作都是上面这样一连串的。 js的回调机制,就很容易引入如上所示不断嵌套的代码,让结果变得十分难看,所谓的回调地狱

为了解决这个问题,JavaScript引入了Promise机制。 这样每个函数就都返回一个Promise对象。 Promise对象支持使用.then()这样的方法,将流程串联起来:

readFile("/path/to/somefile")
.then(encrypt)
.then(upload)
.catch(function(err){
    ...
    // 如果上面某一步出错的话,就直接跳到这里
});

这样简明了许多。

Transcrypt在Promise机制的基础上,结合了Python 3引入的asyncawait语法,让上面的过程变得更加直观:

async def encryptFileAndUpload(path):
    try:
        data = await readFile(path)
        ciphertext = await encrypt(data)
        await upload(ciphertext)
    except:
        # 处理错误
encryptFileAndUpload("/path/to/somefile")

使用await,这些异步的调用又可以写成一系列按顺序完成的代码,而不失对各步的控制,思路清晰很多。

2. 如何使用Transcrypt?

2.1 安装和调用

Transcrypt可以通过pip安装:

$ sudo pip install transcrypt

安装后,直接在命令行调用就可以了,例如,

$ transcrypt main.py

可以将main.py转换成js文件。

类似Python 3在执行代码前会在__pycache__目录中放置Python字节码那样, transcrypt会将转换后的文件放到相应的__javascript__目录。

如果要求转换的是main.py,则在__javascript__/中,一般会有如下文件:

  1. main.mod.js,这是仅仅包含main.py各行代码相应js转译的文件。 这个文件中会用很多transcrypt的函数“包装”输入的Python代码,但基本上是一对一的对照, 因此可供用户检查代码是否有问题等。
  2. main.js,这是将main.py转译为js,并嵌入transcrypt本身需要的js代码,打包而成的文件。 这个文件可以独立嵌入到网页了。
  3. main.min.js。如果见到这个文件,说明transcrypt还进行了代码压缩,这个文件应该也可以单独运行。

代码压缩(minify)需要很多运算,耗费时间,在开发程序时不方便。可以使用-n参数,跳过这一步:

$ transcrypt -n main.py

这样就可以只生成main.js

2.2 一些坑

Python和Javascript毕竟是两种语言,虽然transcrypt可以在很大程度上将前者翻译为后者, 但有些两种语言内在的不一致,决定了编程时还需要注意许多坑。

Transcrypt官网的文档中,详细列明了各种坑和其理由。一定要仔细阅读。

2.2.1 类的继承,方法的重载

在Python中,定义基类和子类,是很方便的事情,有时候这种编程方式会节约大量时间。

在transcrypt中,可以使用类的继承,但必须在转译时,于命令行使用-e6选项。 这样生成的代码为ECMAScript 6代码,才可以启用这样的诸多功能。

重载Python的类,自定义诸如__str____getattr____setattr__这样的方法, 可以编写出非常简明的程序。例如一个模板程序:

class Template:

    def __init__(self):
        self.__kv = {}

    def __str__(self):
        return """Welcome to {sitename}!""".format(self.__kv)

    def __setattr__(self, key, value):
        self.__kv[key] = value

t = Template()
t.sitename = "NeoAtlantis"

print(str(t)) # 输出 Welcome to NeoAtlantis!

这个程序定义的Template类,只需要像操作一般的类那样给它的属性赋值, 然后用str函数令其生成文本即可。这种用法非常直观,但也必须在启用了ECMAScript 6转译后才能利用。

2.2.2 特殊的方法名称

如果要在python中调用一个js模块(比如jQuery的$.get)的get方法,直接调用会出现错误。

根据transcrypt文档的解释,这是因为在python中,get本身具有特定的用途, 故须使用py_getjs_get区别调用python内部和原生js的get方法。

同理,对于set方法也有类似的规定。

2.2.3 __pragma__: 很多特性的开关

transcrypt提供了一个__pragma__函数,可以在程序中微调转译时的行为。

例如,要在python代码中调用jQuery,不能直接使用$变量,因为$并不是一个规范的Python变量名。 这时,就要用__pragma__$定义一个别名:

__pragma__("alias", "S", "$")

之后,就可以在程序中用S代替$来调用jQuery了。

__pragma__能进行的调节有许多。 很多情况下,Python本身允许的特性,transcrypt出于效率考虑默认不支持,便需要通过这一函数启用。请读者务必参考文档。