面条html5,使用 babel 全家桶模块化古老的面条代码
在最近的工作中,接手了一個古老的項目,其中的 JS 代碼是一整坨的面條代碼,約 3000 行的代碼全寫在一個文件里,維護起來著實讓人頭疼。
想不通為啥之前維護項目的同學能夠忍受這么難以維護的代碼……既然現在這個鍋被我拿下了,怎么著也不能容忍如此丑陋的代碼繼續存在著,必須把它優化一下。
橫豎看了半天,由于邏輯都揉在了一個文件里,看都看得眼花繚亂,當務之急便是把它進行模塊化拆分,把這一大坨面條狀代碼拆分成一個個模塊并抽離成文件,這樣才方便后續的持續優化。
一、結構分析
說干就干,既然要拆分成模塊,首先就要分析源碼的結構。雖然源碼內容很長很復雜,但萬幸的是它還是有一個清晰的結構,簡化一下,就是下面這種形式:
很容易看出,這是一種 ES5 時代的經典代碼組織方式,在一個 IIFE 里面放一個構造函數,在構造函數的 protorype 上掛載不同的方法,以實現不同的功能。既然代碼結構是清晰的,那么我們要做模塊化的思路也很清晰,就是想辦法把所有綁定在構造函數的 prototype 上的方法抽離出來,以模塊文件的形式放置,而源碼則使用 ES6 的 import 語句把模塊引入進來,完成代碼的模塊化:
為了完成這個效果,我們可以借助 @babel 全家桶來構造我們的轉化腳本。
二、借助 AST 分析代碼
關于 AST 的相關資料一搜一大堆,在這里就不贅述了。在本文中,我們會借助 AST 去分析源碼,挑選源碼中需要被抽離、改造的部分,因此 AST 可以說是本文的核心。在 https://astexplorer.net/ 這個網站,我們可以貼入示例代碼,在線查看它的 AST 長什么樣:
從右側的 AST 樹中可以很清晰地看到,Demo.prototype.func = function () {} 屬于 AssignmentExpression 節點,即為“賦值語句”,擁有左右兩個不同的節點(left,right)。
由于一段 JS 代碼里可能存在多種賦值語句,而我們只想處理形如 Demo.prototype.func = function () {} 的情況,所以我們需要繼續對其左右兩側的節點進行深入分析。
首先看左側的節點,它屬于一個“MemberExpression”,其特征如下圖箭頭所示:
對于左側的節點,只要它的 object.property.name 的值為 prototype 即可,那么對應的函數名就是該節點的 property.name。
接著看右側的節點,它屬于一個“FunctionExpression”:
我們要做的,就是把它提取出來作為一個獨立的文件。
分析完了 AST 以后,我們已經知道需要被處理的代碼都有一些什么樣的特征,接下來就是針對這些特征進行操作了,這時候就需要我們的 @babel 全家桶出場了!
三、處理代碼
首先我們需要安裝四個工具,它們分別是:@babel/parser:用于把 JS 源碼轉化成 AST;
@babel/traverse:用于遍歷 AST 樹,獲取當中的節點內容;
@babel/generator:把 AST 節點轉化成對應的 JS 代碼;
@babel/types:新建 AST 節點。
接下來新建一個 index.js 文件,引入上面四個工具,并設法加載我們的源碼(源碼為 demo/es5code.js):const fs = require('fs')
const { resolve } = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
const INPUT_CODE = resolve(__dirname, '../demo/es5code.js')
const code = fs.readFileSync(`${INPUT_CODE}`, 'utf-8')
接著使用 @babel/parser 獲取源碼的 AST:const ast = parser.parse(code)
拿到 AST 以后,就可以使用 @babel/traverse 來遍歷它的節點。從上一節的 AST 分析可以知道,我們只需要關注“AssignmentExpression”節點即可:traverse(ast, {
AssignmentExpression ({ node }) {
/* ... */
}
})
當前節點即為參數 node,我們需要分析它左右兩側的節點。只有當左側節點的類型為“MemberExpression”且右側節點的類型為“FunctionExpression”才需要進入下一步分析(因為形如 a = 1 之類的節點也屬于 AssignmentExpression 類型,不在我們的處理范圍內)。
由于 JS 中可能存在不同的 MemberExpression 節點,如 a.b.c = function () {},但我們現在只需要處理 a.prototype.func 的情況,意味著要盯著關鍵字 prototype。通過分析 AST 節點,我們知道這個關鍵字位于左側節點的 object.property.name 屬性中:
同時對應的函數名則藏在左側節點的 property.name 屬性中:
因此便可以很方便地提取出方法名:traverse(ast, {
AssignmentExpression ({ node }) {
const { left, right } = node
if (left.type === 'MemberExpression' && right.type === 'FunctionExpression') {
const { object, property } = left
if (object.property.name === 'prototype') {
const funcName = property.name // 提取出方法名
console.log(funcName)
}
}
}
})
可以很方便地把方法名打印出來檢查:
現在我們已經分析完左側節點的代碼,提取出了方法名。接下來則是處理右側節點。由于右側代碼直接就是一個 FunctionExpression 節點,因此我們要做的就是通過 @babel/generator 把該節點轉化成 JS 代碼,并寫入文件。
此外,我們也要把原來的代碼從 Demo.prototype.func = function () {} 轉化成 Demo.prototype.func = func 的形式,因此右側的節點需要從“FuncitionExpression”類型轉化成“Identifier”類型,我們可以借助 @babel/types 來處理。
還有一個事情別忘了,就是我們已經把右側節點的代碼抽離成了 JS 文件,那么我們也應該在最終改造完的源文件里把它們給引入進來,形如 import func1 from './func1'這種形式,因此可以繼續使用 @babel/types 的 importDeclaration() 函數來生成對應的代碼。這個函數參數比較復雜,可以封裝成一個函數:function createImportDeclaration (funcName) {
return t.importDeclaration([t.importDefaultSpecifier(t.identifier(funcName))], t.stringLiteral(`./${funcName}`))
}
只需要傳入一個 funcName,就可以生成一段 import funcName from './funcName' 代碼。
最終整體代碼如下:const fs = require('fs')
const { resolve } = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
const INPUT_CODE = resolve(__dirname, '../demo/es5code.js')
const OUTPUT_FOLDER = resolve(__dirname, '../output')
const code = fs.readFileSync(`${INPUT_CODE}`, 'utf-8')
const ast = parser.parse(code)
function createFile (filename, code) {
fs.writeFileSync(`${OUTPUT_FOLDER}/${filename}.js`, code, 'utf-8')
}
function createImportDeclaration (funcName) {
return t.importDeclaration([t.importDefaultSpecifier(t.identifier(funcName))], t.stringLiteral(`./${funcName}`))
}
traverse(ast, {
AssignmentExpression ({ node }) {
const { left, right } = node
if (left.type === 'MemberExpression' && right.type === 'FunctionExpression') {
const { object, property } = left
if (object.property.name === 'prototype') {
// 獲取左側節點的方法名
const funcName = property.name
// 獲取右側節點對應的 JS 代碼
const { code: funcCode } = generator(right)
// 右側節點改為 Identifier
const replacedNode = t.identifier(funcName)
node.right = replacedNode
// 借助 `fs.writeFileSync()` 把右側節點的 JS 代碼寫入外部文件
createFile(funcName, 'export default ' + funcCode)
// 在文件頭部引入抽離的文件
ast.program.body.unshift(createImportDeclaration(funcName))
}
}
}
})
// 輸出新的文件
createFile('es6code', generate(ast).code)
四、運行腳本
在我們的項目目錄中,其結構如下:.
├── demo
│ └── es5code.js
├── output
├── package.json
└── src
└── index.js
運行腳本,demo/es5code.js 的代碼將會被處理,然后輸出到 output 目錄:.
├── demo
│ └── es5code.js
├── output
│ ├── es6code.js
│ ├── func1.js
│ ├── func2.js
│ └── func3.js
├── package.json
└── src
└── index.js
看看我們的代碼:
大功告成!把腳本運用到我們的項目中,甚至可以發現原來的約 3000 行代碼,已經被整理成了 300 多行:
放到真實環境去跑一遍這段代碼,原有功能不受影響!
小結
剛剛接手這個項目,我的內心是一萬頭神獸奔騰而過,內心是非常崩潰的。但是既然接手了,就值得好好對待它。借助 AST 和 @babel 全家桶,我們就有了充分改造源碼的手段。花半個小時個腳本,把丑陋的面條代碼整理成清晰的模塊化代碼,內心的陰霾一掃而空,對這個古老的項目更是充滿了期待——會不會有更多的地方可以被改造被優化呢?值得拭目以待!
關于找一找教程網
本站文章僅代表作者觀點,不代表本站立場,所有文章非營利性免費分享。
本站提供了軟件編程、網站開發技術、服務器運維、人工智能等等IT技術文章,希望廣大程序員努力學習,讓我們用科技改變世界。
[使用 babel 全家桶模塊化古老的面條代碼]http://www.zyiz.net/tech/detail-147399.html
總結
以上是生活随笔為你收集整理的面条html5,使用 babel 全家桶模块化古老的面条代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c语言表达式与变量区别,C语言实战开发—
- 下一篇: python中seed的用法_Pytho