WebAssembly (WASM) 程式碼可以顯著提高速度,基於它編寫和釋出 npm 軟體包現在已經成為一個非常有趣的目標。本文將向您展示如何開發和釋出 WASM 軟體包,以便您能在自己的工作中應用這項技術。
WebAssembly(通常縮寫為 Wasm)是一種突破性的二進位制指令格式,它改變了網路開發的格局。它允許開發人員在瀏覽器中以接近原生的速度執行 JavaScript 之外的其他語言編寫的程式碼。WebAssembly 最令人興奮的應用之一是在 npm 包領域。
在深入探討文章的其他部分之前,讓我們先快速討論一下使用 WebAssembly 的目的。我們的主要目標是通過構建一個搜尋索引來發現使用 WebAssembly 實現 npm 包的優勢,該索引可以讓我們高效地搜尋字串集合並返回相關匹配結果。這將是 Wade.js npm 軟體包的簡化版本,但使用的是 Rust 和 Wasm。最後,你將知道如何開發一個高效能的搜尋包。
使用 WebAssembly 編寫 npm 軟體包的好處包括:
- 效能:WebAssembly 模組以接近原生的速度執行,比同等 JavaScript 實現快得多。
- 靈活性:開發人員可以在網路環境中利用 Rust、C 和 C++ 等語言的功能和特性。
- 安全性:WebAssembly 提供了一個沙箱式的執行環境,即使程式碼中存在錯誤,也能確保程式碼不會對主機系統造成危害。
要繼續學習,請確保您的開發環境包括:
- Node.js:構建伺服器端應用程式必不可少的 JavaScript 執行環境。
- NPM account:確保您有一個啟用的 NPM 登錄檔賬戶,因為它是釋出 npm 包所必需的。
在接下來的章節中,您將瞭解如何設定 Rust 開發環境、在 Rust 中實現搜尋功能以及編譯和釋出 npm 軟體包。
設定新的 Rust 專案
您可以按照此處的說明在本地計算機上安裝 Rust。
安裝好 Rust 後,就該獲取 wasm-pack 二進位制檔案了。這個小工具可以幫助你將 Rust 程式碼編譯成 WebAssembly,並打包以實現無縫開發。
設定成功後,執行下面的 cargo 命令來設定一個新的 Rust 專案:
cargo new --lib refactored-couscous
通過新增 --lib
,你可以指示 cargo
生成一個 Rust 庫模板。你應該將資料夾名稱 “refactored-couscous” 改為你喜歡的名稱。cargo
生成的資料夾結構應與下面的目錄相同:
refactored-couscous/ ├── Cargo.lock ├── Cargo.toml ├── LICENSE_APACHE ├── src/ │ └── lib.rs └── target/
接下來,讓我們在程式碼執行過程中新增必要的依賴項。開啟 Cargo.toml
檔案,更新其依賴項部分:
# file: ./Cargo.toml [dependencies] wasm-bindgen = "0.2" js-sys = "0.3"
wasm-bindgen
crate 對於促進 Rust 和 JavaScript 之間的高階互動至關重要。它提供了 #[wasm_bindgen]
屬性,你可以用它來註釋 Rust 結構和函式,使 JavaScript 可以訪問它們。
js-sys
crate 為 Rust 中的所有 JavaScript 全域性物件和函式提供繫結。它是 wasm-bindgen
生態系統的一部分,旨在與 wasm-bindgen
配合使用。
用 Rust 重寫簡化的 Wade.js npm 軟體包
在完成設定後,讓我們開始實現搜尋包。
在深入研究程式碼之前,瞭解我們的主要目標至關重要。我們的目標是建立一個搜尋索引,讓我們能夠高效地搜尋字串集合,並返回相關的匹配結果。
// file: src/lib.rs use std::collections::HashMap; use wasm_bindgen::prelude::*; use js_sys::Array; #[wasm_bindgen] #[derive(Debug, Clone)] pub struct Token { word: String, position: usize, frequency: usize, }
我們首先匯入必要的庫。然後宣告包含三個欄位的 Token
結構:word
, position
, 和 frequency
。struct
將表示單個單詞、單詞在資料中的位置以及單詞在給定字串中的頻率。
接下來,讓我們定義搜尋索引結構。搜尋包的核心是 Index
結構,它將儲存我們要搜尋的所有字串,並允許我們快速查詢單詞的出現。
// file: ./src/lib.rs #[wasm_bindgen] #[derive(Debug)] pub struct Index { data: Vec<String>, tokens: HashMap<String, Vec<Token>>, }
在上述程式碼段中,data
欄位是一個儲存所有字串的向量。tokens
欄位是一個 hashmap,其中每個鍵是一個單詞,其值是一個 Token
結構向量。通過這種結構,我們可以快速查詢資料中出現的所有單詞。
接下來,讓我們為每個字串實現標記化。標記化就是將字串分解成單個詞或 “tokens”。有了單個標記,我們就可以單獨分析和處理每個單詞。這種粒度使我們能夠專注於特定的單詞,從而更容易搜尋、分析或處理文字。
// file: ./src/lib.rs impl Token { fn tokenize(s: &str) -> Vec<String> { s.to_lowercase() .split_whitespace() .map(|word| word.to_string()) .collect() } }
tokenize
函式將字串作為輸入。它會將字串轉換為小寫,以確保搜尋不區分大小寫。然後將字串分割成單詞,再將每個單詞轉換成字串。最後,單詞被收集到一個向量中並返回。
接下來,讓我們初始化並填充搜尋 Index
。
// file: ./src/lib.rs #[wasm_bindgen] impl Index { #[wasm_bindgen(constructor)] pub fn new() -> Self { Index { data: Vec::new(), tokens: HashMap::new(), } } pub fn add(&mut self, s: &str) { let position = self.data.len(); self.data.push(s.to_string()); let tokens = Token::tokenize(s); for token in tokens { let frequency = s.matches(&token).count(); self.tokens.entry(token.clone()).or_insert_with(Vec::new).push(Token { word: token, position, frequency, }); } } ... }
在上述程式碼段中,new
函式初始化了一個空 Index
。然後,add
函式允許我們向索引中新增新字串。為此,它會對字串進行標記化處理,計算每個標記的頻率,並相應地更新 tokens
雜湊表。
#[wasm_bindgen(constructor)]
屬性表示當從 JavaScript 訪問 Rust 結構時,相關函式應被視為 Rust 結構的建構函式。
接下來,讓我們實現搜尋功能。為了在索引中搜尋匹配項,我們將如下定義搜尋函式:
// file: ./src/lib.rs #[wasm_bindgen] impl Index { ... pub fn search(&self, query: &str) -> Array { let tokens = Token::tokenize(query); let mut results = Vec::new(); for token in tokens { if let Some(matches) = self.tokens.get(&token) { for match_ in matches { results.push(self.data[match_.position].clone()); } } } results.sort(); results.dedup(); // Convert Vec<String> to js_sys::Array results.into_iter().map(JsValue::from).collect() } }
search
函式首先對查詢進行標記化。對於查詢中的每個標記,它都會檢查 tokens
雜湊表中是否有匹配項。如果發現匹配,就會將 data
向量中的相應字串新增到搜尋結果中。然後對結果進行排序,並刪除重複的結果。最後,使用 js_sys::Array
將結果轉換為 JavaScript 陣列並返回。
有了這個實現,我們就有了一個使用 Rust 構建的強大搜尋索引。在下一節中,我們將深入探討如何將 Rust 程式碼編譯到 WebAssembly 中,以便將其無縫整合到 JavaScript 環境中。
將 Rust 程式碼反編譯為 WebAssembly
本節將深入探討將 Rust 程式碼轉編到 WebAssembly 的不同方法,這取決於特定的 JavaScript 環境。在本討論中,我們將集中討論兩個主要的編譯目標:網路(與基於瀏覽器的應用程式有關)和捆綁程式(與伺服器端操作有關)。
要轉譯為 WebAssembly,需要在專案根目錄下執行下面的 wasm-pack
命令:
wasm-pack build --target web
執行命令後,會啟動一系列程序。首先是將 Rust 原始碼轉譯為 WebAssembly。隨後,在生成的 WebAssembly 上執行 wasm-bindgen
工具,生成一個 JavaScript 封裝器,以促進瀏覽器與 WebAssembly 模組的相容性。這個過程還協調了 pkg
目錄的形成,將 JavaScript 封裝器和原始 WebAssembly 程式碼重新定位到這個位置。根據 Cargo.toml
提供的資訊,它還會建立相應的 package.json
。如果存在 README.md
或許可證檔案,則將其複製到軟體包中。最終,這一系列操作會在 pkg
目錄中建立一個合併的軟體包。
在繼續第二個目標之前,讓我們先簡單瞭解一下生成的 pkg
目錄:
./pkg ├── LICENSE_APACHE ├── package.json ├── refactored_couscous.d.ts ├── refactored_couscous.js ├── refactored_couscous_bg.js ├── refactored_couscous_bg.wasm └── refactored_couscous_bg.wasm.d.ts
refactored_couscous.d.ts
是 TypeScript 宣告檔案,通過詳細說明包中函式和模組的型別,為 TypeScript 開發人員提供了型別安全。 refactored_couscous.js
是由 wasm-bindgen
建立的 JavaScript 封裝器,它將 WebAssembly 模組與 JavaScript 領域連線起來,實現無縫整合。作為補充,refactored_couscous_bg.js
是一個輔助檔案,用於處理一些底層操作以及與 WebAssembly 模組的互動。軟體包的核心在於 refactored_couscous_bg.wasm
,這是一個從 Rust 派生的 WebAssembly 二進位制檔案,封裝了軟體包的主要邏輯。最後,refactored_couscous_bg.wasm.d.ts
是另一個 TypeScript 宣告檔案,與之前的 .d.ts
檔案類似,但根據 WebAssembly 模組的具體情況進行了定製。
下一條 wasm-pack
命令會將 Rust 程式碼轉換為 WebAssembly 模組,該模組專門為使用基於 npm 的捆綁程式(如 Webpack 或 Rollup)而定製:
wasm-pack build --target bundler
-target bundler
標誌表示輸出結果應與這些捆綁工具相容,從而使生成的 WebAssembly 模組更容易整合到現代網路開發工作流程中。該命令生成的 pkg
目錄與 --target web
標誌生成的檔案數量相同,但檔案內容略有不同。
在 Web 應用程式中整合 WebAssembly 模組
既然我們已經知道了如何針對不同的 JavaScript 環境,那麼讓我們從 --target web
轉置開始,在基於瀏覽器的應用程式中使用生成的模組。
在根目錄下建立一個 index.html
檔案,並用以下內容更新它:
<!-- file: ./index.html --> <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8" /> <title>Wade Search in WebAssembly</title> <style> /* for the styles, see the https://github.com/Ikeh-Akinyemi/refactored-couscous/blob/main/index.html */ </style> </head> <body> <input type="text" id="searchInput" placeholder="Search..." /> <button onclick="performSearch()">Search</button> <ul id="results"></ul> <script type="module"> import init, { Index } from "./pkg/refactored_couscous.js"; let index; async function setup() { await init(); index = new Index(); // Sample data for demonstration purposes index.add("Hello world"); index.add("Start your Rust journey here") index.add("Found my empress."); index.add("Talkin about systems") index.add("Wade in Rust"); } function performSearch() { const query = document.getElementById("searchInput").value; const results = index.search(query); displayResults(results); } window.performSearch = performSearch; function displayResults(results) { const resultsElement = document.getElementById("results"); resultsElement.innerHTML = ""; results.forEach((result) => { const li = document.createElement("li"); li.textContent = result; resultsElement.appendChild(li); }); } setup(); </script> </body> </html>
在上述程式碼段中, <script>
元素中的 JavaScript 程式碼從生成的 JavaScript 封裝器(refactored_couscous.js)中匯入了 init
函式和 Index
類。init 函式非常重要,因為它初始化了 WebAssembly 模組,確保其可隨時使用。
頁面載入時會呼叫 setup
函式。它首先使用 await init()
確保 WebAssembly 模組完全初始化。然後,建立 WebAssembly 模組中 Index
類的例項,用於儲存和搜尋資料。
當使用者點選 “Search” 按鈕時,將觸發 performSearch
函式。它從文字欄位中檢索使用者輸入的內容,使用 Index 類的 search
方法查詢匹配內容,然後使用 displayResults 函式顯示結果。
displayResults
函式獲取搜尋結果,為每個結果建立一個列表項,並將其新增到網頁上的 results
無序列表中。
在瀏覽器中載入 index.html
檔案並進行單詞搜尋,如下圖所示:
如何將 WebAssembly 模組作為 NPM 包
在本節中,我們將從 --target bundler
轉譯開始,用 Webpack 使用生成的模組。然後,我們將把該包釋出到 NPM 登錄檔,並在 Node.js 中安裝和使用它。
使用 --target bundler
標誌執行 wasm-pack
transpilation 命令。然後將位置更改為 pkg
目錄,並執行下面的 npm 命令:
npm link
在 pkg
目錄中執行 npm link
命令,就能像全域性安裝 NPM 軟體包一樣訪問該軟體包。這樣,你就可以使用軟體包,而無需將其實際釋出到 NPM 登錄檔中。這對於測試和開發目的尤其有用。
接下來,在專案根目錄下建立一個新資料夾,用於存放我們將要介紹的示例:
mkdir -p examples/webpack-impl
在 ./examples/webpack-impl
資料夾中建立 package.json
檔案,並新增以下配置:
// file: ./examples/webpack-impl/package.json { "scripts": { "serve": "webpack-dev-server" }, "dependencies": { "refactored-couscous": "^0.1.0" }, "devDependencies": { "webpack": "^4.25.1", "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.1.10" } }
接下來,執行以下 npm 命令連結軟體包並安裝其他軟體包:
npm link refactored-couscous && npm install
安裝完成後,建立一個 index.html
檔案並新增以下 HTML 程式碼:
<!-- file: ./examples/webpack-impl/index.html --> <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8" /> <title>refactored-couscous example</title> <style> /* for the styles, see https://github.com/Ikeh-Akinyemi/refactored-couscous/blob/main/examples/webpack-impl/index.html*/ </style> </head> <body> <div id="loading" style="display: none"> <div class="spinner"></div> Loading... </div> <input type="file" id="fileInput" /> <input type="text" id="urlInput" placeholder="Enter URL" /> <button id="buildIndexButton">Build Index</button> <input type="text" id="searchInput" placeholder="Search..." /> <button id="searchButton">Search</button> <ul id="results"></ul> <script src="./index.js"></script> </body> </html>
接下來,讓我們建立一個 index.js
檔案,並新增以下 JavaScript 程式碼:
// file: ./examples/webpack-impl/index.js import("refactored-couscous").then((js) => { function splitIntoSentences(text) { return text.match(/[^\.!\?]+[\.!\?]+/g) || []; } const index = new js.Index(); ... });
本節首先動態匯入 refactored-couscous
模組。模組匯入後,回撥函式將以匯入的模組為引數( js
)執行。
接下來,我們定義了一個名為 split into sentences
的實用函式,用於根據標點符號將給定文字分割成單個句子。然後,從匯入模組中建立 Index
類的例項。
現在,讓我們新增一個事件監聽器,檢查使用者是否上傳了要搜尋的檔案或 URL,然後使用資源的內容建立 Index
。
// file: ./examples/webpack-impl/index.js import("refactored-couscous").then((js) => { ... document .getElementById("buildIndexButton") .addEventListener("click", async () => { const fileInput = document.getElementById("fileInput"); const urlInput = document.getElementById("urlInput"); const loadingDiv = document.getElementById("loading"); loadingDiv.style.display = "block"; if (fileInput.files.length) { const file = fileInput.files[0]; const content = await file.text(); const sentences = splitIntoSentences(content); sentences.forEach((sentence) => { if (sentence.trim()) { console.log(sentence); index.add(sentence.trim()); } }); } else if (urlInput.value) { try { const response = await fetch(urlInput.value); const content = await response.text(); const sentences = splitIntoSentences(content); sentences.forEach((sentence) => { if (sentence.trim()) { index.add(sentence.trim()); } }); } catch (error) { console.error("Error fetching URL:", error); } } loadingDiv.style.display = "none"; }); ... });
在上述程式碼段中,我們通過 fileInput
檔案輸入元素檢查是否提供了檔案。如果是,它將讀取檔案內容,使用實用功能將其分割成句子,並將每個句子新增到索引中。
如果提供的是 URL 而不是檔案,則會從 URL 獲取內容,將其拆分成句子並新增到索引中。
接下來,讓我們實現搜尋 Index
並顯示結果:
// file: ./examples/webpack-impl/index.js import("refactored-couscous").then((js) => { ... document.getElementById("searchButton").addEventListener("click", () => { const loadingDiv = document.getElementById("loading"); loadingDiv.style.display = "block"; const query = document.getElementById("searchInput").value; const results = index.search(query); console.log(results); loadingDiv.style.display = "none"; displayResults(results); }); function displayResults(results) { const resultsList = document.getElementById("results"); resultsList.innerHTML = ""; // Clear previous results results.forEach((result) => { const listItem = document.createElement("li"); listItem.textContent = result; resultsList.appendChild(listItem); }); } });
之後,讓我們在 examples/webpack-impl
資料夾的根目錄下建立一個 webpack.config.js
檔案來配置 Webpack,並在其中填充以下內容:
// file: ./examples/webpack-impl/webpack.config.js const path = require("path"); module.exports = { entry: "./index.js", output: { path: path.resolve(__dirname, "dist"), filename: "index.js", }, mode: "development", };
設定完成後,就可以執行專案了。確保您使用的是 Node.js v16,特別是考慮到 Node.js 環境的特殊性。
npm run serve
執行後,在瀏覽器中訪問 http://localhost:8080/
。您可以選擇直接上傳檔案或輸入指向網路內容的 URL。
顯示的影象證實了我們軟體包的功能。下一步是將此軟體包釋出到 NPM 登錄檔,讓更多人可以訪問它。為此,請切換到 pkg
目錄,並在 package.json
配置中加入 type: "module"
。這將確保軟體包與 CommonJS 和 ESModule 模組系統相容。
npm publish
這將把軟體包釋出到你在 npm.com 上的賬戶,如下圖所示:
釋出後,我們可以使用 npm install refactored-couscous
命令安裝軟體包,並使用 ESModule 系統將其匯入 Node.js 應用程式:
// file: ./examples/cli/src/index.js import { Index } from 'refactored-couscous/refactored_couscous.js'; const index = new Index(); index.add("Hello world"); index.add("Rust is amazing"); index.add("Wade in Rust"); const results = index.search("rust"); console.log(results);
設定好程式碼後,就可以使用 Node.js 執行指令碼了。確保啟用 WebAssembly 模組的實驗標誌:
node --experimental-wasm-modules ./src/index.js
執行指令碼後,你會看到控制檯中列印的搜尋結果,顯示哪些條目包含 “rust” 一詞:
[ 'Rust is amazing', 'Wade in Rust' ]
這表明我們的軟體包在對所提供的資料進行搜尋時非常有效。
小結
在本文中,我們使用 Rust 和 WebAssembly 成功重寫了一個簡化版的 Wade.js npm 軟體包。我們深入研究了 Rust 型別系統的複雜性、WebAssembly 的效能優勢,以及 Rust、WebAssembly 和 JavaScript 之間的無縫互操作性。我們還探討了如何構建文字資料並編制索引、執行搜尋,以及如何將 WebAssembly 模組整合到瀏覽器和伺服器端 JavaScript 環境中。最後,我們邁出了關鍵的一步,將我們的軟體包釋出到 NPM 登錄檔,使其可以被更廣泛地使用。這個練習不僅展示了 Rust 和 WebAssembly 的強大功能和靈活性,還為將來建立更復雜、更高效、更安全的網路應用程式奠定了基礎。
GitHub 庫的連結在此提供。
評論留言