如何為Npm釋出WebAssembly包

如何為Npm釋出WebAssembly包

WebAssembly (WASM) 程式碼可以顯著提高速度,基於它編寫和釋出 npm 軟體包現在已經成為一個非常有趣的目標。本文將向您展示如何開發和釋出 WASM 軟體包,以便您能在自己的工作中應用這項技術。

WebAssembly(通常縮寫為 Wasm)是一種突破性的二進位制指令格式,它改變了網路開發的格局。它允許開發人員在瀏覽器中以接近原生的速度執行 JavaScript 之外的其他語言編寫的程式碼。WebAssembly 最令人興奮的應用之一是在 npm 包領域。

在深入探討文章的其他部分之前,讓我們先快速討論一下使用 WebAssembly 的目的。我們的主要目標是通過構建一個搜尋索引來發現使用 WebAssembly 實現 npm 包的優勢,該索引可以讓我們高效地搜尋字串集合並返回相關匹配結果。這將是 Wade.js npm 軟體包的簡化版本,但使用的是 Rust 和 Wasm。最後,你將知道如何開發一個高效能的搜尋包。

使用 WebAssembly 編寫 npm 軟體包的好處包括:

  1. 效能:WebAssembly 模組以接近原生的速度執行,比同等 JavaScript 實現快得多。
  2. 靈活性:開發人員可以在網路環境中利用 Rust、C 和 C++ 等語言的功能和特性。
  3. 安全性:WebAssembly 提供了一個沙箱式的執行環境,即使程式碼中存在錯誤,也能確保程式碼不會對主機系統造成危害。

要繼續學習,請確保您的開發環境包括:

  • Node.js:構建伺服器端應用程式必不可少的 JavaScript 執行環境。
  • NPM account:確保您有一個啟用的 NPM 登錄檔賬戶,因為它是釋出 npm 包所必需的。

在接下來的章節中,您將瞭解如何設定 Rust 開發環境、在 Rust 中實現搜尋功能以及編譯和釋出 npm 軟體包。

設定新的 Rust 專案

您可以按照此處的說明在本地計算機上安裝 Rust

安裝好 Rust 後,就該獲取 wasm-pack 二進位制檔案了。這個小工具可以幫助你將 Rust 程式碼編譯成 WebAssembly,並打包以實現無縫開發。

設定成功後,執行下面的 cargo 命令來設定一個新的 Rust 專案:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
cargo new --lib refactored-couscous
cargo new --lib refactored-couscous
cargo new --lib refactored-couscous

通過新增 --lib,你可以指示 cargo 生成一個 Rust 庫模板。你應該將資料夾名稱 “refactored-couscous” 改為你喜歡的名稱。cargo 生成的資料夾結構應與下面的目錄相同:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
refactored-couscous/
├── Cargo.lock
├── Cargo.toml
├── LICENSE_APACHE
├── src/
│ └── lib.rs
└── target/
refactored-couscous/ ├── Cargo.lock ├── Cargo.toml ├── LICENSE_APACHE ├── src/ │ └── lib.rs └── target/
refactored-couscous/
├── Cargo.lock
├── Cargo.toml
├── LICENSE_APACHE
├── src/
│   └── lib.rs
└── target/

接下來,讓我們在程式碼執行過程中新增必要的依賴項。開啟 Cargo.toml 檔案,更新其依賴項部分:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# file: ./Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
# file: ./Cargo.toml [dependencies] wasm-bindgen = "0.2" js-sys = "0.3"
# 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 軟體包

在完成設定後,讓我們開始實現搜尋包。

在深入研究程式碼之前,瞭解我們的主要目標至關重要。我們的目標是建立一個搜尋索引,讓我們能夠高效地搜尋字串集合,並返回相關的匹配結果。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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,
}
// 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, }
// 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 結構:wordposition, 和 frequencystruct 將表示單個單詞、單詞在資料中的位置以及單詞在給定字串中的頻率。

