所有耀眼的成绩,都需要苦熬,熬得过,出众;熬不过,出局

大家好,我是柒八九。一个专注于前端开发技术/RustAI应用知识分享Coder

此篇文章所涉及到的技术有

  1. Rust
  2. wasm-bindgen/js-sys/web-sys
  3. Web Worker
  4. WebAssembly
  5. Webpack/Vite配置WebAssembly
  6. OffscreenCanvas
  7. 脚手架生成项目(npx f_cli_f create xxx
  8. tailwindcss
  9. MuPDF.js/mammoth.js

因为,行文字数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请大家酌情观看。


前言

在前一篇文章写一个类ChatGPT应用,前后端数据交互有哪几种我们介绍了,如果要进行一个类ChatGPT应用的开发,可能会用到的前后端数据交互的方式。同时呢,我们也介绍了最近公司所做的项目,做一款基于文档类问答的AI功能。

而谈到文档相关的应用,从操作文档角度来看,无非就是文件上传文件解析文件展示。而我们之前在文件上传 = 拖拽 + 多文件 + 文件夹介绍过更优雅的上传方式。而文件展示如果大家想了解的话,我们可以单独写一篇文章。

而我们今天来聊聊关于文件解析的相关操作。

业务背景

大家肯定用过很多云盘类的应用。在我们对本地文件进行上传后,在展示的时候一般分为两种模式

  1. 列表模式
    列表模式
  2. 大图模式
    大图模式

如果大家观察过云盘针对大图模式的文件资源的展示,就会发现每个文件的头图都是用一个<img/>接收了一个从后端返回的固定图片资源。

而现在,我们针对大图模式有几点改进

  1. 要求该图片能显示文件资料的概要内容(这块可以借助AI对文本进行Summary处理,这个我们后面会单独写一篇文章),而不是单单的把文件的首页信息(pdf/word/pptx)转换成图片(像阿里云盘一样)
  2. 要求前端在上传过程中,就需要显示文件的概要信息,而不是走接口从服务器获取,也就是这是一个纯前端的事情
  3. 还需要在图片的标识文件的类型,例如展示pdf/word/ppt等的图标

为什么做呢,有没有发现我们通过上述的改造和处理,我们直接在大图模式下,通过文件头图信息就能大致知晓文件的内容(概要信息),其次如果展示的资源信息过多,每次从后端获取对应的图片资源也是一件极其耗费带宽的事情。

前端糅合其他语言

讲到这里,大家可能会疑惑,你上面说了那么多,那么这和Rust有啥关系?

关系大着呢,从上面的需求点出发,我们可以看出,其实针对文档解析的处理,都是在前端环境中操作的。同时,针对大体积的文件资源,对其解析处理是一件极其耗时的事情。有时针对特殊文件,可能前端还暂时无法处理。

既然,我们想要在前端执行这些耗时且不易处理的任务,我们就需要请帮手,而在其他语言中有成熟的方案来处理我们遇到的这些问题。(由于种种原因,其他端的小伙伴无瑕处理这种情况)

那么,我们就可以选择一种方式,在前端环境中通过某种方式来糅合其他语言的操作来执行对应的任务。那思来想去,WebAssembly是再合适不过的方式了。如果不了解它,可以看我们之前的文章 - 浏览器第四种语言-WebAssembly

当然,其他语言(C/TypeScript)都可以通过编译工具转化成WebAssembly,此片文章中也会涉及,只不过我们是直接使用别人构建好的WebAssembly,而现行阶段,Rust是对WebAssembly最友好的语言。并且,我们也会用Rust手搓一个WebAssembly。这也是为什么这篇文章的主标题叫Rust赋能前端而不是WebAssembly赋能前端(我们在本文的第三部分,Word 解析中详细介绍了用RustWebAssembly,如果不想看mupdf的可以直接跳到第三节)

好了,天不早了,干点正事哇。

我们能所学到的知识点

  1. 服务配置&项目配置
  2. PDF 解析
  3. Word 解析

1. 服务配置&项目配置

由于,WebAssembly是一个新兴技术,在一些常规的打包工具(vite/webpack)中使用,我们需要额外处理。

使用WebAssembly从来源大致可以两类

  1. npm包/公司私包(针对如何发私包可以参考之前的如何在gitlab上发布npm包
  2. 直接在项目目录中使用已经构建好的wasm

这两种情况我们接下来都会涉及。其实他们的处理方式都是一样的。下面我们就来讲讲Webpack/Vite是如何配置它们的。

Webpack

针对Webpack中使用WebAssembly,我们之前在Rust 编译为 WebAssembly 在前端项目中使用就介绍过。

其实,最关键的点就是需要wasm-pack-plugin

其次,我们还想让WebAssembly模块能够和其他ESM一样,通过import进行方法的导入处理,针对Webapck5我们还可以通过配置experimentsasyncWebAssemblytrue来启动该项功能。

最后,为了兼容性,我们处理TextEncoder/TextDecoder

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'index.js',
    },
    plugins: [
         new HtmlWebpackPlugin({
            template: 'index.html'
        }),
        new WasmPackPlugin({
            crateDirectory: path.resolve(__dirname, ".")
        }),
        // 让这个示例在不包含`TextEncoder`或`TextDecoder`的Edge浏览器中正常工作。
        new webpack.ProvidePlugin({
          TextDecoder: ['text-encoding', 'TextDecoder'],
          TextEncoder: ['text-encoding', 'TextEncoder']
        })
    ],
    mode: 'development',
    experiments: {
        asyncWebAssembly: true
   }
};

