前端Rust开发WebAssembly与Swc插件快速入门

news/2024/12/23 5:22:26 标签: Rust, wasm, swc, 前端, webassembly

前言

现代前端对速度的追求已经进入二进制工具时代,Rust 开发成为每个人的必修课。

一般我们将常见的前端 Rust 开发分为以下几类,难度由上至下递增:

  1. 开发 wasm

  2. 开发 swc 插件。

  3. 开发代码处理工具。

我们将默认读者具备最简单的 Rust 知识,进行快速入门介绍。

正文

wasm_16">开发 wasm

意义

开发 wasm 的意义在于利用浏览器运行 wasm 的优势,在 wasm 中进行大量复杂的计算、音视频、图像处理等,当你有此类需求,可以优先考虑使用 Rust 开发 wasm 分发至浏览器。

初始化

我们使用 wasm-pack 构建 wasm ,参考 wasm-pack > Quickstart 得到一个模板起始项目。

实战 case

使用 tsify 支持输出结构体的 TypeScript 类型,实现一个简单的加法运算:

# Cargo.toml 确保你含有这些依赖
[dependencies]
serde = { version = "1.0.163", features = ["derive"] }
tsify = "0.4.5"
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use tsify::Tsify;

#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Rect {
    pub width: u32,
    pub height: u32
}

#[wasm_bindgen]
pub fn plus(mut rect: Rect) -> Rect {
    rect.width += rect.height;
    rect
}

构建

  # dev
  wasm-pack build --verbose --out-dir pkg --out-name index --dev
  # release
  wasm-pack build --verbose --out-dir pkg --out-name index --release

