Leptos中用wasm-bindgen导入npm的WebComponent框架并使用
本文主要介绍了如何在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框架了
四层模型
- JS层:npm下载下来的nodejs工具类或UI框架等
- ESM层:npm包单文件打包后的esm.js文件
- Bindgen层:绑定注册esm.js的类型、构造与方法到rust中
- 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为例
- 搜索类型定义
- 添加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的支持还是很可以的