Rust Trip 系列
Coding

Leptos中用wasm-bindgen导入npm的WebComponent框架并使用

本文主要介绍了如何在Leptos中使用wasm-bindgen导入npm的WebComponent框架并使用

Hako Chest
更新于
6532 字
10 min read
Leptos中用wasm-bindgen导入npm的WebComponent框架并使用

其实最主要就是方便用wasm-bindgen将npm的WebComponent框架引入到rust中,然后在Leptos中使用,这样就可以在rust全栈中使用npm的UI框架了

大体思路

大致是一个收束中间层的思路

问题动机

  • JS积累发展起来的web开发生态是非常庞大的,相比之下rust的web开发生态就很不成熟了(例如web前端框架与UI框架等等,以Leptos为例,笔者此时也只使用过ThawUI、Leptonic以及Leptix等框架,但是无论是内容完成度、美观程度以及开发体验,都无法满足笔者需求)
    • 解决方案即使通过wasm-bingen将JS的UI框架引入到rust中,但是像Vue、React这些的UI框架就完全没必要了,一是直接编译成wasm导入会有很大的性能开销,二是有点脱裤子放屁的感觉,不如直接用Vue来写了,Tauri对Vue的支持还是很好的
  • 但是想用Rust全栈来进行这个开发,又不想重复造轮子,所以也不得不将JS的UI框架引入到rust中
    • WebComponent的UI框架就是一个非常好的解决方案,因为它基于自定义HTML标签,在业务开发中不需要向其他UI(如Antd等)在单页面里面额外去写js代码(例如vue的script部分),大部分框架是基于原生JS\TS,这样就可以轻松将JS的UI框架优雅地引入到rust中
    • 然后笔者和同学一同逐一鉴赏了OpenWC文档中推荐的各类WCUI框架,最终选型定位SiemensIE的UI,因为他的完成度和设计感都非常对我们胃口,并且对Webcomponents支持的很好
  • 但是这个UI只提供了npm i这种导入方法,需要借助vite生态才能运行,尝试直接在snippets里导入node_modules,结果wasm无法直接将node_modules内容放入snippets中进行导入,控制台出现了灾难性的MIMO报错,去npm官网查看后发现这个UI也没有提供esm.js这样的单文件
    • 所以只能手动进行打包,尝试了swc、vite之后,这里最终选择使用rollup进行打包配置
    • 运气很好,打包成功,并且导入成功,也但是发现很多类都没有export,需要去手动搜索修改esm.js之中的类型定义部分,然后才能顺利进行#[wasm_bindgen(module = "/web/src/bundle.esm.js")]注册
    • 最后通过bindgen_hub()在main.rs里全局注册,就可以在rust框架中使用这个UI框架了

四层模型

  1. JS层:npm下载下来的nodejs工具类或UI框架等
  2. ESM层:npm包单文件打包后的esm.js文件
  3. Bindgen层:绑定注册esm.js的类型、构造与方法到rust中
  4. Rust层:leptos等rust的web框架

如果直接就有esm.js文件,那么就可以从第二层开始进行第三层的绑定注册,省去了npm包的esm单包编译过程,像echarts框架就是直接提供了esm.js文件的

NPM包打包为ESM单文件

新建项目(如果需要)

cd leptos_with_siemens_demo
cargo init

mkdir web

Cargo.toml是这样的

[dependencies]
leptos = {version="0.6.12",features=["csr"]}

web文件夹里面的package.json是这样的

{
  "dependencies": {
    "@siemens/ix": "^2.4.0",
    "@siemens/ix-icons": "^2.2.0"
  },
  "devDependencies": {
    "@swc/cli": "^0.4.0",
    "@swc/core": "^1.6.7"
  }
}

安装待打包依赖

按照UI官网的安装教程,首先通过NPM安装

npm install @siemens/ix @siemens/ix-icons

官网提供的使用方法是在main.js里面写这个,然后在index.html里面用<script type="module" src="./main.js"></script>引入,完成组件初始化,但是这种方法只适用于vite等js的web项目

// main.js
import "@siemens/ix/dist/siemens-ix/siemens-ix.css";
import { defineCustomElements } from "@siemens/ix/loader";
import { defineCustomElements as defineIxIconCustomElement } from "@siemens/ix-icons/loader";

(async () => {
  defineIxIconCustomElement();
  defineCustomElements();
})();

配置JS加载器

// main.js
import "@siemens/ix/dist/siemens-ix/siemens-ix.css";
import { defineCustomElements } from "@siemens/ix/loader";
import { defineCustomElements as defineIxIconCustomElement } from "@siemens/ix-icons/loader";

// 把下面装到一个暴露类里面即可
export class BindSiemens {
  constructor() {
    (async () => {
      defineIxIconCustomElement();
      defineIxCustomElements();
    })();
  }
}

rollup配置与打包

安装所需要的rollup依赖

npm install @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-eslint @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-typescript rollup rollup-plugin-json rollup-plugin-node-resolve rollup-plugin-postcss rollup-plugin-terser