Vite

Vite官网看,它只兼容了引入预编译的.wasm,但是对 WebAssemblyES 模块集成提案 尚未支持。而恰巧,我们今天所涉及到的.wasm都是ESM格式的。

按照官网的提示,我们可以借助vite-plugin-wasm的帮助。

配置也很简单,按照下面的处理即可。

import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";

export default defineConfig({
  plugins: [
    wasm(),
    topLevelAwait()
  ],
  worker: {
    plugins: [
      wasm(),
      topLevelAwait()
    ]
  }
});

项目配置

由于,我们公司的打包工具是Vite,还记得我们之前介绍过的脚手架工具吗。

大家可以在自己电脑中执行,npx f_cli_f create file_to_img来构建一个以Vite为打包工具的前端项目。

然后,我们就可以将上面逻辑写到对应的文件中。

执行到这里,我们的前期的配置工作就算完成了。

如果使用过我们的f_cli_f的人,会知道。我们在项目中内置了很多东西,可以算是开箱即用。

所以,我们保留之前的结构的基础上,在pages中新建一个FileToImg的目录结构,并且将其放置于main路由下。

最后的页面结构如下

  • 左侧的待处理文件类型我们提供了针对pdf/word/text的常规文件的解析
  • 附件上传就是使用最原始的<input type="file"/>
  • 搜索区块的话,是针对PDF的内容检索
  • 右侧的格式输出,可以切换文件的输出格式,有Text/Png/Svg/Html等格式

2. PDF 解析(mupdf + web worker)

所用技术

  1. Mupdf
  2. WebAssembly
  3. Web worker

mupdf

在前端进行pdf展示和解析我们还有很多其他的选择,例如pdf-dist,我们这里就不做各个库的区分。

在解析PDF时,我们选择mupdf,它是一套用C编写的工具库,同时还支持在多种语言环境(java/.net/js/python)中使用。其实,mupdf不仅支持对pdf的解析,然后还支持分割/构建等。具体的功能点可以参考对应的官网。我们这里只关心它的解析功能。

并且,该库还支持对多种文件格式进行处理。不仅是pdf还有我们常见的TXT/Image等。

对于我们前端而言,MuPdf提供了适配Node/JS/TS的包 - mupdf.js

也就是说,我们借助Mupdf可以实现在前端环境利用其他语言处理文档的能力。这是通过Emscripten实现将C编译为可以在js环境执行的WebAssembly。关于如何将C代码编译为WebAssembly我们之前在WebAssembly-C与JS互相操作

同时,我们还可以基于mupdf.js源码来实现符合自己团队的WebAssembly,因为mupdf中的有些特性,我们可以不要,而从选择个别的api来将其打包成.wasm

然后我们可以将打包好的文件(也可以直接使用官方提供的)按照下面的步骤,按照到我们项目中。

