待入門的 SWC

前情提要

1. rust 是什麼

  1. 一種編程語言,一度被賦予是 c++/c 語言的替代品

  2. 支持函數式,面向對象編程

2. 編譯器的基本工作流程

  1. 三個階段:解析、轉換、代碼生成
  • 解析:將原始代碼 (字符) -> 詞法分析 -> 令牌 -> 語法分析 -> 抽象語法樹(AST)

  • 轉換:處理抽象語法樹(AST)-> 轉換器 -> 新的語法樹(AST)

  • 代碼生成:將處理後的新的語法樹(AST)->  新的代碼 (字符)

SWC 爲什麼會誕生

既生瑜何生亮

swc 被推崇,除了大衆追捧外,也一定程度上說明 babel 存在一些不太容易提升的地方。引用社區所言:

  1. 語言劣勢,用 JS 寫的 babel 不能用多核 cpu 處理編譯任務鏈 

“這其中轉換爲 AST 以及編譯成字節碼應該是最耗費性能的”

SWC 是什麼

  • Speedy Web Compiler

  • 基於 Rust 語言的 JS 編譯器 (Javascript Compiler),其生態周邊中包含壓縮插件、打包工具 spack

  • 主要對標 Babel,誓言要代替 Babel (據說:轉譯性能比 Babel 快 20 倍)

相關應用形式

  1. @swc/cli: 自帶了一個內置的 CLI 命令行工具,可通過命令行編譯文件,類似於 @babel/cli

  2. swc-loader: 該模塊使得可以與 webpack 一起使用,類似於 babel-loader

  3. @swc/core:  核心 API 的集合,類似於 @babel/core

  4. ......

接下來簡單說一下:

@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 相似點:

  1. 在 traverse 轉換 ast 過程中,都會基於 helpers、plugins/preset 的規則進行轉換

  2. plugins 和 preset 的執行順序一致

與 babel 的不同點:

  1. 沒有類似 @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();
  1. visitor 對象中沒有 enter/exit 鉤子,由頂層的 visitProgram 往內遞歸執行節點訪問操作。(看源碼中被註釋掉了,可能未來會支持)

面向 js 生態的插件系統

與 babel 類似,swc/core 也暴露一些 api 給開發者,可以自定義轉換代碼的插件。但官網提到目前有些性能問題。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8Bc9k880DpwsaY9pJIowSA