在根目录下新建rollup.config.js文件,配置如下

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import { terser } from 'rollup-plugin-terser';
import { babel } from '@rollup/plugin-babel'
import postcss from 'rollup-plugin-postcss';

export default {
  input: 'src/index.js',
  output: {
    dir: 'dist',
    format: 'esm',
    sourcemap: true,
    entryFileNames: 'bundle.esm.js',
    chunkFileNames: '[name]-[hash].js',
    inlineDynamicImports: true,
  },
  plugins: [
    json(),
    commonjs({
      include: /node_modules/,
    }),
    resolve({
      preferBuiltins: true,
      jsnext: true,
      main: true,
      browser: true,
    }),
    babel({ exclude: "node_modules/**" }),
    // terser(),   //这里把压缩代码的选项注释掉了,因为后期还要编辑esm.js,所以需要保留可读性
    postcss({
      extract: true,
    }),// 这个是因为要打包css内容,所以要加上
  ],
};

运行如下语言进行打包
```bash
npx rollup -c rollup.config.js

在esm.js中注入加载器暴露类

问题出现在打包完成后的esm.js里面的类是没有export的,所以需要我们手动去更改打包后的文件

你去搜索BindSiemens,可以找到这一段代码

// bundle.esm.js

// 上略
class BindSiemens {
  constructor() {
    (async () => {
      defineCustomElements$1();
      defineCustomElements$2();
    })();
  }
}
// 下略

为这个class在前面手动加上export即可

// bundle.esm.js

// 上略
export class BindSiemens {
  constructor() {
    (async () => {
      defineCustomElements$1();
      defineCustomElements$2();
    })();
  }
}
// 下略

此外,关于其他的方法类的引入与使用,也都需要提前去这里进行export暴露,以ThemeSwitcher为例

  1. 搜索类型定义
  2. 添加export

搜索到4147行的class ThemeSwitcher定义,在前面加上export即可

wasm-bindgen引入esm.js包

cluster模式

这个模式是个人命名的,意为团簇模式,即将esm.js包的内容通过一个cluster模块,在leptos框架中进行统一的导入(不过也可能会和集群模式有一些冲突,应该不是一个好的命名);

也就是 npm包->esm.js->cluster->leptos,这样一个流程,cluster模块是专门对接esm包来给rust框架注册js内容的

引入暴露类型、构造与方法

// bindgen.rs
use leptos::*;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(module = "/web/src/bundle.esm.js")]
extern "C" {
    pub type BindSiemens;

    #[wasm_bindgen(constructor)]
    pub fn new() -> BindSiemens;

// 这一部分是用来引入toggleMode方法来切换主题的
    pub type ThemeSwitcher;

    #[wasm_bindgen(constructor)]
    pub fn new() -> ThemeSwitcher;

    #[wasm_bindgen(method)]
    pub fn toggleMode(this: &ThemeSwitcher);
}

pub fn bindgen_hub(){
    BindSiemens::new();
}

配置trunk的css导入

在rust的web项目中,css文件的直接引入是需要通过trunk的方式的,snippets里面放置的css也会无法读取然后出现MIME报错,而直接原生link引入的话也会出现路线错误导致MIME报错

trunk官网还是提供了css文件在index.html中的全局引入方法的,也就是在link标签中加入data-trunk标注

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link data-trunk rel="css" href="./web/src/bundle.esm.css" />
    <title>Ciallo</title>
  </head>
  <body></body>
</html>

组件使用

bindgen_hub()全局注册

// main.rs

mod cluster;
use cluster::bindgen::bindgen_hub;
use leptos::*;

#[component]
fn App() -> impl IntoView {
    view! {
            <h1>Ciallo~</h1>
        }
}

fn main() {
    bindgen_hub();
    mount_to_body(|| view! { <App /> });
}

也就是最开始引入cluster模块以及bindgen_hub方法,然后再在main里面的mount_to_body之前调用bindgen_hub()即可

然后就可以直接在view!宏里直接使用WebComponent框架的标签了

基本组件

直接对着官网文档硬堆组件即可,例如

mod cluster;

#[warn(unused_imports)]
use cluster::bindgen::{bindgen_hub, ThemeSwitcher};
use leptos::*;

fn toggle_theme() ->{
  let themeSwitcher = ThemeSwitcher::new();
  themeSwitcher.toggleTheme();
}

#[component]
fn App() -> impl IntoView {
  view! {
          <ix-button class="m-1" variant="primary">Button</ix-button>
          <ix-button class="m-1" variant="primary" disabled>Button</ix-button>
          <ix-button id="toggle-mode" on:click=move|_|{toggle_theme}>Toggle mode</ix-button>
          <ix-datetime-picker></ix-datetime-picker>
        }
}

fn main() {
    bindgen_hub();
    mount_to_body(|| view! { <App /> });
}

这里就是一个可以切换明暗主题,以及拥有强大的日期选择器的组件页面了

小结

不过实际上写起来感觉还是挺费劲的,后面写的那个Osynic项目最终还是投入了PrimeVue的怀抱,毕竟tauri对vue的支持还是很可以的

#Rust #Web #WASM #WebComponent