mupdf 常用的api

我们可以从mupdf-jsnpm地址查询对应的api地址。这些api我们会在下面的代码解释部分中涉及到。


项目中使用

在我们用f_cli_f构建的前端项目中,使用yarn add mupdf-js来安装mupdfJS版本。

接下来,就到了我们在前端项目中使用它的时候了。下面,有些不重要的代码和实现逻辑,我们就不写了,因为有些代码看起来又臭又长,然后也没啥可聊的。

使用tailwind构建页面结构

我们在FileToImgindex.tsx中的定义了如下的页面结构。

也就是分为两部分

  1. 头部的操作区域,用于选择文件类型/上传文件/搜索文件内容选择输出格式
  2. 非头部部分就是根据现在的处理状态来显示输出结果

因为,我们的f_cli_f是可以自行选择是否按照tailwind,所以在我们选择它后,我们就可以直接使用对应的语法构建页面结构了。该页面中,就是做了一下简单的布局配置,如果像了解更多关于Tailwind可以翻看我们之前的文章 - Tailwind CSS那些事儿

定义processorState

我们选择用一个对象来维护页面中处理文件的各种状态和信息。

const initialState = {
  fileType: FileType.PDF,
  processing: false,
  file: null as File | null,
  mode: ConversionMode.TEXT,
  searchQuery: '',
  error: '',
  output: [] as string[],
};
  • fileType: 就是页面左侧的下来框中的信息(PDF/Word/Text)
  • processing: 文件是否正在被解析,用于展示不同的状态
  • file: 存储本地上传的文件信息
  • output: 最后的解析结果

然后通过useState将其固定到组件内部。

const [processorState, setProcessorState] = useState(initialState);

处理文件上传回调

因为是一个展示项目,我们针对文件上传用了最原始的方式,我们通过在类型为fileinput上绑定了一个针对onChange的事件回调- handleFileChange。如果想用更优雅的方式,可以看我们之前的文章 - 文件上传 = 拖拽 + 多文件 + 文件夹

 <input type="file" onChange={handleFileChange} disabled={processing} />

主要的代码如下:

我们来简单解释一下

  1. e.target.files获取到file信息
  2. processorState获取fileType用于区分是哪种文件格式
  3. processFile基于file信息和mode来解析文件,

    • 当文件被成功解析后,调用setProcessorState用于更新其中的output

这里我们根据fileType处理了两种文件类型(pdf/word),其实它们主要流程都差不多,都是先调用setProcessorState先处理processing等信息,然后调用对应的解析函数(processFile/processWordFile),等文件解析成功后,再次调用setProcessorState来更新output内容。

渲染逻辑

这块的主要逻辑就是依据processorState中的output来展示对应的解析结果。

这个其实也没啥可唠的。就是一个根据文件类型展示的逻辑。接下来,让我们聊聊比较好玩的东西哇 。

解析文件逻辑

handleFileChange中我们不是调用了一个processFile吗,这其实才刚刚触及到本节的核心点。

processFile我们是在pdf.ts中导出的。

从上图中,我们可以得到几点有用的消息

  1. 我们从pdf-worker中导入了PDFWorker实例(vite中导入web worker有很多方式)
  2. processFile返回了一个Promise
  3. processFile被触发时,就通过postMessagePDFWorker发送了收集到的file信息
  4. worker处理完数据后,我们通过res(val.data)返回给processFile调用处

pdf-worker.ts

pdf-worker.ts中就涉及到mupdf-js的引入和实例化操作了。并且,我们还通过self.addEventListener('message', handleMessage);来监听主线中传人的数据信息,并且基于这些信息执行不同的操作。

  1. 首先,在handleMessage中通过判断e.data.type来决定是解析文件呢,还是执行查询
  2. 如果是解析的话,那就调用convertFile

    • 调用loadPdf,用于将pdf文件资源转换成mupdf能够识别的格式
    • 调用convertPdfDocument
  3. convertPdfDocument中通过pdf.countPages(doc)来获取文档的总页数,并且每页都执行convertPdfPage
  4. convertPdfPage中基于mode来处理相关的解析逻辑

    • 在这里我们就看到了,之前介绍过的getPageText/drawPageAsHTML/drawPageAsSVG

