精讀《Rust 是 JS 基建的未來》

黃子毅發表於2021-11-22

Rust Is The Future of JavaScript Infrastructure 這篇文章講述了 Rust 正在 JS 基建圈流行的事實:WebpackBabelTerserPrettierESLint 這些前些年才流行起來的工具都已有了 Rust 替代方案,且效能有著 10~100 倍的提升。

前端基建的迭代浪潮從未停歇,當上面這些工具給 Gulp、js-beautify、tslint 等工具蓋上棺材蓋時,基於 Rust 的新一代構建工具已經悄悄將棺材蓋懸掛在 webpack、babel、prettier、terser、eslint 它們頭上,不知道哪天就會蓋上。

原文已經有了不錯的 中文翻譯,值得一提的是,原文一些英文名詞對應著特定中文解釋,記錄如下:

  • low-level programming:低階程式設計 底層程式設計。
  • ergonomics:人體工程學 人機工程學。
  • opinionated:自以為是,固執的 開箱即用的。
  • critical adoption:批判性採用 技術選型臨界點。

精讀

本文不會介紹 Rust 如何使用,而會重點介紹原文提到的 Rust 工具鏈的一些基本用法,如果你感興趣,可以立刻替換現有的工具庫!

swc

swc 是基於 Rust 開發的一系列編譯、打包、壓縮等工具,並且被廣泛應用於更多更上層的 JS 基建,大大推動了 Rust 在 JS 基建的影響力,所以要第一個介紹。

swc 提供了一系列原子能力,涵蓋構建與執行時:

@swc/cli

@swc/cli 可以同時構建 js 與 ts 檔案:

const a = 1
npm i -D @swc/cli
npx swc ./main.ts

# output:
# Successfully compiled 1 file with swc.
# var a = 1;

具體功能與 babel 類似,都可以讓瀏覽器支援先進語法或者 ts,只是 @swc/cli 比 babel 快了至少 20 倍。可以通過 .swcrc 檔案做 自定義配置

@swc/core

你可以利用 @swc/core 製作更上層的構建工具,所以它是 @swc/cli 的開發者呼叫版本。基本 API 來自官網開發者文件:

const swc = require("@swc/core");

swc
  .transform("source code", {
    // Some options cannot be specified in .swcrc
    filename: "input.js",
    sourceMaps: true,
    // Input files are treated as module by default.
    isModule: false,

    // All options below can be configured via .swcrc
    jsc: {
      parser: {
        syntax: "ecmascript",
      },
      transform: {},
    },
  })
  .then((output) => {
    output.code; // transformed code
    output.map; // source map (in string)
  });

其實就是把 cli 呼叫改成了 node 呼叫。

@swc/wasm-web

@swc/wasm-web 可以在瀏覽器執行時呼叫 wsm 版的 swc,以得到更好的效能。下面是官方的例子:

import { useEffect, useState } from "react";
import initSwc, { transformSync } from "@swc/wasm-web";

export default function App() {
  const [initialized, setInitialized] = useState(false);

  useEffect(() => {
    async function importAndRunSwcOnMount() {
      await initSwc();
      setInitialized(true);
    }
    importAndRunSwcOnMount();
  }, []);

  function compile() {
    if (!initialized) {
      return;
    }
    const result = transformSync(`console.log('hello')`, {});
    console.log(result);
  }

  return (
    <div className="App">
      <button onClick={compile}>Compile</button>
    </div>
  );
}

這個例子可以在瀏覽器執行時做類似 babel 的事情,無論是低程式碼平臺還是線上 coding 平臺都可以用它做執行時編譯。

@swc/jest

@swc/jest 提供了 Rust 版本的 jest 實現,讓 jest 跑得更快。使用方式也很簡單,首先安裝:

npm i @swc/jest

然後在 jest.config.js 配置檔案中,將 ts 檔案 compile 指向 @swc/jest 即可:

module.exports = {
  transform: {
    "^.+\\.(t|j)sx?$": ["@swc/jest"],
  },
};

swc-loader

