esbuild是最近比較火的編譯工具,在有些領域已經開始替代webpack或babel,下面一塊兒來看看這個工具的詳細內容。javascript
這裏是一份壓測數據,從圖中能夠看出esbuild性能拔羣
關於它的性能爲什麼如此優越,在官方解釋中有如下幾點:css
JavaScript必須基於解釋器的node環境才能執行,因此當webpack等工具解釋完自己的代碼後,可能esbuild已經完成編譯工做了,而這時候webpack纔開始執行編譯。java
此外,Go的核心設計是並行的,而JavaScript不是。node
Go有線程之間的共享內存,而JavaScript則必須在線程之間進行數據序列化。react
Go和JavaScript都有並行的垃圾收集器,但Go的堆是在全部線程之間共享的,而JavaScript的每一個線程都有一個獨立的堆。JavaScript工做線程並行量減小了一半,由於還有一半CPU核心正忙於爲另外一半收集垃圾。webpack
esbuild內部的算法是通過精心設計的,以儘量使全部可用的CPU核心徹底飽和。git
大體有三個階段:解析、鏈接和代碼生成。解析和代碼生成是大部分的工做,而且是徹底可並行的。github
因爲全部線程都共享內存,在編譯導入相同的JavaScript庫的不一樣入口點時,工做能夠很容易地被分享。大多數現代計算機都有不少內核,因此並行化是一個很大的性能提高。web
本身編寫而不是使用第三方庫有不少性能上的好處。能夠從一開始就考慮到性能問題,以確保全部的東西都使用一致的數據結構,避免昂貴的轉換,並且在必要時可進行普遍的架構變動。固然,缺點是這是一個很大的工做量。正則表達式
編譯器在理想狀況下大可能是輸入長度的O(n)複雜性。所以,若是你正在處理大量的數據,內存訪問速度可能會嚴重影響性能。你須要對數據處理的越少,編譯器速度就越快。
esbuild加載器的做用與webpack中loader做用相似,都是對於某種類型的文件進行編譯,具體功能介紹以下:
這個加載器默認用於.js、.cjs和.mjs文件。.cjs擴展名被node用於CommonJS模塊,而.mjs擴展名被node用於ECMAScript模塊,儘管esbuild並無對這二者進行區分。
esbuild支持全部現代JavaScript語法。然而,較新的語法可能不被舊的瀏覽器所支持,因此你可能想配置目標選項,告訴esbuild將較新的語法轉換爲適當的舊語法。
但請注意,ES5支持的不是很好,目前還不支持將ES6+語法轉換爲ES5。
這個加載器對於.ts和.tsx文件是默認啓用的,這意味着esbuild內置了對TypeScript語法的解析和丟棄類型註釋的支持。然而,esbuild不作任何類型檢查,因此你仍然須要在esbuild中並行運行tsc -noEmit來檢查類型。
須要注意的是,esbuild在編譯時不會進行類型檢查,這應該在編譯以前使用ide去檢查
將會把xml代碼轉換成js代碼
對於.json文件,這個加載器是默認啓用的。它在構建時將JSON文件解析成一個JavaScript對象,並將該對象做爲默認導出。
對於.css文件,這個加載器是默認啓用的。它以CSS語法的形式加載文件。CSS在esbuild中是一種第一類內容類型,這意味着esbuild能夠直接編譯CSS文件,而不須要從JavaScript代碼中導入你的CSS。
你能夠@導入其餘CSS文件,用url()引用圖片和字體文件,esbuild會把全部東西編譯在一塊兒。注意,你必須爲圖像和字體文件配置一個加載器,由於esbuild沒有任何預配置。一般這是一個數據URL加載器或外部文件加載器。
請注意,esbuild還不支持CSS Module,因此來自CSS文件的導出名稱集目前老是空的。將來計劃支持CSS Module。
對於.txt文件,這個加載器是默認啓用的。它在構建時將文件加載爲字符串,並將字符串導出爲默認導出。使用它看起來像這樣。
這個加載器將在構建時以二進制緩衝區的形式加載文件,並使用Base64編碼將其嵌入到包中。文件的原始字節在運行時被從Base64解碼,並使用默認的導出方式導出爲Uint8Array。
這個加載器將在構建時以二進制緩衝區的形式加載文件,並使用Base64編碼將其嵌入到編譯中的字符串。這個字符串將使用默認的導出方式導出。
這個加載器將在構建時做爲二進制緩衝區加載文件,並將其做爲Base64編碼的數據URL嵌入到編譯中。這個字符串是用默認的導出方式導出的。
這個加載器會將文件複製到輸出目錄,並將文件名做爲一個字符串嵌入到編譯中。這個字符串是使用默認的導出方式導出的。
爲了更加方便的使用,esbuild提供了api調用的方式,在調用api時傳入option進行相應功能的設置。在esbuild的API中,有兩個主要的API調用方式:transform和build。二者的區別在因而否最終生成文件。
Transform API調用對單個字符串進行操做,不須要訪問文件系統。很是適合在沒有文件系統的環境中使用或做爲另外一個工具鏈的一部分。下面是個簡單例子:
require('esbuild').transformSync('let x: number = 1', { loader: 'ts', }) => { code: 'let x = 1;\n', map: '', warnings: [] }
Build API調用對文件系統中的一個或多個文件進行操做。這使得文件能夠相互引用,並被編譯在一塊兒。下面是個簡單例子:
require('fs').writeFileSync('in.ts', 'let x: number = 1') require('esbuild').buildSync({ entryPoints: ['in.ts'], outfile: 'out.js', })
插件API屬於上面提到的API調用的一部分,插件API容許你將代碼注入到構建過程的各個部分。與API的其餘部分不一樣,它不能從命令行中得到。你必須編寫JavaScript或Go代碼來使用插件API。
插件API只能用於Build API,不能用於Transform API
若是你正在尋找一個現有的esbuild插件,你應該看看現有的esbuild插件的列表。這個列表中的插件都是做者特地添加的,目的是爲了讓esbuild社區中的其餘人使用。
一個esbuild插件是一個包含name和setup函數的對象。它們以數組的形式傳遞給構建API調用。setup函數在每次BUILD API調用時都會運行一次。
下面咱們來嘗試自定義一個插件
import fs from 'fs' export default { name: "env", setup(build) { build.onLoad({ filter: /\.tsx$/ }, async (args) => { const source = await fs.promises.readFile(args.path, "utf8"); const contents = source.toString(); console.log('文件內容:',contents) return { contents: contents, loader: "tsx", }; }); }, };
name通用表明這個插件的名稱
每一個模塊都有一個相關的命名空間。默認狀況下,esbuild在文件命名空間中操做,它對應於文件系統中的文件。可是esbuild也能夠處理那些在文件系統上沒有對應位置的 "虛擬 "模塊。虛擬模塊一般使用文件之外的命名空間來區分它們和文件系統模塊。
每一個回調都必須提供一個正則表達式做爲過濾器。當路徑與過濾器不匹配時,esbuild會跳過調用回調,這樣作是爲了提升性能。從esbuild的高度並行的內部調用到單線程的JavaScript代碼是很昂貴的,爲了得到最大的速度,應該儘量地避免。
你應該儘可能使用過濾器正則表達式,而不是使用JavaScript代碼進行過濾。這樣作更快,由於正則表達式是在esbuild內部評估的,根本不須要調用JavaScript。
一個使用onResolve添加的回調將在esbuild構建的每一個模塊的每一個導入路徑上運行。這個回調能夠定製esbuild如何進行路徑解析。
一個使用onLoad添加的回調能夠對文件內容進行處理並返回。
esbuild中的loader是直接把某個格式的文件直接處理並返回,而插件api也具備接觸文件內容的機會,這二者的執行時機在文檔中並無提到。
import fs from "fs"; export default { name: "env", setup(build) { build.onLoad({ filter: /\.tsx$/ }, async (args) => { const source = await fs.promises.readFile(args.path, "utf8"); const contents = source.toString(); //astHandle只能處理js內容,對ts或jsx不認識,編譯報錯 const result = astHandle(contents) return { contents: result, loader: "tsx", }; }); }, };
從上面代碼例子中看出,插件api在接受到文件內容後,並不能直接處理tsx的內容,由於咱們可能不具有處理tsx的能力,這時候並不能顯示定義插件在tsx轉換成js以後執行。要想處理這種狀況只能藉助esbuild的transform api能力。
import fs from "fs"; import esbuild from "esbuild"; export default { name: "env", setup(build) { build.onLoad({ filter: /\.tsx$/ }, async (args) => { const source = await fs.promises.readFile(args.path, "utf8"); const contents = source.toString(); const result = astHandle(esbuild.transformSync(contents, { loader: 'tsx', })) return { contents: result.code, loader: "tsx", }; }); }, };
因爲babel的社區插件較多,這給本來使用babel的項目遷移到esbuild設置了障礙,可使用社區提供的esbuild-plugin-babel一鍵遷移,例如使用antd組件時須要配合使用antd-plugin-import插件,具體以下:
import babel from "esbuild-plugin-babel"; import esbuild from "esbuild"; import path, { dirname } from "path"; import { fileURLToPath } from "url"; const babelJSON = babel({ filter: /\.tsx?/, config: { presets: ["@babel/preset-react", "@babel/preset-typescript"], plugins: [["import", { libraryName: "antd", style: "css" }]], }, }); const __dirname = dirname(fileURLToPath(import.meta.url)); esbuild .build({ entryPoints: [path.join(__dirname + "/app.tsx")], outdir: "out", plugins: [babelJSON], }) .catch(() => process.exit(1));
插件API並不打算涵蓋全部的用例。它不可能關聯編譯過程的每一個部分。例如,目前不可能直接修改AST。這個限制的存在是爲了保持esbuild出色的性能特徵,同時也是爲了不暴露出太多的API表面,這將是一個維護的負擔,而且會阻止涉及改變AST的改進。
一種考慮esbuild的方式是做爲網絡的 "連接器"。就像本地代碼的連接器同樣,esbuild的工做是接收一組文件,解析並綁定它們之間的引用,並生成一個包含全部代碼連接的單一文件。一個插件的工做是生成最終被連接的單個文件。
esbuild中的插件最好是在相對範圍內工做,而且只定制構建的一個小方面。例如,一個自定義格式(如YAML)的特殊配置文件的插件是很是合適的。你使用的插件越多,你的構建速度就越慢,尤爲是當你的插件是用JavaScript編寫的時候。若是一個插件適用於你構建中的每個文件,那麼你的構建極可能會很是慢。若是緩存適用,必須由插件自己來完成。