最后执行文件解析的就是convertPdfPage方法对mupdf各种方法的调用

效果图

这里我们有一个页数为4的PDF文档。

在我们通过mupdf处理后,选择完对应的显示模式,就会有对应的解析结果

将pdf解析为text

将pdf解析为png

将pdf解析为svg

将pdf解析为html


3. Word 解析

所用技术

  1. Rust
  2. WebAssembly
  3. wasm-bindgen/js-sys/web-sys
  4. Web worker
  5. OffscreenCanvas
  6. mammoth.js

设计思路

在前端方面,Word的解析其实和PDF是类似的,都是在inputonChange中执行processFile

而这个processFile是对应的xx.ts(pdf.ts/word.ts)中定义的。

word.ts


上面就是word.ts的主要逻辑

  1. 引入对应的web worker并将其实例化
  2. processFile被执行时,将file和一些配置信息传人到worker
  3. worker将文件解析成功后,通过Promseres返回给processFile的调用处

上面的执行逻辑其实和处理pdf差不多,但是呢,有一点还是有点区别的。因为,Word的解析和构图是我们来维护的,所以我们就需要想办法,

  • 按照规则将Word解析成文本信息(这块我们使用mammoth.js)
  • 将文本信息绘制到图片中,我们选择Canvas,也就是创建一个零时Canvas,将对应的文案信息绘制到上面后,然后将canvas转换为blob或者base64的图片格式。

而对于我们的库的使用者来讲,它们在解析文档的时候,其应用环境是不确定的,如果是主线程还好,但是如果是在Web Worker中调用我们的库,那么这就有一个弊端,我们的逻辑是需要自己维护一个canvas也就是需要document.createElement('canvas'),但是在worker中是没有document的。所以,我们就需要做一个兼容处理。

let offscreen = null;
if (typeof OffscreenCanvas !== 'undefined') {
  offscreen = new OffscreenCanvas(800, 600);
} else {
  offscreen = document.createElement('canvas');
  if (offscreen.transferControlToOffscreen) {
    offscreen = offscreen.transferControlToOffscreen();
  } else {
    return rej(new Error(' 当前浏览器环境中不支持OffscreenCanvas'));
  }
}

word-worker.ts


上面就是word-worker.ts的大部分逻辑。

其实主要的逻辑就是

  1. 利用mammoth.jsword进行解析处理
  2. 当解析处对应的文本信息后(这里我们先截取文本的前100字符),我们调用word2imgdraw_text_as_png
  3. 在第二步返回的结果是blob对象,随后我们使用createObjectURL对其处理,并返回

这里针对convertFile中参数再做一下解释

  1. file: 上传的文件信息,在这里就是word
  2. config: 这里我们简化了配置,只定义了两个字段,用于配置生成canvas的宽高,当然这里还有更多关于canvas的配置可以传人其中
  3. canvas: 这个就是我们之前说的OffscreenCanvas,该类型的canvas可以很好的适配调用环境(主线程/worker)

最后,在代码的最前面有一行

import { draw_text_as_png, CanvasConfig } from '@/wasm/word2img';

其实呢,这一步就是在引入我们的WebAssembly,我们是将其直接放置到前端项目中的wasm文件夹下了。

至于为什么是@开头,是因为我们在tsconfig.json中配置了相关的路径隐射。

然后,我们再配合vite-tsconfig-paths就可以实现此类别名的配置了。针对这些我们在前端项目里都有啥?有过写过,这里就不在过多解释了。

Rust项目初始化

这节算是最重要的部分,完全可以单拎出来重新写一篇,但是呢为了行文的完整性,我们还是将它糅合到这篇文章内。

Rust初始化

之前在Rust学习笔记有过这方面的介绍。所以我们这里就直接上手了。

Rust 中,使用 cargo new 命令可以创建一个新的项目。

  1. cargo new xx

    • 创建一个新的 二进制(binary)项目
    • 生成的项目结构适用于编写一个可以直接运行的可执行程序。
    • src 目录下会有一个 main.rs 文件,这是程序的入口点,包含一个 fn main() {} 函数。
  2. cargo new xx --lib

    • 创建一个新的 库(library)项目
    • 生成的项目结构适用于编写一个库,可以被其他项目依赖。
    • src 目录下会有一个 lib.rs 文件,这是库的入口点,通常定义公共 API。