接下來,讓我們定義搜尋索引結構。搜尋包的核心是 Index 結構,它將儲存我們要搜尋的所有字串,並允許我們快速查詢單詞的出現。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// file: ./src/lib.rs
#[wasm_bindgen]
#[derive(Debug)]
pub struct Index {
data: Vec<String>,
tokens: HashMap<String, Vec<Token>>,
}
// file: ./src/lib.rs #[wasm_bindgen] #[derive(Debug)] pub struct Index { data: Vec<String>, tokens: HashMap<String, Vec<Token>>, }
// file: ./src/lib.rs
#[wasm_bindgen]
#[derive(Debug)]
pub struct Index {
data: Vec<String>,
tokens: HashMap<String, Vec<Token>>,
}

在上述程式碼段中,data 欄位是一個儲存所有字串的向量。tokens 欄位是一個 hashmap,其中每個鍵是一個單詞,其值是一個 Token 結構向量。通過這種結構,我們可以快速查詢資料中出現的所有單詞。

接下來,讓我們為每個字串實現標記化。標記化就是將字串分解成單個詞或 “tokens”。有了單個標記,我們就可以單獨分析和處理每個單詞。這種粒度使我們能夠專注於特定的單詞,從而更容易搜尋、分析或處理文字。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// file: ./src/lib.rs
impl Token {
fn tokenize(s: &str) -> Vec<String> {
s.to_lowercase()
.split_whitespace()
.map(|word| word.to_string())
.collect()
}
}
// file: ./src/lib.rs impl Token { fn tokenize(s: &str) -> Vec<String> { s.to_lowercase() .split_whitespace() .map(|word| word.to_string()) .collect() } }
// 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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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,
});
}
}
...
}
// 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, }); } } ... }
// 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 結構的建構函式。

接下來,讓我們實現搜尋功能。為了在索引中搜尋匹配項,我們將如下定義搜尋函式:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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()
}
}
// 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() } }
// 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 命令:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
wasm-pack build --target web
wasm-pack build --target web
wasm-pack build --target web

執行命令後,會啟動一系列程序。首先是將 Rust 原始碼轉譯為 WebAssembly。隨後,在生成的 WebAssembly 上執行 wasm-bindgen 工具,生成一個 JavaScript 封裝器,以促進瀏覽器與 WebAssembly 模組的相容性。這個過程還協調了 pkg 目錄的形成,將 JavaScript 封裝器和原始 WebAssembly 程式碼重新定位到這個位置。根據 Cargo.toml 提供的資訊,它還會建立相應的 package.json。如果存在 README.md 或許可證檔案,則將其複製到軟體包中。最終,這一系列操作會在 pkg 目錄中建立一個合併的軟體包。

在繼續第二個目標之前,讓我們先簡單瞭解一下生成的 pkg 目錄:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
./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
./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
./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)而定製:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
wasm-pack build --target bundler
wasm-pack build --target bundler
wasm-pack build --target bundler

-target bundler 標誌表示輸出結果應與這些捆綁工具相容,從而使生成的 WebAssembly 模組更容易整合到現代網路開發工作流程中。該命令生成的 pkg 目錄與 --target web 標誌生成的檔案數量相同,但檔案內容略有不同。

在 Web 應用程式中整合 WebAssembly 模組

既然我們已經知道了如何針對不同的 JavaScript 環境,那麼讓我們從 --target web 轉置開始,在基於瀏覽器的應用程式中使用生成的模組。

在根目錄下建立一個 index.html 檔案,並用以下內容更新它:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<!-- 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>
<!-- 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>
<!-- 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 檔案並進行單詞搜尋,如下圖所示:

在瀏覽器中載入 index.html 檔案並進行單詞搜尋

如何將 WebAssembly 模組作為 NPM 包

在本節中,我們將從 --target bundler 轉譯開始,用 Webpack 使用生成的模組。然後,我們將把該包釋出到 NPM 登錄檔,並在 Node.js 中安裝和使用它。

使用 --target bundler 標誌執行 wasm-pack transpilation 命令。然後將位置更改為 pkg 目錄,並執行下面的 npm 命令:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm link
npm link
npm link

