iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0

前言:昨天的檔,今天讓瀏覽器叫它

昨天前言有一句「所以瀏覽器時代這幾天就做一件事:做一個能放進瀏覽器跑的東西,歷史跟安全之後再沾著講」,兩個地方要補清楚。「瀏覽器時代」並不是 WebAssembly 自己劃過的年代,它到今天都還在瀏覽器裡長得很旺;更精確的說法是,在它被拿去雲原生之前,最常見的落腳處就是瀏覽器,我只是借這個先後來排這三十天。「之後再沾著講」則是因為這還是技術文章,花兩三天彙整歷史太累人了,脈絡我們在探索的路上一邊實作一邊講。

昨天你手上多了一顆 392 B 的 add.wasm,export 表上有一個 add,到現在還沒有被任何人叫過。今天讓瀏覽器來叫它,而且一開始就在 SvelteKit 專案裡做,不從一個 index.html 加一支 main.js 的素頁面起手,現在寫前端沒人那樣做了。選 Svelte 是私心:對我這種不是前端起家的工程師來說它很直觀,寫起來、讀起來都蠻爽的。跟 wasm 有關的程式碼會全部住在一個純 TypeScript 檔裡,.svelte 只負責畫面,用 React、Vue 的人把那個檔搬走就能用。之後幾天都在這個專案裡繼續加料。

第一步:立專案,把昨天的 crate 搬進來

npx sv create day03 --template minimal --types ts --no-add-ons --install npm
cd day03

裝出來的是 Svelte5、SvelteKit、Vite。把昨天的 crate 整個搬進專案當 crate/Cargo.tomlsrc/lib.rs 一個字不改),package.jsonscripts 多一條:

"wasm": "cargo build --release --target wasm32-unknown-unknown --manifest-path crate/Cargo.toml && cp crate/target/wasm32-unknown-unknown/release/add.wasm src/lib/add.wasm"

npm run wasm 之後,src/lib/add.wasm 就是昨天那顆 392 B。以後改了 Rust 就跑這一條。

第二步:三行叫它

src/lib/wasm.ts

import addUrl from './add.wasm?url';

// 1. 把 add.wasm 當第四種資源抓下來,一邊下載一邊編譯,建出一個實例。
//    第二個參數是「你要給它的東西」,今天什麼都不用給,所以是空的。
const { instance } = await WebAssembly.instantiateStreaming(fetch(addUrl), {});

// 2. export 表上有 add,像叫普通函式一樣叫它;型別自己標,wasm 那邊不會告訴 TypeScript。
export const add = instance.exports.add as (x: number, y: number) => number;

src/routes/+page.svelte

<script lang="ts">
	import { onMount } from 'svelte';

	let answer = $state('…');

	// wasm 只存在瀏覽器裡;SvelteKit 會在伺服器端先跑一次這個元件,所以等掛到畫面上再載。
	onMount(async () => {
		const { add } = await import('$lib/wasm');
		answer = String(add(7, 35));
	});
</script>

<p>7 + 35 = {answer}</p>

npm run dev,開 http://localhost:5173

7 + 35 = 42

三行程式碼,三件事。第一行是「第四種資源」:.wasm 跟 HTML、CSS、JS 一樣用 fetch 抓回來(?url 只是跟 Vite 要它的網址,沒有別的),instantiateStreaming 一邊下載一邊編譯、建出實例;那個空的 {} 對應昨天看到的「沒有 import」,它什麼都不跟宿主要,你要給它的東西列表就是空的。第二行是「像叫普通函式」:instance.exports.add,名字就是昨天 #[unsafe(no_mangle)] 保住的那個。第三行是「畫面是 JS 畫的」:它算完把數字交回來,寫進畫面的是 Svelte,它自己連 document 這個字都沒有。

Vite 其實讓你直接 import { add } from './add.wasm',背後替你做的就是上面那三行。這個系列不走那條糖,原因是第二個參數:明天要往 {} 裡填東西,你得看得到它。

開 DevTools 的 Network 分頁重新整理,add.wasm 是一個 392 B 的請求。昨天說行李的帳今天算:同一支 add,debug 版編出來 1,529,567 B,幾乎全是除錯資訊;release 392 B,其中兩塊製造商標籤加一段 name(print 裡 $add 這些名字的來源)就佔 276 B;wasm-tools strip -a 剝掉之後 116 B;不用 Rust、用文字格式手寫八行組出來只有 41 B。從 release 往下,放 add 本體的 code 段都是 9 B,變的全是它旁邊的行李。所以下次看到一顆幾 MB 的 .wasm,該問的是「行李是誰放的」,不是「wasm 怎麼這麼肥」。

第三步:壞掉的樣子

跑起來之後,故意弄壞一次,比跑起來學得多。把 crate/src/lib.rs#[unsafe(no_mangle)] 那行拿掉,npm run wasm,重新整理頁面。頁面停在 7 + 35 = …,Console 裡是:

Uncaught (in promise) TypeError: add is not a function

先別急著加回去,wasm-tools print src/lib/add.wasm 看一下這顆壞掉的檔:

  (export "memory" (memory 0))
  (export "__data_end" (global 1))
  (export "__heap_base" (global 2))

add 不見了。函數還在檔案裡(body 那三條指令一個字都沒少),只是沒被放上 export 表,外面的人就叫不到它。這是今天最值得記住的一件事:它跟外面之間只有 export 表這一個窗口,不在表上的東西,對宿主來說等於不存在。這顆檔案完全合法,validate 也過,它只是什麼都不給你。

順帶一提,.wasm 得用 application/wasm 這個 Content-Type 送,instantiateStreaming 才肯一邊下載一邊編譯;Vite 的 dev server 認得,你在這裡遇不到,哪天把它丟上自己的伺服器,用 curl -sI 看一眼回應標頭。

專案就這樣了。明天在同一個專案裡,把那個 {} 填上東西。

結語

一句帶走:三行程式碼就把昨天那顆 .wasm 叫起來了:JS 負責抓它、叫它、畫畫面,它只負責算;不在 export 表上的東西,對外面來說等於不存在。


上一篇
Day 02|用 Rust 寫你的第一顆 .wasm
下一篇
Day 04|import 表與線性記憶體
系列文
Tool Use Is All You Need:30 天用 WebAssembly 試圖控制 Agent 的手腳9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言