所有耀眼的成绩,都需要苦熬,熬得过,出众;熬不过,出局
大家好,我是柒八九。一个专注于前端开发技术/Rust
及AI
应用知识分享的Coder
此篇文章所涉及到的技术有
Rust
wasm-bindgen/js-sys/web-sys
Web Worker
WebAssembly
Webpack/Vite
配置WebAssembly
OffscreenCanvas
- 脚手架生成项目(
npx f_cli_f create xxx
)tailwindcss
等MuPDF.js/mammoth.js
因为,行文字数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请大家酌情观看。
前言
在前一篇文章写一个类ChatGPT应用,前后端数据交互有哪几种我们介绍了,如果要进行一个类ChatGPT
应用的开发,可能会用到的前后端数据交互的方式。同时呢,我们也介绍了最近公司所做的项目,做一款基于文档类问答的AI功能。
而谈到文档相关的应用,从操作文档角度来看,无非就是文件上传
,文件解析
和文件展示
。而我们之前在文件上传 = 拖拽 + 多文件 + 文件夹介绍过更优雅的上传方式。而文件展示
如果大家想了解的话,我们可以单独写一篇文章。
而我们今天来聊聊关于文件解析
的相关操作。
业务背景
大家肯定用过很多云盘类的应用。在我们对本地文件进行上传后,在展示的时候一般分为两种模式
- 列表模式
- 大图模式
如果大家观察过云盘针对大图模式
的文件资源的展示,就会发现每个文件的头图都是用一个<img/>
接收了一个从后端返回的固定图片资源。
而现在,我们针对大图模式
有几点改进
- 要求该图片能显示文件资料的
概要内容
(这块可以借助AI
对文本进行Summary
处理,这个我们后面会单独写一篇文章),而不是单单的把文件的首页信息(pdf/word/pptx
)转换成图片(像阿里云盘一样) - 要求前端在上传过程中,就需要显示文件的
概要信息
,而不是走接口从服务器获取,也就是这是一个纯前端的事情 - 还需要在图片的标识文件的类型,例如展示
pdf/word/ppt
等的图标
为什么做呢,有没有发现我们通过上述的改造和处理,我们直接在大图模式
下,通过文件头图信息就能大致知晓文件的内容(概要信息),其次如果展示的资源信息过多,每次从后端获取对应的图片资源也是一件极其耗费带宽的事情。
前端糅合其他语言
讲到这里,大家可能会疑惑,你上面说了那么多,那么这和Rust
有啥关系?
关系大着呢,从上面的需求点出发,我们可以看出,其实针对文档解析
的处理,都是在前端环境中操作的。同时,针对大体积的文件资源,对其解析处理是一件极其耗时的事情。有时针对特殊文件,可能前端还暂时无法处理。
既然,我们想要在前端执行这些耗时
且不易处理的任务,我们就需要请帮手,而在其他语言中有成熟的方案来处理我们遇到的这些问题。(由于种种原因,其他端的小伙伴无瑕处理这种情况)
那么,我们就可以选择一种方式,在前端环境中通过某种方式来糅合其他语言的操作来执行对应的任务。那思来想去,WebAssembly
是再合适不过的方式了。如果不了解它,可以看我们之前的文章 - 浏览器第四种语言-WebAssembly。
当然,其他语言(C/TypeScript
)都可以通过编译工具转化成WebAssembly
,此片文章中也会涉及,只不过我们是直接使用别人构建好的WebAssembly
,而现行阶段,Rust
是对WebAssembly
最友好的语言。并且,我们也会用Rust
手搓一个WebAssembly
。这也是为什么这篇文章的主标题叫Rust赋能前端
而不是WebAssembly赋能前端
(我们在本文的第三部分,Word 解析
中详细介绍了用Rust
写WebAssembly
,如果不想看mupdf
的可以直接跳到第三节)
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 服务配置&项目配置
- PDF 解析
- Word 解析
1. 服务配置&项目配置
由于,WebAssembly
是一个新兴技术,在一些常规的打包工具(vite/webpack
)中使用,我们需要额外处理。
使用WebAssembly
从来源大致可以两类
npm包/公司私包
(针对如何发私包可以参考之前的如何在gitlab上发布npm包)- 直接在项目目录中使用已经构建好的
wasm
这两种情况我们接下来都会涉及。其实他们的处理方式都是一样的。下面我们就来讲讲Webpack/Vite
是如何配置它们的。
Webpack
针对Webpack
中使用WebAssembly
,我们之前在Rust 编译为 WebAssembly 在前端项目中使用就介绍过。
其实,最关键的点就是需要wasm-pack-plugin
其次,我们还想让WebAssembly
模块能够和其他ESM
一样,通过import
进行方法的导入处理,针对Webapck5
我们还可以通过配置experiments
的asyncWebAssembly为true
来启动该项功能。
最后,为了兼容性,我们处理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
,但是对 WebAssembly
的 ES 模块集成提案
尚未支持。而恰巧,我们今天所涉及到的.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)
所用技术
- Mupdf
- WebAssembly
- 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-js的npm
地址查询对应的api地址。这些api我们会在下面的代码解释部分中涉及到。
项目中使用
在我们用f_cli_f
构建的前端项目中,使用yarn add mupdf-js
来安装mupdf
的JS
版本。
接下来,就到了我们在前端项目中使用它的时候了。下面,有些不重要的代码和实现逻辑,我们就不写了,因为有些代码看起来又臭又长,然后也没啥可聊的。
使用tailwind构建页面结构
我们在FileToImg
的index.tsx
中的定义了如下的页面结构。
也就是分为两部分
- 头部的操作区域,用于选择
文件类型
/上传文件
/搜索文件内容
和选择输出格式
- 非头部部分就是根据现在的处理状态来显示输出结果
因为,我们的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);
处理文件上传回调
因为是一个展示项目,我们针对文件上传用了最原始的方式,我们通过在类型为file
的input
上绑定了一个针对onChange
的事件回调- handleFileChange
。如果想用更优雅的方式,可以看我们之前的文章 - 文件上传 = 拖拽 + 多文件 + 文件夹
<input type="file" onChange={handleFileChange} disabled={processing} />
主要的代码如下:
我们来简单解释一下
e.target.files
获取到file
信息- 从
processorState
获取fileType
用于区分是哪种文件格式 processFile
基于file
信息和mode
来解析文件,- 当文件被成功解析后,调用
setProcessorState
用于更新其中的output
- 当文件被成功解析后,调用
这里我们根据fileType
处理了两种文件类型(pdf/word
),其实它们主要流程都差不多,都是先调用setProcessorState
先处理processing
等信息,然后调用对应的解析函数(processFile/processWordFile
),等文件解析成功后,再次调用setProcessorState
来更新output
内容。
渲染逻辑
这块的主要逻辑就是依据processorState
中的output
来展示对应的解析结果。
这个其实也没啥可唠的。就是一个根据文件类型展示的逻辑。接下来,让我们聊聊比较好玩的东西哇 。
解析文件逻辑
在handleFileChange
中我们不是调用了一个processFile
吗,这其实才刚刚触及到本节的核心点。
processFile
我们是在pdf.ts
中导出的。
从上图中,我们可以得到几点有用的消息
- 我们从
pdf-worker
中导入了PDFWorker
实例(vite中导入web worker有很多方式) processFile
返回了一个Promise
- 在
processFile
被触发时,就通过postMessage
向PDFWorker
发送了收集到的file
信息 - 当
worker
处理完数据后,我们通过res(val.data)
返回给processFile
调用处
pdf-worker.ts
在pdf-worker.ts
中就涉及到mupdf-js
的引入和实例化操作了。并且,我们还通过self.addEventListener('message', handleMessage);
来监听主线中传人的数据信息,并且基于这些信息执行不同的操作。
- 首先,在
handleMessage
中通过判断e.data.type
来决定是解析文件呢,还是执行查询 如果是解析的话,那就调用
convertFile
- 调用
loadPdf
,用于将pdf
文件资源转换成mupdf
能够识别的格式
- 调用
convertPdfDocument
- 调用
- 在
convertPdfDocument
中通过pdf.countPages(doc)
来获取文档的总页数,并且每页都执行convertPdfPage
在
convertPdfPage
中基于mode
来处理相关的解析逻辑- 在这里我们就看到了,之前介绍过的
getPageText/drawPageAsHTML/drawPageAsSVG
- 在这里我们就看到了,之前介绍过的
最后执行文件解析的就是convertPdfPage
方法对mupdf
各种方法的调用
效果图
这里我们有一个页数为4的PDF
文档。
在我们通过mupdf
处理后,选择完对应的显示模式,就会有对应的解析结果
将pdf解析为text
将pdf解析为png
将pdf解析为svg
将pdf解析为html
3. Word 解析
所用技术
Rust
WebAssembly
wasm-bindgen/js-sys/web-sys
Web worker
OffscreenCanvas
mammoth.js
设计思路
在前端方面,Word
的解析其实和PDF
是类似的,都是在input
的onChange
中执行processFile
。
而这个processFile
是对应的xx.ts
(pdf.ts/word.ts
)中定义的。
word.ts
上面就是word.ts
的主要逻辑
- 引入对应的
web worker
并将其实例化 - 在
processFile
被执行时,将file
和一些配置信息传人到worker
中 - 当
worker
将文件解析成功后,通过Promse
的res
返回给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
的大部分逻辑。
其实主要的逻辑就是
- 利用
mammoth.js
对word
进行解析处理 - 当解析处对应的文本信息后(这里我们先截取文本的前100字符),我们调用
word2img
的draw_text_as_png
。 - 在第二步返回的结果是
blob
对象,随后我们使用createObjectURL
对其处理,并返回
这里针对convertFile
中参数再做一下解释
file
: 上传的文件信息,在这里就是word
config
: 这里我们简化了配置,只定义了两个字段,用于配置生成canvas的宽高,当然这里还有更多关于canvas
的配置可以传人其中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
命令可以创建一个新的项目。
cargo new xx:
- 创建一个新的 二进制(binary)项目。
- 生成的项目结构适用于编写一个可以直接运行的可执行程序。
src
目录下会有一个main.rs
文件,这是程序的入口点,包含一个fn main() {}
函数。
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'
]
这里面比较重要的部分是
- wasm-bindgen
- web-sys
js-sys
在使用 Rust
进行 WebAssembly
开发时,web-sys
和 js-sys
是两个常用的 crate
,它们用于与 JavaScript
和 Web API
进行交互。
js-sys
- 用途:
js-sys
提供了对JavaScript
原生对象和函数的低级别绑定。这些绑定使得 Rust 代码可以直接与基本的JavaScript
特性和全局对象(如Math
、Date
、Promise
等)进行交互。 - 功能:包括但不限于与
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 元素处理、网络请求、WebSockets
、Canvas
以及其他浏览器提供的功能。 - 功能:包括与浏览器环境相关的各种接口和对象的交互,比如
Document
、Element
、Window
、Fetch
、WebGL
等。 示例:
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
项目中配置一个前端开发服务器。
npm init
(一路回车),此时的Rust
项目也是一个前端项目- 构建一个
index.html
(方便构建/操作DOM
) - 新建一个
index.js
(前端主入口) - 新建一个
webpack.config.js
- 安装依赖(
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
是一个用于在 Rust
和 JavaScript
之间实现高效绑定的库,主要用于将 Rust
编译成 WebAssembly
,并且使其能够与 JavaScript
进行交互。
主要作用
- 导出 Rust 函数到 JavaScript:使得在
Rust
中定义的函数可以在JavaScript
中调用。 - 导入 JavaScript 函数到 Rust:使得在
JavaScript
中定义的函数可以在Rust
中调用。 - 生成类型定义:帮助生成适当的类型定义,以便在
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]
与 JavaScript
的 document
对象进行交互,从而改变网页的标题。
draw_text_as_png
const base64Image = await draw_text_as_png(
'我是柒八九。 一个专注于前端开发技术/Rust及AI应用知识分享的Coder',
config,
offscreenCanvas
);
我们在JS
环境中,使用上面方式来调用draw_text_as_png
。其接收三个参数
text
:需要被画到canvas上的文案信息config
:用于配置canvas的属性offscreenCanvas
:用于承接text
的canvas
实例
那么,我们可以看看用Rust
是如何实现该方法签名的。
在这个函数体内部,有几点需要注意
config
是我们定义的结构体console_error_panic_hook
crate 实现错误信息的更好输出wrap_text
用于将文本信息画到canvas
上- 最后返回的是
canvas
的base64
格式
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部分内容
- 文本基于空格进行切割,生成单词数组
- 遍历单词数组,将单词逐个添加到当前行
- 处理剩余的文本,绘制最后一行
其实内容也不是很复杂,但是还需要额外说明一下,在这里我们是按照空格
对文本进行分割,其实我们可以按字符分割,这样能准确一点。
其次就是我们见到的measure_text
就是我们在CanvasRenderingContext2D.measureText,而fill_text
对标CanvasRenderingContext2D.fillText
执行打包处理
Rust 编译成 WebAssembly
使用
wasm-pack
:wasm-pack 是一个官方推荐的工具,用于将
Rust
代码编译成WebAssembly
并生成适合JavaScript
项目使用的包。它可以自动处理生成WebAssembly
模块所需的所有步骤,包括编译、优化、并生成绑定代码。cargo install wasm-pack wasm-pack build
使用
cargo-web
:- cargo-web 是一个用于编译
Rust
代码到WebAssembly
并生成相关JavaScript
绑定的工具。它提供了一些额外的功能,例如处理stdweb
库。
cargo install cargo-web cargo web build --target wasm32-unknown-unknown
- cargo-web 是一个用于编译
使用
wasm-bindgen
:wasm-bindgen
是一个用于在Rust
和JavaScript
之间生成互操作性代码的工具。你可以直接使用它来编译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
使用
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多平台发布
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。