待入門的 SWC
前情提要
1. rust 是什麼
一種編程語言,一度被賦予是 c++/c 語言的替代品
支持函數式,面向對象編程
2. 編譯器的基本工作流程
- 三個階段:解析、轉換、代碼生成
解析:將原始代碼 (字符) -> 詞法分析 -> 令牌 -> 語法分析 -> 抽象語法樹(AST)
轉換:處理抽象語法樹(AST)-> 轉換器 -> 新的語法樹(AST)
代碼生成:將處理後的新的語法樹(AST)-> 新的代碼 (字符)
SWC 爲什麼會誕生
既生瑜何生亮
swc 被推崇,除了大衆追捧外,也一定程度上說明 babel 存在一些不太容易提升的地方。引用社區所言:
- 語言劣勢,用 JS 寫的 babel 不能用多核 cpu 處理編譯任務鏈
“這其中轉換爲 AST 以及編譯成字節碼應該是最耗費性能的”
SWC 是什麼
Speedy Web Compiler
基於 Rust 語言的 JS 編譯器 (Javascript Compiler),其生態周邊中包含壓縮插件、打包工具 spack
主要對標 Babel,誓言要代替 Babel (據說:轉譯性能比 Babel 快 20 倍)
相關應用形式
-
@swc/cli: 自帶了一個內置的 CLI 命令行工具,可通過命令行編譯文件,類似於 @babel/cli
-
swc-loader: 該模塊使得可以與 webpack 一起使用,類似於 babel-loader
-
@swc/core: 核心 API 的集合,類似於 @babel/core
-
......
接下來簡單說一下:
@swc/cli 通過命令行調用
// 隨意找一個工程項目
npm i @swc/cli @swc/core
time npx swc[babel] xx.tsx -o after.js
確實會快,當然我的文件裏需要轉義的代碼不太多。
swc-loader
更換項目配置裏的 babel-loader (@vue/cli-plugin-babel)
build 後的耗時如下,由於沒有設置 swc 配套的插件和預設,結果卻是 swc-loader 更耗時。
當沒有額外配置 swc 相關轉換規則,複用 babel 相關的 plugin 和 preset,結果卻出乎意料,使用 babel-loder 是 1.75s,swc-loader 是 3.55s。
@swc/core
在 node 或者 @swc/cli 的任務流中可以使用 api 的形式調用其提供的方法,一般在構建工具中使用。
// const babel = require('@babel/core')
const swc = require('@swc/core')
module.exports = (api, options) => {
// babel.loadPartialConfigSync({ filename: api.resolve('src/main.js') })
swc.loadPartialConfigSync({ filename: api.resolve('src/main.js') })
const res = await swc.transform('xxx.js', {
filename: "out.js",
jsc:{
parser: {
target: 'es5'
}
}
})
}
核心 API
1. swc_ecma_parser 獲得 AST 結構
**Rust版本**
// 聲明swc文件對象
let fm = cm.new_source_file(
FileName::Custom("test.js".into()), // 文件名
"function foo() { console.log('foo')}".into(), // 文件裏具體的代碼
);
// 聲明 詞法分析Lexer解析規則
let lexer = Lexer::new(
Syntax::Es(Default::default()),
EsVersion::Es2016,
StringInput::from(&*fm), // fm 裏的代碼
None,
);
// 聲明Parser對象
let capturing = Capturing::new(lexer);
let mut parser = Parser::new_from(capturing);
// 在JS中每個文件一般是一個Module
let mut module = parser.parse_module();
**JS版本**
async parse(src: string, options?: ParseOptions, filename?: string): Promise<Program> {
options = options || { syntax: "ecmascript" };
options.syntax = options.syntax || "ecmascript";
if (bindings) {
const res = await bindings.parse(src, toBuffer(options), filename);
return JSON.parse(res);
}
一般得到的 ast 樹形結構如下,與 babel 類似:
2. swc_ecma_transform 轉換 AST
Rust版本
// 遍歷ast body體
for item in module.body {
// 當前的引用值與item匹配時,把item ref賦值給 var
if let ModuleItem::ModuleDecl(ModuleDecl::Import(var)) = item {
let source = &*var.src.value;
if source == "antd" {
for specifier in &var.specifiers {
match specifier {
ImportSpecifier::Named(ref s) => {
let ident = format!("{}", s.local.sym);
specifiers.push(format!("antd/es/{}/style/index.css", ident.to_lowercase()));
}
ImportSpecifier::Default(ref s) => {}
ImportSpecifier::Namespace(ref ns) => {}
}
}
}
}
JS版本
transformSync(src, options) {
const { plugin } = options, newOptions = __rest(options, ["plugin"]);
// 是否有plugin
if (plugin) {
const m = typeof src === "string" ? this.parseSync(src, (_c = options === null || options === void 0 ? void 0 : options.jsc) === null || _c === void 0 ? void 0 : _c.parser, options.filename) : src;
return this.transformSync(plugin(m), newOptions);
}
return bindings.transformSync(isModule ? JSON.stringify(src) : src, isModule, toBuffer(newOptions));
}
function loadBinding(dirname, filename = 'index', packageName) {
// 獲取系統信息
const triples = triples_1.platformArchTriples[PlatformName][ArchName];
// 遍歷信息
for (const triple of triples) {
if (packageName) {
try {
// 獲取到需要加載的二進制文件路徑
// /Users/xx/swc-demo/node_modules/@swc/core-darwin-x64/swc.darwin-x64.node
return require(
require.resolve(
`${packageName}-${triple.platformArchABI}`,
{ paths: [dirname] }
));
}
}
}
}
依舊用上面的例子嘗試一下:
可以得到以下結果:
"[\n 1,\n 2,\n 3\n].map(function(i) {\n return i + 1;\n});\n"
這裏沒有像我們理解的編譯器中的三階段,就直接可以得到新的代碼段。
與 babel 相似點:
-
在 traverse 轉換 ast 過程中,都會基於 helpers、plugins/preset 的規則進行轉換
-
plugins 和 preset 的執行順序一致
與 babel 的不同點:
- 沒有類似 @babel/generator 生成新代碼,transform 階段就可以生成。
// 聲明compiler對象
let compiler = Compiler::new(fm.clone());
//生成新的ast
let mut newmodule = module.clone() as Module;
//調用Compile對象的print方法生成新的代碼
let new_res = compiler.print(&newmodule,
EsVersion::Es2016,
...args).unwrap();
- visitor 對象中沒有 enter/exit 鉤子,由頂層的 visitProgram 往內遞歸執行節點訪問操作。(看源碼中被註釋掉了,可能未來會支持)
面向 js 生態的插件系統
與 babel 類似,swc/core 也暴露一些 api 給開發者,可以自定義轉換代碼的插件。但官網提到目前有些性能問題。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/8Bc9k880DpwsaY9pJIowSA