pkg 目錄中執行 npm link 命令,就能像全域性安裝 NPM 軟體包一樣訪問該軟體包。這樣,你就可以使用軟體包,而無需將其實際釋出到 NPM 登錄檔中。這對於測試和開發目的尤其有用。

接下來,在專案根目錄下建立一個新資料夾,用於存放我們將要介紹的示例:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
mkdir -p examples/webpack-impl
mkdir -p examples/webpack-impl
mkdir -p examples/webpack-impl

./examples/webpack-impl 資料夾中建立 package.json 檔案,並新增以下配置:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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"
}
}
// 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" } }
// 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 命令連結軟體包並安裝其他軟體包:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm link refactored-couscous && npm install
npm link refactored-couscous && npm install
npm link refactored-couscous && npm install

安裝完成後,建立一個 index.html 檔案並新增以下 HTML 程式碼:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<!-- 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>
<!-- 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>
<!-- 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 程式碼:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// file: ./examples/webpack-impl/index.js
import("refactored-couscous").then((js) => {
function splitIntoSentences(text) {
return text.match(/[^\.!\?]+[\.!\?]+/g) || [];
}
const index = new js.Index();
...
});
// file: ./examples/webpack-impl/index.js import("refactored-couscous").then((js) => { function splitIntoSentences(text) { return text.match(/[^\.!\?]+[\.!\?]+/g) || []; } const index = new js.Index(); ... });
// 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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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";
});
...
});
// 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"; }); ... });
// 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 並顯示結果:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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);
});
}
});
// 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); }); } });
// 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,並在其中填充以下內容:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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",
};
// 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", };
// 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 環境的特殊性。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm run serve
npm run serve
npm run serve

執行後,在瀏覽器中訪問 http://localhost:8080/ 。您可以選擇直接上傳檔案或輸入指向網路內容的 URL。

直接上傳檔案或輸入指向網路內容的 URL

顯示的影象證實了我們軟體包的功能。下一步是將此軟體包釋出到 NPM 登錄檔,讓更多人可以訪問它。為此,請切換到 pkg 目錄,並在 package.json 配置中加入  type: "module" 。這將確保軟體包與 CommonJS 和 ESModule 模組系統相容。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm publish
npm publish
npm publish

這將把軟體包釋出到你在 npm.com 上的賬戶,如下圖所示:

把軟體包釋出到你在 npm.com 上的賬戶

釋出後,我們可以使用 npm install refactored-couscous 命令安裝軟體包,並使用 ESModule 系統將其匯入 Node.js 應用程式

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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);
// 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);
// 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 模組的實驗標誌:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
node --experimental-wasm-modules ./src/index.js
node --experimental-wasm-modules ./src/index.js
node --experimental-wasm-modules ./src/index.js

執行指令碼後,你會看到控制檯中列印的搜尋結果,顯示哪些條目包含 “rust” 一詞:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[ 'Rust is amazing', 'Wade in Rust' ]
[ 'Rust is amazing', 'Wade in Rust' ]
[ 'Rust is amazing', 'Wade in Rust' ]

這表明我們的軟體包在對所提供的資料進行搜尋時非常有效。

小結

在本文中,我們使用 Rust 和 WebAssembly 成功重寫了一個簡化版的 Wade.js npm 軟體包。我們深入研究了 Rust 型別系統的複雜性、WebAssembly 的效能優勢,以及 Rust、WebAssembly 和 JavaScript 之間的無縫互操作性。我們還探討了如何構建文字資料並編制索引、執行搜尋,以及如何將 WebAssembly 模組整合到瀏覽器和伺服器端 JavaScript 環境中。最後,我們邁出了關鍵的一步,將我們的軟體包釋出到 NPM 登錄檔,使其可以被更廣泛地使用。這個練習不僅展示了 Rust 和 WebAssembly 的強大功能和靈活性,還為將來建立更復雜、更高效、更安全的網路應用程式奠定了基礎。

GitHub 庫的連結在此提供。

評論留言