swc-loader 是針對 webpack 的 loader 外掛,代替 babel-loader

module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /(node_modules)/,
      use: {
        // `.swcrc` can be used to configure swc
        loader: "swc-loader"
      }
    }
  ];
}

swcpack

增強了多檔案 bundle 成一個檔案的功能,基本可以認為是 swc 版本的 webpack,當然效能也會比 swc-loader 方案有進一步提升。

截至目前,該功能還在測試階段,只要安裝了 @swc/cli 就可使用,通過建立 spack.config.js 後執行 npx spack 即可執行,和 webpack 的使用方式一樣。

Deno

Deno 的 linter、code formatter、文件生成器採用 swc 構建,因此也算屬於 Rust 陣營。

Deno 是一種新的 js/ts 執行時,所以我們總喜歡與 node 進行類比。quickjs 也一樣,這三個都是一種對 js 語言的執行器,作為開發者,需求永遠是更好的效能、相容性與生態,三者幾乎缺一不可,所以當下雖然不能完全代替 Nodejs,但作為高效能替代方案是很香的,可以基於他們做一些跨端跨平臺的解析器,比如 kraken 就是基於 quickjs + flutter 實現的一種高效能 web 渲染引擎,是 web 瀏覽器的替代方案,作為一種跨端方案。

esbuild

esbuild 是較早被廣泛使用的新一代 JS 基建,是 JS 打包與壓縮工具。雖然採用 Go 編寫,但效能與 Rust 不相上下,可以與 Rust 風潮放在一起看。

esbuild 目前有兩個功能:編譯和壓縮,理論上分別可代替 babel 與 terser。

編譯功能的基本用法:

require('esbuild').transformSync('let x: number = 1', {
  loader: 'ts',
})

// 'let x = 1;\n'

壓縮功能的基本用法:

require('esbuild').transformSync('fn = obj => { return obj.x }', {
  minify: true,
})

// 'fn=n=>n.x;\n'

壓縮功能比較穩定,適合用在生產環境,而編譯功能要考慮相容 webpack 的地方太多,在成熟穩定後才考慮能在生產環境使用,目前其實已經有不少新專案已經在生產環境使用 esbuild 的編譯功能了。

編譯功能與 @swc 類似,但因為 Rust 支援編譯到 wsm,所以 @swc 提供了 web 執行時編譯能力,而 esbuild 目前還沒有看到這種特性。

Rome

Rome 是 Babel 作者做的基於 Nodejs 的前端基建全家桶,包含但不限於 Babel, ESLint, webpack, Prettier, Jest。目前 計劃使用 Rust 重構,雖然還沒有實現,但我們姑且可以把 Rome 當作 Rust 的一員。

rome 是個全家桶 API,所以你只需要 yarn add rome 就完成了所有環境準備工作。

  • rome bundle 打包專案。
  • rome compile 編譯單個檔案。
  • rome develop 除錯專案。
  • rome parse 解析檔案抽象語法樹。
  • rome analyzeDependencies 分析依賴。

Rome 還將檔案格式化與 Lint 合併為了 rome check 命令,並提供了友好 UI 終端提示

其實我並不太看好 Rome,因為它負擔太重了,測試、編譯、Lint、格式化、壓縮、打包的瑣碎事情太多,把每一塊交給社群可能會做得更好,這不現在還在重構中,牽一髮而動全身。

NAPI-RS

NAPI-RS 提供了高效能的 Rust 到 Node 的銜接層,可以將 Rust 程式碼編譯後成為 Node 可呼叫檔案。下面是官網的例子:

#[js_function(1)]
fn fibonacci(ctx: CallContext) -> Result<JsNumber> {
  let n = ctx.get::<JsNumber>(0)?.try_into()?;
  ctx.env.create_int64(fibonacci_native(n))
}

上面寫了一個斐波那契數列函式,直接呼叫了 fibonacci_native 函式實現。為了讓這個方法被 Node 呼叫,首先安裝 CLI:npm i @napi-rs/cli