总结来说,cargo new xx 创建的是一个二进制项目,适用于开发可执行程序;cargo new xx --lib 创建的是一个库项目,适用于开发可以被其他项目依赖和使用的库。

因为,我们要用Rust生成一个库(library)项目,可以被其他项目依赖,

cargo new file2img --lib

更新Cargo.toml

随后,我们更新我们的Cargo.toml文件

[package]
name = "file2img"
version = "0.1.0"
edition = "2021"


[lib]
crate-type = ["cdylib"]

[dependencies]
js-sys = "0.3.69"
wasm-bindgen = { version = "0.2.92" }
wasm-bindgen-futures = "0.4"
console_error_panic_hook = "0.1.7"

[dependencies.web-sys]
version = "0.3.69"
features = [
  'TextMetrics',
  'OffscreenCanvas',
  'OffscreenCanvasRenderingContext2d'
]

这里面比较重要的部分是

  1. wasm-bindgen
  2. web-sys
  3. js-sys

在使用 Rust 进行 WebAssembly 开发时,web-sysjs-sys 是两个常用的 crate,它们用于与 JavaScriptWeb API 进行交互。

js-sys
  • 用途js-sys 提供了对 JavaScript 原生对象和函数的低级别绑定。这些绑定使得 Rust 代码可以直接与基本的 JavaScript 特性和全局对象(如 MathDatePromise 等)进行交互。
  • 功能:包括但不限于与 JavaScript 基本类型、标准库对象和全局作用域函数的交互。
  • 示例

    use wasm_bindgen::prelude::*;
    use js_sys::Math;
    
    #[wasm_bindgen]
    pub fn get_random_number() -> f64 {
        Math::random()
    }
web-sys
  • 用途web-sys 提供了对 Web API 的高层次绑定。这些 API 包括 DOM 操作、HTML 元素处理、网络请求、WebSocketsCanvas 以及其他浏览器提供的功能。
  • 功能:包括与浏览器环境相关的各种接口和对象的交互,比如 DocumentElementWindowFetchWebGL 等。
  • 示例

    use wasm_bindgen::prelude::*;
    use web_sys::window;
    
    #[wasm_bindgen]
    pub fn alert_message(message: &str) {
        if let Some(win) = window() {
            win.alert_with_message(message).unwrap();
        }
    }
  • js-sys:专注于 JavaScript 的核心对象和函数,提供基础的 JavaScript 环境支持。
  • web-sys:专注于 Web API,提供浏览器环境下的各种高级功能支持。

在实际使用中,通常会根据需要同时使用这两个 crate。例如,web-sys 可能依赖 js-sys 提供的一些基础功能,而我们在开发 Web 应用时可能会同时需要操作 DOM 元素(使用 web-sys)和调用 JavaScript 原生函数(使用 js-sys)。

前端初始化

在这里呢,其实算是我的开发习惯,我们在使用Rust构建WebAssembly时,其实这个算是一种黑盒模式,无法在写完代码后,里面看到效果。虽然,我们可以写测试用例,但是无法更直观的看到效果。

所以,我们在刚才构建的Rust项目中配置一个前端开发服务器。

  1. npm init(一路回车),此时的Rust项目也是一个前端项目
  2. 构建一个index.html(方便构建/操作DOM)
  3. 新建一个index.js(前端主入口)
  4. 新建一个webpack.config.js
  5. 安装依赖(yarn)

通过上述步骤,我们就可以在index.js导入编译后的WebAssembly。然后,通过启动一个前端服务,直接看到效果,从而进行验证。