这将在当前目录的 pkg/* 下生成 wasm 产物与 index.js 等胶水代码,导入 index.js 便即开即用,非常方便。

wasm_67">运行 wasm

为了支持直接导入 .wasm 文件,我们需要 webpack 5 的 asyncWebAssembly 特性支持,此处以在 Umi 4 项目中调试为例,创建一个 Umi 4 Simple 模板项目:

  pnpm create umi wasm-demo

参考 FAQ > 怎么用 WebAssembly 配置开启 wasm 支持:

// .umirc.ts
 
export default {
  chainWebpack(config) {
    config.set('experiments', {
      ...config.get('experiments'),
      asyncWebAssembly: true
    })
 
    const REG = /\.wasm$/
 
    config.module.rule('asset').exclude.add(REG).end();
 
    config.module
      .rule('wasm')
      .test(REG)
      .exclude.add(/node_modules/)
      .end()
      .type('webassembly/async')
      .end()
  },
}

之后便可在项目中直接导入刚刚打包好,在 pkg/*wasm 即开即用:

import * as wasm from './path/to/pkg'
const ret = wasm.plus({
  width: 1,
  height: 2,
})
// { width: 3, height: 2 }
console.log('ret: ', ret);

注:

  1. 由于 wasm 文件可能较大,当你需要优化时,可将使用 .wasm 的组件手动 React.lazy(() => import('./Component')) 拆包,之后在 useEffect 中懒加载 await import('./path/to/pkg')

  2. 对于非 Umi 4 的 webpack 5 项目,请自行开启 experiments.asyncWebAssembly 即可一键支持 wasm 导入。

缺点

由于当下浏览器和 PC 设备性能已足够强大,更多场合下,运行 wasm 进行数据计算,传递数据花费的时间将 远远超出使用 JavaScript 进行同逻辑计算的时间

所以除音视频场景外,你很可能不需要 wasm ,而是优先考虑使用 Worker 等优化策略。

swc__126">开发 swc 插件

意义

现代前端高效构建往往将 babel 替代为 swc 化,为了替代 babel 插件实现代码转换,开发 swc 插件成为了一门必修课。

初始化

参考 swc > Create a project ,我们用 swc 脚手架初始化得到一个插件的模板起始项目。

实战 case

我们编写一个最简单的功能,将所有的 react 导入转换为 preact

# Cargo.toml 确保你含有这些依赖
[dependencies]
serde = "1.0.163"
serde_json = "1.0.96"
swc_core = { version = "0.76.39", features = ["ecma_plugin_transform"] }
use swc_core::ecma::{
    ast::{Program, ImportDecl, ImportSpecifier},
    visit::{as_folder, FoldWith, VisitMut, VisitMutWith},
};
use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
pub struct TransformPluginConfig {
    from: String,
    to: String,
}

pub struct TransformVisitor {
    config: TransformPluginConfig,
}

impl VisitMut for TransformVisitor {
    fn visit_mut_import_decl(&mut self, n: &mut ImportDecl) {
        n.visit_mut_children_with(self);

        if n.specifiers.len() == 1 {
            if let ImportSpecifier::Default(_) = n.specifiers[0] {
                if n.src.value == self.config.from {
                    n.src = Box::new(self.config.to.clone().into());
                }
            }
        }
    }
}

#[plugin_transform]
pub fn process_transform(program: Program, metadata: TransformPluginProgramMetadata) -> Program {
    let config = serde_json::from_str(
        &metadata
            .get_transform_plugin_config()
            .unwrap()
    )
    .expect("invalid config");

    program.fold_with(&mut as_folder(TransformVisitor { config }))
}

在编写过程中,以下文档可供参考:

  • Swc :Implementing a plugin

  • Swc :Rust docs

构建

  # dev
  cargo build --target wasm32-wasi
  # release
  cargo build --target wasm32-wasi --release

通过构建,你可以在当前目录下得到 wasm 形式的 swc 插件产物。

swc__210">运行 swc 插件

import { transformSync } from '@swc/core'

const transform = async () => {
  const { code } = transformSync(
    `
import React from 'react'
  `,
    {
      jsc: {
        experimental: {
          plugins: [
            [
              require.resolve(
                './target/wasm32-wasi/debug/my_first_plugin.wasm'
              ),
              {
                from: 'react',
                to: 'preact',
              },
            ],
          ],
        },
        parser: {
          syntax: 'typescript',
          dynamicImport: true,
          tsx: true,
        },
        target: 'es2015',
        minify: {
          compress: false,
          mangle: false,
        },
        transform: {
          react: {
            runtime: 'automatic',
            throwIfNamespace: true,
            development: true,
            useBuiltins: true,
          },
        },
      },
      module: {
        type: 'es6',
        ignoreDynamic: true,
      },
      minify: false,
      isModule: true,
      sourceMaps: false,
      filename: 'index.tsx',
    }
  )
  // import React from 'preact'
  console.log('code: ', code)
}

transform()

缺点

为了避免多平台差异,我们分发了 wasm32-wasi 目标的 wasm 包,好处是只需构建一次即可全平台通用,缺点是产物较大,同时 wasm 运行速度不如 .node ;但现代前端已无需担心只在本地编译阶段使用的包大小,如 Nextjs 单包依赖已达 40 M 以上,TypeScript 20 M ,你可以无需关心产物体积问题。

至此,我们介绍了如何借助 swc 插件实现 babel 插件的替代,在下文中,我们将继续深入,真正构建多平台分发的二进制包,同时不会做过多细节介绍,推荐只学习到此处为止

开发代码处理工具

意义

目前最主流的前端 Rust 开发即是借助 Swc 来解析 JavaScript 、TypeScript 代码,从而实现代码信息提取、转换、编译等,我们会将 Rust 编译为 Node addon .node 文件,以获得远比 wasm 更快的运行速度。

初始化

使用 napi-rs 构建 ,参考 napi > Create project 得到一个模板起始项目。

实战 case

此处同样我们实现一个:将所有 react 导入转换为 preact 的需求,所需要的依赖与模板代码如下:

# Cargo.toml 确保你含有这些依赖
[dependencies]
napi = { version = "2.12.2", default-features = false, features = ["napi4", "error_anyhow"] }
napi-derive = "2.12.2"
swc_common = { version = "0.31.12", features = ["sourcemap"] }
swc_ecmascript = { version = "0.228.27", features = ["parser", "visit", "codegen"] }
#[macro_use]
extern crate napi_derive;

use std::path::{Path, PathBuf};
use std::str;

use swc_common::comments::SingleThreadedComments;
use swc_common::{sync::Lrc, FileName, Globals, SourceMap};
use swc_ecmascript::ast;
use swc_ecmascript::codegen::text_writer::JsWriter;
use swc_ecmascript::parser::lexer::Lexer;
use swc_ecmascript::parser::{EsConfig, Parser, StringInput, Syntax, TsConfig};
use swc_ecmascript::visit::{VisitMut, VisitMutWith};


#[napi]
pub fn transform(code: String, options: ImportChange) -> String {
  let is_jsx = true;
  let is_typescript = true;
  let filename_path_buf = PathBuf::from("filename.tsx");

  let syntax = if is_typescript {
    Syntax::Typescript(TsConfig {
      tsx: is_jsx,
      decorators: true,
      ..Default::default()
    })
  } else {
    Syntax::Es(EsConfig {
      jsx: is_jsx,
      export_default_from: true,
      ..Default::default()
    })
  };

  let source_map = Lrc::new(SourceMap::default());
  let source_file = source_map.new_source_file(
    FileName::Real(filename_path_buf.clone()),
    code.clone().into(),
  );
  let comments = SingleThreadedComments::default();

  let lexer = Lexer::new(
    syntax,
    Default::default(),
    StringInput::from(&*source_file),
    Some(&comments),
  );

  let mut parser = Parser::new_from(lexer);
  let mut module = parser.parse_module().expect("failed to parse module");

  swc_common::GLOBALS.set(&Globals::new(), || {
    let mut visitor = options;
    module.visit_mut_with(&mut visitor);
  });

  let (code, _map) = emit_source_code(
    Lrc::clone(&source_map),
    Some(comments),
    &module,
    None,
    false,
  )
  .unwrap();

  code
}

#[napi(object)]
pub struct ImportChange {
  pub from: String,
  pub to: String,
}

impl ImportChange {
  pub fn new(from: String, to: String) -> Self {
    Self { from, to }
  }
}

impl VisitMut for ImportChange {
  fn visit_mut_module_decl(&mut self, decl: &mut ast::ModuleDecl) {
    if let ast::ModuleDecl::Import(import_decl) = decl {
      if import_decl.src.value == self.from {
        import_decl.src = Box::new(self.to.clone().into());
      }
    }
  }
}

pub fn emit_source_code(
  source_map: Lrc<SourceMap>,
  comments: Option<SingleThreadedComments>,
  program: &ast::Module,
  root_dir: Option<&Path>,
  source_maps: bool,
) -> Result<(String, Option<String>), napi::Error> {
  let mut src_map_buf = Vec::new();
  let mut buf = Vec::new();
  {
    let writer = Box::new(JsWriter::new(
      Lrc::clone(&source_map),
      "\n",
      &mut buf,
      if source_maps {
        Some(&mut src_map_buf)
      } else {
        None
      },
    ));
    let config = swc_ecmascript::codegen::Config {
      minify: false,
      target: ast::EsVersion::latest(),
      ascii_only: false,
      omit_last_semi: false,
    };
    let mut emitter = swc_ecmascript::codegen::Emitter {
      cfg: config,
      comments: Some(&comments),
      cm: Lrc::clone(&source_map),
      wr: writer,
    };
    emitter.emit_module(program)?;
  }

  let mut map_buf = vec![];
  let emit_source_maps = if source_maps {
    let mut s = source_map.build_source_map(&src_map_buf);
    if let Some(root_dir) = root_dir {
      s.set_source_root(Some(root_dir.to_str().unwrap()));
    }
    s.to_writer(&mut map_buf).is_ok()
  } else {
    false
  };
  if emit_source_maps {
    Ok((
      unsafe { str::from_utf8_unchecked(&buf).to_string() },
      unsafe { Some(str::from_utf8_unchecked(&map_buf).to_string()) },
    ))
  } else {
    Ok((unsafe { str::from_utf8_unchecked(&buf).to_string() }, None))
  }
}

构建

  # dev
  napi build --platform
  # release
  napi build --release

一般情况下,我们通常会分发至以下 9 个平台:

  "napi": {
    "triples": {
      "defaults": false,
      "additional": [
        "x86_64-apple-darwin",
        "aarch64-apple-darwin",
        "x86_64-pc-windows-msvc",
        "aarch64-pc-windows-msvc",
        "x86_64-unknown-linux-gnu",
        "aarch64-unknown-linux-gnu",
        "x86_64-unknown-linux-musl",
        "aarch64-unknown-linux-musl",
        "armv7-unknown-linux-gnueabihf"
      ]
    }
  }

通常本地只能编译自己平台的 .node 二进制文件,所以需要依赖 Github Actions CI 等云环境进行多平台的构建,并且在 CICD 中构造好 npm 包使用 Npm Token 发布,此部分内容往往是大量的模板代码与调试过程,请自行研究学习。

运行二进制包

import { transform } from './index'

console.log(
  // import React from 'preact'
  transform(
    `import React from 'react'`,
    { from: 'react', to: 'preact' }
  )
)

直接导入 napi 生成的 index.js 胶水代码即可使用 .node 二进制包。

缺点

  1. 通过构建 .node 分发至不同平台是目前最高运行效率、最小下载体积的方法,但相对应需要手动管理所有 Rust 代码,且多平台构建也强依赖云环境,这提出了一些较高的要求。

  2. 随着对 napi / Rust 异步、并发编程 / Swc 的理解精进,你可以写出更高运行效率的代码,得到更快的执行速度。但最简单的代码依然够用,因为现代计算机性能已经足够快,1s 还是 10s 的争论没有意义。

  3. 在开发过程中,你可能会遇到各种 Rust 构建相关的问题,请自行研究并解决。

总结

本文对 Rust 浅尝辄止,如希望更有所作为,你可以通过不断精进 Rust ,组织出更优雅的代码结构,实现更高的执行效率。

前端 AST 人尽皆知,如同开发 Babel 插件一样,开发 Rust Swc 插件已然成为现代前端的必修课,文本推荐只入门至 Swc 插件为止,已经能应对绝大多数场景。

另外,对于性能上无需过多追求,由于计算机的性能已经过剩,不管是 wasm 还是 .node 速度都是很快的,秒级之争没有意义。

以上。


http://www.niftyadmin.cn/n/393779.html

相关文章

如何使用Java异常处理来优雅地处理各种异常情况?

在Java编程中&#xff0c;异常处理是一个非常重要的话题。良好的异常处理可以帮助我们更好地调试和排除代码中的错误&#xff0c;同时也可以提高代码的可读性、可维护性和稳定性。本文将详细介绍如何使用Java异常处理来优雅地处理各种异常情况。 异常分类 在Java中&#xff0…

chatgpt赋能python:**Python取余符号:了解%运算符的作用和用法**

Python取余符号&#xff1a;了解%运算符的作用和用法 作为一名有着10年编程经验的工程师&#xff0c;我对Python这门编程语言深有了解。在本文中&#xff0c;我将详细介绍Python的取余符号&#xff0c;即%运算符&#xff0c;其作用和用法。 什么是Python取余符号&#xff…

Java面向对象程序开发——基础

文章目录 前言类和对象类对象 构造方法匿名对象变量作用域this关键字总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; Java 是一种以面向对象编程为基础的编程语言 在 Java 编程中&#xff0c;一切皆为对象 每个对象都有其自己的属性和方法 对象可以…

Delphi11的多线程ⓞ,附送图片处理代码

Delphi11的多线程ⓞ OLD Coder , 习惯使用Pascal 接下来准备启用多线程&#xff0c;毕竟硬件多核&#xff0c;Timer不太爽了&#xff08;曾经的桌面&#xff0c;都是Timer——理解为“片”&#xff09; 突然想写写&#xff0c;不知道还有多少D兄弟们在。 从源码开始 用D11之…

代码随想录算法训练营第四十六天|139.单词拆分、关于多重背包,你该了解这些!、背包问题总结篇!

文章目录 一、139.单词拆分二、关于多重背包&#xff0c;你该了解这些&#xff01;三、背包问题总结篇&#xff01;总结 一、139.单词拆分 public boolean wordBreak(String s, List<String> wordDict) {//完全背包问题&#xff0c;因为可以重复&#xff0c;背包正序排列…

图文并茂教你快速入门React系列04-状态管理

在React中&#xff0c;什么是状态&#xff1f; 响应式 使用 React&#xff0c;你不用直接从代码层面修改 UI。举个栗子哇&#xff0c;不用编写诸如“禁用按钮”、“启用按钮”、“显示成功消息”等命令。相反&#xff0c;你只需要描述组件在不同状态&#xff08;“初始状态”…

Elasticsearch 和 Kibana 的实时大数据分析系统

Elasticsearch 和 Kibana 的实时大数据分析系统 一、简介1. 定义及特点2. 基本功能3. 数据索引与查询 二、Kibana 简介1. 定义及特点2. 基本功能与架构3. Kibana 可视化交互性 三、Elasticsearch 和 Kibana 的集成1. 集成意义2. 集成方法2.1 安装 Elasticsearch2.2 安装 Kibana…

web前端 --- BOM编程、DOM编程

BOM编程&#xff08;browser object model -- 浏览器对象模型&#xff09; BOM给JavaScript提供用来操作浏览器的若干的"方法" 操作 在 js 看来&#xff0c;一个完整的浏览器包含如下组件&#xff1a; window窗口 // 整个浏览器的窗口 |-- history …