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

这篇具有很好参考价值的文章主要介绍了前端Rust开发WebAssembly与Swc插件快速入门。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

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

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

  1. 开发 wasm 。

  2. 开发 swc 插件。

  3. 开发代码处理工具。

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

正文

开发 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

为了支持直接导入 .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 插件

意义

现代前端高效构建往往将 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 插件
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 速度都是很快的,秒级之争没有意义。

以上。文章来源地址https://www.toymoban.com/news/detail-522876.html

到了这里,关于前端Rust开发WebAssembly与Swc插件快速入门的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 快速配置 Rust 开发环境并编写一个小应用

    安装: curl --proto \\\'=https\\\' --tlsv1.2 -sSf https://sh.rustup.rs | sh 更新: Rust 的升级非常频繁. 如果安装 Rustup 后已有一段时间,那么很可能 Rust 版本已经过时, 运行 rustup update 获取最新版本的 Rust rustc:编译Rust程序 rustc只适合简单的Rust程序,较大型的项目还是推荐使用Cargo Cargo:Rust 的构建

    2024年02月16日
    浏览(49)
  • rust 开发入门

    要入门Rust编程,首先需要安装Rust编程环境并创建一个Hello World项目。以下是步骤: 1. 安装Rust 首先,你需要安装Rust编程环境。你可以使用 rustup ,它是Rust的官方工具,用于安装和管理Rust的不同版本。打开终端并运行以下命令: 根据提示,选择你需要的选项进行安装。 2. 验

    2024年02月05日
    浏览(51)
  • Rust 快速入门60分① 看完这篇就能写代码了

    Rust 一门赋予每个人构建可靠且高效软件能力的语言 https://hannyang.blog.csdn.net/article/details/130467813?spm=1001.2014.3001.5502 关于Rust安装等内容请参考上文链接,写完上文就在考虑写点关于Rust的入门文章,本专辑将直接从Rust基础入门内容开始讲起。标题《快速入门60分》并不指60分钟,

    2024年02月04日
    浏览(50)
  • Rust语言从入门到入坑——(2)Rust在windows上搭建开发环境

    开始搭建一个适合在windows上运行的Rust环境。 Rust支持的程序语言很多:可详见官网介绍 本文章主要是在windowns下搭建开发环境 首先,需要安装最新版的 Rust 编译工具和 Visual Studio Code。 Rust 编译工具:https://www.rust-lang.org/zh-CN/tools/install Visual Studio Code:https://code.visualstudio.com

    2024年02月09日
    浏览(45)
  • 给 Web 前端工程师看的用 Rust 开发 wasm 组件实战

    wasm 全称 WebAssembly,是通过虚拟机的方式,可以在服务端、客户端如浏览器等环境执行的二进制程序。他有速度快、效率高、可移植的特点。 对我们 Web 前端工程最大的好处就是可以在浏览器端使用二进制程序处理一些计算量大的处理,使用他比 javascript 快的特点优化性能。

    2024年02月05日
    浏览(41)
  • 性能的极致,Rust的加持,Zed-Dev编辑器快速搭建Python3.10开发环境

    快就一个字,甚至比以快著称于世的Sublime 4编辑器都快,这就是Zed.dev编辑器。其底层由 Rust 编写,比基于Electron技术微软开源的编辑器VSCode快一倍有余,性能上无出其右,同时支持多人编辑代码。 Zed.dev编辑器还在灰度测试阶段,暂时只释出了Mac版本,在Zed.dev官网下载,安装

    2024年01月20日
    浏览(51)
  • Rust入门(十四):不安全Rust

    Rust 可以不强制执行内存安全保证,这被称为 不安全 Rust ( unsafe Rust ),这类代码会提供额外的超能力。 可以通过 unsafe 来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块,有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作: 解引用裸指针 调

    2024年02月11日
    浏览(31)
  • 【rust/入门】windows安装rust gnu环境(折腾)

    首先说明,我是rust入门选手,之前都是在wsl写rust,突然想在windows下装下rust。 windows版本:windows11 22H2 原文 换源 看到教程我陷入了沉默, (官方推荐) 打开 Microsoft C++ Build Tools 我开始不解,这是让我安装vs studio? 我装个rust你让我装几个G的环境? 再见,告辞!我是菜鸡! 等等

    2024年02月13日
    浏览(42)
  • Rust语言从入门到入坑——(5)Rust 所有权

    主要介绍Rust所有权的知识,涉及到变量的作用域,内存释放机制,移动,克隆,引用等知识,很多知识是Rust语言特有机制。 所有权有以下三条规则: - Rust 中的每个值都有一个变量,称为其所有者。 - 一次只能有一个所有者。 - 当所有者不在程序运行范围时,该值将被删除

    2024年02月10日
    浏览(37)
  • Rust编程语言入门之Rust的面向对象编程特性

    Rust 受到多种编程范式的影响,包括面向对象 面向对象通常包含以下特性:命名对象、封装、继承 “设计模式四人帮”在《设计模型》中给面向对象的定义: 面向对象的程序由对象组成 对象包装了数据和操作这些数据的过程,这些过程通常被称作方法或操作 基于此定义:

    2023年04月21日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包