package.json
{
  "name": "file2img",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "author": "Front789",
  "license": "ISC",
  "scripts": {
    "build": "webpack",
    "serve": "webpack serve"
  },
  "devDependencies": {
    "@wasm-tool/wasm-pack-plugin": "1.5.0",
    "html-webpack-plugin": "^5.3.2",
    "text-encoding": "^0.7.0",
    "webpack": "^5.49.0",
    "webpack-cli": "^4.7.2",
    "webpack-dev-server": "^4.15.1"
  }
}
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'index.js',
    },
    plugins: [
         new HtmlWebpackPlugin({
            template: 'index.html'
        }),
        new WasmPackPlugin({
            crateDirectory: path.resolve(__dirname, ".")
        }),
        // 让这个示例在不包含`TextEncoder`或`TextDecoder`的Edge浏览器中正常工作。
        new webpack.ProvidePlugin({
          TextDecoder: ['text-encoding', 'TextDecoder'],
          TextEncoder: ['text-encoding', 'TextEncoder']
        })
    ],
    mode: 'development',
    experiments: {
        asyncWebAssembly: true
   }
};

这个我们在第一节中,如何在Webpack中使用WebAssembly中解释过。

index.js
import { draw_text_as_png,CanvasConfig } from './pkg';
const img = document.createElement('img');
img.width = 800;
img.height = 600;
document.body.appendChild(img);

 let offscreen = null;
if (typeof OffscreenCanvas !== 'undefined') {
    offscreen = new OffscreenCanvas(800, 600);
} else {
    offscreen = document.createElement('canvas');
    if (offscreen.transferControlToOffscreen) {
    offscreen = offscreen.transferControlToOffscreen();
    } else {
        new Error('OffscreenCanvas is not supported in your browser.');
    }
}
const config = new CanvasConfig(800,600)
const base64Image = await draw_text_as_png(
    '我是柒八九。一个专注于前端开发技术/Rust及AI应用知识分享的Coder',
    config,
    offscreen
);
console.log(base64Image, `sss`);
const url = URL.createObjectURL(base64Image);
img.src = url;

当然,在index.js可以引入自己的测试代码。这个是没有硬性要求的。

lib.rs

[wasm_bindgen]

本来这块信息不想讲的,但是还是没忍住,所以我们就简单解释一下哇。

Rust 中,#[wasm_bindgen] 是一个属性宏(attribute macro),用于与 wasm-bindgen 工具一起工作。wasm-bindgen 是一个用于在 RustJavaScript 之间实现高效绑定的库,主要用于将 Rust 编译成 WebAssembly,并且使其能够与 JavaScript 进行交互。

主要作用
  1. 导出 Rust 函数到 JavaScript:使得在 Rust 中定义的函数可以在 JavaScript 中调用。
  2. 导入 JavaScript 函数到 Rust:使得在 JavaScript 中定义的函数可以在 Rust 中调用。
  3. 生成类型定义:帮助生成适当的类型定义,以便在 JavaScript 中正确使用 Rust 导出的函数和类型。
1. 导出 Rust 函数到 JavaScript

使用 #[wasm_bindgen] 可以将 Rust 函数导出,使其可以在 JavaScript 中调用。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

这个例子中,greet 函数可以在编译为 WebAssembly 后,在 JavaScript 中调用:

import { greet } from './your_wasm_module';

console.log(greet("Front789")); // 输出: "Hello, Front789!"
2. 导入 JavaScript 函数到 Rust

通过 #[wasm_bindgen],可以声明外部的 JavaScript 函数,使得它们可以在 Rust 中调用。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn call_alert(message: &str) {
    alert(message);
}

这个例子中,alert 函数在 Rust 中被声明并可以调用,这实际上调用的是 JavaScript 中的 alert 函数。

3. 与 JavaScript 对象交互

#[wasm_bindgen] 也可以用于与 JavaScript 对象和类进行交互。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    type Document;

    #[wasm_bindgen(method, getter, js_name = "title")]
    fn title(this: &Document) -> String;

    #[wasm_bindgen(method, setter, js_name = "title")]
    fn set_title(this: &Document, title: &str);
}

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = window)]
    static document: Document;
}

#[wasm_bindgen]
pub fn change_title(new_title: &str) {
    document.set_title(new_title);
}

这个例子展示了如何使用 #[wasm_bindgen]JavaScriptdocument 对象进行交互,从而改变网页的标题。


draw_text_as_png

const base64Image = await draw_text_as_png(
    '我是柒八九。 一个专注于前端开发技术/Rust及AI应用知识分享的Coder',
    config,
    offscreenCanvas
);

