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的解決方案就復雜許多:
首先,js是不支持帶有換行的字符串變量的。所以我們不能用Python"""..."""這種語法,指定一個帶有換行的字符串。
其次,也不能在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
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個函數:
readFile用于讀取一個文件;
encrypt用于加密數據;
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引入的async和await語法,讓上面的過程變得更加直觀:
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__/中,一般會有如下文件:
main.mod.js,這是僅僅包含main.py各行代碼相應js轉譯的文件。
這個文件中會用很多transcrypt的函數“包裝”輸入的Python代碼,但基本上是一對一的對照,
因此可供用戶檢查代碼是否有問題等。
main.js,這是將main.py轉譯為js,并嵌入transcrypt本身需要的js代碼,打包而成的文件。
這個文件可以獨立嵌入到網頁了。
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_get與js_get區別調用python內部和原生js的get方法。
同理,對于set方法也有類似的規定。
2.2.3 __pragma__: 很多特性的開關
transcrypt提供了一個__pragma__函數,可以在程序中微調轉譯時的行為。
例如,要在python代碼中調用jQuery,不能直接使用$變量,因為$并不是一個規范的Python變量名。
這時,就要用__pragma__為$定義一個別名:
__pragma__("alias", "S", "$")
之后,就可以在程序中用S代替$來調用jQuery了。
__pragma__能進行的調節有許多。
很多情況下,Python本身允許的特性,transcrypt出于效率考慮默認不支持,便需要通過這一函數啟用。請讀者務必參考文檔。