Go hackathon 與 packer
題圖來自美美噠攝像小姐姐~
前言
最近的事情無敵多,上至面試,下至辦證,雜七雜八得從開學開始就不得安生,更別說沉下心來去做點技術上的拓展。本來暑期實習的時候列了一堆 TODO list,結果開學了兩個月也沒見著動工,先小小地羞愧一下。趁著上個周末的空閑去參加了 Go hackathon,本來只是想著去劃划水,結果沒想到收穫頗多,屬引文以記之。
一點微小的想法
我最早報名這次的 hackathon 也是沖著 Go 來的,當時還不知道 asta Xie 會來(如果知道的話就更不會猶豫了)。我對 Go 的感情還是挺複雜的,以後專門出一篇文章來聊聊我對編程語言的一些看法吧,此處按下不表。
由於 hackathon 的主題是 Golang,這對於我這個偽 FE 來說有點頭疼。到底做什麼好呢?
Docker、虛擬化、分散式!
這些東西對於我來說只限於「聽過」這個層面了,連了解都算不上,pass。
拿 Go 做 server ?
講道理這種東西去水水校創還行,拿到比賽上還是有點貽笑大方了,pass 。
卧槽這個不行那個也不行難道你要拿 Go 寫前端啊!!!
唔,好主意!但是 Go 直接跑在瀏覽器得 compile 成 JS,那樣也不太美...
要不然試一試拿 Go 做點前端的工具?這樣就能繞開瀏覽器環境的限制啦~
好的,另一個問題出現了:做什麼前端工具好呢?
唔,前段時間貌似有一個 flag 是讀一讀 rollup 的源碼...
就決定是你了!packer!
先來聊聊 tree shaking
其實我選擇閱讀 rollup 源碼是有原因的,一個是因為它比 webpack 要小,閱讀起來障礙要少一點,關注點會集中一點(並不會);另外一個原因就是想看一看比 webpack 穩健一萬倍的 tree shaking 是怎麼樣實現的。但是最新的 rollup 源碼看得我還是有點頭大,靈機一動(不存在)的我找到了 rollup 的第一個 release 版本,0.3.1版本,看起來是 2015 年提交的(那時候我還不會前端),看起來就要簡單清爽很多了。
其實單論打包,相對而言還不是一件特別複雜的事情(當然循環依賴的問題另說),不過 rollup 是怎麼在短短几百行的代碼里實現 tree shaking 的呢?讓我來岔開一下話題,看一看 tree shaking 這個名字。
tree shaking 其實非常形象:一顆參天大樹甩了甩樹枝,將枯死的樹葉甩去。這項技術就是用於依賴可達性的分析,對於不可達(或者說用不到)的代碼,在打包的時候將可以這些代碼從包中去除。其實上面的解釋有一點不是很對,就是對於不可待的代碼,rollup 並不是將其從包中刪去,而是在打包的時候就只打包那些被依賴的代碼。這其實就是編譯原理中的數據流分析的一個常見應用:可達性分析,而得益於 JS 清晰的模塊化,數據流的源頭其實就是 export 和 import 關鍵字。
那 rollup 是怎麼做到這樣清晰的依賴分析呢?這就涉及到對 JS 代碼的分析了。rollup 有一個 analyse 模塊,專門負責依賴收集,從它的代碼中可見一斑:
walk(statement, {n enter (node) {n // ...nn switch (node.type) {n case FunctionExpression:n case FunctionDeclaration:n case ArrowFunctionExpression:n // ... n newScope = new Scope({n parent: scope,n params: names, n block: falsen })n // ... n case BlockStatement:n // ...n case CatchClause:n // ...n case VariableDeclaration:n // ...n case ClassDeclaration:n // ...n }n if (newScope) {n Object.defineProperty(node, _scope, {value: newScope})n scope = newScopen }n // ...n },n leave (node) {n // ...n }n })nn // ...n })n
這第一步做的就是變數元信息(meta info)的收集,利用 acorn 來對代碼進行分析,針對可能出現變數聲明定義的地方,利用一個 Scope 的類型來做信息綁定
class Scope {n constructor ( options ) {n options = options || {};n this.parent = options.parent;n this.depth = this.parent ? this.parent.depth + 1 : 0;n this.names = options.params || [];n this.isBlockScope = !!options.block;n }nn add ( name, isBlockDeclaration ) {n // ...n }nn contains ( name ) {n return !!this.findDefiningScope( name );n }nn findDefiningScope ( name ) {n // ...n }n}n
這裡其實就是一個詞法作用域的實現,相當於手動把代碼中的隱藏的作用域信息提取了出來,所以我稱之為 元信息 。
ast.body.forEach(statement => {n function checkForReads (node, parent) {n // ...n }nn function checkForWrites (node) {n function addNode (node, disallowImportReassignments) {n // ...n }nn if (node.type === AssignmentExpression) {n addNode(node.left, true)n }nn else if (node.type === UpdateExpression) {n addNode(node.argument, true)n }nn else if (node.type === CallExpression) {n node.arguments.forEach(arg => addNode(arg, false))n }n }nn walk(statement, {n enter (node, parent) {n checkForReads(node, parent)n checkForWrites(node, parent)n },n leave (node) {n // ...n }n })n })n
這第二步才是所謂的依賴收集,通過 checkForReads 和 checkForWrites 這兩個函數來對所有的之前 scope 中的變數進行一遍清理,其中 read 對應的是對依賴的引用,而 write 對應的是對依賴的修改,很明顯這兩者都是依賴收集中的一部分。
當 rollup 收集了這些依賴之後,它將這些信息放在了原先的 ast 的語句節點之中
Object.defineProperties(statement, {n _defines: {value: {}},n _modifies: {value: {}},n _dependsOn: {value: {}},n _included: {value: false, writable: true},n _module: {value: module},n _source: {value: magicString.snip(statement.start, statement.end)}, // TODO dont use snip, its a waste of memoryn _margin: {value: [0, 0]},n _leadingComments: {value: []},n _trailingComment: {value: null, writable: true},n })n
直接修改 ast 這個做法怎麼樣我覺得見仁見智,不過結合我最近看的垠神的 pysonar 的思路,其實可以很簡單的用一個全局的符號表,將依賴放到表中記錄起來,這樣在做打包的時候就不用再 walk 整個 ast 了,既簡單又直觀。
其他細節
解決了 tree shaking 的問題,其他的內容就好多了。循環依賴和重複依賴的問題在該版本的 rollup 中並沒有去解決,我們自己就添加了一個依賴表,在每次解析模塊的過程中去檢查依賴表,比較簡單地解決了這些問題。
最後我們拿 socket.io 的代碼作為打包對象,打包過程一共只花了0.5s 左右的時間,確實具有一定的性能提升。
關於前端工具鏈
這次的開發是基於 Go 的平台,但是可以用的 JS 工具實在是太少了,主要用了 otto/parser 來對 JS 代碼做分析。但是這個 parser 設計的有點捉襟見肘,比如表達式的非正交性,並且可拓展性實在無法和 acorn 相比,這也是我們提出符號依賴表的一個原因,讓分析與結構盡量解耦。
雖然這次開發的結果差強人意,但是還是讓我有了點小想法:前端工具鏈使用 Node 的優勢很明顯是生態,但是前端工具鏈的生態其實並沒有我想的那麼繁複,要達到支持正常的開發工作的程度其實並不需要造太多輪子。比如要完成一個可用級別的打包工具,大概只需要一周的時間,加上其他資源的 loader 和 dev server 其實一個月就夠,而這段時間所換來的性能提升卻是巨大的,這也是我們的想試著用 Go 來試水前端工具鏈的原因。
結語
一次 hard core 的 hackathon,一個有點意思的小 idea,需要沉澱一下了。最近準備花點時間讀一讀程序分析與優化的書,寫點代碼,期待一下 Go 在前端領域的拓展~
推薦閱讀:
※為什麼有些遊戲公司的前端職位和其他的研發職位工資差距那麼大?
※Chrome擴展開發02--擴展中的基本操作01
※女孩子做前端開發容易嗎?最多能做幾年?
※DDFE 技術周刊(第三期)2016.11.18