我们在JS环境中,使用上面方式来调用draw_text_as_png。其接收三个参数

  1. text:需要被画到canvas上的文案信息
  2. config:用于配置canvas的属性
  3. offscreenCanvas:用于承接textcanvas实例

那么,我们可以看看用Rust是如何实现该方法签名的。

在这个函数体内部,有几点需要注意

  1. config是我们定义的结构体
  2. console_error_panic_hook crate 实现错误信息的更好输出
  3. wrap_text用于将文本信息画到canvas
  4. 最后返回的是canvasbase64格式
CanvasConfig

draw_text_as_png中接收的第二个参数,是我们在Rust中定义的结构体。用于配置canvas的各个属性。

#[wasm_bindgen]
pub struct CanvasConfig {
    width: u32,
    height: u32,
}

#[wasm_bindgen]
impl CanvasConfig {
    #[wasm_bindgen(constructor)]
    pub fn new(width: u32, height: u32) -> CanvasConfig {
        CanvasConfig { width, height }
    }
}
console_error_panic_hook

当我们把用Rust编写的WebAssembly在前端环境调用时,如果有些边界情况没考虑到。比方之前我们说过的,当我们在Rust代码中需要用到Window等比较特殊的属性时,在一些特殊环境下(Web Worker)是不存在的。当代码在这些特殊环境下执行时,那么Rust就会发生panic

如果在Rust中没做好错误捕获和提示,那么在浏览器控制台会发生错误,但是提供的错误信息很少,我们不好定位。例如会发生unreachable错误。

为了解决这个小瑕疵,我们可以借助console_error_panic_hook,在函数初始化时,就启用panic hook

panic::set_hook(Box::new(console_error_panic_hook::hook));

wrap_text

大家从注释上可以看出来有3部分内容

  1. 文本基于空格进行切割,生成单词数组
  2. 遍历单词数组,将单词逐个添加到当前行
  3. 处理剩余的文本,绘制最后一行

其实内容也不是很复杂,但是还需要额外说明一下,在这里我们是按照空格对文本进行分割,其实我们可以按字符分割,这样能准确一点。

其次就是我们见到的measure_text就是我们在CanvasRenderingContext2D.measureText,而fill_text对标CanvasRenderingContext2D.fillText

执行打包处理

Rust 编译成 WebAssembly

  1. 使用wasm-pack:

    • wasm-pack 是一个官方推荐的工具,用于将Rust代码编译成WebAssembly并生成适合JavaScript项目使用的包。它可以自动处理生成WebAssembly模块所需的所有步骤,包括编译、优化、并生成绑定代码。

      cargo install wasm-pack
      wasm-pack build
  2. 使用cargo-web:

    • cargo-web 是一个用于编译Rust代码到WebAssembly并生成相关JavaScript绑定的工具。它提供了一些额外的功能,例如处理stdweb库。
    cargo install cargo-web
    cargo web build --target wasm32-unknown-unknown
  3. 使用wasm-bindgen:

    • wasm-bindgen 是一个用于在RustJavaScript之间生成互操作性代码的工具。你可以直接使用它来编译Rust代码成WebAssembly,并生成相应的JavaScript绑定代码。

      cargo install wasm-bindgen-cli
      cargo build --target wasm32-unknown-unknown
      wasm-bindgen target/wasm32-unknown-unknown/debug/your_crate.wasm --out-dir ./out
  4. 使用cargo直接编译:

    • Rust自带的Cargo工具可以直接编译Rust代码到WebAssembly目标。我们需要指定目标三元组为wasm32-unknown-unknown
    cargo build --target wasm32-unknown-unknown

Webpack + wasm-pack-plugin

这是我们这篇文章开头讲的内容,我们使用@wasm-tool/wasm-pack-plugin配合Webpack从而实现了Rust编译成WebAssembly并且它还支持在Rust代码发生变更后,自动编译。

最后,我们在pkg就可以的到对应Rust编译成WebAssembly的相关代码。针对其中每个文件的含义,可以参考我们之前的文章Rust 编译为 WebAssembly 在前端项目中使用

效果图


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

本文由mdnice多平台发布


前端柒八九
18 声望3 粉丝