由於環境比較麻煩,因此需要利用這個腳手架初始化一個工作臺,我們在裡面寫 Rust,然後再利用固定的指令碼釋出 npm 包。執行 napi new 建立一個專案,我們發現入口檔案肯定是個 js,畢竟要被 node 引用,大概長這樣(我建立了一個 myLib 包):

const { loadBinding } = require('@node-rs/helper')

/**
 * __dirname means load native addon from current dir
 * 'myLib' is the name of native addon
 * the second arguments was decided by `napi.name` field in `package.json`
 * the third arguments was decided by `name` field in `package.json`
 * `loadBinding` helper will load `myLib.[PLATFORM].node` from `__dirname` first
 * If failed to load addon, it will fallback to load from `myLib-[PLATFORM]`
 */
module.exports = loadBinding(__dirname, 'myLib', 'myLib')

所以 loadBinding 才是入口,同時專案資料夾下存在三個系統環境包,分別供不同系統環境呼叫:

  • @cool/core-darwin-x64 macOS x64 平臺。
  • @cool/core-win32-x64 Windows x64 平臺。
  • @cool/core-linux-arm64-gnu Linux aarch64 平臺。

@node-rs/helper 這個包的作用是引導 node 執行預編譯的二進位制檔案,loadBinding 函式會嘗試載入當前平臺識別的二進位制包。

src/lib.rs 的程式碼改成上面斐波那契數列的程式碼後,執行 npm run build 編譯。注意在編譯前需要安裝 rust 開發環境,只要一行指令碼即可安裝,具體看 rustup.rs。然後把當前專案整體當作 node 包釋出即可。

釋出後,就可以在 node 程式碼中引用啦:

import { fibonacci } from 'myLib'

function hello() {
  let result = fibonacci(10000)
  console.log(result)
  return result
}

NAPI-RS 作為 Rust 與 Node 的橋樑,很好的解決了 Rust 漸進式替換現有 JS 工具鏈的問題。

Rust + WebAssembly

Rust + WebAssembly 說明 Rust 具備編譯到 wsm 的能力,雖然編譯後程式碼效能會變得稍慢,但還是比 js 快很多,同時由於 wsm 的可移植性,讓 Rust 也變得可移植了。

其實 Rust 支援編譯到 WebAssembly 也不奇怪,因為本來 WebAssembly 的定位之一就是作為其他語言的目標編譯產物,然後它本身支援跨平臺,這樣它就很好的完成了傳播的使命。

WebAssembly 是一個基於棧的虛擬機器 (stack machine),所以跨平臺能力一流。

想要將 Rust 編譯為 wsm,除了安裝 Rust 開發環境外,還要安裝 wasm-pack

安裝後編譯只需執行 wasm-pack build 即可。更多用法可以檢視 API 文件

dprint

dprint 是用 rust 編寫的 js/ts 格式化工具,並提供了 dprint-node 版本,可以直接作為 node 包,通過 npm 安裝使用,從 原始碼 可以看到,使用 NAPI-RS 實現。

dprint-node 可以直接在 Node 中使用:

const dprint = require('dprint-node');
dprint.format(filePath, code, options);

引數文件

Parcel

Parcel 嚴格來說算是上一代 JS 基建,它出現在 Webpack 之後,Rust 風潮之前。不過由於它已經採用 SWC 重寫,所以姑且算是跟上了時髦。

總結

前端全家桶已經有了一整套 Rust 實現,只是對於存量專案的編譯準確性需要大量驗證,我們還需要時間等待這些庫的成熟度。

但毫無疑問的是,Rust 語言對 JS 基建支援已經較為完備了,剩下的只是工具層邏輯覆蓋率的問題,都可以隨時間而解決。而用 Rust 語言重寫後的邏輯帶來的巨幅效能提升將為社群注入巨大活力,就像原文說的,前端社群可以為了巨大效能提升而引入 Rust 語言,即便這可能導致為社群貢獻門檻的提高。

討論地址是:精讀《Rust 是 JS 基建的未來》· Issue #371 · dt-fe/weekly

如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章